diff --git a/.agents/skills/changelog-entries/SKILL.md b/.agents/skills/changelog-entries/SKILL.md new file mode 100644 index 0000000000..5ccd0fd068 --- /dev/null +++ b/.agents/skills/changelog-entries/SKILL.md @@ -0,0 +1,79 @@ +--- +name: changelog-entries +description: > + Write changelog entries for CHANGELOG.md. Use when work has been completed + and changelog entries need to be added, or when the user asks to write, add, or update changelog + entries, release notes, or document changes. Triggers on requests like "add changelog entry", + "update the changelog", "write release notes", or after completing a feature/fix that needs documenting. +--- + +# Changelog Entries + +Write entries that match the established format in the target changelog file. + +## Determine Target File + +1. If the user specifies a file, use that, otherwise use `CHANGELOG.md`. +2. Read the first ~80 lines of the target file to confirm the current format and find the insertion point. + +## CHANGELOG.md Format + +Versioned release changelog. Entries go under `## Unreleased` or a version header like `## X.Y.Z - YYYY-MM-DD`. + +Each entry is a `- ` prefixed line. No blank lines between entries within a section. + +```markdown +## Unreleased + +- Added `craft\helpers\SomeHelper::someMethod()`. +- Fixed a bug where something wasn't working properly. ([#12345](https://github.com/craftcms/cms/pull/12345)) +- Deprecated `craft\old\Thing`. `craft\new\Thing` should be used instead. +``` + +**Patch releases** use a flat list with no subheaders. + +**Major/minor releases** may group entries under `###` subheaders: +`### Content Management`, `### Accessibility`, `### Administration`, `### Development`, `### Extensibility`, `### System` + +**Warnings** go above the entry list using GitHub callout syntax: +```markdown +> [!WARNING] +> Important note about breaking changes. +``` + +## Writing Rules + +1. **Start with a past-tense verb** — capitalize it: + - `Added` — new classes, methods, features, settings + - `Fixed` — bug fixes: `Fixed a bug where...` or `Fixed an error that...` + - `Deprecated` — include replacement: `` `old\Thing`. `new\Thing` should be used instead. `` + - `Removed` — removed classes/features (with replacement if applicable) + - `Improved` — performance or UX improvements + - Other verbs as appropriate: `Updated`, `Renamed`, `Moved`, `Replaced` + +2. **Backtick all code references** — class names, methods, properties, constants, config keys, Twig variables, CLI commands. Use fully qualified class names (no leading backslash). + +3. **One entry per line** — don't wrap. Each `- ` entry is a single line regardless of length. + +4. **End entries with a period.** + +5. **Link issues/PRs** at the end when applicable, prefer PRs when there is one: + - PRs: `([#12345](https://github.com/craftcms/cms/pull/12345))` + - Issues: `([#12345](https://github.com/craftcms/cms/issues/12345))` + - Security: `(GHSA-xxxx-xxxx-xxxx)` + - External repos: `([craftcms/commerce#4006](https://github.com/craftcms/commerce/issues/4006))` + +6. **Security fixes** include severity level: `` Fixed a [high-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) RCE vulnerability. (GHSA-xxxx-xxxx-xxxx) `` + +7. **Keyboard shortcuts** use `` tags: `Return`. + +## Entry Templates + +For the full set of templates and examples, see `references/entry-templates.md`. + +## Workflow + +1. Identify what changed (from conversation context, git diff, or user description). +2. Determine the target file and section/insertion point. +3. Write entries following the format rules above. +4. Insert entries in the appropriate location in the file. diff --git a/.agents/skills/changelog-entries/references/entry-templates.md b/.agents/skills/changelog-entries/references/entry-templates.md new file mode 100644 index 0000000000..2dd6bdb761 --- /dev/null +++ b/.agents/skills/changelog-entries/references/entry-templates.md @@ -0,0 +1,225 @@ +# Entry Templates + +## Added + +**New class or service:** +``` +- Added `CraftCms\Cms\Namespace\ClassName`. +``` + +**New facade:** +``` +- Added `CraftCms\Cms\Support\Facades\FacadeName`. +``` + +**New enum:** +``` +- Added `CraftCms\Cms\Namespace\EnumName` enum. +``` + +**New event:** +``` +- Added `CraftCms\Cms\Namespace\Events\EventName`. +``` + +**New event with description:** +``` +- Added `CraftCms\Cms\Namespace\Events\EventName` event for customizing X behavior. +``` + +**New method or property:** +``` +- Added `craft\helpers\ElementHelper::cleanseQueryCriteria()`. +- Added `craft\base\ElementTrait::$applyingDraft`. ([#18057](https://github.com/craftcms/cms/pull/18057)) +``` + +**New method macro:** +``` +- Added `Request::isPreview()` macro for detecting preview requests via `x-craft-preview` or `x-craft-live-preview` parameters. +``` + +**New console command:** +``` +- Added `php craft twig:cache` - Precompile Twig views. +``` + +**New config setting:** +``` +- Added the `enableTwigSandbox` config setting. ([#18208](https://github.com/craftcms/cms/pull/18208)) +``` + +**New library:** +``` +- Added the Illuminate Support library. +``` + +**Multiple related additions (with sub-list):** +``` +- Added element-specific authorization policies: + - `CraftCms\Cms\Entry\Policies\EntryPolicy` + - `CraftCms\Cms\Asset\Policies\AssetPolicy` +``` + +## Fixed + +**Bug fix:** +``` +- Fixed a bug where something wasn't working properly. ([#12345](https://github.com/craftcms/cms/pull/12345)) +- Fixed a bug where Matrix fields in Blocks view could lose their existing values when they became editable. +``` + +**Error fix:** +``` +- Fixed an error that could occur when editing an element with a Table field. ([#18408](https://github.com/craftcms/cms/pull/18408)) +- Fixed an error that occurred when creating a new element on multi-site installs. ([#18393](https://github.com/craftcms/cms/pull/18393)) +``` + +**JavaScript fix:** +``` +- Fixed a JavaScript error that occurred if a Matrix field's label was hidden. ([#18366](https://github.com/craftcms/cms/pull/18366)) +- Fixed potential JavaScript errors that could occur if a disclosure menu's trigger was missing. ([#18358](https://github.com/craftcms/cms/pull/18358)) +``` + +**Security fix:** +``` +- Fixed a [high-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) RCE vulnerability. (GHSA-fp5j-j7j4-mcxc) +- Fixed a [moderate-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) SSTI vulnerability. (GHSA-qc86-q28f-ggww) +- Fixed [low-severity](https://github.com/craftcms/cms/security/policy#severity--remediation) XSS vulnerabilities. (GHSA-4mgv-366x-qxvx) +``` + +**Styling fix:** +``` +- Fixed a styling issue with slideouts within Live Preview. ([#18383](https://github.com/craftcms/cms/pull/18383)) +``` + +## Deprecated + +**Class replaced by new class:** +``` +- Deprecated `craft\old\ClassName`. `CraftCms\Cms\New\ClassName` should be used instead. +``` + +**Method replaced by new method:** +``` +- Deprecated `craft\old\Class::oldMethod()`. `CraftCms\Cms\New\Class::newMethod()` should be used instead. +``` + +**Property replaced:** +``` +- Deprecated `GeneralConfig::$oldProp` in favor of Laravel's config.key config value. +``` + +**Constant replaced by enum case:** +``` +- Deprecated `craft\web\View::TEMPLATE_MODE_CP`. `CraftCms\Cms\View\TemplateMode::Cp` should be used instead. +``` + +**Event constant replaced by event class:** +``` +- Deprecated `craft\web\View::EVENT_REGISTER_CP_TEMPLATE_ROOTS`. `CraftCms\Cms\View\Events\RegisterCpTemplateRoots` should be used instead. +``` + +**Twig variable replaced:** +``` +- Deprecated `craft.app.config.general` in Twig. `app.config.craft.general` should be used instead. +``` + +**Class replaced with extra context:** +``` +- Deprecated `Craft::$app->getConfig()->getGeneral()`. `CraftCms\Cms\Config\GeneralConfig` should be used instead. This can be used through dependency injection or through `app(CraftCms\Cms\Config\GeneralConfig::class)`. +``` + +**Config setting deprecated:** +``` +- The `disableGraphqlTransformDirective` config setting is now deprecated. +``` + +**Replaced by Laravel built-in (no direct Craft replacement):** +``` +- Deprecated `craft\filters\BasicHttpAuthLogin`. Use the `auth.basic` middleware instead. +``` + +**Multiple methods with "in favor of" and sub-list:** +``` +- Deprecated `craft\events\WidgetEvent` in favor of the following new events: + - `craft\services\Dashboard::EVENT_BEFORE_SAVE_WIDGET` => `CraftCms\Cms\Dashboard\Events\WidgetSaving` + - `craft\services\Dashboard::EVENT_AFTER_SAVE_WIDGET` => `CraftCms\Cms\Dashboard\Events\WidgetSaved` +``` + +**Multiple methods from same class (sub-list with arrow mapping):** +``` +- Deprecated `craft\helpers\App`. The following classes/methods should be used instead: + - `App:devMode()` --> `app()->hasDebugModeEnabled()` + - `App:parseBooleanEnv()` --> `\CraftCms\Cms\Support\Env::parseBoolean()` +``` + +## Removed + +**Class removed with replacement:** +``` +- Removed `craft\old\Class`. `CraftCms\Cms\New\Class` should be used instead. +``` + +**Class removed with multiple replacements:** +``` +- Removed `craft\controllers\DashboardController`. The following controllers now implement this functionality: + - `CraftCms\Cms\Http\Controllers\Dashboard\DashboardController` + - `CraftCms\Cms\Http\Controllers\Dashboard\WidgetsController` +``` + +**Removed because previously deprecated:** +``` +- Removed `craft\helpers\MigrationHelper` as it was deprecated since 4.0.0. +``` + +**Event removed:** +``` +- Removed `craft\events\UpdateReleaseEvent` in favor of `CraftCms\Cms\Update\Events\CriticalUpdateReleasedEvent`. +``` + +**Database column removed:** +``` +- Removed `verificationCode` and `verificationCodeIssuedDate` columns on the `users` table in favor of the `password_reset_tokens` table. +``` + +## Replaced + +``` +- Replaced `craft\controllers\StructuresController`. `CraftCms\Cms\Http\Controllers\StructuresController`. +- Replaced `craft\controllers\SystemMessagesController` with `CraftCms\Cms\Http\Controllers\Utilities\SystemMessagesController`. +``` + +## Improved + +``` +- Improved the performance of `craft\helpers\Typecast`. ([#18426](https://github.com/craftcms/cms/pull/18426)) +- Improved the accessibility of user permission lists. ([#18290](https://github.com/craftcms/cms/pull/18290)) +- Improved drag-n-drop performance. ([#18019](https://github.com/craftcms/cms/pull/18019)) +``` + +## Updated + +``` +- Updated Yii to 2.0.54. +- Updated Twig to 3.21. ([#17603](https://github.com/craftcms/cms/discussions/17603)) +- Updated Axios to 1.12.2. ([#17988](https://github.com/craftcms/cms/pull/17988)) +``` + +## Behavioral Changes (no action verb prefix) + +``` +- `CraftCms\Cms\User\Elements\User` now implements `Illuminate\Contracts\Auth\Authenticatable`. +- `craft\services\Elements::stopCollectingCacheInfo()` no longer sets the returned duration to the `cacheDuration` config setting if a duration wasn't explicitly declared. ([#16796](https://github.com/craftcms/cms/pull/16796)) +- Element indexes now show "Paste" buttons alongside bulk element action buttons. ([#18427](https://github.com/craftcms/cms/pull/18427)) +- `slug` columns referenced in element queries' `select`, `where`, or `orderBy` expressions now explicitly resolve to `elements_sites.slug`. ([#18416](https://github.com/craftcms/cms/pull/18416)) +- The `maxCachedCloudImageSize` config setting is now set to `0` by default. ([#17997](https://github.com/craftcms/cms/pull/17997)) +``` + +## Feature Descriptions (no code reference) + +``` +- Nested entries' edit screens now have a "Field settings" action menu item. +- Legacy entry index URLs now redirect `content/`. +- Bulk element actions are now available on element indexes for mobile devices. +- Revisions now keep track of which element attributes/fields were modified for the revision. +``` diff --git a/.codecov.yml b/.codecov.yml index b38c982afe..5b42cd8db2 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -27,7 +27,7 @@ ignore: - 'src/etc' - 'src/migrations' - 'src/templates' - - 'src/translations' + - 'lang' - 'src/web/assets' - 'docs' - 'templates' diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml new file mode 100644 index 0000000000..6ebacbe923 --- /dev/null +++ b/.github/actions/run-tests/action.yml @@ -0,0 +1,60 @@ +name: 'Run Tests' +description: 'Setup PHP, install dependencies, and run a test command' + +inputs: + test-command: + description: 'The test command to run' + required: true + php-version: + description: 'PHP version to install' + required: false + default: '8.5' + github-token: + description: 'GitHub token for Composer' + required: false + default: '' + packagist-username: + description: 'Packagist username' + required: false + default: '' + packagist-token: + description: 'Packagist token' + required: false + default: '' + +runs: + using: 'composite' + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo, ffi + tools: composer:v2 + ini-values: display_errors=On, memory_limit=2G + coverage: none + # Pass an empty token so setup-php doesn't write GITHUB_TOKEN into + # Composer's auth config. Composer 2.9.7's regex rejects the new + # GITHUB_TOKEN format (composer/composer#12849). + github-token: '' + env: + COMPOSER_AUTH_JSON: | + { + "http-basic": { + "repo.packagist.com": { + "username": "${{ inputs.packagist-username }}", + "password": "${{ inputs.packagist-token }}" + } + } + } + + - name: Set version + shell: bash + run: composer config version "6.x-dev" + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Run tests + shell: bash + run: ${{ inputs.test-command }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e8c794175..a93f4fc425 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,26 +1,190 @@ name: ci on: workflow_dispatch: - push: - branches: - - '5.x' pull_request: -permissions: - contents: read + concurrency: group: ci-${{ github.ref }} cancel-in-progress: true + +permissions: + contents: read + jobs: - ci: - name: ci - uses: craftcms/.github/.github/workflows/ci.yml@v3 - with: - php_version: '["8.2", "8.3"]' - craft_version: '5' - node_version: '20' - jobs: '["ecs", "phpstan", "prettier", "tests", "rector"]' - notify_slack: true - slack_subteam: - secrets: - token: ${{ secrets.GITHUB_TOKEN }} - slack_webhook_url: ${{ secrets.SLACK_COMMERCE_WEBHOOK_URL }} + check-cs: + name: 'Code Quality / ECS' + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + tools: composer:v2 + coverage: none + env: + COMPOSER_AUTH_JSON: | + { + "http-basic": { + "repo.packagist.com": { + "username": "${{ secrets.packagist_username }}", + "password": "${{ secrets.packagist_token }}" + } + } + } + + - name: Set version + run: composer config version "6.x-dev" + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Run ECS + run: composer run check-cs + + rector: + name: 'Code Quality / Rector' + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + coverage: none + env: + COMPOSER_AUTH_JSON: | + { + "http-basic": { + "repo.packagist.com": { + "username": "${{ secrets.packagist_username }}", + "password": "${{ secrets.packagist_token }}" + } + } + } + + - name: Set version + run: composer config version "6.x-dev" + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Rector Cache + uses: actions/cache@v4 + with: + path: /tmp/rector + key: ${{ runner.os }}-rector-${{ github.run_id }} + restore-keys: ${{ runner.os }}-rector- + + - run: mkdir -p /tmp/rector + + - name: Run Rector + run: vendor/bin/rector process --dry-run --ansi + + phpstan: + name: 'Code Quality / Phpstan' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + tools: composer:v2 + coverage: none + env: + COMPOSER_AUTH_JSON: | + { + "http-basic": { + "repo.packagist.com": { + "username": "${{ secrets.packagist_username }}", + "password": "${{ secrets.packagist_token }}" + } + } + } + + - name: Set version + run: composer config version "6.x-dev" + + - name: Install composer dependencies + uses: ramsey/composer-install@v3 + + - name: PHPStan Cache + uses: actions/cache@v4 + with: + path: /tmp/phpstan + key: ${{ runner.os }}-phpstan-${{ github.run_id }} + restore-keys: ${{ runner.os }}-phpstan- + + - name: Run PHPStan + run: ./vendor/bin/phpstan --error-format=github + + unit-tests: + needs: [check-cs, phpstan, rector] + name: 'Tests / Unit' + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run tests + uses: ./.github/actions/run-tests + with: + test-command: ./vendor/bin/pest --ci --compact --testsuite=Unit + github-token: ${{ secrets.GITHUB_TOKEN }} + packagist-username: ${{ secrets.packagist_username }} + packagist-token: ${{ secrets.packagist_token }} + + arch-tests: + needs: [check-cs, phpstan, rector] + name: 'Tests / Arch' + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run tests + uses: ./.github/actions/run-tests + with: + test-command: ./vendor/bin/pest --ci --testsuite=Arch + github-token: ${{ secrets.GITHUB_TOKEN }} + packagist-username: ${{ secrets.packagist_username }} + packagist-token: ${{ secrets.packagist_token }} + + feature-tests: + needs: [check-cs, phpstan, rector] + # SQLite only for now — Commerce's stat/catalog-pricing queries have only been verified + # against SQLite so far. Add a mysql/pgsql matrix (see cms-6's laravel-ci.yml for the + # pattern) once those paths have been checked locally. + name: 'Tests / Feature' + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run tests + uses: ./.github/actions/run-tests + with: + test-command: ./vendor/bin/pest --ci --compact --testsuite=Feature + github-token: ${{ secrets.GITHUB_TOKEN }} + packagist-username: ${{ secrets.packagist_username }} + packagist-token: ${{ secrets.packagist_token }} diff --git a/.gitignore b/.gitignore index 4196d67e98..5eb81f123c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,9 @@ *aspnet_client/* node_modules/ /vendor -/tests/_craft/config/project .env +.phpunit.cache *-private.md *-private.html -/claude.md /.claude +/CLAUDE.md diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md new file mode 100644 index 0000000000..e6fa11820d --- /dev/null +++ b/CHANGELOG-WIP.md @@ -0,0 +1,949 @@ +# Release Notes for Craft Commerce 6.0 (WIP) + +### Product + +- Added `CraftCms\Commerce\Product\Elements\Product`. +- Added `CraftCms\Commerce\Product\Variant\Elements\Variant`. +- Added `CraftCms\Commerce\Product\Queries\ProductQuery`. +- Added `CraftCms\Commerce\Product\Variant\Queries\VariantQuery`. +- Added `CraftCms\Commerce\Product\Models\Product`. +- Added `CraftCms\Commerce\Product\Variant\Models\Variant`. +- Added `CraftCms\Commerce\Product\ProductType\Data\ProductTypeSite`. +- Added `CraftCms\Commerce\Product\Products`. +- Added `CraftCms\Commerce\Product\Variant\Variants`. +- Added `CraftCms\Commerce\Product\ProductType\Data\ProductType`. +- Added `CraftCms\Commerce\Product\ProductType\Models\ProductType`. +- Added `CraftCms\Commerce\Product\ProductType\Models\ProductTypeSite`. +- Added `CraftCms\Commerce\Product\ProductType\ProductTypes`. +- Added `CraftCms\Commerce\Product\Events\CustomizeProductSnapshotDataEvent`. +- Added `CraftCms\Commerce\Product\Events\CustomizeProductSnapshotFieldsEvent`. +- Added `CraftCms\Commerce\Product\Variant\Events\CustomizeVariantSnapshotDataEvent`. +- Added `CraftCms\Commerce\Product\Variant\Events\CustomizeVariantSnapshotFieldsEvent`. +- Added `CraftCms\Commerce\Purchasable\Events\ModifyPurchasablesTableQueryEvent`. +- Added `CraftCms\Commerce\Product\Events\ProductEvent`. +- Added `CraftCms\Commerce\Product\ProductType\Events\ProductTypeEvent`. +- Added `CraftCms\Commerce\Product\Variant\Events\PurchaseVariantEvent`. +- Deprecated `craft\commerce\elements\Product`. `CraftCms\Commerce\Product\Elements\Product` should be used instead. +- Deprecated `craft\commerce\elements\Variant`. `CraftCms\Commerce\Product\Variant\Elements\Variant` should be used instead. +- Deprecated `craft\commerce\elements\db\ProductQuery`. `CraftCms\Commerce\Product\Queries\ProductQuery` should be used instead. +- Deprecated `craft\commerce\elements\db\VariantQuery`. `CraftCms\Commerce\Product\Variant\Queries\VariantQuery` should be used instead. +- Deprecated `craft\commerce\records\Product`. `CraftCms\Commerce\Product\Models\Product` should be used instead. +- Deprecated `craft\commerce\records\Variant`. `CraftCms\Commerce\Product\Variant\Models\Variant` should be used instead. +- Deprecated `craft\commerce\models\ProductType`. `CraftCms\Commerce\Product\ProductType\Data\ProductType` should be used instead. +- Deprecated `craft\commerce\models\ProductTypeSite`. `CraftCms\Commerce\Product\ProductType\Data\ProductTypeSite` should be used instead. +- Deprecated `craft\commerce\records\ProductType`. `CraftCms\Commerce\Product\ProductType\Models\ProductType` should be used instead. +- Deprecated `craft\commerce\records\ProductTypeSite`. `CraftCms\Commerce\Product\ProductType\Models\ProductTypeSite` should be used instead. +- Deprecated `craft\commerce\services\Products`. `CraftCms\Commerce\Product\Products` should be used instead. +- Deprecated `craft\commerce\services\Variants`. `CraftCms\Commerce\Product\Variant\Variants` should be used instead. +- Deprecated `craft\commerce\services\ProductTypes`. `CraftCms\Commerce\Product\ProductType\ProductTypes` should be used instead. +- Deprecated `craft\commerce\events\CustomizeProductSnapshotDataEvent`. `CraftCms\Commerce\Product\Events\CustomizeProductSnapshotDataEvent` should be used instead. +- Deprecated `craft\commerce\events\CustomizeProductSnapshotFieldsEvent`. `CraftCms\Commerce\Product\Events\CustomizeProductSnapshotFieldsEvent` should be used instead. +- Deprecated `craft\commerce\events\CustomizeVariantSnapshotDataEvent`. `CraftCms\Commerce\Product\Variant\Events\CustomizeVariantSnapshotDataEvent` should be used instead. +- Deprecated `craft\commerce\events\CustomizeVariantSnapshotFieldsEvent`. `CraftCms\Commerce\Product\Variant\Events\CustomizeVariantSnapshotFieldsEvent` should be used instead. +- Deprecated `craft\commerce\events\ModifyPurchasablesTableQueryEvent`. `CraftCms\Commerce\Purchasable\Events\ModifyPurchasablesTableQueryEvent` should be used instead. +- Deprecated `craft\commerce\events\ProductEvent`. `CraftCms\Commerce\Product\Events\ProductEvent` should be used instead. +- Deprecated `craft\commerce\events\ProductTypeEvent`. `CraftCms\Commerce\Product\ProductType\Events\ProductTypeEvent` should be used instead. +- Deprecated `craft\commerce\events\PurchaseVariantEvent`. `CraftCms\Commerce\Product\Variant\Events\PurchaseVariantEvent` should be used instead. +- Removed `craft\commerce\records\ProductTypeShippingCategory` as it was unused. +- Removed `craft\commerce\records\ProductTypeTaxCategory` as it was unused. +- Added `CraftCms\Commerce\Product\Conditions\ProductCondition`, `ProductTypeConditionRule`, `ProductVariantSearchConditionRule`, `ProductVariantSkuConditionRule`, `ProductVariantStockConditionRule`, `ProductVariantPriceConditionRule`, and `ProductVariantInventoryTrackedConditionRule`. +- Added `CraftCms\Commerce\Product\Variant\Conditions\VariantCondition`, `VariantProductConditionRule`, and `VariantConditionRule`. +- Added `CraftCms\Commerce\CatalogPricing\Conditions\CatalogPricingRuleProductCondition`, `CatalogPricingRuleVariantCondition`, and `CatalogPricingRuleVariantConditionRule`. +- Deprecated `craft\commerce\elements\conditions\products\ProductCondition`, `ProductTypeConditionRule`, `ProductVariantSearchConditionRule`, `ProductVariantSkuConditionRule`, `ProductVariantStockConditionRule`, `ProductVariantPriceConditionRule`, `ProductVariantInventoryTrackedConditionRule`, and `CatalogPricingRuleProductCondition`. The `CraftCms\Commerce\Product\Conditions` and `CraftCms\Commerce\CatalogPricing\Conditions` equivalents should be used instead. +- Deprecated `craft\commerce\elements\conditions\variants\VariantCondition`, `ProductConditionRule`, `VariantConditionRule`, `CatalogPricingRuleVariantCondition`, and `CatalogPricingRuleVariantConditionRule`. The `CraftCms\Commerce\Product\Variant\Conditions` and `CraftCms\Commerce\CatalogPricing\Conditions` equivalents should be used instead. +- Removed `craft\commerce\elements\conditions\products\ProductVariantHasUnlimitedStockConditionRule`, deprecated since 5.0.0 and already unregistered from `ProductCondition::selectableConditionRules()`. +- Added `CraftCms\Commerce\Product\Variant\Actions\SetDefaultVariant`. +- Added `CraftCms\Commerce\Product\FieldLayoutElements\ProductTitleField`. +- Added `CraftCms\Commerce\Product\Variant\FieldLayoutElements\VariantTitleField` and `VariantsField`. +- Added `CraftCms\Commerce\Product\Fields\Products` and `Variants`. +- Deprecated `craft\commerce\elements\actions\SetDefaultVariant`. `CraftCms\Commerce\Product\Variant\Actions\SetDefaultVariant` should be used instead. +- Deprecated `craft\commerce\fieldlayoutelements\ProductTitleField`, `VariantTitleField`, and `VariantsField`. `CraftCms\Commerce\Product\FieldLayoutElements\ProductTitleField` and the `CraftCms\Commerce\Product\Variant\FieldLayoutElements` equivalents should be used instead. +- Deprecated `craft\commerce\fields\Products` and `Variants`. The `CraftCms\Commerce\Product\Fields` equivalents should be used instead. +- Removed `craft\commerce\linktypes\Product`, superseded by `CraftCms\Commerce\Product\LinkTypes\ProductLinkType`. +- Added `CraftCms\Commerce\Product\Jobs\ResaveProductVariantsJob`, a native Laravel `ShouldQueue` job. +- Removed `craft\commerce\queue\jobs\ResaveProductVariants`. `CraftCms\Commerce\Product\Jobs\ResaveProductVariantsJob` should be used instead. + +#### Controllers + +- Removed `craft\commerce\controllers\ProductsController`. `CraftCms\Commerce\Http\Controllers\ProductsController` should be used instead. +- Removed `craft\commerce\controllers\VariantsController`. `CraftCms\Commerce\Http\Controllers\VariantsController` should be used instead. +- Removed `craft\commerce\controllers\ProductTypesController`. `CraftCms\Commerce\Http\Controllers\Settings\ProductTypesController` should be used instead. + +### Catalog Pricing + +- Added `CraftCms\Commerce\CatalogPricing\CatalogPricing`. +- Added `CraftCms\Commerce\CatalogPricing\CatalogPricingRules`. +- Added `CraftCms\Commerce\CatalogPricing\Records\CatalogPricingQueue`. +- Added `CraftCms\Commerce\CatalogPricing\Records\CatalogPricingRule`. +- Added `CraftCms\Commerce\CatalogPricing\Data\CatalogPricing`. +- Added `CraftCms\Commerce\CatalogPricing\Data\CatalogPricingRule`. +- Added `CraftCms\Commerce\CatalogPricing\Conditions\CatalogPricingCondition`. +- Added `CraftCms\Commerce\CatalogPricing\Conditions\CatalogPricingPurchasableConditionRule`. +- Added `CraftCms\Commerce\CatalogPricing\Conditions\CatalogPricingCustomerConditionRule`. +- Added `CraftCms\Commerce\CatalogPricing\Contracts\CatalogPricingConditionRuleInterface`. +- Deprecated `craft\commerce\services\CatalogPricing`. `CraftCms\Commerce\CatalogPricing\CatalogPricing` should be used instead. +- Deprecated `craft\commerce\services\CatalogPricingRules`. `CraftCms\Commerce\CatalogPricing\CatalogPricingRules` should be used instead. +- Deprecated `craft\commerce\models\CatalogPricing`. `CraftCms\Commerce\CatalogPricing\Data\CatalogPricing` should be used instead. +- Deprecated `craft\commerce\models\CatalogPricingRule`. `CraftCms\Commerce\CatalogPricing\Data\CatalogPricingRule` should be used instead. +- Deprecated `craft\commerce\records\CatalogPricingRule`. `CraftCms\Commerce\CatalogPricing\Records\CatalogPricingRule` should be used instead. +- Deprecated `craft\commerce\elements\conditions\purchasables\CatalogPricingCondition`. `CraftCms\Commerce\CatalogPricing\Conditions\CatalogPricingCondition` should be used instead. +- Deprecated `craft\commerce\elements\conditions\purchasables\CatalogPricingPurchasableConditionRule`. `CraftCms\Commerce\CatalogPricing\Conditions\CatalogPricingPurchasableConditionRule` should be used instead. +- Deprecated `craft\commerce\elements\conditions\purchasables\CatalogPricingCustomerConditionRule`. `CraftCms\Commerce\CatalogPricing\Conditions\CatalogPricingCustomerConditionRule` should be used instead. +- Deprecated `craft\commerce\base\CatalogPricingConditionRuleInterface`. `CraftCms\Commerce\CatalogPricing\Contracts\CatalogPricingConditionRuleInterface` should be used instead. +- Removed `craft\commerce\records\CatalogPricing` as it was unused. +- Removed `craft\commerce\records\CatalogPricingRuleUser` as it was unused. +- Removed `craft\commerce\records\CatalogPricingRule`. `CraftCms\Commerce\CatalogPricing\Records\CatalogPricingRule` should be used instead. +- Removed `craft\commerce\records\CatalogPricingQueue`. `CraftCms\Commerce\CatalogPricing\Records\CatalogPricingQueue` should be used instead. +- `CraftCms\Commerce\CatalogPricing\Data\CatalogPricingRule` now uses `CraftCms\Commerce\Customer\Conditions\CatalogPricingRuleCustomerCondition`, `CraftCms\Commerce\CatalogPricing\Conditions\CatalogPricingRuleProductCondition`, `CatalogPricingRuleVariantCondition`, and `CraftCms\Commerce\Purchasable\Conditions\CatalogPricingRulePurchasableCondition`. +- Added `CraftCms\Commerce\CatalogPricing\Jobs\CatalogPricingJob`, a native Laravel `ShouldQueue` job. +- Removed `craft\commerce\queue\jobs\CatalogPricing`. `CraftCms\Commerce\CatalogPricing\Jobs\CatalogPricingJob` should be used instead. + +#### Controllers + +- Removed `craft\commerce\controllers\CatalogPricingRulesController`. `CraftCms\Commerce\Http\Controllers\Settings\CatalogPricingRulesController` should be used instead. +- Removed `craft\commerce\controllers\CatalogPricingController`. `CraftCms\Commerce\Http\Controllers\Settings\CatalogPricingController` should be used instead. + +### Console + +- Added `CraftCms\Commerce\Console\Commands\ExampleTemplates\ExampleTemplatesCommand` (`commerce:example-templates`). +- Added `CraftCms\Commerce\Console\Commands\Gateways\GatewaysListCommand` (`commerce:gateways:list`). +- Added `CraftCms\Commerce\Console\Commands\Gateways\GatewaysWebhookUrlCommand` (`commerce:gateways:webhook-url`). +- Added `CraftCms\Commerce\Console\Commands\PricingCatalog\PricingCatalogGenerateCommand` (`commerce:pricing-catalog:generate`). +- Added `CraftCms\Commerce\Console\Commands\ResetData\ResetDataCommand` (`commerce:reset-data`). +- Added `CraftCms\Commerce\Console\Commands\TransferCustomerData\TransferCustomerDataCommand` (`commerce:transfer-customer-data`). +- Removed `craft\commerce\console\controllers\ExampleTemplatesController`, `craft\commerce\console\controllers\GatewaysController`, `craft\commerce\console\controllers\PricingCatalogController`, `craft\commerce\console\controllers\ResetDataController`, and `craft\commerce\console\controllers\TransferCustomerDataController`. Their legacy `commerce/*` CLI routes still work as command aliases (e.g. `craft commerce/gateways/list`). +- Removed `craft\commerce\console\Controller`. + +### Controllers + +- Removed `craft\commerce\controllers\BaseController`, `craft\commerce\controllers\BaseCpController`, `craft\commerce\controllers\BaseAdminController`, and `craft\commerce\controllers\BaseFrontEndController`. +- Removed `craft\commerce\controllers\BaseStoreManagementController`. +- Removed `craft\commerce\controllers\BaseTaxSettingsController`. +- Removed `craft\commerce\controllers\BaseShippingSettingsController`. +- Removed `craft\commerce\controllers\SettingsController`. `CraftCms\Commerce\Http\Controllers\Settings\SettingsController` should be used instead. +- Removed `craft\commerce\controllers\PromotionsController`. The `commerce/promotions` URL now redirects to `commerce/promotions/sales` via a plain route closure. + +### Customers + +- Added `CraftCms\Commerce\Customer\Customers`. +- Added `CraftCms\Commerce\Customer\Records\Customer`. +- Added `CraftCms\Commerce\Customer\Customers::afterSaveUserHandler()` and `afterSaveAddressHandler()`, listening for `CraftCms\Cms\Element\Events\ElementSaved` to persist primary billing/shipping addresses, primary payment sources, and order email syncing. +- Deprecated `craft\commerce\services\Customers`. `CraftCms\Commerce\Customer\Customers` should be used instead. +- Removed `craft\commerce\behaviors\CustomerBehavior`. `User::getPrimaryBillingAddressId()`, `getPrimaryShippingAddressId()`, `getPrimaryPaymentSourceId()`, `getActiveCarts()`, `getInactiveCarts()`, and `getOrders()` are provided via a `Illuminate\Support\Traits\Macroable` macro instead. +- Removed `craft\commerce\behaviors\CustomerAddressBehavior`. `Address::getIsPrimaryBilling()` and `getIsPrimaryShipping()` are provided via a `Illuminate\Support\Traits\Macroable` macro instead. +- Added `CraftCms\Commerce\Customer\Fields\IsPrimaryBillingField`, `IsPrimaryShippingField`, `PrimaryBillingAddressIdField`, and `PrimaryShippingAddressIdField`, opt-in read-only field types that expose the primary billing/shipping macros via `toArray()`/GraphQL. +- Removed `craft\commerce\records\Customer`. `CraftCms\Commerce\Customer\Records\Customer` should be used instead. +- Added `CraftCms\Commerce\Customer\Conditions\DiscountCustomerCondition`, `HasOrdersConditionRule`, `SignedInConditionRule`, `DiscountGroupConditionRule`, `ShippingMethodCustomerCondition`, `ShippingRuleCustomerCondition`, `CatalogPricingRuleCustomerCondition`, and `CatalogPricingRuleCustomerConditionRule`. +- Deprecated `craft\commerce\elements\conditions\customers\DiscountCustomerCondition`, `HasOrdersConditionRule`, `SignedInConditionRule`, `ShippingMethodCustomerCondition`, `ShippingRuleCustomerCondition`, `CatalogPricingRuleCustomerCondition`, and `CatalogPricingRuleCustomerConditionRule`. The `CraftCms\Commerce\Customer\Conditions` equivalents should be used instead. +- Deprecated `craft\commerce\elements\conditions\users\DiscountGroupConditionRule`. `CraftCms\Commerce\Customer\Conditions\DiscountGroupConditionRule` should be used instead. +- Added `CraftCms\Commerce\Customer\FieldLayoutElements\UserAddressSettings`. +- Deprecated `craft\commerce\fieldlayoutelements\UserAddressSettings`. `CraftCms\Commerce\Customer\FieldLayoutElements\UserAddressSettings` should be used instead. + +### Dashboard & Widgets + +- Added `CraftCms\Commerce\Stats\Stat`. +- Added `CraftCms\Commerce\Stats\Contracts\StatInterface`. +- Added `CraftCms\Commerce\Dashboard\Widgets\Concerns\StatWidgetTrait` trait. +- Added `CraftCms\Commerce\Stats\AverageOrderTotal`. +- Added `CraftCms\Commerce\Stats\NewCustomers`. +- Added `CraftCms\Commerce\Stats\RepeatCustomers`. +- Added `CraftCms\Commerce\Stats\TopCustomers`. +- Added `CraftCms\Commerce\Stats\TopProductTypes`. +- Added `CraftCms\Commerce\Stats\TopPurchasables`. +- Added `CraftCms\Commerce\Stats\TopProducts`. +- Added `CraftCms\Commerce\Stats\TotalOrders`. +- Added `CraftCms\Commerce\Stats\TotalOrdersByCountry`. +- Added `CraftCms\Commerce\Stats\TotalRevenue`. +- Added `CraftCms\Commerce\Dashboard\Widgets\AverageOrderTotal`. +- Added `CraftCms\Commerce\Dashboard\Widgets\NewCustomers`. +- Added `CraftCms\Commerce\Dashboard\Widgets\RepeatCustomers`. +- Added `CraftCms\Commerce\Dashboard\Widgets\TopCustomers`. +- Added `CraftCms\Commerce\Dashboard\Widgets\TopProductTypes`. +- Added `CraftCms\Commerce\Dashboard\Widgets\TopPurchasables`. +- Added `CraftCms\Commerce\Dashboard\Widgets\TopProducts`. +- Added `CraftCms\Commerce\Dashboard\Widgets\TotalOrders`. +- Added `CraftCms\Commerce\Dashboard\Widgets\TotalOrdersByCountry`. +- Added `CraftCms\Commerce\Dashboard\Widgets\TotalRevenue`. +- Added `CraftCms\Commerce\Dashboard\Widgets\Orders`. +- Deprecated `craft\commerce\stats\AverageOrderTotal`. `CraftCms\Commerce\Stats\AverageOrderTotal` should be used instead. +- Deprecated `craft\commerce\stats\NewCustomers`. `CraftCms\Commerce\Stats\NewCustomers` should be used instead. +- Deprecated `craft\commerce\stats\RepeatCustomers`. `CraftCms\Commerce\Stats\RepeatCustomers` should be used instead. +- Deprecated `craft\commerce\stats\TopCustomers`. `CraftCms\Commerce\Stats\TopCustomers` should be used instead. +- Deprecated `craft\commerce\stats\TopProductTypes`. `CraftCms\Commerce\Stats\TopProductTypes` should be used instead. +- Deprecated `craft\commerce\stats\TopPurchasables`. `CraftCms\Commerce\Stats\TopPurchasables` should be used instead. +- Deprecated `craft\commerce\stats\TopProducts`. `CraftCms\Commerce\Stats\TopProducts` should be used instead. +- Deprecated `craft\commerce\stats\TotalOrders`. `CraftCms\Commerce\Stats\TotalOrders` should be used instead. +- Deprecated `craft\commerce\stats\TotalOrdersByCountry`. `CraftCms\Commerce\Stats\TotalOrdersByCountry` should be used instead. +- Deprecated `craft\commerce\stats\TotalRevenue`. `CraftCms\Commerce\Stats\TotalRevenue` should be used instead. +- Deprecated `craft\commerce\base\StatInterface`. `CraftCms\Commerce\Stats\Contracts\StatInterface` should be used instead. +- `CraftCms\Commerce\Stats\Stat` and its subclasses now build their queries entirely through the Laravel query builder, using `tpetry/laravel-query-expressions` for cross-database SQL differences instead of manual driver checks. +- `CraftCms\Commerce\Stats\Stat` now implements `CraftCms\Commerce\Store\Contracts\HasStoreInterface`, matching its legacy counterpart (`StoreTrait` already satisfied the interface's contract; the `implements` clause itself had been dropped). +- Added `CraftCms\Commerce\Support\Expressions\LocalTimestamp`, `DateOnly`, `MonthKey`, and `Round` query expressions. +- Deprecated `craft\commerce\widgets\AverageOrderTotal`. `CraftCms\Commerce\Dashboard\Widgets\AverageOrderTotal` should be used instead. +- Deprecated `craft\commerce\widgets\NewCustomers`. `CraftCms\Commerce\Dashboard\Widgets\NewCustomers` should be used instead. +- Deprecated `craft\commerce\widgets\RepeatCustomers`. `CraftCms\Commerce\Dashboard\Widgets\RepeatCustomers` should be used instead. +- Deprecated `craft\commerce\widgets\TopCustomers`. `CraftCms\Commerce\Dashboard\Widgets\TopCustomers` should be used instead. +- Deprecated `craft\commerce\widgets\TopProductTypes`. `CraftCms\Commerce\Dashboard\Widgets\TopProductTypes` should be used instead. +- Deprecated `craft\commerce\widgets\TopPurchasables`. `CraftCms\Commerce\Dashboard\Widgets\TopPurchasables` should be used instead. +- Deprecated `craft\commerce\widgets\TopProducts`. `CraftCms\Commerce\Dashboard\Widgets\TopProducts` should be used instead. +- Deprecated `craft\commerce\widgets\TotalOrders`. `CraftCms\Commerce\Dashboard\Widgets\TotalOrders` should be used instead. +- Deprecated `craft\commerce\widgets\TotalOrdersByCountry`. `CraftCms\Commerce\Dashboard\Widgets\TotalOrdersByCountry` should be used instead. +- Deprecated `craft\commerce\widgets\TotalRevenue`. `CraftCms\Commerce\Dashboard\Widgets\TotalRevenue` should be used instead. +- Deprecated `craft\commerce\widgets\Orders`. `CraftCms\Commerce\Dashboard\Widgets\Orders` should be used instead. +- Deprecated `craft\commerce\base\Stat`. `CraftCms\Commerce\Stats\Stat` should be used instead. +- Deprecated `craft\commerce\base\StatWidgetTrait`. `CraftCms\Commerce\Dashboard\Widgets\Concerns\StatWidgetTrait` should be used instead. +- Removed `craft\commerce\base\StatTrait`. Its properties are now declared directly on `CraftCms\Commerce\Stats\Stat`. + +### Email + +- Added `CraftCms\Commerce\Email\Emails`. +- Added `CraftCms\Commerce\Email\Records\Email`. +- Added `CraftCms\Commerce\Email\Models\Email`. +- Added `CraftCms\Commerce\Email\Exceptions\EmailException`. +- Added `CraftCms\Commerce\Email\Events\EmailEvent`. +- Added `CraftCms\Commerce\Email\Events\MailEvent`. +- Added `CraftCms\Commerce\Email\Jobs\SendEmailJob`, a native Laravel `ShouldQueue` job. +- Removed `craft\commerce\queue\jobs\SendEmail`. `CraftCms\Commerce\Email\Jobs\SendEmailJob` should be used instead. +- Deprecated `craft\commerce\services\Emails`. `CraftCms\Commerce\Email\Emails` should be used instead. +- Deprecated `craft\commerce\models\Email`. `CraftCms\Commerce\Email\Models\Email` should be used instead. +- Deprecated `craft\commerce\errors\EmailException`. `CraftCms\Commerce\Email\Exceptions\EmailException` should be used instead. +- Deprecated `craft\commerce\events\EmailEvent`. `CraftCms\Commerce\Email\Events\EmailEvent` should be used instead. +- Deprecated `craft\commerce\events\MailEvent`. `CraftCms\Commerce\Email\Events\MailEvent` should be used instead. +- Removed `craft\commerce\records\Email`. `CraftCms\Commerce\Email\Records\Email` should be used instead. + +#### Controllers + +- Removed `craft\commerce\controllers\EmailsController`. `CraftCms\Commerce\Http\Controllers\Settings\EmailsController` should be used instead. +- Removed `craft\commerce\controllers\EmailPreviewController`. `CraftCms\Commerce\Http\Controllers\EmailPreviewController` should be used instead. + +### Pdf + +- Added `CraftCms\Commerce\Pdf\Pdfs`. +- Added `CraftCms\Commerce\Pdf\Records\Pdf`. +- Added `CraftCms\Commerce\Pdf\Models\Pdf`. +- Added `CraftCms\Commerce\Http\RateLimiters\PdfChallengeRateLimiter`, replacing the per-action `yii\filters\RateLimiter` behavior used to throttle PDF download challenge requests. +- Added `CraftCms\Commerce\Pdf\Events\PdfEvent`. +- Added `CraftCms\Commerce\Pdf\Events\PdfRenderEvent`. +- Added `CraftCms\Commerce\Pdf\Events\PdfRenderOptionsEvent`. +- Deprecated `craft\commerce\services\Pdfs`. `CraftCms\Commerce\Pdf\Pdfs` should be used instead. +- Deprecated `craft\commerce\models\Pdf`. `CraftCms\Commerce\Pdf\Models\Pdf` should be used instead. +- Deprecated `craft\commerce\events\PdfEvent`. `CraftCms\Commerce\Pdf\Events\PdfEvent` should be used instead. +- Deprecated `craft\commerce\events\PdfRenderEvent`. `CraftCms\Commerce\Pdf\Events\PdfRenderEvent` should be used instead. +- Deprecated `craft\commerce\events\PdfRenderOptionsEvent`. `CraftCms\Commerce\Pdf\Events\PdfRenderOptionsEvent` should be used instead. +- Removed `craft\commerce\records\Pdf`. `CraftCms\Commerce\Pdf\Records\Pdf` should be used instead. +- Updated dompdf/dompdf to ^3.1.6 (from ^2.0.2). + +#### Controllers + +- Removed `craft\commerce\controllers\PdfsController`. `CraftCms\Commerce\Http\Controllers\Settings\PdfsController` should be used instead. + +### Formulas + +- Added `CraftCms\Commerce\Formula\Formulas`. +- Deprecated `craft\commerce\services\Formulas`. `CraftCms\Commerce\Formula\Formulas` should be used instead. + +#### Controllers + +- Removed `craft\commerce\controllers\FormulasController`. `CraftCms\Commerce\Http\Controllers\FormulasController` should be used instead. + +### GraphQL + +- Added `CraftCms\Commerce\Gql\Arguments\Elements\Product`. +- Added `CraftCms\Commerce\Gql\Arguments\Elements\Variant`. +- Added `CraftCms\Commerce\Gql\Interfaces\Elements\Product`. +- Added `CraftCms\Commerce\Gql\Interfaces\Elements\Variant`. +- Added `CraftCms\Commerce\Gql\Queries\Product`. +- Added `CraftCms\Commerce\Gql\Queries\Variant`. +- Added `CraftCms\Commerce\Gql\Resolvers\Elements\Product`. +- Added `CraftCms\Commerce\Gql\Resolvers\Elements\Variant`. +- Added `CraftCms\Commerce\Gql\Types\Elements\Product`. +- Added `CraftCms\Commerce\Gql\Types\Elements\Variant`. +- Added `CraftCms\Commerce\Gql\Types\Generators\ProductType`. +- Added `CraftCms\Commerce\Gql\Types\Generators\VariantType`. +- Added `CraftCms\Commerce\Gql\Types\Input\IntFalse`. +- Added `CraftCms\Commerce\Gql\Types\Input\Product`. +- Added `CraftCms\Commerce\Gql\Types\Input\Variant`. +- Added `CraftCms\Commerce\Gql\Types\SaleType`. +- Deprecated `craft\commerce\gql\arguments\elements\Product`. `CraftCms\Commerce\Gql\Arguments\Elements\Product` should be used instead. +- Deprecated `craft\commerce\gql\arguments\elements\Variant`. `CraftCms\Commerce\Gql\Arguments\Elements\Variant` should be used instead. +- Deprecated `craft\commerce\gql\interfaces\elements\Product`. `CraftCms\Commerce\Gql\Interfaces\Elements\Product` should be used instead. +- Deprecated `craft\commerce\gql\interfaces\elements\Variant`. `CraftCms\Commerce\Gql\Interfaces\Elements\Variant` should be used instead. +- Deprecated `craft\commerce\gql\queries\Product`. `CraftCms\Commerce\Gql\Queries\Product` should be used instead. +- Deprecated `craft\commerce\gql\queries\Variant`. `CraftCms\Commerce\Gql\Queries\Variant` should be used instead. +- Deprecated `craft\commerce\gql\resolvers\elements\Product`. `CraftCms\Commerce\Gql\Resolvers\Elements\Product` should be used instead. +- Deprecated `craft\commerce\gql\resolvers\elements\Variant`. `CraftCms\Commerce\Gql\Resolvers\Elements\Variant` should be used instead. +- Deprecated `craft\commerce\gql\types\elements\Product`. `CraftCms\Commerce\Gql\Types\Elements\Product` should be used instead. +- Deprecated `craft\commerce\gql\types\elements\Variant`. `CraftCms\Commerce\Gql\Types\Elements\Variant` should be used instead. +- Deprecated `craft\commerce\gql\types\generators\ProductType`. `CraftCms\Commerce\Gql\Types\Generators\ProductType` should be used instead. +- Deprecated `craft\commerce\gql\types\generators\VariantType`. `CraftCms\Commerce\Gql\Types\Generators\VariantType` should be used instead. +- Deprecated `craft\commerce\gql\types\input\IntFalse`. `CraftCms\Commerce\Gql\Types\Input\IntFalse` should be used instead. +- Deprecated `craft\commerce\gql\types\input\Product`. `CraftCms\Commerce\Gql\Types\Input\Product` should be used instead. +- Deprecated `craft\commerce\gql\types\input\Variant`. `CraftCms\Commerce\Gql\Types\Input\Variant` should be used instead. +- Deprecated `craft\commerce\gql\types\SaleType`. `CraftCms\Commerce\Gql\Types\SaleType` should be used instead. +- Deprecated `craft\commerce\helpers\Gql`. `CraftCms\Commerce\Helpers\Gql` should be used instead. + +### Helpers + +- Added `CraftCms\Commerce\Helpers\Cp`. +- Added `CraftCms\Commerce\Helpers\Currency`. +- Added `CraftCms\Commerce\Helpers\Locale`. +- Added `CraftCms\Commerce\Helpers\Localization`. +- Added `CraftCms\Commerce\Helpers\Order`. +- Added `CraftCms\Commerce\Helpers\ProductQuery`. +- Added `CraftCms\Commerce\Helpers\ProjectConfigData`. +- Added `CraftCms\Commerce\Helpers\Purchasable`. +- Deprecated `craft\commerce\helpers\Cp`. `CraftCms\Commerce\Helpers\Cp` should be used instead. +- Deprecated `craft\commerce\helpers\Currency`. `CraftCms\Commerce\Helpers\Currency` should be used instead. +- Deprecated `craft\commerce\helpers\Locale`. `CraftCms\Commerce\Helpers\Locale` should be used instead. +- Deprecated `craft\commerce\helpers\Localization`. `CraftCms\Commerce\Helpers\Localization` should be used instead. +- Deprecated `craft\commerce\helpers\Order`. `CraftCms\Commerce\Helpers\Order` should be used instead. +- Deprecated `craft\commerce\helpers\ProductQuery`. `CraftCms\Commerce\Helpers\ProductQuery` should be used instead. +- Deprecated `craft\commerce\helpers\ProjectConfigData`. `CraftCms\Commerce\Helpers\ProjectConfigData` should be used instead. +- Deprecated `craft\commerce\helpers\Purchasable`. `CraftCms\Commerce\Helpers\Purchasable` should be used instead. + +### Inventory + +- Added `CraftCms\Commerce\Inventory\Inventory`. +- Added `CraftCms\Commerce\Inventory\InventoryLocations`. +- Added `CraftCms\Commerce\Inventory\Records\InventoryItem`. +- Added `CraftCms\Commerce\Inventory\Records\InventoryLocation`. +- Added `CraftCms\Commerce\Inventory\Collections\InventoryMovementCollection`. +- Added `CraftCms\Commerce\Inventory\Collections\UpdateInventoryLevelCollection`. +- Added `CraftCms\Commerce\Inventory\Models\InventoryLocation`. +- Added `CraftCms\Commerce\Inventory\Models\InventoryMovement`. +- Added `CraftCms\Commerce\Inventory\Models\InventoryManualMovement`. +- Added `CraftCms\Commerce\Inventory\Models\InventoryCommittedMovement`. +- Added `CraftCms\Commerce\Inventory\Models\InventoryFulfillMovement`. +- Added `CraftCms\Commerce\Inventory\Models\InventoryRestockMovement`. +- Added `CraftCms\Commerce\Inventory\Models\InventoryTransferMovement`. +- Added `CraftCms\Commerce\Inventory\Models\InventoryLocationDeactivatedMovement`. +- Added `CraftCms\Commerce\Inventory\Models\DeactivateInventoryLocation`. +- Added `CraftCms\Commerce\Inventory\Models\InventoryItem`. +- Added `CraftCms\Commerce\Inventory\Models\InventoryFulfillmentLevel`. +- Added `CraftCms\Commerce\Inventory\Models\InventoryLevel`. +- Added `CraftCms\Commerce\Inventory\Models\InventoryTransaction`. +- Added `CraftCms\Commerce\Inventory\Models\UpdateInventoryLevel`. +- Added `CraftCms\Commerce\Inventory\Models\UpdateInventoryLevelInTransfer`. +- Added `CraftCms\Commerce\Inventory\Concerns\InventoryItemTrait`. +- Added `CraftCms\Commerce\Inventory\Concerns\InventoryLocationTrait`. +- Added `CraftCms\Commerce\Inventory\Contracts\InventoryMovementInterface`. +- Added `CraftCms\Commerce\Inventory\Enums\InventoryTransactionType` enum. +- Added `CraftCms\Commerce\Inventory\Enums\InventoryUpdateQuantityType` enum. +- Added `CraftCms\Commerce\Inventory\Events\InventoryMovementEvent`. +- Added `CraftCms\Commerce\Inventory\Events\UpdateInventoryLevelEvent`. +- Deprecated `craft\commerce\services\Inventory`. `CraftCms\Commerce\Inventory\Inventory` should be used instead. +- Deprecated `craft\commerce\services\InventoryLocations`. `CraftCms\Commerce\Inventory\InventoryLocations` should be used instead. +- Deprecated `craft\commerce\collections\InventoryMovementCollection`. `CraftCms\Commerce\Inventory\Collections\InventoryMovementCollection` should be used instead. +- Deprecated `craft\commerce\collections\UpdateInventoryLevelCollection`. `CraftCms\Commerce\Inventory\Collections\UpdateInventoryLevelCollection` should be used instead. +- Deprecated `craft\commerce\models\InventoryLocation`. `CraftCms\Commerce\Inventory\Models\InventoryLocation` should be used instead. +- Deprecated `craft\commerce\base\InventoryMovement`. `CraftCms\Commerce\Inventory\Models\InventoryMovement` should be used instead. +- Deprecated `craft\commerce\models\inventory\InventoryManualMovement`. `CraftCms\Commerce\Inventory\Models\InventoryManualMovement` should be used instead. +- Deprecated `craft\commerce\models\inventory\InventoryCommittedMovement`. `CraftCms\Commerce\Inventory\Models\InventoryCommittedMovement` should be used instead. +- Deprecated `craft\commerce\models\inventory\InventoryFulfillMovement`. `CraftCms\Commerce\Inventory\Models\InventoryFulfillMovement` should be used instead. +- Deprecated `craft\commerce\models\inventory\InventoryRestockMovement`. `CraftCms\Commerce\Inventory\Models\InventoryRestockMovement` should be used instead. +- Deprecated `craft\commerce\models\inventory\InventoryTransferMovement`. `CraftCms\Commerce\Inventory\Models\InventoryTransferMovement` should be used instead. +- Deprecated `craft\commerce\models\inventory\InventoryLocationDeactivatedMovement`. `CraftCms\Commerce\Inventory\Models\InventoryLocationDeactivatedMovement` should be used instead. +- Deprecated `craft\commerce\models\inventory\DeactivateInventoryLocation`. `CraftCms\Commerce\Inventory\Models\DeactivateInventoryLocation` should be used instead. +- Deprecated `craft\commerce\models\InventoryItem`. `CraftCms\Commerce\Inventory\Models\InventoryItem` should be used instead. +- Deprecated `craft\commerce\models\InventoryFulfillmentLevel`. `CraftCms\Commerce\Inventory\Models\InventoryFulfillmentLevel` should be used instead. +- Deprecated `craft\commerce\models\InventoryLevel`. `CraftCms\Commerce\Inventory\Models\InventoryLevel` should be used instead. +- Deprecated `craft\commerce\models\InventoryTransaction`. `CraftCms\Commerce\Inventory\Models\InventoryTransaction` should be used instead. +- Deprecated `craft\commerce\models\inventory\UpdateInventoryLevel`. `CraftCms\Commerce\Inventory\Models\UpdateInventoryLevel` should be used instead. +- Deprecated `craft\commerce\models\inventory\UpdateInventoryLevelInTransfer`. `CraftCms\Commerce\Inventory\Models\UpdateInventoryLevelInTransfer` should be used instead. +- Deprecated `craft\commerce\base\InventoryItemTrait`. `CraftCms\Commerce\Inventory\Concerns\InventoryItemTrait` should be used instead. +- Deprecated `craft\commerce\base\InventoryLocationTrait`. `CraftCms\Commerce\Inventory\Concerns\InventoryLocationTrait` should be used instead. +- Deprecated `craft\commerce\base\InventoryMovementInterface`. `CraftCms\Commerce\Inventory\Contracts\InventoryMovementInterface` should be used instead. +- Deprecated `craft\commerce\enums\InventoryTransactionType`. `CraftCms\Commerce\Inventory\Enums\InventoryTransactionType` should be used instead. +- Deprecated `craft\commerce\enums\InventoryUpdateQuantityType`. `CraftCms\Commerce\Inventory\Enums\InventoryUpdateQuantityType` should be used instead. +- Deprecated `craft\commerce\events\InventoryMovementEvent`. `CraftCms\Commerce\Inventory\Events\InventoryMovementEvent` should be used instead. +- Deprecated `craft\commerce\events\UpdateInventoryLevelEvent`. `CraftCms\Commerce\Inventory\Events\UpdateInventoryLevelEvent` should be used instead. +- Removed `craft\commerce\records\InventoryItem`. `CraftCms\Commerce\Inventory\Records\InventoryItem` should be used instead. +- Removed `craft\commerce\records\InventoryLocation`. `CraftCms\Commerce\Inventory\Records\InventoryLocation` should be used instead. + +#### Controllers + +- Removed `craft\commerce\controllers\InventoryController`. `CraftCms\Commerce\Http\Controllers\InventoryController` should be used instead. +- Removed `craft\commerce\controllers\InventoryLocationsController`. `CraftCms\Commerce\Http\Controllers\InventoryLocationsController` should be used instead. + +### Orders + +- Added `CraftCms\Commerce\Order\Elements\Order`. +- Added `CraftCms\Commerce\Order\Queries\OrderQuery`. +- Added `CraftCms\Commerce\Order\Models\Order`. +- Added `CraftCms\Commerce\Order\Models\OrderStatus`. +- Added `CraftCms\Commerce\Order\Models\OrderAdjustment`. +- Added `CraftCms\Commerce\Order\Models\OrderNotice`. +- Added `CraftCms\Commerce\Order\Models\OrderHistory`. +- Added `CraftCms\Commerce\Order\Models\LineItemStatus`. +- Added `CraftCms\Commerce\Order\Records\OrderHistory`. +- Added `CraftCms\Commerce\Order\Records\OrderAdjustment`. +- Added `CraftCms\Commerce\Order\Records\LineItemStatus`. +- Added `CraftCms\Commerce\Order\Records\OrderStatus`. +- Added `CraftCms\Commerce\Order\Records\OrderNotice`. +- Added `CraftCms\Commerce\Order\Exceptions\OrderAdjustmentNotFoundException`. +- Added `CraftCms\Commerce\Order\Exceptions\CurrencyException`. +- Added `CraftCms\Commerce\Order\Exceptions\LineItemNotFoundException`. +- Added `CraftCms\Commerce\Order\Exceptions\OrderStatusException`. +- Added `CraftCms\Commerce\Order\Exceptions\LineItemException`. +- Added `CraftCms\Commerce\Order\LineItem\Data\LineItem`. +- Added `CraftCms\Commerce\Order\LineItem\Models\LineItem`. +- Added `CraftCms\Commerce\Order\LineItem\LineItems`. +- Added `CraftCms\Commerce\Order\LineItem\Enums\LineItemType` enum. +- Added `CraftCms\Commerce\Order\Orders`. +- Added `CraftCms\Commerce\Order\Carts`. +- Added `CraftCms\Commerce\Order\OrderNotices`. +- Added `CraftCms\Commerce\Order\OrderHistories`. +- Added `CraftCms\Commerce\Order\OrderAdjustments`. +- Added `CraftCms\Commerce\Order\OrderStatuses`. +- Added `CraftCms\Commerce\Order\LineItemStatuses`. +- Added `CraftCms\Commerce\Order\Adjuster\Tax`. +- Added `CraftCms\Commerce\Order\Adjuster\Shipping`. +- Added `CraftCms\Commerce\Order\Adjuster\Discount`. +- Added `CraftCms\Commerce\Order\Adjuster\Contracts\AdjusterInterface`. +- Added `CraftCms\Commerce\Order\Adjuster\AdjusterTypes`, a `CraftCms\Cms\Component\TypeRegistry` for registering order adjuster types. +- Added `CraftCms\Commerce\Order\Adjuster\DiscountAdjusterTypes`, a `CraftCms\Cms\Component\TypeRegistry` for registering adjuster types that should be treated as discounts. +- Deprecated `craft\commerce\services\OrderAdjustments::EVENT_REGISTER_ORDER_ADJUSTERS`. `CraftCms\Commerce\Order\Adjuster\AdjusterTypes::register()` should be used instead. +- Deprecated `craft\commerce\services\OrderAdjustments::EVENT_REGISTER_DISCOUNT_ADJUSTERS`. `CraftCms\Commerce\Order\Adjuster\DiscountAdjusterTypes::register()` should be used instead. +- Added `CraftCms\Commerce\Order\Exporters\Expanded`. +- Added `CraftCms\Commerce\Order\Exporters\LineItemExport`. +- Added `CraftCms\Commerce\Order\Exporters\OrderExport`. +- Added `CraftCms\Commerce\Http\Controllers\Concerns\HasCartArray`, a trait shared between `CartController` and `PaymentsController` for building a cart's array representation. +- Added `CraftCms\Commerce\Http\RateLimiters\CartRateLimiter` and `CraftCms\Commerce\Http\RateLimiters\CartChallengeRateLimiter`. +- Added `CraftCms\Commerce\Order\Events\AddLineItemEvent`. +- Added `CraftCms\Commerce\Order\Events\CartEvent`. +- Added `CraftCms\Commerce\Order\Events\CartPurgeEvent`. +- Added `CraftCms\Commerce\Order\Events\DefaultLineItemStatusEvent`. +- Added `CraftCms\Commerce\Order\Events\DefaultOrderStatusEvent`. +- Added `CraftCms\Commerce\Order\Events\LineItemEvent`. +- Added `CraftCms\Commerce\Order\Events\ModifyCartInfoEvent`. +- Added `CraftCms\Commerce\Order\Events\OrderLineItemsRefreshEvent`. +- Added `CraftCms\Commerce\Order\Events\OrderNoticeEvent`. +- Added `CraftCms\Commerce\Order\Events\OrderStatusEmailsEvent`. +- Added `CraftCms\Commerce\Order\Events\OrderStatusEvent`. +- Added `CraftCms\Commerce\Order\Events\PurgeAddressesEvent`. +- Deprecated `craft\commerce\elements\Order`. `CraftCms\Commerce\Order\Elements\Order` should be used instead. +- Deprecated `craft\commerce\elements\db\OrderQuery`. `CraftCms\Commerce\Order\Queries\OrderQuery` should be used instead. +- Deprecated `craft\commerce\records\Order`. `CraftCms\Commerce\Order\Models\Order` should be used instead. +- Deprecated `craft\commerce\models\OrderStatus`. `CraftCms\Commerce\Order\Models\OrderStatus` should be used instead. +- Deprecated `craft\commerce\models\OrderAdjustment`. `CraftCms\Commerce\Order\Models\OrderAdjustment` should be used instead. +- Deprecated `craft\commerce\models\OrderNotice`. `CraftCms\Commerce\Order\Models\OrderNotice` should be used instead. +- Deprecated `craft\commerce\models\OrderHistory`. `CraftCms\Commerce\Order\Models\OrderHistory` should be used instead. +- Deprecated `craft\commerce\models\LineItemStatus`. `CraftCms\Commerce\Order\Models\LineItemStatus` should be used instead. +- Deprecated `craft\commerce\models\LineItem`. `CraftCms\Commerce\Order\LineItem\Data\LineItem` should be used instead. +- Deprecated `craft\commerce\records\LineItem`. `CraftCms\Commerce\Order\LineItem\Models\LineItem` should be used instead. +- Deprecated `craft\commerce\services\LineItems`. `CraftCms\Commerce\Order\LineItem\LineItems` should be used instead. +- Deprecated `craft\commerce\enums\LineItemType`. `CraftCms\Commerce\Order\LineItem\Enums\LineItemType` should be used instead. +- Deprecated `craft\commerce\services\Orders`. `CraftCms\Commerce\Order\Orders` should be used instead. +- Deprecated `craft\commerce\services\Carts`. `CraftCms\Commerce\Order\Carts` should be used instead. +- Deprecated `craft\commerce\services\OrderNotices`. `CraftCms\Commerce\Order\OrderNotices` should be used instead. +- Deprecated `craft\commerce\services\OrderHistories`. `CraftCms\Commerce\Order\OrderHistories` should be used instead. +- Deprecated `craft\commerce\services\OrderAdjustments`. `CraftCms\Commerce\Order\OrderAdjustments` should be used instead. +- Deprecated `craft\commerce\services\OrderStatuses`. `CraftCms\Commerce\Order\OrderStatuses` should be used instead. +- Deprecated `craft\commerce\services\LineItemStatuses`. `CraftCms\Commerce\Order\LineItemStatuses` should be used instead. +- Deprecated `craft\commerce\adjusters\Tax`. `CraftCms\Commerce\Order\Adjuster\Tax` should be used instead. +- Deprecated `craft\commerce\adjusters\Shipping`. `CraftCms\Commerce\Order\Adjuster\Shipping` should be used instead. +- Deprecated `craft\commerce\adjusters\Discount`. `CraftCms\Commerce\Order\Adjuster\Discount` should be used instead. +- Deprecated `craft\commerce\base\AdjusterInterface`. `CraftCms\Commerce\Order\Adjuster\Contracts\AdjusterInterface` should be used instead. +- Deprecated `craft\commerce\exports\Expanded`. `CraftCms\Commerce\Order\Exporters\Expanded` should be used instead. +- Deprecated `craft\commerce\exports\LineItemExport`. `CraftCms\Commerce\Order\Exporters\LineItemExport` should be used instead. +- Deprecated `craft\commerce\exports\OrderExport`. `CraftCms\Commerce\Order\Exporters\OrderExport` should be used instead. +- Deprecated `craft\commerce\errors\OrderAdjustmentNotFoundException`. `CraftCms\Commerce\Order\Exceptions\OrderAdjustmentNotFoundException` should be used instead. +- Deprecated `craft\commerce\errors\CurrencyException`. `CraftCms\Commerce\Order\Exceptions\CurrencyException` should be used instead. +- Deprecated `craft\commerce\errors\LineItemNotFoundException`. `CraftCms\Commerce\Order\Exceptions\LineItemNotFoundException` should be used instead. +- Deprecated `craft\commerce\errors\OrderStatusException`. `CraftCms\Commerce\Order\Exceptions\OrderStatusException` should be used instead. +- Deprecated `craft\commerce\errors\LineItemException`. `CraftCms\Commerce\Order\Exceptions\LineItemException` should be used instead. +- Deprecated `craft\commerce\events\AddLineItemEvent`. `CraftCms\Commerce\Order\Events\AddLineItemEvent` should be used instead. +- Deprecated `craft\commerce\events\CartEvent`. `CraftCms\Commerce\Order\Events\CartEvent` should be used instead. +- Deprecated `craft\commerce\events\CartPurgeEvent`. `CraftCms\Commerce\Order\Events\CartPurgeEvent` should be used instead. +- Deprecated `craft\commerce\events\DefaultLineItemStatusEvent`. `CraftCms\Commerce\Order\Events\DefaultLineItemStatusEvent` should be used instead. +- Deprecated `craft\commerce\events\DefaultOrderStatusEvent`. `CraftCms\Commerce\Order\Events\DefaultOrderStatusEvent` should be used instead. +- Deprecated `craft\commerce\events\LineItemEvent`. `CraftCms\Commerce\Order\Events\LineItemEvent` should be used instead. +- Deprecated `craft\commerce\events\ModifyCartInfoEvent`. `CraftCms\Commerce\Order\Events\ModifyCartInfoEvent` should be used instead. +- Deprecated `craft\commerce\events\OrderLineItemsRefreshEvent`. `CraftCms\Commerce\Order\Events\OrderLineItemsRefreshEvent` should be used instead. +- Deprecated `craft\commerce\events\OrderNoticeEvent`. `CraftCms\Commerce\Order\Events\OrderNoticeEvent` should be used instead. +- Deprecated `craft\commerce\events\OrderStatusEmailsEvent`. `CraftCms\Commerce\Order\Events\OrderStatusEmailsEvent` should be used instead. +- Deprecated `craft\commerce\events\OrderStatusEvent`. `CraftCms\Commerce\Order\Events\OrderStatusEvent` should be used instead. +- Deprecated `craft\commerce\events\PurgeAddressesEvent`. `CraftCms\Commerce\Order\Events\PurgeAddressesEvent` should be used instead. +- Removed `craft\commerce\records\OrderHistory`. `CraftCms\Commerce\Order\Records\OrderHistory` should be used instead. +- Removed `craft\commerce\records\OrderAdjustment`. `CraftCms\Commerce\Order\Records\OrderAdjustment` should be used instead. +- Removed `craft\commerce\records\LineItemStatus`. `CraftCms\Commerce\Order\Records\LineItemStatus` should be used instead. +- Removed `craft\commerce\records\OrderStatus`. `CraftCms\Commerce\Order\Records\OrderStatus` should be used instead. +- Removed `craft\commerce\records\OrderNotice`. `CraftCms\Commerce\Order\Records\OrderNotice` should be used instead. +- Removed `LineItem::getSaleAmount()`, `refreshFromPurchasable()`, and `populateFromPurchasable()`. +- Removed `LineItems::createLineItem()`. +- Removed `Carts::getCartName()`. The `cartCookie['name']` config setting should be used instead. +- Added `CraftCms\Commerce\Order\Conditions\OrderCondition`, `CompletedConditionRule`, `CouponCodeConditionRule`, `CustomerConditionRule`, `DateOrderedConditionRule`, `HasAdminNoticesConditionRule`, `PaidConditionRule`, `HasPurchasableConditionRule`, `ContainsPurchasablesConditionRule`, `OrderStatusConditionRule`, `OrderSiteConditionRule`, `PaymentGatewayConditionRule`, `ReferenceConditionRule`, `ShippingMethodConditionRule`, `ShippingAddressZoneConditionRule`, `DiscountedItemSubtotalConditionRule`, `ItemSubtotalConditionRule`, `ItemTotalConditionRule`, `TotalConditionRule`, `TotalDiscountConditionRule`, `TotalPaidConditionRule`, `TotalPriceConditionRule`, `TotalQtyConditionRule`, `TotalTaxConditionRule`, and `TotalWeightConditionRule`. +- Added `CraftCms\Commerce\Order\Conditions\OrderTextValuesAttributeConditionRule`, `OrderValuesAttributeConditionRule`, and `OrderCurrencyValuesAttributeConditionRule`. +- Added `CraftCms\Commerce\Order\Conditions\DiscountOrderCondition`, `GatewayOrderCondition`, `ShippingMethodOrderCondition`, and `ShippingRuleOrderCondition`. +- Deprecated `craft\commerce\elements\conditions\orders\*`. The `CraftCms\Commerce\Order\Conditions` equivalents should be used instead. +- Added `CraftCms\Commerce\Order\Actions\CopyLoadCartUrl`, `DownloadOrderPdfAction`, and `UpdateOrderStatus`. +- Deprecated `craft\commerce\elements\actions\CopyLoadCartUrl`, `DownloadOrderPdfAction`, and `UpdateOrderStatus`. The `CraftCms\Commerce\Order\Actions` equivalents should be used instead. +- Added `Order::getObjectTemplateVariables()`, ported from 5.x, fixing a bug where object templates referencing a datetime attribute (e.g. the order reference format using `dateOrdered|date(...)`) threw an "Array to string conversion" error. ([#4255](https://github.com/craftcms/commerce/issues/4255)) +- Added `CraftCms\Commerce\Order\Conditions\OrderCondition::$queryParams`, matching the per-instance exclusive-query-param scoping `craft\base\conditions\BaseCondition::$queryParams` provided in the legacy Yii2 condition system. + +#### Controllers + +- Removed `craft\commerce\controllers\OrdersController`. `CraftCms\Commerce\Http\Controllers\OrdersController` should be used instead. +- Removed `craft\commerce\controllers\CartController`. `CraftCms\Commerce\Http\Controllers\CartController` should be used instead. +- Removed `craft\commerce\controllers\OrderStatusesController`. `CraftCms\Commerce\Http\Controllers\Settings\OrderStatusesController` should be used instead. +- Removed `craft\commerce\controllers\LineItemStatusesController`. `CraftCms\Commerce\Http\Controllers\Settings\LineItemStatusesController` should be used instead. +- Removed `craft\commerce\controllers\UserOrdersController`. `CraftCms\Commerce\Http\Controllers\UserOrdersController` should be used instead. +- Removed `craft\commerce\controllers\OrderSettingsController`. `CraftCms\Commerce\Http\Controllers\Settings\OrderSettingsController` should be used instead. +- Removed `craft\commerce\controllers\DownloadsController`. `CraftCms\Commerce\Http\Controllers\DownloadsController` should be used instead. + +### Payments + +- Added `CraftCms\Commerce\Payment\Transactions`. +- Added `CraftCms\Commerce\Payment\PaymentSources`. +- Added `CraftCms\Commerce\Payment\Gateway\Gateways`. +- Added `CraftCms\Commerce\Payment\Payments`. +- Added `CraftCms\Commerce\Payment\Webhooks`. +- Added `CraftCms\Commerce\Payment\Currencies`. +- Added `CraftCms\Commerce\Payment\PaymentCurrencies`. +- Added `CraftCms\Commerce\Payment\Records\Transaction`. +- Added `CraftCms\Commerce\Payment\Records\PaymentSource`. +- Added `CraftCms\Commerce\Payment\Gateway\Records\Gateway`. +- Added `CraftCms\Commerce\Payment\Records\PaymentCurrency`. +- Added `CraftCms\Commerce\Payment\Models\Transaction`. +- Added `CraftCms\Commerce\Payment\Models\PaymentSource`. +- Added `CraftCms\Commerce\Payment\Models\PaymentCurrency`. +- Added `CraftCms\Commerce\Payment\Forms\BasePaymentForm`. +- Added `CraftCms\Commerce\Payment\Forms\OffsitePaymentForm`. +- Added `CraftCms\Commerce\Payment\Forms\CreditCardPaymentForm`. +- Added `CraftCms\Commerce\Payment\Forms\DummyPaymentForm`. +- Added `CraftCms\Commerce\Payment\Gateway\Responses\Dummy`. +- Added `CraftCms\Commerce\Payment\Gateway\Responses\Manual`. +- Added `CraftCms\Commerce\Payment\Exceptions\PaymentException`. +- Added `CraftCms\Commerce\Payment\Exceptions\PaymentSourceException`. +- Added `CraftCms\Commerce\Payment\Exceptions\PaymentSourceCreatedLaterException`. +- Added `CraftCms\Commerce\Payment\Exceptions\RefundException`. +- Added `CraftCms\Commerce\Payment\Exceptions\TransactionException`. +- Added `CraftCms\Commerce\Payment\Gateway\Exceptions\GatewayException`. +- Added `CraftCms\Commerce\Payment\Gateway\Contracts\GatewayInterface`. +- Added `CraftCms\Commerce\Payment\Gateway\Contracts\RequestResponseInterface`. +- Added `CraftCms\Commerce\Payment\Gateway\GatewayTypes`, a `CraftCms\Cms\Component\TypeRegistry` for registering gateway types. +- Deprecated `craft\commerce\services\Gateways::EVENT_REGISTER_GATEWAY_TYPES`. `CraftCms\Commerce\Payment\Gateway\GatewayTypes::register()` should be used instead. +- Added `CraftCms\Commerce\Payment\Gateway\Gateway`. +- Added `CraftCms\Commerce\Payment\Gateway\Types\Dummy`. +- Added `CraftCms\Commerce\Payment\Gateway\Types\Manual`. +- Added `CraftCms\Commerce\Payment\Gateway\Types\MissingGateway`. +- Added `CraftCms\Commerce\Helpers\PaymentForm`. +- Added `CraftCms\Commerce\Payment\Events\PaymentCurrencyRateEvent`. +- Added `CraftCms\Commerce\Payment\Events\PaymentSourceEvent`. +- Added `CraftCms\Commerce\Payment\Events\ProcessPaymentEvent`. +- Added `CraftCms\Commerce\Payment\Events\RefundTransactionEvent`. +- Added `CraftCms\Commerce\Payment\Events\TransactionEvent`. +- Added `CraftCms\Commerce\Payment\Events\UpdatePrimaryPaymentSourceEvent`. +- Added `CraftCms\Commerce\Payment\Events\WebhookEvent`. +- Deprecated `craft\commerce\services\Transactions`. `CraftCms\Commerce\Payment\Transactions` should be used instead. +- Deprecated `craft\commerce\services\PaymentSources`. `CraftCms\Commerce\Payment\PaymentSources` should be used instead. +- Deprecated `craft\commerce\services\Gateways`. `CraftCms\Commerce\Payment\Gateway\Gateways` should be used instead. +- Deprecated `craft\commerce\base\Gateway`. `CraftCms\Commerce\Payment\Gateway\Gateway` should be used instead. +- Removed `craft\commerce\base\GatewayTrait`. Its properties and methods are now part of `CraftCms\Commerce\Payment\Gateway\Gateway`. +- Deprecated `craft\commerce\gateways\Dummy`. `CraftCms\Commerce\Payment\Gateway\Types\Dummy` should be used instead. +- Deprecated `craft\commerce\gateways\Manual`. `CraftCms\Commerce\Payment\Gateway\Types\Manual` should be used instead. +- Deprecated `craft\commerce\gateways\MissingGateway`. `CraftCms\Commerce\Payment\Gateway\Types\MissingGateway` should be used instead. +- Deprecated `craft\commerce\helpers\PaymentForm`. `CraftCms\Commerce\Helpers\PaymentForm` should be used instead. +- Deprecated `craft\commerce\services\Payments`. `CraftCms\Commerce\Payment\Payments` should be used instead. +- Deprecated `craft\commerce\services\Webhooks`. `CraftCms\Commerce\Payment\Webhooks` should be used instead. +- Deprecated `craft\commerce\services\Currencies`. `CraftCms\Commerce\Payment\Currencies` should be used instead. +- Deprecated `craft\commerce\services\PaymentCurrencies`. `CraftCms\Commerce\Payment\PaymentCurrencies` should be used instead. +- Deprecated `craft\commerce\models\Transaction`. `CraftCms\Commerce\Payment\Models\Transaction` should be used instead. +- Deprecated `craft\commerce\models\PaymentSource`. `CraftCms\Commerce\Payment\Models\PaymentSource` should be used instead. +- Deprecated `craft\commerce\models\PaymentCurrency`. `CraftCms\Commerce\Payment\Models\PaymentCurrency` should be used instead. +- Deprecated `craft\commerce\models\payments\BasePaymentForm`. `CraftCms\Commerce\Payment\Forms\BasePaymentForm` should be used instead. +- Deprecated `craft\commerce\models\payments\OffsitePaymentForm`. `CraftCms\Commerce\Payment\Forms\OffsitePaymentForm` should be used instead. +- Deprecated `craft\commerce\models\payments\CreditCardPaymentForm`. `CraftCms\Commerce\Payment\Forms\CreditCardPaymentForm` should be used instead. +- Deprecated `craft\commerce\models\payments\DummyPaymentForm`. `CraftCms\Commerce\Payment\Forms\DummyPaymentForm` should be used instead. +- Deprecated `craft\commerce\models\responses\Dummy`. `CraftCms\Commerce\Payment\Gateway\Responses\Dummy` should be used instead. +- Deprecated `craft\commerce\models\responses\Manual`. `CraftCms\Commerce\Payment\Gateway\Responses\Manual` should be used instead. +- Deprecated `craft\commerce\errors\PaymentException`. `CraftCms\Commerce\Payment\Exceptions\PaymentException` should be used instead. +- Deprecated `craft\commerce\errors\PaymentSourceException`. `CraftCms\Commerce\Payment\Exceptions\PaymentSourceException` should be used instead. +- Deprecated `craft\commerce\errors\PaymentSourceCreatedLaterException`. `CraftCms\Commerce\Payment\Exceptions\PaymentSourceCreatedLaterException` should be used instead. +- Deprecated `craft\commerce\errors\RefundException`. `CraftCms\Commerce\Payment\Exceptions\RefundException` should be used instead. +- Deprecated `craft\commerce\errors\TransactionException`. `CraftCms\Commerce\Payment\Exceptions\TransactionException` should be used instead. +- Deprecated `craft\commerce\errors\GatewayException`. `CraftCms\Commerce\Payment\Gateway\Exceptions\GatewayException` should be used instead. +- Deprecated `craft\commerce\base\GatewayInterface`. `CraftCms\Commerce\Payment\Gateway\Contracts\GatewayInterface` should be used instead. +- Deprecated `craft\commerce\base\RequestResponseInterface`. `CraftCms\Commerce\Payment\Gateway\Contracts\RequestResponseInterface` should be used instead. +- Deprecated `craft\commerce\events\PaymentSourceEvent`. `CraftCms\Commerce\Payment\Events\PaymentSourceEvent` should be used instead. +- Deprecated `craft\commerce\events\ProcessPaymentEvent`. `CraftCms\Commerce\Payment\Events\ProcessPaymentEvent` should be used instead. +- Deprecated `craft\commerce\events\RefundTransactionEvent`. `CraftCms\Commerce\Payment\Events\RefundTransactionEvent` should be used instead. +- Deprecated `craft\commerce\events\TransactionEvent`. `CraftCms\Commerce\Payment\Events\TransactionEvent` should be used instead. +- Deprecated `craft\commerce\events\UpdatePrimaryPaymentSourceEvent`. `CraftCms\Commerce\Payment\Events\UpdatePrimaryPaymentSourceEvent` should be used instead. +- Deprecated `craft\commerce\events\WebhookEvent`. `CraftCms\Commerce\Payment\Events\WebhookEvent` should be used instead. +- Removed `craft\commerce\records\Transaction`. `CraftCms\Commerce\Payment\Records\Transaction` should be used instead. +- Removed `craft\commerce\records\PaymentSource`. `CraftCms\Commerce\Payment\Records\PaymentSource` should be used instead. +- Removed `craft\commerce\records\Gateway`. `CraftCms\Commerce\Payment\Gateway\Records\Gateway` should be used instead. +- Removed `craft\commerce\records\PaymentCurrency`. `CraftCms\Commerce\Payment\Records\PaymentCurrency` should be used instead. +- Removed `Gateways::getGatewayOverrides()`. +- Removed `Transactions::deleteTransaction()`. `deleteTransactionById()` should be used instead. +- Widened `RefundTransactionEvent::$amount` to `?float` to allow `null` for a full refund. +- Widened `WebhookEvent::$response` to accept both `Illuminate\Http\Response` and `yii\web\Response`. +- `craft\commerce\base\Gateway` now uses `CraftCms\Commerce\Order\Conditions\GatewayOrderCondition` and `CraftCms\Commerce\Address\Conditions\GatewayAddressCondition`. + +#### Controllers + +- Removed `craft\commerce\controllers\PaymentsController`. `CraftCms\Commerce\Http\Controllers\PaymentsController` should be used instead. +- Removed `craft\commerce\controllers\PaymentSourcesController`. `CraftCms\Commerce\Http\Controllers\PaymentSourcesController` should be used instead. +- Removed `craft\commerce\controllers\WebhooksController`. `CraftCms\Commerce\Http\Controllers\WebhooksController` should be used instead. +- Removed `craft\commerce\controllers\Settings\GatewaysController`. `CraftCms\Commerce\Http\Controllers\Settings\GatewaysController` should be used instead. +- Removed `craft\commerce\controllers\PaymentCurrenciesController`. `CraftCms\Commerce\Http\Controllers\Settings\PaymentCurrenciesController` should be used instead. + +### Promotions + +- Added `CraftCms\Commerce\Promotion\Discounts`. +- Added `CraftCms\Commerce\Promotion\Sales`. +- Added `CraftCms\Commerce\Promotion\Coupons`. +- Added `CraftCms\Commerce\Promotion\Models\Discount`. +- Added `CraftCms\Commerce\Promotion\Models\Sale`. +- Added `CraftCms\Commerce\Promotion\Models\Coupon`. +- Added `CraftCms\Commerce\Promotion\Records\Discount`. +- Added `CraftCms\Commerce\Promotion\Records\DiscountCategory`. +- Added `CraftCms\Commerce\Promotion\Records\DiscountPurchasable`. +- Added `CraftCms\Commerce\Promotion\Records\CustomerDiscountUse`. +- Added `CraftCms\Commerce\Promotion\Records\EmailDiscountUse`. +- Added `CraftCms\Commerce\Promotion\Records\Sale`. +- Added `CraftCms\Commerce\Promotion\Records\SaleCategory`. +- Added `CraftCms\Commerce\Promotion\Records\SalePurchasable`. +- Added `CraftCms\Commerce\Promotion\Records\SaleUserGroup`. +- Added `CraftCms\Commerce\Promotion\Records\Coupon`. +- Added `CraftCms\Commerce\Promotion\Events\DiscountAdjustmentsEvent`. +- Added `CraftCms\Commerce\Promotion\Events\DiscountEvent`. +- Added `CraftCms\Commerce\Promotion\Events\MatchLineItemEvent`. +- Added `CraftCms\Commerce\Promotion\Events\MatchOrderEvent`. +- Added `CraftCms\Commerce\Promotion\Events\SaleEvent`. +- Added `CraftCms\Commerce\Promotion\Events\SaleMatchEvent`. +- Deprecated `craft\commerce\services\Discounts`. `CraftCms\Commerce\Promotion\Discounts` should be used instead. +- Deprecated `craft\commerce\services\Sales`. `CraftCms\Commerce\Promotion\Sales` should be used instead. +- Deprecated `craft\commerce\services\Coupons`. `CraftCms\Commerce\Promotion\Coupons` should be used instead. +- Deprecated `craft\commerce\models\Discount`. `CraftCms\Commerce\Promotion\Models\Discount` should be used instead. +- Deprecated `craft\commerce\models\Sale`. `CraftCms\Commerce\Promotion\Models\Sale` should be used instead. +- Deprecated `craft\commerce\models\Coupon`. `CraftCms\Commerce\Promotion\Models\Coupon` should be used instead. +- Deprecated `craft\commerce\events\DiscountAdjustmentsEvent`. `CraftCms\Commerce\Promotion\Events\DiscountAdjustmentsEvent` should be used instead. +- Deprecated `craft\commerce\events\DiscountEvent`. `CraftCms\Commerce\Promotion\Events\DiscountEvent` should be used instead. +- Deprecated `craft\commerce\events\MatchLineItemEvent`. `CraftCms\Commerce\Promotion\Events\MatchLineItemEvent` should be used instead. +- Deprecated `craft\commerce\events\MatchOrderEvent`. `CraftCms\Commerce\Promotion\Events\MatchOrderEvent` should be used instead. +- Deprecated `craft\commerce\events\SaleEvent`. `CraftCms\Commerce\Promotion\Events\SaleEvent` should be used instead. +- Deprecated `craft\commerce\events\SaleMatchEvent`. `CraftCms\Commerce\Promotion\Events\SaleMatchEvent` should be used instead. +- Removed `craft\commerce\records\DiscountCategory`. `CraftCms\Commerce\Promotion\Records\DiscountCategory` should be used instead. +- Removed `craft\commerce\records\DiscountPurchasable`. `CraftCms\Commerce\Promotion\Records\DiscountPurchasable` should be used instead. +- Removed `craft\commerce\records\CustomerDiscountUse`. `CraftCms\Commerce\Promotion\Records\CustomerDiscountUse` should be used instead. +- Removed `craft\commerce\records\EmailDiscountUse`. `CraftCms\Commerce\Promotion\Records\EmailDiscountUse` should be used instead. +- Removed `craft\commerce\records\Sale`. `CraftCms\Commerce\Promotion\Records\Sale` should be used instead. +- Removed `craft\commerce\records\SaleCategory`. `CraftCms\Commerce\Promotion\Records\SaleCategory` should be used instead. +- Removed `craft\commerce\records\SalePurchasable`. `CraftCms\Commerce\Promotion\Records\SalePurchasable` should be used instead. +- Removed `craft\commerce\records\SaleUserGroup`. `CraftCms\Commerce\Promotion\Records\SaleUserGroup` should be used instead. +- Removed `craft\commerce\records\Discount`. `CraftCms\Commerce\Promotion\Records\Discount` should be used instead. +- Removed `craft\commerce\records\Coupon`. `CraftCms\Commerce\Promotion\Records\Coupon` should be used instead. +- Removed `craft\commerce\models\Discount::setExcludeOnSale()`/`getExcludeOnSale()` and the `excludeOnSale` shim. `Discount::$excludeOnPromotion` should be used instead. +- `CraftCms\Commerce\Promotion\Models\Discount` now uses `CraftCms\Commerce\Order\Conditions\DiscountOrderCondition`, `CraftCms\Commerce\Customer\Conditions\DiscountCustomerCondition`, and `CraftCms\Commerce\Address\Conditions\DiscountAddressCondition`. +- Added `CraftCms\Commerce\Address\Conditions\DiscountAddressCondition`, `ZoneAddressCondition`, `GatewayAddressCondition`, and `PostalCodeFormulaConditionRule`. +- Deprecated `craft\commerce\elements\conditions\addresses\DiscountAddressCondition`, `ZoneAddressCondition`, `GatewayAddressCondition`, and `PostalCodeFormulaConditionRule`. The `CraftCms\Commerce\Address\Conditions` equivalents should be used instead. +- Added `CraftCms\Commerce\Promotion\Actions\CreateDiscount` and `CreateSale`. +- Deprecated `craft\commerce\elements\actions\CreateDiscount` and `CreateSale`. The `CraftCms\Commerce\Promotion\Actions` equivalents should be used instead. +- `CraftCms\Commerce\Promotion\Models\Coupon::getRules()` now validates that `code` is unique (case-insensitively, against every coupon regardless of discount), replacing `craft\commerce\validators\CouponsValidator`. +- Removed `craft\commerce\validators\CouponsValidator`. + +#### Controllers + +- Removed `craft\commerce\controllers\SalesController`. `CraftCms\Commerce\Http\Controllers\Settings\SalesController` should be used instead. +- Removed `craft\commerce\controllers\DiscountsController`. `CraftCms\Commerce\Http\Controllers\Settings\DiscountsController` should be used instead. + +### Purchasables + +- Added `CraftCms\Commerce\Purchasable\Elements\Purchasable`. +- Added `CraftCms\Commerce\Purchasable\Elements\Donation`. +- Added `CraftCms\Commerce\Purchasable\Models\Donation`. +- Added `CraftCms\Commerce\Purchasable\Models\PurchasableStore`. +- Added `CraftCms\Commerce\Purchasable\Queries\PurchasableQuery`. +- Added `CraftCms\Commerce\Purchasable\Queries\DonationQuery`. +- Added `CraftCms\Commerce\Purchasable\Queries\PurchasableConditionQuery`. +- Added `CraftCms\Commerce\Purchasable\Records\Purchasable`. +- Added `CraftCms\Commerce\Purchasable\Records\PurchasableStore`. +- Added `CraftCms\Commerce\Purchasable\Validation\PurchasableRules`. +- Added `CraftCms\Commerce\Purchasable\Validation\DonationRules`. +- Added `CraftCms\Commerce\Purchasable\Purchasables`. +- Added `CraftCms\Commerce\Purchasable\Contracts\PurchasableInterface`. +- Added `CraftCms\Commerce\Purchasable\Events\PurchasableAvailableEvent`. +- Added `CraftCms\Commerce\Purchasable\Events\PurchasableOutOfStockPurchasesAllowedEvent`. +- Added `CraftCms\Commerce\Purchasable\Events\PurchasableShippableEvent`. +- Added `CraftCms\Commerce\Purchasable\PurchasableTypes`, a `CraftCms\Cms\Component\TypeRegistry` for registering purchasable element types. +- Deprecated `craft\commerce\services\Purchasables::EVENT_REGISTER_PURCHASABLE_ELEMENT_TYPES`. `CraftCms\Commerce\Purchasable\PurchasableTypes::register()` should be used instead. +- Deprecated `craft\commerce\base\Purchasable`. `CraftCms\Commerce\Purchasable\Elements\Purchasable` should be used instead. +- Deprecated `craft\commerce\elements\Donation`. `CraftCms\Commerce\Purchasable\Elements\Donation` should be used instead. +- Deprecated `craft\commerce\records\Donation`. `CraftCms\Commerce\Purchasable\Models\Donation` should be used instead. +- Deprecated `craft\commerce\models\PurchasableStore`. `CraftCms\Commerce\Purchasable\Models\PurchasableStore` should be used instead. +- Deprecated `craft\commerce\elements\db\DonationQuery`. `CraftCms\Commerce\Purchasable\Queries\DonationQuery` should be used instead. +- Deprecated `craft\commerce\services\Purchasables`. `CraftCms\Commerce\Purchasable\Purchasables` should be used instead. +- Deprecated `craft\commerce\base\PurchasableInterface`. `CraftCms\Commerce\Purchasable\Contracts\PurchasableInterface` should be used instead. +- Deprecated `craft\commerce\events\PurchasableAvailableEvent`. `CraftCms\Commerce\Purchasable\Events\PurchasableAvailableEvent` should be used instead. +- Deprecated `craft\commerce\events\PurchasableOutOfStockPurchasesAllowedEvent`. `CraftCms\Commerce\Purchasable\Events\PurchasableOutOfStockPurchasesAllowedEvent` should be used instead. +- Deprecated `craft\commerce\events\PurchasableShippableEvent`. `CraftCms\Commerce\Purchasable\Events\PurchasableShippableEvent` should be used instead. +- Removed `craft\commerce\records\Purchasable`. `CraftCms\Commerce\Purchasable\Records\Purchasable` should be used instead. +- Removed `craft\commerce\records\PurchasableStore`. `CraftCms\Commerce\Purchasable\Records\PurchasableStore` should be used instead. +- Removed `craft\commerce\elements\db\PurchasableQuery`. `CraftCms\Commerce\Purchasable\Queries\PurchasableQuery` should be used instead. +- Removed `craft\commerce\records\OrderStatusEmail` as it was unused. +- Added `CraftCms\Commerce\Purchasable\Conditions\PurchasableConditionRule`, `PurchasableTypeConditionRule`, `SkuConditionRule`, `CatalogPricingRulePurchasableCategoryConditionRule`, and `CatalogPricingRulePurchasableCondition`. +- Deprecated `craft\commerce\elements\conditions\purchasables\PurchasableConditionRule`, `PurchasableTypeConditionRule`, `SkuConditionRule`, `CatalogPricingRulePurchasableCategoryConditionRule`, and `CatalogPricingRulePurchasableCondition`. The `CraftCms\Commerce\Purchasable\Conditions` equivalents should be used instead. +- Added `CraftCms\Commerce\Purchasable\FieldLayoutElements\PurchasableSkuField`, `PurchasablePriceField`, `PurchasableStockField`, `PurchasableWeightField`, `PurchasableDimensionsField`, `PurchasableAllowedQtyField`, `PurchasableAvailableForPurchaseField`, `PurchasableFreeShippingField`, and `PurchasablePromotableField`. +- Deprecated `craft\commerce\fieldlayoutelements\PurchasableSkuField`, `PurchasablePriceField`, `PurchasableStockField`, `PurchasableWeightField`, `PurchasableDimensionsField`, `PurchasableAllowedQtyField`, `PurchasableAvailableForPurchaseField`, `PurchasableFreeShippingField`, and `PurchasablePromotableField`. The `CraftCms\Commerce\Purchasable\FieldLayoutElements` equivalents should be used instead. +- **Breaking**: Changed `PurchasableInterface::getLineItemRules(LineItem $lineItem): array` to `validateLineItem(LineItem $lineItem): void`, which adds errors directly to `$lineItem->errors()` instead of returning legacy Yii2-shaped inline-closure validation rules. Third-party purchasable types must update their implementation accordingly. + +#### Controllers + +- Removed `craft\commerce\controllers\DonationsController`. `CraftCms\Commerce\Http\Controllers\DonationsController` should be used instead. + +### Shipping + +- Added `CraftCms\Commerce\Shipping\ShippingMethods`. +- Added `CraftCms\Commerce\Shipping\ShippingRules`. +- Added `CraftCms\Commerce\Shipping\ShippingRuleCategories`. +- Added `CraftCms\Commerce\Shipping\ShippingCategories`. +- Added `CraftCms\Commerce\Shipping\ShippingZones`. +- Added `CraftCms\Commerce\Shipping\Models\ShippingRule`. +- Added `CraftCms\Commerce\Shipping\Models\ShippingMethod`. +- Added `CraftCms\Commerce\Shipping\Models\ShippingMethodOption`. +- Added `CraftCms\Commerce\Shipping\Models\BaseShippingMethod`. +- Added `CraftCms\Commerce\Shipping\Models\ShippingAddressZone`. +- Added `CraftCms\Commerce\Shipping\Models\ShippingRuleCategory`. +- Added `CraftCms\Commerce\Shipping\Models\ShippingCategory`. +- Added `CraftCms\Commerce\Shipping\Records\ShippingZone`. +- Added `CraftCms\Commerce\Shipping\Records\ShippingMethod`. +- Added `CraftCms\Commerce\Shipping\Records\ShippingRule`. +- Added `CraftCms\Commerce\Shipping\Records\ShippingRuleCategory`. +- Added `CraftCms\Commerce\Shipping\Records\ShippingCategory`. +- Added `CraftCms\Commerce\Shipping\Contracts\ShippingMethodInterface`. +- Added `CraftCms\Commerce\Shipping\Contracts\ShippingRuleInterface`. +- Added `CraftCms\Commerce\Shipping\Exceptions\ShippingMethodException`. +- Added `CraftCms\Commerce\Shipping\Events\RegisterAvailableShippingMethodsEvent`. +- Deprecated `craft\commerce\services\ShippingMethods`. `CraftCms\Commerce\Shipping\ShippingMethods` should be used instead. +- Deprecated `craft\commerce\services\ShippingRules`. `CraftCms\Commerce\Shipping\ShippingRules` should be used instead. +- Deprecated `craft\commerce\services\ShippingRuleCategories`. `CraftCms\Commerce\Shipping\ShippingRuleCategories` should be used instead. +- Deprecated `craft\commerce\services\ShippingCategories`. `CraftCms\Commerce\Shipping\ShippingCategories` should be used instead. +- Deprecated `craft\commerce\services\ShippingZones`. `CraftCms\Commerce\Shipping\ShippingZones` should be used instead. +- Deprecated `craft\commerce\models\ShippingRule`. `CraftCms\Commerce\Shipping\Models\ShippingRule` should be used instead. +- Deprecated `craft\commerce\models\ShippingMethod`. `CraftCms\Commerce\Shipping\Models\ShippingMethod` should be used instead. +- Deprecated `craft\commerce\models\ShippingMethodOption`. `CraftCms\Commerce\Shipping\Models\ShippingMethodOption` should be used instead. +- Deprecated `craft\commerce\base\ShippingMethod`. `CraftCms\Commerce\Shipping\Models\BaseShippingMethod` should be used instead. +- Deprecated `craft\commerce\models\ShippingAddressZone`. `CraftCms\Commerce\Shipping\Models\ShippingAddressZone` should be used instead. +- Deprecated `craft\commerce\models\ShippingRuleCategory`. `CraftCms\Commerce\Shipping\Models\ShippingRuleCategory` should be used instead. +- Deprecated `craft\commerce\models\ShippingCategory`. `CraftCms\Commerce\Shipping\Models\ShippingCategory` should be used instead. +- Deprecated `craft\commerce\base\ShippingMethodInterface`. `CraftCms\Commerce\Shipping\Contracts\ShippingMethodInterface` should be used instead. +- Deprecated `craft\commerce\base\ShippingRuleInterface`. `CraftCms\Commerce\Shipping\Contracts\ShippingRuleInterface` should be used instead. +- Deprecated `craft\commerce\errors\ShippingMethodException`. `CraftCms\Commerce\Shipping\Exceptions\ShippingMethodException` should be used instead. +- Deprecated `craft\commerce\events\RegisterAvailableShippingMethodsEvent`. `CraftCms\Commerce\Shipping\Events\RegisterAvailableShippingMethodsEvent` should be used instead. +- Removed `craft\commerce\records\ShippingZone`. `CraftCms\Commerce\Shipping\Records\ShippingZone` should be used instead. +- Removed `craft\commerce\records\ShippingMethod`. `CraftCms\Commerce\Shipping\Records\ShippingMethod` should be used instead. +- Removed `craft\commerce\records\ShippingRule`. `CraftCms\Commerce\Shipping\Records\ShippingRule` should be used instead. +- Removed `craft\commerce\records\ShippingRuleCategory`. `CraftCms\Commerce\Shipping\Records\ShippingRuleCategory` should be used instead. +- Removed `craft\commerce\records\ShippingCategory`. `CraftCms\Commerce\Shipping\Records\ShippingCategory` should be used instead. +- `CraftCms\Commerce\Shipping\Models\ShippingRule` and `BaseShippingMethod` now use `CraftCms\Commerce\Order\Conditions\ShippingRuleOrderCondition`, `ShippingMethodOrderCondition`, `CraftCms\Commerce\Customer\Conditions\ShippingRuleCustomerCondition`, and `ShippingMethodCustomerCondition`. +- `CraftCms\Commerce\Base\Zone` and `ZoneInterface` now use `CraftCms\Commerce\Address\Conditions\ZoneAddressCondition`. +- `CraftCms\Commerce\Shipping\Data\ShippingMethodOption` no longer includes `dateCreated`/`dateUpdated` in its serialized fields, as they never reflected anything meaningful for a per-order computed option. + +#### Controllers + +- Removed `craft\commerce\controllers\ShippingZonesController`. `CraftCms\Commerce\Http\Controllers\Settings\ShippingZonesController` should be used instead. +- Removed `craft\commerce\controllers\ShippingMethodsController`. `CraftCms\Commerce\Http\Controllers\Settings\ShippingMethodsController` should be used instead. +- Removed `craft\commerce\controllers\ShippingRulesController`. `CraftCms\Commerce\Http\Controllers\Settings\ShippingRulesController` should be used instead. +- Removed `craft\commerce\controllers\ShippingCategoriesController`. `CraftCms\Commerce\Http\Controllers\Settings\ShippingCategoriesController` should be used instead. + +### Stores + +- Added `CraftCms\Commerce\Store\Stores`. +- Added `CraftCms\Commerce\Store\StoreSettings`. +- Added `CraftCms\Commerce\Store\Models\Store`. +- Added `CraftCms\Commerce\Store\Models\StoreSettings`. +- Added `CraftCms\Commerce\Store\Models\SiteStore`. +- Added `CraftCms\Commerce\Store\Records\Store`. +- Added `CraftCms\Commerce\Store\Records\SiteStore`. +- Added `CraftCms\Commerce\Store\Records\StoreSettings`. +- Added `CraftCms\Commerce\Store\Concerns\StoreTrait`. +- Added `CraftCms\Commerce\Store\Contracts\HasStoreInterface`. +- Added `CraftCms\Commerce\Store\Exceptions\StoreNotFoundException`. +- Added `CraftCms\Commerce\Http\Controllers\Concerns\HasStoreManagementScreen`, a trait shared by store-scoped settings controllers for their CP screen chrome. +- Added `CraftCms\Commerce\Store\Events\DeleteStoreEvent`. +- Added `CraftCms\Commerce\Store\Events\StoreEvent`. +- Deprecated `craft\commerce\services\Stores`. `CraftCms\Commerce\Store\Stores` should be used instead. +- Removed `craft\commerce\behaviors\StoreBehavior`. `Site::getStore()` is provided via a `Illuminate\Support\Traits\Macroable` macro instead. +- Deprecated `craft\commerce\services\StoreSettings`. `CraftCms\Commerce\Store\StoreSettings` should be used instead. +- Deprecated `craft\commerce\models\Store`. `CraftCms\Commerce\Store\Models\Store` should be used instead. +- Deprecated `craft\commerce\models\StoreSettings`. `CraftCms\Commerce\Store\Models\StoreSettings` should be used instead. +- Deprecated `craft\commerce\models\SiteStore`. `CraftCms\Commerce\Store\Models\SiteStore` should be used instead. +- Deprecated `craft\commerce\base\StoreTrait`. `CraftCms\Commerce\Store\Concerns\StoreTrait` should be used instead. +- Deprecated `craft\commerce\base\HasStoreInterface`. `CraftCms\Commerce\Store\Contracts\HasStoreInterface` should be used instead. +- Deprecated `craft\commerce\errors\StoreNotFoundException`. `CraftCms\Commerce\Store\Exceptions\StoreNotFoundException` should be used instead. +- Deprecated `craft\commerce\events\DeleteStoreEvent`. `CraftCms\Commerce\Store\Events\DeleteStoreEvent` should be used instead. +- Deprecated `craft\commerce\events\StoreEvent`. `CraftCms\Commerce\Store\Events\StoreEvent` should be used instead. +- Removed `craft\commerce\services\Store`. `CraftCms\Commerce\Store\Stores` should be used instead. +- Removed `craft\commerce\models\Store::setCountries()`, `getCountries()`, `getCountriesList()`, `getAdministrativeAreasListByCountryCode()`, and `getMarketAddressCondition()`. `Store::getSettings()` (returning `CraftCms\Commerce\Store\Models\StoreSettings`) should be used instead. +- Removed `craft\commerce\records\SiteStore`. `CraftCms\Commerce\Store\Records\SiteStore` should be used instead. +- Removed `craft\commerce\records\StoreSettings`. `CraftCms\Commerce\Store\Records\StoreSettings` should be used instead. +- Removed `craft\commerce\records\Store`. `CraftCms\Commerce\Store\Records\Store` should be used instead. +- Removed `craft\commerce\base\StoreRecordTrait` as it was unused. + +#### Controllers + +- Removed `craft\commerce\controllers\StoreManagementController`. `CraftCms\Commerce\Http\Controllers\Settings\StoreManagementController` should be used instead. +- Removed `craft\commerce\controllers\StoresController`. `CraftCms\Commerce\Http\Controllers\Settings\StoresController` should be used instead. + +### Subscriptions + +> [!WARNING] +> Subscription and billing-plan functionality has been removed from Craft Commerce entirely — there is no `Subscription` element type, no subscription field layout, and gateways can no longer implement subscription support. The `commerce_subscriptions`, `commerce_plans`, and related database tables are **not** dropped, so existing data is preserved for a future standalone migration path; only the application code (elements, services, records, models, forms, events, controllers, CP screens, and the gateway subscription interface) has been removed. + +- Removed `craft\commerce\elements\Subscription`, its element query, condition support, and deletion blocker. No replacement. +- Removed `craft\commerce\services\Subscriptions` and `CraftCms\Commerce\Subscription\Subscriptions`. No replacement. +- Removed `craft\commerce\services\Plans` and `CraftCms\Commerce\Subscription\Plans`. No replacement. +- Removed `craft\commerce\records\Subscription` and `CraftCms\Commerce\Subscription\Records\Subscription`. No replacement. +- Removed `craft\commerce\records\Plan` and `CraftCms\Commerce\Subscription\Records\Plan`. No replacement. +- Removed `craft\commerce\base\Plan`, `craft\commerce\base\PlanInterface`, `craft\commerce\base\PlanTrait`, and `CraftCms\Commerce\Subscription\Contracts\PlanInterface`. No replacement. +- Removed `craft\commerce\base\SubscriptionGateway` and `craft\commerce\base\SubscriptionGatewayInterface` — gateways can no longer declare subscription support. `craft\commerce\gateways\Dummy` now extends `craft\commerce\base\Gateway` directly. +- Removed `craft\commerce\base\SubscriptionResponseInterface` and `CraftCms\Commerce\Subscription\Contracts\SubscriptionResponseInterface`. No replacement. +- Removed `craft\commerce\models\subscriptions\DummyPlan` and `CraftCms\Commerce\Subscription\Models\DummyPlan`. No replacement. +- Removed `craft\commerce\models\subscriptions\SubscriptionPayment` and `CraftCms\Commerce\Subscription\Models\SubscriptionPayment`. No replacement. +- Removed `craft\commerce\models\subscriptions\CancelSubscriptionForm`, `SubscriptionForm`, `SwitchPlansForm`, and their `CraftCms\Commerce\Subscription\Forms\*` equivalents. No replacement. +- Removed `craft\commerce\models\responses\DummySubscriptionResponse` and `CraftCms\Commerce\Subscription\Responses\DummySubscriptionResponse`. No replacement. +- Removed `craft\commerce\errors\SubscriptionException` and `CraftCms\Commerce\Subscription\Exceptions\SubscriptionException`. No replacement. +- Removed `craft\commerce\events\CancelSubscriptionEvent`, `CreateSubscriptionEvent`, `PlanEvent`, `SubscriptionEvent`, `SubscriptionPaymentEvent`, `SubscriptionSwitchPlansEvent`, and their `CraftCms\Commerce\Subscription\Events\*` equivalents. No replacement. +- Removed the `commerce-manageSubscriptions` and `commerce-manageSubscriptionPlans` permissions. +- Removed the "Subscriptions" and "Subscription Plans" Control Panel nav items, the "Subscriptions" tab on the Edit User screen, and the "Subscription Settings" section of the general settings page (including the `updateBillingDetailsUrl` setting and `Settings::VIEW_URI_SUBSCRIPTIONS` constant). +- Removed the `craft.commerce.subscriptions()` Twig variable. No replacement. +- Removed `Gateways::getAllSubscriptionGateways()` (both `craft\commerce\services\Gateways` and `CraftCms\Commerce\Payment\Gateway\Gateways`). + +#### Controllers + +- Removed `craft\commerce\controllers\SubscriptionsController` and `CraftCms\Commerce\Http\Controllers\SubscriptionsController`. No replacement. +- Removed `craft\commerce\controllers\PlansController` and `CraftCms\Commerce\Http\Controllers\Settings\PlansController`. No replacement. + +### Tax + +- Added `CraftCms\Commerce\Tax\TaxCategories`. +- Added `CraftCms\Commerce\Tax\TaxZones`. +- Added `CraftCms\Commerce\Tax\Records\TaxCategory`. +- Added `CraftCms\Commerce\Tax\Records\TaxZone`. +- Added `CraftCms\Commerce\Tax\Records\TaxRate`. +- Added `CraftCms\Commerce\Tax\Models\TaxRate`. +- Added `CraftCms\Commerce\Tax\Models\TaxAddressZone`. +- Added `CraftCms\Commerce\Tax\Models\TaxCategory`. +- Added `CraftCms\Commerce\Tax\Contracts\TaxIdValidatorInterface`. +- Added `CraftCms\Commerce\Tax\Contracts\TaxEngineInterface`. +- Added `CraftCms\Commerce\Tax\Events\TaxEngineEvent`. +- Added `CraftCms\Commerce\Tax\Events\TaxIdValidatorsEvent`. +- Deprecated `craft\commerce\services\TaxCategories`. `CraftCms\Commerce\Tax\TaxCategories` should be used instead. +- Deprecated `craft\commerce\services\TaxZones`. `CraftCms\Commerce\Tax\TaxZones` should be used instead. +- Deprecated `craft\commerce\models\TaxRate`. `CraftCms\Commerce\Tax\Models\TaxRate` should be used instead. +- Deprecated `craft\commerce\models\TaxAddressZone`. `CraftCms\Commerce\Tax\Models\TaxAddressZone` should be used instead. +- Deprecated `craft\commerce\models\TaxCategory`. `CraftCms\Commerce\Tax\Models\TaxCategory` should be used instead. +- Deprecated `craft\commerce\base\TaxIdValidatorInterface`. `CraftCms\Commerce\Tax\Contracts\TaxIdValidatorInterface` should be used instead. +- Deprecated `craft\commerce\base\TaxEngineInterface`. `CraftCms\Commerce\Tax\Contracts\TaxEngineInterface` should be used instead. +- Deprecated `craft\commerce\events\TaxEngineEvent`. `CraftCms\Commerce\Tax\Events\TaxEngineEvent` should be used instead. +- Deprecated `craft\commerce\events\TaxIdValidatorsEvent`. `CraftCms\Commerce\Tax\Events\TaxIdValidatorsEvent` should be used instead. +- Removed `craft\commerce\records\TaxZone`. `CraftCms\Commerce\Tax\Records\TaxZone` should be used instead. +- Removed `craft\commerce\records\TaxRate`. `CraftCms\Commerce\Tax\Records\TaxRate` should be used instead. +- Removed `craft\commerce\records\TaxCategory`. `CraftCms\Commerce\Tax\Records\TaxCategory` should be used instead. + +#### Controllers + +- Removed `craft\commerce\controllers\TaxZonesController`. `CraftCms\Commerce\Http\Controllers\Settings\TaxZonesController` should be used instead. +- Removed `craft\commerce\controllers\TaxCategoriesController`. `CraftCms\Commerce\Http\Controllers\Settings\TaxCategoriesController` should be used instead. +- Removed `craft\commerce\controllers\TaxRatesController`. `CraftCms\Commerce\Http\Controllers\Settings\TaxRatesController` should be used instead. + +### Transfers + +- Added `CraftCms\Commerce\Transfer\Elements\Transfer`. +- Added `CraftCms\Commerce\Transfer\Queries\TransferQuery`. +- Added `CraftCms\Commerce\Transfer\Conditions\TransferCondition`. +- Added `CraftCms\Commerce\Transfer\FieldLayoutElements\TransferManagementField`. +- Added `CraftCms\Commerce\Transfer\Transfers`. +- Added `CraftCms\Commerce\Transfer\Models\TransferDetail`. +- Added `CraftCms\Commerce\Transfer\Records\Transfer`. +- Added `CraftCms\Commerce\Transfer\Records\TransferDetail`. +- Added `CraftCms\Commerce\Transfer\Enums\TransferStatusType` enum. +- Deprecated `craft\commerce\elements\Transfer`. `CraftCms\Commerce\Transfer\Elements\Transfer` should be used instead. +- Deprecated `craft\commerce\elements\db\TransferQuery`. `CraftCms\Commerce\Transfer\Queries\TransferQuery` should be used instead. +- Deprecated `craft\commerce\elements\conditions\transfers\TransferCondition`. `CraftCms\Commerce\Transfer\Conditions\TransferCondition` should be used instead. +- Deprecated `craft\commerce\fieldlayoutelements\TransferManagementField`. `CraftCms\Commerce\Transfer\FieldLayoutElements\TransferManagementField` should be used instead. +- Deprecated `craft\commerce\services\Transfers`. `CraftCms\Commerce\Transfer\Transfers` should be used instead. +- Deprecated `craft\commerce\models\TransferDetail`. `CraftCms\Commerce\Transfer\Models\TransferDetail` should be used instead. +- Deprecated `craft\commerce\enums\TransferStatusType`. `CraftCms\Commerce\Transfer\Enums\TransferStatusType` should be used instead. +- Removed `craft\commerce\records\Transfer`. `CraftCms\Commerce\Transfer\Records\Transfer` should be used instead. +- Removed `craft\commerce\records\TransferDetail`. `CraftCms\Commerce\Transfer\Records\TransferDetail` should be used instead. + +#### Controllers + +- Removed `craft\commerce\controllers\TransfersController`. `CraftCms\Commerce\Http\Controllers\TransfersController` should be used instead. + +### Users + +- Removed `craft\commerce\controllers\UsersController`. `CraftCms\Commerce\Http\Controllers\Users\UsersController` should be used instead. +- The Commerce tab on the Edit User screen is now built from `CraftCms\Cms\Http\Controllers\Users\EditUserTrait`; Commerce listens for `CraftCms\Cms\User\Events\EditUserScreensResolving` instead of the removed `craft\controllers\UsersController::EVENT_DEFINE_EDIT_SCREENS`. + +### Extensibility + +- Added `CraftCms\Commerce\Plugin`. `craft\commerce\Plugin` now extends this class instead of `craft\base\Plugin`. +- Added `Plugin::getPermissions()`, exposing Commerce's permissions as `CraftCms\Cms\User\Data\Permission` objects, including a dynamic `commerce-viewProductType:{uid}` permission (with nested create/save/delete permissions) per product type. +- Added `Plugin::getCpNavItem()`, building the Commerce CP nav item and its permission-gated subnav via `CraftCms\Cms\Cp\Data\NavItem` instead of the legacy array-based `getCpNavItem()` override. +- Added `CraftCms\Commerce\Console\Commands\Resave\ResaveProductsCommand`, `ResaveVariantsCommand`, `ResaveOrdersCommand`, and `ResaveCartsCommand`, registered as `craft:resave:products`, `craft:resave:variants`, `craft:resave:orders`, and `craft:resave:carts` (also picked up automatically by `craft:resave:all`). +- Added `CraftCms\Commerce\Support\ObjectState`, a `WeakMap`-backed per-instance state store used by the `Site`/`User`/`Address` `Macroable` macros above. +- Added GraphQL schema component and eager-loadable field registration via `CraftCms\Cms\Gql\Events\GqlSchemaComponentsResolving` and `GqlEagerLoadableFieldsResolving`. +- Added garbage collection registration via `CraftCms\Cms\GarbageCollection\Events\RunningGarbageCollection`, purging incomplete carts, orphaned variants, and partial Donation/Order/Product/Variant/Transfer elements. +- Added `craft.commerce`, `craft.orders`, `craft.products`, and `craft.variants` Twig variables via `CraftCms\Cms\Twig\Variables\CraftVariable::macro()`. +- Added `Plugin::getDonation()`, reachable in Twig as `craft.commerce.getDonation()`, matching the legacy `craft\commerce\plugin\Variables::getDonation()` trait method. +- The `commerce/products//`, `commerce/variants/`, and `commerce/inventory/transfers/` element-edit screens, previously Craft core's generic `elements/edit` action registered via a legacy `UrlManager` rule, are now registered directly in `routes/cp.php` against `CraftCms\Cms\Http\Controllers\Elements\EditElementController`. +- Registered Commerce's Twig extension via the `CraftCms\Cms\Support\Facades\Twig` facade. +- Added `CraftCms\Commerce\Order\Elements\Order::defineExporters()`, registering `CraftCms\Commerce\Order\Exporters\OrderExport` and `LineItemExport`. +- Added `CraftCms\Commerce\Base\Zone`. +- Added `CraftCms\Commerce\Base\ZoneInterface`. +- Added `CraftCms\Commerce\Database\Table`. +- Added `CraftCms\Commerce\Settings`. +- Added `CraftCms\Commerce\Exceptions\NotImplementedException`. +- Added `CraftCms\Commerce\Events\UpgradeEvent`. +- Added `CraftCms\Commerce\Twig\Extension`, replacing `craft\commerce\web\twig\Extension`. Registers the same `commerceCurrency`/`commercePaymentFormNamespace` filters and a `currentStore` global, now sourced from `CraftCms\Cms\Support\Facades\Sites::getCurrentSite()->getStore()` (the `Site::macro('getStore', ...)` registered in `Plugin::registerBehaviorMacros()`) instead of the removed `StoreBehavior`. +- Removed `craft\commerce\web\twig\CraftVariableBehavior` as it was unused under Craft 6 — the `craft.commerce`/`craft.orders`/`craft.products`/`craft.variants` Twig variables are already served by `NewCraftVariable::macro(...)` registrations in `Plugin::registerVariableMacros()`. +- Deprecated `craft\commerce\base\Zone`. `CraftCms\Commerce\Base\Zone` should be used instead. +- Deprecated `craft\commerce\base\ZoneInterface`. `CraftCms\Commerce\Base\ZoneInterface` should be used instead. +- Deprecated `craft\commerce\db\Table`. `CraftCms\Commerce\Database\Table` should be used instead. +- Deprecated `craft\commerce\models\Settings`. `CraftCms\Commerce\Settings` should be used instead. +- Deprecated `craft\commerce\errors\NotImplementedException`. `CraftCms\Commerce\Exceptions\NotImplementedException` should be used instead. +- Deprecated `craft\commerce\events\UpgradeEvent`. `CraftCms\Commerce\Events\UpgradeEvent` should be used instead. +- Deprecated `craft\commerce\events\*`. Cancelable Commerce events (previously extending `craft\events\CancelableEvent`) now use the `CraftCms\Cms\Shared\Concerns\ValidatableEvent` trait instead. +- Removed `craft\commerce\web\twig\Extension`. `CraftCms\Commerce\Twig\Extension` should be used instead. +- Removed `craft\commerce\models\Settings::VIEW_URI_CUSTOMERS`, `VIEW_URI_PROMOTIONS`, `VIEW_URI_SHIPPING`, and `VIEW_URI_TAX` constants. +- Improved `craft\commerce\Plugin`'s `Plugin::getInstance()->getX()` service getters to be backed by a lazy-instantiate-and-cache trait rather than Yii2's component locator. + +### System + +- Raised `Plugin::$minVersionRequired` from `3.4.11` to `5.7.3` (the latest 5.x patch release, kept in sync as new patches ship until 6.0 stable). Installs must be on at least that version before updating to Commerce 6.0. +- Removed the Commerce Yii2 debug panel and all related classes (`CommercePanel`, `DebugPanel` helper, `CommerceDebugPanelDataEvent`, and its Twig views) — the Yii2 debug module they relied on no longer exists in Craft CMS 6. +- Added `getPriceAsCurrency()` to `CraftCms\Commerce\Shipping\Models\ShippingMethodOption` and `getAmountAsCurrency()` to `CraftCms\Commerce\Order\Models\OrderAdjustment` (the latter used repeatedly in the shipped `example-templates/`), closing out the rest of the `CurrencyAttributeBehavior` removal — third-party templates/plugins could still call these via the legacy behavior's magic `__call`, independent of whether Commerce's own code used them. +- Added `CraftCms\Commerce\Plugin::HANDLE`, replacing the `Plugin::getInstance()->handle` runtime lookup at its one call site (`Catalog\Products::afterSaveSiteHandler()`), which was also fixed to reference the new `CraftCms\Commerce\Plugin` instead of the legacy `craft\commerce\Plugin`. +- Moved CKEditor's product/variant rich-text link options registration from `craft\commerce\Plugin::boot()` to `CraftCms\Commerce\Plugin::registerCKEditorLinkOptions()`. Dropped Redactor support entirely, since the Redactor plugin isn't supported under Craft 6. +- Reorganized every domain's `Records\*`/`Models\*` split under `src/` into a consistent two-namespace convention: `Models\*` is now Eloquent persistence models only, and `Data\*` holds everything else (plain data/config objects). `Records\*` is gone. +- Moved `CraftCms\Commerce\Tax\Models\EuVatIdValidator` to `CraftCms\Commerce\Tax\VatValidator\Eu`. +- Moved `craft\commerce\Plugin::_registerProjectConfigEventListeners()` to `CraftCms\Commerce\Plugin::registerProjectConfigEventListeners()`, using `CraftCms\Cms\ProjectConfig\ProjectConfig::onAdd()`/`onUpdate()`/`onRemove()` and `CraftCms\Cms\ProjectConfig\Events\ConfigEvent` instead of their legacy Yii2 equivalents. `ProductTypes::pruneDeletedSite()` now listens for the Laravel `SiteDeleted` event instead of `craft\services\Sites::EVENT_AFTER_DELETE_SITE`. +- Moved `craft\commerce\Plugin::_registerPoweredByHeader()` to a real Laravel middleware, `CraftCms\Commerce\Http\Middleware\PoweredByHeader`, pushed onto the `craft` middleware group. +- Added `CraftCms\Commerce\Plugin\Concerns\HasCommerceEditions` and moved `$schemaVersion`, `$minVersionRequired`, `$hasCpSettings`, and `$hasReadOnlyCpSettings` from `craft\commerce\Plugin` onto `CraftCms\Commerce\Plugin`. +- `craft\commerce\Plugin` is now a pure `class_alias` shim for `CraftCms\Commerce\Plugin`. Its custom `boot()`/method overrides and the `craft\commerce\plugin\Routes` trait have been ported onto `CraftCms\Commerce\Plugin` or dropped as dead/redundant code (`_registerGqlInterfaces()`/`_registerGqlQueries()`, `beforeInstall()`'s version guards, the `@commerceLib` alias). +- Split `CraftCms\Commerce\Plugin` into two more `Concerns` traits: `Plugin\Concerns\HasCommerceMacros` (all `Macroable` macro registration) and `Plugin\Concerns\HasCommerceEventListeners` (all event listener registration). +- Moved the 8 listeners previously registered imperatively in `registerCraftEventListeners()` into 8 dedicated classes under `CraftCms\Commerce\Plugin\Listeners\*`, registered declaratively via `Plugin::$events`. +- Replaced every `Plugin::getInstance()` call in `src/` with dependency injection. +- Removed `CraftCms\Commerce\CatalogPricing\CatalogPricing::afterSavePurchasableHandler()` (and its legacy `craft\commerce\services\CatalogPricing` pass-through) as it was deprecated since 5.5.0. +- Changed `CatalogPricing::generateCatalogPrices()`'s `bool $showConsoleOutput` parameter to `?\Symfony\Component\Console\Output\OutputInterface $output`. +- Updated every `CraftCms\Commerce\*\Conditions\*ConditionRule` for cms-6's nestable-condition-groups rework: `ElementQueryConditionRuleInterface::modifyQuery()` now takes `(Builder $query, ElementQueryInterface $elementQuery)` instead of a single `ElementQueryInterface $query`. Added `CatalogPricingCondition::createGroup()`, required by the new `ConditionInterface` contract. +- Fixed several call sites (`Order::validateAddressesInMarketAddressCondition()`, `Order::modifyCustomSource()`, `Gateway::hasOrderCondition()`/`hasBillingAddressCondition()`/`hasShippingAddressCondition()`, `CatalogPricingCondition::modifyQuery()`) that assumed `getConditionRules()` returned an array, now that it returns a `ConditionGroupInterface` object. +- Removed `GatewayOrderCondition::getBuilderHtml()` and `Address\Conditions\GatewayAddressCondition::getBuilderHtml()`, as `ConditionInterface::getBuilderHtml()` no longer exists; rendering now goes through `CraftCms\Cms\Condition\ConditionBuilderRenderer`. +- Ported every condition rule's custom input from the removed `inputHtml()`/`elementSelectConfig()`/`inputOptions()` HTML-string methods to the new Form API's `inputNodes()`. Third-party condition rules overriding these methods must be updated accordingly. + +### Translations + +- Moved `src-yii2/translations/` to a top-level `lang/` directory (e.g. `lang/en/commerce.php`, `lang/de/commerce.php`), matching the Laravel convention `CraftCms\Cms\Plugin\Concerns\HasTranslations` looks for (`dirname($plugin->getBasePath()).'/lang'`) ahead of the legacy `src/translations` fallback. Message file structure and content are unchanged. +- Updated `crowdin.yml`'s `base_path` from `/src/translations` to `/lang` to match. + +### Testing + +- Added a Pest/Orchestra Testbench harness under `tests/` (`TestCase`, `UnitTestCase`, `Pest.php`, `Feature/`, `Unit/`, `Arch/`) for testing `CraftCms\Commerce\` code in `src/`. Run via `composer run tests`. +- Added `CraftCms\Commerce\Product\Variant\Elements\VariantCollection`. +- Deprecated `craft\commerce\elements\VariantCollection`. +- Added `CraftCms\Commerce\Order\DeletionBlockers\OrderCustomersDeletionBlocker`. Deprecated `craft\commerce\elements\deletionblockers\OrderCustomersDeletionBlocker`. +- Removed the remaining `Craft::createObject()` calls from `src/`, replaced with direct `new X()` construction. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 53d6661684..a612fce0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ ## 5.7.3 - 2026-09-02 -- Fixed a bug where `craft\commerce\elements\Order::setShippingAddress()` and `setBillingAddress()` weren’t setting custom field values. ([#4353](https://github.com/craftcms/commerce/issues/4353)) -- Fixed a bug where variants’ auto-generated SKUs would be incorrect if the SKU Format contained `{id}`. +- Fixed a bug where `craft\commerce\elements\Order::setShippingAddress()` and `setBillingAddress()` weren't setting custom field values. ([#4353](https://github.com/craftcms/commerce/issues/4353)) +- Fixed a bug where variants' auto-generated SKUs would be incorrect if the SKU Format contained `{id}`. - Fixed a bug where adding a new site via project config apply could cause additional project config changes. ([#4348](https://github.com/craftcms/commerce/issues/4348)) - Fixed a deprecation warning that was getting logged when accessing `/admin/commerce`. ([#4349](https://github.com/craftcms/commerce/issues/4349)) diff --git a/codeception.yml b/codeception.yml deleted file mode 100644 index bf3ef1f863..0000000000 --- a/codeception.yml +++ /dev/null @@ -1,46 +0,0 @@ -actor: Tester -paths: - tests: tests - log: tests/_output - output: tests/_output - data: tests/_data - support: tests/_support - envs: tests/_envs -bootstrap: _bootstrap.php -coverage: - enabled: true - include: - - src/* - exclude: - - src/etc/* - - src/migrations/* - - src/templates/* - - src/translations/* - - src/web/assets/* - - docs/* - - templates/* - - tests/* - - vendor/* -params: - - env - - tests/.env -modules: - config: - \craft\test\Craft: - configFile: 'tests/_craft/config/test.php' - entryUrl: 'http://test.craftcms.test/index.php' - projectConfig: {} - migrations: [] - plugins: - commerce: - class: '\craft\commerce\Plugin' - handle: commerce - cleanup: true - transaction: true - dbSetup: {clean: true, setupCraft: true} - fullMock: false -groups: - elements: [tests/unit/elements] - models: [tests/unit/models] - services: [tests/unit/services] - gql: [tests/unit/gql] diff --git a/composer.json b/composer.json index a1b36c7b01..d6d47fac53 100644 --- a/composer.json +++ b/composer.json @@ -21,34 +21,38 @@ "minimum-stability": "dev", "prefer-stable": true, "require": { - "php": "^8.2", - "craftcms/cms": "^5.10.0", - "dompdf/dompdf": "^2.0.2 || ^3.0", + "php": "^8.5", + "craftcms/cms": "dev-feature/6.x-form-component-compatibility-and-usage-updates", + "dompdf/dompdf": "^3.1.6", "ibericode/vat": "^2.0", "iio/libmergepdf": "^4.0", "moneyphp/money": "^4.2.0" }, "require-dev": { - "codeception/codeception": "^5.0.11", - "codeception/module-asserts": "^3.0.0", - "codeception/module-datafactory": "^3.0.0", - "codeception/module-phpbrowser": "^3.0.0", - "codeception/module-rest": "^3.3.2", - "codeception/module-yii2": "^1.1.9", - "craftcms/ckeditor": "^4.0.0", - "craftcms/redactor": "*", "craftcms/ecs": "dev-main", - "craftcms/phpstan": "dev-main", + "craftcms/yii2-adapter": "6.x-dev", + "dg/bypass-finals": "^1.9", "fakerphp/faker": "^1.19.0", + "larastan/larastan": "^3.4", "league/factory-muffin": "^3.3.0", - "phpstan/phpstan": "^1.10.56", - "vlucas/phpdotenv": "^5.4.1", - "craftcms/rector": "dev-main" + "orchestra/testbench": "^11.0", + "pestphp/pest": "^4.0", + "pestphp/pest-plugin-arch": "^4.0", + "pestphp/pest-plugin-laravel": "^4.0", + "phpstan/phpstan": "^2.1", + "rector/rector": "^2.0", + "vlucas/phpdotenv": "^5.4.1" }, "autoload": { "psr-4": { - "craft\\commerce\\": "src/", - "craftcommercetests\\fixtures\\": "tests/fixtures/" + "craft\\commerce\\": "src-yii2/", + "CraftCms\\Commerce\\": "src/", + "craftcommercetests\\fixtures\\": "tests-yii2/fixtures/" + } + }, + "autoload-dev": { + "psr-4": { + "CraftCms\\Commerce\\Tests\\": "tests/" } }, "extra": { @@ -62,16 +66,23 @@ "check-cs": "ecs check --ansi", "fix-cs": "ecs check --ansi --fix", "phpstan": "phpstan --memory-limit=1G", - "testunit": [ + "tests": [ "Composer\\Config::disableProcessTimeout", - "codecept run unit" - ] + "./vendor/bin/pest --compact" + ], + "post-autoload-dump": [ + "@clear", + "@prepare" + ], + "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "prepare": "@php vendor/bin/testbench package:discover --ansi" }, "config": { "sort-packages": true, "allow-plugins": { "yiisoft/yii2-composer": true, - "craftcms/plugin-installer": true + "craftcms/plugin-installer": true, + "pestphp/pest-plugin": true } } } diff --git a/composer.lock b/composer.lock index 4a615231e6..c08bd01f45 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cceec5dfa88beb3ef42037526eac053d", + "content-hash": "2f4612d50e83bc3840bdd89cbe9b1227", "packages": [ { "name": "bacon/bacon-qr-code", @@ -63,16 +63,16 @@ }, { "name": "brick/math", - "version": "0.17.2", + "version": "0.19.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "8189e751995f9e15729c1aa2f89fa8f166ffe818" + "reference": "a89bc96a7cf3d7b59e725afe57ccb95eb03cf6ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/8189e751995f9e15729c1aa2f89fa8f166ffe818", - "reference": "8189e751995f9e15729c1aa2f89fa8f166ffe818", + "url": "https://api.github.com/repos/brick/math/zipball/a89bc96a7cf3d7b59e725afe57ccb95eb03cf6ce", + "reference": "a89bc96a7cf3d7b59e725afe57ccb95eb03cf6ce", "shasum": "" }, "require": { @@ -110,7 +110,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.17.2" + "source": "https://github.com/brick/math/tree/0.19.1" }, "funding": [ { @@ -118,20 +118,20 @@ "type": "github" } ], - "time": "2026-05-25T20:34:43+00:00" + "time": "2026-08-08T23:03:16+00:00" }, { "name": "carbonphp/carbon-doctrine-types", - "version": "3.2.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", - "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + "reference": "5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", - "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc", + "reference": "5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc", "shasum": "" }, "require": { @@ -146,6 +146,11 @@ "phpunit/phpunit": "^10.3" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, "autoload": { "psr-4": { "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" @@ -171,7 +176,7 @@ ], "support": { "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", - "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.1" }, "funding": [ { @@ -187,88 +192,24 @@ "type": "tidelift" } ], - "time": "2024-02-09T16:56:22+00:00" - }, - { - "name": "cebe/markdown", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/cebe/markdown.git", - "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/cebe/markdown/zipball/9bac5e971dd391e2802dca5400bbeacbaea9eb86", - "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86", - "shasum": "" - }, - "require": { - "lib-pcre": "*", - "php": ">=5.4.0" - }, - "require-dev": { - "cebe/indent": "*", - "facebook/xhprof": "*@dev", - "phpunit/phpunit": "4.1.*" - }, - "bin": [ - "bin/markdown" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-4": { - "cebe\\markdown\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc", - "homepage": "http://cebe.cc/", - "role": "Creator" - } - ], - "description": "A super fast, highly extensible markdown parser for PHP", - "homepage": "https://github.com/cebe/markdown#readme", - "keywords": [ - "extensible", - "fast", - "gfm", - "markdown", - "markdown-extra" - ], - "support": { - "issues": "https://github.com/cebe/markdown/issues", - "source": "https://github.com/cebe/markdown" - }, - "time": "2018-03-26T11:24:36+00:00" + "time": "2026-09-06T14:11:38+00:00" }, { "name": "commerceguys/addressing", - "version": "v2.2.5", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/commerceguys/addressing.git", - "reference": "15b789e5e6ededaf803d23c56cdc300a94522a7c" + "reference": "504ed1765d7a6788830c197390879d677f54d868" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/commerceguys/addressing/zipball/15b789e5e6ededaf803d23c56cdc300a94522a7c", - "reference": "15b789e5e6ededaf803d23c56cdc300a94522a7c", + "url": "https://api.github.com/repos/commerceguys/addressing/zipball/504ed1765d7a6788830c197390879d677f54d868", + "reference": "504ed1765d7a6788830c197390879d677f54d868", "shasum": "" }, "require": { - "doctrine/collections": "^1.6 || ^2.0", + "doctrine/collections": "^1.6 || ^2.0 || ^3.0", "php": ">=8.0" }, "require-dev": { @@ -313,34 +254,35 @@ ], "support": { "issues": "https://github.com/commerceguys/addressing/issues", - "source": "https://github.com/commerceguys/addressing/tree/v2.2.5" + "source": "https://github.com/commerceguys/addressing/tree/v2.3.1" }, - "time": "2026-01-05T13:00:32+00:00" + "time": "2026-08-23T14:17:54+00:00" }, { "name": "composer/pcre", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -378,7 +320,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -388,13 +330,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2026-06-07T11:47:49+00:00" }, { "name": "composer/semver", @@ -475,28 +413,31 @@ }, { "name": "craftcms/cms", - "version": "5.10.10", + "version": "6.x-dev", "source": { "type": "git", "url": "https://github.com/craftcms/cms.git", - "reference": "d4c5f814e59de56a3124776da6b3c399cb28d64f" + "reference": "7d89b7c43132bdd9ea1642e18b4d44c8049ef3a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/cms/zipball/d4c5f814e59de56a3124776da6b3c399cb28d64f", - "reference": "d4c5f814e59de56a3124776da6b3c399cb28d64f", + "url": "https://api.github.com/repos/craftcms/cms/zipball/7d89b7c43132bdd9ea1642e18b4d44c8049ef3a6", + "reference": "7d89b7c43132bdd9ea1642e18b4d44c8049ef3a6", "shasum": "" }, "require": { "bacon/bacon-qr-code": "^3.0", "commerceguys/addressing": "^2.1.1", "composer/semver": "^3.3.2", + "craftcms/cms-assets": "self.version", + "craftcms/laravel-aliases": "^2.0", + "craftcms/laravel-dependency-aware-cache": "^1.2.3", + "craftcms/laravel-ruleset-validation": "^1.1", "craftcms/plugin-installer": "~1.6.0", - "craftcms/server-check": "~5.1.0", + "craftcms/server-check": "~6.0.0", "craftcms/url-validator": "^1.0", - "creocoder/yii2-nested-sets": "~0.9.0", - "elvanto/litemoji": "~4.3.0", - "enshrined/svg-sanitize": "~0.22.0", + "elvanto/litemoji": "^5.2.0", + "enshrined/svg-sanitize": "^1.0.0", "ext-bcmath": "*", "ext-curl": "*", "ext-dom": "*", @@ -508,72 +449,129 @@ "ext-pdo": "*", "ext-zip": "*", "guzzlehttp/guzzle": "^7.2.0", - "illuminate/collections": "^v10.42.0", - "illuminate/support": "^10.49", + "inertiajs/inertia-laravel": "^3.0", + "intervention/image": "^4.2", + "laravel/framework": "^13.17", + "laravel/wayfinder": "^0.1.12", + "league/commonmark": "^2.8", + "league/flysystem-path-prefixing": "^3.31", "league/uri": "^7.0", - "mikehaertl/php-shellcommand": "^1.6.3", "moneyphp/money": "^4.0", - "monolog/monolog": "^3.0", - "php": "^8.2", - "phpdocumentor/reflection-docblock": "^5.3", + "php": "^8.5", "phpoffice/phpspreadsheet": "^5.3", - "pixelandtonic/graphql-php": "~14.11.10.1", - "pixelandtonic/imagine": "~1.5.2.1", - "pragmarx/google2fa": "^8.0", + "pragmarx/google2fa": "^9.0", "pragmarx/recovery": "^0.2.1", - "samdark/yii2-psr-log-target": "^1.1.3", - "seld/cli-prompt": "^1.0.4", - "symfony/css-selector": "^6.0|^7.0", - "symfony/dom-crawler": "^6.0|^7.0", - "symfony/filesystem": "^6.3", - "symfony/http-client": "^6.0.3|^7.0", - "symfony/property-access": "^7.0", - "symfony/property-info": "^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/var-dumper": "^5.0|^6.0|^7.0", - "symfony/yaml": "^5.2.3|^6.0|^7.0", + "symfony/css-selector": "^7.0|^8.0", + "symfony/dom-crawler": "^7.0|^8.0", + "symfony/filesystem": "^7.0|^8.0", + "symfony/html-sanitizer": "^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/yaml": "^7.0|^8.0", "theiconic/name-parser": "^1.2", - "twig/twig": "~3.27.0", + "tpetry/laravel-query-expressions": "^1.5", + "twig/twig": "~3.28.0", "voku/portable-ascii": "^2.0", "web-auth/webauthn-lib": "~5.3.5", - "yiisoft/yii2": "~2.0.55.0", - "yiisoft/yii2-debug": "~2.1.27.0", - "yiisoft/yii2-queue": "~2.3.2", - "yiisoft/yii2-symfonymailer": "^4.0.0" - }, - "provide": { - "bower-asset/inputmask": "5.0.9", - "bower-asset/jquery": "3.6.1", - "bower-asset/punycode": "^2.2", - "bower-asset/yii2-pjax": "~2.0.1", - "yii2tech/ar-softdelete": "1.0.4" + "webonyx/graphql-php": "~15.33.1", + "yiisoft/arrays": "^3.2", + "yiisoft/html": "^4.1", + "yiisoft/translator": "^3.2", + "yiisoft/translator-message-php": "^1.1" }, "require-dev": { - "codeception/codeception": "^5.2.0", - "codeception/lib-innerbrowser": "4.0.1", - "codeception/module-asserts": "^3.0.0", - "codeception/module-datafactory": "^3.0.0", - "codeception/module-phpbrowser": "^3.0.0", - "codeception/module-rest": "^3.3.2", - "codeception/module-yii2": "^1.1.9", - "craftcms/ecs": "dev-main", + "barryvdh/laravel-debugbar": "^4.2", + "craftcms/test-plugin": "1.0.1", + "dg/bypass-finals": "^1.9", + "driftingly/rector-laravel": "^2.1", "fakerphp/faker": "^1.19.0", - "league/factory-muffin": "^3.3.0", + "intervention/image-driver-vips": "^4.1", + "larastan/larastan": "^3.12", + "laravel/boost": "^2.6", + "laravel/pao": "^1.0", + "laravel/pint": "^1.22", + "laravel/socialite": "^5.23", + "orchestra/testbench": "^11.0", + "pestphp/pest": "^5.0", + "pestphp/pest-plugin-agent": "^5.0", + "pestphp/pest-plugin-arch": "^5.0", + "pestphp/pest-plugin-laravel": "^5.0", "phpstan/phpstan": "^2.1", - "rector/rector": "^2.0", - "vlucas/phpdotenv": "^5.4.1", - "yiisoft/yii2-redis": "^2.0" + "rector/rector": "^2.3", + "spatie/laravel-typescript-transformer": "^3.2" }, "suggest": { "ext-exif": "Adds support for parsing image EXIF data.", "ext-iconv": "Adds support for more character encodings than PHP’s built-in mb_convert_encoding() function, which Craft will take advantage of when converting strings to UTF-8.", - "ext-imagick": "Adds support for more image processing formats and options." + "ext-imagick": "Adds support for more image processing formats and options.", + "intervention/image-driver-vips": "Adds support for image processing with libvips (requires ext-ffi and libvips).", + "laravel/socialite": "Adds OAuth login support for providers configured with `GeneralConfig::oauthProviders()`." }, "type": "library", + "extra": { + "laravel": { + "aliases": { + "Gql": "CraftCms\\Cms\\Support\\Facades\\Gql", + "I18N": "CraftCms\\Cms\\Support\\Facades\\I18N", + "Path": "CraftCms\\Cms\\Support\\Facades\\Path", + "Twig": "CraftCms\\Cms\\Support\\Facades\\Twig", + "OAuth": "CraftCms\\Cms\\Support\\Facades\\OAuth", + "Sites": "CraftCms\\Cms\\Support\\Facades\\Sites", + "Users": "CraftCms\\Cms\\Support\\Facades\\Users", + "Assets": "CraftCms\\Cms\\Support\\Facades\\Assets", + "Drafts": "CraftCms\\Cms\\Support\\Facades\\Drafts", + "Fields": "CraftCms\\Cms\\Support\\Facades\\Fields", + "Images": "CraftCms\\Cms\\Support\\Facades\\Images", + "Search": "CraftCms\\Cms\\Support\\Facades\\Search", + "BulkOps": "CraftCms\\Cms\\Support\\Facades\\BulkOps", + "Entries": "CraftCms\\Cms\\Support\\Facades\\Entries", + "Folders": "CraftCms\\Cms\\Support\\Facades\\Folders", + "Plugins": "CraftCms\\Cms\\Support\\Facades\\Plugins", + "Updates": "CraftCms\\Cms\\Support\\Facades\\Updates", + "Volumes": "CraftCms\\Cms\\Support\\Facades\\Volumes", + "Elements": "CraftCms\\Cms\\Support\\Facades\\Elements", + "Markdown": "CraftCms\\Cms\\Support\\Facades\\Markdown", + "Sections": "CraftCms\\Cms\\Support\\Facades\\Sections", + "Security": "CraftCms\\Cms\\Support\\Facades\\Security", + "Template": "CraftCms\\Cms\\Support\\Facades\\Template", + "Addresses": "CraftCms\\Cms\\Support\\Facades\\Addresses", + "HtmlStack": "CraftCms\\Cms\\Support\\Facades\\HtmlStack", + "Revisions": "CraftCms\\Cms\\Support\\Facades\\Revisions", + "Activities": "CraftCms\\Cms\\Support\\Facades\\Activities", + "Conditions": "CraftCms\\Cms\\Support\\Facades\\Conditions", + "Deprecator": "CraftCms\\Cms\\Support\\Facades\\Deprecator", + "EntryTypes": "CraftCms\\Cms\\Support\\Facades\\EntryTypes", + "SiteGroups": "CraftCms\\Cms\\Support\\Facades\\SiteGroups", + "Structures": "CraftCms\\Cms\\Support\\Facades\\Structures", + "UserGroups": "CraftCms\\Cms\\Support\\Facades\\UserGroups", + "AuthMethods": "CraftCms\\Cms\\Support\\Facades\\AuthMethods", + "Filesystems": "CraftCms\\Cms\\Support\\Facades\\Filesystems", + "JobProgress": "CraftCms\\Cms\\Support\\Facades\\JobProgress", + "AssetIndexer": "CraftCms\\Cms\\Support\\Facades\\AssetIndexer", + "DeltaRegistry": "CraftCms\\Cms\\Support\\Facades\\DeltaRegistry", + "ElementCaches": "CraftCms\\Cms\\Support\\Facades\\ElementCaches", + "ProjectConfig": "CraftCms\\Cms\\Support\\Facades\\ProjectConfig", + "TemplateHooks": "CraftCms\\Cms\\Support\\Facades\\TemplateHooks", + "ElementActions": "CraftCms\\Cms\\Support\\Facades\\ElementActions", + "ElementSources": "CraftCms\\Cms\\Support\\Facades\\ElementSources", + "HtmlSanitizers": "CraftCms\\Cms\\Support\\Facades\\HtmlSanitizers", + "InputNamespace": "CraftCms\\Cms\\Support\\Facades\\InputNamespace", + "ElementActivity": "CraftCms\\Cms\\Support\\Facades\\ElementActivity", + "ImageTransforms": "CraftCms\\Cms\\Support\\Facades\\ImageTransforms", + "ResponseHeaders": "CraftCms\\Cms\\Support\\Facades\\ResponseHeaders", + "UserPermissions": "CraftCms\\Cms\\Support\\Facades\\UserPermissions", + "ElementExporters": "CraftCms\\Cms\\Support\\Facades\\ElementExporters" + }, + "providers": [ + "CraftCms\\Cms\\Providers\\CraftServiceProvider" + ] + } + }, "autoload": { + "files": [ + "src/helpers.php" + ], "psr-4": { - "craft\\": "src/", - "yii2tech\\ar\\softdelete\\": "lib/ar-softdelete/src/" + "CraftCms\\Cms\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -591,7 +589,7 @@ "keywords": [ "cms", "craftcms", - "yii2" + "laravel" ], "support": { "docs": "https://craftcms.com/docs/5.x/", @@ -601,131 +599,214 @@ "rss": "https://github.com/craftcms/cms/releases.atom", "source": "https://github.com/craftcms/cms" }, - "time": "2026-07-08T22:13:52+00:00" + "time": "2026-09-15T10:22:17+00:00" }, { - "name": "craftcms/plugin-installer", - "version": "1.6.0", + "name": "craftcms/cms-assets", + "version": "6.x-dev", "source": { "type": "git", - "url": "https://github.com/craftcms/plugin-installer.git", - "reference": "bd1650e8da6d5ca7a8527068d3e51c34bc7b6b4f" + "url": "https://github.com/craftcms/cms-assets.git", + "reference": "983f260b06918498f322e08d455d67c62e70bf73" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/plugin-installer/zipball/bd1650e8da6d5ca7a8527068d3e51c34bc7b6b4f", - "reference": "bd1650e8da6d5ca7a8527068d3e51c34bc7b6b4f", + "url": "https://api.github.com/repos/craftcms/cms-assets/zipball/983f260b06918498f322e08d455d67c62e70bf73", + "reference": "983f260b06918498f322e08d455d67c62e70bf73", + "shasum": "" + }, + "default-branch": true, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "proprietary" + ], + "description": "Built assets for Craft CMS.", + "support": { + "source": "https://github.com/craftcms/cms-assets/tree/6.x" + }, + "time": "2026-09-15T10:24:35+00:00" + }, + { + "name": "craftcms/laravel-aliases", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/craftcms/laravel-aliases.git", + "reference": "734ea28e7fc7acfc3988f64475b416ecbc46419e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/craftcms/laravel-aliases/zipball/734ea28e7fc7acfc3988f64475b416ecbc46419e", + "reference": "734ea28e7fc7acfc3988f64475b416ecbc46419e", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.4" + "illuminate/contracts": "^12.0||^13.0", + "php": "^8.2", + "yiisoft/aliases": "^3.0" }, "require-dev": { - "composer/composer": "^1.0 || ^2.0" + "larastan/larastan": "^2.9||^3.0", + "laravel/pint": "^1.14", + "nunomaduro/collision": "^8.1.1||^7.10.0", + "orchestra/testbench": "^10.0.0||^11.0", + "pestphp/pest": "^4.0", + "pestphp/pest-plugin-arch": "^4.0", + "pestphp/pest-plugin-laravel": "^4.0", + "phpstan/extension-installer": "^1.3||^2.0", + "phpstan/phpstan-deprecation-rules": "^1.1||^2.0", + "phpstan/phpstan-phpunit": "^1.3||^2.0" }, - "type": "composer-plugin", + "type": "library", "extra": { - "class": "craft\\composer\\Plugin" + "laravel": { + "aliases": { + "Aliases": "CraftCms\\Aliases\\Facades\\Aliases" + }, + "providers": [ + "CraftCms\\Aliases\\AliasesServiceProvider" + ] + } }, "autoload": { "psr-4": { - "craft\\composer\\": "src/" + "CraftCms\\Aliases\\": "src/", + "CraftCms\\Aliases\\Database\\Factories\\": "database/factories/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Craft CMS Plugin Installer", - "homepage": "https://craftcms.com/", + "authors": [ + { + "name": "Pixel & Tonic", + "homepage": "https://pixelandtonic.com/" + } + ], + "description": "A Laravel wrapper around yiisoft/aliases", + "homepage": "https://github.com/craftcms/laravel-aliases", "keywords": [ - "cms", - "composer", "craftcms", - "installer", - "plugin" + "laravel", + "laravel-aliases", + "yii" ], "support": { - "docs": "https://craftcms.com/docs", - "email": "support@craftcms.com", - "forum": "https://craftcms.stackexchange.com/", - "issues": "https://github.com/craftcms/cms/issues?state=open", - "rss": "https://craftcms.com/changelog.rss", - "source": "https://github.com/craftcms/cms" + "issues": "https://github.com/craftcms/laravel-aliases/issues", + "source": "https://github.com/craftcms/laravel-aliases/tree/2.1.0" }, - "time": "2023-02-22T13:17:00+00:00" + "time": "2026-03-17T14:59:00+00:00" }, { - "name": "craftcms/server-check", - "version": "5.1.0", + "name": "craftcms/laravel-dependency-aware-cache", + "version": "1.2.3", "source": { "type": "git", - "url": "https://github.com/craftcms/server-check.git", - "reference": "7a4f1720c4fe1f0731254a82e63060a217f51cdc" + "url": "https://github.com/craftcms/laravel-dependency-aware-cache.git", + "reference": "5d5352c6a56b901d1c61f87bf912301785c7f350" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/server-check/zipball/7a4f1720c4fe1f0731254a82e63060a217f51cdc", - "reference": "7a4f1720c4fe1f0731254a82e63060a217f51cdc", + "url": "https://api.github.com/repos/craftcms/laravel-dependency-aware-cache/zipball/5d5352c6a56b901d1c61f87bf912301785c7f350", + "reference": "5d5352c6a56b901d1c61f87bf912301785c7f350", "shasum": "" }, + "require": { + "illuminate/cache": "^12.0|^13.0", + "illuminate/contracts": "^12.0||^13.0", + "php": "^8.2" + }, + "require-dev": { + "larastan/larastan": "^2.9||^3.0", + "laravel/pint": "^1.14", + "nunomaduro/collision": "^8.1.1||^7.10.0", + "orchestra/testbench": "^10.0.0||^9.0.0||^8.22.0||^9.0||^10.0||^11.0", + "pestphp/pest": "^4.0", + "pestphp/pest-plugin-arch": "^v4.0.0", + "pestphp/pest-plugin-laravel": "^v4.1.0", + "phpstan/extension-installer": "^1.3||^2.0", + "phpstan/phpstan-deprecation-rules": "^1.1||^2.0", + "phpstan/phpstan-phpunit": "^1.3||^2.0" + }, "type": "library", + "extra": { + "laravel": { + "aliases": { + "DependencyCache": "CraftCms\\DependencyAwareCache\\Facades\\DependencyCache" + }, + "providers": [ + "CraftCms\\DependencyAwareCache\\CacheServiceProvider" + ] + } + }, "autoload": { - "classmap": [ - "server/requirements" - ] + "psr-4": { + "CraftCms\\DependencyAwareCache\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "Craft CMS Server Check", - "homepage": "https://craftcms.com/", + "authors": [ + { + "name": "Pixel & Tonic", + "homepage": "https://pixelandtonic.com/" + } + ], + "description": "A dependency aware cache repository for Laravel", + "homepage": "https://github.com/craftcms/laravel-dependency-aware-cache", "keywords": [ - "cms", + "cache", "craftcms", - "requirements", - "yii2" + "laravel" ], "support": { - "docs": "https://github.com/craftcms/docs", - "email": "support@craftcms.com", - "forum": "https://craftcms.stackexchange.com/", - "issues": "https://github.com/craftcms/server-check/issues?state=open", - "rss": "https://github.com/craftcms/server-check/releases.atom", - "source": "https://github.com/craftcms/server-check" + "issues": "https://github.com/craftcms/laravel-dependency-aware-cache/issues", + "source": "https://github.com/craftcms/laravel-dependency-aware-cache/tree/1.2.3" }, - "time": "2026-04-07T16:48:35+00:00" + "time": "2026-05-01T12:08:11+00:00" }, { - "name": "craftcms/url-validator", + "name": "craftcms/laravel-ruleset-validation", "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/craftcms/url-validator.git", - "reference": "3918c21317a856d6313b3fbf40d70ce013cdd715" + "url": "https://github.com/craftcms/laravel-ruleset-validation.git", + "reference": "5fd54f4643f4746d6ac662bbec13aa225f62edfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/url-validator/zipball/3918c21317a856d6313b3fbf40d70ce013cdd715", - "reference": "3918c21317a856d6313b3fbf40d70ce013cdd715", + "url": "https://api.github.com/repos/craftcms/laravel-ruleset-validation/zipball/5fd54f4643f4746d6ac662bbec13aa225f62edfb", + "reference": "5fd54f4643f4746d6ac662bbec13aa225f62edfb", "shasum": "" }, "require": { - "ext-filter": "*", - "php": "^8.0.2" + "illuminate/contracts": "^13.0", + "illuminate/support": "^13.0", + "illuminate/validation": "^13.0", + "php": "^8.4" }, "require-dev": { - "laravel/pint": "^1.29", - "nunomaduro/collision": "^8.1", - "pestphp/pest": "^3.0", - "phpstan/phpstan": "^2.0" + "larastan/larastan": "^3.0", + "laravel/pint": "^v1.29", + "nunomaduro/collision": "^8.1.1", + "orchestra/testbench": "^11.0", + "pestphp/pest": "^4.0" }, "type": "library", + "extra": { + "laravel": { + "providers": [ + "CraftCms\\RulesetValidation\\RulesetValidationServiceProvider" + ] + } + }, "autoload": { "psr-4": { - "CraftCms\\UrlValidator\\": "src/" + "CraftCms\\RulesetValidation\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -738,65 +819,170 @@ "homepage": "https://pixelandtonic.com/" } ], - "description": "Validate URLs and IP addresses against SSRF, DNS rebinding, and cloud-metadata attacks.", - "homepage": "https://github.com/craftcms/url-validator", + "description": "Validate requests and objects with reusable Laravel rulesets.", + "homepage": "https://github.com/craftcms/laravel-ruleset-validation", "keywords": [ - "IP", "craftcms", - "security", - "ssrf", - "url", + "laravel", + "rulesets", "validation" ], "support": { - "issues": "https://github.com/craftcms/url-validator/issues", - "source": "https://github.com/craftcms/url-validator/tree/1.1.0" + "issues": "https://github.com/craftcms/laravel-ruleset-validation/issues", + "source": "https://github.com/craftcms/laravel-ruleset-validation/tree/1.1.0" }, - "time": "2026-07-06T20:35:29+00:00" + "time": "2026-04-22T11:01:37+00:00" }, { - "name": "creocoder/yii2-nested-sets", - "version": "0.9.0", + "name": "craftcms/plugin-installer", + "version": "1.6.0", "source": { "type": "git", - "url": "https://github.com/creocoder/yii2-nested-sets.git", - "reference": "cb8635a459b6246e5a144f096b992dcc30cf9954" + "url": "https://github.com/craftcms/plugin-installer.git", + "reference": "bd1650e8da6d5ca7a8527068d3e51c34bc7b6b4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/creocoder/yii2-nested-sets/zipball/cb8635a459b6246e5a144f096b992dcc30cf9954", - "reference": "cb8635a459b6246e5a144f096b992dcc30cf9954", + "url": "https://api.github.com/repos/craftcms/plugin-installer/zipball/bd1650e8da6d5ca7a8527068d3e51c34bc7b6b4f", + "reference": "bd1650e8da6d5ca7a8527068d3e51c34bc7b6b4f", "shasum": "" }, "require": { - "yiisoft/yii2": "*" + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.4" }, - "type": "yii2-extension", - "autoload": { + "require-dev": { + "composer/composer": "^1.0 || ^2.0" + }, + "type": "composer-plugin", + "extra": { + "class": "craft\\composer\\Plugin" + }, + "autoload": { "psr-4": { - "creocoder\\nestedsets\\": "src" + "craft\\composer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" + ], + "description": "Craft CMS Plugin Installer", + "homepage": "https://craftcms.com/", + "keywords": [ + "cms", + "composer", + "craftcms", + "installer", + "plugin" + ], + "support": { + "docs": "https://craftcms.com/docs", + "email": "support@craftcms.com", + "forum": "https://craftcms.stackexchange.com/", + "issues": "https://github.com/craftcms/cms/issues?state=open", + "rss": "https://craftcms.com/changelog.rss", + "source": "https://github.com/craftcms/cms" + }, + "time": "2023-02-22T13:17:00+00:00" + }, + { + "name": "craftcms/server-check", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/craftcms/server-check.git", + "reference": "8628114d482ac2c18747619b82b8d06d337145fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/craftcms/server-check/zipball/8628114d482ac2c18747619b82b8d06d337145fb", + "reference": "8628114d482ac2c18747619b82b8d06d337145fb", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "server/requirements" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Craft CMS Server Check", + "homepage": "https://craftcms.com/", + "keywords": [ + "cms", + "craftcms", + "requirements", + "yii2" + ], + "support": { + "docs": "https://github.com/craftcms/docs", + "email": "support@craftcms.com", + "forum": "https://craftcms.stackexchange.com/", + "issues": "https://github.com/craftcms/server-check/issues?state=open", + "rss": "https://github.com/craftcms/server-check/releases.atom", + "source": "https://github.com/craftcms/server-check" + }, + "time": "2026-05-06T18:15:11+00:00" + }, + { + "name": "craftcms/url-validator", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/craftcms/url-validator.git", + "reference": "3918c21317a856d6313b3fbf40d70ce013cdd715" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/craftcms/url-validator/zipball/3918c21317a856d6313b3fbf40d70ce013cdd715", + "reference": "3918c21317a856d6313b3fbf40d70ce013cdd715", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.0.2" + }, + "require-dev": { + "laravel/pint": "^1.29", + "nunomaduro/collision": "^8.1", + "pestphp/pest": "^3.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "CraftCms\\UrlValidator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" ], "authors": [ { - "name": "Alexander Kochetov", - "email": "creocoder@gmail.com" + "name": "Pixel & Tonic", + "homepage": "https://pixelandtonic.com/" } ], - "description": "The nested sets behavior for the Yii framework", + "description": "Validate URLs and IP addresses against SSRF, DNS rebinding, and cloud-metadata attacks.", + "homepage": "https://github.com/craftcms/url-validator", "keywords": [ - "nested sets", - "yii2" + "IP", + "craftcms", + "security", + "ssrf", + "url", + "validation" ], "support": { - "issues": "https://github.com/creocoder/yii2-nested-sets/issues", - "source": "https://github.com/creocoder/yii2-nested-sets/tree/master" + "issues": "https://github.com/craftcms/url-validator/issues", + "source": "https://github.com/craftcms/url-validator/tree/1.1.0" }, - "time": "2015-01-27T10:53:51+00:00" + "time": "2026-07-06T20:35:29+00:00" }, { "name": "dasprid/enum", @@ -848,31 +1034,106 @@ }, "time": "2025-09-16T12:23:56+00:00" }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, { "name": "doctrine/collections", - "version": "2.6.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2" + "reference": "7c2f25ed928553a3de389ccb99aa46c0eedb2d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/7713da39d8e237f28411d6a616a3dce5e20d5de2", - "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2", + "url": "https://api.github.com/repos/doctrine/collections/zipball/7c2f25ed928553a3de389ccb99aa46c0eedb2d4d", + "reference": "7c2f25ed928553a3de389ccb99aa46c0eedb2d4d", "shasum": "" }, "require": { "doctrine/deprecations": "^1", - "php": "^8.1", - "symfony/polyfill-php84": "^1.30" + "php": "^8.4", + "symfony/polyfill-php86": "^1.36" }, "require-dev": { "doctrine/coding-standard": "^14", "ext-json": "*", "phpstan/phpstan": "^2.1.30", "phpstan/phpstan-phpunit": "^2.0.7", - "phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4" + "phpunit/phpunit": "^12.4" }, "type": "library", "autoload": { @@ -916,7 +1177,7 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/2.6.0" + "source": "https://github.com/doctrine/collections/tree/3.1.0" }, "funding": [ { @@ -932,7 +1193,7 @@ "type": "tidelift" } ], - "time": "2026-01-15T10:01:58+00:00" + "time": "2026-04-29T20:41:02+00:00" }, { "name": "doctrine/deprecations", @@ -1151,16 +1412,16 @@ }, { "name": "dompdf/dompdf", - "version": "v3.1.5", + "version": "v3.1.6", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496" + "reference": "6d4b4eb8500f7a786da8868ba463a71b725a4005" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", - "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/6d4b4eb8500f7a786da8868ba463a71b725a4005", + "reference": "6d4b4eb8500f7a786da8868ba463a71b725a4005", "shasum": "" }, "require": { @@ -1209,9 +1470,9 @@ "homepage": "https://github.com/dompdf/dompdf", "support": { "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v3.1.5" + "source": "https://github.com/dompdf/dompdf/tree/v3.1.6" }, - "time": "2026-03-03T13:54:37+00:00" + "time": "2026-07-20T12:29:38+00:00" }, { "name": "dompdf/php-font-lib", @@ -1304,6 +1565,70 @@ }, "time": "2026-01-02T16:01:13+00:00" }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, { "name": "egulias/email-validator", "version": "4.0.4", @@ -1373,24 +1698,24 @@ }, { "name": "elvanto/litemoji", - "version": "4.3.0", + "version": "5.2.0", "source": { "type": "git", "url": "https://github.com/elvanto/litemoji.git", - "reference": "f13cf10686f7110a3b17d09de03050d0708840b8" + "reference": "859dbcaa31ac5eb99c0811a981ba9d9ca3dab1c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/elvanto/litemoji/zipball/f13cf10686f7110a3b17d09de03050d0708840b8", - "reference": "f13cf10686f7110a3b17d09de03050d0708840b8", + "url": "https://api.github.com/repos/elvanto/litemoji/zipball/859dbcaa31ac5eb99c0811a981ba9d9ca3dab1c1", + "reference": "859dbcaa31ac5eb99c0811a981ba9d9ca3dab1c1", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": ">=7.3" + "php": ">=7.4" }, "require-dev": { - "milesj/emojibase": "7.0.*", + "milesj/emojibase": "^16.0.3", "phpunit/phpunit": "^9.0" }, "type": "library", @@ -1410,22 +1735,22 @@ ], "support": { "issues": "https://github.com/elvanto/litemoji/issues", - "source": "https://github.com/elvanto/litemoji/tree/4.3.0" + "source": "https://github.com/elvanto/litemoji/tree/5.2.0" }, - "time": "2022-10-28T02:32:19+00:00" + "time": "2025-07-15T04:24:11+00:00" }, { "name": "enshrined/svg-sanitize", - "version": "0.22.0", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/darylldoyle/svg-sanitizer.git", - "reference": "0afa95ea74be155a7bcd6c6fb60c276c39984500" + "reference": "f3300fcd1bbf67d205b52217c75d0f7d6a8c47ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/0afa95ea74be155a7bcd6c6fb60c276c39984500", - "reference": "0afa95ea74be155a7bcd6c6fb60c276c39984500", + "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/f3300fcd1bbf67d205b52217c75d0f7d6a8c47ff", + "reference": "f3300fcd1bbf67d205b52217c75d0f7d6a8c47ff", "shasum": "" }, "require": { @@ -1455,89 +1780,161 @@ "description": "An SVG sanitizer for PHP", "support": { "issues": "https://github.com/darylldoyle/svg-sanitizer/issues", - "source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.22.0" + "source": "https://github.com/darylldoyle/svg-sanitizer/tree/1.0.0" }, - "time": "2025-08-12T10:13:48+00:00" + "time": "2026-09-01T09:35:47+00:00" }, { - "name": "ezyang/htmlpurifier", - "version": "v4.19.0", + "name": "fruitcake/php-cors", + "version": "v1.4.0", "source": { "type": "git", - "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", - "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", "shasum": "" }, "require": { - "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" }, "require-dev": { - "cerdic/css-tidy": "^1.7 || ^2.0", - "simpletest/simpletest": "dev-master" - }, - "suggest": { - "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", - "ext-bcmath": "Used for unit conversion and imagecrash protection", - "ext-iconv": "Converts text to and from non-UTF-8 encodings", - "ext-tidy": "Used for pretty-printing HTML" + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, "autoload": { - "files": [ - "library/HTMLPurifier.composer.php" - ], - "psr-0": { - "HTMLPurifier": "library/" - }, - "exclude-from-classmap": [ - "/library/HTMLPurifier/Language/" - ] + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1-or-later" + "MIT" ], "authors": [ { - "name": "Edward Z. Yang", - "email": "admin@htmlpurifier.org", - "homepage": "http://ezyang.com" + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" } ], - "description": "Standards compliant HTML filter written in PHP", - "homepage": "http://htmlpurifier.org/", + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", "keywords": [ - "html" + "cors", + "laravel", + "symfony" ], "support": { - "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" }, - "time": "2025-10-17T16:34:55+00:00" + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "adccca3324eece92ca35463648c12b9e6293c05b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/adccca3324eece92ca35463648c12b9e6293c05b", + "reference": "adccca3324eece92ca35463648c12b9e6293c05b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.10" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.52 || ^9.6.34 || ^10.5.63 || ^11.5.55 || ^12.5.14" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:06:52+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.14.0", + "version": "7.15.5", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "aef242412e13128b5049864867bb49fc37dd39de" + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aef242412e13128b5049864867bb49fc37dd39de", - "reference": "aef242412e13128b5049864867bb49fc37dd39de", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ee80339fd9177ba44c49cdb653ff02a4d1106b9a", + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.5.1", - "guzzlehttp/psr7": "^2.12.4", + "guzzlehttp/promises": "^2.5.3", + "guzzlehttp/psr7": "^2.13.1", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", @@ -1550,7 +1947,7 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.3", - "guzzlehttp/test-server": "^0.6", + "guzzlehttp/test-server": "^0.7", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -1630,7 +2027,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.14.0" + "source": "https://github.com/guzzle/guzzle/tree/7.15.5" }, "funding": [ { @@ -1646,20 +2043,20 @@ "type": "tidelift" } ], - "time": "2026-07-08T22:54:09+00:00" + "time": "2026-08-24T09:21:06+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.5.1", + "version": "2.5.3", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29" + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/9ad1e4fc607446a055b95870c7f668e93b5cff29", - "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29", + "url": "https://api.github.com/repos/guzzle/promises/zipball/cde49999552d185d64715fe9c1f77a2aadd2f9f1", + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1", "shasum": "" }, "require": { @@ -1714,7 +2111,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.5.1" + "source": "https://github.com/guzzle/promises/tree/2.5.3" }, "funding": [ { @@ -1730,20 +2127,20 @@ "type": "tidelift" } ], - "time": "2026-07-08T15:48:39+00:00" + "time": "2026-08-24T09:11:28+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.12.4", + "version": "2.13.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c" + "reference": "95e7828100de18b4e269fb1703be530082d5166d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c", - "reference": "51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/95e7828100de18b4e269fb1703be530082d5166d", + "reference": "95e7828100de18b4e269fb1703be530082d5166d", "shasum": "" }, "require": { @@ -1833,7 +2230,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.12.4" + "source": "https://github.com/guzzle/psr7/tree/2.13.1" }, "funding": [ { @@ -1849,7 +2246,93 @@ "type": "tidelift" } ], - "time": "2026-07-08T15:56:20+00:00" + "time": "2026-08-24T09:13:11+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/7a466ad606491eb6528c717482f7cca77f1851f3", + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "symfony/polyfill-php80": "^1.25" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^9.6.34", + "uri-template/tests": "1.0.2" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2026-08-24T17:13:02+00:00" }, { "name": "ibericode/vat", @@ -1963,40 +2446,53 @@ "time": "2020-12-07T12:18:49+00:00" }, { - "name": "illuminate/collections", - "version": "v10.49.0", + "name": "inertiajs/inertia-laravel", + "version": "v3.3.4", "source": { "type": "git", - "url": "https://github.com/illuminate/collections.git", - "reference": "6ae9c74fa92d4e1824d1b346cd435e8eacdc3232" + "url": "https://github.com/inertiajs/inertia-laravel.git", + "reference": "15fb5a7b2f984780ff968d9da3787aef6138326d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/6ae9c74fa92d4e1824d1b346cd435e8eacdc3232", - "reference": "6ae9c74fa92d4e1824d1b346cd435e8eacdc3232", + "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/15fb5a7b2f984780ff968d9da3787aef6138326d", + "reference": "15fb5a7b2f984780ff968d9da3787aef6138326d", "shasum": "" }, "require": { - "illuminate/conditionable": "^10.0", - "illuminate/contracts": "^10.0", - "illuminate/macroable": "^10.0", - "php": "^8.1" + "ext-json": "*", + "laravel/framework": "^11.35|^12.0|^13.0", + "php": "^8.2.0", + "symfony/console": "^7.0|^8.0" + }, + "conflict": { + "laravel/boost": "<2.5.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.15.2|^8.0", + "larastan/larastan": "^3.0", + "laravel/pint": "^1.16", + "mockery/mockery": "^1.3.3", + "orchestra/testbench": "^9.2|^10.0|^11.0", + "phpunit/phpunit": "^11.5|^12.0" }, "suggest": { - "symfony/var-dumper": "Required to use the dump method (^6.2)." + "ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command." }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "10.x-dev" + "laravel": { + "providers": [ + "Inertia\\ServiceProvider" + ] } }, "autoload": { "files": [ - "helpers.php" + "./helpers.php" ], "psr-4": { - "Illuminate\\Support\\": "" + "Inertia\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2005,44 +2501,49 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "Jonathan Reinink", + "email": "jonathan@reinink.ca", + "homepage": "https://reinink.ca" } ], - "description": "The Illuminate Collections package.", - "homepage": "https://laravel.com", + "description": "The Laravel adapter for Inertia.js.", + "keywords": [ + "inertia", + "laravel" + ], "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "issues": "https://github.com/inertiajs/inertia-laravel/issues", + "source": "https://github.com/inertiajs/inertia-laravel/tree/v3.3.4" }, - "time": "2025-09-08T19:05:53+00:00" + "time": "2026-09-11T13:57:53+00:00" }, { - "name": "illuminate/conditionable", - "version": "v10.49.0", + "name": "intervention/gif", + "version": "5.0.1", "source": { "type": "git", - "url": "https://github.com/illuminate/conditionable.git", - "reference": "47c700320b7a419f0d188d111f3bbed978fcbd3f" + "url": "https://github.com/Intervention/gif.git", + "reference": "bb395af960deffe64d70c976b4df9283f68e762d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/conditionable/zipball/47c700320b7a419f0d188d111f3bbed978fcbd3f", - "reference": "47c700320b7a419f0d188d111f3bbed978fcbd3f", + "url": "https://api.github.com/repos/Intervention/gif/zipball/bb395af960deffe64d70c976b4df9283f68e762d", + "reference": "bb395af960deffe64d70c976b4df9283f68e762d", "shasum": "" }, "require": { - "php": "^8.0.2" + "php": "^8.3" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "10.x-dev" - } + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^12.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^4" }, + "type": "library", "autoload": { "psr-4": { - "Illuminate\\Support\\": "" + "Intervention\\Gif\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2051,46 +2552,72 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" } ], - "description": "The Illuminate Conditionable package.", - "homepage": "https://laravel.com", + "description": "PHP GIF Encoder/Decoder", + "homepage": "https://github.com/intervention/gif", + "keywords": [ + "animation", + "gd", + "gif", + "image" + ], "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "issues": "https://github.com/Intervention/gif/issues", + "source": "https://github.com/Intervention/gif/tree/5.0.1" }, - "time": "2025-03-24T11:47:24+00:00" + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2026-05-03T06:04:47+00:00" }, { - "name": "illuminate/contracts", - "version": "v10.49.0", + "name": "intervention/image", + "version": "4.3.2", "source": { "type": "git", - "url": "https://github.com/illuminate/contracts.git", - "reference": "2393ef579e020d88e24283913c815c3e2c143323" + "url": "https://github.com/Intervention/image.git", + "reference": "bc15ed24bb88dc58f921daf78091a019ce42fe2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/2393ef579e020d88e24283913c815c3e2c143323", - "reference": "2393ef579e020d88e24283913c815c3e2c143323", + "url": "https://api.github.com/repos/Intervention/image/zipball/bc15ed24bb88dc58f921daf78091a019ce42fe2c", + "reference": "bc15ed24bb88dc58f921daf78091a019ce42fe2c", "shasum": "" }, "require": { - "php": "^8.1", - "psr/container": "^1.1.1|^2.0.1", - "psr/simple-cache": "^1.0|^2.0|^3.0" + "ext-mbstring": "*", + "intervention/gif": "^5", + "php": "^8.3" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "10.x-dev" - } + "require-dev": { + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^12.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^4" + }, + "suggest": { + "ext-exif": "Recommended to be able to read EXIF data properly." }, + "type": "library", "autoload": { "psr-4": { - "Illuminate\\Contracts\\": "" + "Intervention\\Image\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2099,44 +2626,244 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io" } ], - "description": "The Illuminate Contracts package.", - "homepage": "https://laravel.com", + "description": "PHP Image Processing", + "homepage": "https://image.intervention.io", + "keywords": [ + "gd", + "image", + "imagick", + "resize", + "thumbnail", + "watermark" + ], "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/4.3.2" }, - "time": "2025-03-24T11:47:24+00:00" + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2026-08-29T05:35:03+00:00" }, { - "name": "illuminate/macroable", - "version": "v10.49.0", + "name": "laravel/framework", + "version": "v13.31.0", "source": { "type": "git", - "url": "https://github.com/illuminate/macroable.git", - "reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27" + "url": "https://github.com/laravel/framework.git", + "reference": "7c75fbf93f91fa077d3df1c820cc14f4e59a9774" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/macroable/zipball/dff667a46ac37b634dcf68909d9d41e94dc97c27", - "reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27", + "url": "https://api.github.com/repos/laravel/framework/zipball/7c75fbf93f91fa077d3df1c820cc14f4e59a9774", + "reference": "7c75fbf93f91fa077d3df1c820cc14f4e59a9774", "shasum": "" }, "require": { - "php": "^8.1" + "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2 || ^8.0", + "guzzlehttp/promises": "^2.0.3 || ^3.0", + "guzzlehttp/psr7": "^2.9 || ^3.0", + "guzzlehttp/uri-template": "^1.0 || ^2.0", + "laravel/prompts": "^0.3.11", + "laravel/serializable-closure": "^2.0.10", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.10", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.3", + "psr/container": "^1.1.1 || ^2.0.1", + "psr/http-message": "^1.0 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/error-handler": "^7.4.0 || ^8.0.0", + "symfony/finder": "^7.4.0 || ^8.0.0", + "symfony/http-foundation": "^7.4.0 || ^8.0.0", + "symfony/http-kernel": "^7.4.0 || ^8.0.0", + "symfony/mailer": "^7.4.0 || ^8.0.0", + "symfony/mime": "^7.4.0 || ^8.0.0", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", + "symfony/polyfill-php86": "^1.36", + "symfony/process": "^7.4.5 || ^8.0.5", + "symfony/routing": "^7.4.0 || ^8.0.0", + "symfony/uid": "^7.4.0 || ^8.0.0", + "symfony/var-dumper": "^7.4.0 || ^8.0.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1 || 2.0", + "psr/log-implementation": "1.0 || 2.0 || 3.0", + "psr/simple-cache-implementation": "1.0 || 2.0 || 3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/image": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "intervention/image": "^4.0", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^11.0.0", + "pda/pheanstalk": "^7.0.0 || ^8.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", + "predis/predis": "^2.3 || ^3.0", + "rector/rector": "2.6.3", + "resend/resend-php": "^1.0", + "symfony/cache": "^7.4.0 || ^8.0.0", + "symfony/http-client": "^7.4.0 || ^8.0.0", + "symfony/psr-http-message-bridge": "^7.4.0 || ^8.0.0", + "symfony/translation": "^7.4.0 || ^8.0.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0 || ^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0 || ^5.0 || ^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "intervention/image": "Required to use the image processing features (^4.0).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^7.0 || ^8.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).", + "predis/predis": "Required to use the predis connector (^2.3 || ^3.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0 || ^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0 || ^1.0).", + "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.4 || ^8.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.4 || ^8.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.4 || ^8.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.4 || ^8.0)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "10.x-dev" + "dev-master": "13.0.x-dev" } }, "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], "psr-4": { - "Illuminate\\Support\\": "" + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] } }, "notification-url": "https://packagist.org/downloads/", @@ -2149,130 +2876,110 @@ "email": "taylor@laravel.com" } ], - "description": "The Illuminate Macroable package.", + "description": "The Laravel Framework.", "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], "support": { "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2023-06-05T12:46:42+00:00" + "time": "2026-09-08T14:22:55+00:00" }, { - "name": "illuminate/support", - "version": "v10.49.0", + "name": "laravel/prompts", + "version": "v0.3.24", "source": { "type": "git", - "url": "https://github.com/illuminate/support.git", - "reference": "28b505e671dbe119e4e32a75c78f87189d046e39" + "url": "https://github.com/laravel/prompts.git", + "reference": "5d3cdef29e93ca3b62b1871359db3078cd99908b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/28b505e671dbe119e4e32a75c78f87189d046e39", - "reference": "28b505e671dbe119e4e32a75c78f87189d046e39", + "url": "https://api.github.com/repos/laravel/prompts/zipball/5d3cdef29e93ca3b62b1871359db3078cd99908b", + "reference": "5d3cdef29e93ca3b62b1871359db3078cd99908b", "shasum": "" }, "require": { - "doctrine/inflector": "^2.0", - "ext-ctype": "*", - "ext-filter": "*", + "composer-runtime-api": "^2.2", "ext-mbstring": "*", - "illuminate/collections": "^10.0", - "illuminate/conditionable": "^10.0", - "illuminate/contracts": "^10.0", - "illuminate/macroable": "^10.0", - "nesbot/carbon": "^2.67", "php": "^8.1", - "voku/portable-ascii": "^2.0" + "symfony/console": "^6.2|^7.0|^8.0" }, "conflict": { - "tightenco/collect": "<5.5.33" + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" }, "suggest": { - "illuminate/filesystem": "Required to use the composer class (^10.0).", - "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.6).", - "ramsey/uuid": "Required to use Str::uuid() (^4.7).", - "symfony/process": "Required to use the composer class (^6.2).", - "symfony/uid": "Required to use Str::ulid() (^6.2).", - "symfony/var-dumper": "Required to use the dd function (^6.2).", - "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.4.1)." + "ext-pcntl": "Required for the spinner to be animated." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "10.x-dev" + "dev-main": "0.3.x-dev" } }, "autoload": { "files": [ - "helpers.php" + "src/helpers.php" ], "psr-4": { - "Illuminate\\Support\\": "" + "Laravel\\Prompts\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Illuminate Support package.", - "homepage": "https://laravel.com", + "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.24" }, - "time": "2025-09-08T19:05:53+00:00" + "time": "2026-08-20T12:55:36+00:00" }, { - "name": "league/uri", - "version": "7.8.1", + "name": "laravel/serializable-closure", + "version": "v2.0.16", "source": { "type": "git", - "url": "https://github.com/thephpleague/uri.git", - "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", - "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed", + "reference": "7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.8.1", - "php": "^8.1", - "psr/http-factory": "^1" - }, - "conflict": { - "league/uri-schemes": "^1.0" + "php": "^8.1" }, - "suggest": { - "ext-bcmath": "to improve IPV4 host parsing", - "ext-dom": "to convert the URI into an HTML anchor tag", - "ext-fileinfo": "to create Data URI from file contennts", - "ext-gmp": "to improve IPV4 host parsing", - "ext-intl": "to handle IDN host with the best performance", - "ext-uri": "to use the PHP native URI class", - "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", - "league/uri-components": "to provide additional tools to manipulate URI objects components", - "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", - "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", - "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "7.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { "psr-4": { - "League\\Uri\\": "" + "Laravel\\SerializableClosure\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2281,87 +2988,66 @@ ], "authors": [ { - "name": "Ignace Nyamagana Butera", - "email": "nyamsprod@gmail.com", - "homepage": "https://nyamsprod.com" + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" } ], - "description": "URI manipulation library", - "homepage": "https://uri.thephpleague.com", + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", "keywords": [ - "URN", - "data-uri", - "file-uri", - "ftp", - "hostname", - "http", - "https", - "middleware", - "parse_str", - "parse_url", - "psr-7", - "query-string", - "querystring", - "rfc2141", - "rfc3986", - "rfc3987", - "rfc6570", - "rfc8141", - "uri", - "uri-template", - "url", - "ws" + "closure", + "laravel", + "serializable" ], "support": { - "docs": "https://uri.thephpleague.com", - "forum": "https://thephpleague.slack.com", - "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.8.1" + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" }, - "funding": [ - { - "url": "https://github.com/sponsors/nyamsprod", - "type": "github" - } - ], - "time": "2026-03-15T20:22:25+00:00" + "time": "2026-08-18T20:28:54+00:00" }, { - "name": "league/uri-interfaces", - "version": "7.8.1", + "name": "laravel/wayfinder", + "version": "v0.1.21", "source": { "type": "git", - "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + "url": "https://github.com/laravel/wayfinder.git", + "reference": "a85a996cea189f59cac14854f8b13319a29007f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", - "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "url": "https://api.github.com/repos/laravel/wayfinder/zipball/a85a996cea189f59cac14854f8b13319a29007f3", + "reference": "a85a996cea189f59cac14854f8b13319a29007f3", "shasum": "" }, "require": { - "ext-filter": "*", - "php": "^8.1", - "psr/http-message": "^1.1 || ^2.0" + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/filesystem": "^11.0|^12.0|^13.0", + "illuminate/routing": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "phpstan/phpdoc-parser": "^2.3" }, - "suggest": { - "ext-bcmath": "to improve IPV4 host parsing", - "ext-gmp": "to improve IPV4 host parsing", - "ext-intl": "to handle IDN host with the best performance", - "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", - "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + "conflict": { + "laravel/boost": "<2.5.0" + }, + "require-dev": { + "laravel/pint": "^1.21", + "orchestra/testbench": "^11.0|^10.1|^9.0" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "7.x-dev" + "laravel": { + "providers": [ + "Laravel\\Wayfinder\\WayfinderServiceProvider" + ] } }, "autoload": { "psr-4": { - "League\\Uri\\": "" + "Laravel\\Wayfinder\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2370,206 +3056,264 @@ ], "authors": [ { - "name": "Ignace Nyamagana Butera", - "email": "nyamsprod@gmail.com", - "homepage": "https://nyamsprod.com" + "name": "Taylor Otwell", + "email": "taylor@laravel.com" } ], - "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", - "homepage": "https://uri.thephpleague.com", + "description": "Generate TypeScript representations of your Laravel actions and routes.", + "homepage": "https://github.com/laravel/wayfinder", "keywords": [ - "data-uri", - "file-uri", - "ftp", - "hostname", - "http", - "https", - "parse_str", - "parse_url", - "psr-7", - "query-string", - "querystring", - "rfc3986", - "rfc3987", - "rfc6570", - "uri", - "url", - "ws" + "laravel", + "php", + "routes", + "typescript" ], "support": { - "docs": "https://uri.thephpleague.com", - "forum": "https://thephpleague.slack.com", - "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + "issues": "https://github.com/laravel/wayfinder/issues", + "source": "https://github.com/laravel/wayfinder" }, - "funding": [ - { - "url": "https://github.com/sponsors/nyamsprod", - "type": "github" - } - ], - "time": "2026-03-08T20:05:35+00:00" + "time": "2026-08-04T21:55:43+00:00" }, { - "name": "maennchen/zipstream-php", - "version": "3.1.2", + "name": "league/commonmark", + "version": "2.10.1", "source": { "type": "git", - "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f" + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "9d489ab67a02960fd8ffe624d93f751daf95439e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f", - "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/9d489ab67a02960fd8ffe624d93f751daf95439e", + "reference": "9d489ab67a02960fd8ffe624d93f751daf95439e", "shasum": "" }, "require": { "ext-mbstring": "*", - "ext-zlib": "*", - "php-64bit": "^8.2" + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" }, "require-dev": { - "brianium/paratest": "^7.7", - "ext-zip": "*", - "friendsofphp/php-cs-fixer": "^3.16", - "guzzlehttp/guzzle": "^7.5", - "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^2.5", - "phpunit/phpunit": "^11.0", - "vimeo/psalm": "^6.0" + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^2.0.0", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" }, "suggest": { - "guzzlehttp/psr7": "^2.4", - "psr/http-message": "^2.0" + "symfony/yaml": "v2.3+ required if using the Front Matter extension" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.11-dev" + } + }, "autoload": { "psr-4": { - "ZipStream\\": "src/" + "League\\CommonMark\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Paul Duncan", - "email": "pabs@pablotron.org" - }, - { - "name": "Jonatan Männchen", - "email": "jonatan@maennchen.ch" - }, - { - "name": "Jesse Donat", - "email": "donatj@gmail.com" - }, - { - "name": "András Kolesár", - "email": "kolesar@kolesar.hu" + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" } ], - "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", "keywords": [ - "stream", - "zip" + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" ], "support": { - "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2" + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" }, "funding": [ { - "url": "https://github.com/maennchen", + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" } ], - "time": "2025-01-27T12:07:53+00:00" + "time": "2026-09-07T13:44:26+00:00" }, { - "name": "markbaker/complex", - "version": "3.0.2", + "name": "league/config", + "version": "v1.2.0", "source": { "type": "git", - "url": "https://github.com/MarkBaker/PHPComplex.git", - "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", - "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "dev-master", - "phpcompatibility/php-compatibility": "^9.3", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", - "squizlabs/php_codesniffer": "^3.7" + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, "autoload": { "psr-4": { - "Complex\\": "classes/src/" + "League\\Config\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mark Baker", - "email": "mark@lange.demon.co.uk" + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" } ], - "description": "PHP Class for working with complex numbers", - "homepage": "https://github.com/MarkBaker/PHPComplex", + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", "keywords": [ - "complex", - "mathematics" + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" ], "support": { - "issues": "https://github.com/MarkBaker/PHPComplex/issues", - "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" }, - "time": "2022-12-06T16:21:08+00:00" + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" }, { - "name": "markbaker/matrix", - "version": "3.0.1", + "name": "league/flysystem", + "version": "3.36.0", "source": { "type": "git", - "url": "https://github.com/MarkBaker/PHPMatrix.git", - "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", - "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f7fb152932f30072d573510cbd4dd657d6475b25", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "dev-master", - "phpcompatibility/php-compatibility": "^9.3", - "phpdocumentor/phpdocumentor": "2.*", - "phploc/phploc": "^4.0", - "phpmd/phpmd": "2.*", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", - "sebastian/phpcpd": "^4.0", - "squizlabs/php_codesniffer": "^3.7" + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" }, - "type": "library", + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", "autoload": { "psr-4": { - "Matrix\\": "classes/src/" + "League\\Flysystem\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2578,53 +3322,54 @@ ], "authors": [ { - "name": "Mark Baker", - "email": "mark@demon-angel.eu" + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" } ], - "description": "PHP Class for working with matrices", - "homepage": "https://github.com/MarkBaker/PHPMatrix", + "description": "File storage abstraction for PHP", "keywords": [ - "mathematics", - "matrix", - "vector" + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" ], "support": { - "issues": "https://github.com/MarkBaker/PHPMatrix/issues", - "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.36.0" }, - "time": "2022-12-02T22:17:43+00:00" + "time": "2026-09-02T08:00:27+00:00" }, { - "name": "masterminds/html5", - "version": "2.10.1", + "name": "league/flysystem-local", + "version": "3.35.3", "source": { "type": "git", - "url": "https://github.com/Masterminds/html5-php.git", - "reference": "fd5018f6815fff903946d0564977b44ce8010e29" + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29", - "reference": "fd5018f6815fff903946d0564977b44ce8010e29", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/a099b24dce160f3b2239043d13d47c4a1a214ea4", + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4", "shasum": "" }, "require": { - "ext-dom": "*", - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10" + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, "autoload": { "psr-4": { - "Masterminds\\": "src" + "League\\Flysystem\\Local\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -2633,59 +3378,45 @@ ], "authors": [ { - "name": "Matt Butcher", - "email": "technosophos@gmail.com" - }, - { - "name": "Matt Farina", - "email": "matt@mattfarina.com" - }, - { - "name": "Asmir Mustafic", - "email": "goetas@gmail.com" + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" } ], - "description": "An HTML5 parser and serializer.", - "homepage": "http://masterminds.github.io/html5-php", + "description": "Local filesystem adapter for Flysystem.", "keywords": [ - "HTML5", - "dom", - "html", - "parser", - "querypath", - "serializer", - "xml" + "Flysystem", + "file", + "files", + "filesystem", + "local" ], "support": { - "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.10.1" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.35.3" }, - "time": "2026-06-23T18:43:15+00:00" + "time": "2026-08-12T13:29:21+00:00" }, { - "name": "mikehaertl/php-shellcommand", - "version": "1.7.0", + "name": "league/flysystem-path-prefixing", + "version": "3.31.0", "source": { "type": "git", - "url": "https://github.com/mikehaertl/php-shellcommand.git", - "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545" + "url": "https://github.com/thephpleague/flysystem-path-prefixing.git", + "reference": "d7f667c2d9d6684b74f30c6ad81ae7a0c23232f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mikehaertl/php-shellcommand/zipball/e79ea528be155ffdec6f3bf1a4a46307bb49e545", - "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545", + "url": "https://api.github.com/repos/thephpleague/flysystem-path-prefixing/zipball/d7f667c2d9d6684b74f30c6ad81ae7a0c23232f3", + "reference": "d7f667c2d9d6684b74f30c6ad81ae7a0c23232f3", "shasum": "" }, "require": { - "php": ">= 5.3.0" - }, - "require-dev": { - "phpunit/phpunit": ">4.0 <=9.4" + "league/flysystem": "^3.10.0", + "php": "^8.0.2" }, "type": "library", "autoload": { "psr-4": { - "mikehaertl\\shellcommand\\": "src/" + "League\\Flysystem\\PathPrefixing\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -2694,76 +3425,49 @@ ], "authors": [ { - "name": "Michael Härtl", - "email": "haertl.mike@gmail.com" + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" } ], - "description": "An object oriented interface to shell commands", + "description": "Path prefixing filesystem adapter for Flysystem.", "keywords": [ - "shell" + "Flysystem", + "filesystem", + "prefix", + "prefixing" ], "support": { - "issues": "https://github.com/mikehaertl/php-shellcommand/issues", - "source": "https://github.com/mikehaertl/php-shellcommand/tree/1.7.0" + "source": "https://github.com/thephpleague/flysystem-path-prefixing/tree/3.31.0" }, - "time": "2023-04-19T08:25:22+00:00" + "time": "2026-01-23T15:30:45+00:00" }, { - "name": "moneyphp/money", - "version": "v4.9.0", + "name": "league/mime-type-detection", + "version": "1.17.0", "source": { "type": "git", - "url": "https://github.com/moneyphp/money.git", - "reference": "d49ee625c6ba79b9d7a228ce153b02fc1032152b" + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/moneyphp/money/zipball/d49ee625c6ba79b9d7a228ce153b02fc1032152b", - "reference": "d49ee625c6ba79b9d7a228ce153b02fc1032152b", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/f5f47eff7c48ed1003069a2ca67f316fb4021c76", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76", "shasum": "" }, "require": { - "ext-bcmath": "*", - "ext-filter": "*", - "ext-json": "*", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" }, "require-dev": { - "cache/taggable-cache": "^1.1.0", - "doctrine/coding-standard": "^12.0", - "doctrine/instantiator": "^1.5.0 || ^2.0", - "ext-gmp": "*", - "ext-intl": "*", - "florianv/exchanger": "^2.8.1", - "florianv/swap": "^4.3.0", - "moneyphp/crypto-currencies": "^1.1.0", - "moneyphp/iso-currencies": "^3.4", - "php-http/message": "^1.16.0", - "php-http/mock-client": "^1.6.0", - "phpbench/phpbench": "^1.2.5", - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.1.9", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5.9", - "psr/cache": "^1.0.1 || ^2.0 || ^3.0", - "ticketswap/phpstan-error-formatter": "^1.1" - }, - "suggest": { - "ext-gmp": "Calculate without integer limits", - "ext-intl": "Format Money objects with intl", - "florianv/exchanger": "Exchange rates library for PHP", - "florianv/swap": "Exchange rates library for PHP", - "psr/cache-implementation": "Used for Currency caching" + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0 || ^11.0 || ^12.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "autoload": { "psr-4": { - "Money\\": "src/" + "League\\MimeTypeDetection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2772,99 +3476,72 @@ ], "authors": [ { - "name": "Mathias Verraes", - "email": "mathias@verraes.net", - "homepage": "http://verraes.net" - }, + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.17.0" + }, + "funding": [ { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" + "url": "https://github.com/frankdejonge", + "type": "github" }, { - "name": "Frederik Bosch", - "email": "f.bosch@genkgo.nl" + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" } ], - "description": "PHP implementation of Fowler's Money pattern", - "homepage": "http://moneyphp.org", - "keywords": [ - "Value Object", - "money", - "vo" - ], - "support": { - "issues": "https://github.com/moneyphp/money/issues", - "source": "https://github.com/moneyphp/money/tree/v4.9.0" - }, - "time": "2026-05-04T20:23:15+00:00" + "time": "2026-07-09T11:49:27+00:00" }, { - "name": "monolog/monolog", - "version": "3.10.0", + "name": "league/uri", + "version": "7.8.1", "source": { "type": "git", - "url": "https://github.com/Seldaek/monolog.git", - "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", - "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", "shasum": "" }, "require": { - "php": ">=8.1", - "psr/log": "^2.0 || ^3.0" - }, - "provide": { - "psr/log-implementation": "3.0.0" + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" }, - "require-dev": { - "aws/aws-sdk-php": "^3.0", - "doctrine/couchdb": "~1.0@dev", - "elasticsearch/elasticsearch": "^7 || ^8", - "ext-json": "*", - "graylog2/gelf-php": "^1.4.2 || ^2.0", - "guzzlehttp/guzzle": "^7.4.5", - "guzzlehttp/psr7": "^2.2", - "mongodb/mongodb": "^1.8 || ^2.0", - "php-amqplib/php-amqplib": "~2.4 || ^3", - "php-console/php-console": "^3.1.8", - "phpstan/phpstan": "^2", - "phpstan/phpstan-deprecation-rules": "^2", - "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "^10.5.17 || ^11.0.7", - "predis/predis": "^1.1 || ^2", - "rollbar/rollbar": "^4.0", - "ruflin/elastica": "^7 || ^8", - "symfony/mailer": "^5.4 || ^6", - "symfony/mime": "^5.4 || ^6" + "conflict": { + "league/uri-schemes": "^1.0" }, "suggest": { - "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", - "doctrine/couchdb": "Allow sending log messages to a CouchDB server", - "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", - "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", - "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", - "ext-mbstring": "Allow to work properly with unicode symbols", - "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", - "ext-openssl": "Required to send log messages using SSL", - "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", - "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", - "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", - "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", - "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.x-dev" + "dev-master": "7.x-dev" } }, "autoload": { "psr-4": { - "Monolog\\": "src/Monolog" + "League\\Uri\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -2873,96 +3550,87 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "https://seld.be" + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" } ], - "description": "Sends your logs to files, sockets, inboxes, databases and various web services", - "homepage": "https://github.com/Seldaek/monolog", + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", "keywords": [ - "log", - "logging", - "psr-3" + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" ], "support": { - "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" }, "funding": [ { - "url": "https://github.com/Seldaek", + "url": "https://github.com/sponsors/nyamsprod", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", - "type": "tidelift" } ], - "time": "2026-01-02T08:56:05+00:00" + "time": "2026-03-15T20:22:25+00:00" }, { - "name": "nesbot/carbon", - "version": "2.73.0", + "name": "league/uri-interfaces", + "version": "7.8.1", "source": { "type": "git", - "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "9228ce90e1035ff2f0db84b40ec2e023ed802075" + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/9228ce90e1035ff2f0db84b40ec2e023ed802075", - "reference": "9228ce90e1035ff2f0db84b40ec2e023ed802075", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", "shasum": "" }, "require": { - "carbonphp/carbon-doctrine-types": "*", - "ext-json": "*", - "php": "^7.1.8 || ^8.0", - "psr/clock": "^1.0", - "symfony/polyfill-mbstring": "^1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" - }, - "provide": { - "psr/clock-implementation": "1.0" + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" }, - "require-dev": { - "doctrine/dbal": "^2.0 || ^3.1.4 || ^4.0", - "doctrine/orm": "^2.7 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.0", - "kylekatarnls/multi-tester": "^2.0", - "ondrejmirtes/better-reflection": "<6", - "phpmd/phpmd": "^2.9", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12.99 || ^1.7.14", - "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6", - "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", - "squizlabs/php_codesniffer": "^3.4" + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, - "bin": [ - "bin/carbon" - ], "type": "library", "extra": { - "laravel": { - "providers": [ - "Carbon\\Laravel\\ServiceProvider" - ] - }, - "phpstan": { - "includes": [ - "extension.neon" - ] - }, "branch-alias": { - "dev-2.x": "2.x-dev", - "dev-master": "3.x-dev" + "dev-master": "7.x-dev" } }, "autoload": { "psr-4": { - "Carbon\\": "src/Carbon/" + "League\\Uri\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -2971,70 +3639,83 @@ ], "authors": [ { - "name": "Brian Nesbitt", - "email": "brian@nesbot.com", - "homepage": "https://markido.com" - }, - { - "name": "kylekatarnls", - "homepage": "https://github.com/kylekatarnls" + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" } ], - "description": "An API extension for DateTime that supports 281 different languages.", - "homepage": "https://carbon.nesbot.com", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", "keywords": [ - "date", - "datetime", - "time" + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" ], "support": { - "docs": "https://carbon.nesbot.com/docs", - "issues": "https://github.com/briannesbitt/Carbon/issues", - "source": "https://github.com/briannesbitt/Carbon" + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" }, "funding": [ { - "url": "https://github.com/sponsors/kylekatarnls", + "url": "https://github.com/sponsors/nyamsprod", "type": "github" - }, - { - "url": "https://opencollective.com/Carbon#sponsor", - "type": "opencollective" - }, - { - "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", - "type": "tidelift" } ], - "time": "2025-01-08T20:10:23+00:00" + "time": "2026-03-08T20:05:35+00:00" }, { - "name": "paragonie/constant_time_encoding", - "version": "v3.1.3", + "name": "maennchen/zipstream-php", + "version": "3.2.2", "source": { "type": "git", - "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e", + "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e", "shasum": "" }, "require": { - "php": "^8" + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.3" }, "require-dev": { - "infection/infection": "^0", - "nikic/php-fuzzer": "^0", - "phpunit/phpunit": "^9|^10|^11", - "vimeo/psalm": "^4|^5|^6" + "brianium/paratest": "^7.7", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.86", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^12.0", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" }, "type": "library", "autoload": { "psr-4": { - "ParagonIE\\ConstantTime\\": "src/" + "ZipStream\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3043,66 +3724,66 @@ ], "authors": [ { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com", - "role": "Maintainer" + "name": "Paul Duncan", + "email": "pabs@pablotron.org" }, { - "name": "Steve 'Sc00bz' Thomas", - "email": "steve@tobtu.com", - "homepage": "https://www.tobtu.com", - "role": "Original Developer" + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" } ], - "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", "keywords": [ - "base16", - "base32", - "base32_decode", - "base32_encode", - "base64", - "base64_decode", - "base64_encode", - "bin2hex", - "encoding", - "hex", - "hex2bin", - "rfc4648" + "stream", + "zip" ], "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/constant_time_encoding/issues", - "source": "https://github.com/paragonie/constant_time_encoding" + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.2" }, - "time": "2025-09-24T15:06:41+00:00" + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2026-04-11T18:38:28+00:00" }, { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", + "name": "markbaker/complex", + "version": "3.0.2", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" }, + "type": "library", "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": "src/" + "Complex\\": "classes/src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3111,66 +3792,53 @@ ], "authors": [ { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" } ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" + "complex", + "mathematics" ], "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" }, - "time": "2020-06-27T09:03:43+00:00" + "time": "2022-12-06T16:21:08+00:00" }, { - "name": "phpdocumentor/reflection-docblock", - "version": "5.6.7", + "name": "markbaker/matrix", + "version": "3.0.1", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "31a105931bc8ffa3a123383829772e832fd8d903" + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/31a105931bc8ffa3a123383829772e832fd8d903", - "reference": "31a105931bc8ffa3a123383829772e832fd8d903", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.1", - "ext-filter": "*", - "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.7", - "phpstan/phpdoc-parser": "^1.7|^2.0", - "webmozart/assert": "^1.9.1 || ^2" + "php": "^7.1 || ^8.0" }, "require-dev": { - "mockery/mockery": "~1.3.5 || ~1.6.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-webmozart-assert": "^1.2", - "phpunit/phpunit": "^9.5", - "psalm/phar": "^5.26" + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": "src" + "Matrix\\": "classes/src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3179,60 +3847,53 @@ ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" + "name": "Mark Baker", + "email": "mark@demon-angel.eu" } ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.7" + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" }, - "time": "2026-03-18T20:47:46+00:00" + "time": "2022-12-02T22:17:43+00:00" }, { - "name": "phpdocumentor/type-resolver", - "version": "1.12.0", + "name": "masterminds/html5", + "version": "2.11.0", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7", + "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.3 || ^8.0", - "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.18|^2.0" + "ext-dom": "*", + "php": ">=7.4" }, "require-dev": { - "ext-tokenizer": "*", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" + "phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10" }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev" + "dev-master": "2.7-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": "src" + "Masterminds\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -3241,79 +3902,91 @@ ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" } ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.11.0" }, - "time": "2025-11-21T15:09:14+00:00" + "time": "2026-08-18T06:18:41+00:00" }, { - "name": "phpoffice/phpspreadsheet", - "version": "5.8.0", + "name": "moneyphp/money", + "version": "v4.9.0", "source": { "type": "git", - "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "01964d92536edf1a3a874b9580a52824bebf6fbb" + "url": "https://github.com/moneyphp/money.git", + "reference": "d49ee625c6ba79b9d7a228ce153b02fc1032152b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/01964d92536edf1a3a874b9580a52824bebf6fbb", - "reference": "01964d92536edf1a3a874b9580a52824bebf6fbb", + "url": "https://api.github.com/repos/moneyphp/money/zipball/d49ee625c6ba79b9d7a228ce153b02fc1032152b", + "reference": "d49ee625c6ba79b9d7a228ce153b02fc1032152b", "shasum": "" }, "require": { - "composer/pcre": "^1||^2||^3", - "ext-ctype": "*", - "ext-dom": "*", - "ext-fileinfo": "*", + "ext-bcmath": "*", "ext-filter": "*", - "ext-gd": "*", - "ext-iconv": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-simplexml": "*", - "ext-xml": "*", - "ext-xmlreader": "*", - "ext-xmlwriter": "*", - "ext-zip": "*", - "ext-zlib": "*", - "maennchen/zipstream-php": "^2.1 || ^3.0", - "markbaker/complex": "^3.0", - "markbaker/matrix": "^3.0", - "php": "^8.1", - "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + "ext-json": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "dev-main", - "dompdf/dompdf": "^2.0 || ^3.0", + "cache/taggable-cache": "^1.1.0", + "doctrine/coding-standard": "^12.0", + "doctrine/instantiator": "^1.5.0 || ^2.0", + "ext-gmp": "*", "ext-intl": "*", - "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "^10.5", - "mpdf/mpdf": "^8.1.1", - "phpcompatibility/php-compatibility": "^9.3", - "phpstan/phpstan": "^1.1 || ^2.0", - "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", - "phpstan/phpstan-phpunit": "^1.0 || ^2.0", - "phpunit/phpunit": "^10.5", - "squizlabs/php_codesniffer": "^3.7", - "tecnickcom/tcpdf": "^6.5" + "florianv/exchanger": "^2.8.1", + "florianv/swap": "^4.3.0", + "moneyphp/crypto-currencies": "^1.1.0", + "moneyphp/iso-currencies": "^3.4", + "php-http/message": "^1.16.0", + "php-http/mock-client": "^1.6.0", + "phpbench/phpbench": "^1.2.5", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1.9", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.9", + "psr/cache": "^1.0.1 || ^2.0 || ^3.0", + "ticketswap/phpstan-error-formatter": "^1.1" }, "suggest": { - "dompdf/dompdf": "Option for rendering PDF with PDF Writer", - "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()", - "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", - "mpdf/mpdf": "Option for rendering PDF with PDF Writer", - "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + "ext-gmp": "Calculate without integer limits", + "ext-intl": "Format Money objects with intl", + "florianv/exchanger": "Exchange rates library for PHP", + "florianv/swap": "Exchange rates library for PHP", + "psr/cache-implementation": "Used for Currency caching" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, "autoload": { "psr-4": { - "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + "Money\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3322,191 +3995,197 @@ ], "authors": [ { - "name": "Maarten Balliauw", - "homepage": "https://blog.maartenballiauw.be" - }, - { - "name": "Mark Baker", - "homepage": "https://markbakeruk.net" - }, - { - "name": "Franck Lefevre", - "homepage": "https://rootslabs.net" - }, - { - "name": "Erik Tilt" + "name": "Mathias Verraes", + "email": "mathias@verraes.net", + "homepage": "http://verraes.net" }, { - "name": "Adrien Crivelli" + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" }, { - "name": "Owen Leibman" + "name": "Frederik Bosch", + "email": "f.bosch@genkgo.nl" } ], - "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", - "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "description": "PHP implementation of Fowler's Money pattern", + "homepage": "http://moneyphp.org", "keywords": [ - "OpenXML", - "excel", - "gnumeric", - "ods", - "php", - "spreadsheet", - "xls", - "xlsx" + "Value Object", + "money", + "vo" ], "support": { - "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.8.0" + "issues": "https://github.com/moneyphp/money/issues", + "source": "https://github.com/moneyphp/money/tree/v4.9.0" }, - "time": "2026-06-07T03:51:10+00:00" + "time": "2026-05-04T20:23:15+00:00" }, { - "name": "phpstan/phpdoc-parser", - "version": "2.3.3", + "name": "monolog/monolog", + "version": "3.12.0", "source": { "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" + "url": "https://github.com/Seldaek/monolog.git", + "reference": "72c534fc0ab181ef52d92a68382318631e301608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", - "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/72c534fc0ab181ef52d92a68382318631e301608", + "reference": "72c534fc0ab181ef52d92a68382318631e301608", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" }, "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^5.3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/process": "^5.2" + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "psr/clock": "^1.0", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "psr/clock": "Required to pass a clock to the Logger and control the timestamp of log records", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, "autoload": { "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] + "Monolog\\": "src/Monolog" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" - }, - "time": "2026-07-08T07:01:06+00:00" - }, - { - "name": "pixelandtonic/graphql-php", - "version": "v14.11.10.1", - "source": { - "type": "git", - "url": "https://github.com/pixelandtonic/graphql-php.git", - "reference": "fdb4a288878fc9ee449245e17209d676fc7c57fd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pixelandtonic/graphql-php/zipball/fdb4a288878fc9ee449245e17209d676fc7c57fd", - "reference": "fdb4a288878fc9ee449245e17209d676fc7c57fd", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-mbstring": "*", - "php": "^7.1 || ^8" - }, - "require-dev": { - "amphp/amp": "^2.3", - "doctrine/coding-standard": "^6.0", - "nyholm/psr7": "^1.2", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.82", - "phpstan/phpstan-phpunit": "0.12.18", - "phpstan/phpstan-strict-rules": "0.12.9", - "phpunit/phpunit": "^7.2 || ^8.5", - "psr/http-message": "^1.0", - "react/promise": "2.*", - "simpod/php-coveralls-mirror": "^3.0" - }, - "suggest": { - "psr/http-message": "To use standard GraphQL server", - "react/promise": "To leverage async resolving on React PHP platform" - }, - "type": "library", - "autoload": { - "psr-4": { - "GraphQL\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A PHP port of GraphQL reference implementation", - "homepage": "https://github.com/webonyx/graphql-php", - "keywords": [ - "api", - "graphql" - ], - "support": { - "source": "https://github.com/pixelandtonic/graphql-php/tree/v14.11.10.1" + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.12.0" }, "funding": [ { - "url": "https://opencollective.com/webonyx-graphql-php", - "type": "open_collective" + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" } ], - "time": "2026-04-14T15:27:52+00:00" + "time": "2026-09-09T08:34:20+00:00" }, { - "name": "pixelandtonic/imagine", - "version": "1.5.2.1", + "name": "nesbot/carbon", + "version": "3.14.0", "source": { "type": "git", - "url": "https://github.com/pixelandtonic/Imagine.git", - "reference": "8e6c5cf929400142724b31482da51dc556277e15" + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "0023eaa2c9110e47446dd512a263c69c40cd41f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pixelandtonic/Imagine/zipball/8e6c5cf929400142724b31482da51dc556277e15", - "reference": "8e6c5cf929400142724b31482da51dc556277e15", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/0023eaa2c9110e47446dd512a263c69c40cd41f2", + "reference": "0023eaa2c9110e47446dd512a263c69c40cd41f2", "shasum": "" }, "require": { - "php": ">=7.1" + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" }, - "require-dev": { - "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4 || ^9.3" + "provide": { + "psr/clock-implementation": "1.0" }, - "suggest": { - "ext-exif": "to read EXIF metadata", - "ext-gd": "to use the GD implementation", - "ext-gmagick": "to use the Gmagick implementation", - "ext-imagick": "to use the Imagick implementation" + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" }, + "bin": [ + "bin/carbon" + ], "type": "library", "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, "branch-alias": { - "dev-develop": "1.x-dev" + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" } }, "autoload": { "psr-4": { - "Imagine\\": "src/" + "Carbon\\": "src/Carbon/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3515,178 +4194,247 @@ ], "authors": [ { - "name": "Bulat Shakirzyanov", - "email": "mallluhuct@gmail.com", - "homepage": "http://avalanche123.com" + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" } ], - "description": "Image processing for PHP", - "homepage": "http://imagine.readthedocs.org/", + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", "keywords": [ - "drawing", - "graphics", - "image manipulation", - "image processing" + "date", + "datetime", + "time" ], "support": { - "source": "https://github.com/pixelandtonic/Imagine/tree/1.5.2.1" + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" }, - "time": "2026-02-25T23:13:43+00:00" + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-09-12T20:45:19+00:00" }, { - "name": "pragmarx/google2fa", - "version": "v8.0.3", + "name": "nette/schema", + "version": "v1.3.6", "source": { "type": "git", - "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad" + "url": "https://github.com/nette/schema.git", + "reference": "c54350438cd6914616f790a49cb424605f421562" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", - "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "url": "https://api.github.com/repos/nette/schema/zipball/c54350438cd6914616f790a49cb424605f421562", + "reference": "c54350438cd6914616f790a49cb424605f421562", "shasum": "" }, "require": { - "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", - "php": "^7.1|^8.0" + "nette/utils": "^4.0", + "php": "8.1 - 8.5" }, "require-dev": { - "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, "autoload": { "psr-4": { - "PragmaRX\\Google2FA\\": "src/" - } + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" ], "authors": [ { - "name": "Antonio Carlos Ribeiro", - "email": "acr@antoniocarlosribeiro.com", - "role": "Creator & Designer" + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" } ], - "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", "keywords": [ - "2fa", - "Authentication", - "Two Factor Authentication", - "google2fa" + "config", + "nette" ], "support": { - "issues": "https://github.com/antonioribeiro/google2fa/issues", - "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3" + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.6" }, - "time": "2024-09-05T11:56:40+00:00" + "time": "2026-08-16T21:58:41+00:00" }, { - "name": "pragmarx/random", - "version": "v0.2.2", + "name": "nette/utils", + "version": "v4.1.5", "source": { "type": "git", - "url": "https://github.com/antonioribeiro/random.git", - "reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7" + "url": "https://github.com/nette/utils.git", + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/random/zipball/daf08a189c5d2d40d1a827db46364d3a741a51b7", - "reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7", + "url": "https://api.github.com/repos/nette/utils/zipball/b043439dbdf954e6c28b5ea7e34b0100f83165e0", + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0", "shasum": "" }, "require": { - "php": ">=7.0" + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" }, "require-dev": { - "fzaninotto/faker": "~1.7", - "phpunit/phpunit": "~6.4", - "pragmarx/trivia": "~0.1", - "squizlabs/php_codesniffer": "^2.3" + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" }, "suggest": { - "fzaninotto/faker": "Allows you to get dozens of randomized types", - "pragmarx/trivia": "For the trivia database" + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::chr(), ord() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { "psr-4": { - "PragmaRX\\Random\\": "src" - } + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" ], "authors": [ { - "name": "Antonio Carlos Ribeiro", - "email": "acr@antoniocarlosribeiro.com", - "homepage": "https://antoniocarlosribeiro.com", - "role": "Developer" + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" } ], - "description": "Create random chars, numbers, strings", - "homepage": "https://github.com/antonioribeiro/random", + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", "keywords": [ - "Randomize", - "faker", - "pragmarx", - "random", - "random number", - "random pattern", - "random string" + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" ], "support": { - "issues": "https://github.com/antonioribeiro/random/issues", - "source": "https://github.com/antonioribeiro/random/tree/master" + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.5" }, - "time": "2017-11-21T05:26:22+00:00" + "time": "2026-07-17T23:02:45+00:00" }, { - "name": "pragmarx/recovery", - "version": "v0.2.1", + "name": "nunomaduro/termwind", + "version": "v2.4.0", "source": { "type": "git", - "url": "https://github.com/antonioribeiro/recovery.git", - "reference": "b5ce4082f059afac6761714a84497816f45271cc" + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/recovery/zipball/b5ce4082f059afac6761714a84497816f45271cc", - "reference": "b5ce4082f059afac6761714a84497816f45271cc", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", "shasum": "" }, "require": { - "php": ">=7.0", - "pragmarx/random": "~0.1" + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" }, "require-dev": { - "phpunit/phpunit": ">=5.4.3", - "squizlabs/php_codesniffer": "^2.3", - "tightenco/collect": "^5.0" - }, - "suggest": { - "tightenco/collect": "Allows to generate recovery codes as collections" + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, "branch-alias": { - "dev-master": "1.0-dev" + "dev-2.x": "2.x-dev" } }, "autoload": { + "files": [ + "src/Functions.php" + ], "psr-4": { - "PragmaRX\\Recovery\\": "src" + "Termwind\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3695,52 +4443,66 @@ ], "authors": [ { - "name": "Antonio Carlos Ribeiro", - "email": "acr@antoniocarlosribeiro.com", - "homepage": "https://antoniocarlosribeiro.com", - "role": "Developer" + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" } ], - "description": "Create recovery codes for two factor auth", - "homepage": "https://github.com/antonioribeiro/recovery", + "description": "It's like Tailwind CSS, but for the console.", "keywords": [ - "2fa", - "account recovery", - "auth", - "backup codes", - "google2fa", - "pragmarx", - "recovery", - "recovery codes", - "two factor auth" + "cli", + "console", + "css", + "package", + "php", + "style" ], "support": { - "issues": "https://github.com/antonioribeiro/recovery/issues", - "source": "https://github.com/antonioribeiro/recovery/tree/v0.2.1" + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" }, - "time": "2021-08-15T12:26:51+00:00" + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" }, { - "name": "psr/clock", - "version": "1.0.0", + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", "source": { "type": "git", - "url": "https://github.com/php-fig/clock.git", - "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", - "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", "shasum": "" }, "require": { - "php": "^7.0 || ^8.0" + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" }, "type": "library", "autoload": { "psr-4": { - "Psr\\Clock\\": "src/" + "ParagonIE\\ConstantTime\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3749,51 +4511,66 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } ], - "description": "Common interface for reading the clock.", - "homepage": "https://github.com/php-fig/clock", + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", "keywords": [ - "clock", - "now", - "psr", - "psr-20", - "time" + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" ], "support": { - "issues": "https://github.com/php-fig/clock/issues", - "source": "https://github.com/php-fig/clock/tree/1.0.0" + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2022-11-25T14:36:26+00:00" + "time": "2025-09-24T15:06:41+00:00" }, { - "name": "psr/container", - "version": "2.0.2", + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", "shasum": "" }, "require": { - "php": ">=7.4.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-2.x": "2.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Container\\": "src/" + "phpDocumentor\\Reflection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3802,51 +4579,67 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" ], "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" }, - "time": "2021-11-05T16:47:00+00:00" + "time": "2020-06-27T09:03:43+00:00" }, { - "name": "psr/event-dispatcher", - "version": "1.0.0", + "name": "phpdocumentor/reflection-docblock", + "version": "6.0.3", "source": { "type": "git", - "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { - "php": ">=7.2.0" + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "5.x-dev" } }, "autoload": { "psr-4": { - "Psr\\EventDispatcher\\": "src/" + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -3855,49 +4648,60 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" } ], - "description": "Standard interfaces for event handling.", - "keywords": [ - "events", - "psr", - "psr-14" - ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2019-01-08T18:20:26+00:00" + "time": "2026-03-18T20:49:53+00:00" }, { - "name": "psr/http-client", - "version": "1.0.3", + "name": "phpdocumentor/type-resolver", + "version": "2.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/http-client.git", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", "shasum": "" }, "require": { - "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0 || ^2.0" + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Http\\Client\\": "src/" + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -3906,50 +4710,79 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Mike van Riel", + "email": "me@mikevanriel.com" } ], - "description": "Common interface for HTTP clients", - "homepage": "https://github.com/php-fig/http-client", - "keywords": [ - "http", - "http-client", - "psr", - "psr-18" - ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { - "source": "https://github.com/php-fig/http-client" + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" }, - "time": "2023-09-23T14:17:50+00:00" + "time": "2026-01-06T21:53:42+00:00" }, { - "name": "psr/http-factory", - "version": "1.1.0", + "name": "phpoffice/phpspreadsheet", + "version": "5.9.0", "source": { "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "05e99ebf61238a70227b4d9cc02d0030d34f6339" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/05e99ebf61238a70227b4d9cc02d0030d34f6339", + "reference": "05e99ebf61238a70227b4d9cc02d0030d34f6339", "shasum": "" }, "require": { - "php": ">=7.1", - "psr/http-message": "^1.0 || ^2.0" + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-filter": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^8.2", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^2.0 || ^3.0", + "ext-intl": "*", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.5", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1 || ^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": "^10.5 || ^11.0", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" }, + "type": "library", "autoload": { "psr-4": { - "Psr\\Http\\Message\\": "src/" + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" } }, "notification-url": "https://packagist.org/downloads/", @@ -3958,155 +4791,196 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" } ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" ], "support": { - "source": "https://github.com/php-fig/http-factory" + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.9.0" }, - "time": "2024-04-15T12:06:14+00:00" + "time": "2026-07-12T19:17:39+00:00" }, { - "name": "psr/http-message", - "version": "2.0", + "name": "phpoption/phpoption", + "version": "1.10.0", "source": { "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/67b192b6a42ec03944b972d6e633ddec78ad2c6d", + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.54 || ^9.6.36 || ^10.5.64 || ^11.5.56 || ^12.5.33" }, "type": "library", "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.9-dev" } }, "autoload": { "psr-4": { - "Psr\\Http\\Message\\": "src/" + "PhpOption\\": "src/PhpOption/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" } ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", + "description": "Option Type for PHP", "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" + "language", + "option", + "php", + "type" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.10.0" }, - "time": "2023-04-04T09:54:51+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2026-08-24T00:54:40+00:00" }, { - "name": "psr/log", - "version": "3.0.2", + "name": "phpstan/phpdoc-parser", + "version": "2.3.5", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/148cefffaf0233e4c08cc13db8a195a56dd6dfe9", + "reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9", "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": "^7.4 || ^8.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" }, + "type": "library", "autoload": { "psr-4": { - "Psr\\Log\\": "src" + "PHPStan\\PhpDocParser\\": [ + "src/" + ] } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.5" }, - "time": "2024-09-11T13:17:53+00:00" + "time": "2026-08-31T16:05:28+00:00" }, { - "name": "psr/simple-cache", - "version": "3.0.0", + "name": "pragmarx/google2fa", + "version": "v9.1.0", "source": { "type": "git", - "url": "https://github.com/php-fig/simple-cache.git", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "f00bc788c555adfb6765c437ff3538e59cd88af1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/f00bc788c555adfb6765c437ff3538e59cd88af1", + "reference": "f00bc788c555adfb6765c437ff3538e59cd88af1", "shasum": "" }, "require": { - "php": ">=8.0.0" + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", + "php": "^7.1|^8.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } + "require-dev": { + "phpstan/phpstan": "^1.0|^2.0", + "phpstan/phpstan-phpunit": "^1.0|^2.0", + "phpunit/phpunit": "~9|~10|~11|~12|~13", + "psalm/plugin-phpunit": "^0.19|^0.20", + "vimeo/psalm": "^5.26|^6.13" }, + "type": "library", "autoload": { "psr-4": { - "Psr\\SimpleCache\\": "src/" + "PragmaRX\\Google2FA\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4115,49 +4989,70 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" } ], - "description": "Common interfaces for simple caching", + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", "keywords": [ - "cache", - "caching", - "psr", - "psr-16", - "simple-cache" + "2fa", + "Authentication", + "MFA", + "Two Factor Authentication", + "google-authenticator", + "google2fa", + "hotp", + "otp", + "rfc4226", + "rfc6238", + "totp" ], "support": { - "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + "docs": "https://github.com/antonioribeiro/google2fa#readme", + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "security": "https://github.com/antonioribeiro/google2fa/security/policy", + "source": "https://github.com/antonioribeiro/google2fa" }, - "time": "2021-10-29T13:26:27+00:00" + "time": "2026-08-15T13:22:01+00:00" }, { - "name": "ralouphie/getallheaders", - "version": "3.0.3", + "name": "pragmarx/random", + "version": "v0.2.2", "source": { "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" + "url": "https://github.com/antonioribeiro/random.git", + "reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", + "url": "https://api.github.com/repos/antonioribeiro/random/zipball/daf08a189c5d2d40d1a827db46364d3a741a51b7", + "reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=7.0" }, "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" + "fzaninotto/faker": "~1.7", + "phpunit/phpunit": "~6.4", + "pragmarx/trivia": "~0.1", + "squizlabs/php_codesniffer": "^2.3" + }, + "suggest": { + "fzaninotto/faker": "Allows you to get dozens of randomized types", + "pragmarx/trivia": "For the trivia database" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, "autoload": { - "files": [ - "src/getallheaders.php" - ] + "psr-4": { + "PragmaRX\\Random\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4165,65 +5060,64 @@ ], "authors": [ { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "homepage": "https://antoniocarlosribeiro.com", + "role": "Developer" } ], - "description": "A polyfill for getallheaders.", + "description": "Create random chars, numbers, strings", + "homepage": "https://github.com/antonioribeiro/random", + "keywords": [ + "Randomize", + "faker", + "pragmarx", + "random", + "random number", + "random pattern", + "random string" + ], "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" + "issues": "https://github.com/antonioribeiro/random/issues", + "source": "https://github.com/antonioribeiro/random/tree/master" }, - "time": "2019-03-08T08:55:37+00:00" + "time": "2017-11-21T05:26:22+00:00" }, { - "name": "sabberworm/php-css-parser", - "version": "v9.4.0", + "name": "pragmarx/recovery", + "version": "v0.2.1", "source": { "type": "git", - "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", - "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f" + "url": "https://github.com/antonioribeiro/recovery.git", + "reference": "b5ce4082f059afac6761714a84497816f45271cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", - "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", + "url": "https://api.github.com/repos/antonioribeiro/recovery/zipball/b5ce4082f059afac6761714a84497816f45271cc", + "reference": "b5ce4082f059afac6761714a84497816f45271cc", "shasum": "" }, "require": { - "ext-iconv": "*", - "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4" + "php": ">=7.0", + "pragmarx/random": "~0.1" }, "require-dev": { - "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/extension-installer": "1.4.3", - "phpstan/phpstan": "1.12.33 || 2.2.2", - "phpstan/phpstan-phpunit": "1.4.2 || 2.0.16", - "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.11", - "phpunit/phpunit": "8.5.52", - "rawr/phpunit-data-provider": "3.3.1", - "rector/rector": "1.2.10 || 2.4.6", - "rector/type-perfect": "1.0.0 || 2.1.3", - "squizlabs/php_codesniffer": "4.0.1", - "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.3" + "phpunit/phpunit": ">=5.4.3", + "squizlabs/php_codesniffer": "^2.3", + "tightenco/collect": "^5.0" }, "suggest": { - "ext-mbstring": "for parsing UTF-8 CSS" + "tightenco/collect": "Allows to generate recovery codes as collections" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "9.5.x-dev" + "dev-master": "1.0-dev" } }, "autoload": { - "files": [ - "src/Rule/Rule.php", - "src/RuleSet/RuleContainer.php" - ], "psr-4": { - "Sabberworm\\CSS\\": "src/" + "PragmaRX\\Recovery\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -4232,121 +5126,105 @@ ], "authors": [ { - "name": "Raphael Schweikert" - }, - { - "name": "Oliver Klee", - "email": "github@oliverklee.de" - }, - { - "name": "Jake Hotson", - "email": "jake.github@qzdesign.co.uk" + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "homepage": "https://antoniocarlosribeiro.com", + "role": "Developer" } ], - "description": "Parser for CSS Files written in PHP", - "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "description": "Create recovery codes for two factor auth", + "homepage": "https://github.com/antonioribeiro/recovery", "keywords": [ - "css", - "parser", - "stylesheet" + "2fa", + "account recovery", + "auth", + "backup codes", + "google2fa", + "pragmarx", + "recovery", + "recovery codes", + "two factor auth" ], "support": { - "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", - "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.4.0" + "issues": "https://github.com/antonioribeiro/recovery/issues", + "source": "https://github.com/antonioribeiro/recovery/tree/v0.2.1" }, - "time": "2026-06-18T15:10:53+00:00" + "time": "2021-08-15T12:26:51+00:00" }, { - "name": "samdark/yii2-psr-log-target", - "version": "1.1.4", + "name": "psr/clock", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/samdark/yii2-psr-log-target.git", - "reference": "5f14f21d5ee4294fe9eb3e723ec8a3908ca082ea" + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/samdark/yii2-psr-log-target/zipball/5f14f21d5ee4294fe9eb3e723ec8a3908ca082ea", - "reference": "5f14f21d5ee4294fe9eb3e723ec8a3908ca082ea", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", "shasum": "" }, "require": { - "psr/log": "~1.0.2|~1.1.0|~3.0.0", - "yiisoft/yii2": "~2.0.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.4|~10.4.2" + "php": "^7.0 || ^8.0" }, - "type": "yii2-extension", + "type": "library", "autoload": { "psr-4": { - "samdark\\log\\": "src", - "samdark\\log\\tests\\": "tests" + "Psr\\Clock\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Alexander Makarov", - "email": "sam@rmcreative.ru" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Yii 2 log target which uses PSR-3 compatible logger", - "homepage": "https://github.com/samdark/yii2-psr-log-target", + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", "keywords": [ - "extension", - "log", - "psr-3", - "yii" + "clock", + "now", + "psr", + "psr-20", + "time" ], "support": { - "issues": "https://github.com/samdark/yii2-psr-log-target/issues", - "source": "https://github.com/samdark/yii2-psr-log-target" + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" }, - "funding": [ - { - "url": "https://github.com/samdark", - "type": "github" - }, - { - "url": "https://www.patreon.com/samdark", - "type": "patreon" - } - ], - "time": "2023-11-23T14:11:29+00:00" + "time": "2022-11-25T14:36:26+00:00" }, { - "name": "seld/cli-prompt", - "version": "1.0.4", + "name": "psr/container", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/Seldaek/cli-prompt.git", - "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5" + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/b8dfcf02094b8c03b40322c229493bb2884423c5", - "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { - "php": ">=5.3" - }, - "require-dev": { - "phpstan/phpstan": "^0.12.63" + "php": ">=7.4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "psr-4": { - "Seld\\CliPrompt\\": "src/" + "Psr\\Container\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4355,59 +5233,51 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Allows you to prompt for user input on the command line, and optionally hide the characters they type", + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "cli", - "console", - "hidden", - "input", - "prompt" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], "support": { - "issues": "https://github.com/Seldaek/cli-prompt/issues", - "source": "https://github.com/Seldaek/cli-prompt/tree/1.0.4" + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2020-12-15T21:32:01+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { - "name": "setasign/fpdi", - "version": "v2.6.8", + "name": "psr/event-dispatcher", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/Setasign/FPDI.git", - "reference": "881945be29a4996ad3d008eb18ddc01fa3df890c" + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Setasign/FPDI/zipball/881945be29a4996ad3d008eb18ddc01fa3df890c", - "reference": "881945be29a4996ad3d008eb18ddc01fa3df890c", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", "shasum": "" }, "require": { - "ext-zlib": "*", - "php": ">=7.2 <=8.5.99999" - }, - "conflict": { - "setasign/tfpdf": "<1.31" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.52", - "setasign/fpdf": "^1.9.0", - "setasign/tfpdf": "~1.33", - "squizlabs/php_codesniffer": "^3.5", - "tecnickcom/tcpdf": "^6.8" - }, - "suggest": { - "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." + "php": ">=7.2.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { "psr-4": { - "setasign\\Fpdi\\": "src/" + "Psr\\EventDispatcher\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4416,68 +5286,49 @@ ], "authors": [ { - "name": "Jan Slabon", - "email": "jan.slabon@setasign.com", - "homepage": "https://www.setasign.com" - }, - { - "name": "Maximilian Kresse", - "email": "maximilian.kresse@setasign.com", - "homepage": "https://www.setasign.com" + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" } ], - "description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.", - "homepage": "https://www.setasign.com/fpdi", + "description": "Standard interfaces for event handling.", "keywords": [ - "fpdf", - "fpdi", - "pdf" + "events", + "psr", + "psr-14" ], "support": { - "issues": "https://github.com/Setasign/FPDI/issues", - "source": "https://github.com/Setasign/FPDI/tree/v2.6.8" + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", - "type": "tidelift" - } - ], - "time": "2026-06-11T10:37:24+00:00" + "time": "2019-01-08T18:20:26+00:00" }, { - "name": "spomky-labs/cbor-php", - "version": "3.2.3", + "name": "psr/http-client", + "version": "1.0.3", "source": { "type": "git", - "url": "https://github.com/Spomky-Labs/cbor-php.git", - "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32" + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32", - "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { - "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", - "ext-mbstring": "*", - "php": ">=8.0" - }, - "require-dev": { - "ext-json": "*", - "roave/security-advisories": "dev-latest", - "symfony/error-handler": "^6.4|^7.1|^8.0", - "symfony/var-dumper": "^6.4|^7.1|^8.0" - }, - "suggest": { - "ext-bcmath": "GMP or BCMath extensions will drastically improve the library performance. BCMath extension needed to handle the Big Float and Decimal Fraction Tags", - "ext-gmp": "GMP or BCMath extensions will drastically improve the library performance" + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { "psr-4": { - "CBOR\\": "src/" + "Psr\\Http\\Client\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4486,83 +5337,50 @@ ], "authors": [ { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" - }, - { - "name": "All contributors", - "homepage": "https://github.com/Spomky-Labs/cbor-php/contributors" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "CBOR Encoder/Decoder for PHP", + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", "keywords": [ - "Concise Binary Object Representation", - "RFC7049", - "cbor" + "http", + "http-client", + "psr", + "psr-18" ], "support": { - "issues": "https://github.com/Spomky-Labs/cbor-php/issues", - "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.2.3" + "source": "https://github.com/php-fig/http-client" }, - "funding": [ - { - "url": "https://github.com/Spomky", - "type": "github" - }, - { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" - } - ], - "time": "2026-04-01T12:15:20+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { - "name": "spomky-labs/pki-framework", - "version": "1.4.2", + "name": "psr/http-factory", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/Spomky-Labs/pki-framework.git", - "reference": "aa576cbd07128075bef97ac2f8af9854e67513d8" + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/aa576cbd07128075bef97ac2f8af9854e67513d8", - "reference": "aa576cbd07128075bef97ac2f8af9854e67513d8", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", - "ext-mbstring": "*", - "php": ">=8.1", - "psr/clock": "^1.0" - }, - "require-dev": { - "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0", - "ext-gmp": "*", - "ext-openssl": "*", - "infection/infection": "^0.28|^0.29|^0.31|^0.32", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpstan/extension-installer": "^1.3|^2.0", - "phpstan/phpstan": "^1.8|^2.0", - "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", - "phpstan/phpstan-phpunit": "^1.1|^2.0", - "phpstan/phpstan-strict-rules": "^1.3|^2.0", - "phpunit/phpunit": "^10.1|^11.0|^12.0|^13.0", - "rector/rector": "^1.0|^2.0", - "roave/security-advisories": "dev-latest", - "symfony/string": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0", - "symplify/easy-coding-standard": "^12.0|^13.0" - }, - "suggest": { - "ext-bcmath": "For better performance (or GMP)", - "ext-gmp": "For better performance (or BCMath)", - "ext-openssl": "For OpenSSL based cyphering" + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { "psr-4": { - "SpomkyLabs\\Pki\\": "src/" + "Psr\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4571,94 +5389,53 @@ ], "authors": [ { - "name": "Joni Eskelinen", - "email": "jonieske@gmail.com", - "role": "Original developer" - }, - { - "name": "Florent Morselli", - "email": "florent.morselli@spomky-labs.com", - "role": "Spomky-Labs PKI Framework developer" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.", - "homepage": "https://github.com/spomky-labs/pki-framework", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ - "DER", - "Private Key", - "ac", - "algorithm identifier", - "asn.1", - "asn1", - "attribute certificate", - "certificate", - "certification request", - "cryptography", - "csr", - "decrypt", - "ec", - "encrypt", - "pem", - "pkcs", - "public key", - "rsa", - "sign", - "signature", - "verify", - "x.509", - "x.690", - "x509", - "x690" + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" ], "support": { - "issues": "https://github.com/Spomky-Labs/pki-framework/issues", - "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.4.2" + "source": "https://github.com/php-fig/http-factory" }, - "funding": [ - { - "url": "https://github.com/Spomky", - "type": "github" - }, - { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" - } - ], - "time": "2026-03-23T22:56:56+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { - "name": "symfony/clock", - "version": "v7.4.8", + "name": "psr/http-message", + "version": "2.0", "source": { "type": "git", - "url": "https://github.com/symfony/clock.git", - "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", - "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { - "php": ">=8.2", - "psr/clock": "^1.0", - "symfony/polyfill-php83": "^1.28" - }, - "provide": { - "psr/clock-implementation": "1.0" + "php": "^7.2 || ^8.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { - "files": [ - "Resources/now.php" - ], "psr-4": { - "Symfony\\Component\\Clock\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Psr\\Http\\Message\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4666,69 +5443,52 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Decouples applications from the system clock", - "homepage": "https://symfony.com", + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", "keywords": [ - "clock", - "psr20", - "time" + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.4.8" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2023-04-04T09:54:51+00:00" }, { - "name": "symfony/css-selector", - "version": "v7.4.9", + "name": "psr/log", + "version": "3.0.2", "source": { "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "b75663ed96cf4756e28e3105476f220f92886cc4" + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/b75663ed96cf4756e28e3105476f220f92886cc4", - "reference": "b75663ed96cf4756e28e3105476f220f92886cc4", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.0.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, "autoload": { "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Psr\\Log\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4736,74 +5496,49 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Converts CSS selectors to XPath expressions", - "homepage": "https://symfony.com", + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.4.9" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-04-18T13:18:21+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { - "name": "symfony/deprecation-contracts", - "version": "v3.7.1", + "name": "psr/simple-cache", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", - "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.0.0" }, "type": "library", "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, "branch-alias": { - "dev-main": "3.7-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { - "files": [ - "function.php" - ] + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4811,70 +5546,48 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-06-05T06:23:12+00:00" + "time": "2021-10-29T13:26:27+00:00" }, { - "name": "symfony/dom-crawler", - "version": "v7.4.12", + "name": "ralouphie/getallheaders", + "version": "3.0.3", "source": { "type": "git", - "url": "https://github.com/symfony/dom-crawler.git", - "reference": "b59b59122690976550fd142c23fab62c84738db6" + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/b59b59122690976550fd142c23fab62c84738db6", - "reference": "b59b59122690976550fd142c23fab62c84738db6", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", "shasum": "" }, "require": { - "masterminds/html5": "^2.6", - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=5.6" }, "require-dev": { - "symfony/css-selector": "^6.4|^7.0|^8.0" + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\DomCrawler\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "src/getallheaders.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4883,84 +5596,66 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" } ], - "description": "Eases DOM navigation for HTML and XML documents", - "homepage": "https://symfony.com", + "description": "A polyfill for getallheaders.", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v7.4.12" + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-05-20T07:20:23+00:00" + "time": "2019-03-08T08:55:37+00:00" }, { - "name": "symfony/event-dispatcher", - "version": "v7.4.14", + "name": "ramsey/collection", + "version": "2.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "51fe3d170227be8d1772214b82ae506e15ed78ff" + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/51fe3d170227be8d1772214b82ae506e15ed78ff", - "reference": "51fe3d170227be8d1772214b82ae506e15ed78ff", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/event-dispatcher-contracts": "^2.5|^3" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/service-contracts": "<2.5" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0|3.0" + "php": "^8.1" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/error-handler": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/framework-bundle": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0|^8.0" + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" }, "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, "autoload": { "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Ramsey\\Collection\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4968,149 +5663,154 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" } ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", - "homepage": "https://symfony.com", + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.14" + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-06-06T11:10:32+00:00" + "time": "2025-03-22T05:38:12+00:00" }, { - "name": "symfony/event-dispatcher-contracts", - "version": "v3.7.1", + "name": "ramsey/uuid", + "version": "4.x-dev", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" + "url": "https://github.com/ramsey/uuid.git", + "reference": "40988c506689c11cfc76c18445f7b26819d0d259" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", - "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/40988c506689c11cfc76c18445f7b26819d0d259", + "reference": "40988c506689c11cfc76c18445f7b26819d0d259", "shasum": "" }, "require": { - "php": ">=8.1", - "psr/event-dispatcher": "^1" + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." }, + "default-branch": true, "type": "library", "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.7-dev" + "captainhook": { + "force-install": true } }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" + "Ramsey\\Uuid\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "guid", + "identifier", + "uuid" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.x" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-06-05T06:23:12+00:00" + "time": "2026-09-13T14:33:28+00:00" }, { - "name": "symfony/filesystem", - "version": "v6.4.39", + "name": "sabberworm/php-css-parser", + "version": "v9.4.0", "source": { "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "c507b077756b4e3e09adbbe7975fac81cd3722ca" + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/c507b077756b4e3e09adbbe7975fac81cd3722ca", - "reference": "c507b077756b4e3e09adbbe7975fac81cd3722ca", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8" + "ext-iconv": "*", + "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4" }, "require-dev": { - "symfony/process": "^5.4|^6.4|^7.0" + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.33 || 2.2.2", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.16", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.11", + "phpunit/phpunit": "8.5.52", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.4.6", + "rector/type-perfect": "1.0.0 || 2.1.3", + "squizlabs/php_codesniffer": "4.0.1", + "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.3" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.5.x-dev" + } + }, "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "files": [ + "src/Rule/Rule.php", + "src/RuleSet/RuleContainer.php" + ], + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5118,97 +5818,66 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides basic utilities for the filesystem", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.39" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" + "name": "Raphael Schweikert" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "name": "Oliver Klee", + "email": "github@oliverklee.de" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" } ], - "time": "2026-05-07T13:11:42+00:00" + "description": "Parser for CSS Files written in PHP", + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "css", + "parser", + "stylesheet" + ], + "support": { + "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.4.0" + }, + "time": "2026-06-18T15:10:53+00:00" }, { - "name": "symfony/http-client", - "version": "v7.4.14", + "name": "setasign/fpdi", + "version": "v2.6.7", "source": { "type": "git", - "url": "https://github.com/symfony/http-client.git", - "reference": "f6bc6b5a54ff5afac4725cacec9bf2f52eb15920" + "url": "https://github.com/Setasign/FPDI.git", + "reference": "388c51e69982a3fc16698710b763e8107a49f510" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/f6bc6b5a54ff5afac4725cacec9bf2f52eb15920", - "reference": "f6bc6b5a54ff5afac4725cacec9bf2f52eb15920", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/388c51e69982a3fc16698710b763e8107a49f510", + "reference": "388c51e69982a3fc16698710b763e8107a49f510", "shasum": "" }, "require": { - "php": ">=8.2", - "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "~3.4.4|^3.5.2", - "symfony/polyfill-php83": "^1.29", - "symfony/service-contracts": "^2.5|^3" + "ext-zlib": "*", + "php": ">=7.2 <=8.5.99999" }, "conflict": { - "amphp/amp": "<2.5", - "amphp/socket": "<1.1", - "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.4" - }, - "provide": { - "php-http/async-client-implementation": "*", - "php-http/client-implementation": "*", - "psr/http-client-implementation": "1.0", - "symfony/http-client-implementation": "3.0" + "setasign/tfpdf": "<1.31" }, "require-dev": { - "amphp/http-client": "^4.2.1|^5.0", - "amphp/http-tunnel": "^1.0|^2.0", - "guzzlehttp/promises": "^1.4|^2.0", - "nyholm/psr7": "^1.0", - "php-http/httplug": "^1.0|^2.0", - "psr/http-client": "^1.0", - "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/cache": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/rate-limiter": "^6.4|^7.0|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0" + "phpunit/phpunit": "^8.5.52", + "setasign/fpdf": "~1.8.6", + "setasign/tfpdf": "~1.33", + "squizlabs/php_codesniffer": "^3.5", + "tecnickcom/tcpdf": "^6.8" + }, + "suggest": { + "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "setasign\\Fpdi\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5216,76 +5885,70 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Jan Slabon", + "email": "jan.slabon@setasign.com", + "homepage": "https://www.setasign.com" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Maximilian Kresse", + "email": "maximilian.kresse@setasign.com", + "homepage": "https://www.setasign.com" } ], - "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", - "homepage": "https://symfony.com", + "description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.", + "homepage": "https://www.setasign.com/fpdi", "keywords": [ - "http" + "fpdf", + "fpdi", + "pdf" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.14" + "issues": "https://github.com/Setasign/FPDI/issues", + "source": "https://github.com/Setasign/FPDI/tree/v2.6.7" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", "type": "tidelift" } ], - "time": "2026-06-16T11:50:14+00:00" + "time": "2026-05-13T10:16:22+00:00" }, { - "name": "symfony/http-client-contracts", - "version": "v3.7.1", + "name": "spomky-labs/cbor-php", + "version": "3.4.2", "source": { "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "41fc42d276aeff21192465331ebbab7d83a743c0" + "url": "https://github.com/Spomky-Labs/cbor-php.git", + "reference": "8f5ea00a07ad529d20886505cdbeb2b9ac7bb2d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41fc42d276aeff21192465331ebbab7d83a743c0", - "reference": "41fc42d276aeff21192465331ebbab7d83a743c0", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/8f5ea00a07ad529d20886505cdbeb2b9ac7bb2d6", + "reference": "8f5ea00a07ad529d20886505cdbeb2b9ac7bb2d6", "shasum": "" }, "require": { - "php": ">=8.1" + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17|^0.18|^0.19|^0.20|^1.0", + "ext-mbstring": "*", + "php": ">=8.0", + "symfony/polyfill-php81": "^1.32" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.7-dev" - } + "require-dev": { + "ext-json": "*", + "roave/security-advisories": "dev-latest", + "symfony/error-handler": "^6.4|^7.1|^8.0", + "symfony/var-dumper": "^6.4|^7.1|^8.0" + }, + "suggest": { + "ext-bcmath": "Improves the library performance when ext-gmp is missing, and is required to handle the Big Float and Decimal Fraction Tags (4 and 5)", + "ext-gmp": "Strongly recommended when decoding untrusted input: without it, converting the byte string of a Big Number Tag (2 and 3) is quadratic in its length. Also improves the library performance overall" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Contracts\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] + "CBOR\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5293,91 +5956,84 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/cbor-php/contributors" } ], - "description": "Generic abstractions related to HTTP clients", - "homepage": "https://symfony.com", + "description": "CBOR Encoder/Decoder for PHP", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "Concise Binary Object Representation", + "RFC7049", + "cbor" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.1" + "issues": "https://github.com/Spomky-Labs/cbor-php/issues", + "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.4.2" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", + "url": "https://github.com/Spomky", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" } ], - "time": "2026-06-05T06:23:12+00:00" + "time": "2026-09-15T06:55:29+00:00" }, { - "name": "symfony/mailer", - "version": "v7.4.14", + "name": "spomky-labs/pki-framework", + "version": "1.6.3", "source": { "type": "git", - "url": "https://github.com/symfony/mailer.git", - "reference": "f88ce03ae73e3edb5c176ce1f337709996e88495" + "url": "https://github.com/Spomky-Labs/pki-framework.git", + "reference": "792e909d4e387adffe3c4f404451c7d57a3d2022" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/f88ce03ae73e3edb5c176ce1f337709996e88495", - "reference": "f88ce03ae73e3edb5c176ce1f337709996e88495", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/792e909d4e387adffe3c4f404451c7d57a3d2022", + "reference": "792e909d4e387adffe3c4f404451c7d57a3d2022", "shasum": "" }, "require": { - "egulias/email-validator": "^2.1.10|^3|^4", - "php": ">=8.2", - "psr/event-dispatcher": "^1", - "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/mime": "^7.2|^8.0", - "symfony/service-contracts": "^2.5|^3" - }, - "conflict": { - "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<6.4", - "symfony/messenger": "<6.4", - "symfony/mime": "<6.4", - "symfony/twig-bridge": "<6.4" + "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17|^0.18|^0.19|^0.20|^1.0", + "ext-mbstring": "*", + "php": ">=8.1" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/http-client": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/twig-bridge": "^6.4|^7.0|^8.0" + "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0", + "ext-gmp": "*", + "ext-openssl": "*", + "infection/infection": "^0.28|^0.29|^0.31", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.3|^2.0", + "phpstan/phpstan": "^1.8|^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", + "phpstan/phpstan-phpunit": "^1.1|^2.0", + "phpstan/phpstan-strict-rules": "^1.3|^2.0", + "phpunit/phpunit": "^10.1|^11.0|^12.0", + "rector/rector": "^1.0|^2.0", + "roave/security-advisories": "dev-latest", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symplify/easy-coding-standard": "^12.0 || ^13.0" + }, + "suggest": { + "ext-bcmath": "For better performance (or GMP)", + "ext-gmp": "For better performance (or BCMath)", + "ext-openssl": "For OpenSSL based cyphering", + "ext-sodium": "To verify Ed25519 signatures where the OpenSSL extension has no EdDSA" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Mailer\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "SpomkyLabs\\Pki\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5385,80 +6041,89 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Helps sending emails", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.14" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "name": "Joni Eskelinen", + "email": "jonieske@gmail.com", + "role": "Original developer" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "name": "Florent Morselli", + "email": "florent.morselli@spomky-labs.com", + "role": "Spomky-Labs PKI Framework developer" } ], - "time": "2026-06-13T08:51:35+00:00" - }, - { - "name": "symfony/mime", - "version": "v7.4.13", + "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.", + "homepage": "https://github.com/spomky-labs/pki-framework", + "keywords": [ + "DER", + "Private Key", + "ac", + "algorithm identifier", + "asn.1", + "asn1", + "attribute certificate", + "certificate", + "certification request", + "cryptography", + "csr", + "decrypt", + "ec", + "encrypt", + "pem", + "pkcs", + "public key", + "rsa", + "sign", + "signature", + "verify", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/pki-framework/issues", + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.6.3" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-09-12T19:02:49+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/mime.git", - "reference": "a845722765c4f6b2ce88beaf4f4479975b186770" + "url": "https://github.com/symfony/clock.git", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770", - "reference": "a845722765c4f6b2ce88beaf4f4479975b186770", + "url": "https://api.github.com/repos/symfony/clock/zipball/701ef4de9705d6c32292ebee5e8044094a09fbf6", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0" - }, - "conflict": { - "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<5.2|>=7", - "phpdocumentor/type-resolver": "<1.5.1", - "symfony/mailer": "<6.4", - "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + "php": ">=8.4.1", + "psr/clock": "^1.0" }, - "require-dev": { - "egulias/email-validator": "^2.1.10|^3.1|^4", - "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^5.2|^6.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/property-info": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + "provide": { + "psr/clock-implementation": "1.0" }, "type": "library", "autoload": { + "files": [ + "Resources/now.php" + ], "psr-4": { - "Symfony\\Component\\Mime\\": "" + "Symfony\\Component\\Clock\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -5470,22 +6135,23 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Allows manipulating MIME messages", + "description": "Decouples applications from the system clock", "homepage": "https://symfony.com", "keywords": [ - "mime", - "mime-type" + "clock", + "psr20", + "time" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.13" + "source": "https://github.com/symfony/clock/tree/v8.1.0" }, "funding": [ { @@ -5505,45 +6171,62 @@ "type": "tidelift" } ], - "time": "2026-05-23T16:22:37+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "v1.37.0", + "name": "symfony/console", + "version": "v8.1.7", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + "url": "https://github.com/symfony/console.git", + "reference": "29afb89f4e941f68a6e90f28e3f52ff1f6793a7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", - "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "url": "https://api.github.com/repos/symfony/console/zipball/29afb89f4e941f68a6e90f28e3f52ff1f6793a7d", + "reference": "29afb89f4e941f68a6e90f28e3f52ff1f6793a7d", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4.6|^8.0.6" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/event-dispatcher": "<8.1" }, "provide": { - "ext-ctype": "*" + "psr/log-implementation": "1.0|2.0|3.0" }, - "suggest": { - "ext-ctype": "For best performance" + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5551,24 +6234,24 @@ ], "authors": [ { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for ctype functions", + "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" + "cli", + "command-line", + "console", + "terminal" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + "source": "https://github.com/symfony/console/tree/v8.1.7" }, "funding": [ { @@ -5588,42 +6271,33 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-09-13T10:55:57+00:00" }, { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.38.1", + "name": "symfony/css-selector", + "version": "v8.1.6", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + "url": "https://github.com/symfony/css-selector.git", + "reference": "08e2905152a39cf3fd1745d83f8c483e258887d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/08e2905152a39cf3fd1745d83f8c483e258887d9", + "reference": "08e2905152a39cf3fd1745d83f8c483e258887d9", "shasum": "" }, "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" + "php": ">=8.4.1" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5631,26 +6305,22 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's grapheme_* functions", + "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + "source": "https://github.com/symfony/css-selector/tree/v8.1.6" }, "funding": [ { @@ -5670,43 +6340,39 @@ "type": "tidelift" } ], - "time": "2026-05-26T05:58:03+00:00" + "time": "2026-08-23T10:06:25+00:00" }, { - "name": "symfony/polyfill-intl-idn", - "version": "v1.38.1", + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "dc21118016c039a66235cf93d96b435ffb282412" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", - "reference": "dc21118016c039a66235cf93d96b435ffb282412", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { - "php": ">=7.2", - "symfony/polyfill-intl-normalizer": "^1.10" - }, - "suggest": { - "ext-intl": "For best performance" + "php": ">=8.1" }, "type": "library", "extra": { "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" } }, "autoload": { "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" - } + "function.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5714,30 +6380,18 @@ ], "authors": [ { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" - }, - { - "name": "Trevor Rowbotham", - "email": "trevor.rowbotham@pm.me" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "idn", - "intl", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -5757,44 +6411,37 @@ "type": "tidelift" } ], - "time": "2026-05-25T15:22:23+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.38.0", + "name": "symfony/dom-crawler", + "version": "v8.1.5", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "94f70e1b2e5b7b8f8618871fd9eaad3e39e9f03d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/94f70e1b2e5b7b8f8618871fd9eaad3e39e9f03d", + "reference": "94f70e1b2e5b7b8f8618871fd9eaad3e39e9f03d", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.0" }, - "suggest": { - "ext-intl": "For best performance" + "require-dev": { + "symfony/css-selector": "^7.4|^8.0" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + "Symfony\\Component\\DomCrawler\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -5803,26 +6450,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", + "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + "source": "https://github.com/symfony/dom-crawler/tree/v8.1.5" }, "funding": [ { @@ -5842,46 +6481,49 @@ "type": "tidelift" } ], - "time": "2026-05-25T13:48:31+00:00" + "time": "2026-08-21T17:47:34+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.38.2", + "name": "symfony/error-handler", + "version": "v8.1.5", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + "url": "https://github.com/symfony/error-handler.git", + "reference": "8b2a4289ffe5e2dc8fcf645b8e7870e1fa0325ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", - "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8b2a4289ffe5e2dc8fcf645b8e7870e1fa0325ce", + "reference": "8b2a4289ffe5e2dc8fcf645b8e7870e1fa0325ce", "shasum": "" }, "require": { - "ext-iconv": "*", - "php": ">=7.2" + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^7.4|^8.0" }, - "provide": { - "ext-mbstring": "*" + "conflict": { + "symfony/deprecation-contracts": "<2.5" }, - "suggest": { - "ext-mbstring": "For best performance" + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5889,25 +6531,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + "source": "https://github.com/symfony/error-handler/tree/v8.1.5" }, "funding": [ { @@ -5927,41 +6562,53 @@ "type": "tidelift" } ], - "time": "2026-05-27T06:59:30+00:00" + "time": "2026-08-21T17:47:34+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.37.0", + "name": "symfony/event-dispatcher", + "version": "v8.1.5", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "7458da64220376b2e0dc2d8451bf43382c1ad297" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", - "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7458da64220376b2e0dc2d8451bf43382c1ad297", + "reference": "7458da64220376b2e0dc2d8451bf43382c1ad297", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" }, + "type": "library", "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" + "Symfony\\Component\\EventDispatcher\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -5970,28 +6617,18 @@ ], "authors": [ { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.5" }, "funding": [ { @@ -6011,42 +6648,40 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-08-21T17:47:34+00:00" }, { - "name": "symfony/polyfill-php83", - "version": "v1.38.2", + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8" + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8", - "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.1", + "psr/event-dispatcher": "^1" }, "type": "library", "extra": { "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" } }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php83\\": "" - }, - "classmap": [ - "Resources/stubs" - ] + "Symfony\\Contracts\\EventDispatcher\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6062,16 +6697,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "description": "Generic abstractions related to dispatching event", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" }, "funding": [ { @@ -6091,41 +6728,38 @@ "type": "tidelift" } ], - "time": "2026-05-27T06:51:48+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { - "name": "symfony/polyfill-php84", - "version": "v1.38.1", + "name": "symfony/filesystem", + "version": "v8.1.6", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" + "url": "https://github.com/symfony/filesystem.git", + "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", - "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7599ebb855fede59413ddb7f15198d67bfcae7ba", + "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } + "require-dev": { + "symfony/process": "^7.4|^8.0" }, + "type": "library", "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php84\\": "" + "Symfony\\Component\\Filesystem\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -6134,24 +6768,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" + "source": "https://github.com/symfony/filesystem/tree/v8.1.6" }, "funding": [ { @@ -6171,45 +6799,36 @@ "type": "tidelift" } ], - "time": "2026-05-26T12:51:13+00:00" + "time": "2026-08-23T10:06:25+00:00" }, { - "name": "symfony/polyfill-uuid", - "version": "v1.37.0", + "name": "symfony/finder", + "version": "v8.1.7", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + "url": "https://github.com/symfony/finder.git", + "reference": "4fbe46a3eb64abf8a57f0364075b91f4a233e062" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", - "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "url": "https://api.github.com/repos/symfony/finder/zipball/4fbe46a3eb64abf8a57f0364075b91f4a233e062", + "reference": "4fbe46a3eb64abf8a57f0364075b91f4a233e062", "shasum": "" }, "require": { - "php": ">=7.2" - }, - "provide": { - "ext-uuid": "*" + "php": ">=8.4.1" }, - "suggest": { - "ext-uuid": "For best performance" + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Uuid\\": "" - } + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6217,24 +6836,18 @@ ], "authors": [ { - "name": "Grégoire Pineau", - "email": "lyrixx@lyrixx.info" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for uuid functions", + "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "uuid" - ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" + "source": "https://github.com/symfony/finder/tree/v8.1.7" }, "funding": [ { @@ -6254,29 +6867,31 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-09-10T19:14:39+00:00" }, { - "name": "symfony/process", - "version": "v7.4.13", + "name": "symfony/html-sanitizer", + "version": "v8.1.7", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "f5804be144caceb570f6747519999636b664f24c" + "url": "https://github.com/symfony/html-sanitizer.git", + "reference": "3d79bdecd01f71bccc45ddfa1d33c024df347a1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", - "reference": "f5804be144caceb570f6747519999636b664f24c", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/3d79bdecd01f71bccc45ddfa1d33c024df347a1f", + "reference": "3d79bdecd01f71bccc45ddfa1d33c024df347a1f", "shasum": "" }, "require": { - "php": ">=8.2" + "ext-dom": "*", + "league/uri": "^6.5|^7.0", + "php": ">=8.4.1" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Process\\": "" + "Symfony\\Component\\HtmlSanitizer\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -6288,19 +6903,24 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Titouan Galopin", + "email": "galopintitouan@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Executes commands in sub-processes", + "description": "Provides an object-oriented API to sanitize untrusted HTML input for safe insertion into a document's DOM.", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v7.4.13" - }, + "keywords": [ + "Purifier", + "html", + "sanitizer" + ], + "support": { + "source": "https://github.com/symfony/html-sanitizer/tree/v8.1.7" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -6319,34 +6939,45 @@ "type": "tidelift" } ], - "time": "2026-05-23T16:05:06+00:00" + "time": "2026-09-04T10:14:04+00:00" }, { - "name": "symfony/property-access", - "version": "v7.4.8", + "name": "symfony/http-foundation", + "version": "v8.1.7", "source": { "type": "git", - "url": "https://github.com/symfony/property-access.git", - "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc" + "url": "https://github.com/symfony/http-foundation.git", + "reference": "d8fdc670ed510a69e3a9eb4f5eac89f5ed627e17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", - "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d8fdc670ed510a69e3a9eb4f5eac89f5ed627e17", + "reference": "d8fdc670ed510a69e3a9eb4f5eac89f5ed627e17", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4|^8.0.4" + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<4.3" }, "require-dev": { - "symfony/cache": "^6.4|^7.0|^8.0", - "symfony/var-exporter": "^6.4.1|^7.0.1|^8.0" + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\PropertyAccess\\": "" + "Symfony\\Component\\HttpFoundation\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -6366,21 +6997,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", - "keywords": [ - "access", - "array", - "extraction", - "index", - "injection", - "object", - "property", - "property-path", - "reflection" - ], "support": { - "source": "https://github.com/symfony/property-access/tree/v7.4.8" + "source": "https://github.com/symfony/http-foundation/tree/v8.1.7" }, "funding": [ { @@ -6400,46 +7020,74 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-09-14T17:47:24+00:00" }, { - "name": "symfony/property-info", - "version": "v7.4.8", + "name": "symfony/http-kernel", + "version": "v8.1.7", "source": { "type": "git", - "url": "https://github.com/symfony/property-info.git", - "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175" + "url": "https://github.com/symfony/http-kernel.git", + "reference": "ec7a3a5832c22cf880cf27d2fdc7ad2e94838e85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/ac5e82528b986c4f7cfccbf7764b5d2e824d6175", - "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ec7a3a5832c22cf880cf27d2fdc7ad2e94838e85", + "reference": "ec7a3a5832c22cf880cf27d2fdc7ad2e94838e85", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0|^8.0", - "symfony/type-info": "^7.4.7|^8.0.7" + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "phpdocumentor/reflection-docblock": "<5.2|>=7", - "phpdocumentor/type-resolver": "<1.5.1", - "symfony/cache": "<6.4", - "symfony/dependency-injection": "<6.4", - "symfony/serializer": "<6.4" + "symfony/dependency-injection": "<8.1", + "symfony/flex": "<2.10", + "symfony/http-client-contracts": "<2.5", + "symfony/serializer": "<7.4.15|>=8.0,<8.0.15|>=8.1,<8.1.2", + "symfony/translation-contracts": "<2.5", + "symfony/var-dumper": "<8.1", + "symfony/web-profiler-bundle": "<8.1", + "twig/twig": "<3.21" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { - "phpdocumentor/reflection-docblock": "^5.2|^6.0", - "phpstan/phpdoc-parser": "^1.0|^2.0", - "symfony/cache": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0" + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^8.1", + "symfony/var-exporter": "^7.4|^8.0", + "twig/twig": "^3.21|^4.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\PropertyInfo\\": "" + "Symfony\\Component\\HttpKernel\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -6451,26 +7099,18 @@ ], "authors": [ { - "name": "Kévin Dunglas", - "email": "dunglas@gmail.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Extracts information about PHP class' properties using metadata of popular sources", + "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", - "keywords": [ - "doctrine", - "phpdoc", - "property", - "symfony", - "type", - "validator" - ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.4.8" + "source": "https://github.com/symfony/http-kernel/tree/v8.1.7" }, "funding": [ { @@ -6490,68 +7130,48 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-09-15T07:12:52+00:00" }, { - "name": "symfony/serializer", - "version": "v7.4.14", + "name": "symfony/mailer", + "version": "v7.4.19", "source": { "type": "git", - "url": "https://github.com/symfony/serializer.git", - "reference": "55acb01b9c8a5211dfbaf68c314d90d0ed2cc3d1" + "url": "https://github.com/symfony/mailer.git", + "reference": "6f6f2441bef07a42d2617b9b3aadf04a321aa514" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/55acb01b9c8a5211dfbaf68c314d90d0ed2cc3d1", - "reference": "55acb01b9c8a5211dfbaf68c314d90d0ed2cc3d1", + "url": "https://api.github.com/repos/symfony/mailer/zipball/6f6f2441bef07a42d2617b9b3aadf04a321aa514", + "reference": "6f6f2441bef07a42d2617b9b3aadf04a321aa514", "shasum": "" }, "require": { + "egulias/email-validator": "^2.1.10|^3|^4", "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-php84": "^1.30" + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "phpdocumentor/reflection-docblock": "<5.2|>=7", - "phpdocumentor/type-resolver": "<1.5.1", - "symfony/dependency-injection": "<6.4", - "symfony/property-access": "<6.4.31|>=7.0,<7.4.2|>=8.0,<8.0.2", - "symfony/property-info": "<6.4", - "symfony/type-info": "<7.2.5", - "symfony/uid": "<6.4", - "symfony/validator": "<6.4", - "symfony/yaml": "<6.4" + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" }, "require-dev": { - "phpdocumentor/reflection-docblock": "^5.2|^6.0", - "phpstan/phpdoc-parser": "^1.0|^2.0", - "seld/jsonlint": "^1.10", - "symfony/cache": "^6.4|^7.0|^8.0", - "symfony/config": "^6.4|^7.0|^8.0", "symfony/console": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^7.2|^8.0", - "symfony/error-handler": "^6.4|^7.0|^8.0", - "symfony/filesystem": "^6.4|^7.0|^8.0", - "symfony/form": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/mime": "^6.4|^7.0|^8.0", - "symfony/property-access": "^6.4.31|^7.4.2|^8.0.2", - "symfony/property-info": "^6.4|^7.0|^8.0", - "symfony/translation-contracts": "^2.5|^3", - "symfony/type-info": "^7.2.5|^8.0", - "symfony/uid": "^6.4|^7.0|^8.0", - "symfony/validator": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0", - "symfony/var-exporter": "^6.4|^7.0|^8.0", - "symfony/yaml": "^6.4|^7.0|^8.0" + "symfony/twig-bridge": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Serializer\\": "" + "Symfony\\Component\\Mailer\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -6571,10 +7191,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v7.4.14" + "source": "https://github.com/symfony/mailer/tree/v7.4.19" }, "funding": [ { @@ -6594,46 +7214,52 @@ "type": "tidelift" } ], - "time": "2026-06-27T08:31:18+00:00" + "time": "2026-09-15T06:01:15+00:00" }, { - "name": "symfony/service-contracts", - "version": "v3.7.1", + "name": "symfony/mime", + "version": "v7.4.19", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" + "url": "https://github.com/symfony/mime.git", + "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", - "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", + "url": "https://api.github.com/repos/symfony/mime/zipball/f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693", + "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693", "shasum": "" }, "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { - "ext-psr": "<1.1|>=2" + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.7-dev" - } + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.44|^7.4.17|^8.1.5" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Contracts\\Service\\": "" + "Symfony\\Component\\Mime\\": "" }, "exclude-from-classmap": [ - "/Test/" + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -6642,26 +7268,22 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "Allows manipulating MIME messages", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "mime", + "mime-type" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" + "source": "https://github.com/symfony/mime/tree/v7.4.19" }, "funding": [ { @@ -6681,51 +7303,45 @@ "type": "tidelift" } ], - "time": "2026-06-16T09:55:08+00:00" + "time": "2026-09-04T10:45:44+00:00" }, { - "name": "symfony/string", - "version": "v7.4.13", + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", "source": { "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", - "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.33", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=7.2" }, - "conflict": { - "symfony/translation-contracts": "<2.5" + "provide": { + "ext-ctype": "*" }, - "require-dev": { - "symfony/emoji": "^7.1|^8.0", - "symfony/http-client": "^6.4|^7.0|^8.0", - "symfony/intl": "^6.4|^7.0|^8.0", - "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0|^8.0" + "suggest": { + "ext-ctype": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { "files": [ - "Resources/functions.php" + "bootstrap.php" ], "psr-4": { - "Symfony\\Component\\String\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6733,26 +7349,24 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" + "compatibility", + "ctype", + "polyfill", + "portable" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.13" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -6772,67 +7386,42 @@ "type": "tidelift" } ], - "time": "2026-05-23T15:23:29+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { - "name": "symfony/translation", - "version": "v6.4.42", + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.41.0", "source": { "type": "git", - "url": "https://github.com/symfony/translation.git", - "reference": "fef99cef37890b350976f5f492854faefadd4e15" + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/fef99cef37890b350976f5f492854faefadd4e15", - "reference": "fef99cef37890b350976f5f492854faefadd4e15", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" - }, - "conflict": { - "symfony/config": "<5.4", - "symfony/console": "<5.4", - "symfony/dependency-injection": "<5.4", - "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<5.4", - "symfony/service-contracts": "<2.5", - "symfony/twig-bundle": "<5.4", - "symfony/yaml": "<5.4" - }, - "provide": { - "symfony/translation-implementation": "2.3|3.0" + "php": ">=7.2" }, - "require-dev": { - "nikic/php-parser": "^4.18|^5.0", - "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", - "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/intl": "^5.4|^6.0|^7.0", - "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^5.4|^6.0|^7.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^5.4|^6.0|^7.0" + "suggest": { + "ext-intl": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { "files": [ - "Resources/functions.php" + "bootstrap.php" ], "psr-4": { - "Symfony\\Component\\Translation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6840,18 +7429,26 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides tools to internationalize your application", + "description": "Symfony polyfill for intl's grapheme_* functions", "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], "support": { - "source": "https://github.com/symfony/translation/tree/v6.4.42" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" }, "funding": [ { @@ -6871,42 +7468,43 @@ "type": "tidelift" } ], - "time": "2026-06-05T16:46:18+00:00" + "time": "2026-07-28T08:25:59+00:00" }, { - "name": "symfony/translation-contracts", - "version": "v3.7.1", + "name": "symfony/polyfill-intl-idn", + "version": "v1.42.0", "source": { "type": "git", - "url": "https://github.com/symfony/translation-contracts.git", - "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "51b5ff5ba85452b31ec6f55490b08148612339d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", - "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/51b5ff5ba85452b31ec6f55490b08148612339d9", + "reference": "51b5ff5ba85452b31ec6f55490b08148612339d9", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" }, - "type": "library", + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", "extra": { "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.7-dev" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Contracts\\Translation\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6914,26 +7512,30 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to translation", + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.42.0" }, "funding": [ { @@ -6953,40 +7555,44 @@ "type": "tidelift" } ], - "time": "2026-06-05T06:23:12+00:00" + "time": "2026-08-24T10:51:20+00:00" }, { - "name": "symfony/type-info", - "version": "v7.4.9", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.42.0", "source": { "type": "git", - "url": "https://github.com/symfony/type-info.git", - "reference": "cafeedbf157b890e94ac5b83eaed85595106d5d6" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/cafeedbf157b890e94ac5b83eaed85595106d5d6", - "reference": "cafeedbf157b890e94ac5b83eaed85595106d5d6", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502", "shasum": "" }, "require": { - "php": ">=8.2", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "conflict": { - "phpstan/phpdoc-parser": "<1.30" + "php": ">=7.2" }, - "require-dev": { - "phpstan/phpdoc-parser": "^1.30|^2.0" + "suggest": { + "ext-intl": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\TypeInfo\\": "" + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -6995,28 +7601,26 @@ ], "authors": [ { - "name": "Mathias Arlaud", - "email": "mathias.arlaud@gmail.com" - }, - { - "name": "Baptiste LEDUC", - "email": "baptiste.leduc@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Extracts PHP types information.", + "description": "Symfony polyfill for intl's Normalizer class and related functions", "homepage": "https://symfony.com", "keywords": [ - "PHPStan", - "phpdoc", - "symfony", - "type" + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.4.9" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0" }, "funding": [ { @@ -7036,47 +7640,52 @@ "type": "tidelift" } ], - "time": "2026-04-22T15:21:55+00:00" + "time": "2026-08-07T06:33:24+00:00" }, { - "name": "symfony/uid", - "version": "v7.4.9", + "name": "symfony/polyfill-mbstring", + "version": "v1.38.2", "source": { "type": "git", - "url": "https://github.com/symfony/uid.git", - "reference": "2676b524340abcfe4d6151ec698463cebafee439" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/2676b524340abcfe4d6151ec698463cebafee439", - "reference": "2676b524340abcfe4d6151ec698463cebafee439", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-uuid": "^1.15" + "ext-iconv": "*", + "php": ">=7.2" }, - "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0" + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\Uid\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ - { - "name": "Grégoire Pineau", - "email": "lyrixx@lyrixx.info" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -7086,15 +7695,17 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides an object-oriented API to generate and represent UIDs", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", "keywords": [ - "UID", - "ulid", - "uuid" + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.9" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -7114,50 +7725,41 @@ "type": "tidelift" } ], - "time": "2026-04-30T15:19:22+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { - "name": "symfony/var-dumper", - "version": "v7.4.14", + "name": "symfony/polyfill-php80", + "version": "v1.37.0", "source": { "type": "git", - "url": "https://github.com/symfony/var-dumper.git", - "reference": "9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358", - "reference": "9a3a56a4a1e65a5cb4f8d13801fe8ab0a170e358", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/console": "<6.4" - }, - "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/uid": "^6.4|^7.0|^8.0", - "twig/twig": "^3.12" + "php": ">=7.2" }, - "bin": [ - "Resources/bin/var-dump-server" - ], "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { "files": [ - "Resources/functions/dump.php" + "bootstrap.php" ], "psr-4": { - "Symfony\\Component\\VarDumper\\": "" + "Symfony\\Polyfill\\Php80\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -7165,6 +7767,10 @@ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -7174,14 +7780,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ - "debug", - "dump" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.14" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" }, "funding": [ { @@ -7201,43 +7809,41 @@ "type": "tidelift" } ], - "time": "2026-06-08T20:24:16+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { - "name": "symfony/yaml", - "version": "v7.4.14", + "name": "symfony/polyfill-php84", + "version": "v1.38.1", "source": { "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "f8f328665ace2370d1e10645b807ba1646dc7dcc" + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/f8f328665ace2370d1e10645b807ba1646dc7dcc", - "reference": "f8f328665ace2370d1e10645b807ba1646dc7dcc", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "symfony/console": "<6.4" - }, - "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0" + "php": ">=7.2" }, - "bin": [ - "Resources/bin/yaml-lint" - ], "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\Yaml\\": "" + "Symfony\\Polyfill\\Php84\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -7246,18 +7852,24 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Loads and dumps YAML files", + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.14" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" }, "funding": [ { @@ -7277,257 +7889,2156 @@ "type": "tidelift" } ], - "time": "2026-06-08T20:24:16+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { - "name": "tecnickcom/tcpdf", - "version": "6.11.3", + "name": "symfony/polyfill-php85", + "version": "v1.41.0", "source": { "type": "git", - "url": "https://github.com/tecnickcom/TCPDF.git", - "reference": "b18f6119161019916c5bb07cb8da5205ae5c1b63" + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/b18f6119161019916c5bb07cb8da5205ae5c1b63", - "reference": "b18f6119161019916c5bb07cb8da5205ae5c1b63", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", "shasum": "" }, "require": { - "ext-curl": "*", - "php": ">=7.1.0" - }, - "suggest": { - "ext-gd": "Enables additional image handling in some workflows.", - "ext-imagick": "Enables additional image format support when available.", - "ext-zlib": "Recommended for compressed streams and related features.", - "tecnickcom/tc-lib-pdf": "Modern replacement for TCPDF for new projects." + "php": ">=7.2" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, "classmap": [ - "config", - "include", - "tcpdf.php", - "tcpdf_barcodes_1d.php", - "tcpdf_barcodes_2d.php", - "include/tcpdf_colors.php", - "include/tcpdf_filters.php", - "include/tcpdf_font_data.php", - "include/tcpdf_fonts.php", - "include/tcpdf_images.php", - "include/tcpdf_static.php", - "include/barcodes/datamatrix.php", - "include/barcodes/pdf417.php", - "include/barcodes/qrcode.php" + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "MIT" ], "authors": [ { - "name": "Nicola Asuni", - "email": "info@tecnick.com", - "role": "lead" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Deprecated legacy PDF engine for PHP. For new projects use tecnickcom/tc-lib-pdf.", - "homepage": "https://tcpdf.org", + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "PDFD32000-2008", - "TCPDF", - "barcodes", - "datamatrix", - "pdf", - "pdf417", - "qrcode" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/tecnickcom/TCPDF/issues", - "source": "https://github.com/tecnickcom/TCPDF" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" }, "funding": [ { - "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", - "type": "paypal" + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2026-04-21T17:00:18+00:00" + "time": "2026-07-01T12:47:55+00:00" }, { - "name": "thecodingmachine/safe", - "version": "v3.4.0", + "name": "symfony/polyfill-php86", + "version": "v1.41.0", "source": { "type": "git", - "url": "https://github.com/thecodingmachine/safe.git", - "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" + "url": "https://github.com/symfony/polyfill-php86.git", + "reference": "6bc356ed3d8dbfeea8f0de235e34d670704e880e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", - "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/6bc356ed3d8dbfeea8f0de235e34d670704e880e", + "reference": "6bc356ed3d8dbfeea8f0de235e34d670704e880e", "shasum": "" }, "require": { - "php": "^8.1" - }, - "require-dev": { - "php-parallel-lint/php-parallel-lint": "^1.4", - "phpstan/phpstan": "^2", - "phpunit/phpunit": "^10", - "squizlabs/php_codesniffer": "^3.2" + "php": ">=7.2" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { "files": [ - "lib/special_cases.php", - "generated/apache.php", - "generated/apcu.php", - "generated/array.php", - "generated/bzip2.php", - "generated/calendar.php", - "generated/classobj.php", - "generated/com.php", - "generated/cubrid.php", - "generated/curl.php", - "generated/datetime.php", - "generated/dir.php", - "generated/eio.php", - "generated/errorfunc.php", - "generated/exec.php", - "generated/fileinfo.php", - "generated/filesystem.php", - "generated/filter.php", - "generated/fpm.php", - "generated/ftp.php", - "generated/funchand.php", - "generated/gettext.php", - "generated/gmp.php", - "generated/gnupg.php", - "generated/hash.php", - "generated/ibase.php", - "generated/ibmDb2.php", - "generated/iconv.php", - "generated/image.php", - "generated/imap.php", - "generated/info.php", - "generated/inotify.php", - "generated/json.php", - "generated/ldap.php", - "generated/libxml.php", - "generated/lzf.php", - "generated/mailparse.php", - "generated/mbstring.php", - "generated/misc.php", - "generated/mysql.php", - "generated/mysqli.php", - "generated/network.php", - "generated/oci8.php", - "generated/opcache.php", - "generated/openssl.php", - "generated/outcontrol.php", - "generated/pcntl.php", - "generated/pcre.php", - "generated/pgsql.php", - "generated/posix.php", - "generated/ps.php", - "generated/pspell.php", - "generated/readline.php", - "generated/rnp.php", - "generated/rpminfo.php", - "generated/rrd.php", - "generated/sem.php", - "generated/session.php", - "generated/shmop.php", - "generated/sockets.php", - "generated/sodium.php", - "generated/solr.php", - "generated/spl.php", - "generated/sqlsrv.php", - "generated/ssdeep.php", - "generated/ssh2.php", - "generated/stream.php", - "generated/strings.php", - "generated/swoole.php", - "generated/uodbc.php", - "generated/uopz.php", - "generated/url.php", - "generated/var.php", - "generated/xdiff.php", - "generated/xml.php", - "generated/xmlrpc.php", - "generated/yaml.php", - "generated/yaz.php", - "generated/zip.php", - "generated/zlib.php" + "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php86\\": "" + }, "classmap": [ - "lib/DateTime.php", - "lib/DateTimeImmutable.php", - "lib/Exceptions/", - "generated/Exceptions/" + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "support": { - "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" - }, - "funding": [ + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php86/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-02T13:42:24+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "ed0ae095b86994d370d5791612e55984f15aa30e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/ed0ae095b86994d370d5791612e55984f15aa30e", + "reference": "ed0ae095b86994d370d5791612e55984f15aa30e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-02T12:38:34+00:00" + }, + { + "name": "symfony/property-access", + "version": "v8.1.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "1a41232c678972b93ce499a504e19ea09dfcd0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/1a41232c678972b93ce499a504e19ea09dfcd0b2", + "reference": "1a41232c678972b93ce499a504e19ea09dfcd0b2", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/property-info": "^7.4.4|^8.0.4" + }, + "require-dev": { + "symfony/cache": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v8.1.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-30T12:40:56+00:00" + }, + { + "name": "symfony/property-info", + "version": "v8.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "b42ee98197831d33788c33492cfc35ba62d1f701" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/b42ee98197831d33788c33492cfc35ba62d1f701", + "reference": "b42ee98197831d33788c33492cfc35ba62d1f701", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/string": "^7.4|^8.0", + "symfony/type-info": "^7.4.7|^8.0.7" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v8.1.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-04T10:14:04+00:00" + }, + { + "name": "symfony/routing", + "version": "v8.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "3c188091b6b4fa2e4bc83a135caede12deb8576c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/3c188091b6b4fa2e4bc83a135caede12deb8576c", + "reference": "3c188091b6b4fa2e4bc83a135caede12deb8576c", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v8.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-17T13:18:34+00:00" + }, + { + "name": "symfony/serializer", + "version": "v8.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "9a88015b4bb1a2bc5a3bbdf6ca3025444ba67321" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/9a88015b4bb1a2bc5a3bbdf6ca3025444ba67321", + "reference": "9a88015b4bb1a2bc5a3bbdf6ca3025444ba67321", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/property-access": "<8.1", + "symfony/property-info": "<7.4.15", + "symfony/type-info": "<7.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/property-access": "^8.1", + "symfony/property-info": "^7.4.15|~8.0.15|^8.1.2", + "symfony/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v8.1.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-08T13:39:13+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-27T15:39:01+00:00" + }, + { + "name": "symfony/string", + "version": "v8.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/d950140b5f56f31901e5b7a0c04ffc3a3deb943c", + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.1.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-11T14:51:16+00:00" + }, + { + "name": "symfony/translation", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "d9e1caba0d6b6f9a26710af8a2f88d37f001215a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/d9e1caba0d6b6f9a26710af8a2f88d37f001215a", + "reference": "d9e1caba0d6b6f9a26710af8a2f88d37f001215a", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/http-client-contracts": "<2.5", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:47:34+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/type-info", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "ceb48db5b38d6a48640c414be0c69d53980ae5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/ceb48db5b38d6a48640c414be0c69d53980ae5c5", + "reference": "ceb48db5b38d6a48640c414be0c69d53980ae5c5", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.30|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:47:34+00:00" + }, + { + "name": "symfony/uid", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "a08aef47989093f32fe50fd11859be1b427df389" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/a08aef47989093f32fe50fd11859be1b427df389", + "reference": "a08aef47989093f32fe50fd11859be1b427df389", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-11T13:39:01+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v8.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "741a8c4c948b0bb9bd3653daacd3644c73c40ea5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/741a8c4c948b0bb9bd3653daacd3644c73c40ea5", + "reference": "741a8c4c948b0bb9bd3653daacd3644c73c40ea5", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "twig/twig": "^3.12|^4.0" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v8.1.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-03T16:02:27+00:00" + }, + { + "name": "symfony/yaml", + "version": "v8.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "0b4aa53a67f9fece88c665f1a1dadcfd25d93fe5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0b4aa53a67f9fece88c665f1a1dadcfd25d93fe5", + "reference": "0b4aa53a67f9fece88c665f1a1dadcfd25d93fe5", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "yaml/yaml-test-suite": "*" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v8.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-30T01:03:44+00:00" + }, + { + "name": "tecnickcom/tcpdf", + "version": "6.11.3", + "source": { + "type": "git", + "url": "https://github.com/tecnickcom/TCPDF.git", + "reference": "b18f6119161019916c5bb07cb8da5205ae5c1b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/b18f6119161019916c5bb07cb8da5205ae5c1b63", + "reference": "b18f6119161019916c5bb07cb8da5205ae5c1b63", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=7.1.0" + }, + "suggest": { + "ext-gd": "Enables additional image handling in some workflows.", + "ext-imagick": "Enables additional image format support when available.", + "ext-zlib": "Recommended for compressed streams and related features.", + "tecnickcom/tc-lib-pdf": "Modern replacement for TCPDF for new projects." + }, + "type": "library", + "autoload": { + "classmap": [ + "config", + "include", + "tcpdf.php", + "tcpdf_barcodes_1d.php", + "tcpdf_barcodes_2d.php", + "include/tcpdf_colors.php", + "include/tcpdf_filters.php", + "include/tcpdf_font_data.php", + "include/tcpdf_fonts.php", + "include/tcpdf_images.php", + "include/tcpdf_static.php", + "include/barcodes/datamatrix.php", + "include/barcodes/pdf417.php", + "include/barcodes/qrcode.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Nicola Asuni", + "email": "info@tecnick.com", + "role": "lead" + } + ], + "description": "Deprecated legacy PDF engine for PHP. For new projects use tecnickcom/tc-lib-pdf.", + "homepage": "https://tcpdf.org", + "keywords": [ + "PDFD32000-2008", + "TCPDF", + "barcodes", + "datamatrix", + "pdf", + "pdf417", + "qrcode" + ], + "support": { + "issues": "https://github.com/tecnickcom/TCPDF/issues", + "source": "https://github.com/tecnickcom/TCPDF" + }, + "funding": [ + { + "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", + "type": "paypal" + } + ], + "time": "2026-04-21T17:00:18+00:00" + }, + { + "name": "thecodingmachine/safe", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2026-02-04T18:08:13+00:00" + }, + { + "name": "theiconic/name-parser", + "version": "v1.2.11", + "source": { + "type": "git", + "url": "https://github.com/theiconic/name-parser.git", + "reference": "9a54a713bf5b2e7fd990828147d42de16bf8a253" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theiconic/name-parser/zipball/9a54a713bf5b2e7fd990828147d42de16bf8a253", + "reference": "9a54a713bf5b2e7fd990828147d42de16bf8a253", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "php-mock/php-mock-phpunit": "^2.1", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "TheIconic\\NameParser\\": [ + "src/", + "tests/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "The Iconic", + "email": "engineering@theiconic.com.au" + } + ], + "description": "PHP library for parsing a string containing a full name into its parts", + "support": { + "issues": "https://github.com/theiconic/name-parser/issues", + "source": "https://github.com/theiconic/name-parser/tree/v1.2.11" + }, + "time": "2019-11-14T14:08:48+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "tpetry/laravel-query-expressions", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/tpetry/laravel-query-expressions.git", + "reference": "e9e6c7e7c570de6820b617868e8073a1fa71f461" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tpetry/laravel-query-expressions/zipball/e9e6c7e7c570de6820b617868e8073a1fa71f461", + "reference": "e9e6c7e7c570de6820b617868e8073a1fa71f461", + "shasum": "" + }, + "require": { + "laravel/framework": "^10.13.1|^11.0|^12.0|^13.0", + "php": "^8.1" + }, + "require-dev": { + "laravel/pint": "^1.0", + "orchestra/testbench": "^8.0|^9.0|^10.0|^11.0", + "pestphp/pest": "^2.28.1|^3.0.0", + "pestphp/pest-plugin-laravel": "^2.2.0|^3.0.0", + "phpstan/phpstan": "^1.11|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Tpetry\\QueryExpressions\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "tpetry", + "email": "github@tpetry.me", + "role": "Developer" + } + ], + "description": "Database-independent Query Expressions as a replacement to DB::raw calls", + "homepage": "https://github.com/tpetry/laravel-query-expressions", + "keywords": [ + "database", + "expression", + "laravel", + "query" + ], + "support": { + "issues": "https://github.com/tpetry/laravel-query-expressions/issues", + "source": "https://github.com/tpetry/laravel-query-expressions/tree/1.6.0" + }, + "time": "2026-03-13T08:48:18+00:00" + }, + { + "name": "twig/twig", + "version": "v3.28.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/597c12ed286fb9d1701a36684ce6e0cbe28ebc8b", + "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "php-cs-fixer/shim": "^3.0@stable", + "phpstan/phpstan": "^2.0@stable", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.28.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-07-03T20:44:34+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "301c07936b16d88628b126b01d082ba153cf4c40" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/301c07936b16d88628b126b01d082ba153cf4c40", + "reference": "301c07936b16d88628b126b01d082ba153cf4c40", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.2", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.10", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `$_ENV` and `$_SERVER` automagically, and optionally to `getenv()`.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2026-08-24T18:07:49+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2026-04-26T05:33:54+00:00" + }, + { + "name": "web-auth/cose-lib", + "version": "4.8.1", + "source": { + "type": "git", + "url": "https://github.com/web-auth/cose-lib.git", + "reference": "a4359209df2dfe81d083bcf33eda95a93fcaedb9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/a4359209df2dfe81d083bcf33eda95a93fcaedb9", + "reference": "a4359209df2dfe81d083bcf33eda95a93fcaedb9", + "shasum": "" + }, + "require": { + "brick/math": "^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0", + "ext-json": "*", + "ext-openssl": "*", + "php": ">=8.1", + "spomky-labs/pki-framework": "^1.0" + }, + "conflict": { + "spomky-labs/cbor-php": "<3.4.0" + }, + "replace": { + "symfony/polyfill-php81": "*" + }, + "require-dev": { + "spomky-labs/cbor-php": "^3.4" + }, + "suggest": { + "ext-bcmath": "Recommended: without GMP or BCMath, signing with RSASSA-PSS (PS256/PS384/PS512) blinds its private exponentiation in pure PHP", + "ext-gmp": "Recommended: without GMP or BCMath, signing with RSASSA-PSS (PS256/PS384/PS512) blinds its private exponentiation in pure PHP", + "ext-sodium": "Required by the EdDSA/Ed25519 signature algorithms (-8, -19, -260, -261) and to recompute an OKP public key from its private key", + "spomky-labs/cbor-php": "Required by the RFC 9052 header reader and cryptographic structures. 3.4.0 or later: it ships the six COSE message classes (CBOR\\Tag\\CoseSign1Tag and its siblings) that replace the deprecated Cose\\...Tag classes, and its decoder is what enforces the RFC 9052 label uniqueness and nesting bounds this library relies on" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cose\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/cose/contributors" + } + ], + "description": "CBOR Object Signing and Encryption (COSE) For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "COSE", + "RFC8152" + ], + "support": { + "issues": "https://github.com/web-auth/cose-lib/issues", + "source": "https://github.com/web-auth/cose-lib/tree/4.8.1" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-09-12T18:56:36+00:00" + }, + { + "name": "web-auth/webauthn-lib", + "version": "5.3.9", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-lib.git", + "reference": "727e378fb7a36c26be5c911e4a0120c146741ce7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/727e378fb7a36c26be5c911e4a0120c146741ce7", + "reference": "727e378fb7a36c26be5c911e4a0120c146741ce7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "paragonie/constant_time_encoding": "^2.6|^3.0", + "php": ">=8.2", + "phpdocumentor/reflection-docblock": "^5.3|^6.0", + "psr/clock": "^1.0", + "psr/event-dispatcher": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "spomky-labs/cbor-php": "^3.4", + "spomky-labs/pki-framework": "^1.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^3.2", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "web-auth/cose-lib": "^4.8" + }, + "suggest": { + "psr/log-implementation": "Recommended to receive logs from the library", + "symfony/event-dispatcher": "Recommended to use dispatched events", + "web-token/jwt-library": "Mandatory for fetching Metadata Statement from distant sources" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/web-auth/webauthn-framework", + "name": "web-auth/webauthn-framework" + } + }, + "autoload": { + "psr-4": { + "Webauthn\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://github.com/OskarStark", - "type": "github" + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" }, { - "url": "https://github.com/shish", - "type": "github" - }, + "name": "All contributors", + "homepage": "https://github.com/web-auth/webauthn-library/contributors" + } + ], + "description": "FIDO2/Webauthn Support For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "FIDO2", + "fido", + "webauthn" + ], + "support": { + "source": "https://github.com/web-auth/webauthn-lib/tree/5.3.9" + }, + "funding": [ { - "url": "https://github.com/silasjoisten", + "url": "https://github.com/Spomky", "type": "github" }, { - "url": "https://github.com/staabm", - "type": "github" + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" } ], - "time": "2026-02-04T18:08:13+00:00" + "time": "2026-09-10T21:10:45+00:00" }, { - "name": "theiconic/name-parser", - "version": "v1.2.11", + "name": "webmozart/assert", + "version": "2.4.1", "source": { "type": "git", - "url": "https://github.com/theiconic/name-parser.git", - "reference": "9a54a713bf5b2e7fd990828147d42de16bf8a253" + "url": "https://github.com/webmozarts/assert.git", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theiconic/name-parser/zipball/9a54a713bf5b2e7fd990828147d42de16bf8a253", - "reference": "9a54a713bf5b2e7fd990828147d42de16bf8a253", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "php-mock/php-mock-phpunit": "^2.1", - "phpunit/phpunit": "^7.0" + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", + "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, + "branch-alias": { + "dev-master": "2.0-dev", + "dev-feature/2-0": "2.0-dev" + } + }, "autoload": { "psr-4": { - "TheIconic\\NameParser\\": [ - "src/", - "tests/" - ] + "Webmozart\\Assert\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7536,926 +10047,929 @@ ], "authors": [ { - "name": "The Iconic", - "email": "engineering@theiconic.com.au" + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" } ], - "description": "PHP library for parsing a string containing a full name into its parts", + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], "support": { - "issues": "https://github.com/theiconic/name-parser/issues", - "source": "https://github.com/theiconic/name-parser/tree/v1.2.11" + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.4.1" }, - "time": "2019-11-14T14:08:48+00:00" + "time": "2026-06-15T15:31:57+00:00" }, { - "name": "twig/twig", - "version": "v3.27.1", + "name": "webonyx/graphql-php", + "version": "v15.33.1", "source": { "type": "git", - "url": "https://github.com/twigphp/Twig.git", - "reference": "ae2071bffb38f04847fc0864d730c94b9cb8ab74" + "url": "https://github.com/webonyx/graphql-php.git", + "reference": "e0f40ce40a527ee27413cceced4825aacbc7de5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/ae2071bffb38f04847fc0864d730c94b9cb8ab74", - "reference": "ae2071bffb38f04847fc0864d730c94b9cb8ab74", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/e0f40ce40a527ee27413cceced4825aacbc7de5b", + "reference": "e0f40ce40a527ee27413cceced4825aacbc7de5b", "shasum": "" }, "require": { - "php": ">=8.1.0", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3" + "ext-json": "*", + "ext-mbstring": "*", + "php": "^7.4 || ^8" }, "require-dev": { - "php-cs-fixer/shim": "^3.0@stable", - "phpstan/phpstan": "^2.0@stable", - "psr/container": "^1.0|^2.0", - "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + "amphp/amp": "^2.6 || ^3", + "amphp/http-server": "^2.1 || ^3", + "dms/phpunit-arraysubset-asserts": "dev-master", + "ergebnis/composer-normalize": "^2.28", + "friendsofphp/php-cs-fixer": "3.95.8", + "mll-lab/php-cs-fixer-config": "5.13.0", + "nyholm/psr7": "^1.5", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "2.2.2", + "phpstan/phpstan-phpunit": "2.0.16", + "phpstan/phpstan-strict-rules": "2.0.11", + "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", + "psr/http-message": "^1 || ^2", + "react/http": "^1.6", + "react/promise": "^2.0 || ^3.0", + "rector/rector": "^2.0", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5 || ^6 || ^7 || ^8", + "thecodingmachine/safe": "^1.3 || ^2 || ^3", + "ticketswap/phpstan-error-formatter": "1.3.0" + }, + "suggest": { + "amphp/amp": "To leverage async resolving on AMPHP platform (v3 with AmpFutureAdapter, v2 with AmpPromiseAdapter)", + "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", + "psr/http-message": "To use standard GraphQL server", + "react/promise": "To leverage async resolving on React PHP platform" }, "type": "library", "autoload": { - "files": [ - "src/Resources/core.php", - "src/Resources/debug.php", - "src/Resources/escaper.php", - "src/Resources/string_loader.php" - ], "psr-4": { - "Twig\\": "src/" + "GraphQL\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "authors": [ + "description": "A PHP port of GraphQL reference implementation", + "homepage": "https://github.com/webonyx/graphql-php", + "keywords": [ + "api", + "graphql" + ], + "support": { + "issues": "https://github.com/webonyx/graphql-php/issues", + "source": "https://github.com/webonyx/graphql-php/tree/v15.33.1" + }, + "funding": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" + "url": "https://github.com/spawnia", + "type": "github" }, { - "name": "Twig Team", - "role": "Contributors" + "url": "https://opencollective.com/webonyx-graphql-php", + "type": "open_collective" + } + ], + "time": "2026-06-17T06:05:59+00:00" + }, + { + "name": "yiisoft/aliases", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/aliases.git", + "reference": "6f876dbf899f604fd3aefa3b3fd37e2ff2549ead" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/aliases/zipball/6f876dbf899f604fd3aefa3b3fd37e2ff2549ead", + "reference": "6f876dbf899f604fd3aefa3b3fd37e2ff2549ead", + "shasum": "" + }, + "require": { + "php": "8.1 - 8.5" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.3", + "maglnet/composer-require-checker": "^4.7.1", + "phpunit/phpunit": "^10.5.48", + "psr/container": "^2.0.2", + "rector/rector": "^2.1.2", + "spatie/phpunit-watcher": "^1.24.0", + "yiisoft/definitions": "^3.4", + "yiisoft/di": "^1.4", + "yiisoft/test-support": "^3.0.2" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true, + "target-directory": "tools" }, - { - "name": "Armin Ronacher", - "email": "armin.ronacher@active-4.com", - "role": "Project Founder" + "config-plugin": { + "di": "di.php", + "params": "params.php" + }, + "config-plugin-options": { + "source-directory": "config" + } + }, + "autoload": { + "psr-4": { + "Yiisoft\\Aliases\\": "src" } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" ], - "description": "Twig, the flexible, fast, and secure template language for PHP", - "homepage": "https://twig.symfony.com", + "description": "Named paths and URLs storage", + "homepage": "https://www.yiiframework.com/", "keywords": [ - "templating" + "alias" ], "support": { - "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.27.1" + "chat": "https://t.me/yii3en", + "forum": "https://www.yiiframework.com/forum/", + "irc": "ircs://irc.libera.chat:6697/yii", + "issues": "https://github.com/yiisoft/aliases/issues?state=open", + "source": "https://github.com/yiisoft/aliases", + "wiki": "https://www.yiiframework.com/wiki/" }, "funding": [ { - "url": "https://github.com/fabpot", + "url": "https://github.com/sponsors/yiisoft", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/twig/twig", - "type": "tidelift" + "url": "https://opencollective.com/yiisoft", + "type": "opencollective" } ], - "time": "2026-05-30T17:09:26+00:00" + "time": "2025-12-04T12:53:43+00:00" }, { - "name": "voku/portable-ascii", - "version": "2.1.1", + "name": "yiisoft/arrays", + "version": "3.2.1", "source": { "type": "git", - "url": "https://github.com/voku/portable-ascii.git", - "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + "url": "https://github.com/yiisoft/arrays.git", + "reference": "8efada90e4fd540b3da476779bc1b7bd9319b62f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", - "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "url": "https://api.github.com/repos/yiisoft/arrays/zipball/8efada90e4fd540b3da476779bc1b7bd9319b62f", + "reference": "8efada90e4fd540b3da476779bc1b7bd9319b62f", "shasum": "" }, "require": { - "php": ">=7.1.0" + "php": "8.1 - 8.5", + "yiisoft/strings": "^2.6" }, "require-dev": { - "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" - }, - "suggest": { - "ext-intl": "Use Intl for transliterator_transliterate() support" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpbench/phpbench": "^1.4.1", + "phpunit/phpunit": "^10.5.48", + "rector/rector": "^2.1.2", + "spatie/phpunit-watcher": "^1.24" }, "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true, + "target-directory": "tools" + } + }, "autoload": { "psr-4": { - "voku\\": "src/voku/" + "Yiisoft\\Arrays\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "Lars Moelleken", - "homepage": "https://www.moelleken.org/" - } + "BSD-3-Clause" ], - "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", - "homepage": "https://github.com/voku/portable-ascii", + "description": "Yii Array Helper", + "homepage": "https://www.yiiframework.com/", "keywords": [ - "ascii", - "clean", - "php" + "array", + "helper", + "yii" ], "support": { - "issues": "https://github.com/voku/portable-ascii/issues", - "source": "https://github.com/voku/portable-ascii/tree/2.1.1" + "chat": "https://t.me/yii3en", + "forum": "https://forum.yiiframework.com/", + "irc": "ircs://irc.libera.chat:6697/yii", + "issues": "https://github.com/yiisoft/arrays/issues?state=open", + "source": "https://github.com/yiisoft/arrays", + "wiki": "https://www.yiiframework.com/wiki/" }, "funding": [ { - "url": "https://www.paypal.me/moelleken", - "type": "custom" - }, - { - "url": "https://github.com/voku", + "url": "https://github.com/sponsors/yiisoft", "type": "github" }, { - "url": "https://opencollective.com/portable-ascii", - "type": "open_collective" - }, - { - "url": "https://www.patreon.com/voku", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", - "type": "tidelift" + "url": "https://opencollective.com/yiisoft", + "type": "opencollective" } ], - "time": "2026-04-26T05:33:54+00:00" + "time": "2025-11-26T12:25:21+00:00" }, { - "name": "web-auth/cose-lib", - "version": "4.5.2", + "name": "yiisoft/files", + "version": "2.1.0", "source": { "type": "git", - "url": "https://github.com/web-auth/cose-lib.git", - "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d" + "url": "https://github.com/yiisoft/files.git", + "reference": "465650fd9e4295669f42ab7e9fec2386700540a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/5b38660f90070a8e45f3dbc9528ade3b608dd77d", - "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d", + "url": "https://api.github.com/repos/yiisoft/files/zipball/465650fd9e4295669f42ab7e9fec2386700540a7", + "reference": "465650fd9e4295669f42ab7e9fec2386700540a7", "shasum": "" }, "require": { - "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", - "ext-json": "*", - "ext-openssl": "*", - "php": ">=8.1", - "spomky-labs/pki-framework": "^1.0" + "php": "8.0 - 8.5", + "yiisoft/strings": "^2.0" }, "require-dev": { - "spomky-labs/cbor-php": "^3.2.2" - }, - "suggest": { - "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension", - "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension", - "spomky-labs/cbor-php": "For COSE Signature support" + "bamarni/composer-bin-plugin": "^1.8.3", + "ext-zlib": "*", + "maglnet/composer-require-checker": "^4.4", + "phpunit/phpunit": "^9.6.22", + "rector/rector": "^2.0.10", + "spatie/phpunit-watcher": "^1.23.6" }, "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true, + "target-directory": "tools" + } + }, "autoload": { "psr-4": { - "Cose\\": "src/" + "Yiisoft\\Files\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" - }, - { - "name": "All contributors", - "homepage": "https://github.com/web-auth/cose/contributors" - } + "BSD-3-Clause" ], - "description": "CBOR Object Signing and Encryption (COSE) For PHP", - "homepage": "https://github.com/web-auth", + "description": "Helper to manage files and directories", + "homepage": "https://www.yiiframework.com/", "keywords": [ - "COSE", - "RFC8152" + "files" ], "support": { - "issues": "https://github.com/web-auth/cose-lib/issues", - "source": "https://github.com/web-auth/cose-lib/tree/4.5.2" + "chat": "https://t.me/yii3en", + "forum": "https://www.yiiframework.com/forum/", + "irc": "ircs://irc.libera.chat:6697/yii", + "issues": "https://github.com/yiisoft/files/issues?state=open", + "source": "https://github.com/yiisoft/files", + "wiki": "https://www.yiiframework.com/wiki/" }, "funding": [ { - "url": "https://github.com/Spomky", + "url": "https://github.com/sponsors/yiisoft", "type": "github" }, { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" + "url": "https://opencollective.com/yiisoft", + "type": "opencollective" } ], - "time": "2026-05-03T09:49:50+00:00" + "time": "2025-12-01T06:30:27+00:00" }, { - "name": "web-auth/webauthn-lib", - "version": "5.3.5", + "name": "yiisoft/html", + "version": "4.2.0", "source": { "type": "git", - "url": "https://github.com/web-auth/webauthn-lib.git", - "reference": "9e0986d999f4102e24ac8a598d3a80d98b56c19f" + "url": "https://github.com/yiisoft/html.git", + "reference": "eeb0bea275c87c4ee0dbc444dbc24fbf44a5aa77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/9e0986d999f4102e24ac8a598d3a80d98b56c19f", - "reference": "9e0986d999f4102e24ac8a598d3a80d98b56c19f", + "url": "https://api.github.com/repos/yiisoft/html/zipball/eeb0bea275c87c4ee0dbc444dbc24fbf44a5aa77", + "reference": "eeb0bea275c87c4ee0dbc444dbc24fbf44a5aa77", "shasum": "" }, "require": { - "ext-json": "*", - "ext-openssl": "*", - "paragonie/constant_time_encoding": "^2.6|^3.0", - "php": ">=8.2", - "phpdocumentor/reflection-docblock": "^5.3|^6.0", - "psr/clock": "^1.0", - "psr/event-dispatcher": "^1.0", - "psr/log": "^1.0|^2.0|^3.0", - "spomky-labs/cbor-php": "^3.0", - "spomky-labs/pki-framework": "^1.0", - "symfony/clock": "^6.4|^7.0|^8.0", - "symfony/deprecation-contracts": "^3.2", - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/property-info": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0", - "symfony/uid": "^6.4|^7.0|^8.0", - "web-auth/cose-lib": "^4.2.3" + "php": "8.1 - 8.5", + "yiisoft/arrays": "^2.0 || ^3.0", + "yiisoft/json": "^1.0" }, - "suggest": { - "psr/log-implementation": "Recommended to receive logs from the library", - "symfony/event-dispatcher": "Recommended to use dispatched events", - "web-token/jwt-library": "Mandatory for fetching Metadata Statement from distant sources" + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.90", + "infection/infection": "^0.27.11 || ^0.32", + "maglnet/composer-require-checker": "^4.7.1", + "phpunit/phpunit": "^10.5.46", + "rector/rector": "^2.0.17", + "spatie/phpunit-watcher": "^1.24", + "vimeo/psalm": "^5.26.1 || ^6.12" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/web-auth/webauthn-framework", - "name": "web-auth/webauthn-framework" - } - }, "autoload": { "psr-4": { - "Webauthn\\": "src/" + "Yiisoft\\Html\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" - }, - { - "name": "All contributors", - "homepage": "https://github.com/web-auth/webauthn-library/contributors" - } + "BSD-3-Clause" ], - "description": "FIDO2/Webauthn Support For PHP", - "homepage": "https://github.com/web-auth", + "description": "Handy library to generate HTML", + "homepage": "https://www.yiiframework.com/", "keywords": [ - "FIDO2", - "fido", - "webauthn" + "html" ], "support": { - "source": "https://github.com/web-auth/webauthn-lib/tree/5.3.5" + "chat": "https://t.me/yii3en", + "forum": "https://forum.yiiframework.com/", + "irc": "ircs://irc.libera.chat:6697/yii", + "issues": "https://github.com/yiisoft/html/issues?state=open", + "source": "https://github.com/yiisoft/html", + "wiki": "https://www.yiiframework.com/wiki/" }, "funding": [ { - "url": "https://github.com/Spomky", + "url": "https://github.com/sponsors/yiisoft", "type": "github" }, { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" + "url": "https://opencollective.com/yiisoft", + "type": "opencollective" } ], - "time": "2026-05-31T15:00:08+00:00" + "time": "2026-06-05T07:38:14+00:00" }, { - "name": "webmozart/assert", - "version": "2.4.1", + "name": "yiisoft/i18n", + "version": "1.2.2", "source": { "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70" + "url": "https://github.com/yiisoft/i18n.git", + "reference": "028fbcee0ea772dab150d0a8f40344a32e639d3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70", - "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "url": "https://api.github.com/repos/yiisoft/i18n/zipball/028fbcee0ea772dab150d0a8f40344a32e639d3f", + "reference": "028fbcee0ea772dab150d0a8f40344a32e639d3f", "shasum": "" }, "require": { - "ext-ctype": "*", - "ext-date": "*", - "ext-filter": "*", - "php": "^8.2" + "php": "8.0 - 8.5" }, - "suggest": { - "ext-intl": "", - "ext-simplexml": "", - "ext-spl": "" + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.3", + "maglnet/composer-require-checker": "^4.4", + "phpunit/phpunit": "^9.6.22", + "rector/rector": "^2.0.10", + "spatie/phpunit-watcher": "^1.23.6" }, "type": "library", "extra": { - "psalm": { - "pluginClass": "Webmozart\\Assert\\PsalmPlugin" - }, - "branch-alias": { - "dev-master": "2.0-dev", - "dev-feature/2-0": "2.0-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": true, + "target-directory": "tools" } }, "autoload": { "psr-4": { - "Webmozart\\Assert\\": "src/" + "Yiisoft\\I18n\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "authors": [ + "description": "Yii Internationalization Library", + "homepage": "https://www.yiiframework.com/", + "keywords": [ + "i18n", + "locale" + ], + "support": { + "chat": "https://t.me/yii3en", + "forum": "https://www.yiiframework.com/forum/", + "irc": "ircs://irc.libera.chat:6697/yii", + "issues": "https://github.com/yiisoft/i18n/issues?state=open", + "source": "https://github.com/yiisoft/i18n", + "wiki": "https://www.yiiframework.com/wiki/" + }, + "funding": [ { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "url": "https://github.com/sponsors/yiisoft", + "type": "github" }, { - "name": "Woody Gilk", - "email": "woody.gilk@gmail.com" + "url": "https://opencollective.com/yiisoft", + "type": "opencollective" } ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.4.1" - }, - "time": "2026-06-15T15:31:57+00:00" + "time": "2025-11-29T15:57:19+00:00" }, { - "name": "yiisoft/yii2", - "version": "2.0.55", + "name": "yiisoft/json", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/yiisoft/yii2-framework.git", - "reference": "b900eecdb225041a4c4e0f5e0e5336f606a23bdb" + "url": "https://github.com/yiisoft/json.git", + "reference": "6af88ed2c653f4b6cbe3ea9114e4aebc5a463c80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/b900eecdb225041a4c4e0f5e0e5336f606a23bdb", - "reference": "b900eecdb225041a4c4e0f5e0e5336f606a23bdb", + "url": "https://api.github.com/repos/yiisoft/json/zipball/6af88ed2c653f4b6cbe3ea9114e4aebc5a463c80", + "reference": "6af88ed2c653f4b6cbe3ea9114e4aebc5a463c80", "shasum": "" }, "require": { - "bower-asset/inputmask": "^5.0.8 ", - "bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", - "bower-asset/punycode": "^2.2", - "bower-asset/yii2-pjax": "~2.0.1", - "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0", - "ext-ctype": "*", - "ext-mbstring": "*", - "ezyang/htmlpurifier": "^4.17", - "lib-pcre": "*", - "php": ">=7.4.0", - "yiisoft/yii2-composer": "~2.0.4" + "ext-json": "*", + "ext-simplexml": "*", + "php": "~7.4.0 || 8.0 - 8.5" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "maglnet/composer-require-checker": "^3.8 || ^4.2", + "phpunit/phpunit": "^9.6.22", + "rector/rector": "^2.0.8", + "spatie/phpunit-watcher": "^1.23.6" }, - "bin": [ - "yii" - ], "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "yii\\": "" + "bamarni-bin": { + "bin-links": true, + "forward-command": true, + "target-directory": "tools" } }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Qiang Xue", - "email": "qiang.xue@gmail.com", - "homepage": "https://www.yiiframework.com/", - "role": "Founder and project lead" - }, - { - "name": "Alexander Makarov", - "email": "sam@rmcreative.ru", - "homepage": "https://rmcreative.ru/", - "role": "Core framework development" - }, - { - "name": "Maurizio Domba", - "homepage": "http://mdomba.info/", - "role": "Core framework development" - }, - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc", - "homepage": "https://www.cebe.cc/", - "role": "Core framework development" - }, - { - "name": "Timur Ruziev", - "email": "resurtm@gmail.com", - "homepage": "http://resurtm.com/", - "role": "Core framework development" - }, - { - "name": "Paul Klimov", - "email": "klimov.paul@gmail.com", - "role": "Core framework development" - }, - { - "name": "Dmitry Naumenko", - "email": "d.naumenko.a@gmail.com", - "role": "Core framework development" - }, - { - "name": "Boudewijn Vahrmeijer", - "email": "info@dynasource.eu", - "homepage": "http://dynasource.eu", - "role": "Core framework development" + "autoload": { + "psr-4": { + "Yiisoft\\Json\\": "src" } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" ], - "description": "Yii PHP Framework Version 2", + "description": "Yii JSON encoding and decoding", "homepage": "https://www.yiiframework.com/", "keywords": [ - "framework", - "yii2" + "json" ], "support": { - "forum": "https://forum.yiiframework.com/", + "chat": "https://t.me/yii3en", + "forum": "https://www.yiiframework.com/forum/", "irc": "ircs://irc.libera.chat:6697/yii", - "issues": "https://github.com/yiisoft/yii2/issues?state=open", - "source": "https://github.com/yiisoft/yii2", - "wiki": "https://www.yiiframework.com/wiki" + "issues": "https://github.com/yiisoft/json/issues?state=open", + "source": "https://github.com/yiisoft/json", + "wiki": "https://www.yiiframework.com/wiki/" }, "funding": [ { - "url": "https://github.com/yiisoft", + "url": "https://github.com/sponsors/yiisoft", "type": "github" }, { "url": "https://opencollective.com/yiisoft", - "type": "open_collective" - }, - { - "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2", - "type": "tidelift" + "type": "opencollective" } ], - "time": "2026-05-09T14:50:57+00:00" + "time": "2025-11-21T19:39:36+00:00" }, { - "name": "yiisoft/yii2-composer", - "version": "2.0.11", + "name": "yiisoft/strings", + "version": "2.7.0", "source": { "type": "git", - "url": "https://github.com/yiisoft/yii2-composer.git", - "reference": "b684b01ecb119c8287721def726a0e24fec2fef2" + "url": "https://github.com/yiisoft/strings.git", + "reference": "9bc7fea56374619cccd4587848029fe97f98bb33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/b684b01ecb119c8287721def726a0e24fec2fef2", - "reference": "b684b01ecb119c8287721def726a0e24fec2fef2", + "url": "https://api.github.com/repos/yiisoft/strings/zipball/9bc7fea56374619cccd4587848029fe97f98bb33", + "reference": "9bc7fea56374619cccd4587848029fe97f98bb33", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 | ^2.0" + "ext-mbstring": "*", + "php": "8.1 - 8.5" }, "require-dev": { - "composer/composer": "^1.0 | ^2.0@dev", - "phpunit/phpunit": "<7" + "bamarni/composer-bin-plugin": "^1.8.2", + "maglnet/composer-require-checker": "^4.7.1", + "phpbench/phpbench": "^1.4.1", + "phpunit/phpunit": "^10.5.48", + "rector/rector": "^2.1.2", + "spatie/phpunit-watcher": "^1.24" }, - "type": "composer-plugin", + "type": "library", "extra": { - "class": "yii\\composer\\Plugin", - "branch-alias": { - "dev-master": "2.0.x-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": true, + "target-directory": "tools" } }, "autoload": { "psr-4": { - "yii\\composer\\": "" + "Yiisoft\\Strings\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "authors": [ - { - "name": "Qiang Xue", - "email": "qiang.xue@gmail.com" - }, - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc" - } - ], - "description": "The composer plugin for Yii extension installer", + "description": "Yii Strings Helper", + "homepage": "https://www.yiiframework.com/", "keywords": [ - "composer", - "extension installer", - "yii2" + "helper", + "string", + "yii" ], "support": { + "chat": "https://t.me/yii3en", "forum": "https://www.yiiframework.com/forum/", "irc": "ircs://irc.libera.chat:6697/yii", - "issues": "https://github.com/yiisoft/yii2-composer/issues", - "source": "https://github.com/yiisoft/yii2-composer", + "issues": "https://github.com/yiisoft/strings/issues?state=open", + "source": "https://github.com/yiisoft/strings", "wiki": "https://www.yiiframework.com/wiki/" }, "funding": [ { - "url": "https://github.com/yiisoft", + "url": "https://github.com/sponsors/yiisoft", "type": "github" }, { "url": "https://opencollective.com/yiisoft", - "type": "open_collective" - }, - { - "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-composer", - "type": "tidelift" + "type": "opencollective" } ], - "time": "2025-02-13T20:59:36+00:00" + "time": "2025-11-23T18:00:58+00:00" }, { - "name": "yiisoft/yii2-debug", - "version": "2.1.27", + "name": "yiisoft/translator", + "version": "3.2.1", "source": { "type": "git", - "url": "https://github.com/yiisoft/yii2-debug.git", - "reference": "44e158914911ef81cd7111fd6d46b918f65fae7c" + "url": "https://github.com/yiisoft/translator.git", + "reference": "62c64c9009a570597cc31dd42ff951d74049a60b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-debug/zipball/44e158914911ef81cd7111fd6d46b918f65fae7c", - "reference": "44e158914911ef81cd7111fd6d46b918f65fae7c", + "url": "https://api.github.com/repos/yiisoft/translator/zipball/62c64c9009a570597cc31dd42ff951d74049a60b", + "reference": "62c64c9009a570597cc31dd42ff951d74049a60b", "shasum": "" }, "require": { - "ext-mbstring": "*", - "php": ">=5.4", - "yiisoft/yii2": "~2.0.13" + "php": "8.0 - 8.5", + "psr/event-dispatcher": "1.0.0", + "yiisoft/files": "^1.0 || ^2.0", + "yiisoft/i18n": "^1.0" }, "require-dev": { - "cweagans/composer-patches": "^1.7", - "phpunit/phpunit": "4.8.34", - "yiisoft/yii2-coding-standards": "~2.0", - "yiisoft/yii2-swiftmailer": "*" + "bamarni/composer-bin-plugin": "^1.8.3", + "maglnet/composer-require-checker": "^4.4", + "phpunit/phpunit": "^9.6.29", + "rector/rector": "^2.1.7", + "spatie/phpunit-watcher": "^1.23.6", + "yiisoft/di": "^1.2.1" }, - "type": "yii2-extension", + "suggest": { + "ext-intl": "Allows using intl message formatter", + "ext-tokenizer": "Allows using message extraction", + "yiisoft/event-dispatcher": "To listen for events about missing categories and messages" + }, + "type": "library", "extra": { - "patches": { - "phpunit/phpunit": { - "Fix PHP 7 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php7.patch", - "Fix PHP 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php8.patch", - "Fix PHP 8.1 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php81.patch" - }, - "phpunit/phpunit-mock-objects": { - "Fix PHP 7 and 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_mock_objects.patch" - } + "bamarni-bin": { + "bin-links": true, + "forward-command": true, + "target-directory": "tools" }, - "branch-alias": { - "dev-master": "2.0.x-dev" + "config-plugin": { + "di": "di.php", + "params": "params.php" }, - "composer-exit-on-patch-failure": true + "config-plugin-options": { + "source-directory": "config" + } }, "autoload": { "psr-4": { - "yii\\debug\\": "src" + "Yiisoft\\Translator\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "authors": [ - { - "name": "Qiang Xue", - "email": "qiang.xue@gmail.com" - }, - { - "name": "Simon Karlen", - "email": "simi.albi@outlook.com" - } - ], - "description": "The debugger extension for the Yii framework", + "description": "Yii Message Translator", + "homepage": "https://www.yiiframework.com/", "keywords": [ - "debug", - "debugger", - "dev", - "yii2" + "i18n", + "internationalization", + "translation" ], "support": { + "chat": "https://t.me/yii3en", "forum": "https://www.yiiframework.com/forum/", "irc": "ircs://irc.libera.chat:6697/yii", - "issues": "https://github.com/yiisoft/yii2-debug/issues", - "source": "https://github.com/yiisoft/yii2-debug", + "issues": "https://github.com/yiisoft/translator/issues?state=open", + "source": "https://github.com/yiisoft/translator", "wiki": "https://www.yiiframework.com/wiki/" }, "funding": [ { - "url": "https://github.com/yiisoft", + "url": "https://github.com/sponsors/yiisoft", "type": "github" }, { "url": "https://opencollective.com/yiisoft", - "type": "open_collective" - }, - { - "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-debug", - "type": "tidelift" + "type": "opencollective" } ], - "time": "2025-06-08T13:32:11+00:00" + "time": "2025-12-06T05:18:44+00:00" }, { - "name": "yiisoft/yii2-queue", - "version": "2.3.8", + "name": "yiisoft/translator-message-php", + "version": "1.1.2", "source": { "type": "git", - "url": "https://github.com/yiisoft/yii2-queue.git", - "reference": "e0f935e5b868d53347acfb14ec19faaf16085005" + "url": "https://github.com/yiisoft/translator-message-php.git", + "reference": "ef597d4df3d991ba4d7e9feb98d818870e8c747a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-queue/zipball/e0f935e5b868d53347acfb14ec19faaf16085005", - "reference": "e0f935e5b868d53347acfb14ec19faaf16085005", + "url": "https://api.github.com/repos/yiisoft/translator-message-php/zipball/ef597d4df3d991ba4d7e9feb98d818870e8c747a", + "reference": "ef597d4df3d991ba4d7e9feb98d818870e8c747a", "shasum": "" }, "require": { - "php": ">=5.5.0", - "symfony/process": "^3.3||^4.0||^5.0||^6.0||^7.0", - "yiisoft/yii2": "~2.0.14" + "php": "8.0 - 8.5", + "yiisoft/translator": "1.0 - 3" }, "require-dev": { - "aws/aws-sdk-php": ">=2.4", - "cweagans/composer-patches": "^1.7", - "enqueue/amqp-lib": "^0.8||^0.9.10||^0.10.0", - "enqueue/stomp": "^0.8.39||0.10.19", - "opis/closure": "*", - "pda/pheanstalk": "~3.2.1", - "php-amqplib/php-amqplib": "^2.8.0||^3.0.0", - "phpunit/phpunit": "4.8.34", - "yiisoft/yii2-debug": "~2.1.0", - "yiisoft/yii2-gii": "~2.2.0", - "yiisoft/yii2-redis": "2.0.19" - }, - "suggest": { - "aws/aws-sdk-php": "Need for aws SQS.", - "enqueue/amqp-lib": "Need for AMQP interop queue.", - "enqueue/stomp": "Need for Stomp queue.", - "ext-gearman": "Need for Gearman queue.", - "ext-pcntl": "Need for process signals.", - "pda/pheanstalk": "Need for Beanstalk queue.", - "php-amqplib/php-amqplib": "Need for AMQP queue.", - "yiisoft/yii2-redis": "Need for Redis queue." + "bamarni/composer-bin-plugin": "^1.8.3", + "maglnet/composer-require-checker": "^4.4", + "phpunit/phpunit": "^9.6.22", + "rector/rector": "^2.0.10", + "spatie/phpunit-watcher": "^1.23.6" }, - "type": "yii2-extension", + "type": "library", "extra": { - "patches": { - "phpunit/phpunit": { - "Fix PHP 7 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php7.patch", - "Fix PHP 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php8.patch" - }, - "phpunit/phpunit-mock-objects": { - "Fix PHP 7 and 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_mock_objects.patch" - } - }, - "branch-alias": { - "dev-master": "2.x-dev" - }, - "composer-exit-on-patch-failure": true + "bamarni-bin": { + "bin-links": true, + "forward-command": true, + "target-directory": "tools" + } }, "autoload": { "psr-4": { - "yii\\queue\\": "src", - "yii\\queue\\db\\": "src/drivers/db", - "yii\\queue\\sqs\\": "src/drivers/sqs", - "yii\\queue\\amqp\\": "src/drivers/amqp", - "yii\\queue\\file\\": "src/drivers/file", - "yii\\queue\\sync\\": "src/drivers/sync", - "yii\\queue\\redis\\": "src/drivers/redis", - "yii\\queue\\stomp\\": "src/drivers/stomp", - "yii\\queue\\gearman\\": "src/drivers/gearman", - "yii\\queue\\beanstalk\\": "src/drivers/beanstalk", - "yii\\queue\\amqp_interop\\": "src/drivers/amqp_interop" + "Yiisoft\\Translator\\Message\\Php\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "authors": [ + "description": "Yii Translator PHP Message Storage", + "homepage": "https://www.yiiframework.com/", + "keywords": [ + "formatting", + "i18n", + "internationalization", + "message storage" + ], + "support": { + "chat": "https://t.me/yii3en", + "forum": "https://www.yiiframework.com/forum/", + "irc": "ircs://irc.libera.chat:6697/yii", + "issues": "https://github.com/yiisoft/translator-message-php/issues?state=open", + "source": "https://github.com/yiisoft/translator-message-php", + "wiki": "https://www.yiiframework.com/wiki/" + }, + "funding": [ { - "name": "Roman Zhuravlev", - "email": "zhuravljov@gmail.com" + "url": "https://github.com/sponsors/yiisoft", + "type": "github" + }, + { + "url": "https://opencollective.com/yiisoft", + "type": "opencollective" } ], - "description": "Yii2 Queue Extension which supports queues based on DB, Redis, RabbitMQ, Beanstalk, SQS, and Gearman", - "keywords": [ - "async", - "beanstalk", - "db", - "gearman", - "gii", - "queue", - "rabbitmq", - "redis", - "sqs", - "yii" + "time": "2025-12-06T14:38:12+00:00" + } + ], + "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v7.20.0", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.3.0", + "jean85/pretty-package-versions": "^2.1.1", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", + "phpunit/php-file-iterator": "^6.0.1 || ^7", + "phpunit/php-timer": "^8 || ^9", + "phpunit/phpunit": "^12.5.14 || ^13.0.5", + "sebastian/environment": "^8.0.3 || ^9", + "symfony/console": "^7.4.7 || ^8.0.7", + "symfony/process": "^7.4.5 || ^8.0.5" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0.0", + "ext-pcntl": "*", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^2.1.44", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "symfony/filesystem": "^7.4.6 || ^8.0.6" + }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" ], "support": { - "docs": "https://github.com/yiisoft/yii2-queue/blob/master/docs/guide", - "issues": "https://github.com/yiisoft/yii2-queue/issues", - "source": "https://github.com/yiisoft/yii2-queue" + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.20.0" }, "funding": [ { - "url": "https://github.com/yiisoft", + "url": "https://github.com/sponsors/Slamdunk", "type": "github" }, { - "url": "https://opencollective.com/yiisoft", - "type": "open_collective" - }, - { - "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-queue", - "type": "tidelift" + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" } ], - "time": "2026-01-08T07:52:05+00:00" + "time": "2026-03-29T15:46:14+00:00" }, { - "name": "yiisoft/yii2-symfonymailer", - "version": "4.0.0", + "name": "cebe/markdown", + "version": "1.2.1", "source": { "type": "git", - "url": "https://github.com/yiisoft/yii2-symfonymailer.git", - "reference": "21f407239c51fc6d50d369e4469d006afa8c9b2c" + "url": "https://github.com/cebe/markdown.git", + "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-symfonymailer/zipball/21f407239c51fc6d50d369e4469d006afa8c9b2c", - "reference": "21f407239c51fc6d50d369e4469d006afa8c9b2c", + "url": "https://api.github.com/repos/cebe/markdown/zipball/9bac5e971dd391e2802dca5400bbeacbaea9eb86", + "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86", "shasum": "" }, "require": { - "php": ">=8.1", - "psr/event-dispatcher": "1.0.0", - "symfony/mailer": "^6.4 || ^7.0", - "symfony/mime": "^6.4 || ^7.0", - "yiisoft/yii2": ">=2.0.4" + "lib-pcre": "*", + "php": ">=5.4.0" }, "require-dev": { - "maglnet/composer-require-checker": "^4.7", - "phpunit/phpunit": "^10.5", - "roave/infection-static-analysis-plugin": "^1.34", - "symplify/easy-coding-standard": "^12.1", - "vimeo/psalm": "^5.20" - }, - "suggest": { - "yiisoft/yii2-psr-log-source": "Allows routing transport logs to your Yii2 logger" + "cebe/indent": "*", + "facebook/xhprof": "*@dev", + "phpunit/phpunit": "4.1.*" }, - "type": "yii2-extension", + "bin": [ + "bin/markdown" + ], + "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" - }, - "sort-packages": true + "dev-master": "1.2.x-dev" + } }, "autoload": { "psr-4": { - "yii\\symfonymailer\\": "src" + "cebe\\markdown\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Kirill Petrov", - "email": "archibeardrinker@gmail.com" + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "http://cebe.cc/", + "role": "Creator" } ], - "description": "The SymfonyMailer integration for the Yii framework", + "description": "A super fast, highly extensible markdown parser for PHP", + "homepage": "https://github.com/cebe/markdown#readme", "keywords": [ - "email", - "mail", - "mailer", - "symfony", - "symfonymailer", - "yii2" + "extensible", + "fast", + "gfm", + "markdown", + "markdown-extra" ], "support": { - "forum": "http://www.yiiframework.com/forum/", - "irc": "irc://irc.freenode.net/yii", - "issues": "https://github.com/yiisoft/yii2-symfonymailer/issues", - "source": "https://github.com/yiisoft/yii2-symfonymailer", - "wiki": "http://www.yiiframework.com/wiki/" + "issues": "https://github.com/cebe/markdown/issues", + "source": "https://github.com/cebe/markdown" }, - "funding": [ - { - "url": "https://github.com/yiisoft", - "type": "github" - }, - { - "url": "https://opencollective.com/yiisoft", - "type": "open_collective" - }, - { - "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-symfonymailer", - "type": "tidelift" - } - ], - "time": "2024-01-29T14:13:45+00:00" - } - ], - "packages-dev": [ + "time": "2018-03-26T11:24:36+00:00" + }, { - "name": "behat/gherkin", - "version": "v4.17.0", + "name": "composer/xdebug-handler", + "version": "3.0.5", "source": { "type": "git", - "url": "https://github.com/Behat/Gherkin.git", - "reference": "5c8b3149fac39b5a79942b64eeec59a5ee4001c0" + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c8b3149fac39b5a79942b64eeec59a5ee4001c0", - "reference": "5c8b3149fac39b5a79942b64eeec59a5ee4001c0", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { - "composer-runtime-api": "^2.2", - "php": ">=8.1 <8.6" + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { - "cucumber/gherkin-monorepo": "dev-gherkin-v39.1.0", - "friendsofphp/php-cs-fixer": "^3.77", - "mikey179/vfsstream": "^1.6", - "phpstan/extension-installer": "^1", - "phpstan/phpstan": "^2", - "phpstan/phpstan-phpunit": "^2", - "phpunit/phpunit": "^10.5", - "symfony/yaml": "^5.4 || ^6.4 || ^7.0" - }, - "suggest": { - "symfony/yaml": "If you want to parse features, represented in YAML files" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, "autoload": { "psr-4": { - "Behat\\Gherkin\\": "src/" + "Composer\\XdebugHandler\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -8464,307 +10978,245 @@ ], "authors": [ { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "https://everzet.com" + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" } ], - "description": "Gherkin DSL parser for PHP", - "homepage": "https://behat.org/", + "description": "Restarts a process without Xdebug.", "keywords": [ - "BDD", - "Behat", - "Cucumber", - "DSL", - "gherkin", - "parser" + "Xdebug", + "performance" ], "support": { - "issues": "https://github.com/Behat/Gherkin/issues", - "source": "https://github.com/Behat/Gherkin/tree/v4.17.0" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { - "url": "https://github.com/acoulton", - "type": "github" + "url": "https://packagist.com", + "type": "custom" }, { - "url": "https://github.com/carlos-granados", + "url": "https://github.com/composer", "type": "github" }, { - "url": "https://github.com/stof", - "type": "github" + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" } ], - "time": "2026-05-18T09:33:47+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { - "name": "codeception/codeception", - "version": "5.3.5", + "name": "craftcms/ecs", + "version": "dev-main", "source": { "type": "git", - "url": "https://github.com/Codeception/Codeception.git", - "reference": "83c2986ec2abe594cee2f706d9ec7aca2878fbe0" + "url": "https://github.com/craftcms/ecs.git", + "reference": "3823f989668e12a85ba681f8c7f3fd8488e23066" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/83c2986ec2abe594cee2f706d9ec7aca2878fbe0", - "reference": "83c2986ec2abe594cee2f706d9ec7aca2878fbe0", + "url": "https://api.github.com/repos/craftcms/ecs/zipball/3823f989668e12a85ba681f8c7f3fd8488e23066", + "reference": "3823f989668e12a85ba681f8c7f3fd8488e23066", "shasum": "" }, "require": { - "behat/gherkin": "^4.12", - "codeception/lib-asserts": "^2.2 | ^3.0.1", - "codeception/stub": "^4.1", - "ext-curl": "*", - "ext-json": "*", - "ext-mbstring": "*", - "php": "^8.2", - "phpunit/php-code-coverage": "^9.2 | ^10.0 | ^11.0 | ^12.0 | ^13.0", - "phpunit/php-text-template": "^2.0 | ^3.0 | ^4.0 | ^5.0 | ^6.0", - "phpunit/php-timer": "^5.0.3 | ^6.0 | ^7.0 | ^8.0 | ^9.0", - "phpunit/phpunit": "^9.5.20 | ^10.0 | ^11.0 | ^12.0 | ^13.0", - "psy/psysh": "^0.11.2 | ^0.12", - "sebastian/comparator": "^4.0.5 | ^5.0 | ^6.0 | ^7.0 | ^8.0", - "sebastian/diff": "^4.0.3 | ^5.0 | ^6.0 | ^7.0 | ^8.0", - "symfony/console": ">=5.4.24 <9.0", - "symfony/css-selector": ">=5.4.24 <9.0", - "symfony/event-dispatcher": ">=5.4.24 <9.0", - "symfony/finder": ">=5.4.24 <9.0", - "symfony/var-dumper": ">=5.4.24 <9.0", - "symfony/yaml": ">=5.4.24 <9.0" - }, - "conflict": { - "codeception/lib-innerbrowser": "<3.1.3", - "codeception/module-filesystem": "<3.0", - "codeception/module-phpbrowser": "<2.5" - }, - "replace": { - "codeception/phpunit-wrapper": "*" - }, - "require-dev": { - "codeception/lib-innerbrowser": "*@dev", - "codeception/lib-web": "*@dev", - "codeception/module-asserts": "dev-master", - "codeception/module-cli": "*@dev", - "codeception/module-db": "*@dev", - "codeception/module-filesystem": "*@dev", - "codeception/module-phpbrowser": "*@dev", - "codeception/module-webdriver": "*@dev", - "codeception/util-universalframework": "*@dev", - "doctrine/orm": "^3.3", - "ext-simplexml": "*", - "jetbrains/phpstorm-attributes": "^1.0", - "laravel-zero/phar-updater": "^1.4", - "php-webdriver/webdriver": "^1.15", - "stecman/symfony-console-completion": "^0.14 || ^0.15", - "symfony/dotenv": ">=5.4.24 <9.0", - "symfony/error-handler": ">=5.4.24 <9.0", - "symfony/process": ">=5.4.24 <9.0", - "vlucas/phpdotenv": "^5.1" - }, - "suggest": { - "codeception/specify": "BDD-style code blocks", - "codeception/verify": "BDD-style assertions", - "ext-simplexml": "For loading params from XML files", - "stecman/symfony-console-completion": "For BASH autocompletion", - "symfony/dotenv": "For loading params from .env files", - "symfony/phpunit-bridge": "For phpunit-bridge support", - "vlucas/phpdotenv": "For loading params from .env files" + "php": "^7.2.5|^8.0.2", + "symplify/easy-coding-standard": "^10.3.3" }, - "bin": [ - "codecept" - ], + "default-branch": true, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.3.x-dev" - } - }, "autoload": { - "files": [ - "functions.php" - ], "psr-4": { - "Codeception\\": "src/Codeception", - "Codeception\\Extension\\": "ext" - }, - "classmap": [ - "src/PHPUnit/TestCase.php" - ] + "craft\\ecs\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert.ua@gmail.com", - "homepage": "https://codeception.com" - } - ], - "description": "All-in-one PHP Testing Framework", - "homepage": "https://codeception.com/", - "keywords": [ - "BDD", - "TDD", - "acceptance testing", - "functional testing", - "unit testing" - ], + "description": "Easy Coding Standard configurations for Craft CMS projects", "support": { - "issues": "https://github.com/Codeception/Codeception/issues", - "source": "https://github.com/Codeception/Codeception/tree/5.3.5" + "issues": "https://github.com/craftcms/ecs/issues", + "source": "https://github.com/craftcms/ecs/tree/main" }, - "funding": [ - { - "url": "https://opencollective.com/codeception", - "type": "open_collective" - } - ], - "time": "2026-02-18T06:18:00+00:00" + "time": "2024-08-07T21:54:45+00:00" }, { - "name": "codeception/lib-asserts", - "version": "3.2.0", + "name": "craftcms/yii2-adapter", + "version": "6.x-dev", "source": { "type": "git", - "url": "https://github.com/Codeception/lib-asserts.git", - "reference": "f161e5d3a9e5ae573ca01cfb3b5601ff5303df03" + "url": "https://github.com/craftcms/yii2-adapter.git", + "reference": "2740c58e2178c003f8086bf35b790958d39b4e46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/f161e5d3a9e5ae573ca01cfb3b5601ff5303df03", - "reference": "f161e5d3a9e5ae573ca01cfb3b5601ff5303df03", + "url": "https://api.github.com/repos/craftcms/yii2-adapter/zipball/2740c58e2178c003f8086bf35b790958d39b4e46", + "reference": "2740c58e2178c003f8086bf35b790958d39b4e46", "shasum": "" }, "require": { + "craftcms/cms": "self.version", + "creocoder/yii2-nested-sets": "~0.9.0", + "ext-bcmath": "*", + "ext-curl": "*", "ext-dom": "*", - "php": "^8.2 || ^8.3 || ^8.4 || ^8.5", - "phpunit/phpunit": "^11.5 || ^12.0 || ^13.0" + "ext-intl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-pcre": "*", + "ext-pdo": "*", + "ext-zip": "*", + "mikehaertl/php-shellcommand": "^1.6.3", + "php": "^8.5", + "samdark/yii2-psr-log-target": "^1.1.3", + "seld/cli-prompt": "^1.0.4", + "thamtech/yii2-ratelimiter-advanced": "^0.5.0", + "voku/portable-ascii": "^2.0", + "yiisoft/yii2": "~2.0.55.0", + "yiisoft/yii2-debug": "~2.1.27.0", + "yiisoft/yii2-queue": "~2.3.2", + "yiisoft/yii2-symfonymailer": "^4.0.0" + }, + "provide": { + "bower-asset/inputmask": "5.0.9", + "bower-asset/jquery": "3.6.1", + "bower-asset/punycode": "^2.2", + "bower-asset/yii2-pjax": "~2.0.1", + "yii2tech/ar-softdelete": "1.0.4" + }, + "require-dev": { + "codeception/codeception": "^5.2.0", + "codeception/lib-innerbrowser": "4.0.6", + "codeception/module-asserts": "^3.0.0", + "codeception/module-datafactory": "^3.0.0", + "codeception/module-phpbrowser": "^3.0.0", + "codeception/module-rest": "^3.3.2", + "codeception/module-yii2": "^1.1.9", + "craftcms/ecs": "dev-main", + "dg/bypass-finals": "^1.9", + "larastan/larastan": "^3.12", + "laravel/socialite": "^5.25", + "league/factory-muffin": "^3.3.0", + "orchestra/testbench": "^11.0", + "pestphp/pest": "^4.0", + "phpstan/phpstan": "^2.1", + "rector/rector": "^2.0", + "vlucas/phpdotenv": "^5.4.1", + "yiisoft/yii2-redis": "^2.0" }, + "default-branch": true, "type": "library", + "extra": { + "laravel": { + "providers": [ + "CraftCms\\Yii2Adapter\\Yii2ServiceProvider" + ] + } + }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/Helpers/Queries.php" + ], + "psr-4": { + "craft\\": "legacy/", + "CraftCms\\Cms\\": "constants/", + "CraftCms\\Yii2Adapter\\": "src/", + "yii2tech\\ar\\softdelete\\": "lib/ar-softdelete/src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "proprietary" ], "authors": [ { - "name": "Michael Bodnarchuk", - "email": "davert@mail.ua", - "homepage": "http://codegyre.com" - }, - { - "name": "Gintautas Miselis" - }, - { - "name": "Gustavo Nieves", - "homepage": "https://medium.com/@ganieves" + "name": "Pixel & Tonic", + "homepage": "https://pixelandtonic.com/" } ], - "description": "Assertion methods used by Codeception core and Asserts module", - "homepage": "https://codeception.com/", + "description": "Craft CMS Yii2 adapter", + "homepage": "https://craftcms.com", "keywords": [ - "codeception" + "cms", + "craftcms", + "yii2" ], "support": { - "issues": "https://github.com/Codeception/lib-asserts/issues", - "source": "https://github.com/Codeception/lib-asserts/tree/3.2.0" + "docs": "https://craftcms.com/docs/5.x/", + "email": "support@craftcms.com", + "forum": "https://craftcms.stackexchange.com/", + "issues": "https://github.com/craftcms/cms/issues?state=open", + "rss": "https://github.com/craftcms/cms/releases.atom", + "source": "https://github.com/craftcms/cms" }, - "time": "2026-02-06T15:19:32+00:00" + "time": "2026-09-15T00:35:22+00:00" }, { - "name": "codeception/lib-innerbrowser", - "version": "4.1.1", + "name": "creocoder/yii2-nested-sets", + "version": "0.9.0", "source": { "type": "git", - "url": "https://github.com/Codeception/lib-innerbrowser.git", - "reference": "0fa80deaed7da6a92a0cd4117338394c69196ec6" + "url": "https://github.com/creocoder/yii2-nested-sets.git", + "reference": "cb8635a459b6246e5a144f096b992dcc30cf9954" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/0fa80deaed7da6a92a0cd4117338394c69196ec6", - "reference": "0fa80deaed7da6a92a0cd4117338394c69196ec6", + "url": "https://api.github.com/repos/creocoder/yii2-nested-sets/zipball/cb8635a459b6246e5a144f096b992dcc30cf9954", + "reference": "cb8635a459b6246e5a144f096b992dcc30cf9954", "shasum": "" }, "require": { - "codeception/codeception": "^5.0.8", - "codeception/lib-web": "^1.0.1 || ^2", - "ext-dom": "*", - "ext-json": "*", - "ext-mbstring": "*", - "php": "^8.1", - "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0", - "symfony/browser-kit": "^4.4.24 || ^5.4 || ^6.0 || ^7.0 || ^8.0", - "symfony/dom-crawler": "^4.4.30 || ^5.4 || ^6.0 || ^7.0 || ^8.0" - }, - "require-dev": { - "codeception/util-universalframework": "^1.0 || ^2.0" + "yiisoft/yii2": "*" }, - "type": "library", + "type": "yii2-extension", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "creocoder\\nestedsets\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Michael Bodnarchuk", - "email": "davert@mail.ua", - "homepage": "https://codegyre.com" - }, - { - "name": "Gintautas Miselis" + "name": "Alexander Kochetov", + "email": "creocoder@gmail.com" } ], - "description": "Parent library for all Codeception framework modules and PhpBrowser", - "homepage": "https://codeception.com/", + "description": "The nested sets behavior for the Yii framework", "keywords": [ - "codeception" + "nested sets", + "yii2" ], "support": { - "issues": "https://github.com/Codeception/lib-innerbrowser/issues", - "source": "https://github.com/Codeception/lib-innerbrowser/tree/4.1.1" + "issues": "https://github.com/creocoder/yii2-nested-sets/issues", + "source": "https://github.com/creocoder/yii2-nested-sets/tree/master" }, - "time": "2026-06-26T22:06:27+00:00" + "time": "2015-01-27T10:53:51+00:00" }, { - "name": "codeception/lib-web", - "version": "2.1.0", + "name": "dg/bypass-finals", + "version": "v1.10.1", "source": { "type": "git", - "url": "https://github.com/Codeception/lib-web.git", - "reference": "a030a3a22fc8e856b5957086794ed5403c7992d9" + "url": "https://github.com/dg/bypass-finals.git", + "reference": "62d4ea18f8937af7d794b3358c125ecb52862e98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-web/zipball/a030a3a22fc8e856b5957086794ed5403c7992d9", - "reference": "a030a3a22fc8e856b5957086794ed5403c7992d9", + "url": "https://api.github.com/repos/dg/bypass-finals/zipball/62d4ea18f8937af7d794b3358c125ecb52862e98", + "reference": "62d4ea18f8937af7d794b3358c125ecb52862e98", "shasum": "" }, "require": { - "ext-mbstring": "*", - "guzzlehttp/psr7": "^2.0", - "php": "^8.2", - "phpunit/phpunit": "^11.5 | ^12 | ^13", - "symfony/css-selector": ">=4.4.24 <9.0" - }, - "conflict": { - "codeception/codeception": "<5.0.0-alpha3" + "php": ">=7.1" }, "require-dev": { - "php-webdriver/webdriver": "^1.12" + "nette/tester": "^2.3", + "phpstan/phpstan": "^0.12" }, "type": "library", "autoload": { @@ -8774,100 +11226,132 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" ], "authors": [ { - "name": "Gintautas Miselis" + "name": "David Grudl", + "homepage": "https://davidgrudl.com" } ], - "description": "Library containing files used by module-webdriver and lib-innerbrowser or module-phpbrowser", - "homepage": "https://codeception.com/", + "description": "Removes final keyword from source code on-the-fly and allows mocking of final methods and classes", "keywords": [ - "codeception" + "finals", + "mocking", + "phpunit", + "testing", + "unit" ], "support": { - "issues": "https://github.com/Codeception/lib-web/issues", - "source": "https://github.com/Codeception/lib-web/tree/2.1.0" + "issues": "https://github.com/dg/bypass-finals/issues", + "source": "https://github.com/dg/bypass-finals/tree/v1.10.1" }, - "time": "2026-02-06T15:22:13+00:00" + "time": "2026-06-04T15:48:46+00:00" }, { - "name": "codeception/lib-xml", - "version": "1.1.1", + "name": "ezyang/htmlpurifier", + "version": "v4.19.0", "source": { "type": "git", - "url": "https://github.com/Codeception/lib-xml.git", - "reference": "758a525ed766ad641cc66cd619d96dbb9e887be2" + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-xml/zipball/758a525ed766ad641cc66cd619d96dbb9e887be2", - "reference": "758a525ed766ad641cc66cd619d96dbb9e887be2", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", "shasum": "" }, "require": { - "codeception/lib-web": "^1.0.6 || ^2", - "ext-dom": "*", - "php": "^8.2", - "symfony/css-selector": ">=4.4.24 <9.0" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" }, - "conflict": { - "codeception/codeception": "<5.0.0-alpha3" + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" }, "type": "library", "autoload": { - "classmap": [ - "src/" + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "LGPL-2.1-or-later" ], "authors": [ { - "name": "Gintautas Miselis" + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" } ], - "description": "Files used by module-rest and module-soap", - "homepage": "https://codeception.com/", + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", "keywords": [ - "codeception" + "html" ], "support": { - "issues": "https://github.com/Codeception/lib-xml/issues", - "source": "https://github.com/Codeception/lib-xml/tree/1.1.1" + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" }, - "time": "2025-11-28T08:21:33+00:00" + "time": "2025-10-17T16:34:55+00:00" }, { - "name": "codeception/module-asserts", - "version": "3.3.0", + "name": "fakerphp/faker", + "version": "v1.24.1", "source": { "type": "git", - "url": "https://github.com/Codeception/module-asserts.git", - "reference": "3b4ec5dc771a135e13c79f7e9a6eacd74779e4ad" + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/3b4ec5dc771a135e13c79f7e9a6eacd74779e4ad", - "reference": "3b4ec5dc771a135e13c79f7e9a6eacd74779e4ad", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", "shasum": "" }, "require": { - "codeception/codeception": "*@dev", - "codeception/lib-asserts": "^3.1", - "php": "^8.2" + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" }, "conflict": { - "codeception/codeception": "<5.0" + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." }, "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Faker\\": "src/Faker/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8875,54 +11359,54 @@ ], "authors": [ { - "name": "Michael Bodnarchuk" - }, - { - "name": "Gintautas Miselis" - }, - { - "name": "Gustavo Nieves", - "homepage": "https://medium.com/@ganieves" + "name": "François Zaninotto" } ], - "description": "Codeception module containing various assertions", - "homepage": "https://codeception.com/", + "description": "Faker is a PHP library that generates fake data for you.", "keywords": [ - "assertions", - "asserts", - "codeception" + "data", + "faker", + "fixtures" ], "support": { - "issues": "https://github.com/Codeception/module-asserts/issues", - "source": "https://github.com/Codeception/module-asserts/tree/3.3.0" + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" }, - "time": "2025-12-23T21:16:13+00:00" + "time": "2024-11-21T13:46:39+00:00" }, { - "name": "codeception/module-datafactory", - "version": "3.0.0", + "name": "fidry/cpu-core-counter", + "version": "1.3.0", "source": { "type": "git", - "url": "https://github.com/Codeception/module-datafactory.git", - "reference": "90b87b554cc8e254865f5e9dbb86d7ce112c51ab" + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-datafactory/zipball/90b87b554cc8e254865f5e9dbb86d7ce112c51ab", - "reference": "90b87b554cc8e254865f5e9dbb86d7ce112c51ab", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { - "codeception/codeception": "^5.0.0-RC6", - "league/factory-muffin": "^3.3", - "league/factory-muffin-faker": "^2.3", - "php": "^8.0" + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" }, "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8930,61 +11414,64 @@ ], "authors": [ { - "name": "Michael Bodnarchuk" + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" } ], - "description": "DataFactory module for Codeception", - "homepage": "https://codeception.com/", + "description": "Tiny utility to get the number of CPU cores.", "keywords": [ - "codeception" + "CPU", + "core" ], "support": { - "issues": "https://github.com/Codeception/module-datafactory/issues", - "source": "https://github.com/Codeception/module-datafactory/tree/3.0.0" + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, - "time": "2022-07-18T16:38:21+00:00" + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" }, { - "name": "codeception/module-phpbrowser", - "version": "3.0.2", + "name": "filp/whoops", + "version": "2.18.4", "source": { "type": "git", - "url": "https://github.com/Codeception/module-phpbrowser.git", - "reference": "460e392c77370f7836012b16e06071eb1607876a" + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-phpbrowser/zipball/460e392c77370f7836012b16e06071eb1607876a", - "reference": "460e392c77370f7836012b16e06071eb1607876a", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", "shasum": "" }, "require": { - "codeception/codeception": "*@dev", - "codeception/lib-innerbrowser": "*@dev", - "ext-json": "*", - "guzzlehttp/guzzle": "^7.4", - "php": "^8.1", - "symfony/browser-kit": "^5.4 | ^6.0 | ^7.0" - }, - "conflict": { - "codeception/codeception": "<5.0", - "codeception/lib-innerbrowser": "<3.0" + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "require-dev": { - "aws/aws-sdk-php": "^3.199", - "codeception/module-rest": "^2.0 | *@dev", - "ext-curl": "*", - "phpstan/phpstan": "^1.10", - "squizlabs/php_codesniffer": "^3.10" + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" }, "suggest": { - "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests" + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Whoops\\": "src/Whoops/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8992,118 +11479,107 @@ ], "authors": [ { - "name": "Michael Bodnarchuk" - }, - { - "name": "Gintautas Miselis" + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" } ], - "description": "Codeception module for testing web application over HTTP", - "homepage": "https://codeception.com/", + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", "keywords": [ - "codeception", - "functional-testing", - "http" + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" ], "support": { - "issues": "https://github.com/Codeception/module-phpbrowser/issues", - "source": "https://github.com/Codeception/module-phpbrowser/tree/3.0.2" + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" }, - "time": "2025-09-04T10:45:58+00:00" + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" }, { - "name": "codeception/module-rest", - "version": "3.4.3", + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", "source": { "type": "git", - "url": "https://github.com/Codeception/module-rest.git", - "reference": "596817fcb5a603f6f55306f67f9eb84943df8998" + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-rest/zipball/596817fcb5a603f6f55306f67f9eb84943df8998", - "reference": "596817fcb5a603f6f55306f67f9eb84943df8998", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", "shasum": "" }, "require": { - "codeception/codeception": "^5.0.8", - "codeception/lib-xml": "^1.0", - "ext-dom": "*", - "ext-json": "*", - "justinrainbow/json-schema": "^5.2.9 || ^6", - "php": "^8.2", - "softcreatr/jsonpath": "^0.8 || ^0.9 || ^0.10 || ^0.11 || ^1.0" + "php": "^7.4|^8.0" }, - "require-dev": { - "codeception/lib-innerbrowser": "^3.0 | ^4.0", - "codeception/stub": "^4.0", - "codeception/util-universalframework": "^2.0", - "ext-libxml": "*", - "ext-simplexml": "*" + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" }, - "suggest": { - "aws/aws-sdk-php": "For using AWS Auth" + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, "autoload": { "classmap": [ - "src/" + "hamcrest" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gintautas Miselis" - } + "BSD-3-Clause" ], - "description": "REST module for Codeception", - "homepage": "https://codeception.com/", + "description": "This is the PHP port of Hamcrest Matchers", "keywords": [ - "codeception", - "rest" + "test" ], "support": { - "issues": "https://github.com/Codeception/module-rest/issues", - "source": "https://github.com/Codeception/module-rest/tree/3.4.3" + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" }, - "time": "2025-12-22T14:13:56+00:00" + "time": "2025-04-30T06:54:44+00:00" }, { - "name": "codeception/module-yii2", - "version": "1.1.12", + "name": "iamcal/sql-parser", + "version": "v0.7", "source": { "type": "git", - "url": "https://github.com/Codeception/module-yii2.git", - "reference": "1ebe6bc2a7f307a6c246026a905612a40ef64859" + "url": "https://github.com/iamcal/SQLParser.git", + "reference": "610392f38de49a44dab08dc1659960a29874c4b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-yii2/zipball/1ebe6bc2a7f307a6c246026a905612a40ef64859", - "reference": "1ebe6bc2a7f307a6c246026a905612a40ef64859", + "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/610392f38de49a44dab08dc1659960a29874c4b8", + "reference": "610392f38de49a44dab08dc1659960a29874c4b8", "shasum": "" }, - "require": { - "codeception/codeception": "^5.0.8", - "codeception/lib-innerbrowser": "^3.0 | ^4.0", - "php": "^8.0" - }, "require-dev": { - "codeception/module-asserts": ">= 3.0", - "codeception/module-filesystem": "> 3.0", - "codeception/verify": "^3.0", - "codemix/yii2-localeurls": "^1.7", - "phpstan/phpstan": "^1.10", - "yiisoft/yii2": "dev-master", - "yiisoft/yii2-app-advanced": "dev-master" + "php-coveralls/php-coveralls": "^1.0", + "phpunit/phpunit": "^5|^6|^7|^8|^9" }, "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "iamcal\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -9111,102 +11587,132 @@ ], "authors": [ { - "name": "Alexander Makarov" - }, - { - "name": "Sam Mouse" - }, - { - "name": "Michael Bodnarchuk" + "name": "Cal Henderson", + "email": "cal@iamcal.com" } ], - "description": "Codeception module for Yii2 framework", - "homepage": "https://codeception.com/", - "keywords": [ - "codeception", - "yii2" - ], + "description": "MySQL schema parser", "support": { - "issues": "https://github.com/Codeception/module-yii2/issues", - "source": "https://github.com/Codeception/module-yii2/tree/1.1.12" + "issues": "https://github.com/iamcal/SQLParser/issues", + "source": "https://github.com/iamcal/SQLParser/tree/v0.7" }, - "time": "2024-12-09T14:34:26+00:00" + "time": "2026-01-28T22:20:33+00:00" }, { - "name": "codeception/stub", - "version": "4.3.0", + "name": "jean85/pretty-package-versions", + "version": "2.1.1", "source": { "type": "git", - "url": "https://github.com/Codeception/Stub.git", - "reference": "6305b97eaf6ea9bdaed29a5bd4d6f2948f577d8f" + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/6305b97eaf6ea9bdaed29a5bd4d6f2948f577d8f", - "reference": "6305b97eaf6ea9bdaed29a5bd4d6f2948f577d8f", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", "shasum": "" }, "require": { - "php": "^8.1", - "phpunit/phpunit": "^8.4 | ^9.0 | ^10.0 | ^11 | ^12 | ^13" - }, - "conflict": { - "codeception/codeception": "<5.0.6" + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" }, "require-dev": { - "consolidation/robo": "^4.0" + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, "autoload": { "psr-4": { - "Codeception\\": "src/" + "Jean85\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], "support": { - "issues": "https://github.com/Codeception/Stub/issues", - "source": "https://github.com/Codeception/Stub/tree/4.3.0" + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" }, - "time": "2026-02-06T15:19:04+00:00" + "time": "2025-03-19T14:43:43+00:00" }, { - "name": "composer/ca-bundle", - "version": "1.5.12", + "name": "larastan/larastan", + "version": "v3.10.0", "source": { "type": "git", - "url": "https://github.com/composer/ca-bundle.git", - "reference": "00a2f4201641d5c53f7fc0195e6c8d9fcc321a78" + "url": "https://github.com/larastan/larastan.git", + "reference": "2970f83398154178a739609c244577267c7ee8eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/00a2f4201641d5c53f7fc0195e6c8d9fcc321a78", - "reference": "00a2f4201641d5c53f7fc0195e6c8d9fcc321a78", + "url": "https://api.github.com/repos/larastan/larastan/zipball/2970f83398154178a739609c244577267c7ee8eb", + "reference": "2970f83398154178a739609c244577267c7ee8eb", "shasum": "" }, "require": { - "ext-openssl": "*", - "ext-pcre": "*", - "php": "^7.2 || ^8.0" + "ext-json": "*", + "iamcal/sql-parser": "^0.7.0", + "illuminate/console": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/container": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/contracts": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/database": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/http": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/support": "^11.44.2 || ^12.4.1 || ^13", + "php": "^8.2", + "phpstan/phpstan": "^2.2.0" }, "require-dev": { - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8 || ^9", - "psr/log": "^1.0 || ^2.0 || ^3.0", - "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "doctrine/coding-standard": "^14", + "laravel/framework": "^11.44.2 || ^12.7.2 || ^13", + "mockery/mockery": "^1.6.12", + "nikic/php-parser": "^5.4", + "orchestra/canvas": "^v9.2.2 || ^10.0.1 || ^11", + "orchestra/testbench-core": "^9.12.0 || ^10.1 || ^11", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8 || ^13.1.8" }, - "type": "library", + "suggest": { + "orchestra/testbench": "Using Larastan for analysing a package needs Testbench", + "phpmyadmin/sql-parser": "Install to enable Larastan's optional phpMyAdmin-based SQL parser automatically" + }, + "type": "phpstan-extension", "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, "branch-alias": { - "dev-main": "1.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { "psr-4": { - "Composer\\CaBundle\\": "src" + "Larastan\\Larastan\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -9215,164 +11721,222 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "Can Vural", + "email": "can9119@gmail.com" } ], - "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel", "keywords": [ - "cabundle", - "cacert", - "certificate", - "ssl", - "tls" + "PHPStan", + "code analyse", + "code analysis", + "larastan", + "laravel", + "package", + "php", + "static analysis" ], "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.12" + "issues": "https://github.com/larastan/larastan/issues", + "source": "https://github.com/larastan/larastan/tree/v3.10.0" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/canvural", "type": "github" } ], - "time": "2026-05-19T11:26:22+00:00" + "time": "2026-05-28T08:00:58+00:00" }, { - "name": "craftcms/ckeditor", - "version": "4.11.4", + "name": "laravel/pail", + "version": "v1.2.7", "source": { "type": "git", - "url": "https://github.com/craftcms/ckeditor.git", - "reference": "f33fa49e868c3a26d799d0b2c7c410119599d9a2" + "url": "https://github.com/laravel/pail.git", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/ckeditor/zipball/f33fa49e868c3a26d799d0b2c7c410119599d9a2", - "reference": "f33fa49e868c3a26d799d0b2c7c410119599d9a2", + "url": "https://api.github.com/repos/laravel/pail/zipball/2f7d27dada8effc48b8c424445a69cca7007daaa", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa", "shasum": "" }, "require": { - "craftcms/cms": "^5.6.1", - "craftcms/html-field": "^3.4.0", - "embed/embed": "^4.4", - "nystudio107/craft-code-editor": ">=1.0.8 <=1.0.13 || ^1.0.16", - "php": "^8.2" + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" }, "require-dev": { - "craftcms/ecs": "dev-main", - "craftcms/phpstan": "dev-main", - "craftcms/rector": "dev-main", - "vlucas/phpdotenv": "^5.5" + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" }, - "type": "craft-plugin", + "type": "library", "extra": { - "name": "CKEditor", - "handle": "ckeditor", - "documentationUrl": "https://github.com/craftcms/ckeditor/blob/master/README.md" + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } }, "autoload": { "psr-4": { - "craft\\ckeditor\\": "src/" + "Laravel\\Pail\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "GPL-3.0-or-later" + "MIT" ], "authors": [ { - "name": "Pixel & Tonic", - "homepage": "https://pixelandtonic.com/" + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" } ], - "description": "Edit rich text content in Craft CMS using CKEditor.", + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", "keywords": [ - "CKEditor", - "cms", - "craftcms", - "html", - "yii2" + "dev", + "laravel", + "logs", + "php", + "tail" ], "support": { - "docs": "https://github.com/craftcms/ckeditor/blob/master/README.md", - "email": "support@craftcms.com", - "issues": "https://github.com/craftcms/ckeditor/issues?state=open", - "rss": "https://github.com/craftcms/ckeditor/commits/master.atom", - "source": "https://github.com/craftcms/ckeditor" + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" }, - "time": "2026-03-30T19:13:59+00:00" + "time": "2026-05-20T22:24:57+00:00" }, { - "name": "craftcms/ecs", - "version": "dev-main", + "name": "laravel/tinker", + "version": "v3.0.2", "source": { "type": "git", - "url": "https://github.com/craftcms/ecs.git", - "reference": "3823f989668e12a85ba681f8c7f3fd8488e23066" + "url": "https://github.com/laravel/tinker.git", + "reference": "4faba77764bd33411735936acdf30446d058c78b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/ecs/zipball/3823f989668e12a85ba681f8c7f3fd8488e23066", - "reference": "3823f989668e12a85ba681f8c7f3fd8488e23066", + "url": "https://api.github.com/repos/laravel/tinker/zipball/4faba77764bd33411735936acdf30446d058c78b", + "reference": "4faba77764bd33411735936acdf30446d058c78b", "shasum": "" }, "require": { - "php": "^7.2.5|^8.0.2", - "symplify/easy-coding-standard": "^10.3.3" + "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "psy/psysh": "^0.12.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5|^11.5" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^8.0|^9.0|^10.0|^11.0|^12.0|^13.0)." }, - "default-branch": true, "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.x-dev" + } + }, "autoload": { "psr-4": { - "craft\\ecs\\": "src" + "Laravel\\Tinker\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", - "description": "Easy Coding Standard configurations for Craft CMS projects", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], "support": { - "issues": "https://github.com/craftcms/ecs/issues", - "source": "https://github.com/craftcms/ecs/tree/main" + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v3.0.2" }, - "time": "2024-08-07T21:54:45+00:00" + "time": "2026-03-17T14:54:13+00:00" }, { - "name": "craftcms/html-field", - "version": "3.5.1", + "name": "league/factory-muffin", + "version": "v3.3.0", "source": { "type": "git", - "url": "https://github.com/craftcms/html-field.git", - "reference": "b4e1ae3f020d6081cfe3abf653f663da744c4cec" + "url": "https://github.com/thephpleague/factory-muffin.git", + "reference": "62c8c31d47667523da14e83df36cc897d34173cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/html-field/zipball/b4e1ae3f020d6081cfe3abf653f663da744c4cec", - "reference": "b4e1ae3f020d6081cfe3abf653f663da744c4cec", + "url": "https://api.github.com/repos/thephpleague/factory-muffin/zipball/62c8c31d47667523da14e83df36cc897d34173cd", + "reference": "62c8c31d47667523da14e83df36cc897d34173cd", "shasum": "" }, "require": { - "craftcms/cms": "^5.5.0", - "league/html-to-markdown": "^5.1", - "php": "^8.2", - "symfony/css-selector": "^6.0|^7.0", - "symfony/dom-crawler": "^6.0|^7.0" + "php": ">=5.4.0" + }, + "replace": { + "zizaco/factory-muff": "self.version" }, "require-dev": { - "craftcms/ecs": "dev-main", - "craftcms/phpstan": "dev-main", - "craftcms/rector": "dev-main" + "doctrine/orm": "^2.5", + "illuminate/database": "5.0.* || 5.1.* || 5.5.* || ^6.0", + "league/factory-muffin-faker": "^2.3", + "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20" + }, + "suggest": { + "doctrine/orm": "Factory Muffin supports doctrine through the repository store.", + "illuminate/database": "Factory Muffin supports eloquent through the model store.", + "league/factory-muffin-faker": "Factory Muffin is very powerful together with faker." }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, "autoload": { "psr-4": { - "craft\\htmlfield\\": "src/" + "League\\FactoryMuffin\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -9381,328 +11945,332 @@ ], "authors": [ { - "name": "Pixel & Tonic", - "homepage": "https://pixelandtonic.com/" + "name": "Graham Campbell", + "email": "graham@alt-three.com" + }, + { + "name": "Scott Robertson", + "email": "scottymeuk@gmail.com" } ], - "description": "Base class for Craft CMS field types with HTML values.", - "support": { - "docs": "https://github.com/craftcms/html-field/blob/main/README.md", - "email": "support@craftcms.com", - "issues": "https://github.com/craftcms/html-field/issues?state=open", - "rss": "https://github.com/craftcms/html-field/commits/main.atom", - "source": "https://github.com/craftcms/html-field" - }, - "time": "2026-03-19T18:11:31+00:00" - }, - { - "name": "craftcms/phpstan", - "version": "dev-main", - "source": { - "type": "git", - "url": "https://github.com/craftcms/phpstan.git", - "reference": "b61bba102b5ec8599406e6e29a28a20c915a6abc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/craftcms/phpstan/zipball/b61bba102b5ec8599406e6e29a28a20c915a6abc", - "reference": "b61bba102b5ec8599406e6e29a28a20c915a6abc", - "shasum": "" - }, - "require": { - "phpstan/phpstan": "^1.4.6" - }, - "default-branch": true, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "description": "PHPStan configuration for Craft CMS projects", + "description": "The goal of this package is to enable the rapid creation of objects for the purpose of testing.", + "homepage": "http://factory-muffin.thephpleague.com/", + "keywords": [ + "factory", + "testing" + ], "support": { - "issues": "https://github.com/craftcms/phpstan/issues", - "source": "https://github.com/craftcms/phpstan/tree/main" + "issues": "https://github.com/thephpleague/factory-muffin/issues", + "source": "https://github.com/thephpleague/factory-muffin/tree/v3.3.0" }, - "time": "2022-04-12T20:50:18+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/factory-muffin", + "type": "tidelift" + } + ], + "time": "2020-12-13T18:38:47+00:00" }, { - "name": "craftcms/rector", - "version": "dev-main", + "name": "mikehaertl/php-shellcommand", + "version": "1.7.0", "source": { "type": "git", - "url": "https://github.com/craftcms/rector.git", - "reference": "fab0a0ed308aa6cf59968a526f5db635103e5de1" + "url": "https://github.com/mikehaertl/php-shellcommand.git", + "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/rector/zipball/fab0a0ed308aa6cf59968a526f5db635103e5de1", - "reference": "fab0a0ed308aa6cf59968a526f5db635103e5de1", + "url": "https://api.github.com/repos/mikehaertl/php-shellcommand/zipball/e79ea528be155ffdec6f3bf1a4a46307bb49e545", + "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545", "shasum": "" }, "require": { - "rector/rector": "^1.0.0" + "php": ">= 5.3.0" }, "require-dev": { - "craftcms/cms": "^4.0.0|^5.0.0", - "craftcms/ecs": "dev-main", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.0", - "symfony/var-exporter": "^6.0" + "phpunit/phpunit": ">4.0 <=9.4" }, - "default-branch": true, "type": "library", "autoload": { "psr-4": { - "craft\\rector\\": "src" + "mikehaertl\\shellcommand\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", - "description": "Rector sets to automate Craft CMS upgrades", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Härtl", + "email": "haertl.mike@gmail.com" + } + ], + "description": "An object oriented interface to shell commands", + "keywords": [ + "shell" + ], "support": { - "issues": "https://github.com/craftcms/rector/issues", - "source": "https://github.com/craftcms/rector/tree/main" + "issues": "https://github.com/mikehaertl/php-shellcommand/issues", + "source": "https://github.com/mikehaertl/php-shellcommand/tree/1.7.0" }, - "time": "2024-05-17T09:09:56+00:00" + "time": "2023-04-19T08:25:22+00:00" }, { - "name": "craftcms/redactor", - "version": "4.2.0", + "name": "mockery/mockery", + "version": "1.6.12", "source": { "type": "git", - "url": "https://github.com/craftcms/redactor.git", - "reference": "47bc7bc40312b7d02ef5bf4fd36bd2fd62f8043c" + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/redactor/zipball/47bc7bc40312b7d02ef5bf4fd36bd2fd62f8043c", - "reference": "47bc7bc40312b7d02ef5bf4fd36bd2fd62f8043c", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", "shasum": "" }, "require": { - "craftcms/cms": "^5.3.0", - "craftcms/html-field": "^3.0.0", - "php": "^8.2" + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" }, - "require-dev": { - "craftcms/ecs": "dev-main", - "craftcms/phpstan": "dev-main", - "craftcms/rector": "dev-main" + "conflict": { + "phpunit/phpunit": "<8.0" }, - "type": "craft-plugin", - "extra": { - "name": "Redactor", - "handle": "redactor", - "documentationUrl": "https://github.com/craftcms/redactor/blob/v2/README.md" + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" }, + "type": "library", "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], "psr-4": { - "craft\\redactor\\": "src/" + "Mockery\\": "library/Mockery" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Pixel & Tonic", - "homepage": "https://pixelandtonic.com/" + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" } ], - "description": "Edit rich text content in Craft CMS using Redactor by Imperavi.", + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", "keywords": [ - "Redactor", - "cms", - "craftcms", - "html", - "yii2" + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" ], "support": { - "docs": "https://github.com/craftcms/redactor/blob/v2/README.md", - "email": "support@craftcms.com", - "issues": "https://github.com/craftcms/redactor/issues?state=open", - "rss": "https://github.com/craftcms/redactor/commits/v2.atom", - "source": "https://github.com/craftcms/redactor" + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" }, - "abandoned": "craftcms/ckeditor", - "time": "2024-09-03T13:38:27+00:00" + "time": "2024-05-16T03:13:13+00:00" }, { - "name": "embed/embed", - "version": "v4.4.19", + "name": "myclabs/deep-copy", + "version": "1.14.0", "source": { "type": "git", - "url": "https://github.com/php-embed/Embed.git", - "reference": "c45a9007285524350499f3fa70e7e2f0967af470" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-embed/Embed/zipball/c45a9007285524350499f3fa70e7e2f0967af470", - "reference": "c45a9007285524350499f3fa70e7e2f0967af470", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", "shasum": "" }, "require": { - "composer/ca-bundle": "^1.0", - "ext-curl": "*", - "ext-dom": "*", - "ext-json": "*", - "ext-mbstring": "*", - "ml/json-ld": "^1.1", - "oscarotero/html-parser": "^0.1.4", - "php": "^7.4|^8", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0|^2.0" + "php": "^8.0" }, - "require-dev": { - "brick/varexporter": "^0.3.1", - "friendsofphp/php-cs-fixer": "^2.0", - "nyholm/psr7": "^1.2", - "oscarotero/php-cs-fixer-config": "^1.0", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.0", - "symfony/css-selector": "^5.0" + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, - "suggest": { - "symfony/css-selector": "If you want to get elements using css selectors" + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", "autoload": { "files": [ - "src/functions.php" + "src/DeepCopy/deep_copy.php" ], "psr-4": { - "Embed\\": "src" + "DeepCopy\\": "src/DeepCopy/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Oscar Otero", - "email": "oom@oscarotero.com", - "homepage": "http://oscarotero.com", - "role": "Developer" - } - ], - "description": "PHP library to retrieve page info using oembed, opengraph, etc", - "homepage": "https://github.com/oscarotero/Embed", + "description": "Create deep copies (clones) of your objects", "keywords": [ - "embed", - "embedly", - "oembed", - "opengraph", - "twitter cards" + "clone", + "copy", + "duplicate", + "object", + "object graph" ], "support": { - "email": "oom@oscarotero.com", - "issues": "https://github.com/oscarotero/Embed/issues", - "source": "https://github.com/php-embed/Embed/tree/v4.4.19" + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" }, "funding": [ { - "url": "https://paypal.me/oscarotero", - "type": "custom" - }, - { - "url": "https://github.com/oscarotero", + "url": "https://github.com/mnapoli", "type": "github" - }, - { - "url": "https://www.patreon.com/misteroom", - "type": "patreon" } ], - "time": "2026-07-08T19:24:10+00:00" + "time": "2026-08-11T10:17:44+00:00" }, { - "name": "fakerphp/faker", - "version": "v1.24.1", + "name": "nikic/php-parser", + "version": "v5.9.0", "source": { "type": "git", - "url": "https://github.com/FakerPHP/Faker.git", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0", - "psr/container": "^1.0 || ^2.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" - }, - "conflict": { - "fzaninotto/faker": "*" + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", - "doctrine/persistence": "^1.3 || ^2.0", - "ext-intl": "*", - "phpunit/phpunit": "^9.5.26", - "symfony/phpunit-bridge": "^5.4.16" - }, - "suggest": { - "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", - "ext-curl": "Required by Faker\\Provider\\Image to download images.", - "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", - "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", - "ext-mbstring": "Required for multibyte Unicode string functionality." + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" }, + "bin": [ + "bin/php-parse" + ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, "autoload": { "psr-4": { - "Faker\\": "src/Faker/" + "PhpParser\\": "lib/PhpParser" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "François Zaninotto" + "name": "Nikita Popov" } ], - "description": "Faker is a PHP library that generates fake data for you.", + "description": "A PHP parser written in PHP", "keywords": [ - "data", - "faker", - "fixtures" + "parser", + "php" ], "support": { - "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" }, - "time": "2024-11-21T13:46:39+00:00" + "time": "2026-09-13T18:51:52+00:00" }, { - "name": "graham-campbell/result-type", - "version": "v1.1.4", + "name": "nunomaduro/collision", + "version": "v8.9.4", "source": { "type": "git", - "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + "url": "https://github.com/nunomaduro/collision.git", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", - "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.5" + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.8 || ^8.0.8" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" }, "require-dev": { - "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" }, "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], "psr-4": { - "GrahamCampbell\\ResultType\\": "src/" + "NunoMaduro\\Collision\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -9711,74 +12279,92 @@ ], "authors": [ { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" } ], - "description": "An Implementation Of The Result Type", + "description": "Cli error handling for console/command-line PHP applications.", "keywords": [ - "Graham Campbell", - "GrahamCampbell", - "Result Type", - "Result-Type", - "result" + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" ], "support": { - "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" }, "funding": [ { - "url": "https://github.com/GrahamCampbell", + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", - "type": "tidelift" + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" } ], - "time": "2025-12-27T19:43:20+00:00" + "time": "2026-04-21T14:04:20+00:00" }, { - "name": "justinrainbow/json-schema", - "version": "6.10.0", + "name": "orchestra/canvas", + "version": "v11.0.1", "source": { "type": "git", - "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33" + "url": "https://github.com/orchestral/canvas.git", + "reference": "d240410f4cd89b380d7d89b5bbaf60c32f4fb691" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", - "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", + "url": "https://api.github.com/repos/orchestral/canvas/zipball/d240410f4cd89b380d7d89b5bbaf60c32f4fb691", + "reference": "d240410f4cd89b380d7d89b5bbaf60c32f4fb691", "shasum": "" }, "require": { - "ext-json": "*", - "marc-mabe/php-enum": "^4.4", - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "3.3.0", - "json-schema/json-schema-test-suite": "dev-main", - "marc-mabe/php-enum-phpstan": "^2.0", - "phpspec/prophecy": "^1.19", - "phpstan/phpstan": "^1.12", - "phpunit/phpunit": "^8.5" + "composer-runtime-api": "^2.2", + "composer/semver": "^3.0", + "illuminate/console": "^13.0.0", + "illuminate/database": "^13.0.0", + "illuminate/filesystem": "^13.0.0", + "illuminate/support": "^13.0.0", + "orchestra/canvas-core": "^11.0.0", + "orchestra/sidekick": "~1.1.23|~1.2.20", + "orchestra/testbench-core": "^11.0.0", + "php": "^8.3", + "symfony/yaml": "^7.4.0|^8.0.0" + }, + "require-dev": { + "laravel/framework": "^13.0.0", + "laravel/pint": "^1.24", + "mockery/mockery": "^1.6.10", + "phpstan/phpstan": "^2.1.14", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0" }, "bin": [ - "bin/validate-json" + "canvas" ], "type": "library", "extra": { - "branch-alias": { - "dev-master": "6.x-dev" + "laravel": { + "providers": [ + "Orchestra\\Canvas\\LaravelServiceProvider" + ] } }, "autoload": { "psr-4": { - "JsonSchema\\": "src/JsonSchema/" + "Orchestra\\Canvas\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -9787,74 +12373,63 @@ ], "authors": [ { - "name": "Bruno Prieto Reis", - "email": "bruno.p.reis@gmail.com" - }, - { - "name": "Justin Rainbow", - "email": "justin.rainbow@gmail.com" - }, - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" + "name": "Taylor Otwell", + "email": "taylor@laravel.com" }, { - "name": "Robert Schönthal", - "email": "seroscho@googlemail.com" + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" } ], - "description": "A library to validate a json schema.", - "homepage": "https://github.com/jsonrainbow/json-schema", - "keywords": [ - "json", - "schema" - ], + "description": "Code Generators for Laravel Applications and Packages", "support": { - "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.10.0" + "issues": "https://github.com/orchestral/canvas/issues", + "source": "https://github.com/orchestral/canvas/tree/v11.0.1" }, - "time": "2026-06-16T20:50:26+00:00" + "time": "2026-03-18T22:46:12+00:00" }, { - "name": "league/factory-muffin", - "version": "v3.3.0", + "name": "orchestra/canvas-core", + "version": "v11.0.0", "source": { "type": "git", - "url": "https://github.com/thephpleague/factory-muffin.git", - "reference": "62c8c31d47667523da14e83df36cc897d34173cd" + "url": "https://github.com/orchestral/canvas-core.git", + "reference": "88d091ff989748e2ca447bca0cd06ab14671ba82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/factory-muffin/zipball/62c8c31d47667523da14e83df36cc897d34173cd", - "reference": "62c8c31d47667523da14e83df36cc897d34173cd", + "url": "https://api.github.com/repos/orchestral/canvas-core/zipball/88d091ff989748e2ca447bca0cd06ab14671ba82", + "reference": "88d091ff989748e2ca447bca0cd06ab14671ba82", "shasum": "" }, "require": { - "php": ">=5.4.0" - }, - "replace": { - "zizaco/factory-muff": "self.version" + "composer-runtime-api": "^2.2", + "composer/semver": "^3.0", + "illuminate/console": "^13.0", + "illuminate/support": "^13.0", + "orchestra/sidekick": "~1.1.23|~1.2.20", + "php": "^8.3" }, "require-dev": { - "doctrine/orm": "^2.5", - "illuminate/database": "5.0.* || 5.1.* || 5.5.* || ^6.0", - "league/factory-muffin-faker": "^2.3", - "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20" - }, - "suggest": { - "doctrine/orm": "Factory Muffin supports doctrine through the repository store.", - "illuminate/database": "Factory Muffin supports eloquent through the model store.", - "league/factory-muffin-faker": "Factory Muffin is very powerful together with faker." + "laravel/framework": "^13.0", + "laravel/pint": "^1.24", + "mockery/mockery": "^1.6.10", + "orchestra/testbench-core": "^11.0", + "phpstan/phpstan": "^2.1.17", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", + "symfony/yaml": "^7.4|^8.0" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "3.3-dev" + "laravel": { + "providers": [ + "Orchestra\\Canvas\\Core\\LaravelServiceProvider" + ] } }, "autoload": { "psr-4": { - "League\\FactoryMuffin\\": "src/" + "Orchestra\\Canvas\\Core\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -9863,66 +12438,61 @@ ], "authors": [ { - "name": "Graham Campbell", - "email": "graham@alt-three.com" + "name": "Taylor Otwell", + "email": "taylor@laravel.com" }, { - "name": "Scott Robertson", - "email": "scottymeuk@gmail.com" + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" } ], - "description": "The goal of this package is to enable the rapid creation of objects for the purpose of testing.", - "homepage": "http://factory-muffin.thephpleague.com/", - "keywords": [ - "factory", - "testing" - ], + "description": "Code Generators Builder for Laravel Applications and Packages", "support": { - "issues": "https://github.com/thephpleague/factory-muffin/issues", - "source": "https://github.com/thephpleague/factory-muffin/tree/v3.3.0" + "issues": "https://github.com/orchestral/canvas/issues", + "source": "https://github.com/orchestral/canvas-core/tree/v11.0.0" }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/league/factory-muffin", - "type": "tidelift" - } - ], - "time": "2020-12-13T18:38:47+00:00" + "time": "2026-03-16T15:10:50+00:00" }, { - "name": "league/factory-muffin-faker", - "version": "v2.3.0", + "name": "orchestra/sidekick", + "version": "v1.2.20", "source": { "type": "git", - "url": "https://github.com/thephpleague/factory-muffin-faker.git", - "reference": "258068c840e8fdc45d1cb1636a0890e92f2e864a" + "url": "https://github.com/orchestral/sidekick.git", + "reference": "267a71b56cb2fe1a634d69fc99889c671b77ff43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/factory-muffin-faker/zipball/258068c840e8fdc45d1cb1636a0890e92f2e864a", - "reference": "258068c840e8fdc45d1cb1636a0890e92f2e864a", + "url": "https://api.github.com/repos/orchestral/sidekick/zipball/267a71b56cb2fe1a634d69fc99889c671b77ff43", + "reference": "267a71b56cb2fe1a634d69fc99889c671b77ff43", "shasum": "" }, "require": { - "fakerphp/faker": "^1.9.1", - "php": ">=5.4.0" + "composer-runtime-api": "^2.2", + "composer/semver": "^3.0", + "php": "^8.1", + "symfony/polyfill-php83": "^1.32" }, "require-dev": { - "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20" + "fakerphp/faker": "^1.21", + "laravel/framework": "^10.48.29|^11.44.7|^12.1.1|^13.0", + "laravel/pint": "^1.4", + "mockery/mockery": "^1.5.1", + "orchestra/testbench-core": "^8.37.0|^9.14.0|^10.2.0|^11.0", + "phpstan/phpstan": "^2.1.14", + "phpunit/phpunit": "^10.0|^11.0|^12.0", + "symfony/process": "^6.0|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.3-dev" - } - }, "autoload": { + "files": [ + "src/Eloquent/functions.php", + "src/Filesystem/functions.php", + "src/Http/functions.php", + "src/functions.php" + ], "psr-4": { - "League\\FactoryMuffin\\Faker\\": "src/" + "Orchestra\\Sidekick\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -9931,72 +12501,136 @@ ], "authors": [ { - "name": "Graham Campbell", - "email": "graham@alt-three.com" + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" } ], - "description": "The goal of this package is to wrap faker to make it super easy to use with factory muffin.", - "homepage": "http://factory-muffin.thephpleague.com/", - "keywords": [ - "factory", - "faker", - "testing" - ], + "description": "Packages Toolkit Utilities and Helpers for Laravel", "support": { - "issues": "https://github.com/thephpleague/factory-muffin-faker/issues", - "source": "https://github.com/thephpleague/factory-muffin-faker/tree/v2.3.0" + "issues": "https://github.com/orchestral/sidekick/issues", + "source": "https://github.com/orchestral/sidekick/tree/v1.2.20" }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, + "time": "2026-01-12T11:09:33+00:00" + }, + { + "name": "orchestra/testbench", + "version": "v11.1.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/testbench.git", + "reference": "997f33e5200c7e8db4756b35a9deb3f5f3086759" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/testbench/zipball/997f33e5200c7e8db4756b35a9deb3f5f3086759", + "reference": "997f33e5200c7e8db4756b35a9deb3f5f3086759", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "fakerphp/faker": "^1.23", + "laravel/framework": "^13.1.1", + "mockery/mockery": "^1.6.10", + "orchestra/testbench-core": "^11.2.0", + "orchestra/workbench": "^11.0.1", + "php": "^8.3", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", + "symfony/process": "^7.4.5|^8.0.5", + "symfony/yaml": "^7.4|^8.0", + "vlucas/phpdotenv": "^5.6.1" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://tidelift.com/funding/github/packagist/league/factory-muffin-faker", - "type": "tidelift" + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com", + "homepage": "https://github.com/crynobone" } ], - "time": "2020-12-13T15:53:28+00:00" + "description": "Laravel Testing Helper for Packages Development", + "homepage": "https://packages.tools/testbench/", + "keywords": [ + "BDD", + "TDD", + "dev", + "laravel", + "laravel-packages", + "testing" + ], + "support": { + "issues": "https://github.com/orchestral/testbench/issues", + "source": "https://github.com/orchestral/testbench/tree/v11.1.0" + }, + "time": "2026-04-09T05:11:06+00:00" }, { - "name": "league/html-to-markdown", - "version": "5.1.1", + "name": "orchestra/testbench-core", + "version": "v11.3.4", "source": { "type": "git", - "url": "https://github.com/thephpleague/html-to-markdown.git", - "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd" + "url": "https://github.com/orchestral/testbench-core.git", + "reference": "527fe9941b8bdec2914d2a19048b0c40c6c5d87c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0b4066eede55c48f38bcee4fb8f0aa85654390fd", - "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd", + "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/527fe9941b8bdec2914d2a19048b0c40c6c5d87c", + "reference": "527fe9941b8bdec2914d2a19048b0c40c6c5d87c", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-xml": "*", - "php": "^7.2.5 || ^8.0" + "composer-runtime-api": "^2.2", + "orchestra/sidekick": "~1.1.23|~1.2.20", + "php": "^8.3", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-php84": "^1.34.0" }, - "require-dev": { - "mikehaertl/php-shellcommand": "^1.1.0", - "phpstan/phpstan": "^1.8.8", - "phpunit/phpunit": "^8.5 || ^9.2", - "scrutinizer/ocular": "^1.6", - "unleashedtech/php-coding-standard": "^2.7 || ^3.0", - "vimeo/psalm": "^4.22 || ^5.0" + "conflict": { + "brianium/paratest": "<7.3.0|>=8.0.0", + "laravel/framework": "<13.10.0|>=14.0.0", + "laravel/serializable-closure": ">=2.0.0 <2.0.10|>=3.0.0", + "nunomaduro/collision": "<8.9.0|>=9.0.0", + "phpunit/phpunit": "<11.5.50|>=12.0.0 <12.5.8|>=13.2.0" + }, + "require-dev": { + "fakerphp/faker": "^1.24", + "laravel/framework": "^13.10.0", + "laravel/pint": "^1.24", + "laravel/serializable-closure": "^2.0.10", + "mockery/mockery": "^1.6.10", + "phpstan/phpstan": "^2.1.38", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", + "spatie/laravel-ray": "^1.43.6", + "symfony/process": "^7.4.5|^8.0.5", + "symfony/yaml": "^7.4.0|^8.0.0", + "vlucas/phpdotenv": "^5.6.1" + }, + "suggest": { + "brianium/paratest": "Allow using parallel testing (^7.3).", + "ext-pcntl": "Required to use all features of the console signal trapping.", + "fakerphp/faker": "Allow using Faker for testing (^1.23).", + "laravel/framework": "Required for testing (^13.9.0).", + "mockery/mockery": "Allow using Mockery for testing (^1.6).", + "nunomaduro/collision": "Allow using Laravel style tests output and parallel testing (^8.9).", + "orchestra/testbench-dusk": "Allow using Laravel Dusk for testing (^11.0).", + "phpunit/phpunit": "Allow using PHPUnit for testing (^11.5.50|^12.5.8|^13.0.0).", + "symfony/process": "Required to use Orchestra\\Testbench\\remote function (^7.4|^8.0).", + "symfony/yaml": "Required for Testbench CLI (^7.4|^8.0).", + "vlucas/phpdotenv": "Required for Testbench CLI (^5.6.1)." }, "bin": [ - "bin/html-to-markdown" + "testbench" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.2-dev" - } - }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "League\\HTMLToMarkdown\\": "src/" + "Orchestra\\Testbench\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -10005,144 +12639,176 @@ ], "authors": [ { - "name": "Colin O'Dell", - "email": "colinodell@gmail.com", - "homepage": "https://www.colinodell.com", - "role": "Lead Developer" - }, - { - "name": "Nick Cernis", - "email": "nick@cern.is", - "homepage": "http://modernnerd.net", - "role": "Original Author" + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com", + "homepage": "https://github.com/crynobone" } ], - "description": "An HTML-to-markdown conversion helper for PHP", - "homepage": "https://github.com/thephpleague/html-to-markdown", + "description": "Testing Helper for Laravel Development", + "homepage": "https://packages.tools/testbench", "keywords": [ - "html", - "markdown" + "BDD", + "TDD", + "dev", + "laravel", + "laravel-packages", + "testing" ], "support": { - "issues": "https://github.com/thephpleague/html-to-markdown/issues", - "source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.1" + "issues": "https://github.com/orchestral/testbench/issues", + "source": "https://github.com/orchestral/testbench-core" }, - "funding": [ - { - "url": "https://www.colinodell.com/sponsor", - "type": "custom" - }, - { - "url": "https://www.paypal.me/colinpodell/10.00", - "type": "custom" - }, - { - "url": "https://github.com/colinodell", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown", - "type": "tidelift" - } - ], - "time": "2023-07-12T21:21:09+00:00" + "time": "2026-06-02T03:43:15+00:00" }, { - "name": "marc-mabe/php-enum", - "version": "v4.7.2", + "name": "orchestra/workbench", + "version": "v11.1.0", "source": { "type": "git", - "url": "https://github.com/marc-mabe/php-enum.git", - "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + "url": "https://github.com/orchestral/workbench.git", + "reference": "e750c7bcae4405e054ff286475502e23274de04b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", - "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "url": "https://api.github.com/repos/orchestral/workbench/zipball/e750c7bcae4405e054ff286475502e23274de04b", + "reference": "e750c7bcae4405e054ff286475502e23274de04b", "shasum": "" }, "require": { - "ext-reflection": "*", - "php": "^7.1 | ^8.0" + "composer-runtime-api": "^2.2", + "fakerphp/faker": "^1.23", + "laravel/framework": "^13.0.0", + "laravel/pail": "^1.2.5", + "laravel/tinker": "^3.0.0", + "nunomaduro/collision": "^8.9", + "orchestra/canvas": "^11.0.1", + "orchestra/sidekick": "~1.1.23|~1.2.20", + "orchestra/testbench-core": "^11.1.0", + "php": "^8.3", + "symfony/process": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "require-dev": { + "laravel/pint": "^1.22.0", + "mockery/mockery": "^1.6.12", + "phpstan/phpstan": "^2.1.33", + "phpunit/phpunit": "^11.5.50|^12.5.8|^13.0.0", + "spatie/laravel-ray": "^1.43.6" }, - "require-dev": { - "phpbench/phpbench": "^0.16.10 || ^1.0.4", - "phpstan/phpstan": "^1.3.1", - "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", - "vimeo/psalm": "^4.17.0 | ^5.26.1" + "suggest": { + "ext-pcntl": "Required to use all features of the console signal trapping." }, "type": "library", - "extra": { - "branch-alias": { - "dev-3.x": "3.2-dev", - "dev-master": "4.7-dev" - } - }, "autoload": { "psr-4": { - "MabeEnum\\": "src/" - }, - "classmap": [ - "stubs/Stringable.php" - ] + "Orchestra\\Workbench\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Marc Bennewitz", - "email": "dev@mabe.berlin", - "homepage": "https://mabe.berlin/", - "role": "Lead" + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" } ], - "description": "Simple and fast implementation of enumerations with native PHP", - "homepage": "https://github.com/marc-mabe/php-enum", + "description": "Workbench Companion for Laravel Packages Development", "keywords": [ - "enum", - "enum-map", - "enum-set", - "enumeration", - "enumerator", - "enummap", - "enumset", - "map", - "set", - "type", - "type-hint", - "typehint" + "dev", + "laravel", + "laravel-packages", + "testing" ], "support": { - "issues": "https://github.com/marc-mabe/php-enum/issues", - "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + "issues": "https://github.com/orchestral/workbench/issues", + "source": "https://github.com/orchestral/workbench/tree/v11.1.0" }, - "time": "2025-09-14T11:18:39+00:00" + "time": "2026-03-24T23:09:55+00:00" }, { - "name": "ml/iri", - "version": "1.1.4", - "target-dir": "ML/IRI", + "name": "pestphp/pest", + "version": "v4.7.2", "source": { "type": "git", - "url": "https://github.com/lanthaler/IRI.git", - "reference": "cbd44fa913e00ea624241b38cefaa99da8d71341" + "url": "https://github.com/pestphp/pest.git", + "reference": "40b88b62ef8a7c6fcae5fc28f1fa747f601c131b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lanthaler/IRI/zipball/cbd44fa913e00ea624241b38cefaa99da8d71341", - "reference": "cbd44fa913e00ea624241b38cefaa99da8d71341", + "url": "https://api.github.com/repos/pestphp/pest/zipball/40b88b62ef8a7c6fcae5fc28f1fa747f601c131b", + "reference": "40b88b62ef8a7c6fcae5fc28f1fa747f601c131b", "shasum": "" }, "require": { - "lib-pcre": ">=4.0", - "php": ">=5.3.0" + "brianium/paratest": "^7.20.0", + "composer/xdebug-handler": "^3.0.5", + "nunomaduro/collision": "^8.9.4", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest-plugin": "^4.0.0", + "pestphp/pest-plugin-arch": "^4.0.2", + "pestphp/pest-plugin-mutate": "^4.0.1", + "pestphp/pest-plugin-profanity": "^4.2.1", + "php": "^8.3.0", + "phpunit/phpunit": "^12.5.28", + "symfony/process": "^7.4.13|^8.1.0" + }, + "conflict": { + "filp/whoops": "<2.18.3", + "phpunit/phpunit": ">12.5.28", + "sebastian/exporter": "<7.0.0", + "webmozart/assert": "<1.11.0" + }, + "require-dev": { + "mrpunyapal/peststan": "^0.2.10", + "pestphp/pest-dev-tools": "^4.1.0", + "pestphp/pest-plugin-browser": "^4.3.1", + "pestphp/pest-plugin-type-coverage": "^4.0.4", + "psy/psysh": "^0.12.23" }, + "bin": [ + "bin/pest" + ], "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Mutate\\Plugins\\Mutate", + "Pest\\Plugins\\Configuration", + "Pest\\Plugins\\Bail", + "Pest\\Plugins\\Cache", + "Pest\\Plugins\\Coverage", + "Pest\\Plugins\\Init", + "Pest\\Plugins\\Environment", + "Pest\\Plugins\\Help", + "Pest\\Plugins\\Memory", + "Pest\\Plugins\\Only", + "Pest\\Plugins\\Printer", + "Pest\\Plugins\\ProcessIsolation", + "Pest\\Plugins\\Profile", + "Pest\\Plugins\\Retry", + "Pest\\Plugins\\Snapshot", + "Pest\\Plugins\\Verbose", + "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", + "Pest\\Plugins\\Tia", + "Pest\\Plugins\\Parallel" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, "autoload": { - "psr-0": { - "ML\\IRI": "" + "files": [ + "src/Functions.php", + "src/Pest.php" + ], + "psr-4": { + "Pest\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -10151,226 +12817,278 @@ ], "authors": [ { - "name": "Markus Lanthaler", - "email": "mail@markus-lanthaler.com", - "homepage": "http://www.markus-lanthaler.com", - "role": "Developer" + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" } ], - "description": "IRI handling for PHP", - "homepage": "http://www.markus-lanthaler.com", + "description": "The elegant PHP Testing Framework.", "keywords": [ - "URN", - "iri", - "uri", - "url" + "framework", + "pest", + "php", + "test", + "testing", + "unit" ], "support": { - "issues": "https://github.com/lanthaler/IRI/issues", - "source": "https://github.com/lanthaler/IRI/tree/master" + "issues": "https://github.com/pestphp/pest/issues", + "source": "https://github.com/pestphp/pest/tree/v4.7.2" }, - "time": "2014-01-21T13:43:39+00:00" + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-06-01T06:08:59+00:00" }, { - "name": "ml/json-ld", - "version": "1.2.1", + "name": "pestphp/pest-plugin", + "version": "v4.0.0", "source": { "type": "git", - "url": "https://github.com/lanthaler/JsonLD.git", - "reference": "537e68e87a6bce23e57c575cd5dcac1f67ce25d8" + "url": "https://github.com/pestphp/pest-plugin.git", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lanthaler/JsonLD/zipball/537e68e87a6bce23e57c575cd5dcac1f67ce25d8", - "reference": "537e68e87a6bce23e57c575cd5dcac1f67ce25d8", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568", "shasum": "" }, "require": { - "ext-json": "*", - "ml/iri": "^1.1.1", - "php": ">=5.3.0" + "composer-plugin-api": "^2.0.0", + "composer-runtime-api": "^2.2.2", + "php": "^8.3" + }, + "conflict": { + "pestphp/pest": "<4.0.0" }, "require-dev": { - "json-ld/tests": "1.0", - "phpunit/phpunit": "^4" + "composer/composer": "^2.8.10", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Pest\\Plugin\\Manager" }, - "type": "library", "autoload": { "psr-4": { - "ML\\JsonLD\\": "" + "Pest\\Plugin\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Markus Lanthaler", - "email": "mail@markus-lanthaler.com", - "homepage": "http://www.markus-lanthaler.com", - "role": "Developer" - } - ], - "description": "JSON-LD Processor for PHP", - "homepage": "http://www.markus-lanthaler.com", + "description": "The Pest plugin manager", "keywords": [ - "JSON-LD", - "jsonld" + "framework", + "manager", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" ], "support": { - "issues": "https://github.com/lanthaler/JsonLD/issues", - "source": "https://github.com/lanthaler/JsonLD/tree/1.2.1" + "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0" }, - "time": "2022-09-29T08:45:17+00:00" + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-08-20T12:35:58+00:00" }, { - "name": "myclabs/deep-copy", - "version": "1.13.4", + "name": "pestphp/pest-plugin-arch", + "version": "v4.0.2", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + "url": "https://github.com/pestphp/pest-plugin-arch.git", + "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", + "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3 <3.2.2" + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "ta-tikoma/phpunit-architecture-test": "^0.8.7" }, "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpspec/prophecy": "^1.10", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + "pestphp/pest": "^4.4.6", + "pestphp/pest-dev-tools": "^4.1.0" }, "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Arch\\Plugin" + ] + } + }, "autoload": { "files": [ - "src/DeepCopy/deep_copy.php" + "src/Autoload.php" ], "psr-4": { - "DeepCopy\\": "src/DeepCopy/" + "Pest\\Arch\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Create deep copies (clones) of your objects", + "description": "The Arch plugin for Pest PHP.", "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" + "arch", + "architecture", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" ], "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.2" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" } ], - "time": "2025-08-01T08:46:24+00:00" + "time": "2026-04-10T17:20:19+00:00" }, { - "name": "nikic/php-parser", - "version": "v5.8.0", + "name": "pestphp/pest-plugin-laravel", + "version": "v4.1.0", "source": { "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" + "url": "https://github.com/pestphp/pest-plugin-laravel.git", + "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", - "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/3057a36669ff11416cc0dc2b521b3aec58c488d0", + "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0", "shasum": "" }, "require": { - "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" + "laravel/framework": "^11.45.2|^12.52.0|^13.0", + "pestphp/pest": "^4.4.1", + "php": "^8.3.0" }, "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^9.0" + "laravel/dusk": "^8.3.6", + "orchestra/testbench": "^9.13.0|^10.9.0|^11.0", + "pestphp/pest-dev-tools": "^4.1.0" }, - "bin": [ - "bin/php-parse" - ], "type": "library", "extra": { - "branch-alias": { - "dev-master": "5.x-dev" + "pest": { + "plugins": [ + "Pest\\Laravel\\Plugin" + ] + }, + "laravel": { + "providers": [ + "Pest\\Laravel\\PestServiceProvider" + ] } }, "autoload": { + "files": [ + "src/Autoload.php" + ], "psr-4": { - "PhpParser\\": "lib/PhpParser" + "Pest\\Laravel\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } + "MIT" ], - "description": "A PHP parser written in PHP", + "description": "The Pest Laravel Plugin", "keywords": [ - "parser", - "php" + "framework", + "laravel", + "pest", + "php", + "test", + "testing", + "unit" ], "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" + "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v4.1.0" }, - "time": "2026-07-04T14:30:18+00:00" + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-02-21T00:29:45+00:00" }, { - "name": "nystudio107/craft-code-editor", - "version": "1.0.29", + "name": "pestphp/pest-plugin-mutate", + "version": "v4.0.1", "source": { "type": "git", - "url": "https://github.com/nystudio107/craft-code-editor.git", - "reference": "5b071512ee2ad2b8004f979f88ff3bf722bbcb4d" + "url": "https://github.com/pestphp/pest-plugin-mutate.git", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nystudio107/craft-code-editor/zipball/5b071512ee2ad2b8004f979f88ff3bf722bbcb4d", - "reference": "5b071512ee2ad2b8004f979f88ff3bf722bbcb4d", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c", "shasum": "" }, "require": { - "craftcms/cms": "^3.0.0 || ^4.0.0 || ^5.0.0", - "phpdocumentor/reflection-docblock": "^5.0.0" + "nikic/php-parser": "^5.6.1", + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "psr/simple-cache": "^3.0.0" }, "require-dev": { - "craftcms/ecs": "dev-main", - "craftcms/phpstan": "dev-main", - "craftcms/rector": "dev-main" - }, - "type": "yii2-extension", - "extra": { - "bootstrap": "nystudio107\\codeeditor\\CodeEditor" + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.0" }, + "type": "library", "autoload": { "psr-4": { - "nystudio107\\codeeditor\\": "src/" + "Pest\\Mutate\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -10379,88 +13097,100 @@ ], "authors": [ { - "name": "nystudio107", - "homepage": "https://nystudio107.com" + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" } ], - "description": "Provides a code editor field with Twig & Craft API autocomplete", + "description": "Mutates your code to find untested cases", "keywords": [ - "Craft", - "Monaco", - "cms", - "code", - "craftcms", - "css", - "editor", - "javascript", - "markdown", - "twig" + "framework", + "mutate", + "mutation", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" ], "support": { - "docs": "https://github.com/nystudio107/craft-code-editor/blob/v1/README.md", - "issues": "https://github.com/nystudio107/craft-code-editor/issues", - "source": "https://github.com/nystudio107/craft-code-editor" + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1" }, "funding": [ { - "url": "https://github.com/khalwat", + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", "type": "github" } ], - "time": "2026-01-30T19:10:22+00:00" + "time": "2025-08-21T20:19:25+00:00" }, { - "name": "oscarotero/html-parser", - "version": "v0.1.8", + "name": "pestphp/pest-plugin-profanity", + "version": "v4.2.1", "source": { "type": "git", - "url": "https://github.com/oscarotero/html-parser.git", - "reference": "10f3219267a365d9433f2f7d1694209c9d436c8d" + "url": "https://github.com/pestphp/pest-plugin-profanity.git", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/oscarotero/html-parser/zipball/10f3219267a365d9433f2f7d1694209c9d436c8d", - "reference": "10f3219267a365d9433f2f7d1694209c9d436c8d", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27", "shasum": "" }, "require": { - "php": "^7.2 || ^8" + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.11", - "phpunit/phpunit": "^8.0" + "faissaloux/pest-plugin-inside": "^1.9", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" }, "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Profanity\\Plugin" + ] + } + }, "autoload": { "psr-4": { - "HtmlParser\\": "src" + "Pest\\Profanity\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Oscar Otero", - "email": "oom@oscarotero.com", - "homepage": "http://oscarotero.com", - "role": "Developer" - } - ], - "description": "Parse html strings to DOMDocument", - "homepage": "https://github.com/oscarotero/html-parser", + "description": "The Pest Profanity Plugin", "keywords": [ - "dom", - "html", - "parser" + "framework", + "pest", + "php", + "plugin", + "profanity", + "test", + "testing", + "unit" ], "support": { - "email": "oom@oscarotero.com", - "issues": "https://github.com/oscarotero/html-parser/issues", - "source": "https://github.com/oscarotero/html-parser/tree/v0.1.8" + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1" }, - "time": "2023-11-29T20:28:41+00:00" + "time": "2025-12-08T00:13:17+00:00" }, { "name": "phar-io/manifest", @@ -10580,92 +13310,17 @@ }, "time": "2022-02-21T01:04:05+00:00" }, - { - "name": "phpoption/phpoption", - "version": "1.9.5", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/php-option.git", - "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", - "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - }, - "branch-alias": { - "dev-master": "1.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpOption\\": "src/PhpOption/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh" - }, - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - } - ], - "description": "Option Type for PHP", - "keywords": [ - "language", - "option", - "php", - "type" - ], - "support": { - "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", - "type": "tidelift" - } - ], - "time": "2025-12-27T19:41:33+00:00" - }, { "name": "phpstan/phpstan", - "version": "1.12.33", + "version": "2.2.8", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1", - "reference": "37982d6fc7cbb746dda7773530cda557cdf119e1", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e285254e60f33c21902efef4a926ca0987c06804", + "reference": "e285254e60f33c21902efef4a926ca0987c06804", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -10684,6 +13339,17 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], "description": "PHPStan - PHP Static Analysis Tool", "keywords": [ "dev", @@ -10706,20 +13372,20 @@ "type": "github" } ], - "time": "2026-02-28T20:30:03+00:00" + "time": "2026-08-04T22:21:45+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.12", + "version": "12.5.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + "reference": "186dab580576598076de6818596d12b61801880e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", - "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", + "reference": "186dab580576598076de6818596d12b61801880e", "shasum": "" }, "require": { @@ -10727,18 +13393,16 @@ "ext-libxml": "*", "ext-xmlwriter": "*", "nikic/php-parser": "^5.7.0", - "php": ">=8.2", - "phpunit/php-file-iterator": "^5.1.0", - "phpunit/php-text-template": "^4.0.1", - "sebastian/code-unit-reverse-lookup": "^4.0.1", - "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.1", - "sebastian/lines-of-code": "^3.0.1", - "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.3.1" + "php": ">=8.3", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.1.2", + "sebastian/lines-of-code": "^4.0.1", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^11.5.46" + "phpunit/phpunit": "^12.5.28" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -10747,7 +13411,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "11.0.x-dev" + "dev-main": "12.5.x-dev" } }, "autoload": { @@ -10776,7 +13440,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.7" }, "funding": [ { @@ -10796,32 +13460,32 @@ "type": "tidelift" } ], - "time": "2025-12-24T07:01:01+00:00" + "time": "2026-06-01T13:24:19+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "5.1.1", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", - "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -10849,7 +13513,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" }, "funding": [ { @@ -10869,28 +13533,28 @@ "type": "tidelift" } ], - "time": "2026-02-02T13:52:54+00:00" + "time": "2026-02-02T14:04:18+00:00" }, { "name": "phpunit/php-invoker", - "version": "5.0.1", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-pcntl": "*" @@ -10898,7 +13562,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -10925,7 +13589,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" }, "funding": [ { @@ -10933,32 +13597,32 @@ "type": "github" } ], - "time": "2024-07-03T05:07:44+00:00" + "time": "2025-02-07T04:58:58+00:00" }, { "name": "phpunit/php-text-template", - "version": "4.0.1", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -10985,7 +13649,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" }, "funding": [ { @@ -10993,32 +13657,32 @@ "type": "github" } ], - "time": "2024-07-03T05:08:43+00:00" + "time": "2025-02-07T04:59:16+00:00" }, { "name": "phpunit/php-timer", - "version": "7.0.1", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -11045,7 +13709,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", "security": "https://github.com/sebastianbergmann/php-timer/security/policy", - "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" }, "funding": [ { @@ -11053,61 +13717,57 @@ "type": "github" } ], - "time": "2024-07-03T05:09:35+00:00" + "time": "2025-02-07T04:59:38+00:00" }, { "name": "phpunit/phpunit", - "version": "11.5.56", + "version": "12.5.28", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "5f83edffa6967c3db468d48a695ec7bcb02e9256" + "reference": "5895d05f5bf421ed230fbd76e1277e4b8955def4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5f83edffa6967c3db468d48a695ec7bcb02e9256", - "reference": "5f83edffa6967c3db468d48a695ec7bcb02e9256", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5895d05f5bf421ed230fbd76e1277e4b8955def4", + "reference": "5895d05f5bf421ed230fbd76e1277e4b8955def4", "shasum": "" }, "require": { "ext-dom": "*", - "ext-filter": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", + "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", - "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.12", - "phpunit/php-file-iterator": "^5.1.1", - "phpunit/php-invoker": "^5.0.1", - "phpunit/php-text-template": "^4.0.1", - "phpunit/php-timer": "^7.0.1", - "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.3", - "sebastian/comparator": "^6.3.3", - "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.1", - "sebastian/exporter": "^6.3.2", - "sebastian/global-state": "^7.0.2", - "sebastian/object-enumerator": "^6.0.1", - "sebastian/recursion-context": "^6.0.3", - "sebastian/type": "^5.1.3", - "sebastian/version": "^5.0.2", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.6", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.1", + "sebastian/comparator": "^7.1.8", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.1.2", + "sebastian/exporter": "^7.0.3", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.4", + "sebastian/version": "^6.0.0", "staabm/side-effects-detector": "^1.0.5" }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files" - }, "bin": [ "phpunit" ], "type": "library", "extra": { "branch-alias": { - "dev-main": "11.5-dev" + "dev-main": "12.5-dev" } }, "autoload": { @@ -11139,178 +13799,461 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.56" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.28" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-05-27T14:01:10+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.24", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.24" + }, + "time": "2026-06-29T15:41:09+00:00" + }, + { + "name": "rector/rector", + "version": "2.6.3", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "7e46709996a4b3dc59e1d6ecbb6a38ace335bd58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/7e46709996a4b3dc59e1d6ecbb6a38ace335bd58", + "reference": "7e46709996a4b3dc59e1d6ecbb6a38ace335bd58", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.2.6" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "homepage": "https://getrector.com/", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.6.3" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2026-08-18T22:01:18+00:00" + }, + { + "name": "samdark/yii2-psr-log-target", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/samdark/yii2-psr-log-target.git", + "reference": "5f14f21d5ee4294fe9eb3e723ec8a3908ca082ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/samdark/yii2-psr-log-target/zipball/5f14f21d5ee4294fe9eb3e723ec8a3908ca082ea", + "reference": "5f14f21d5ee4294fe9eb3e723ec8a3908ca082ea", + "shasum": "" + }, + "require": { + "psr/log": "~1.0.2|~1.1.0|~3.0.0", + "yiisoft/yii2": "~2.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4|~10.4.2" + }, + "type": "yii2-extension", + "autoload": { + "psr-4": { + "samdark\\log\\": "src", + "samdark\\log\\tests\\": "tests" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Alexander Makarov", + "email": "sam@rmcreative.ru" + } + ], + "description": "Yii 2 log target which uses PSR-3 compatible logger", + "homepage": "https://github.com/samdark/yii2-psr-log-target", + "keywords": [ + "extension", + "log", + "psr-3", + "yii" + ], + "support": { + "issues": "https://github.com/samdark/yii2-psr-log-target/issues", + "source": "https://github.com/samdark/yii2-psr-log-target" + }, + "funding": [ + { + "url": "https://github.com/samdark", + "type": "github" + }, + { + "url": "https://www.patreon.com/samdark", + "type": "patreon" + } + ], + "time": "2023-11-23T14:11:29+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1" }, "funding": [ { - "url": "https://phpunit.de/sponsoring.html", - "type": "other" + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" } ], - "time": "2026-07-06T14:52:39+00:00" + "time": "2026-05-17T05:29:34+00:00" }, { - "name": "psy/psysh", - "version": "v0.12.24", + "name": "sebastian/comparator", + "version": "7.1.8", "source": { "type": "git", - "url": "https://github.com/bobthecow/psysh.git", - "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1" + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "7c65c1e79836812819705b473a90c12399542485" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", - "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", + "reference": "7c65c1e79836812819705b473a90c12399542485", "shasum": "" }, "require": { - "ext-json": "*", - "ext-tokenizer": "*", - "nikic/php-parser": "^5.0 || ^4.0", - "php": "^8.0 || ^7.4", - "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" - }, - "conflict": { - "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0.3" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.2", - "composer/class-map-generator": "^1.6" + "phpunit/phpunit": "^12.5.25" }, "suggest": { - "composer/class-map-generator": "Improved tab completion performance with better class discovery.", - "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", - "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + "ext-bcmath": "For comparing BcMath\\Number objects" }, - "bin": [ - "bin/psysh" - ], "type": "library", "extra": { - "bamarni-bin": { - "bin-links": false, - "forward-command": false - }, "branch-alias": { - "dev-main": "0.12.x-dev" + "dev-main": "7.1-dev" } }, "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Psy\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Justin Hileman", - "email": "justin@justinhileman.info" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" } ], - "description": "An interactive shell for modern PHP.", - "homepage": "https://psysh.org", + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ - "REPL", - "console", - "interactive", - "shell" + "comparator", + "compare", + "equality" ], "support": { - "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.24" + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" }, - "time": "2026-06-29T15:41:09+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-05-21T04:45:25+00:00" }, { - "name": "rector/rector", - "version": "1.2.10", + "name": "sebastian/complexity", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/rectorphp/rector.git", - "reference": "40f9cf38c05296bd32f444121336a521a293fa61" + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/40f9cf38c05296bd32f444121336a521a293fa61", - "reference": "40f9cf38c05296bd32f444121336a521a293fa61", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", "shasum": "" }, "require": { - "php": "^7.2|^8.0", - "phpstan/phpstan": "^1.12.5" - }, - "conflict": { - "rector/rector-doctrine": "*", - "rector/rector-downgrade-php": "*", - "rector/rector-phpunit": "*", - "rector/rector-symfony": "*" + "nikic/php-parser": "^5.0", + "php": ">=8.3" }, - "suggest": { - "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + "require-dev": { + "phpunit/phpunit": "^12.0" }, - "bin": [ - "bin/rector" - ], "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, "autoload": { - "files": [ - "bootstrap.php" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "Instant Upgrade and Automated Refactoring of any PHP code", - "keywords": [ - "automation", - "dev", - "migration", - "refactoring" + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", "support": { - "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/1.2.10" + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" }, "funding": [ { - "url": "https://github.com/tomasvotruba", + "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2024-11-08T13:59:10+00:00" + "time": "2025-02-07T04:55:25+00:00" }, { - "name": "sebastian/cli-parser", - "version": "3.0.2", + "name": "sebastian/diff", + "version": "7.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.5.33", + "symfony/process": "^7.4.17" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -11325,49 +14268,73 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" } ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" } ], - "time": "2024-07-03T04:41:36+00:00" + "time": "2026-08-25T15:35:54+00:00" }, { - "name": "sebastian/code-unit", - "version": "3.0.3", + "name": "sebastian/environment", + "version": "8.1.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", - "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.5" + "phpunit/phpunit": "^12.5.26" + }, + "suggest": { + "ext-posix": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "8.1-dev" } }, "autoload": { @@ -11382,49 +14349,67 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" } ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2025-03-19T07:56:08+00:00" + "time": "2026-05-25T13:40:20+00:00" }, { - "name": "sebastian/code-unit-reverse-lookup", - "version": "4.0.1", + "name": "sebastian/exporter", + "version": "7.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", "shasum": "" }, "require": { - "php": ">=8.2" + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0.1" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -11440,54 +14425,82 @@ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-07-03T04:45:54+00:00" + "time": "2026-05-20T04:37:17+00:00" }, { - "name": "sebastian/comparator", - "version": "6.3.3", + "name": "sebastian/global-state", + "version": "8.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", - "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/diff": "^6.0", - "sebastian/exporter": "^6.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0.1" }, "require-dev": { - "phpunit/phpunit": "^11.4" - }, - "suggest": { - "ext-bcmath": "For comparing BcMath\\Number objects" + "ext-dom": "*", + "phpunit/phpunit": "^12.5.28" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.3-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -11503,31 +14516,17 @@ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" } ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ - "comparator", - "compare", - "equality" + "global state" ], "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3" }, "funding": [ { @@ -11543,32 +14542,32 @@ "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", "type": "tidelift" } ], - "time": "2026-01-24T09:26:40+00:00" + "time": "2026-06-01T15:10:33+00:00" }, { - "name": "sebastian/complexity", + "name": "sebastian/lines-of-code", "version": "4.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", "shasum": "" }, "require": { - "nikic/php-parser": "^5.0", - "php": ">=8.2" + "nikic/php-parser": "^5.7.0", + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { @@ -11592,46 +14591,59 @@ "role": "lead" } ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" } ], - "time": "2024-07-03T04:49:50+00:00" + "time": "2026-05-19T16:22:07+00:00" }, { - "name": "sebastian/diff", - "version": "6.0.2", + "name": "sebastian/object-enumerator", + "version": "7.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^11.0", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -11647,24 +14659,14 @@ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" } ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" }, "funding": [ { @@ -11672,35 +14674,32 @@ "type": "github" } ], - "time": "2024-07-03T04:53:05+00:00" + "time": "2025-02-07T04:57:48+00:00" }, { - "name": "sebastian/environment", - "version": "7.2.1", + "name": "sebastian/object-reflector", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", - "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" - }, - "suggest": { - "ext-posix": "*" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.2-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -11718,64 +14717,45 @@ "email": "sebastian@phpunit.de" } ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "https://github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", - "type": "tidelift" } ], - "time": "2025-05-21T11:55:47+00:00" + "time": "2025-02-07T04:58:17+00:00" }, { - "name": "sebastian/exporter", - "version": "6.3.2", + "name": "sebastian/recursion-context", + "version": "7.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", - "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", "shasum": "" }, "require": { - "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/recursion-context": "^6.0" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.3-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -11796,29 +14776,17 @@ "name": "Jeff Welch", "email": "whatthejeff@gmail.com" }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, { "name": "Adam Harvey", "email": "aharvey@php.net" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" } ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "https://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" }, "funding": [ { @@ -11834,39 +14802,36 @@ "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", "type": "tidelift" } ], - "time": "2025-09-24T06:12:51+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { - "name": "sebastian/global-state", - "version": "7.0.2", + "name": "sebastian/type", + "version": "6.0.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "82ff822c2edc46724be9f7411d3163021f602773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", + "reference": "82ff822c2edc46724be9f7411d3163021f602773", "shasum": "" }, "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" + "php": ">=8.3" }, "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.5.25" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -11881,52 +14846,58 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Snapshotting of global state", - "homepage": "https://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2024-07-03T04:57:36+00:00" + "time": "2026-05-20T06:45:45+00:00" }, { - "name": "sebastian/lines-of-code", - "version": "3.0.1", + "name": "sebastian/version", + "version": "6.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", "shasum": "" }, "require": { - "nikic/php-parser": "^5.0", - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" + "php": ">=8.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -11945,12 +14916,12 @@ "role": "lead" } ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" }, "funding": [ { @@ -11958,762 +14929,770 @@ "type": "github" } ], - "time": "2024-07-03T04:58:38+00:00" + "time": "2025-02-07T05:00:38+00:00" }, { - "name": "sebastian/object-enumerator", - "version": "6.0.1", + "name": "seld/cli-prompt", + "version": "1.0.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + "url": "https://github.com/Seldaek/cli-prompt.git", + "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/b8dfcf02094b8c03b40322c229493bb2884423c5", + "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5", "shasum": "" }, "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" + "php": ">=5.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpstan/phpstan": "^0.12.63" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-master": "1.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + "psr-4": { + "Seld\\CliPrompt\\": "src/" + } }, - "funding": [ + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" } ], - "time": "2024-07-03T05:00:13+00:00" + "description": "Allows you to prompt for user input on the command line, and optionally hide the characters they type", + "keywords": [ + "cli", + "console", + "hidden", + "input", + "prompt" + ], + "support": { + "issues": "https://github.com/Seldaek/cli-prompt/issues", + "source": "https://github.com/Seldaek/cli-prompt/tree/1.0.4" + }, + "time": "2020-12-15T21:32:01+00:00" }, { - "name": "sebastian/object-reflector", - "version": "4.0.1", + "name": "staabm/side-effects-detector", + "version": "1.0.5", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", "shasum": "" }, "require": { - "php": ">=8.2" + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, "autoload": { "classmap": [ - "src/" + "lib/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/staabm", "type": "github" } ], - "time": "2024-07-03T05:01:32+00:00" + "time": "2024-10-20T05:08:20+00:00" }, { - "name": "sebastian/recursion-context", - "version": "6.0.3", + "name": "symfony/polyfill-php83", + "version": "v1.41.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", - "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/5ea99087fb99c273a9b9236ed4c31e78b16103c6", + "reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6", "shasum": "" }, "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.3" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "6.0-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, "classmap": [ - "src/" + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Adam Harvey", - "email": "aharvey@php.net" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "https://github.com/sebastianbergmann/recursion-context", + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.41.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://symfony.com/sponsor", + "type": "custom" }, { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" + "url": "https://github.com/fabpot", + "type": "github" }, { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" + "url": "https://github.com/nicolas-grekas", + "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-08-13T04:42:22+00:00" + "time": "2026-07-01T12:47:55+00:00" }, { - "name": "sebastian/type", - "version": "5.1.3", + "name": "symplify/easy-coding-standard", + "version": "10.3.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + "url": "https://github.com/easy-coding-standard/ecs.git", + "reference": "c93878b3c052321231519b6540e227380f90be17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", - "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "url": "https://api.github.com/repos/easy-coding-standard/ecs/zipball/c93878b3c052321231519b6540e227380f90be17", + "reference": "c93878b3c052321231519b6540e227380f90be17", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=7.2" }, - "require-dev": { - "phpunit/phpunit": "^11.3" + "conflict": { + "friendsofphp/php-cs-fixer": "<3.0", + "squizlabs/php_codesniffer": "<3.6" }, + "bin": [ + "bin/ecs" + ], "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "10.3-dev" } }, "autoload": { - "classmap": [ - "src/" + "files": [ + "bootstrap.php" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } + "MIT" ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", + "description": "Prefixed scoped version of ECS package", "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + "issues": "https://github.com/easy-coding-standard/ecs/issues", + "source": "https://github.com/easy-coding-standard/ecs/tree/10.3.3" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" + "url": "https://www.paypal.me/rectorphp", + "type": "custom" }, { - "url": "https://tidelift.com/funding/github/packagist/sebastian/type", - "type": "tidelift" + "url": "https://github.com/tomasvotruba", + "type": "github" } ], - "time": "2025-08-09T06:55:48+00:00" + "time": "2022-06-13T14:03:37+00:00" }, { - "name": "sebastian/version", - "version": "5.0.2", + "name": "ta-tikoma/phpunit-architecture-test", + "version": "0.8.7", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/1248f3f506ca9641d4f68cebcd538fa489754db8", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8", "shasum": "" }, "require": { - "php": ">=8.2" + "nikic/php-parser": "^4.18.0 || ^5.0.0", + "php": "^8.1.0", + "phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" - } + "require-dev": { + "laravel/pint": "^1.13.7", + "phpstan/phpstan": "^1.10.52" }, + "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "PHPUnit\\Architecture\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Ni Shi", + "email": "futik0ma011@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", + "description": "Methods for testing application architecture", + "keywords": [ + "architecture", + "phpunit", + "stucture", + "test", + "testing" + ], "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.7" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-10-09T05:16:32+00:00" + "time": "2026-02-17T17:25:14+00:00" }, { - "name": "softcreatr/jsonpath", - "version": "0.10.0", + "name": "thamtech/yii2-ratelimiter-advanced", + "version": "0.5", "source": { "type": "git", - "url": "https://github.com/SoftCreatR/JSONPath.git", - "reference": "74f0b330a98135160db947ba7bc65216b64a0c86" + "url": "https://github.com/thamtech/yii2-ratelimiter-advanced.git", + "reference": "2fde10eaa1ec67e689d06babfc9c68d144d35433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/74f0b330a98135160db947ba7bc65216b64a0c86", - "reference": "74f0b330a98135160db947ba7bc65216b64a0c86", + "url": "https://api.github.com/repos/thamtech/yii2-ratelimiter-advanced/zipball/2fde10eaa1ec67e689d06babfc9c68d144d35433", + "reference": "2fde10eaa1ec67e689d06babfc9c68d144d35433", "shasum": "" }, "require": { - "ext-json": "*", - "php": "8.1 - 8.4" - }, - "replace": { - "flow/jsonpath": "*" + "php": ">=5.6.0", + "yiisoft/yii2": ">=2.0.14 <2.1" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.58", - "phpunit/phpunit": "10 - 12", - "squizlabs/php_codesniffer": "^3.10" + "codeception/codeception": "2.0.*", + "codeception/specify": "*", + "codeception/verify": "*", + "flow/jsonpath": "^0.3", + "yiisoft/yii2-codeception": "*", + "yiisoft/yii2-debug": "*", + "yiisoft/yii2-faker": "*" }, - "type": "library", + "type": "yii2-extension", "autoload": { "psr-4": { - "Flow\\JSONPath\\": "src/" + "thamtech\\ratelimiter\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Stephen Frank", - "email": "stephen@flowsa.com", - "homepage": "https://prismaticbytes.com", - "role": "Developer" - }, - { - "name": "Sascha Greuel", - "email": "hello@1-2.dev", - "homepage": "https://1-2.dev", - "role": "Developer" + "name": "Tyler Ham", + "email": "tyler@thamtech.com" } ], - "description": "JSONPath implementation for parsing, searching and flattening arrays", + "description": "An advanced request rate limiter", "support": { - "email": "hello@1-2.dev", - "forum": "https://github.com/SoftCreatR/JSONPath/discussions", - "issues": "https://github.com/SoftCreatR/JSONPath/issues", - "source": "https://github.com/SoftCreatR/JSONPath" + "issues": "https://github.com/thamtech/yii2-ratelimiter-advanced/issues", + "source": "https://github.com/thamtech/yii2-ratelimiter-advanced/tree/master" }, - "funding": [ - { - "url": "https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4", - "type": "custom" - }, - { - "url": "https://github.com/softcreatr", - "type": "github" - } - ], - "time": "2025-03-22T00:28:17+00:00" + "time": "2020-08-05T04:29:29+00:00" }, { - "name": "staabm/side-effects-detector", - "version": "1.0.5", + "name": "theseer/tokenizer", + "version": "2.0.1", "source": { "type": "git", - "url": "https://github.com/staabm/side-effects-detector.git", - "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", - "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", "shasum": "" }, "require": { - "ext-tokenizer": "*", - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^1.12.6", - "phpunit/phpunit": "^9.6.21", - "symfony/var-dumper": "^5.4.43", - "tomasvotruba/type-coverage": "1.0.0", - "tomasvotruba/unused-public": "1.0.0" + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" }, "type": "library", "autoload": { "classmap": [ - "lib/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "A static analysis tool to detect side effects in PHP code", - "keywords": [ - "static analysis" + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { - "issues": "https://github.com/staabm/side-effects-detector/issues", - "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" }, "funding": [ { - "url": "https://github.com/staabm", + "url": "https://github.com/theseer", "type": "github" } ], - "time": "2024-10-20T05:08:20+00:00" + "time": "2025-12-08T11:19:18+00:00" }, { - "name": "symfony/browser-kit", - "version": "v7.4.14", + "name": "yiisoft/yii2", + "version": "2.0.55", "source": { "type": "git", - "url": "https://github.com/symfony/browser-kit.git", - "reference": "bb28e8761a6c33975972948010f00d4a10f0a634" + "url": "https://github.com/yiisoft/yii2-framework.git", + "reference": "b900eecdb225041a4c4e0f5e0e5336f606a23bdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/bb28e8761a6c33975972948010f00d4a10f0a634", - "reference": "bb28e8761a6c33975972948010f00d4a10f0a634", + "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/b900eecdb225041a4c4e0f5e0e5336f606a23bdb", + "reference": "b900eecdb225041a4c4e0f5e0e5336f606a23bdb", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/dom-crawler": "^6.4|^7.0|^8.0" - }, - "require-dev": { - "symfony/css-selector": "^6.4|^7.0|^8.0", - "symfony/http-client": "^6.4|^7.0|^8.0", - "symfony/mime": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0" + "bower-asset/inputmask": "^5.0.8 ", + "bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", + "bower-asset/punycode": "^2.2", + "bower-asset/yii2-pjax": "~2.0.1", + "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0", + "ext-ctype": "*", + "ext-mbstring": "*", + "ezyang/htmlpurifier": "^4.17", + "lib-pcre": "*", + "php": ">=7.4.0", + "yiisoft/yii2-composer": "~2.0.4" }, + "bin": [ + "yii" + ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "psr-4": { - "Symfony\\Component\\BrowserKit\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "yii\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com", + "homepage": "https://www.yiiframework.com/", + "role": "Founder and project lead" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Alexander Makarov", + "email": "sam@rmcreative.ru", + "homepage": "https://rmcreative.ru/", + "role": "Core framework development" + }, + { + "name": "Maurizio Domba", + "homepage": "http://mdomba.info/", + "role": "Core framework development" + }, + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "https://www.cebe.cc/", + "role": "Core framework development" + }, + { + "name": "Timur Ruziev", + "email": "resurtm@gmail.com", + "homepage": "http://resurtm.com/", + "role": "Core framework development" + }, + { + "name": "Paul Klimov", + "email": "klimov.paul@gmail.com", + "role": "Core framework development" + }, + { + "name": "Dmitry Naumenko", + "email": "d.naumenko.a@gmail.com", + "role": "Core framework development" + }, + { + "name": "Boudewijn Vahrmeijer", + "email": "info@dynasource.eu", + "homepage": "http://dynasource.eu", + "role": "Core framework development" } ], - "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", - "homepage": "https://symfony.com", + "description": "Yii PHP Framework Version 2", + "homepage": "https://www.yiiframework.com/", + "keywords": [ + "framework", + "yii2" + ], "support": { - "source": "https://github.com/symfony/browser-kit/tree/v7.4.14" + "forum": "https://forum.yiiframework.com/", + "irc": "ircs://irc.libera.chat:6697/yii", + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "source": "https://github.com/yiisoft/yii2", + "wiki": "https://www.yiiframework.com/wiki" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/yiisoft", "type": "github" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2", "type": "tidelift" } ], - "time": "2026-06-08T20:24:16+00:00" + "time": "2026-05-09T14:50:57+00:00" }, { - "name": "symfony/console", - "version": "v7.4.14", + "name": "yiisoft/yii2-composer", + "version": "2.0.11", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87" + "url": "https://github.com/yiisoft/yii2-composer.git", + "reference": "b684b01ecb119c8287721def726a0e24fec2fef2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87", - "reference": "92f58bc4bf97a92ed1b9f367f0cd44f20bde0e87", + "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/b684b01ecb119c8287721def726a0e24fec2fef2", + "reference": "b684b01ecb119c8287721def726a0e24fec2fef2", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2|^8.0" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" + "composer-plugin-api": "^1.0 | ^2.0" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/lock": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "composer/composer": "^1.0 | ^2.0@dev", + "phpunit/phpunit": "<7" + }, + "type": "composer-plugin", + "extra": { + "class": "yii\\composer\\Plugin", + "branch-alias": { + "dev-master": "2.0.x-dev" + } }, - "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "yii\\composer\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Carsten Brandt", + "email": "mail@cebe.cc" } ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", + "description": "The composer plugin for Yii extension installer", "keywords": [ - "cli", - "command-line", - "console", - "terminal" + "composer", + "extension installer", + "yii2" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.14" + "forum": "https://www.yiiframework.com/forum/", + "irc": "ircs://irc.libera.chat:6697/yii", + "issues": "https://github.com/yiisoft/yii2-composer/issues", + "source": "https://github.com/yiisoft/yii2-composer", + "wiki": "https://www.yiiframework.com/wiki/" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/yiisoft", "type": "github" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-composer", "type": "tidelift" } ], - "time": "2026-06-16T11:50:14+00:00" + "time": "2025-02-13T20:59:36+00:00" }, { - "name": "symfony/finder", - "version": "v7.4.14", + "name": "yiisoft/yii2-debug", + "version": "2.1.27", "source": { "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "13b38720174286f55d1761152b575a8d1436fc25" + "url": "https://github.com/yiisoft/yii2-debug.git", + "reference": "44e158914911ef81cd7111fd6d46b918f65fae7c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/13b38720174286f55d1761152b575a8d1436fc25", - "reference": "13b38720174286f55d1761152b575a8d1436fc25", + "url": "https://api.github.com/repos/yiisoft/yii2-debug/zipball/44e158914911ef81cd7111fd6d46b918f65fae7c", + "reference": "44e158914911ef81cd7111fd6d46b918f65fae7c", "shasum": "" }, "require": { - "php": ">=8.2" + "ext-mbstring": "*", + "php": ">=5.4", + "yiisoft/yii2": "~2.0.13" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0|^8.0" + "cweagans/composer-patches": "^1.7", + "phpunit/phpunit": "4.8.34", + "yiisoft/yii2-coding-standards": "~2.0", + "yiisoft/yii2-swiftmailer": "*" + }, + "type": "yii2-extension", + "extra": { + "patches": { + "phpunit/phpunit": { + "Fix PHP 7 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php7.patch", + "Fix PHP 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php8.patch", + "Fix PHP 8.1 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php81.patch" + }, + "phpunit/phpunit-mock-objects": { + "Fix PHP 7 and 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_mock_objects.patch" + } + }, + "branch-alias": { + "dev-master": "2.0.x-dev" + }, + "composer-exit-on-patch-failure": true }, - "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "yii\\debug\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Simon Karlen", + "email": "simi.albi@outlook.com" } ], - "description": "Finds files and directories via an intuitive fluent interface", - "homepage": "https://symfony.com", + "description": "The debugger extension for the Yii framework", + "keywords": [ + "debug", + "debugger", + "dev", + "yii2" + ], "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.14" + "forum": "https://www.yiiframework.com/forum/", + "irc": "ircs://irc.libera.chat:6697/yii", + "issues": "https://github.com/yiisoft/yii2-debug/issues", + "source": "https://github.com/yiisoft/yii2-debug", + "wiki": "https://www.yiiframework.com/wiki/" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/yiisoft", "type": "github" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-debug", "type": "tidelift" } ], - "time": "2026-06-27T08:31:18+00:00" + "time": "2025-06-08T13:32:11+00:00" }, { - "name": "symplify/easy-coding-standard", - "version": "10.3.3", + "name": "yiisoft/yii2-queue", + "version": "2.3.8", "source": { "type": "git", - "url": "https://github.com/ecsphp/ecs.git", - "reference": "c93878b3c052321231519b6540e227380f90be17" + "url": "https://github.com/yiisoft/yii2-queue.git", + "reference": "e0f935e5b868d53347acfb14ec19faaf16085005" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ecsphp/ecs/zipball/c93878b3c052321231519b6540e227380f90be17", - "reference": "c93878b3c052321231519b6540e227380f90be17", + "url": "https://api.github.com/repos/yiisoft/yii2-queue/zipball/e0f935e5b868d53347acfb14ec19faaf16085005", + "reference": "e0f935e5b868d53347acfb14ec19faaf16085005", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=5.5.0", + "symfony/process": "^3.3||^4.0||^5.0||^6.0||^7.0", + "yiisoft/yii2": "~2.0.14" }, - "conflict": { - "friendsofphp/php-cs-fixer": "<3.0", - "squizlabs/php_codesniffer": "<3.6" + "require-dev": { + "aws/aws-sdk-php": ">=2.4", + "cweagans/composer-patches": "^1.7", + "enqueue/amqp-lib": "^0.8||^0.9.10||^0.10.0", + "enqueue/stomp": "^0.8.39||0.10.19", + "opis/closure": "*", + "pda/pheanstalk": "~3.2.1", + "php-amqplib/php-amqplib": "^2.8.0||^3.0.0", + "phpunit/phpunit": "4.8.34", + "yiisoft/yii2-debug": "~2.1.0", + "yiisoft/yii2-gii": "~2.2.0", + "yiisoft/yii2-redis": "2.0.19" }, - "bin": [ - "bin/ecs" - ], - "type": "library", + "suggest": { + "aws/aws-sdk-php": "Need for aws SQS.", + "enqueue/amqp-lib": "Need for AMQP interop queue.", + "enqueue/stomp": "Need for Stomp queue.", + "ext-gearman": "Need for Gearman queue.", + "ext-pcntl": "Need for process signals.", + "pda/pheanstalk": "Need for Beanstalk queue.", + "php-amqplib/php-amqplib": "Need for AMQP queue.", + "yiisoft/yii2-redis": "Need for Redis queue." + }, + "type": "yii2-extension", "extra": { + "patches": { + "phpunit/phpunit": { + "Fix PHP 7 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php7.patch", + "Fix PHP 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php8.patch" + }, + "phpunit/phpunit-mock-objects": { + "Fix PHP 7 and 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_mock_objects.patch" + } + }, "branch-alias": { - "dev-main": "10.3-dev" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Prefixed scoped version of ECS package", - "support": { - "issues": "https://github.com/easy-coding-standard/ecs/issues", - "source": "https://github.com/easy-coding-standard/ecs/tree/10.3.3" - }, - "funding": [ - { - "url": "https://www.paypal.me/rectorphp", - "type": "custom" + "dev-master": "2.x-dev" }, - { - "url": "https://github.com/tomasvotruba", - "type": "github" - } - ], - "time": "2022-06-13T14:03:37+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.3.1", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "composer-exit-on-patch-failure": true }, - "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "yii\\queue\\": "src", + "yii\\queue\\db\\": "src/drivers/db", + "yii\\queue\\sqs\\": "src/drivers/sqs", + "yii\\queue\\amqp\\": "src/drivers/amqp", + "yii\\queue\\file\\": "src/drivers/file", + "yii\\queue\\sync\\": "src/drivers/sync", + "yii\\queue\\redis\\": "src/drivers/redis", + "yii\\queue\\stomp\\": "src/drivers/stomp", + "yii\\queue\\gearman\\": "src/drivers/gearman", + "yii\\queue\\beanstalk\\": "src/drivers/beanstalk", + "yii\\queue\\amqp_interop\\": "src/drivers/amqp_interop" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -12721,68 +15700,85 @@ ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" + "name": "Roman Zhuravlev", + "email": "zhuravljov@gmail.com" } ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "description": "Yii2 Queue Extension which supports queues based on DB, Redis, RabbitMQ, Beanstalk, SQS, and Gearman", + "keywords": [ + "async", + "beanstalk", + "db", + "gearman", + "gii", + "queue", + "rabbitmq", + "redis", + "sqs", + "yii" + ], "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + "docs": "https://github.com/yiisoft/yii2-queue/blob/master/docs/guide", + "issues": "https://github.com/yiisoft/yii2-queue/issues", + "source": "https://github.com/yiisoft/yii2-queue" }, "funding": [ { - "url": "https://github.com/theseer", + "url": "https://github.com/yiisoft", "type": "github" + }, + { + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-queue", + "type": "tidelift" } ], - "time": "2025-11-17T20:03:58+00:00" + "time": "2026-01-08T07:52:05+00:00" }, { - "name": "vlucas/phpdotenv", - "version": "v5.6.4", + "name": "yiisoft/yii2-symfonymailer", + "version": "4.0.0", "source": { "type": "git", - "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b" + "url": "https://github.com/yiisoft/yii2-symfonymailer.git", + "reference": "21f407239c51fc6d50d369e4469d006afa8c9b2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/416df702837983f8d5ff48c9c3fee4f5f57b980b", - "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b", + "url": "https://api.github.com/repos/yiisoft/yii2-symfonymailer/zipball/21f407239c51fc6d50d369e4469d006afa8c9b2c", + "reference": "21f407239c51fc6d50d369e4469d006afa8c9b2c", "shasum": "" }, "require": { - "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.4", - "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.5", - "symfony/polyfill-ctype": "^1.26", - "symfony/polyfill-mbstring": "^1.26", - "symfony/polyfill-php80": "^1.26" + "php": ">=8.1", + "psr/event-dispatcher": "1.0.0", + "symfony/mailer": "^6.4 || ^7.0", + "symfony/mime": "^6.4 || ^7.0", + "yiisoft/yii2": ">=2.0.4" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "ext-filter": "*", - "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + "maglnet/composer-require-checker": "^4.7", + "phpunit/phpunit": "^10.5", + "roave/infection-static-analysis-plugin": "^1.34", + "symplify/easy-coding-standard": "^12.1", + "vimeo/psalm": "^5.20" }, "suggest": { - "ext-filter": "Required to use the boolean validator." + "yiisoft/yii2-psr-log-source": "Allows routing transport logs to your Yii2 logger" }, - "type": "library", + "type": "yii2-extension", "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - }, "branch-alias": { - "dev-master": "5.6-dev" - } + "dev-master": "3.0.x-dev" + }, + "sort-packages": true }, "autoload": { "psr-4": { - "Dotenv\\": "src/" + "yii\\symfonymailer\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -12791,50 +15787,54 @@ ], "authors": [ { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Vance Lucas", - "email": "vance@vancelucas.com", - "homepage": "https://github.com/vlucas" + "name": "Kirill Petrov", + "email": "archibeardrinker@gmail.com" } ], - "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "description": "The SymfonyMailer integration for the Yii framework", "keywords": [ - "dotenv", - "env", - "environment" + "email", + "mail", + "mailer", + "symfony", + "symfonymailer", + "yii2" ], "support": { - "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.4" + "forum": "http://www.yiiframework.com/forum/", + "irc": "irc://irc.freenode.net/yii", + "issues": "https://github.com/yiisoft/yii2-symfonymailer/issues", + "source": "https://github.com/yiisoft/yii2-symfonymailer", + "wiki": "http://www.yiiframework.com/wiki/" }, "funding": [ { - "url": "https://github.com/GrahamCampbell", + "url": "https://github.com/yiisoft", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-symfonymailer", "type": "tidelift" } ], - "time": "2026-07-06T19:11:50+00:00" + "time": "2024-01-29T14:13:45+00:00" } ], "aliases": [], "minimum-stability": "dev", "stability-flags": { + "craftcms/cms": 20, "craftcms/ecs": 20, - "craftcms/phpstan": 20, - "craftcms/rector": 20 + "craftcms/yii2-adapter": 20 }, "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.2" + "php": "^8.5" }, "platform-dev": {}, "plugin-api-version": "2.9.0" diff --git a/crowdin.yml b/crowdin.yml index 33f84c38e0..442f8df6cd 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,6 +1,6 @@ project_identifier: craft-commerce api_key_env: CROWDIN_API_KEY -base_path: /src/translations +base_path: /lang preserve_hierarchy: true files: - source: /en/commerce.php diff --git a/database/migrations/Install.php b/database/migrations/Install.php new file mode 100644 index 0000000000..64842cf560 --- /dev/null +++ b/database/migrations/Install.php @@ -0,0 +1,1425 @@ +silent) { + $callback(); + + return; + } + + PromptTask::run( + label: str($label)->finish('...')->toString(), + callback: function (Logger $logger) use ($callback, $label) { + $callback($logger); + $logger->label($label); + }, + keepSummary: true, + output: $this->output, + ); + } + + public function up(): void + { + $this->task('Install Craft Commerce', function (?Logger $logger = null) { + $logger?->subLabel('Creating tables...'); + $this->createTables(); + $logger?->success('Tables created.'); + + $logger?->subLabel('Creating indexes...'); + $this->createIndexes(); + $logger?->success('Indexes created.'); + + $logger?->subLabel('Adding foreign keys...'); + $this->addForeignKeys(); + $logger?->success('Foreign keys added.'); + }); + + $this->task('Seed default Craft Commerce data', function (?Logger $logger = null) { + $this->insertDefaultData(); + $logger?->success('Default data seeded.'); + }); + } + + /** + * Creates the tables for Craft Commerce. + */ + public function createTables(): void + { + Schema::create(Table::CATALOG_PRICING_RULES, function (Blueprint $table) { + $table->integer('id', true); + $table->string('name'); + $table->text('description')->nullable(); + $table->integer('storeId'); + $table->dateTime('dateFrom')->nullable(); + $table->dateTime('dateTo')->nullable(); + $table->enum('apply', ['toPercent', 'toFlat', 'byPercent', 'byFlat']); + $table->decimal('applyAmount', 14, 4); + $table->enum('applyPriceType', [CatalogPricingRule::APPLY_PRICE_TYPE_PRICE, CatalogPricingRule::APPLY_PRICE_TYPE_PROMOTIONAL_PRICE]); + $table->text('productCondition')->nullable(); + $table->text('variantCondition')->nullable(); + $table->text('purchasableCondition')->nullable(); + $table->text('customerCondition')->nullable(); + $table->boolean('enabled')->default(true); + $table->boolean('isPromotionalPrice')->default(false); + $table->text('metadata')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::CATALOG_PRICING_RULES_USERS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('catalogPricingRuleId'); + $table->integer('userId'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::CATALOG_PRICING, function (Blueprint $table) { + $table->integer('id', true); + $table->decimal('price', 14, 4)->nullable(); // @TODO Consider storing as string to avoid float-precision issues + $table->integer('purchasableId'); + $table->integer('storeId')->nullable(); + $table->integer('catalogPricingRuleId')->nullable(); + $table->integer('userId')->nullable(); + $table->dateTime('dateFrom')->nullable(); + $table->dateTime('dateTo')->nullable(); + $table->boolean('isPromotionalPrice')->default(false)->nullable(); + $table->boolean('hasUpdatePending')->default(false)->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::CATALOG_PRICING_QUEUE, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('storeId')->nullable(); + $table->enum('type', [CatalogPricingQueue::TYPE_PURCHASABLE, CatalogPricingQueue::TYPE_RULE]); + $table->mediumText('ids')->nullable(); + $table->boolean('reserved')->default(false); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::CUSTOMERS, function (Blueprint $table) { + $table->integer('id', true); // Not used in v4 but is the old customerId + $table->integer('customerId'); // This is the User element ID + $table->integer('primaryBillingAddressId')->nullable(); + $table->integer('primaryShippingAddressId')->nullable(); + $table->integer('primaryPaymentSourceId')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::COUPONS, function (Blueprint $table) { + $table->integer('id', true); + $table->string('code')->nullable(); + $table->integer('discountId'); + $table->integer('uses')->default(0); + $table->integer('maxUses')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::CUSTOMER_DISCOUNTUSES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('discountId'); + $table->integer('customerId'); + $table->unsignedInteger('uses'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::EMAIL_DISCOUNTUSES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('discountId'); + $table->string('email'); + $table->unsignedInteger('uses'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::DISCOUNT_PURCHASABLES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('discountId'); + $table->integer('purchasableId'); + $table->string('purchasableType'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + // @TODO Rename to `discount_entries` table in Commerce 6.0, or remove if the purchasable condition builder fully replaces it + Schema::create(Table::DISCOUNT_CATEGORIES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('discountId'); + $table->integer('categoryId'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::DISCOUNTS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('storeId'); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('couponFormat', 20)->default(Coupons::DEFAULT_COUPON_FORMAT); + $table->text('orderCondition')->nullable(); + $table->text('customerCondition')->nullable(); + $table->text('shippingAddressCondition')->nullable(); + $table->text('billingAddressCondition')->nullable(); + $table->boolean('requireCouponCode')->default(false); + $table->unsignedInteger('perUserLimit')->default(0); + $table->unsignedInteger('perEmailLimit')->default(0); + $table->unsignedInteger('totalDiscountUses')->default(0); + $table->unsignedInteger('totalDiscountUseLimit')->default(0); + $table->dateTime('dateFrom')->nullable(); + $table->dateTime('dateTo')->nullable(); + $table->integer('purchaseQty')->default(0); + $table->decimal('purchaseTotal', 14, 4)->default(0); + $table->integer('maxPurchaseQty')->default(0); + $table->decimal('baseDiscount', 14, 4)->default(0); + $table->decimal('perItemDiscount', 14, 4)->default(0); + $table->decimal('percentDiscount', 14, 4)->default(0); + $table->enum('percentageOffSubject', ['original', 'discounted']); + $table->boolean('excludeOnPromotion')->default(false); + $table->boolean('hasFreeShippingForMatchingItems')->default(false); + $table->boolean('hasFreeShippingForOrder')->default(false); + $table->boolean('allPurchasables')->default(false); + $table->text('purchasableIds')->nullable(); + $table->boolean('allCategories')->default(false); + $table->text('categoryIds')->nullable(); + $table->enum('appliedTo', ['matchingLineItems', 'allLineItems'])->default('matchingLineItems'); + $table->enum('categoryRelationshipType', ['element', 'sourceElement', 'targetElement'])->default('element'); + $table->text('orderConditionFormula')->nullable(); + $table->boolean('enabled')->default(true); + $table->boolean('stopProcessing')->default(false); + $table->boolean('ignorePromotions')->default(false); + $table->integer('sortOrder')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::DONATIONS, function (Blueprint $table) { + $table->integer('id', true); + $table->string('sku'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::EMAILS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('storeId')->nullable(); + $table->string('name'); + $table->string('senderAddress')->nullable(); + $table->string('senderName')->nullable(); + $table->string('subject'); + $table->enum('recipientType', ['customer', 'custom'])->default('custom')->nullable(); + $table->string('to')->nullable(); + $table->string('bcc')->nullable(); + $table->string('cc')->nullable(); + $table->string('replyTo')->nullable(); + $table->boolean('enabled')->default(true); + $table->string('templatePath'); + $table->string('plainTextTemplatePath')->nullable(); + $table->integer('pdfId')->nullable(); + $table->string('language')->nullable(); + $table->integer('renderSiteId')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::PDFS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('storeId')->nullable(); + $table->string('name'); + $table->string('handle'); + $table->string('description')->nullable(); + $table->string('templatePath'); + $table->string('fileNameFormat')->nullable(); + $table->string('paperOrientation')->default('portrait')->nullable(); + $table->string('paperSize')->default('letter')->nullable(); + $table->boolean('enabled')->default(true); + $table->boolean('isDefault')->default(false); + $table->integer('sortOrder')->nullable(); + $table->string('language')->nullable(); + $table->integer('linkExpiry')->default(86400); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::GATEWAYS, function (Blueprint $table) { + $table->integer('id', true); + $table->string('type'); + $table->string('name'); + $table->string('handle'); + $table->text('settings')->nullable(); + $table->enum('paymentType', ['authorize', 'purchase'])->default('purchase'); + $table->string('isFrontendEnabled', 500)->default('1'); + $table->text('orderCondition')->nullable(); + $table->text('shippingAddressCondition')->nullable(); + $table->text('billingAddressCondition')->nullable(); + $table->boolean('isArchived')->default(false); + $table->dateTime('dateArchived')->nullable(); + $table->integer('sortOrder')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::INVENTORYITEMS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('purchasableId'); + $table->string('countryCodeOfOrigin')->nullable(); + $table->string('administrativeAreaCodeOfOrigin')->nullable(); + $table->string('harmonizedSystemCode')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::INVENTORYLOCATIONS, function (Blueprint $table) { + $table->integer('id', true); + $table->string('handle'); + $table->string('name'); + $table->integer('addressId')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->dateTime('dateDeleted')->nullable(); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::INVENTORYLOCATIONS_STORES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('inventoryLocationId'); + $table->integer('storeId'); + $table->integer('sortOrder')->nullable(); // per store + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::INVENTORYTRANSACTIONS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('inventoryLocationId'); + $table->integer('inventoryItemId'); + $table->string('movementHash'); + $table->integer('quantity'); + $table->enum('type', [ + 'incoming', + 'available', + 'committed', + 'reserved', + 'damaged', + 'safety', + 'fulfilled', + 'qualityControl', + ]); + $table->string('note')->nullable(); + $table->integer('transferId')->nullable(); // Can be null + $table->integer('lineItemId')->nullable(); // Can be null + $table->integer('userId')->nullable(); // Can be null + $table->dateTime('dateCreated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::LINEITEMS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('orderId'); + $table->enum('type', ['purchasable', 'custom'])->default('purchasable'); + $table->integer('purchasableId')->nullable(); + $table->integer('taxCategoryId'); + $table->integer('shippingCategoryId'); + $table->text('description')->nullable(); + $table->text('options')->nullable(); + $table->string('optionsSignature'); + $table->decimal('price', 14, 4)->unsigned(); + $table->decimal('promotionalPrice', 14, 4)->unsigned()->nullable(); + $table->decimal('promotionalAmount', 14, 4)->default(0); + $table->decimal('salePrice', 14, 4)->default(0); + $table->string('sku')->nullable(); + $table->decimal('weight', 14, 4)->default(0)->unsigned(); + $table->decimal('height', 14, 4)->default(0)->unsigned(); + $table->decimal('length', 14, 4)->default(0)->unsigned(); + $table->decimal('width', 14, 4)->default(0)->unsigned(); + $table->decimal('subtotal', 14, 4)->default(0)->unsigned(); + $table->decimal('total', 14, 4)->default(0); + $table->unsignedInteger('qty'); + $table->text('note')->nullable(); + $table->text('privateNote')->nullable(); + $table->boolean('hasFreeShipping')->nullable(); + $table->boolean('isPromotable')->nullable(); + $table->boolean('isShippable')->nullable(); + $table->boolean('isTaxable')->nullable(); + $table->longText('snapshot')->nullable(); + $table->integer('lineItemStatusId')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::LINEITEMSTATUSES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('storeId')->nullable(); + $table->string('name'); + $table->string('handle'); + $table->enum('color', ['green', 'orange', 'red', 'blue', 'yellow', 'pink', 'purple', 'turquoise', 'light', 'grey', 'black'])->default('green'); + $table->boolean('isArchived')->default(false); + $table->dateTime('dateArchived')->nullable(); + $table->integer('sortOrder')->nullable(); + $table->boolean('default')->default(false); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::ORDERADJUSTMENTS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('orderId'); + $table->integer('lineItemId')->nullable(); + $table->string('type'); + $table->string('name')->nullable(); + $table->string('description')->nullable(); + $table->decimal('amount', 14, 4); + $table->boolean('included')->default(false); + $table->boolean('isEstimated')->default(false); + $table->longText('sourceSnapshot')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::ORDERNOTICES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('orderId'); + $table->string('type')->nullable(); + $table->string('attribute')->nullable(); + $table->text('message')->nullable(); + $table->string('noticeType')->default('customer'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::ORDERHISTORIES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('orderId'); + $table->integer('userId')->nullable(); + $table->string('userName')->nullable(); + $table->integer('prevStatusId')->nullable(); + $table->integer('newStatusId')->nullable(); + $table->text('message')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::ORDERS, function (Blueprint $table) { + $table->integer('id'); + $table->integer('storeId'); + $table->integer('billingAddressId')->nullable(); + $table->integer('shippingAddressId')->nullable(); + $table->integer('estimatedBillingAddressId')->nullable(); + $table->integer('estimatedShippingAddressId')->nullable(); + $table->integer('sourceShippingAddressId')->nullable(); + $table->integer('sourceBillingAddressId')->nullable(); + $table->integer('gatewayId')->nullable(); + $table->integer('paymentSourceId')->nullable(); + $table->integer('customerId')->nullable(); // Customer ID is a User element ID + $table->boolean('customerDeleted')->default(false); + $table->integer('orderStatusId')->nullable(); + $table->string('number', 32)->nullable(); + $table->string('reference')->nullable(); + $table->string('couponCode')->nullable(); + $table->decimal('itemTotal', 14, 4)->default(0)->nullable(); + $table->decimal('itemSubtotal', 14, 4)->default(0)->nullable(); + $table->unsignedInteger('totalQty')->nullable(); + $table->decimal('totalWeight', 14, 4)->default(0)->unsigned()->nullable(); + $table->decimal('total', 14, 4)->default(0)->nullable(); + $table->decimal('totalPrice', 14, 4)->default(0)->nullable(); + $table->decimal('totalPaid', 14, 4)->default(0)->nullable(); + $table->decimal('totalDiscount', 14, 4)->default(0)->nullable(); + $table->decimal('totalTax', 14, 4)->default(0)->nullable(); + $table->decimal('totalTaxIncluded', 14, 4)->default(0)->nullable(); + $table->decimal('totalShippingCost', 14, 4)->default(0)->nullable(); + $table->enum('paidStatus', ['paid', 'partial', 'unpaid', 'overPaid'])->nullable(); + $table->string('email')->nullable(); + $table->string('orderCompletedEmail')->nullable(); + $table->boolean('isCompleted')->default(false); + $table->dateTime('dateOrdered')->nullable(); + $table->dateTime('datePaid')->nullable(); + $table->dateTime('dateFirstPaid')->nullable(); + $table->dateTime('dateAuthorized')->nullable(); + $table->string('currency')->nullable(); + $table->string('paymentCurrency')->nullable(); + $table->string('lastIp')->nullable(); + $table->string('orderLanguage', 12); + $table->enum('origin', ['web', 'cp', 'remote'])->default('web'); + $table->text('message')->nullable(); + $table->boolean('registerUserOnOrderComplete')->default(false); + $table->boolean('saveBillingAddressOnOrderComplete')->default(false); + $table->boolean('makePrimaryBillingAddress')->default(false); + $table->boolean('saveShippingAddressOnOrderComplete')->default(false); + $table->boolean('makePrimaryShippingAddress')->default(false); + $table->enum('recalculationMode', ['all', 'none', 'adjustmentsOnly'])->default('all'); + $table->text('returnUrl')->nullable(); + $table->text('cancelUrl')->nullable(); + $table->string('shippingMethodHandle')->default(''); + $table->string('shippingMethodName')->default(''); + $table->integer('orderSiteId')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + $table->primary('id'); + }); + + Schema::create(Table::ORDERSTATUS_EMAILS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('orderStatusId'); + $table->integer('emailId'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::ORDERSTATUSES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('storeId')->nullable(); + $table->string('name'); + $table->string('handle'); + $table->enum('color', ['green', 'orange', 'red', 'blue', 'yellow', 'pink', 'purple', 'turquoise', 'light', 'grey', 'black'])->default('green'); + $table->string('description')->nullable(); + $table->dateTime('dateDeleted')->nullable(); + $table->integer('sortOrder')->nullable(); + $table->boolean('default')->default(false); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::PAYMENTCURRENCIES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('storeId'); + $table->string('iso', 3); + $table->boolean('primary')->default(false); + $table->decimal('rate', 14, 4)->default(0); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::PAYMENTSOURCES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('customerId'); + $table->integer('gatewayId'); + $table->string('token'); + $table->string('description')->nullable(); + $table->text('response')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::PLANS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('gatewayId')->nullable(); + $table->integer('planInformationId')->nullable(); + $table->string('name'); + $table->string('handle'); + $table->string('reference'); + $table->boolean('enabled')->default(false); + $table->text('planData')->nullable(); + $table->boolean('isArchived')->default(false); + $table->dateTime('dateArchived')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->integer('sortOrder')->nullable(); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::PRODUCTS, function (Blueprint $table) { + $table->integer('id'); + $table->integer('typeId')->nullable(); + $table->integer('defaultVariantId')->nullable(); + $table->dateTime('postDate')->nullable(); + $table->dateTime('expiryDate')->nullable(); + $table->string('defaultSku')->nullable(); + $table->decimal('defaultPrice', 14, 4)->nullable(); + $table->decimal('defaultHeight', 14, 4)->nullable(); + $table->decimal('defaultLength', 14, 4)->nullable(); + $table->decimal('defaultWidth', 14, 4)->nullable(); + $table->decimal('defaultWeight', 14, 4)->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + $table->primary('id'); + }); + + Schema::create(Table::PRODUCTTYPES, function (Blueprint $table) { + $table->integer('id', true); + $table->boolean('isStructure')->default(false); + $table->unsignedSmallInteger('maxLevels')->nullable(); + $table->enum('defaultPlacement', [ProductType::DEFAULT_PLACEMENT_BEGINNING, ProductType::DEFAULT_PLACEMENT_END])->default('end'); + $table->integer('structureId')->nullable(); + $table->integer('fieldLayoutId')->nullable(); + $table->integer('variantFieldLayoutId')->nullable(); + $table->string('name'); + $table->string('handle'); + $table->boolean('enableVersioning')->default(false); + $table->integer('maxVariants')->nullable(); + $table->boolean('hasDimensions')->default(false); + + // Variant title stuff + $table->boolean('hasVariantTitleField')->default(true); + $table->string('variantTitleFormat'); + $table->string('variantTitleTranslationMethod')->default('site'); + $table->string('variantTitleTranslationKeyFormat')->nullable(); + $table->string('variantUiLabelFormat')->default('{title}'); + + // Product title stuff + $table->boolean('hasProductTitleField')->default(true); + $table->string('productTitleFormat')->nullable(); + $table->string('productTitleTranslationMethod')->default('site'); + $table->string('productTitleTranslationKeyFormat')->nullable(); + $table->string('productUiLabelFormat')->default('{title}'); + + // Slug stuff + $table->boolean('showSlugField')->default(true); + $table->string('slugTranslationMethod')->default('site'); + $table->string('slugTranslationKeyFormat')->nullable(); + + $table->string('propagationMethod')->default(PropagationMethod::All->value); + $table->json('previewTargets')->nullable(); + + $table->string('skuFormat')->nullable(); + $table->string('descriptionFormat')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::PRODUCTTYPES_SITES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('productTypeId'); + $table->integer('siteId'); + $table->text('uriFormat')->nullable(); + $table->string('template', 500)->nullable(); + $table->boolean('hasUrls')->default(false); + $table->boolean('enabledByDefault')->default(true); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::PRODUCTTYPES_SHIPPINGCATEGORIES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('productTypeId'); + $table->integer('shippingCategoryId'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::PRODUCTTYPES_TAXCATEGORIES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('productTypeId'); + $table->integer('taxCategoryId'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::PURCHASABLES, function (Blueprint $table) { + $table->integer('id', true); + $table->string('sku'); + $table->text('description')->nullable(); + $table->decimal('width', 14, 4)->nullable(); + $table->decimal('height', 14, 4)->nullable(); + $table->decimal('length', 14, 4)->nullable(); + $table->decimal('weight', 14, 4)->nullable(); + $table->integer('taxCategoryId'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::PURCHASABLES_STORES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('purchasableId'); + $table->integer('storeId'); + $table->decimal('basePrice', 14, 4)->nullable(); // @TODO Consider storing as string to avoid float-precision issues + $table->decimal('basePromotionalPrice', 14, 4)->nullable(); // @TODO Consider storing as string to avoid float-precision issues + $table->boolean('promotable')->default(false); + $table->boolean('availableForPurchase')->default(true); + $table->boolean('freeShipping')->default(true); + $table->boolean('inventoryTracked')->default(true); + $table->boolean('allowOutOfStockPurchases')->default(false); + $table->integer('stock')->nullable(); // This is a summary value used for searching and sorting + $table->boolean('tracked')->default(false); + $table->integer('minQty')->nullable(); + $table->integer('maxQty')->nullable(); + $table->integer('shippingCategoryId')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::SALE_PURCHASABLES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('saleId'); + $table->integer('purchasableId'); + $table->string('purchasableType'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + // @TODO Rename to `sale_entries` table in Commerce 6.0, or remove if the purchasable condition builder fully replaces it + Schema::create(Table::SALE_CATEGORIES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('saleId'); + $table->integer('categoryId'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::SALE_USERGROUPS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('saleId'); + $table->integer('userGroupId'); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::SALES, function (Blueprint $table) { + $table->integer('id', true); + $table->string('name'); + $table->text('description')->nullable(); + $table->dateTime('dateFrom')->nullable(); + $table->dateTime('dateTo')->nullable(); + $table->enum('apply', ['toPercent', 'toFlat', 'byPercent', 'byFlat']); + $table->decimal('applyAmount', 14, 4); + $table->boolean('allGroups')->default(false); + $table->boolean('allPurchasables')->default(false); + $table->boolean('allCategories')->default(false); + $table->enum('categoryRelationshipType', ['element', 'sourceElement', 'targetElement'])->default('element'); + $table->boolean('enabled')->default(true); + $table->boolean('ignorePrevious')->default(false); + $table->boolean('stopProcessing')->default(false); + $table->integer('sortOrder')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::SHIPPINGCATEGORIES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('storeId'); + $table->string('name'); + $table->string('handle'); + $table->string('icon')->nullable(); + $table->string('color')->nullable(); + $table->string('description')->nullable(); + $table->boolean('default')->default(false); + $table->dateTime('dateDeleted')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::SHIPPINGMETHODS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('storeId'); + $table->string('name'); + $table->string('handle'); + $table->string('icon')->nullable(); + $table->string('color')->nullable(); + $table->text('orderCondition')->nullable(); + $table->text('customerCondition')->nullable(); + $table->boolean('enabled')->default(true); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::SHIPPINGRULE_CATEGORIES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('shippingRuleId')->nullable(); + $table->integer('shippingCategoryId')->nullable(); + $table->enum('condition', ['allow', 'disallow', 'require']); + $table->decimal('perItemRate', 14, 4)->nullable(); + $table->decimal('weightRate', 14, 4)->nullable(); + $table->decimal('percentageRate', 14, 4)->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::SHIPPINGRULES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('methodId'); + $table->string('name'); + $table->string('description')->nullable(); + $table->integer('priority')->default(0); + $table->boolean('enabled')->default(true); + $table->text('orderConditionFormula')->nullable(); + $table->text('orderCondition')->nullable(); + $table->text('customerCondition')->nullable(); + $table->decimal('baseRate', 14, 4)->default(0); + $table->decimal('perItemRate', 14, 4)->default(0); + $table->decimal('weightRate', 14, 4)->default(0); + $table->decimal('percentageRate', 14, 4)->default(0); + $table->decimal('minRate', 14, 4)->default(0); + $table->decimal('maxRate', 14, 4)->default(0); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::SHIPPINGZONES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('storeId')->nullable(); + $table->string('name'); + $table->string('description')->nullable(); + $table->text('condition')->nullable(); + $table->boolean('default')->default(false); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::SITESTORES, function (Blueprint $table) { + $table->integer('siteId'); + $table->integer('storeId')->nullable(); // defaults to primary store in app + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + $table->primary('siteId'); + }); + + Schema::create(Table::STORES, function (Blueprint $table) { + $table->integer('id', true); + $table->string('name'); + $table->string('handle'); + $table->boolean('primary'); + $table->string('currency')->default('USD'); + $table->string('autoSetCartShippingMethodOption')->default('false'); + $table->string('autoSetNewCartAddresses')->default('false'); + $table->string('autoSetPaymentSource')->default('false'); + $table->string('allowEmptyCartOnCheckout')->default('false'); + $table->string('allowCheckoutWithoutPayment')->default('false'); + $table->string('allowPartialPaymentOnCheckout')->default('false'); + $table->string('requireShippingAddressAtCheckout')->default('false'); + $table->string('requireBillingAddressAtCheckout')->default('false'); + $table->string('requireShippingMethodSelectionAtCheckout')->default('false'); + $table->string('useBillingAddressForTax')->default('false'); + $table->string('validateOrganizationTaxIdAsVatId')->default('false'); + $table->string('orderReferenceFormat')->nullable(); + $table->string('freeOrderPaymentStrategy')->default('complete')->nullable(); + $table->string('minimumTotalPriceStrategy')->default('default')->nullable(); + $table->integer('sortOrder')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::STORESETTINGS, function (Blueprint $table) { + $table->integer('id'); + $table->integer('locationAddressId')->nullable(); + $table->text('countries')->nullable(); + $table->text('marketAddressCondition')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + $table->primary('id'); + }); + + Schema::create(Table::SUBSCRIPTIONS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('userId'); + $table->integer('planId')->nullable(); + $table->integer('gatewayId')->nullable(); + $table->integer('orderId')->nullable(); + $table->string('reference'); + $table->text('subscriptionData')->nullable(); + $table->integer('trialDays'); + $table->dateTime('nextPaymentDate')->nullable(); + $table->boolean('hasStarted')->default(true); + $table->boolean('isSuspended')->default(false); + $table->dateTime('dateSuspended')->nullable(); + $table->boolean('isCanceled')->default(false); + $table->dateTime('dateCanceled')->nullable(); + $table->boolean('isExpired')->default(false); + $table->text('returnUrl')->nullable(); + $table->dateTime('dateExpired')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::TAXCATEGORIES, function (Blueprint $table) { + $table->integer('id', true); + $table->string('name'); + $table->string('handle'); + $table->string('icon')->nullable(); + $table->string('color')->nullable(); + $table->string('description')->nullable(); + $table->boolean('default')->default(false); + $table->dateTime('dateDeleted')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::TAXRATES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('storeId'); + $table->integer('taxZoneId')->nullable(); + $table->boolean('isEverywhere')->default(true); + $table->integer('taxCategoryId')->nullable(); + $table->string('name'); + $table->string('code')->nullable(); + $table->decimal('rate', 14, 10); + $table->boolean('include')->default(false); + $table->boolean('isVat')->default(false); // @TODO Remove in Commerce 6.0 + $table->text('taxIdValidators')->nullable(); + $table->boolean('removeIncluded')->default(false); + $table->boolean('removeVatIncluded')->default(false); + $table->enum('taxable', ['purchasable', 'price', 'shipping', 'price_shipping', 'order_total_shipping', 'order_total_price']); + $table->boolean('enabled')->default(true); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::TAXZONES, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('storeId'); + $table->string('name'); + $table->string('description')->nullable(); + $table->text('condition')->nullable(); + $table->boolean('default')->default(false); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::TRANSACTIONS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('orderId'); + $table->integer('parentId')->nullable(); + $table->integer('gatewayId')->nullable(); + $table->integer('userId')->nullable(); // Stays as userId since it could be a logged-in user or store administrator. So not just a customer. + $table->string('hash', 32)->nullable(); + $table->enum('type', ['authorize', 'capture', 'purchase', 'refund']); + $table->decimal('amount', 14, 4)->nullable(); + $table->decimal('paymentAmount', 14, 4)->nullable(); + $table->string('currency')->nullable(); + $table->string('paymentCurrency')->nullable(); + $table->decimal('paymentRate', 14, 4)->nullable(); + $table->enum('status', ['pending', 'redirect', 'success', 'failed', 'processing']); + $table->string('reference')->nullable(); + $table->string('code')->nullable(); + $table->text('message')->nullable(); + $table->mediumText('note')->nullable(); + $table->text('response')->nullable(); + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::TRANSFERS, function (Blueprint $table) { + $table->integer('id', true); + $table->enum('transferStatus', [ + 'draft', + 'pending', + 'partial', + 'received', + ]); + $table->integer('originLocationId')->nullable(); + $table->integer('destinationLocationId')->nullable(); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::TRANSFERDETAILS, function (Blueprint $table) { + $table->integer('id', true); + $table->integer('transferId'); + $table->integer('inventoryItemId')->nullable(); + $table->string('inventoryItemDescription'); + $table->integer('quantity'); + $table->integer('quantityAccepted'); + $table->integer('quantityRejected'); + $table->char('uid', 36)->default('0'); + }); + + Schema::create(Table::VARIANTS, function (Blueprint $table) { + $table->integer('id'); + $table->integer('primaryOwnerId')->nullable(); + $table->boolean('isDefault')->default(false); + $table->boolean('deletedWithProduct')->default(false); // @TODO Remove in Commerce 6.0 + $table->dateTime('dateCreated'); + $table->dateTime('dateUpdated'); + $table->char('uid', 36)->default('0'); + $table->primary('id'); + }); + } + + /** + * Creates the indexes. + */ + public function createIndexes(): void + { + Schema::createIndex(Table::CATALOG_PRICING, ['catalogPricingRuleId']); + Schema::createIndex(Table::CATALOG_PRICING, ['isPromotionalPrice']); + Schema::createIndex(Table::CATALOG_PRICING, ['purchasableId']); + Schema::createIndex(Table::CATALOG_PRICING, ['storeId']); + Schema::createIndex(Table::CATALOG_PRICING, ['userId']); + Schema::createIndex(Table::CATALOG_PRICING, ['purchasableId', 'storeId', 'isPromotionalPrice', 'price', 'catalogPricingRuleId', 'dateFrom', 'dateTo']); + Schema::createIndex(Table::CATALOG_PRICING, ['purchasableId', 'storeId', 'isPromotionalPrice', 'price']); + Schema::createIndex(Table::CATALOG_PRICING, ['purchasableId', 'storeId']); + Schema::createIndex(Table::CATALOG_PRICING_QUEUE, ['reserved']); + Schema::createIndex(Table::CATALOG_PRICING_QUEUE, ['storeId', 'type', 'reserved']); + Schema::createIndex(Table::CATALOG_PRICING_RULES, ['storeId']); + Schema::createIndex(Table::CATALOG_PRICING_RULES_USERS, ['catalogPricingRuleId']); + Schema::createIndex(Table::CATALOG_PRICING_RULES_USERS, ['userId']); + Schema::createIndex(Table::COUPONS, ['code']); + Schema::createIndex(Table::COUPONS, ['discountId']); + Schema::createIndex(Table::CUSTOMERS, ['customerId'], unique: true); + Schema::createIndex(Table::CUSTOMERS, ['primaryBillingAddressId']); + Schema::createIndex(Table::CUSTOMERS, ['primaryPaymentSourceId']); + Schema::createIndex(Table::CUSTOMERS, ['primaryShippingAddressId']); + Schema::createIndex(Table::CUSTOMER_DISCOUNTUSES, ['discountId']); + Schema::createIndex(Table::CUSTOMER_DISCOUNTUSES, ['customerId', 'discountId'], unique: true); + Schema::createIndex(Table::DISCOUNTS, ['dateFrom']); + Schema::createIndex(Table::DISCOUNTS, ['dateTo']); + Schema::createIndex(Table::DISCOUNT_CATEGORIES, ['categoryId']); + Schema::createIndex(Table::DISCOUNT_CATEGORIES, ['discountId', 'categoryId'], unique: true); + Schema::createIndex(Table::DISCOUNT_PURCHASABLES, ['purchasableId']); + Schema::createIndex(Table::DISCOUNT_PURCHASABLES, ['discountId', 'purchasableId'], unique: true); + Schema::createIndex(Table::EMAILS, ['storeId']); + Schema::createIndex(Table::EMAIL_DISCOUNTUSES, ['discountId']); + Schema::createIndex(Table::EMAIL_DISCOUNTUSES, ['email', 'discountId'], unique: true); + Schema::createIndex(Table::GATEWAYS, ['handle']); + Schema::createIndex(Table::GATEWAYS, ['isArchived']); + Schema::createIndex(Table::INVENTORYITEMS, ['purchasableId'], unique: true); + Schema::createIndex(Table::INVENTORYTRANSACTIONS, ['inventoryItemId']); + Schema::createIndex(Table::INVENTORYTRANSACTIONS, ['lineItemId']); + Schema::createIndex(Table::INVENTORYTRANSACTIONS, ['transferId']); + Schema::createIndex(Table::INVENTORYTRANSACTIONS, ['userId']); + Schema::createIndex(Table::LINEITEMS, ['purchasableId']); + Schema::createIndex(Table::LINEITEMS, ['shippingCategoryId']); + Schema::createIndex(Table::LINEITEMS, ['taxCategoryId']); + Schema::createIndex(Table::LINEITEMS, ['orderId', 'purchasableId', 'optionsSignature'], unique: true); + Schema::createIndex(Table::LINEITEMSTATUSES, ['storeId']); + Schema::createIndex(Table::ORDERADJUSTMENTS, ['orderId']); + Schema::createIndex(Table::ORDERHISTORIES, ['newStatusId']); + Schema::createIndex(Table::ORDERHISTORIES, ['orderId']); + Schema::createIndex(Table::ORDERHISTORIES, ['prevStatusId']); + Schema::createIndex(Table::ORDERHISTORIES, ['userId']); + Schema::createIndex(Table::ORDERNOTICES, ['orderId']); + Schema::createIndex(Table::ORDERS, ['billingAddressId']); + Schema::createIndex(Table::ORDERS, ['customerId']); + Schema::createIndex(Table::ORDERS, ['email']); + Schema::createIndex(Table::ORDERS, ['estimatedBillingAddressId']); + Schema::createIndex(Table::ORDERS, ['estimatedShippingAddressId']); + Schema::createIndex(Table::ORDERS, ['gatewayId']); + Schema::createIndex(Table::ORDERS, ['number'], unique: true); + Schema::createIndex(Table::ORDERS, ['orderStatusId']); + Schema::createIndex(Table::ORDERS, ['reference']); + Schema::createIndex(Table::ORDERS, ['shippingAddressId']); + Schema::createIndex(Table::ORDERS, ['sourceBillingAddressId']); + Schema::createIndex(Table::ORDERS, ['sourceShippingAddressId']); + Schema::createIndex(Table::ORDERS, ['storeId']); + Schema::createIndex(Table::ORDERSTATUSES, ['storeId']); + Schema::createIndex(Table::ORDERSTATUS_EMAILS, ['emailId']); + Schema::createIndex(Table::ORDERSTATUS_EMAILS, ['orderStatusId']); + Schema::createIndex(Table::PAYMENTCURRENCIES, ['iso']); + Schema::createIndex(Table::PDFS, ['handle']); + Schema::createIndex(Table::PDFS, ['storeId']); + Schema::createIndex(Table::PLANS, ['gatewayId']); + Schema::createIndex(Table::PLANS, ['handle'], unique: true); + Schema::createIndex(Table::PLANS, ['reference']); + Schema::createIndex(Table::PRODUCTS, ['expiryDate']); + Schema::createIndex(Table::PRODUCTS, ['postDate']); + Schema::createIndex(Table::PRODUCTS, ['typeId']); + Schema::createIndex(Table::PRODUCTTYPES, ['structureId']); + Schema::createIndex(Table::PRODUCTTYPES, ['fieldLayoutId']); + Schema::createIndex(Table::PRODUCTTYPES, ['handle'], unique: true); + Schema::createIndex(Table::PRODUCTTYPES, ['variantFieldLayoutId']); + Schema::createIndex(Table::PRODUCTTYPES_SHIPPINGCATEGORIES, ['shippingCategoryId']); + Schema::createIndex(Table::PRODUCTTYPES_SHIPPINGCATEGORIES, ['productTypeId', 'shippingCategoryId'], unique: true); + Schema::createIndex(Table::PRODUCTTYPES_SITES, ['siteId']); + Schema::createIndex(Table::PRODUCTTYPES_SITES, ['productTypeId', 'siteId'], unique: true); + Schema::createIndex(Table::PRODUCTTYPES_TAXCATEGORIES, ['taxCategoryId']); + Schema::createIndex(Table::PRODUCTTYPES_TAXCATEGORIES, ['productTypeId', 'taxCategoryId'], unique: true); + Schema::createIndex(Table::PURCHASABLES, ['sku']); // Application layer enforces unique + Schema::createIndex(Table::PURCHASABLES_STORES, ['purchasableId']); // Application layer enforces unique + Schema::createIndex(Table::PURCHASABLES_STORES, ['storeId']); // Application layer enforces unique + Schema::createIndex(Table::SALE_CATEGORIES, ['categoryId']); + Schema::createIndex(Table::SALE_CATEGORIES, ['saleId', 'categoryId'], unique: true); + Schema::createIndex(Table::SALE_PURCHASABLES, ['purchasableId']); + Schema::createIndex(Table::SALE_PURCHASABLES, ['saleId', 'purchasableId'], unique: true); + Schema::createIndex(Table::SALE_USERGROUPS, ['userGroupId']); + Schema::createIndex(Table::SALE_USERGROUPS, ['saleId', 'userGroupId'], unique: true); + Schema::createIndex(Table::SHIPPINGCATEGORIES, ['storeId']); + Schema::createIndex(Table::SHIPPINGMETHODS, ['name']); + Schema::createIndex(Table::SHIPPINGMETHODS, ['storeId']); + Schema::createIndex(Table::SHIPPINGRULES, ['methodId']); + Schema::createIndex(Table::SHIPPINGRULES, ['name']); + Schema::createIndex(Table::SHIPPINGRULE_CATEGORIES, ['shippingCategoryId']); + Schema::createIndex(Table::SHIPPINGRULE_CATEGORIES, ['shippingRuleId']); + Schema::createIndex(Table::SHIPPINGZONES, ['name']); + Schema::createIndex(Table::SHIPPINGZONES, ['storeId']); + Schema::createIndex(Table::SUBSCRIPTIONS, ['dateCreated']); + Schema::createIndex(Table::SUBSCRIPTIONS, ['dateExpired']); + Schema::createIndex(Table::SUBSCRIPTIONS, ['gatewayId']); + Schema::createIndex(Table::SUBSCRIPTIONS, ['nextPaymentDate']); + Schema::createIndex(Table::SUBSCRIPTIONS, ['planId']); + Schema::createIndex(Table::SUBSCRIPTIONS, ['reference'], unique: true); + Schema::createIndex(Table::SUBSCRIPTIONS, ['userId']); + Schema::createIndex(Table::TAXRATES, ['storeId']); + Schema::createIndex(Table::TAXRATES, ['taxCategoryId']); + Schema::createIndex(Table::TAXRATES, ['taxZoneId']); + Schema::createIndex(Table::TAXZONES, ['name']); + Schema::createIndex(Table::TAXZONES, ['storeId']); + Schema::createIndex(Table::TRANSACTIONS, ['gatewayId']); + Schema::createIndex(Table::TRANSACTIONS, ['orderId']); + Schema::createIndex(Table::TRANSACTIONS, ['parentId']); + Schema::createIndex(Table::TRANSACTIONS, ['userId']); + Schema::createIndex(Table::TRANSACTIONS, ['hash']); + Schema::createIndex(Table::TRANSFERS, ['destinationLocationId']); + Schema::createIndex(Table::TRANSFERS, ['originLocationId']); + Schema::createIndex(Table::TRANSFERDETAILS, ['transferId']); + Schema::createIndex(Table::TRANSFERDETAILS, ['inventoryItemId']); + Schema::createIndex(Table::VARIANTS, ['primaryOwnerId']); + } + + /** + * Adds the foreign keys. + */ + public function addForeignKeys(): void + { + Schema::table(Table::CATALOG_PRICING, fn (Blueprint $table) => $table->foreign('catalogPricingRuleId')->references('id')->on(Table::CATALOG_PRICING_RULES)->cascadeOnDelete()); + Schema::table(Table::CATALOG_PRICING, fn (Blueprint $table) => $table->foreign('purchasableId')->references('id')->on(Table::PURCHASABLES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::CATALOG_PRICING, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()); + Schema::table(Table::CATALOG_PRICING, fn (Blueprint $table) => $table->foreign('userId')->references('id')->on(CraftTable::USERS)->cascadeOnDelete()); + Schema::table(Table::CATALOG_PRICING_QUEUE, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::CATALOG_PRICING_RULES, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::CATALOG_PRICING_RULES_USERS, fn (Blueprint $table) => $table->foreign('catalogPricingRuleId')->references('id')->on(Table::CATALOG_PRICING_RULES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::CATALOG_PRICING_RULES_USERS, fn (Blueprint $table) => $table->foreign('userId')->references('id')->on(CraftTable::USERS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::COUPONS, fn (Blueprint $table) => $table->foreign('discountId')->references('id')->on(Table::DISCOUNTS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::CUSTOMERS, fn (Blueprint $table) => $table->foreign('customerId')->references('id')->on(CraftTable::ELEMENTS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::CUSTOMERS, fn (Blueprint $table) => $table->foreign('primaryBillingAddressId')->references('id')->on(CraftTable::ELEMENTS)->nullOnDelete()); + Schema::table(Table::CUSTOMERS, fn (Blueprint $table) => $table->foreign('primaryPaymentSourceId')->references('id')->on(Table::PAYMENTSOURCES)->nullOnDelete()); + Schema::table(Table::CUSTOMERS, fn (Blueprint $table) => $table->foreign('primaryShippingAddressId')->references('id')->on(CraftTable::ELEMENTS)->nullOnDelete()); + Schema::table(Table::CUSTOMER_DISCOUNTUSES, fn (Blueprint $table) => $table->foreign('customerId')->references('id')->on(CraftTable::ELEMENTS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::CUSTOMER_DISCOUNTUSES, fn (Blueprint $table) => $table->foreign('discountId')->references('id')->on(Table::DISCOUNTS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::DISCOUNTS, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::DISCOUNT_CATEGORIES, fn (Blueprint $table) => $table->foreign('categoryId')->references('id')->on(CraftTable::ELEMENTS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::DISCOUNT_CATEGORIES, fn (Blueprint $table) => $table->foreign('discountId')->references('id')->on(Table::DISCOUNTS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::DISCOUNT_PURCHASABLES, fn (Blueprint $table) => $table->foreign('discountId')->references('id')->on(Table::DISCOUNTS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::DISCOUNT_PURCHASABLES, fn (Blueprint $table) => $table->foreign('purchasableId')->references('id')->on(Table::PURCHASABLES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::DONATIONS, fn (Blueprint $table) => $table->foreign('id')->references('id')->on(CraftTable::ELEMENTS)->cascadeOnDelete()); + Schema::table(Table::EMAILS, fn (Blueprint $table) => $table->foreign('pdfId')->references('id')->on(Table::PDFS)->nullOnDelete()); + Schema::table(Table::EMAILS, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::EMAILS, fn (Blueprint $table) => $table->foreign('renderSiteId')->references('id')->on(CraftTable::SITES)->nullOnDelete()); + Schema::table(Table::EMAIL_DISCOUNTUSES, fn (Blueprint $table) => $table->foreign('discountId')->references('id')->on(Table::DISCOUNTS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::INVENTORYITEMS, fn (Blueprint $table) => $table->foreign('purchasableId')->references('id')->on(Table::PURCHASABLES)->cascadeOnDelete()); + Schema::table(Table::INVENTORYLOCATIONS, fn (Blueprint $table) => $table->foreign('addressId')->references('id')->on(CraftTable::ELEMENTS)->cascadeOnDelete()); + Schema::table(Table::INVENTORYLOCATIONS_STORES, fn (Blueprint $table) => $table->foreign('inventoryLocationId')->references('id')->on(Table::INVENTORYLOCATIONS)->cascadeOnDelete()); + Schema::table(Table::INVENTORYLOCATIONS_STORES, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()); + Schema::table(Table::INVENTORYTRANSACTIONS, fn (Blueprint $table) => $table->foreign('inventoryItemId')->references('id')->on(Table::INVENTORYITEMS)->cascadeOnDelete()); + Schema::table(Table::INVENTORYTRANSACTIONS, fn (Blueprint $table) => $table->foreign('inventoryLocationId')->references('id')->on(Table::INVENTORYLOCATIONS)->cascadeOnDelete()); + Schema::table(Table::INVENTORYTRANSACTIONS, fn (Blueprint $table) => $table->foreign('lineItemId')->references('id')->on(Table::LINEITEMS)->cascadeOnDelete()); + // NOTE: the legacy migration added this same FK twice (once here, once further down); only ported once. + Schema::table(Table::INVENTORYTRANSACTIONS, fn (Blueprint $table) => $table->foreign('transferId')->references('id')->on(Table::TRANSFERS)->nullOnDelete()); + Schema::table(Table::INVENTORYTRANSACTIONS, fn (Blueprint $table) => $table->foreign('userId')->references('id')->on(CraftTable::USERS)->nullOnDelete()); + Schema::table(Table::LINEITEMS, fn (Blueprint $table) => $table->foreign('orderId')->references('id')->on(Table::ORDERS)->cascadeOnDelete()); + Schema::table(Table::LINEITEMS, fn (Blueprint $table) => $table->foreign('purchasableId')->references('id')->on(CraftTable::ELEMENTS)->nullOnDelete()->cascadeOnUpdate()); + Schema::table(Table::LINEITEMS, fn (Blueprint $table) => $table->foreign('shippingCategoryId')->references('id')->on(Table::SHIPPINGCATEGORIES)->cascadeOnUpdate()); + Schema::table(Table::LINEITEMS, fn (Blueprint $table) => $table->foreign('taxCategoryId')->references('id')->on(Table::TAXCATEGORIES)->cascadeOnUpdate()); + Schema::table(Table::LINEITEMSTATUSES, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::ORDERADJUSTMENTS, fn (Blueprint $table) => $table->foreign('orderId')->references('id')->on(Table::ORDERS)->cascadeOnDelete()); + Schema::table(Table::ORDERHISTORIES, fn (Blueprint $table) => $table->foreign('newStatusId')->references('id')->on(Table::ORDERSTATUSES)->restrictOnDelete()->cascadeOnUpdate()); + Schema::table(Table::ORDERHISTORIES, fn (Blueprint $table) => $table->foreign('orderId')->references('id')->on(Table::ORDERS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::ORDERHISTORIES, fn (Blueprint $table) => $table->foreign('prevStatusId')->references('id')->on(Table::ORDERSTATUSES)->restrictOnDelete()->cascadeOnUpdate()); + Schema::table(Table::ORDERHISTORIES, fn (Blueprint $table) => $table->foreign('userId')->references('id')->on(CraftTable::ELEMENTS)->nullOnDelete()); + Schema::table(Table::ORDERNOTICES, fn (Blueprint $table) => $table->foreign('orderId')->references('id')->on(Table::ORDERS)->cascadeOnDelete()); + Schema::table(Table::ORDERS, fn (Blueprint $table) => $table->foreign('billingAddressId')->references('id')->on(CraftTable::ELEMENTS)->nullOnDelete()); + Schema::table(Table::ORDERS, fn (Blueprint $table) => $table->foreign('customerId')->references('id')->on(CraftTable::ELEMENTS)->nullOnDelete()); + Schema::table(Table::ORDERS, fn (Blueprint $table) => $table->foreign('estimatedBillingAddressId')->references('id')->on(CraftTable::ELEMENTS)->nullOnDelete()); + Schema::table(Table::ORDERS, fn (Blueprint $table) => $table->foreign('estimatedShippingAddressId')->references('id')->on(CraftTable::ELEMENTS)->nullOnDelete()); + Schema::table(Table::ORDERS, fn (Blueprint $table) => $table->foreign('gatewayId')->references('id')->on(Table::GATEWAYS)->nullOnDelete()); + Schema::table(Table::ORDERS, fn (Blueprint $table) => $table->foreign('id')->references('id')->on(CraftTable::ELEMENTS)->cascadeOnDelete()); + Schema::table(Table::ORDERS, fn (Blueprint $table) => $table->foreign('orderStatusId')->references('id')->on(Table::ORDERSTATUSES)->restrictOnDelete()->cascadeOnUpdate()); + Schema::table(Table::ORDERS, fn (Blueprint $table) => $table->foreign('paymentSourceId')->references('id')->on(Table::PAYMENTSOURCES)->nullOnDelete()); + Schema::table(Table::ORDERS, fn (Blueprint $table) => $table->foreign('shippingAddressId')->references('id')->on(CraftTable::ELEMENTS)->nullOnDelete()); + Schema::table(Table::ORDERS, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::ORDERSTATUSES, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::ORDERSTATUS_EMAILS, fn (Blueprint $table) => $table->foreign('emailId')->references('id')->on(Table::EMAILS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::ORDERSTATUS_EMAILS, fn (Blueprint $table) => $table->foreign('orderStatusId')->references('id')->on(Table::ORDERSTATUSES)->restrictOnDelete()->cascadeOnUpdate()); + Schema::table(Table::PAYMENTCURRENCIES, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::PAYMENTSOURCES, fn (Blueprint $table) => $table->foreign('customerId')->references('id')->on(CraftTable::ELEMENTS)->cascadeOnDelete()); + Schema::table(Table::PAYMENTSOURCES, fn (Blueprint $table) => $table->foreign('gatewayId')->references('id')->on(Table::GATEWAYS)->cascadeOnDelete()); + Schema::table(Table::PDFS, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()); + Schema::table(Table::PLANS, fn (Blueprint $table) => $table->foreign('gatewayId')->references('id')->on(Table::GATEWAYS)->cascadeOnDelete()); + Schema::table(Table::PLANS, fn (Blueprint $table) => $table->foreign('planInformationId')->references('id')->on(CraftTable::ELEMENTS)->nullOnDelete()); + Schema::table(Table::PRODUCTS, fn (Blueprint $table) => $table->foreign('id')->references('id')->on(CraftTable::ELEMENTS)->cascadeOnDelete()); + Schema::table(Table::PRODUCTS, fn (Blueprint $table) => $table->foreign('typeId')->references('id')->on(Table::PRODUCTTYPES)->cascadeOnDelete()); + Schema::table(Table::PRODUCTS, fn (Blueprint $table) => $table->foreign('defaultVariantId')->references('id')->on(CraftTable::ELEMENTS)->nullOnDelete()); + Schema::table(Table::PRODUCTTYPES, fn (Blueprint $table) => $table->foreign('fieldLayoutId')->references('id')->on(CraftTable::FIELDLAYOUTS)->nullOnDelete()); + Schema::table(Table::PRODUCTTYPES, fn (Blueprint $table) => $table->foreign('variantFieldLayoutId')->references('id')->on(CraftTable::FIELDLAYOUTS)->nullOnDelete()); + Schema::table(Table::PRODUCTTYPES, fn (Blueprint $table) => $table->foreign('structureId')->references('id')->on(CraftTable::STRUCTURES)->nullOnDelete()); + Schema::table(Table::PRODUCTTYPES_SHIPPINGCATEGORIES, fn (Blueprint $table) => $table->foreign('productTypeId')->references('id')->on(Table::PRODUCTTYPES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::PRODUCTTYPES_SHIPPINGCATEGORIES, fn (Blueprint $table) => $table->foreign('shippingCategoryId', 'commerce_pts_shippingcategoryid_foreign')->references('id')->on(Table::SHIPPINGCATEGORIES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::PRODUCTTYPES_SITES, fn (Blueprint $table) => $table->foreign('productTypeId')->references('id')->on(Table::PRODUCTTYPES)->cascadeOnDelete()); + Schema::table(Table::PRODUCTTYPES_SITES, fn (Blueprint $table) => $table->foreign('siteId')->references('id')->on(CraftTable::SITES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::PRODUCTTYPES_TAXCATEGORIES, fn (Blueprint $table) => $table->foreign('productTypeId')->references('id')->on(Table::PRODUCTTYPES)->cascadeOnDelete()); + Schema::table(Table::PRODUCTTYPES_TAXCATEGORIES, fn (Blueprint $table) => $table->foreign('taxCategoryId')->references('id')->on(Table::TAXCATEGORIES)->cascadeOnDelete()); + Schema::table(Table::PURCHASABLES, fn (Blueprint $table) => $table->foreign('id')->references('id')->on(CraftTable::ELEMENTS)->cascadeOnDelete()); + Schema::table(Table::PURCHASABLES, fn (Blueprint $table) => $table->foreign('taxCategoryId')->references('id')->on(Table::TAXCATEGORIES)); + // NOTE: the legacy migration added this same FK twice (once with just cascadeOnDelete, once with cascadeOnDelete+cascadeOnUpdate); only the more complete one is ported. + Schema::table(Table::PURCHASABLES_STORES, fn (Blueprint $table) => $table->foreign('purchasableId')->references('id')->on(Table::PURCHASABLES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::PURCHASABLES_STORES, fn (Blueprint $table) => $table->foreign('shippingCategoryId')->references('id')->on(Table::SHIPPINGCATEGORIES)->nullOnDelete()); + Schema::table(Table::PURCHASABLES_STORES, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()); + Schema::table(Table::SALE_CATEGORIES, fn (Blueprint $table) => $table->foreign('categoryId')->references('id')->on(CraftTable::ELEMENTS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::SALE_CATEGORIES, fn (Blueprint $table) => $table->foreign('saleId')->references('id')->on(Table::SALES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::SALE_PURCHASABLES, fn (Blueprint $table) => $table->foreign('purchasableId')->references('id')->on(Table::PURCHASABLES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::SALE_PURCHASABLES, fn (Blueprint $table) => $table->foreign('saleId')->references('id')->on(Table::SALES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::SALE_USERGROUPS, fn (Blueprint $table) => $table->foreign('saleId')->references('id')->on(Table::SALES)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::SALE_USERGROUPS, fn (Blueprint $table) => $table->foreign('userGroupId')->references('id')->on(CraftTable::USERGROUPS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::SHIPPINGCATEGORIES, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()); + Schema::table(Table::SHIPPINGMETHODS, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()); + Schema::table(Table::SHIPPINGRULES, fn (Blueprint $table) => $table->foreign('methodId')->references('id')->on(Table::SHIPPINGMETHODS)->cascadeOnDelete()); + Schema::table(Table::SHIPPINGRULE_CATEGORIES, fn (Blueprint $table) => $table->foreign('shippingCategoryId')->references('id')->on(Table::SHIPPINGCATEGORIES)->cascadeOnDelete()); + Schema::table(Table::SHIPPINGRULE_CATEGORIES, fn (Blueprint $table) => $table->foreign('shippingRuleId')->references('id')->on(Table::SHIPPINGRULES)->cascadeOnDelete()); + Schema::table(Table::SHIPPINGZONES, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()); + Schema::table(Table::STORESETTINGS, fn (Blueprint $table) => $table->foreign('locationAddressId')->references('id')->on(CraftTable::ELEMENTS)->nullOnDelete()); + Schema::table(Table::STORESETTINGS, fn (Blueprint $table) => $table->foreign('id')->references('id')->on(Table::STORES)->cascadeOnDelete()); + Schema::table(Table::SUBSCRIPTIONS, fn (Blueprint $table) => $table->foreign('gatewayId')->references('id')->on(Table::GATEWAYS)->restrictOnDelete()); + Schema::table(Table::SUBSCRIPTIONS, fn (Blueprint $table) => $table->foreign('id')->references('id')->on(CraftTable::ELEMENTS)->cascadeOnDelete()); + Schema::table(Table::SUBSCRIPTIONS, fn (Blueprint $table) => $table->foreign('orderId')->references('id')->on(Table::ORDERS)->nullOnDelete()); + Schema::table(Table::SUBSCRIPTIONS, fn (Blueprint $table) => $table->foreign('planId')->references('id')->on(Table::PLANS)->restrictOnDelete()); + Schema::table(Table::SUBSCRIPTIONS, fn (Blueprint $table) => $table->foreign('userId')->references('id')->on(CraftTable::USERS)->cascadeOnDelete()); + Schema::table(Table::TAXRATES, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()); + Schema::table(Table::TAXRATES, fn (Blueprint $table) => $table->foreign('taxCategoryId')->references('id')->on(Table::TAXCATEGORIES)->cascadeOnUpdate()); + Schema::table(Table::TAXRATES, fn (Blueprint $table) => $table->foreign('taxZoneId')->references('id')->on(Table::TAXZONES)->cascadeOnUpdate()); + Schema::table(Table::TAXZONES, fn (Blueprint $table) => $table->foreign('storeId')->references('id')->on(Table::STORES)->cascadeOnDelete()); + Schema::table(Table::TRANSACTIONS, fn (Blueprint $table) => $table->foreign('gatewayId')->references('id')->on(Table::GATEWAYS)->cascadeOnUpdate()); + Schema::table(Table::TRANSACTIONS, fn (Blueprint $table) => $table->foreign('orderId')->references('id')->on(Table::ORDERS)->cascadeOnDelete()); + Schema::table(Table::TRANSACTIONS, fn (Blueprint $table) => $table->foreign('parentId')->references('id')->on(Table::TRANSACTIONS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::TRANSACTIONS, fn (Blueprint $table) => $table->foreign('userId')->references('id')->on(CraftTable::ELEMENTS)->nullOnDelete()); + Schema::table(Table::TRANSFERS, fn (Blueprint $table) => $table->foreign('id')->references('id')->on(CraftTable::ELEMENTS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::TRANSFERDETAILS, fn (Blueprint $table) => $table->foreign('transferId')->references('id')->on(Table::TRANSFERS)->cascadeOnDelete()->cascadeOnUpdate()); + Schema::table(Table::TRANSFERDETAILS, fn (Blueprint $table) => $table->foreign('inventoryItemId')->references('id')->on(Table::INVENTORYITEMS)->nullOnDelete()->cascadeOnUpdate()); + Schema::table(Table::VARIANTS, fn (Blueprint $table) => $table->foreign('id')->references('id')->on(CraftTable::ELEMENTS)->cascadeOnDelete()); + Schema::table(Table::VARIANTS, fn (Blueprint $table) => $table->foreign('primaryOwnerId')->references('id')->on(Table::PRODUCTS)->cascadeOnDelete()); + } + + /** + * Inserts the default data. + * + * This is a fresh-install migration only (there is no upgrade path through this class), so — unlike the + * legacy Yii2 version — there's no need to guard against a pre-existing project config, patch up an old + * 5.0.0-beta.1 project config bug, or skip re-seeding a store/gateway that already exist. We also insert + * rows directly rather than going through the Commerce services (e.g. Stores::saveStore()), since those + * write through the project config system, which isn't guaranteed to apply synchronously in every context + * this migration runs in (e.g. the Testbench-based test harness). + */ + public function insertDefaultData(): void + { + $now = now()->toDateTimeString(); + + // Default (primary) store + $storeId = DB::table(Table::STORES)->insertGetId([ + 'name' => 'Primary', + 'handle' => 'primary', + 'primary' => true, + 'currency' => 'USD', + 'orderReferenceFormat' => '{{number[:7]}}', + 'sortOrder' => 1, + 'dateCreated' => $now, + 'dateUpdated' => $now, + 'uid' => Str::uuid()->toString(), + ]); + + // Map every existing site to the new store, using the site's own uid (there's only ever one + // site-store mapping row per site, so it can safely share the site's uid). + $sites = DB::table(CraftTable::SITES)->select(['id', 'uid'])->get(); + foreach ($sites as $site) { + DB::table(Table::SITESTORES)->insert([ + 'siteId' => $site->id, + 'storeId' => $storeId, + 'uid' => $site->uid, + 'dateCreated' => $now, + 'dateUpdated' => $now, + ]); + } + + // Default payment currency for the store + DB::table(Table::PAYMENTCURRENCIES)->insert([ + 'storeId' => $storeId, + 'iso' => 'USD', + 'rate' => 1, + 'dateCreated' => $now, + 'dateUpdated' => $now, + ]); + + // Default shipping category for the store + DB::table(Table::SHIPPINGCATEGORIES)->insert([ + 'storeId' => $storeId, + 'name' => 'General', + 'handle' => 'general', + 'default' => true, + 'dateCreated' => $now, + 'dateUpdated' => $now, + ]); + + // Default order status for the store + DB::table(Table::ORDERSTATUSES)->insert([ + 'storeId' => $storeId, + 'name' => 'New', + 'handle' => 'new', + 'color' => 'green', + 'default' => true, + 'dateCreated' => $now, + 'dateUpdated' => $now, + ]); + + // Default (Dummy) gateway + DB::table(Table::GATEWAYS)->insert([ + // TODO: update to the CraftCms\Commerce\... FQCN once the Dummy gateway is migrated to src/ + 'type' => 'craft\\commerce\\gateways\\Dummy', + 'name' => 'Dummy', + 'handle' => 'dummy', + 'isFrontendEnabled' => '1', + 'isArchived' => false, + 'dateCreated' => $now, + 'dateUpdated' => $now, + ]); + + // Default tax category (global, not store-specific) + DB::table(Table::TAXCATEGORIES)->insert([ + 'name' => 'General', + 'handle' => 'general', + 'default' => true, + 'dateCreated' => $now, + 'dateUpdated' => $now, + ]); + + // Default inventory location, assigned to the store + $inventoryLocationId = DB::table(Table::INVENTORYLOCATIONS)->insertGetId([ + 'handle' => 'default', + 'name' => 'Default', + 'dateCreated' => $now, + 'dateUpdated' => $now, + ]); + + DB::table(Table::INVENTORYLOCATIONS_STORES)->insert([ + 'inventoryLocationId' => $inventoryLocationId, + 'storeId' => $storeId, + 'sortOrder' => 1, + 'dateCreated' => $now, + 'dateUpdated' => $now, + ]); + } + + public function down(): void + { + $this->task('Uninstall Craft Commerce', function (?Logger $logger = null) { + $logger?->subLabel('Dropping tables...'); + $this->dropTables(); + $logger?->success('Tables dropped.'); + + $logger?->subLabel('Removing field layouts...'); + $this->dropFieldLayouts(); + $logger?->success('Field layouts removed.'); + + $logger?->subLabel('Removing project config...'); + ProjectConfig::remove('commerce'); + $logger?->success('Project config removed.'); + }); + } + + /** + * Drops all of Commerce's tables. + */ + public function dropTables(): void + { + Schema::disableForeignKeyConstraints(); + + foreach ($this->allTableNames() as $table) { + Schema::dropIfExists($table); + } + + Schema::enableForeignKeyConstraints(); + } + + /** + * Deletes the field layouts belonging to Commerce's element types, including legacy + * `craft\commerce\*` type strings left behind by installs that predate the Laravel port. + */ + public function dropFieldLayouts(): void + { + DB::table(CraftTable::FIELDLAYOUTS)->whereIn('type', [ + Order::class, + Product::class, + Variant::class, + 'craft\\commerce\\elements\\Order', + 'craft\\commerce\\elements\\Product', + 'craft\\commerce\\elements\\Variant', + // Subscription and Transfer field layouts predate/are pending the Laravel port — + // these strings mirror the FQCNs `craft\commerce\elements\Subscription` (element + // removed in 6.0) and `craft\commerce\elements\Transfer` (not yet ported to src/). + 'craft\\commerce\\elements\\Subscription', + 'craft\\commerce\\elements\\Transfer', + ])->delete(); + } + + /** @return string[] */ + private function allTableNames(): array + { + return array_values((new ReflectionClass(Table::class))->getConstants()); + } +} diff --git a/example-templates/dist/shop/_private/layouts/includes/nav-main.twig b/example-templates/dist/shop/_private/layouts/includes/nav-main.twig index 21c6374319..78ddaf6303 100644 --- a/example-templates/dist/shop/_private/layouts/includes/nav-main.twig +++ b/example-templates/dist/shop/_private/layouts/includes/nav-main.twig @@ -10,10 +10,6 @@ Outputs the site’s global main navigation based on path and included `pages` a label: 'Products', url: 'shop/products' }, - { - label: 'Plans', - url: 'shop/plans' - }, { label: 'Donations', url: 'shop/donations' diff --git a/example-templates/dist/shop/plans/index.twig b/example-templates/dist/shop/plans/index.twig deleted file mode 100644 index a0cc6e42a7..0000000000 --- a/example-templates/dist/shop/plans/index.twig +++ /dev/null @@ -1,164 +0,0 @@ -{% extends 'shop/_private/layouts' %} - -{# @var plans \craft\commerce\base\Plan[] #} -{% set plans = craft.commerce.getPlans().getAllEnabledPlans() %} - -{% block main %} - -

- {{- 'Plans'|t -}} -

- {% if not plans|length %} -

- {{- 'No plans set up.'|t -}} -

- {% endif %} - - {# @var currentUser \craft\elements\User #} - {% if currentUser %} -
- {% set subscriptions = craft.subscriptions.status(null).userId(currentUser.id).all() %} - - {% if subscriptions|length %} - - - - - - - - - - - {% for subscription in subscriptions %} - - - - - - - {% endfor %} - -
{{ 'Plan'|t }}{{ 'Created'|t }}{{ 'Next Payment'|t }} 
- {% set plan = subscription.plan ?? null %} - {% if plan %} -
- {{ plan.name }} - - {# @var information \craft\elements\Entry #} - {% set information = plan.getInformation() ?? null %} - {% if information %} -
-

{{ 'Plan Information Entry'|t }}

-
    -
  • {{ 'ID' }}: {{ information.id }}
  • -
  • {{ 'Title' }}: {{ information.title }}
  • -
-
-
- {% endif %} -
- {% endif %} - - {% if subscription.isCanceled %} -
- - {{- 'Canceled on {date}.'|t({ date: subscription.dateCanceled|date('Y-m-d') }) -}} - -
- {% endif %} -
- {{ subscription.dateCreated|date('Y-m-d') }} - - {{- subscription.isCanceled - ? 'Expires on {date}.'|t({ date: subscription.nextPaymentDate|date('Y-m-d') }) - : '' - -}} - - - {{- 'Manage'|t -}} - - - {% if subscription.isSuspended and subscription.hasBillingIssues %} - - {{- 'Fix Billing'|t -}} - - {% endif %} - -
- {% else %} -

- {{- 'You do not have any active subscriptions.'|t -}} -

- {% endif %} -
- {% endif %} - - {% if currentUser and plans|length %} -
-

- {{ 'Available Plans'|t }} -

- -
- {% for plan in plans %} - {% set paymentSources = craft.commerce.paymentSources.getAllPaymentSourcesByCustomerId(currentUser.id, plan.gateway.id) %} - -
-
-
- {{ csrfInput() }} - {{ actionInput('commerce/subscriptions/subscribe') }} - {{ redirectInput('shop/plans') }} - {{ hiddenInput('planUid', plan.uid|hash) }} - -

- {{- plan.name -}} -

- {% if paymentSources|length %} -
- {% tag 'select' with { - name: 'trialDays', - 'data-plan': plan.id, - class: 'border border-gray-300 hover:border-gray-500 px-4 py-2 leading-tight rounded' - } %} - {% for i in [0, 3, 7, 14] %} - {% if i == 0 %} - {{ tag('option', { - value: (plan.uid ~ ':0')|hash, - text: 'No trial period.'|t - }) }} - {% else %} - {{ tag('option', { - value: (plan.uid ~ ':' ~ i)|hash, - text: 'Trial for {n} days.'|t({ n: i }) - }) }} - {% endif %} - {% endfor %} - {% endtag %} -
- {{ tag('button', { - type: 'submit', - class: 'cursor-pointer rounded px-4 py-2 inline-block bg-blue-500 hover:bg-blue-600 text-white hover:text-white', - text: 'Subscribe'|t - }) }} -
- {% else %} -

- {{- 'You do not have any payment sources set up for {gateway}.'|t({ gateway: plan.gateway.name }) -}} -

- Add Card - {% endif %} -
-
-
-
- {% endfor %} -
-
- {% endif %} -{% endblock %} diff --git a/example-templates/dist/shop/plans/subscription/index.twig b/example-templates/dist/shop/plans/subscription/index.twig deleted file mode 100644 index 606bed76f9..0000000000 --- a/example-templates/dist/shop/plans/subscription/index.twig +++ /dev/null @@ -1,160 +0,0 @@ -{% extends 'shop/_private/layouts' %} - -{# @var subscriptionId string #} -{% set subscriptionId = craft.app.request.param('subscription') %} -{# @var subscription \craft\commerce\elements\Subscription #} -{% set subscription = craft.subscriptions() - .id(subscriptionId) - .one() %} - -{% if not subscription or currentUser is null or subscription.userId != currentUser.id %} - {% redirect 'shop/plans' %} -{% endif %} - -{% block main %} - - -

- {{- 'Manage {plan}'|t({ plan: subscription.plan.name }) -}} -

- - {# @var information \craft\elements\Entry #} - {% set information = subscription.plan.getInformation() ?? null %} -
-
- - {{- 'Subscription Information'|t -}} - -
-
 
- {% if information %} -
- {{- 'Plan Information Entry'|t -}} -
-
-
    -
  • {{ 'ID'|t }}: {{ information.id }}
  • -
  • {{ 'Title'|t }}: {{ information.title }}
  • -
-
- {% endif %} - {% if subscription.isExpired %} -
{{ 'Expired on'|t }}
-
{{ subscription.dateExpired|date('Y-m-d') }}
- {% else %} - {% if subscription.isCanceled %} -
{{ 'Cancelled on'|t }}
-
{{ subscription.dateCanceled|date('Y-m-d') }}
-
{{ 'Expires on'|t }}
-
{{ subscription.nextPaymentDate|date('Y-m-d') }}
- {% if subscription.canReactivate() %} -
-
-
- {{ csrfInput() }} - {{ actionInput('commerce/subscriptions/reactivate') }} - {{ hiddenInput('subscriptionUid', subscription.uid|hash) }} - {{ redirectInput('shop/plans') }} - {{ tag('button', { - type: 'submit', - class: 'cursor-pointer rounded px-4 py-2 inline-block bg-blue-500 hover:bg-blue-600 text-white hover:text-white', - text: 'Reactivate'|t - }) }} -
-
- {% endif %} - {% else %} -
{{ 'Payment Amount'|t }}
-
{{ subscription.getNextPaymentAmount() }}
-
{{ 'Next Payment'|t }}
-
{{ subscription.nextPaymentDate|date('Y-m-d') }}
-
 
-
-
- {{ csrfInput() }} - {{ actionInput('commerce/subscriptions/cancel') }} - {{ hiddenInput('subscriptionUid', subscription.uid|hash) }} - {{ redirectInput('shop/plans') }} - - {{ subscription.plan.getGateway().getCancelSubscriptionFormHtml(subscription)|raw }} - - {{ tag('button', { - type: 'submit', - class: 'cursor-pointer rounded px-4 py-2 inline-block bg-gray-500 hover:bg-gray-600 text-white hover:text-white', - text: 'Unsubscribe'|t - }) }} -
-
- {% endif %} - {% endif %} -
- - {% if not subscription.isCanceled and not subscription.isExpired and subscription.alternativePlans|length %} -
-

- {{- 'Alternative Plans'|t -}} -

- - - - - - - - - {% for plan in subscription.alternativePlans %} - - - - - {% endfor %} - -
{{ 'Plan'|t }} 
{{ plan.name }} -
- {{ csrfInput() }} - {{ actionInput('commerce/subscriptions/switch') }} - {{ hiddenInput('subscriptionUid', subscription.uid|hash) }} - {{ hiddenInput('planUid', plan.uid|hash) }} - {{ redirectInput('shop/plans') }} - - {{ plan.gateway.getSwitchPlansFormHtml(subscription.plan, plan)|raw }} - - {{ tag('button', { - type: 'submit', - class: 'cursor-pointer rounded px-4 py-2 inline-block bg-blue-500 hover:bg-blue-600 text-white hover:text-white', - text: 'Switch'|t - }) }} -
-
-
- {% endif %} - - {# @var payments \craft\commerce\models\subscriptions\SubscriptionPayment[] #} - {% set payments = subscription.getAllPayments() %} - {% if payments|length %} -

- {{- 'Payment History'|t -}} -

- - - - - - - - -
{{ 'Date'|t }}{{ 'Amount'|t }}
- - {% for payment in payments %} - - - {{ payment.paymentDate|date("Y-m-d H:i") }} - - - {{ payment.paymentCurrency }} {{ payment.paymentAmount }} - - - {% endfor %} - - {% endif %} -{% endblock %} diff --git a/example-templates/dist/shop/plans/update-billing-details.twig b/example-templates/dist/shop/plans/update-billing-details.twig deleted file mode 100644 index 4591167ad6..0000000000 --- a/example-templates/dist/shop/plans/update-billing-details.twig +++ /dev/null @@ -1,55 +0,0 @@ -{% extends 'shop/_private/layouts' %} - -{# @var subscriptionUid string #} -{% set subscriptionUid = craft.app.request.getParam('subscription') %} -{# @var subscription \craft\commerce\elements\Subscription #} -{% set subscription = craft.subscriptions() - .status(null) - .uid(subscriptionUid) - .one() %} - -{% block main %} - - - {# @var currentUser \craft\elements\User #} - {% if currentUser is null or not subscriptionUid or not subscription %} - {% exit 404 %} - {% endif %} - - {% if subscription.subscriber.id != currentUser.id %} - {% exit 404 %} - {% endif %} - - {% if subscription.isExpired == true %} - {% exit 404 %} - {% endif %} - - {% if subscription.isCanceled == true %} - {% exit 404 %} - {% endif %} - - {% set planName = subscription.getPlan().name %} - -
-
- {% if subscription.isSuspended and subscription.hasBillingIssues %} -

- {{- 'Billing issue for subscription to {plan}'|t({ plan: planName }) -}} -

- -
{{ subscription.getBillingIssueDescription() }}
- -
-
- {{ redirectInput('shop/plans') }} - {{ subscription.getBillingIssueResolveFormHtml()|raw }} -
-
- {% else %} -

- {{- 'No issues with subscription to {plan}'|t({ plan: planName }) -}} -

- {% endif %} -
-
-{% endblock %} diff --git a/example-templates/src/shop/_private/layouts/includes/nav-main.twig b/example-templates/src/shop/_private/layouts/includes/nav-main.twig index 9c4a3ec453..886f66c2ea 100644 --- a/example-templates/src/shop/_private/layouts/includes/nav-main.twig +++ b/example-templates/src/shop/_private/layouts/includes/nav-main.twig @@ -10,10 +10,6 @@ Outputs the site’s global main navigation based on path and included `pages` a label: 'Products', url: '[[folderName]]/products' }, - { - label: 'Plans', - url: '[[folderName]]/plans' - }, { label: 'Donations', url: '[[folderName]]/donations' diff --git a/example-templates/src/shop/plans/index.twig b/example-templates/src/shop/plans/index.twig deleted file mode 100755 index 0c004d042a..0000000000 --- a/example-templates/src/shop/plans/index.twig +++ /dev/null @@ -1,164 +0,0 @@ -{% extends '[[folderName]]/_private/layouts' %} - -{# @var plans \craft\commerce\base\Plan[] #} -{% set plans = craft.commerce.getPlans().getAllEnabledPlans() %} - -{% block main %} - -

- {{- 'Plans'|t -}} -

- {% if not plans|length %} -

- {{- 'No plans set up.'|t -}} -

- {% endif %} - - {# @var currentUser \craft\elements\User #} - {% if currentUser %} -
- {% set subscriptions = craft.subscriptions.status(null).userId(currentUser.id).all() %} - - {% if subscriptions|length %} - - - - - - - - - - - {% for subscription in subscriptions %} - - - - - - - {% endfor %} - -
{{ 'Plan'|t }}{{ 'Created'|t }}{{ 'Next Payment'|t }} 
- {% set plan = subscription.plan ?? null %} - {% if plan %} -
- {{ plan.name }} - - {# @var information \craft\elements\Entry #} - {% set information = plan.getInformation() ?? null %} - {% if information %} -
-

{{ 'Plan Information Entry'|t }}

-
    -
  • {{ 'ID' }}: {{ information.id }}
  • -
  • {{ 'Title' }}: {{ information.title }}
  • -
-
-
- {% endif %} -
- {% endif %} - - {% if subscription.isCanceled %} -
- - {{- 'Canceled on {date}.'|t({ date: subscription.dateCanceled|date('Y-m-d') }) -}} - -
- {% endif %} -
- {{ subscription.dateCreated|date('Y-m-d') }} - - {{- subscription.isCanceled - ? 'Expires on {date}.'|t({ date: subscription.nextPaymentDate|date('Y-m-d') }) - : '' - -}} - - - {{- 'Manage'|t -}} - - - {% if subscription.isSuspended and subscription.hasBillingIssues %} - - {{- 'Fix Billing'|t -}} - - {% endif %} - -
- {% else %} -

- {{- 'You do not have any active subscriptions.'|t -}} -

- {% endif %} -
- {% endif %} - - {% if currentUser and plans|length %} -
-

- {{ 'Available Plans'|t }} -

- -
- {% for plan in plans %} - {% set paymentSources = craft.commerce.paymentSources.getAllPaymentSourcesByCustomerId(currentUser.id, plan.gateway.id) %} - -
-
-
- {{ csrfInput() }} - {{ actionInput('commerce/subscriptions/subscribe') }} - {{ redirectInput('[[folderName]]/plans') }} - {{ hiddenInput('planUid', plan.uid|hash) }} - -

- {{- plan.name -}} -

- {% if paymentSources|length %} -
- {% tag 'select' with { - name: 'trialDays', - 'data-plan': plan.id, - class: '[[classes.input]]' - } %} - {% for i in [0, 3, 7, 14] %} - {% if i == 0 %} - {{ tag('option', { - value: (plan.uid ~ ':0')|hash, - text: 'No trial period.'|t - }) }} - {% else %} - {{ tag('option', { - value: (plan.uid ~ ':' ~ i)|hash, - text: 'Trial for {n} days.'|t({ n: i }) - }) }} - {% endif %} - {% endfor %} - {% endtag %} -
- {{ tag('button', { - type: 'submit', - class: '[[classes.btn.base]] [[classes.btn.mainColor]]', - text: 'Subscribe'|t - }) }} -
- {% else %} -

- {{- 'You do not have any payment sources set up for {gateway}.'|t({ gateway: plan.gateway.name }) -}} -

- Add Card - {% endif %} -
-
-
-
- {% endfor %} -
-
- {% endif %} -{% endblock %} diff --git a/example-templates/src/shop/plans/subscription/index.twig b/example-templates/src/shop/plans/subscription/index.twig deleted file mode 100755 index dec06f877d..0000000000 --- a/example-templates/src/shop/plans/subscription/index.twig +++ /dev/null @@ -1,160 +0,0 @@ -{% extends '[[folderName]]/_private/layouts' %} - -{# @var subscriptionId string #} -{% set subscriptionId = craft.app.request.param('subscription') %} -{# @var subscription \craft\commerce\elements\Subscription #} -{% set subscription = craft.subscriptions() - .id(subscriptionId) - .one() %} - -{% if not subscription or currentUser is null or subscription.userId != currentUser.id %} - {% redirect '[[folderName]]/plans' %} -{% endif %} - -{% block main %} - - -

- {{- 'Manage {plan}'|t({ plan: subscription.plan.name }) -}} -

- - {# @var information \craft\elements\Entry #} - {% set information = subscription.plan.getInformation() ?? null %} -
-
- - {{- 'Subscription Information'|t -}} - -
-
 
- {% if information %} -
- {{- 'Plan Information Entry'|t -}} -
-
-
    -
  • {{ 'ID'|t }}: {{ information.id }}
  • -
  • {{ 'Title'|t }}: {{ information.title }}
  • -
-
- {% endif %} - {% if subscription.isExpired %} -
{{ 'Expired on'|t }}
-
{{ subscription.dateExpired|date('Y-m-d') }}
- {% else %} - {% if subscription.isCanceled %} -
{{ 'Cancelled on'|t }}
-
{{ subscription.dateCanceled|date('Y-m-d') }}
-
{{ 'Expires on'|t }}
-
{{ subscription.nextPaymentDate|date('Y-m-d') }}
- {% if subscription.canReactivate() %} -
-
-
- {{ csrfInput() }} - {{ actionInput('commerce/subscriptions/reactivate') }} - {{ hiddenInput('subscriptionUid', subscription.uid|hash) }} - {{ redirectInput('[[folderName]]/plans') }} - {{ tag('button', { - type: 'submit', - class: '[[classes.btn.base]] [[classes.btn.mainColor]]', - text: 'Reactivate'|t - }) }} -
-
- {% endif %} - {% else %} -
{{ 'Payment Amount'|t }}
-
{{ subscription.getNextPaymentAmount() }}
-
{{ 'Next Payment'|t }}
-
{{ subscription.nextPaymentDate|date('Y-m-d') }}
-
 
-
-
- {{ csrfInput() }} - {{ actionInput('commerce/subscriptions/cancel') }} - {{ hiddenInput('subscriptionUid', subscription.uid|hash) }} - {{ redirectInput('[[folderName]]/plans') }} - - {{ subscription.plan.getGateway().getCancelSubscriptionFormHtml(subscription)|raw }} - - {{ tag('button', { - type: 'submit', - class: '[[classes.btn.base]] [[classes.btn.grayColor]]', - text: 'Unsubscribe'|t - }) }} -
-
- {% endif %} - {% endif %} -
- - {% if not subscription.isCanceled and not subscription.isExpired and subscription.alternativePlans|length %} -
-

- {{- 'Alternative Plans'|t -}} -

- - - - - - - - - {% for plan in subscription.alternativePlans %} - - - - - {% endfor %} - -
{{ 'Plan'|t }} 
{{ plan.name }} -
- {{ csrfInput() }} - {{ actionInput('commerce/subscriptions/switch') }} - {{ hiddenInput('subscriptionUid', subscription.uid|hash) }} - {{ hiddenInput('planUid', plan.uid|hash) }} - {{ redirectInput('[[folderName]]/plans') }} - - {{ plan.gateway.getSwitchPlansFormHtml(subscription.plan, plan)|raw }} - - {{ tag('button', { - type: 'submit', - class: '[[classes.btn.base]] [[classes.btn.mainColor]]', - text: 'Switch'|t - }) }} -
-
-
- {% endif %} - - {# @var payments \craft\commerce\models\subscriptions\SubscriptionPayment[] #} - {% set payments = subscription.getAllPayments() %} - {% if payments|length %} -

- {{- 'Payment History'|t -}} -

- - - - - - - - -
{{ 'Date'|t }}{{ 'Amount'|t }}
- - {% for payment in payments %} - - - {{ payment.paymentDate|date("Y-m-d H:i") }} - - - {{ payment.paymentCurrency }} {{ payment.paymentAmount }} - - - {% endfor %} - - {% endif %} -{% endblock %} diff --git a/example-templates/src/shop/plans/update-billing-details.twig b/example-templates/src/shop/plans/update-billing-details.twig deleted file mode 100755 index 9ca5b5b6cb..0000000000 --- a/example-templates/src/shop/plans/update-billing-details.twig +++ /dev/null @@ -1,55 +0,0 @@ -{% extends '[[folderName]]/_private/layouts' %} - -{# @var subscriptionUid string #} -{% set subscriptionUid = craft.app.request.getParam('subscription') %} -{# @var subscription \craft\commerce\elements\Subscription #} -{% set subscription = craft.subscriptions() - .status(null) - .uid(subscriptionUid) - .one() %} - -{% block main %} - - - {# @var currentUser \craft\elements\User #} - {% if currentUser is null or not subscriptionUid or not subscription %} - {% exit 404 %} - {% endif %} - - {% if subscription.subscriber.id != currentUser.id %} - {% exit 404 %} - {% endif %} - - {% if subscription.isExpired == true %} - {% exit 404 %} - {% endif %} - - {% if subscription.isCanceled == true %} - {% exit 404 %} - {% endif %} - - {% set planName = subscription.getPlan().name %} - -
-
- {% if subscription.isSuspended and subscription.hasBillingIssues %} -

- {{- 'Billing issue for subscription to {plan}'|t({ plan: planName }) -}} -

- -
{{ subscription.getBillingIssueDescription() }}
- -
-
- {{ redirectInput('[[folderName]]/plans') }} - {{ subscription.getBillingIssueResolveFormHtml()|raw }} -
-
- {% else %} -

- {{- 'No issues with subscription to {plan}'|t({ plan: planName }) -}} -

- {% endif %} -
-
-{% endblock %} diff --git a/lang/de/commerce.php b/lang/de/commerce.php new file mode 100644 index 0000000000..1900277d17 --- /dev/null +++ b/lang/de/commerce.php @@ -0,0 +1,1424 @@ + '(neuer Preis)', + '(of original price)' => '(vom ursprünglichen Preis)', + '(off original price)' => '(Nachlass vom ursprünglichen Preis)', + 'A cart number must be specified.' => 'Es muss eine Warenkorbnummer angegeben werden.', + 'A cart recovery link has been sent to {email}.' => 'Ein neuer Warenkorb-Wiederherstellungslink wurde an {email} gesendet.', + 'A cart recovery link will be sent to {email}.' => 'Ein Warenkorb-Wiederherstellungslink wird an {email} gesendet.', + 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Eine benutzerfreundliche Referenznummer auf Grundlage dieses Formats wird erstellt, sobald der Warenkorb abgeschlossen und die Artikel darin bestellt wurden. Beispielsweise {ex1} oder
{ex2}. Das Ergebnis dieses Formats muss einmalig sein.', + 'A new download link has been sent to {email}' => 'Ein neuer Download-Link wurde an {email} gesendet', + 'A new download link will be sent to {email}' => 'Ein neuer Download-Link wird an {email} gesendet', + 'A valid email is required to create a customer.' => 'Eine gültige E-Mail wird erfordert, um einen Kunden zu erstellen.', + 'Accept' => 'Akzeptieren', + 'Accepted' => 'Akzeptiert', + 'Actions' => 'Aktionen', + 'Active Carts' => 'Aktive Einkaufskörbe', + 'Active subscriptions' => 'Aktive Abonnements', + 'Active' => 'Aktiv', + 'Add Address' => 'Adresse hinzufügen', + 'Add a coupon' => 'Coupon hinzufügen', + 'Add a custom line item' => 'Benutzerdefinierten Einzelposten hinzufügen', + 'Add a line item' => 'Einen Einzelposten hinzufügen', + 'Add a product' => 'Ein Produkt hinzufügen', + 'Add a variant' => 'Variante hinzufügen', + 'Add an adjustment' => 'Eine Anpassung hinzufügen', + 'Add an item' => 'Posten hinzufügen', + 'Add an option' => 'Eine Option hinzufügen', + 'Add catalog price' => 'Katalogpreis hinzufügen', + 'Add' => 'Hinzufügen', + 'Additional Actions' => 'Zusätzliche Aktionen', + 'Additional recipients that should receive this email. Twig code can be used here.' => 'Zusätzliche Empfänger, die diese E-Mail erhalten sollen. Twig-Code kann hier verwendet werden.', + 'Address 1' => 'Adresse 1', + 'Address 2' => 'Adresse 2', + 'Address 3' => 'Adresse 3', + 'Address Line 1' => 'Adresszeile 1', + 'Address Line 2' => 'Adresszeile 2', + 'Address Updated.' => 'Adresse aktualisiert.', + 'Address copied to user.' => 'Adresse an Benutzer kopiert.', + 'Address not found.' => 'Adresse nicht gefunden.', + 'Adjust Quantity' => 'Menge anpassen', + 'Adjust by' => 'Anpassen durch', + 'Adjust price when included rate is disqualified?' => 'Preis anpassen, wenn der enthaltene Steuersatz nicht dafür qualifiziert ist?', + 'Adjustments' => 'Anpassungen', + 'Admin Notices' => 'Administratorhinweise', + 'Administrative Area Code of Origin' => 'Code des Verwaltungsgebiets', + 'Advanced' => 'Erweitert', + 'All Orders' => 'Alle Bestellungen', + 'All Totals' => 'Alle Summen', + 'All Transfers' => 'Alle Übertragungen', + 'All active subscriptions' => 'Alle aktive Abonnements', + 'All customers' => 'Alle Kunden', + 'All products' => 'Alle Produkte', + 'All variants must have a SKU.' => 'Alle Varianten müssen eine SKU haben.', + 'All' => 'Alle', + 'Allow Checkout Without Payment' => 'Bestellung ohne Zahlung erlauben', + 'Allow Empty Cart On Checkout' => 'Leeren Warenkorb an der Kasse erlauben', + 'Allow Partial Payment On Checkout' => 'Teilzahlung an der Kasse erlauben', + 'Allow out of stock purchases' => 'Erlauben Sie Käufe von nicht vorrätigen Artikeln', + 'Allow' => 'Zulassen', + 'Allowed Qty' => 'Höchstmenge', + 'Alternative Phone' => 'Alternative Telefonnummer', + 'Amount' => 'Menge', + 'An ID must be provided' => 'Ein Ausweis muss beigestellt werden', + 'An error occurred while generating this PDF.' => 'Beim Erstellen dieser PDF ist ein Fehler aufgetreten.', + 'Any' => 'Beliebig', + 'Anywhere' => 'Überall', + 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Sind Sie sicher, dass Sie den Abonnementplan „{name}“ wirklich archivieren möchten? Dadurch werden die bestehenden Abonnements NICHT gekündigt.', + 'Are you sure you want to capture this transaction?' => 'Sind Sie sicher, dass Sie diese Transaktion erfassen möchten?', + 'Are you sure you want to complete this order?' => 'Sind Sie sicher, dass Sie diese Bestellung abschließen möchten?', + 'Are you sure you want to delete the selected orders?' => 'Sind Sie sicher, dass Sie die ausgewählten Bestellungen löschen möchten?', + 'Are you sure you want to delete the selected product and its variants?' => 'Sind Sie sicher, dass Sie die ausgewählten Produkte und ihre Varianten löschen möchten?', + 'Are you sure you want to delete this shipping rule?' => 'Möchten Sie diese Versandregel wirklich löschen?', + 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Sind Sie sicher, dass Sie "{name}" und alle seine Produkte löschen möchten? Bitte vergewissern Sie sich, dass Sie eine Sicherungskopie Ihrer Datenbank besitzen, bevor Sie diese Löschaktion ausführen.', + 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Sind Sie sicher, dass Sie „{name}“ löschen möchten? Dies wird alle Einzelposten mit diesem Status zu „Kein Status“ umstellen.', + 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Sind Sie sicher, dass Sie diese Übertragung als ausstehend markieren möchten? Sie wird am Zielort als eingehend angezeigt.', + 'Are you sure you want to overwrite the billing address?' => 'Sind Sie sicher, dass Sie die Rechnungsadresse überschreiben möchten?', + 'Are you sure you want to overwrite the shipping address?' => 'Sind Sie sicher, dass Sie die Lieferadresse überschreiben möchten?', + 'Are you sure you want to permanently delete this store and everything in it?' => 'Möchten Sie diesen Shop und alle darin enthaltenen Artikel wirklich dauerhaft löschen?', + 'Are you sure you want to refund this transaction?' => 'Sind Sie sicher, dass Sie diese Transaktion erstatten möchten?', + 'Are you sure you want to remove this customer?' => 'Sind Sie sicher, dass Sie diesen Kunden entfernen möchten?', + 'Are you sure you want to save this as a new shipping rule?' => 'Möchten Sie dies wirklich als eine neue Versandregel speichern?', + 'Are you sure you want to send email: {name}?' => 'Sind Sie sicher dass Sie die E-Mail „{name}“ versenden möchten?', + 'At least one site must be enabled for the product type.' => 'Mindestens eine Website muss für den Produkttyp aktiviert sein.', + 'Attempted Payments' => 'Versuchte Zahlungen', + 'Attention' => 'Achtung', + 'Authorize Only (Manually Capture)' => 'Nur autorisieren (Manuelle Erfassung)', + 'Auto Set Cart Shipping Method Option' => 'Versandart des Warenkorbs autom. setzen', + 'Auto Set New Cart Addresses' => 'Neue Warenkorbadressen autom. setzen', + 'Auto Set Payment Source' => 'Zahlungsquelle autom. setzen', + 'Automatic SKU Format' => 'Automatisches Format für die Bestandseinheit (SKU)', + 'Available Shipping Categories' => 'Verfügbare Versandkategorien', + 'Available Tax Categories' => 'Verfügbare Steuerkategorien', + 'Available for purchase' => 'Für den Kauf verfügbar', + 'Available for purchase?' => 'Für den Kauf verfügbar?', + 'Available inventory for "{description}" has gone below zero.' => 'Der verfügbare Lagerbestand für „{description}“ ist unter null gesunken.', + 'Available to Product Types' => 'Für Produkttypen verfügbar', + 'Available' => 'Verfügbar', + 'Available?' => 'Verfügbar?', + 'Average Order Total' => 'Durchschnittlicher Bestellungspreis', + 'Average' => 'Durchschnitt', + 'BCC’d Recipient' => 'Empfänger auf BCC gesetzt', + 'Bad Request' => 'Ungültige Anfrage', + 'Bad address ID.' => 'Ungültige Lieferadressen-ID.', + 'Bad order ID.' => 'Ungültige Bestellungs-ID.', + 'Base Price' => 'Grundpreis', + 'Base Promotional Price' => 'Grundpreis der Aktion', + 'Base Rate' => 'Basissatz', + 'Base' => 'Basis', + 'Bcc' => 'BCC', + 'Billing Address' => 'Rechnungsadresse', + 'Billing Business Name' => 'Geschäftsname (Rechnung)', + 'Billing First Name' => 'Vorname (Rechnung)', + 'Billing Full Name' => 'Vollständiger Name (Rechnung)', + 'Billing Last Name' => 'Nachname (Rechnung)', + 'Billing address required.' => 'Rechnungsadresse erforderlich.', + 'Billing detail update URL' => 'URL zum Aktualisieren der Zahlungsinformationen', + 'Billing issues' => 'Rechnungprobleme', + 'Billing' => 'Abrechnung', + 'Both (Line item price + Line item shipping costs)' => 'Beide (Einzelpostenpries + Einzelposten-Versandkosten)', + 'Business ID' => 'Unternehmens-ID', + 'Business Name' => 'Name des Unternehmens', + 'Business Tax ID' => 'Gewerbesteuer-ID', + 'CC’d Recipient' => 'Empfänger auf CC gesetzt', + 'CVV' => 'CVC', + 'Can be used as an internal reference.' => 'Kann als interne Referenz verwendet werden.', + 'Can not complete payment for missing transaction.' => 'Zahlung für fehlende Transaktion konnte nicht abgeschlossen werden.', + 'Can not create a new order' => 'Neue Bestellung konnte nicht erstellt werden', + 'Can not find an order to pay.' => 'Konnte keine zu Bestellung zum Bezahlen finden.', + 'Can not find enabled email.' => 'Es wurde keine aktivierte E-Mail-Addresse gefunden.', + 'Can not find order' => 'Bestellung konnte nicht gefunden werden', + 'Can not find order.' => 'Bestellung konnte nicht gefunden werden.', + 'Can not find the transaction to refund' => 'Die zu erstattende Transaktion konnte nicht gefunden werden', + 'Can not move between these inventory types.' => 'Sie können nicht zwischen diesen Bestandstypen wechseln.', + 'Can not refund amount greater than the remaining amount' => 'Es kann kein Betrag über dem Restbetrag zurückerstattet werden', + 'Cancel subscription' => 'Abonnement stornieren', + 'Cancel with gateway now' => 'Jetzt über das Zahlungsportal stornieren', + 'Cancel' => 'Abbrechen', + 'Cancellation date' => 'Stornierungsdatum', + 'Cancellation' => 'Stornierung', + 'Cannot switch plans for this subscription.' => 'Pläne für dieses Abonnement können nicht gewechselt werden.', + 'Can’t preview this email.' => 'E-Mail-Vorschau für diese E-Mail nicht verfügbar.', + 'Capture payment' => 'Zahlung erfassen', + 'Capture' => 'Erfassen', + 'Card Holder' => 'Karteninhaber', + 'Card Number' => 'Kartennummer', + 'Card' => 'Karte', + 'Cart Recovery Link' => 'Warenkorb-Wiederherstellungslink', + 'Cart forgotten.' => 'Warenkorb vergessen.', + 'Cart updated.' => 'Warenkorb aktualisiert.', + 'Cart {number}' => 'Warenkorb {number}', + 'Catalog Pricing Rule' => 'Katalogpreisregel', + 'Catalog pricing rule description.' => 'Beschreibung der Katalogpreisregel.', + 'Catalog pricing rule saved.' => 'Katalogpreisregel gespeichert.', + 'Catalog pricing rules deleted.' => 'Katalogpreisregeln gelöscht.', + 'Catalog pricing rules updated.' => 'Katalogpreisregeln aktualisiert.', + 'Categories Relationship Type' => 'Kategorien Beziehungstyp', + 'Categories' => 'Kategorien', + 'Category Rate Overrides' => 'Preiskategorie-Overrides', + 'Centimeters (cm)' => 'Zentimeter (cm)', + 'Changing this value may affect your ability to refund existing transactions.' => 'Das Ändern dieses Wertes könnte Ihre Fähigkeit, bestehende Transaktionen zurück zu erstatten, beeinflussen.', + 'Choose a color to represent the order’s status' => 'Wählen Sie eine Farbe, die den Status der Bestellung repräsentieren soll', + 'Choose a new customer' => 'Neuen Kunden wählen', + 'Choose adjustment values to include when calculating the product revenue total.' => 'Wählen Sie Anpassungswerte, die bei der Berechnung der Summe der Produktumsätze berücksichtigt werden sollen.', + 'Choose the currency’s ISO code.' => 'Wählen Sie den ISO-Code der Währung aus.', + 'Choose the destination inventory location for the existing on hand stock.' => 'Wählen Sie den Ziellagerbestand für den vorhandenen Lagerbestand.', + 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Wählen Sie, in welchen Websites dieser Produkttyp verfügbar sein soll, und konfigurieren Sie die sitespezifischen Einstellungen.', + 'City' => 'Stadt', + 'Clear counter' => 'Zähler zurücksetzen', + 'Clear notices' => 'Anmerkungen entfernen', + 'Close' => 'Schließen', + 'Code' => 'Code', + 'Collated PDF' => 'Zusammengestellte PDF', + 'Color' => 'Farbe', + 'Commerce Products' => 'Commerce Produkte', + 'Commerce Settings' => 'Commerce Einstellungen', + 'Commerce Variants' => 'Commerce-Varianten', + 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Die Commerce E-Mail "{email}" konnte nicht für die Bestellung "{order}" gesendet werden.', + 'Commerce order exports' => 'Exporte von Commerce Bestellungen', + 'Commerce' => 'Commerce', + 'Committed' => 'Zugewiesen', + 'Completed Email' => 'Abgeschlossene E-Mail-Adresse', + 'Completed' => 'Abgeschlossen', + 'Completing order failed.' => 'Bestellungsabschluss fehlgeschlagen.', + 'Condition' => 'Bedingung', + 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Die Bedingungen werden hier mit einer Bestellung abgeglichen, bevor die Regeln abgesucht werden. Dies ist nützlich, wenn Sie die Verfügbarkeit einer Methode frühzeitig prüfen möchten oder wenn es gemeinsame Bedingungen für alle Regeln für diese Methode gibt.', + 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Die Bedingungen werden hier mit denen des Kunden abgeglichen, bevor die Regeln abgesucht werden. Dies ist nützlich, wenn Sie die Verfügbarkeit einer Methode frühzeitig prüfen möchten oder wenn es gemeinsame Bedingungen für alle Regeln für diese Methode gibt.', + 'Conditions' => 'Bedingungen', + 'Contains Purchasables' => 'Enthält kaufbare Artikel', + 'Control Panel Settings' => 'Control Panel Einstellungen', + 'Control panel' => 'Control Panel', + 'Conversion Rate' => 'Wechselkurs', + 'Converted Price' => 'Umgerechneter Preis', + 'Copied!' => 'Kopiert!', + 'Copy the URL' => 'Die URL kopieren', + 'Copy to {location}' => 'Zu {location} kopieren', + 'Copy' => 'Kopieren', + 'Costs' => 'Kosten', + 'Could not archive gateway.' => 'Gateway konnte nicht archiviert werden.', + 'Could not cancel “{reference}”.' => '"{reference}" konnte nicht storniert werden.', + 'Could not create the payment source.' => 'Die Zahlungsquelle konnte nicht erstellt werden.', + 'Could not delete shipping rule' => 'Lieferregel konnte nicht gelöscht werden', + 'Could not delete shipping zone' => 'Lieferzone konnte nicht gelöscht werden', + 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Fehler beim Löschen von {count, number} {count, plural, one{Versandkategorie} other{Versandkategorien}}.', + 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Fehler beim Löschen von {count, number} {count, plural, one{Versandmethode} other{Versandmethoden}} und Regeln.', + 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Fehler beim Löschen von {count, number} {count, plural, one{Steuerkategorie} other{Steuerkategorien}}.', + 'Could not find the email or template.' => 'E-Mail oder Vorlage konnte nicht gefunden werden.', + 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Bestellung {number} konnte nicht als abgeschlossen markiert werden. Speichern der Bestellung ist beim Bestellungsabschluss aufgrund von Fehlern gescheitert: {order}', + 'Could not reactivate “{reference}”.' => '"{reference}" konnte nicht reaktiviert werden.', + 'Could not send email' => 'E-Mail konnte nicht versendet werden', + 'Could not switch “{reference}” to “{plan}”.' => '"{reference}" konnte nicht zu "{plan}" gewechselt werden.', + 'Could not update orders address.' => 'Bestellungsadresse konnte nicht aktualisiert werden.', + 'Couldn’t archive Line Item Status.' => 'Einzelpostenstatus konnte nicht archiviert werden.', + 'Couldn’t archive Order Status.' => 'Bestellungsstatus konnte nicht archiviert werden.', + 'Couldn’t capture transaction.' => 'Transaktion konnte nicht erfasst werden.', + 'Couldn’t capture transaction: {message}' => 'Die Transaktion konnte nicht erfasst werden: {message}', + 'Couldn’t delete email.' => 'E-Mail konnte nicht gelöscht werden.', + 'Couldn’t delete the payment source.' => 'Die Zahlungsquelle konnte nicht gelöscht werden.', + 'Couldn’t get order.' => 'Bestellung konnte nicht empfangen werden.', + 'Couldn’t recalculate order.' => 'Konnte Bestellung nicht erneut berechnen.', + 'Couldn’t refund transaction.' => 'Transaktion konnte nicht erstattet werden.', + 'Couldn’t refund transaction: {message}' => 'Konnte die Transaktion nicht erstatten: {message}', + 'Couldn’t reorder Line Item Statuses.' => 'Status von Einzelposten konnten nicht neu sortiert werden.', + 'Couldn’t reorder Order Statuses.' => 'Die Bestellstatus konnten nicht neu sortiert werden.', + 'Couldn’t reorder PDFs.' => 'PDFs konnten nicht neu sortiert werden.', + 'Couldn’t reorder discounts.' => 'Die Rabatte konnten nicht neu sortiert werden.', + 'Couldn’t reorder gateways.' => 'Gateways konnten nicht neu sortiert werden.', + 'Couldn’t reorder plans.' => 'Pläne konnten nicht neu geordnet werden.', + 'Couldn’t reorder rules.' => 'Regeln konnten nicht umsortiert werden.', + 'Couldn’t reorder sale.' => 'Aktion konnte nicht umsortiert werden.', + 'Couldn’t reorder sales.' => 'Aktionen konnten nicht umsortiert werden.', + 'Couldn’t reorder statuses.' => 'Status konnten nicht umsortiert werden.', + 'Couldn’t reorder stores.' => 'Shops konnten nicht umsortiert werden.', + 'Couldn’t save PDF.' => 'PDF konnte nicht gespeichert werden.', + 'Couldn’t save catalog pricing rule.' => 'Katalogpreisregel konnte nicht gespeichert werden.', + 'Couldn’t save currency.' => 'Währung konnte nicht gespeichert werden.', + 'Couldn’t save discount.' => 'Rabatt konnte nicht gespeichert werden.', + 'Couldn’t save email.' => 'E-Mail konnte nicht gespeichert werden.', + 'Couldn’t save gateway.' => 'Gateway konnte nicht gespeichert werden.', + 'Couldn’t save inventory location.' => 'Lagerbestandsort konnte nicht gespeichert werden.', + 'Couldn’t save line item status.' => 'Einzelpostenstatus konnte nicht gespeichert werden.', + 'Couldn’t save order fields.' => 'Bestellfelder konnten nicht gespeichert werden.', + 'Couldn’t save order status.' => 'Bestellstatus konnte nicht gespeichert werden.', + 'Couldn’t save order.' => 'Die Bestellung konnte nicht gespeichert werden.', + 'Couldn’t save product type.' => 'Produkttyp konnte nicht gespeichert werden.', + 'Couldn’t save sale.' => 'Aktion konnte nicht gespeichert werden.', + 'Couldn’t save settings.' => 'Einstellungen konnten nicht gespeichert werden.', + 'Couldn’t save shipping category.' => 'Die Versandkategorie konnte nicht gespeichert werden.', + 'Couldn’t save shipping method.' => 'Versandart konnte nicht gespeichert werden.', + 'Couldn’t save shipping rule.' => 'Versandregel konnte nicht gespeichert werden.', + 'Couldn’t save shipping zone.' => 'Versandzone konnte nicht gespeichert werden.', + 'Couldn’t save store.' => 'Shop konnte nicht gespeichert werden.', + 'Couldn’t save subscription fields.' => 'Abonnementfelder konnten nicht gespeichert werden.', + 'Couldn’t save subscription plan.' => 'Abonnementplan konnte nicht gespeichert werden.', + 'Couldn’t save subscription.' => 'Abonnement konnte nicht gespeichert werden.', + 'Couldn’t save tax category.' => 'Steuerklasse konnte nicht gespeichert werden.', + 'Couldn’t save tax rate.' => 'Steuersatz konnte nicht gespeichert werden.', + 'Couldn’t save tax zone.' => 'Steuerzone konnte nicht gespeichert werden.', + 'Couldn’t save transfer fields.' => 'Übertragungsfelder konnten nicht gespeichert werden.', + 'Couldn’t update catalog pricing rule statuses.' => 'Status der Katalogpreisregeln konnte nicht aktualisiert werden.', + 'Couldn’t update status.' => 'Status konnte nicht aktualisiert werden.', + 'Couldn’t updated sales status.' => 'Aktionsstatus konnte nicht aktualisiert werden.', + 'Country Code of Origin' => 'Land – Ursprünglicher Code', + 'Country List' => 'Länderliste', + 'Country not allowed.' => 'Land nicht erlaubt.', + 'Country' => 'Land', + 'Coupon Code' => 'Coupon-Code', + 'Coupon can not apply discount to this order due to address mismatch.' => 'Der Coupon kann aufgrund einer Adressabweichung keinen Rabatt auf diese Bestellung anwenden.', + 'Coupon can not apply discount to this order due to customer mismatch.' => 'Der Coupon kann aufgrund einer Unstimmigkeit seitens des Kunden keinen Rabatt auf diese Bestellung gewähren.', + 'Coupon can not apply discount to this order.' => 'Der Coupon kann keinen Rabatt auf diese Bestellung anwenden.', + 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Gutscheincode “{code}” wird bereits vom Rabatt “{name}” verwendet.', + 'Coupon codes cannot be blank.' => 'Coupon-Codes dürfen nicht leer sein.', + 'Coupon codes must be unique.' => 'Coupon-Codes müssen einzigartig sein.', + 'Coupon format is required and must contain at least one `#`.' => 'Das Coupon-Format ist erforderlich und muss mindestens ein `#` enthalten.', + 'Coupon not valid.' => 'Coupon ungültig.', + 'Coupon removed: {explanation}' => 'Coupon entfernt: {explanation}', + 'Coupons' => 'Coupons', + 'Craft Commerce - Administration' => 'Craft Commerce - Administration', + 'Craft Commerce - Inventory' => 'Craft Commerce - Lagerbestand', + 'Craft Commerce - Orders' => 'Craft Commerce - Bestellungen', + 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Produkttyp - {name}', + 'Craft Commerce - Subscriptions' => 'Craft Commerce - Abonnements', + 'Create a Discount' => 'Rabatt erstellen', + 'Create a Subscription Plan' => 'Einen Abonnementplan erstellen', + 'Create a new PDF' => 'Neue PDF erstellen', + 'Create a new catalog pricing rule' => 'Neue Katalogpreisregel erstellen', + 'Create a new currency' => 'Neue Währung erstellen', + 'Create a new email' => 'Eine neue E-Mail erstellen', + 'Create a new gateway' => 'Ein neues Gateway erstellen', + 'Create a new line item status' => 'Neuen Einzelpostenstatus erstellen', + 'Create a new order status' => 'Neuen Bestellstatus anlegen', + 'Create a new product type' => 'Neuen Produkttyp erstellen', + 'Create a new sale' => 'Eine neue Aktion erstellen', + 'Create a new shipping category' => 'Eine neue Versandkategorie erstellen', + 'Create a new shipping method' => 'Eine neue Versandart anlegen', + 'Create a new shipping rule' => 'Eine neue Versandregel erstellen', + 'Create a new tax category' => 'Eine neue Steuerklasse anlegen.', + 'Create a new tax rate' => 'Neuen Steuersatz anlegen', + 'Create a product type' => 'Einen Produkttyp erstellen', + 'Create a shipping zone' => 'Versandzone erstellen', + 'Create a tax zone' => 'Eine Steuerzone anlegen', + 'Create catalog pricing rules' => 'Katalogpreisregeln erstellen', + 'Create customer: “{email}”' => 'Erstelle Kunde: „{email}“', + 'Create discounts' => 'Rabatte erstellen', + 'Create discount…' => 'Rabatt erstellen…', + 'Create rules that allow this discount to match the order.' => 'Erstellen Sie Regeln, die diesen Rabatt für die Bestellung zugehörig machen.', + 'Create rules that allow this discount to match the order’s billing address.' => 'Erstellen Sie Regeln, die diesen Rabatt für die Bestelladresse zugehörig machen.', + 'Create rules that allow this discount to match the order’s customer.' => 'Erstellen Sie Regeln, die diesen Rabatt für den Bestellkunden zugehörig machen.', + 'Create rules that allow this discount to match the order’s shipping address.' => 'Erstellen Sie Regeln, die diesen Rabatt für die Versandadresse zugehörig machen.', + 'Create rules that allow this gateway to match the billing address.' => 'Erstellen Sie Regeln, die es diesem Gateway ermöglichen, die Rechnungsadresse zuzuordnen.', + 'Create rules that allow this gateway to match the order.' => 'Erstellen Sie Regeln, die es diesem Gateway ermöglichen, die Bestellung zuzuordnen.', + 'Create rules that allow this gateway to match the shipping address.' => 'Erstellen Sie Regeln, die es diesem Gateway ermöglichen, die Versandadresse zuzuordnen.', + 'Create sales' => 'Aktionen erstellen', + 'Create sale…' => 'Aktion erstellen…', + 'Created' => 'Erstellt', + 'Credit Card Payment Type' => 'Zahlungsmethode Kreditkarte', + 'Currency Code' => 'Währungs-Code', + 'Currency saved.' => 'Währung gespeichert.', + 'Currency' => 'Währung', + 'Current' => 'laufende Rechnung', + 'Custom 1' => 'Benutzerdefiniert 1', + 'Custom 2' => 'Benutzerdefiniert 2', + 'Custom 3' => 'Benutzerdefiniert 3', + 'Custom 4' => 'Benutzerdefiniert 4', + 'Custom' => 'Benutzerdefiniert', + 'Customer Enabled?' => 'Kunde freigegeben?', + 'Customer ID is required.' => 'Kunden-ID erforderlich.', + 'Customer Note' => 'Kundenanmerkung', + 'Customer Notices' => 'Kundenanmerkungen', + 'Customer data' => 'Kundendaten', + 'Customer' => 'Kunde', + 'Damaged' => 'Beschädigt', + 'Data shown might be outdated.' => 'Die angezeigten Daten können veraltet sein.', + 'Date Authorized' => 'Autorisierungsdatum', + 'Date Created' => 'Erstellungsdatum', + 'Date First Paid' => 'Erstes Zahlungsdatum', + 'Date Ordered' => 'Bestelldatum', + 'Date Paid' => 'Bezahldatum', + 'Date Updated' => 'Aktualisierungsdatum', + 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Datum, an dem die Katalogpreisregel aktiviert wird. Keine Angabe für unbegrenztes Startdatum', + 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Datum, an dem der Rabatt aktiv wird. Keine Eingabe, wenn das Startdatum nicht begrenzt ist', + 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Datum, an dem die Aktion aktiviert wird. Keine Angabe für unbegrenztes Startdatum', + 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Datum, an dem die Katalogpreisregel beendet wird. Keine Eingabe, wenn die Aktion unbefristet läuft', + 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Datum, zu dem die Rabattaktion beendet sein wird. Keine Eingabe bei unbefristeter Aktion.', + 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Datum, an dem die Aktion beendet wird. Keine Eingabe, wenn die Aktion unbefristet läuft', + 'Date' => 'Datum', + 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Standard - Erlaubt es dem Preis, negativ zu sein, wenn Rabatte größer als der Wert der Bestellung sind.', + 'Default Category' => 'Standardkategorie', + 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Die Standardansicht des Commerce Control Panels. Wenn der Benutzer keine Zugangsrechte besitzt, wird auf eine Seite zurückgegriffen, auf die er Zugriff hat.', + 'Default Order PDF' => 'Standard Bestell-PDF', + 'Default Per Item Rate' => 'Standard Pro Artikel-Satz', + 'Default Percentage Rate' => 'Standardprozentsatz', + 'Default Status?' => 'Standardstatus?', + 'Default View' => 'Standardansicht', + 'Default Weight Rate' => 'Standard Gewichtspreis', + 'Default Zone' => 'Standardzone', + 'Default status?' => 'Standardstatus?', + 'Default to this tax zone when no billing address is set' => 'Standardmäßig auf diese Steuerzone festlegen, wenn keine Rechnungsadresse angegeben wurde', + 'Default to this tax zone when no shipping address is set' => 'Diese Steuerzone als Standard wählen, wenn keine Versandadresse angegeben wurde', + 'Default variant updated.' => 'Standardvariante aktualisiert.', + 'Default' => 'Standard', + 'Default?' => 'Standard?', + 'Delete catalog pricing rules' => 'Katalogpreisregeln löschen', + 'Delete discounts' => 'Rabatte löschen', + 'Delete orders' => 'Bestellungen löschen', + 'Delete sales' => 'Aktionen löschen', + 'Delete' => 'Löschen', + 'Deleting the {location} location.' => 'Löschen des Ortes {location}.', + 'Describe this rule.' => 'Diese Regel beschreiben.', + 'Describe this shipping zone.' => 'Beschreiben Sie diese Versandzone.', + 'Describe this tax zone.' => 'Beschreiben Sie diese Steuerzone.', + 'Description' => 'Beschreibung', + 'Destination Inventory Location' => 'Ziellagerbestandsort', + 'Destination' => 'Zielort', + 'Details' => 'Details', + 'Dimension Unit' => 'Maßeinheit', + 'Dimensions' => 'Abmessungen', + 'Disabled' => 'Deaktiviert', + 'Disallow' => 'Verbieten', + 'Discount all line items' => 'Alle Einzelposten rabattieren', + 'Discount description.' => 'Beschreibung des Rabatts.', + 'Discount is not allowed for the order' => 'Der Rabatt ist für die Bestellung nicht zugelassen', + 'Discount is out of date.' => 'Rabatt ist veraltet.', + 'Discount saved.' => 'Rabatt gespeichert.', + 'Discount the matching items only' => 'Nur die entsprechenden Artikel rabattieren', + 'Discount use has reached its limit.' => 'Die Nutzungsgrenze des Rabattes wurde erreicht.', + 'Discount' => 'Rabatt', + 'Discounted Item Subtotal' => 'Zwischensumme rabattierter Artikel', + 'Discounted Items' => 'Ermäßigte Artikel', + 'Discounts deleted.' => 'Rabatte entfernt.', + 'Discounts reordered.' => 'Rabatte umsortiert.', + 'Discounts updated.' => 'Rabatte aktualisiert.', + 'Discounts' => 'Rabatte', + 'Disqualify with valid business tax ID?' => 'Mit gültiger Gewerbesteueridentifikationsnummer disqualifizieren?', + 'Do not apply subsequent matching sales beyond applying this sale.' => 'Nach dem Anwenden dieses Verkaufs keine weiteren passenden Verkäufe mehr suchen.', + 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Wenden Sie diesen Steuersatz nicht an, wenn die Bestelladresse eine der ausgewählten gültigen Gewerbesteueridentifikationsnummern hat.', + 'Do not attach a PDF to this email' => 'Keine PDF an diese E-Mail anhängen', + 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Recalculate bei dieser Bestellung (Nummer: {orderNumber}) nicht aufrufen, wenn Fehler vorhanden sind.', + 'Donation can not be zero.' => 'Spendenbetrag kann nicht null sein.', + 'Donation needs to be an amount.' => 'Spenden müssen ein Betrag sein.', + 'Donation settings saved.' => 'Spendeneinstellungen gespeichert.', + 'Donation' => 'Spende', + 'Donations' => 'Spenden', + 'Done' => 'Fertig', + 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Keine nachfolgenden Rabatte bei einer Bestellung anwenden, wenn dieser Rabatt angewendet wird', + 'Download PDF' => 'PDF herunterladen', + 'Download PDF…' => 'PDF herunterladen …', + 'Download Type' => 'Art des Downloads', + 'Download' => 'Herunterladen', + 'Draft' => 'Entwurf', + 'Dummy gateway payment failed.' => 'Platzhalter Gateway Zahlung fehlgeschlagen.', + 'Duplicate options exist' => 'Es existieren Duplikate in den Optionen', + 'Duration' => 'Dauer', + 'EU VAT ID' => 'EU-STEUERNUMMER', + 'Edit address' => 'Adresse bearbeiten', + 'Edit adjustments' => 'Anpassungen bearbeiten', + 'Edit catalog pricing rules' => 'Katalogpreisregeln bearbeiten', + 'Edit discounts' => 'Rabatte bearbeiten', + 'Edit options' => 'Optionen bearbeiten', + 'Edit orders' => 'Bestellungen bearbeiten', + 'Edit sales' => 'Aktionen bearbeiten', + 'Edit' => 'Bearbeiten', + 'Effect' => 'Ausführen', + 'Either (Default) - The relationship field is on the purchasable or the category' => 'Beide (Standard) - Das Beziehungsfeld ist bei der Kaufoption oder Kategorie', + 'Either way' => 'Egal', + 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'E-Mail PDF Generierungsfehler für E-Mail "{email}". Bestellung: "{order}". PDF-Vorlagenfehler: "{message}" {file}:{line}', + 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'Die E-Mail PDF-Vorlage unter "{templatePath}" ist nicht für die E-Mail "{email}" vorhanden. Bestellung: "{order}".', + 'Email Subject' => 'E-Mail-Betreff', + 'Email error. No email address found for order. Order: “{order}”' => 'E-Mail-Fehler. Für die folgende Bestellung wurde keine E-Mail-Adresse gefunden. Bestellung: "{order}"', + 'Email is not enabled.' => 'Die E-Mail ist nicht aktiviert.', + 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Die Klartext-E-Mail-Vorlage unter "{templatePath}", die zu "{templateParsedPath}" für die E-Mail "{email}" führte ist nicht vorhanden. Bestellung: "{order}".', + 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parsingfehler der Klartext E-Mail Vorlage für E-Mail "{email}" Bestellung "{order}". Vorlagenfehler: "{message}" {file}:{line}', + 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Pfad-Parsingfehler der E-Mail Klartext-Vorlage für E-Mail "{email}" in "Vorlagenpfad". Bestellung "{order}". Vorlagenfehler: "{message}" {file}:{line}', + 'Email required to make payments on a completed order.' => 'Sie benötigen eine E-Mail-Adresse, um Zahlungen für eine abgeschlossene Bestellung vorzunehmen.', + 'Email saved.' => 'E-Mail gespeichert.', + 'Email sent' => 'E-Mail gesendet', + 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Die E-Mail-Vorlage unter "{templatePath}", die zu "{templateParsedPath}" für die E-Mail „{email}“ führte, ist nicht vorhanden. Bestellung: "{order}".', + 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parsingfehler in der E-Mail Vorlage für benutzerdefinierte E-Mail "{email}" in "An:". Bestellung "{order}". Vorlagenfehler: "{message}" {file}:{line}', + 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-Mail Vorlagen-Parserfehler für E-Mail "{email}" in "BCC:". Bestellung: "{order}". Vorlagenfehler: "{message}" {file}:{line}', + 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-Mail-Vorlagen Parserfehler für E-Mail "{email}" in "CC:". Bestellung: "{order}". Vorlagenfehler: "{message}" {file}:{line}', + 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-Mail-Vorlagen Parserfehler für E-Mail "{email}" in "Antwort an:". Bestellung: "{order}". Vorlagenfehler: "{message}" {file}:{line}', + 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-Mail-Vorlagen Parserfehler für E-Mail "{email}" in "Betreff:". Bestellung: "{order}". Vorlagenfehler: "{message}" {file}:{line}', + 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-Mail-Vorlage Parsingfehler für E-Mail "{email}" Bestellung "{order}". Vorlagenfehler: "{message}" {file}:{line}', + 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Pfad-Parsingfehler bei der E-Mail Vorlage für E-Mail "{email}" in "Vorlagenpfad". Bestellung "{order}". Vorlagenfehler: "{message}" {file}:{line}', + 'Email unavailable.' => 'E-Mail nicht verfügbar.', + 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'E-Mail "{email}" konnte nicht für die Bestellung "{order}" versendet werden. Fehler: {error} {file}:{line}', + 'Email “{email}” for order {order} was cancelled.' => 'E-Mail "{email}", für die Bestellung "{order}" wurde abgebrochen.', + 'Email' => 'E-Mail', + 'Emails' => 'E-Mails', + 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Aktivieren Sie diese Option, wenn dieser Steuersatz in den steuerpflichtigen Einzelpreis einkalkuliert werden soll, anstatt der Bestellung zusätzliche Kosten hinzuzufügen.', + 'Enable structure for products of this type' => 'Struktur für Produkte dieser Art aktivieren', + 'Enable this discount' => 'Diesen Rabatt aktivieren', + 'Enable this rule' => 'Diese Regel aktivieren', + 'Enable this sale' => 'Diese Aktion aktivieren', + 'Enable this shipping method on the front end' => 'Diese Versandart auf dem Frontend verfügbar machen', + 'Enable this shipping rule' => 'Die Versandart aktivieren', + 'Enable this tax rate' => 'Diesen Steuersatz aktivieren', + 'Enabled for customers to select during checkout?' => 'Ist den Kunden die Auswahl an der Kasse gestattet?', + 'Enabled for customers to select?' => 'Wurde die Auswahl für Kunden aktiviert?', + 'Enabled' => 'Aktiviert', + 'Enabled?' => 'Aktiviert?', + 'End Date' => 'Enddatum', + 'Enter SKU' => 'Bestandseinheit (SKU) eingeben', + 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Geben Sie einen verständlichen Namen für diesen Steuersatz ein, der im Control Panel verwendet werden soll.', + 'Enter a percentage like {ex1} or {ex2}.' => 'Geben Sie einen Prozentsatz ein, z. B. {ex1} oder {ex2}.', + 'Enter coupon code' => 'Gutscheincode eingeben', + 'Enter reference' => 'Referenz eingeben', + 'Error refunding transaction: {transactionHash}' => 'Fehler bei der Zurückerstattung der Transaktion: {transactionHash}', + 'Every new store must be assigned to at least one site.' => 'Jeder neue Shop muss mindestens einer Website zugeordnet werden.', + 'Everywhere' => 'Überall', + 'Example' => 'Beispiel', + 'Exclude this discount for products that are already on promotion' => 'Diesen Rabatt nicht auf Produkte anwenden, die bereits Teil einer Aktion sind', + 'Expired Link' => 'Abgelaufener Link', + 'Expired' => 'Abgelaufen', + 'Expiry Date' => 'Ablaufdatum', + 'Expiry date' => 'Ablaufdatum', + 'Expiry' => 'Verfall', + 'Failed to receive transfer: {error}' => 'Übertragung nicht empfangen: {error}', + 'Failed to send email. Please try again.' => 'E-Mail kann nicht gesendet werden. Bitte versuchen Sie es erneut.', + 'Failed to start' => 'Konnte nicht gestartet werden', + 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Fehler beim Aktualisieren {num, plural, =1{des Bestellstatus} other{der Bestellstatus}}.', + 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Fehler beim Aktualisieren des Bestellstatus der {num, plural, =1{Bestellung} other{Bestellungen}}.', + 'Feet (ft)' => 'Fuß (ft)', + 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Filterbedingungen zur Beschreibung, auf welche Bestellungen diese Regel angewandt werden kann. 0 eingeben, um eine Bedingung auszulassen.', + 'First Name' => 'Vorname', + 'Flat Amount Off Order' => 'Pauschalbetrag abgezogen von der Bestellung', + 'Flat Order Discount Amount Off' => 'Pauschaler Bestellungsrabattbetrag', + 'Free Order Payment Strategy' => 'Zahlungsstrategie für kostenlose Bestellungen', + 'Free Shipping' => 'Kostenloser Versand', + 'Free orders are processed by the payment gateway' => 'Kostenlose Bestellungen werden vom Zahlungsgateway verarbeitet', + 'Free orders complete immediately' => 'Kostenlose Bestellungen schließen sofort ab', + 'Free shipping can only be for whole order or matching items, not both.' => 'Kostenloser Versand kann nur auf die gesamte Bestellung oder auf passende Artikel angewendet werden, nicht auf beides gleichzeitig.', + 'From Name' => 'Absender', + 'Fulfill' => 'Erfüllen', + 'Fulfilled' => 'Erfüllt', + 'Fulfillment' => 'Erfüllung', + 'Full Name' => 'Vollständiger Name', + 'Gateway Code' => 'Gateway Code', + 'Gateway Message' => 'Gatewaynachricht', + 'Gateway Reference' => 'Gateway Referenz', + 'Gateway Response' => 'Antwort des Gateways', + 'Gateway doesn’t support authorize' => 'Gateway unterstützt Autorisierung nicht', + 'Gateway doesn’t support partial refunds.' => 'Gateway unterstützt keine teilweise Rückerstattungen.', + 'Gateway doesn’t support purchase' => 'Gateway unterstützt Kauf nicht', + 'Gateway doesn’t support refunds.' => 'Gateway unterstützt keine Rückerstattungen.', + 'Gateway saved.' => 'Gateway wurde gespeichert.', + 'Gateway' => 'Zahlungseingang', + 'Gateways reordered.' => 'Gateways umsortiert.', + 'Gateways' => 'Gateways', + 'General Settings' => 'Allgemeine Einstellungen', + 'General' => 'Allgemein', + 'Generate' => 'Erzeugen', + 'Generated Coupon Format' => 'Erzeugtes Coupon-Format', + 'Grams (g)' => 'Gramm (g)', + 'Groups for which this sale will be applicable to.' => 'Gruppen, auf die dieses Angebot angewendet werden kann.', + 'HTML Email Template Path' => 'Template-Pfad für HTML-E-Mails', + 'Handle' => 'Kurzname', + 'Harmonized System Code' => 'Code des Harmonisierten Systems', + 'Has Admin Notices' => 'Enthält Administratorhinweise', + 'Has Emails?' => 'Hat E-Mails?', + 'Has Free Shipping' => 'Mit kostenlosem Versand', + 'Has Orders' => 'Hat Bestellungen', + 'Has Purchasable' => 'Hat Kaufoptionen', + 'Has Variants?' => 'Hat Varianten?', + 'Height ({unit})' => 'Höhe ({unit})', + 'Height' => 'Höhe', + 'Hide snapshot' => 'Schattenkopie ausblenden', + 'History' => 'Verlauf', + 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Wie lange (in Sekunden) ein PDF-Download-Link gültig bleiben soll, bevor er abläuft. Der Standardwert ist 86400 (24 Stunden).', + 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Wie häufig eine E-Mail-Adresse diesen Rabatt nutzen darf. Dies gilt für alle vorherigen Bestellungen, egal, ob sie von einem Gast oder Benutzer kommen. Legen Sie hier zero fest für eine unbegrenzte Nutzung durch Gäste oder Benutzer.', + 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Wie oft ein Benutzer diesen Rabatt in Anspruch nehmen kann. Wenn dieser Wert auf etwas anderes als Null gesetzt wird, ist der Rabatt nur für angemeldete Benutzer verfügbar.', + 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Wie oft dieser Rabatt insgesamt von Gästen oder angemeldeten Benutzern genutzt werden kann. Wählen Sie Null für die unbegrenzte Nutzung.', + 'How products should be labeled within the control panel.' => 'Wie Produkte im Control Panel bezeichnet werden sollen.', + 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'Wie Kaufoptionen und Kategorien zusammenhängen und was die übereinstimmenden Artikel bestimmt. Siehe [Beziehungsterminologie]({link}).', + 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'So wird dieses Produkt innerhalb eines Posten in einer Bestellung beschrieben. Sie können Tags hinzufügen, die Eigenschaften ausgeben, wie z. B. {ex1} oder {ex2}', + 'How this shipping method will be referred to in templates and forms.' => 'Wie Sie in Ihren Templates und Formularen auf diese Versandart verweisen.', + 'How variants should be labeled within the control panel.' => 'Wie Varianten im Control Panel bezeichnet werden sollen.', + 'How you’ll refer to this PDF in the templates.' => 'Wie Sie in den Vorlagen auf diese PDF Bezug nehmen.', + 'How you’ll refer to this product type in the templates.' => 'Wie Sie sich in den Templates auf diesen Produkttyp beziehen.', + 'How you’ll refer to this shipping category in the templates.' => 'Unter diesem Namen wird diese Versandkategorie in den Vorlagen erscheinen.', + 'How you’ll refer to this status in the templates.' => 'Wie Sie in Ihren Template auf diesen Status verweisen.', + 'How you’ll refer to this subscription plan in the templates.' => 'Die Art und Weise, wie Sie auf diesen Abonnementplan in den Vorlagen verweisen.', + 'How you’ll refer to this tax category in the templates.' => 'Wie Sie sich in den Templates auf diese Steuerklasse beziehen.', + 'ID' => 'ID', + 'IP Address' => 'IP-Adresse', + 'If disabled, this PDF will not be available or sent with emails.' => 'Wenn die Option deaktiviert ist, wird diese PDF nicht verfügbar sein oder mit E-Mails versendet werden.', + 'If disabled, this email will not send.' => 'Wenn die Option deaktiviert ist, wird die E-Mail nicht gesendet.', + 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Wenn diese Option aktiviert ist und dieser Steuersatz nicht mit der Bestellung übereinstimmt, wird der Betrag vom Einzelpreis im Warenkorb entfernt.', + 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Wenn "Nur Autorisieren" ausgewählt ist, müssen Sie Zahlungen manuell erfassen, bevor Gelder auf Ihr Konto überwiesen werden. Der Gateway muss die ausgewählte Option unterstützen.', + 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Wenn Sie den Prozentsatz als „Nachlass vom reduzierten Artikelpreis“ wählen, wird dieser den „Betrag pro Artikel“ als auch jegliche weitere Rabatte enthalten, die vor diesem angewendet wurden.', + 'Ignore Promotions?' => 'Aktionen ignorieren?', + 'Ignore previous matching sales if this sale matches.' => 'Ignorieren Sie vorherige übereinstimmende Verkäufe, wenn dieser Verkauf übereinstimmt.', + 'Ignore promotional prices when this discount is applied to matching line items' => 'Aktionspreise ignorieren, wenn der Rabatt auf übereinstimmende Einzelposten angewendet wird', + 'Inactive Carts' => 'Inaktive Einkaufskörbe', + 'Inches (in)' => 'Zoll (in)', + 'Include built-in line item tax.' => 'Beziehen Sie die integrierte Einzelpostensteuer mit ein.', + 'Include in price?' => 'In Preis mit einbeziehen?', + 'Include line item discounts.' => 'Beziehen Sie die Einzelpostenrabatte mit ein.', + 'Include line item shipping costs.' => 'Beziehen Sie die Einzelpostenversandkosten mit ein.', + 'Include separate line item tax.' => 'Beziehen Sie eine separate Einzelpostensteuer mit ein.', + 'Included in price?' => 'In Preis mit einbeziehen?', + 'Included' => 'Enthalten', + 'Incoming transfer from Transfer ID: ' => 'Eingehende Übertragung von Übertragung mit ID: ', + 'Incoming' => 'Eingehend', + 'Info' => 'Info', + 'Information linked?' => 'Wurden die Informationen verlinkt?', + 'Information' => 'Information', + 'Invalid JSON' => 'Ungültiges JSON', + 'Invalid Order ID' => 'Ungültige Bestellungs-ID', + 'Invalid VAT ID.' => 'Ungültige USt-IdNr.', + 'Invalid condition syntax' => 'Üngültige Bedingungssyntax', + 'Invalid email.' => 'Ungültige E-Mail-Adresse.', + 'Invalid formula syntax' => 'Ungültige Formelsyntax', + 'Invalid gateway: {value}' => 'Ungültiges Gateway: {value}', + 'Invalid inventory movements.' => 'Ungültige Lagerbestandsbewegungen.', + 'Invalid order condition syntax.' => 'Ungültige Bestellungsbedingungssyntax.', + 'Invalid payment or order. Please review.' => 'Ungültige Zahlung oder Bestellung. Bitte überprüfen.', + 'Invalid payment source ID: {value}' => 'Ungültige Zahlungsquellen-ID: {value}', + 'Invalid store.' => 'Der Shop ist ungültig.', + 'Invalid user.' => 'Ungültiger Benutzer.', + 'Inventory Item' => 'Lagerbestandsposten aktualisiert', + 'Inventory Location' => 'Lagerbestandsort', + 'Inventory Locations' => 'Lagerbestandsorte', + 'Inventory Tracked' => 'Lagerbestand verfolgt', + 'Inventory Transfers' => 'Lagerbestandsübertragungen', + 'Inventory could not be set.' => 'Lagerbestand konnte nicht gesetzt werden.', + 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'Der Lagerbestandsort hat einen zugewiesenen Bestand, die Bestellung(en) müssen erst erfüllt werden.', + 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Der Lagerbestandsort hat einen zugewiesenen Bestand, die Übertragung(en) müssen erst erfüllt werden.', + 'Inventory location is already deactivated.' => 'Der Lagerbestandsort ist bereits deaktiviert.', + 'Inventory location saved.' => 'Lagerbestandsort wurde gespeichert.', + 'Inventory locations not saved.' => 'Lagerbestandsort wurde nicht gespeichert.', + 'Inventory movement could not be saved.' => 'Die Lagerbestandsverschiebung konnte nicht gespeichert werden.', + 'Inventory movement saved.' => 'Lagerbestandsverschiebung gespeichert.', + 'Inventory updated.' => 'Lagerbestand aktualisiert.', + 'Inventory was not updated.' => 'Lagerbestand nicht aktualisiert.', + 'Inventory' => 'Lagerbestand', + 'Invoice amount' => 'Rechnungsbetrag', + 'Invoice date' => 'Rechnungsdatum', + 'Is Promotable' => 'Ist werbeaktionsfähig', + 'Is Promotional Price?' => 'Ist Aktionspreis?', + 'Is Shippable' => 'Ist lieferbar', + 'Is Taxable' => 'Ist steuerbar', + 'Item Rates' => 'Artikel-Preise', + 'Item Subtotal' => 'Zwischensumme Artikel', + 'Item Total' => 'Artikel Gesamtmenge', + 'Item' => 'Artikel', + 'Items' => 'Artikel', + 'Kilograms (kg)' => 'Kilogramm (kg)', + 'Label' => 'Bezeichnung', + 'Landscape' => 'Querformat', + 'Language' => 'Sprache', + 'Last Name' => 'Nachname', + 'Last Updated' => 'Zuletzt aktualisiert', + 'Leave a category rate override blank to use the rate from above.' => 'Lassen Sie den Kategoriepreis-Override leer, um den Preis von oben zu verwenden.', + 'Leave blank for unlimited uses.' => 'Leer lassen für unbegrenzte Nutzung.', + 'Leave blank if products don’t have URLs' => 'Bei Produkten ohne URL leer lassen', + 'Leave gateway subscription as-is' => 'Gateway-Abonnement unverändert lassen', + 'Length ({unit})' => 'Länge ({unit})', + 'Length' => 'Länge', + 'Let each product choose which sites it should be saved to' => 'Lassen Sie jedes Produkt wählen, zu welchen Websites es gespeichert werden soll', + 'Limit which orders this discount applies to based on its line items.' => 'Schränken Sie die Bestellungen, für die dieser Rabatt gilt, anhand ihrer Einzelposten ein.', + 'Limit which purchasables this sale applies to.' => 'Schränken Sie ein, für welche Kaufoptionen diese Aktion gilt.', + 'Limit' => 'Begrenzung', + 'Line Item Statuses' => 'Einzelpostenstatus', + 'Line Item' => 'Einzelposten', + 'Line Items' => 'Einzelposten', + 'Line item price (minus discounts)' => 'Einzelpostenpreis (abzüglich Rabatte)', + 'Line item shipping cost' => 'Versandkosten für Einzelposten', + 'Line item statuses reordered.' => 'Einzelpostenstatus umsortiert.', + 'Link Duration' => 'Link-Dauer', + 'Link Sent' => 'Link gesendet', + 'Link to a product' => 'Link zu einem Produkt', + 'Link to a variant' => 'Mit einer Variante verlinken', + 'Link' => 'Link', + 'Live' => 'Aktiv', + 'Location' => 'Standort', + 'Locations that should be available for previewing products in this product type.' => 'Standorte, die für die Vorschau von Produkten dieses Produkttyps verfügbar sein sollten.', + 'MM' => 'MM', + 'Make a payment' => 'Eine Zahlung tätigen', + 'Make this the primary store' => 'Als primären Shop verwenden', + 'Manage Inventory' => 'Lagerbestand verwalten', + 'Manage donation settings' => 'Spendeneinstellungen verwalten', + 'Manage general store settings' => 'Allgemeine Geschäftseinstellungen verwalten', + 'Manage inventory locations' => 'Lagerbestandsorte verwalten', + 'Manage inventory stock levels' => 'Lagerbestände verwalten', + 'Manage inventory transfers' => 'Lagerbestandsübertragungen verwalten', + 'Manage orders' => 'Bestellungen verwalten', + 'Manage payment currencies' => 'Zahlungswährungen verwalten', + 'Manage promotions' => 'Werbeaktionen verwalten', + 'Manage shipping' => 'Versand verwalten', + 'Manage store settings' => 'Geschäftseinstellungen verwalten', + 'Manage subscription plans' => 'Abonnementpläne verwalten', + 'Manage subscription' => 'Abonnement verwalten', + 'Manage subscriptions' => 'Abonnements verwalten', + 'Manage taxes' => 'Steuern verwalten', + 'Manage' => 'Verwalten', + 'Mark as Pending' => 'Als ausstehend markieren', + 'Mark as completed' => 'Als abgeschlossen markieren', + 'Match Billing Address' => 'Abhängig von Rechnungsadresse', + 'Match Customer' => 'Abhängig von Kunde', + 'Match Order' => 'Abhängig von Bestellung', + 'Match Orders' => 'Bestellungen abgleichen', + 'Match Product' => 'Produkt abgleichen', + 'Match Purchasable' => 'Abhängig von Kaufoption', + 'Match Shipping Address' => 'Abhängig von Versandadresse', + 'Match Variant' => 'Variante abgleichen', + 'Matching Items' => 'Übereinstimmende Artikel', + 'Max Qty' => 'Max. Menge', + 'Max Uses' => 'Max. Verwendungen', + 'Max Variants' => 'Max. Varianten', + 'Max quantity must greater than min.' => 'Die maximale Menge muss größer sein als die minimale.', + 'Maximum Purchase Quantity' => 'Maximale Bestellmenge', + 'Maximum Total Shipping Cost' => 'Maximale Gesamt-Versandkosten', + 'Maximum allowed quantity' => 'Erlaubte Höchstmenge', + 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Maximale Anzahl von betreffenden Artikeln, die bestellt werden können, damit dieser Rabatt wirksam ist. Ein Wert von Null sorgt für ein Überspringen dieser Bedingung.', + 'Maximum order quantity for this item is {num}.' => 'Die maximale Bestellmenge für diesen Artikel beträgt {num}.', + 'Message' => 'Nachricht', + 'Meters (m)' => 'Meter (m)', + 'Millimeters (mm)' => 'Millimeter (mm)', + 'Min Qty' => 'Min. Menge', + 'Min quantity must be less than max.' => 'Die minimale Menge muss größer sein als die maximale.', + 'Minimum Purchase Quantity' => 'Mindestabnahmemenge', + 'Minimum Total Price Strategy' => 'Strategie für den minimalen Gesamtpreis', + 'Minimum Total Shipping Cost' => 'Minimale Gesamt-Versandkosten', + 'Minimum allowed quantity' => 'Erlaubte Mindestmenge', + 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Minimale Anzahl von betreffenden Artikeln, die bestellt werden müssen, damit dieser Rabatt wirksam ist.', + 'Minimum order quantity for this item is {num}.' => 'Die Mindestbestellmenge für diesen Artikel beträgt {num}.', + 'Missing Gateway' => 'Fehlendes Gateway', + 'Missing a default inventory location.' => 'Es fehlt ein Standard-Lagerbestandsort.', + 'Move Inventory' => 'Lagerbestand verschieben', + 'Move To' => 'Verschieben nach', + 'Move {qty} from {fromType} to {toType}' => '{qty} von {fromType} nach {toType} verschieben', + 'Move' => 'Verschieben', + 'Movement from deactivated inventory location' => 'Verschieben von deaktiviertem Lagerbestandsort', + 'Movement' => 'Verschieben', + 'Must have at least one variant.' => 'Muss mindestens eine Variante haben.', + 'Name Field' => 'Namensfeld', + 'Name' => 'Name', + 'New Customer' => 'Neukunde', + 'New Customers' => 'Neukunden', + 'New Order' => 'Neue Bestellung', + 'New PDF' => 'Neue PDF', + 'New address' => 'Neue Adresse', + 'New catalog pricing rule' => 'Neue Katalogpreisregel', + 'New currency' => 'Neue Währung', + 'New discount' => 'Neuer Rabatt', + 'New email' => 'Neue E-Mail', + 'New gateway' => 'Neuer Gateway', + 'New line item status' => 'Neuer Einzelpostenstatus', + 'New line items get this status by default when the order is completed' => 'Neue Einzelposten erhalten diesen Status automatisch wenn die Bestellung abgeschlossen ist', + 'New location' => 'Neuer Standort', + 'New order status' => 'Neuer Bestellstatus', + 'New orders get this status by default' => 'Neue Bestellungen erhalten standardmäßig diesen Status', + 'New product type' => 'Neuer Produkttyp', + 'New product' => 'Neues Produkt', + 'New product, choose a type' => 'Neues Produkt, wählen Sie einen Typ', + 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Neue Produkte sind standardmäßig Teil der ersten für sie verfügbaren Steuerklasse. Sollte keine verfügbar sein, wird diese Kategorie verwendet.', + 'New sale' => 'Neue Aktion', + 'New shipping category' => 'Neue Versandkategorie', + 'New shipping method' => 'Neue Versandart', + 'New shipping rule' => 'Neue Versandregel', + 'New shipping zone' => 'Neue Versandzone', + 'New subscription plan' => 'Neuer Abonnementplan', + 'New tax category' => 'Neue Steuerklasse', + 'New tax rate' => 'Neuer Steuersatz', + 'New tax zone' => 'Neue Steuerzone', + 'New transfer' => 'Neue Übertragung', + 'New {productType} product' => 'Neues Produkt vom Typ {productType}', + 'New' => 'Neue', + 'Next payment' => 'Nächste Zahlung', + 'No Address' => 'Keine Adresse', + 'No PDFs exist yet.' => 'Es gibt noch keine PDFs.', + 'No access given to any specific store management features.' => 'Kein Zugriff auf spezielle Funktionen zur Verwaltung des Shops gewährt.', + 'No additional payment currencies exist yet.' => 'Es existieren noch keine zusätzlichen Bezahlwährungen.', + 'No address' => 'Keine Adresse', + 'No billing address' => 'Keine Rechnungsadresse', + 'No catalog pricing rule exists with the ID “{id}”' => 'Es existiert keine Katalogpreisregel mit der ID "{id}"', + 'No catalog pricing rules exist yet.' => 'Es gibt noch keine Katalogpreisregeln.', + 'No currency exists with the ID “{id}”' => 'Es gibt keine Währung mit der ID "{id}"', + 'No customer email address exists on this cart.' => 'Mit diesem Warenkorb ist keine Kunden E-Mail-Adresse verknüpft.', + 'No description' => 'Keine Beschreibung', + 'No discount exists with the ID “{id}”' => 'Es gibt keinen Rabatt mit der ID "{id}"', + 'No discounts exist yet.' => 'Es existieren noch keine Rabatte.', + 'No donation amount supplied.' => 'Keine Spendenmenge geliefert.', + 'No emails exist yet.' => 'Es existieren noch keine E-Mails.', + 'No inventory changes made.' => 'Keine Lagerbestandsänderungen vorgenommen.', + 'No inventory found.' => 'Kein Lagerbestand gefunden.', + 'No inventory movements made.' => 'Keine Lagerbestandsverschiebungen vorgenommen.', + 'No inventory transactions for this location.' => 'Keine Lagerbestandstransaktionen für diesen Standort.', + 'No new customer selected.' => 'Es wurde kein neuer Kunde ausgewählt.', + 'No order history exists with the ID “{id}”' => 'Es gibt keinen Bestellverlauf mit der ID "{id}"', + 'No order status history items will exist until the cart becomes an order.' => 'Artikel werden erst dann im Bestellverlauf angezeigt, wenn für die Artikel im Warenkorb die Bestellung abgeschlossen wird.', + 'No payment source exists with the ID “{id}”' => 'Es ist keine Zahlungsquelle mit der ID "{id}" vorhanden', + 'No private Note.' => 'Keine private Anmerkung.', + 'No product available.' => 'Kein Produkt verfügbar.', + 'No product types exist yet.' => 'Es existieren noch keine Produkttypen.', + 'No purchasable available.' => 'Keine Kaufoption verfügbar.', + 'No sale exists with the ID “{id}”' => 'Es gibt keine Aktion mit der ID "{id}"', + 'No sales exist yet.' => 'Es existieren noch keine Aktionen.', + 'No shipping address' => 'Keine Lieferadresse', + 'No shipping category exists with the ID “{id}”' => 'Es gibt keine Versandkategorie mit der ID "{id}"', + 'No shipping method exists with the ID “{id}”' => 'Es gibt keine Versandart mit der ID "{id}"', + 'No shipping rule exists with the ID “{id}”' => 'Es gibt keine Versandregel mit der ID "{id}"', + 'No shipping rules exist yet.' => 'Es existieren noch keine Versandregeln.', + 'No shipping zone exists with the ID “{id}”' => 'Es existiert keine Versandzone mit der ID "{id}"', + 'No stats available.' => 'Keine Statistik verfügbar.', + 'No subscription plan exists with the ID “{id}”' => 'Es ist kein Abonnementplan mit der ID "{id}" vorhanden', + 'No subscription plans exist yet.' => 'Es ist noch kein Abonnementplan vorhanden.', + 'No tax category exists with the ID “{id}”' => 'Es gibt keine Steuerklasse mit der ID "{id}"', + 'No tax rate exists with the ID “{id}”' => 'Es gibt keinen Steuersatz mit der ID "{id}"', + 'No tax zone exists with the ID “{id}”' => 'Es gibt keine Steuerzone mit der ID "{id}"', + 'No transactions exist.' => 'Keine Transaktionen existieren.', + 'No user authenticated.' => 'Kein Benutzer authentifiziert.', + 'No' => 'Nein', + 'None on hand' => 'Nicht vorrätig', + 'None' => 'Keine', + 'Not a valid address type' => 'Kein gültiger Adresstyp', + 'Not a valid credit card number.' => 'Keine gültige Kreditkartennummer.', + 'Not all SKUs are unique.' => 'Nicht alle SKUs sind einmalig.', + 'Note' => 'Hinweis', + 'Notes' => 'Anmerkungen', + 'Number of Coupons' => 'Anzahl der Coupons', + 'Number' => 'Zahl', + 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Von den oben aktivierten Websites, unter welchen Websites sollen Produkte in diesem Produkttyp gespeichert werden?', + 'On Hand' => 'Auf Lager', + 'Only allow this gateway to be used for zero value orders?' => 'Darf dieser Gateway nur bei Bestellungen ohne Wert verwendet werden?', + 'Only match certain purchasables…' => 'Nur bei bestimmten Kaufoptionen …', + 'Only match purchasables related to…' => 'Nur Kaufoptionen in Zusammenhang mit …', + 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Es werden nur Bestellungen mit den folgenden Bestellstatus berücksichtigt. Lassen Sie das Feld leer, um alle Status zu berücksichtigen.', + 'Only save product to the site they were created in' => 'Produkte nur auf der Website speichern, auf der sie erstellt wurden', + 'Options' => 'Einstellungen', + 'Order Condition Formula' => 'Bestellungsbedingungsformel', + 'Order Description Format' => 'Format für Bestellbeschreibung', + 'Order Details' => 'Bestelldetails', + 'Order Fields' => 'Bestellfelder', + 'Order PDF Download Link' => 'Bestellung-PDF-Download-Link', + 'Order PDF Filename Format' => 'Dateinamensformat für Bestell-PDF', + 'Order Reference Number Format' => 'Format der Bestellreferenznummer', + 'Order Settings' => 'Bestelleinstellungen', + 'Order Site' => 'Bestellseite', + 'Order Status description.' => 'Beschreibung des Bestellstatus.', + 'Order Status' => 'Bestellstatus', + 'Order Statuses' => 'Bestellstatus', + 'Order can not be empty.' => 'Bestellung kann nicht leer sein.', + 'Order count' => 'Bestellungsanzahl', + 'Order customer data removed.' => 'Kundenbestelldaten gelöscht.', + 'Order deleted.' => 'Bestellung gelöscht.', + 'Order fields saved.' => 'Die Bestellungsfelder wurden gespeichert.', + 'Order not found.' => 'Bestellung nicht gefunden.', + 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Zahlungsdifferenz der Bestellung ist {outstandingBalanceAsCurrency}. Das ist der höchste Wert, der berechnet wird.', + 'Order recalculated.' => 'Bestellung neu berechnet.', + 'Order status saved.' => 'Bestellungsstatus gespeichert.', + 'Order statuses reordered.' => 'Bestellungsstatus umsortiert.', + 'Order total shipping cost' => 'Gesamtlieferkosten der Bestellung', + 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Versteuerbarer Gesamtbestellpreis (Einzelposten-Zwischensumme + Gesamtrabatte + Gesamtlieferkosten)', + 'Order' => 'Bestellung', + 'Orders (Legacy)' => 'Bestellungen (Veraltet)', + 'Orders deleted.' => 'Bestellungen gelöscht.', + 'Orders not restored.' => 'Bestellungen wurden nicht wiederhergestellt.', + 'Orders restored.' => 'Bestellungen wiederhergestellt.', + 'Orders' => 'Bestellungen', + 'Organization Name' => 'Name der Organisation', + 'Organization Tax ID' => 'Steuernummer der Organisation', + 'Origin and destination cannot be the same.' => 'Ursprung und Zielort können nicht identisch sein.', + 'Origin' => 'Ursprung', + 'Original Price' => 'Ursprünglicher Preis', + 'Original price' => 'Ursprünglicher Preis', + 'Original promotional price' => 'Ursprünglicher Aktionspreis', + 'Other Languages' => 'Andere Sprachen', + 'Other countries' => 'Andere Länder', + 'Outgoing transfer from Transfer ID: ' => 'Ausgehende Übertragung von Übertragung mit ID: ', + 'Overpaid' => 'Überbezahlt', + 'Overrides previous?' => 'Vorherige überschreiben?', + 'PDF Attachment' => 'PDF Anhang', + 'PDF Template Path' => 'PDF-Vorlagenpfad', + 'PDF saved.' => 'PDF gespeichert.', + 'PDF' => 'PDF', + 'PDFs & Emails' => 'PDFs & E-Mails', + 'PDFs' => 'PDFs', + 'Paid Amount' => 'Bezahlter Betrag', + 'Paid Status' => 'Bezahlstatus', + 'Paid' => 'Bezahlt', + 'Paper Orientation' => 'Papierausrichtung', + 'Paper Size' => 'Papiergröße', + 'Partial payment not allowed.' => 'Teilzahlung ist nicht erlaubt.', + 'Partial' => 'Teilweise', + 'Past year' => 'Vergangenes Jahr', + 'Past {num} days' => 'Vergangene {num} Tage', + 'Pay {amount} of {currency} on the order.' => 'Bezahlen Sie {amount} in {currency} für die Bestellung.', + 'Pay' => 'Bezahlen', + 'Payment Amount' => 'Zahlungsbetrag', + 'Payment Currencies' => 'Zahlungswährungen', + 'Payment Gateway' => 'Zahlungsgateway', + 'Payment Method' => 'Zahlungsmethode', + 'Payment error: {message}' => 'Zahlungsfehler: {message}', + 'Payment method issue' => 'Problem mit der Zahlungsmethode', + 'Payment source created.' => 'Zahlungsquelle wurde erstellt.', + 'Payment source deleted.' => 'Zahlungsquelle wurde gelöscht.', + 'Payments' => 'Zahlungen', + 'Pending' => 'Ausstehend', + 'Per Email Address Discount Limit' => 'Rabattlimit pro E-Mail-Adresse', + 'Per Item Amount Off' => 'Rabattbetrag pro Artikel', + 'Per Item Discount' => 'Rabatt pro Artikel', + 'Per Item Percentage Off' => 'Pro Artikel Prozentnachlass', + 'Per Item Rate' => 'Preis je Einheit', + 'Per User Discount Limit' => 'Rabattlimit pro Nutzer', + 'Percentage Rate' => 'Prozentsatz', + 'Phone (Alt)' => 'Telefon (alt)', + 'Phone' => 'Telefon', + 'Pick a plan' => 'Einen Plan auswählen', + 'Plain Text Email Template Path' => 'Klartext-E-Mail Vorlagenpfad', + 'Plan' => 'Plan', + 'Plans reordered.' => 'Pläne umsortiert.', + 'Portrait' => 'Hochformat', + 'Post Date' => 'Veröffentlichungsdatum', + 'Postal Code Formula' => 'Postleitzahl-Formel', + 'Pounds (lb)' => 'Pfund (lb)', + 'Preview' => 'Vorschau', + 'Previous Status' => 'Voriger Status', + 'Price' => 'Preis', + 'Prices' => 'Preise', + 'Pricing Rules' => 'Preisregeln', + 'Pricing jobs are currently running.' => 'Es laufen derzeit Aufgaben zur Preisgestaltung.', + 'Pricing' => 'Preisgestaltung', + 'Primary Billing Address' => 'Hauptrechnungsadresse', + 'Primary Shipping Address' => 'Hauptlieferadresse', + 'Primary payment source updated.' => 'Primäre Zahlungsquelle aktualisiert.', + 'Primary' => 'Primär', + 'Private Note' => 'Private Anmerkung', + 'Product Fields' => 'Produktfelder', + 'Product ID is required.' => 'Produkt-ID erforderlich.', + 'Product Template' => 'Produkt-Template', + 'Product Title Format' => 'Format für Produkttitel', + 'Product Type' => 'Produkttyp', + 'Product Types' => 'Produkttypen', + 'Product URI Format' => 'Produkt-URL Format', + 'Product Variant' => 'Produktvariante', + 'Product Variants' => 'Produktvarianten', + 'Product type saved.' => 'Produkttyp gespeichert.', + 'Product type settings' => 'Produkttyp-Einstellungen', + 'Product' => 'Produkt', + 'Products and Variants deleted.' => 'Produkte und Varianten gelöscht.', + 'Products not restored.' => 'Produkte nicht wiederhergestellt.', + 'Products restored.' => 'Produkte wurden wiederhergestellt.', + 'Products' => 'Produkte', + 'Promotable' => 'Aktionsfähig', + 'Promotable?' => 'Werbeaktionsfähig?', + 'Promotional Amount' => 'Aktionsbetrag', + 'Promotional Price' => 'Aktionspreis', + 'Purchasable Categories' => 'Kategorien der Kaufoption', + 'Purchasable ID and Sale ID are required.' => 'Kaufoptions-ID und Aktions-ID erforderlich.', + 'Purchasable ID is required.' => 'Kaufoptions-ID erforderlich.', + 'Purchasable Type' => 'Typ der Kaufoption', + 'Purchasable' => 'Kaufoption', + 'Purchase (Authorize and Capture Immediately)' => 'Kauf (sofortige Autorisierung und Erfassung)', + 'Purchase Total' => 'Gesamtbetrag', + 'Qty' => 'Menge', + 'Quality Control' => 'Qualitätskontrolle', + 'Quantity' => 'Menge', + 'Rate' => 'Satz', + 'Reassign {numOrders, plural, =1{order} other{orders}}' => '{numOrders, plural, one {}=1{Bestellung} other{Bestellungen}} neu zuweisen', + 'Recalculate order' => 'Bestellung neu berechnen', + 'Receive Inventory' => 'Lagerbestand empfangen', + 'Receive Transfer' => 'Übertragung empfangen', + 'Receive' => 'Empfangen', + 'Received' => 'Empfangen', + 'Recent Orders' => 'Neueste Bestellungen', + 'Recipient' => 'Empfänger', + 'Recover Cart' => 'Warenkorb wiederherstellen', + 'Reduce price' => 'Preis verringern', + 'Reduce the price by a fixed amount' => 'Den Preis um einen festen Betrag verringern', + 'Reduce the price by a percentage of the original price' => 'Den Preis auf einen Prozentsatz des ursprünglichen Preises reduzieren', + 'Reference' => 'Referenz', + 'Refresh payment history' => 'Zahlungsverlauf aktualisieren', + 'Refund note' => 'Rückerstattungsbenachrichtigung', + 'Refund payment' => 'Zahlung rückerstatten', + 'Refund' => 'Erstattung', + 'Reject' => 'Ablehnen', + 'Rejected' => 'Abgelehnt', + 'Relationship Type' => 'Beziehungstyp', + 'Removable included tax rates are only allowed for the default tax zone.' => 'Die entfernbaren, enthaltenen Steuersätze sind nur für die Standard-Steuerzone zulässig.', + 'Remove address' => 'Adresse entfernen', + 'Remove all shipping costs from the order' => 'Alle Versandgebühren von der Bestellung entfernen', + 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Entfernen Sie die Kundenzuordnung und die E-Mail-Adresse aus {numOrders, plural, one {}=1{der Bestellung} other{den Bestellungen}}. Wählen Sie gegebenenfalls unten weitere Kundendaten aus, die Sie entfernen möchten', + 'Remove customer data' => 'Kundendaten entfernen', + 'Remove from price?' => 'Vom Preis entfernen?', + 'Remove shipping costs for matching items only' => 'Versandkosten nur für passende Artikel entfernen', + 'Remove the included tax when a valid organization tax ID is present?' => 'Die enthaltene Steuer entfernen, wenn eine gültige Steuer-ID der Organisation vorliegt?', + 'Remove' => 'Entfernen', + 'Removed' => 'Entfernt', + 'Repeat Customers' => 'Stammkunden', + 'Reply To' => 'Antwort an', + 'Require Billing Address At Checkout' => 'Rechnungsadresse an der Kasse verlangen', + 'Require Coupon Code' => 'Gutscheincode erfordern', + 'Require Shipping Address At Checkout' => 'Versandadresse an der Kasse verlangen', + 'Require Shipping Method Selection At Checkout' => 'Auswahl der Versandart an der Kasse verlangen', + 'Require' => 'Erfordern', + 'Reserved' => 'Reserviert', + 'Reset usage' => 'Verwendung zurücksetzen', + 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Den Rabatt auf die Bestellungen beschränken, bei denen der Kunde einen Mindestbestellwert mit passenden Artikel erreicht hat.', + 'Revenue Options' => 'Umsatz-Optionen', + 'Revenue' => 'Einnahmen', + 'Rule' => 'Regel', + 'Rules reordered.' => 'Regeln umsortiert.', + 'SKU' => 'Bestandseinheit (SKU)', + 'Safety' => 'Sicherheit', + 'Sale Price' => 'Aktionspreis', + 'Sale description.' => 'Beschreibung der Aktion.', + 'Sale reordered.' => 'Aktionen umsortiert.', + 'Sale saved.' => 'Aktion gespeichert.', + 'Sale' => 'Aktion', + 'Sales deleted.' => 'Verkäufe wurden gelöscht.', + 'Sales updated.' => 'Aktionen aktualisiert.', + 'Sales' => 'Verkäufe', + 'Save and continue editing' => 'Speichern und mit Bearbeitung fortfahren', + 'Save and return to all orders' => 'Speichern und zur Bestellungsübersicht zurückkehren', + 'Save and set rules' => 'Speichern und Regeln angeben', + 'Save as a new rule' => 'Als eine neue Regel speichern', + 'Save product to all sites enabled for this product type' => 'Produkt auf allen Websites speichern, die für diesen Produkttyp aktiviert sind', + 'Save product to other sites in the same site group' => 'Produkt auf anderen Websites in derselben Websitegruppe speichern', + 'Save product to other sites with the same language' => 'Produkt auf anderen Websites mit der gleichen Sprache speichern', + 'Save' => 'Speichern', + 'Search customer…' => 'Kunden suchen…', + 'Search inventory' => 'Warenbestand suchen', + 'Search or enter customer email…' => 'Kunden-E-Mail suchen oder eingeben…', + 'Search…' => 'Suchen…', + 'See Orders' => 'Bestellungen anzeigen', + 'Select a gateway' => 'Einen Gateway auswählen', + 'Select a tax category.' => 'Steuerklasse auswählen.', + 'Select a tax zone. If empty, this rate will match anywhere.' => 'Wählen Sie eine Steuerzone. Bei Leerlassen wird dieser Steuersatz überall angewendet.', + 'Select address' => 'Adresse auswählen', + 'Select an item' => 'Lagerbestandsposten auswählen', + 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Wählen Sie, wie die Katalogpreisregel auf die Kaufoption(en) angewendet wird.', + 'Select how the sale will be applied to the purchasable(s).' => 'Wählen Sie, wie der Verkauf auf die Kaufoption(en) angewendet wird.', + 'Select product type' => 'Produkttyp auswählen', + 'Select the emails that will be sent when transitioning to this status.' => 'E-Mails auswählen, die versendet werden beim Wechsel in diesen Status.', + 'Select what this rate should be applied to.' => 'Wählen Sie aus, auf was dieser Steuersatz angewendet werden soll.', + 'Send Email' => 'E-Mail senden', + 'Send to custom recipient' => 'An benutzerdefinierten Empfänger senden', + 'Send to the customer' => 'An Kunden senden', + 'Set Quantity' => 'Menge setzen', + 'Set default category' => 'Standardkategorie einstellen', + 'Set default variant' => 'Als Standardvariante festlegen', + 'Set or Adjust' => 'Setzen oder Anpassen', + 'Set price' => 'Preis einstellen', + 'Set status' => 'Status festlegen', + 'Set the price to a flat amount' => 'Den Preis auf einen Pauschalbetrag setzen', + 'Set the price to a percentage of the original price' => 'Den Preis auf einen Prozentsatz des ursprünglichen Preises setzen', + 'Set the sale price to a flat amount' => 'Den Aktionspreis auf einen Pauschalbetrag stellen', + 'Set the sale price to a percentage of the original price' => 'Den Aktionspreis auf einen Prozentsatz des ursprünglichen Preises einstellen', + 'Set to' => 'Einstellen auf', + 'Settings saved.' => 'Einstellungen gespeichert.', + 'Settings' => 'Einstellungen', + 'Share cart…' => 'Warenkorb teilen…', + 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Versand - Die Versandkosten sind die minimalen Kosten, sollte der Preis der Bestellung geringer sein als die Versandkosten.', + 'Shipping Address Zone' => 'Lieferadresszone', + 'Shipping Address' => 'Lieferadresse', + 'Shipping Business Name' => 'Geschäftsname (Versand)', + 'Shipping Categories' => 'Versandkategorien', + 'Shipping Category Conditions' => 'Versandkategorie-Bedingungen', + 'Shipping Category' => 'Versandkategorie', + 'Shipping First Name' => 'Vorname (Versand)', + 'Shipping Full Name' => 'Vollständiger Name (Versand)', + 'Shipping Last Name' => 'Nachname (Versand)', + 'Shipping Method' => 'Versandart', + 'Shipping Methods' => 'Versandarten', + 'Shipping Rule' => 'Versandregel', + 'Shipping Zones' => 'Versandzonen', + 'Shipping address required.' => 'Lieferadresse erforderlich.', + 'Shipping categories deleted.' => 'Versandkategorien gelöscht.', + 'Shipping category saved.' => 'Versandkategorie gespeichert.', + 'Shipping category updated.' => 'Versandkategorie aktualisiert.', + 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Versandkosten werden in Gänze zur Rechnung hinzugefügt, bevor Prozent-, Artikel- und Gewichtssätze angewendet werden. Auf Null setzen, um diesen Satz auszuschalten. Die gesamte Regel, inklusive dieses Basissatzes wird nicht übereinstimmen und angewendet werden, wenn der Warenkorb nur nicht-lieferbare Artikel wie digitale Produkte enthält.', + 'Shipping method saved.' => 'Versandart gespeichert.', + 'Shipping methods and rules deleted.' => 'Versandmethoden und Regeln gelöscht.', + 'Shipping methods updated.' => 'Versandmethoden aktualisiert.', + 'Shipping rule saved.' => 'Versandregel gespeichert.', + 'Shipping zone saved.' => 'Versandzone gespeichert.', + 'Shipping' => 'Versand', + 'Short Number' => 'Kurzwahlnummer', + 'Show Chart?' => 'Grafik anzeigen?', + 'Show Order Count?' => 'Bestellungsanzahl anzeigen?', + 'Show all prices' => 'Alle Preise anzeigen', + 'Show archived gateways' => 'Archivierte Gateways anzeigen', + 'Show order count line on chart.' => 'Bestellungsanzahlzeile in Grafik anzeigen.', + 'Show related sales' => 'Zugehörige Aktionen anzeigen', + 'Show rule details' => 'Regelinformationen anzeigen', + 'Show the Dimensions and Weight fields for products of this type' => 'Felder "Abmessungen" und "Gewicht" für Produkte diesen Typs anzeigen', + 'Show the Title field for products' => 'Das Titelfeld für Produkte anzeigen', + 'Show the Title field for variants' => 'Titelfeld für Varianten anzeigen', + 'Signed In' => 'Angemeldet', + 'Site Languages' => 'Seitensprachen', + 'Site store mapping saved.' => 'Zuordnung des Webshops gespeichert.', + 'Sites' => 'Websites', + 'Slug' => 'Slug', + 'Snapshot' => 'Schattenkopie', + 'Snapshots' => 'Schattenkopien', + 'Some orders restored.' => 'Einige Bestellungen wurden wiederhergestellt.', + 'Some products restored.' => 'Einige Produkte wurden wiederhergestellt.', + 'Some variants restored.' => 'Einige Varianten wurden wiederhergestellt.', + 'Something changed with the order before payment, please review your order and submit payment again.' => 'Etwas hat sich bei der Bestellung vor der Bezahlung verändert. Bitte überprüfen Sie Ihre Bestellung und führen Sie die Bezahlung erneut durch.', + 'Sorry, no matching options.' => 'Keine passenden Optionen.', + 'Source - The purchasable relationship field is on the category' => 'Quelle - Das Kaufoption-Beziehungsfeld ist in der Kategorie', + 'Source' => 'Quelle', + 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Bitte spezifizieren Sie eine Twig Bedingung, die feststellt, ob ein Rabatt auf eine gegebene Bestellung angewendet werden soll. (Die Bestellung kann über eine `order` Variable referenziert werden.)', + 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Bitte spezifizieren Sie eine Twig Bedingung, die feststellt, ob eine Versandregel auf eine gegebene Bestellung angewendet werden soll. (Die Bestellung kann über eine `order` Variable referenziert werden.)', + 'Start Date' => 'Startdatum', + 'State' => 'Bundesstaat', + 'Status Email Address' => 'Adresse für Status-E-Mails', + 'Status Emails' => 'Status-Benachrichtigungen', + 'Status History' => 'Statusverlauf', + 'Status Updated.' => 'Status wurde aktualisiert.', + 'Status change message' => 'Nachricht zur Statusänderung', + 'Status' => 'Status', + 'Stock' => 'Lager', + 'Stops Processing?' => 'Bearbeitung wird angehalten?', + 'Stops subsequent?' => 'Stoppt nachfolgende?', + 'Store Location' => 'Standort des Geschäfts', + 'Store Management' => 'Shopverwaltung', + 'Store Markets' => 'Shopmärkte', + 'Store Rule' => 'Shopregel', + 'Store saved.' => 'Shop gespeichert.', + 'Store' => 'Shop', + 'Stores & Sites' => 'Shops & Websites', + 'Stores' => 'Shops', + 'Strategy to apply when an order is free or has a zero balance.' => 'Strategie zum Anwenden, wenn eine Bestellung kostenlos ist oder ein Nullsaldo hat.', + 'Strategy to apply when calculating the minimum order price.' => 'Anzuwendende Strategie beim Berechnen des Mindestbestellungspreises.', + 'Subject' => 'Betreff', + 'Subscribing user' => 'Abonnierter Benutzer', + 'Subscription Fields' => 'Abonnementfelder', + 'Subscription Plans' => 'Abonnementpläne', + 'Subscription Settings' => 'Abonnementeinstellungen', + 'Subscription cancelled.' => 'Abonnement gekündigt.', + 'Subscription date' => 'Abonnementdatum', + 'Subscription fields saved.' => 'Abonnementfelder gespeichert.', + 'Subscription for {user} to {plan} prevented by a plugin.' => 'Abonnement von {user} für {plan} wurde durch ein Plug-in verhindert.', + 'Subscription plan saved.' => 'Abonnementplan wurde gespeichert.', + 'Subscription plan' => 'Abonnementplan', + 'Subscription plans' => 'Abonnementpläne', + 'Subscription reactivated.' => 'Abonnement reaktiviert.', + 'Subscription reference' => 'Abonnementreferenz', + 'Subscription started.' => 'Abonnement gestartet.', + 'Subscription switched.' => 'Abonnement gewechselt.', + 'Subscription to “{plan}”' => 'Abonnement von "{plan}"', + 'Subscription' => 'Abonnement', + 'Subscriptions on hold' => 'Abonnements pausiert', + 'Subscriptions' => 'Abonnements', + 'Suppress emails' => 'E-Mails unterdrücken', + 'Switch plan' => 'Plan wechseln', + 'Switch' => 'Wechseln', + 'System' => 'System', + 'Table Columns' => 'Tabellenspalten', + 'Target - The category relationship field is on the purchasable' => 'Ziel - Das Kategorien-Beziehungsfeld ist bei der Kaufoption', + 'Tax & Shipping' => 'Versandkosten und Steuern', + 'Tax (inc)' => 'Steuer (inkl.)', + 'Tax Categories' => 'Steuerklasse', + 'Tax Category' => 'Steuerklasse', + 'Tax Rates' => 'Steuersatz', + 'Tax Zone' => 'Steuerzone', + 'Tax Zones' => 'Steuerzonen', + 'Tax categories deleted.' => 'Steuerkategorien gelöscht.', + 'Tax category saved.' => 'Steuerklasse gespeichert.', + 'Tax category updated.' => 'Steuerklasse aktualisiert.', + 'Tax rate saved.' => 'Steuersatz gespeichert.', + 'Tax rates updated.' => 'Steuersatz aktualisiert.', + 'Tax zone saved.' => 'Steuerzone gespeichert.', + 'Tax' => 'Steuer', + 'Taxable Subject' => 'Steuerpflichtige Person', + 'Template Path' => 'Template-Pfad', + 'That handle is already in use' => 'Dieser Identifikator wird bereits verwendet', + 'That handle is already in use.' => 'Dieser Identifikator wird bereits verwendet.', + 'The PDF to attach to this email.' => 'Die PDF, die an diese E-Mail angehängt werden soll.', + 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'Die URL zu der Seite, auf der Zahlungsinformationen für ein Abonnement aktualisiert und die 3DS-Authentifizierung durchgeführt werden können.', + 'The address provided is outside the store’s market.' => 'Die angegebene Adresse liegt außerhalb des Marktes des Shops.', + 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'Die Rabattmenge, die auf die ganze Bestellung angewendet wird. Dieser Betrag wird auf die Posten verteilt, vom höchsten Preis bis zum niedrigsten, bis der Rabatt aufgebraucht ist.', + 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'Der Basisrabatt kann Artikel im Warenkorb nur bis zu einem Preis von Null reduzieren, bis er aufgebraucht ist, und kann den Preis der Bestellung nicht negativ machen.', + 'The cart recovery link is invalid. Please request a new one.' => 'Der Warenkorb-Wiederherstellungslink ist ungültig. Bitte fordern Sie einen neuen an.', + 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Der Wechselkurs, der benutzt wird, wenn ein Betrag in diese Währung umgerechnet wird. Wenn ein Artikel zum Beispiel {amount1} kostet, würde sich aus einem Wechselkurs von {rate} ein Betrag von {amount2} in der Zweitwährung ergeben.', + 'The countries that orders are allowed to be placed from.' => 'Die Länder, aus denen Bestellungen aufgegeben werden dürfen.', + 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'Der Coupon „{code}“ hat sein Nutzungslimit von {limit} überschritten.', + 'The customer for this order has been deleted.' => 'Der Kunde für diese Bestellung wurde gelöscht.', + 'The default shipping category is automatically available to all product types.' => 'Die Standardversandkategorie ist automatisch für alle Produkttypen verfügbar.', + 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'Der Rabatt „{name}“ hat sein Gesamtnutzungslimit von {limit} überschritten.', + 'The download link has expired. Please request a new one.' => 'Der Download-Link ist abgelaufen. Bitte fordern Sie einen neuen an.', + 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'Die E-Mail-Adresse von der E-Mails zum Bestellstatus gesendet werden. Keine Angabe, wenn die E-Mail-Adresse aus den allgemeinen Einstellungen in Craft verwendet werden soll.', + 'The entry that contains the description for this subscription’s plan.' => 'Die Eingabe, die eine Beschreibung dieses Abonnementsplans enthält.', + 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'Der Pauschalwert, der von jedem Artikel abgezogen werden soll, z. B. “3” für 3 $ Rabatt auf jeden Artikel.', + 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'Das Format, in dem neue Coupons generiert werden, z. B. {example}. Alle #-Zeichen werden durch einen zufälligen Buchstaben ersetzt.', + 'The from and to inventory locations must be different.' => 'Der Ausgangs- und Ziellagerbestand müssen unterschiedlich sein.', + 'The inventory locations this store uses.' => 'Die Lagerbestandsorte, die dieser Shop verwendet.', + 'The item is not enabled for sale.' => 'Der Artikel ist nicht für den Verkauf freigegeben.', + 'The language the order was made in.' => 'Die Sprache, in der die Bestellung aufgegeben wurde.', + 'The language to be used when this email is rendered.' => 'Die Sprache, die verwendet werden soll, wenn diese E-Mail wiedergegeben wird.', + 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Die maximale Zahl an Hierachiestufen, die dieses Produkt haben kann. Wenn es egal ist, leer lassen.', + 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Der Maximalbetrag, den der Kunde für den Versand ausgeben soll. Zum Deaktivieren auf Null setzen.', + 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Der Minimalbetrag, den der Kunde für den Versand ausgeben soll. Zum Deaktivieren auf Null setzen.', + 'The order is not valid.' => 'Die Bestellung ist nicht gültig.', + 'The payment gateway that will be used for the subscription plan.' => 'Der Zahlungs-Gateway, der für den Abonnementplan verwendet wird.', + 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'Der Prozentwert, der von jedem Artikel nachgelassen werden soll. z.B {ex1} für {ex2} Nachlass. +Prozentwerte sind auf zwei Dezimalstellen gerundet.', + 'The previously-selected shipping method is no longer available.' => 'Die zuvor gewählte Versandoption ist nicht mehr verfügbar.', + 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Der Preis von {description} wurde von {originalSalePriceAsCurrency} auf {newSalePriceAsCurrency} erhöht', + 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Der Preis von {description} wurde von {originalSalePriceAsCurrency} auf {newSalePriceAsCurrency} gesenkt', + 'The primary currency cannot be changed after orders are placed.' => 'Die primäre Währung kann nicht geändert werden, nachdem Bestellungen aufgegeben wurden.', + 'The purchasable defines the relationship' => 'Die Kaufoption definiert die Beziehung', + 'The purchasable is related by another element' => 'Die Kaufoption ist mit einem anderen Element verknüpft', + 'The recipient of the email. Twig code can be used here.' => 'Der Empfänger der E-Mail. Twig-Code kann hier verwendet werden.', + 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'Die E-Mail-Adresse, an die Antworten gesendet werden sollen. Für die normale Antwort-E-Mail-Adresse des Senders frei lassen. Twig-Code kann hier verwendet werden.', + 'The site the order was made in.' => 'Die Website, von der aus die Bestellung aufgegeben wurde.', + 'The site to be used when this email is rendered.' => 'Die Website, die verwendet werden soll, wenn diese E-Mail wiedergegeben wird.', + 'The subject line of the email. Twig code can be used here.' => 'Die Betreffzeile der E-Mail. Twig-Code kann hier verwendet werden.', + 'The template that the PDF should be generated from.' => 'Die Vorlage, aus der die PDF generiert werden soll.', + 'The template to be used for HTML emails.' => 'Das Template, das für HTML-E-Mails genutzt werden soll.', + 'The template to be used for plain text emails. Twig code can be used here.' => 'Die Vorlage, die für Klartext-E-Mails verwendet werden soll. Twig-Code kann verwendet werden.', + 'The template to use when a product’s URL is requested.' => 'Das Template, das genutzt werden soll, wenn die URL eines Produktes angefordert wird.', + 'The total number of order adjustments changed.' => 'Die Gesamtanzahl der Angebotsanpassungen hat sich geändert.', + 'The total price of the order changed.' => 'Der Gesamtpreis der Bestellung hat sich geändert.', + 'The total quantity of items within the order changed.' => 'Die Gesamtanzahl der Artikel in der Bestellung hat sich geändert.', + 'The unique SKU of the donation purchasable.' => 'Die einzigartige SKU für das Spenden-Kaufoption.', + 'The unit of measurement that should be used when specifying product dimensions.' => 'Maßeinheit, die verwendet werden soll, um die Abmessungen eines Produktes anzugeben.', + 'The unit of measurement that should be used when specifying product weights.' => 'Die Maßeinheit, die bei der Gewichtsangabe von Produkten verwendet werden soll.', + 'The webhook URL for this gateway.' => 'Die Webhook URL für dieses Gateway.', + 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'Der Name im Feld “von” wird genutzt, wenn E-Mails zum Bestellstatus versandt werden. Wenn der Absendername aus den allgemeinen Einstellungen in Craft genutzt werden soll, Feld leer lassen.', + 'There are errors on the order' => 'Es gibt Fehler bei dieser Bestellung', + 'There are only {num} “{description}” items left in stock.' => 'Es sind nur noch {num} “{description}” Artikel auf Lager.', + 'There aren’t any product types to select yet.' => 'Es gibt noch keine Produkttypen zur Auswahl.', + 'There is no gateway or payment source available for use with this order.' => 'Es sind keine Gateways oder Zahlungsquellen zur Verwendung für diese Bestellung verfügbar.', + 'There is no gateway selected that supports payment sources.' => 'Es wurde kein Gateway ausgewählt, der Zahlungsquellen unterstützt.', + 'There is no shipping method selected for this order.' => 'Für diese Bestellung ist keine Versandart ausgewählt.', + 'This URL will load the cart into the user’s session, making it the active cart.' => 'Diese URL wird den Warenkorb in die Sitzung des Nutzers laden, wodurch dieser zum aktiven Warenkorb wird.', + 'This action is not allowed for the current user.' => 'Diese Aktion ist für den aktuellen Benutzer nicht zulässig.', + 'This category will be used as the default for all purchasables in this store.' => 'Diese Kategorie wird als Standard für alle in diesem Shop erhältlichen Artikel verwendet.', + 'This coupon is for registered users and limited to {limit} uses.' => 'Dieser Coupon ist nur für registrierte Benutzer und auf {limit} Anwendungen limitiert.', + 'This coupon is limited to {limit} uses.' => 'Dieser Coupon ist auf {limit} Anwendungen begrenzt.', + 'This coupon requires an email address.' => 'Dieser Coupon erfordert eine E-Mail-Adresse.', + 'This gateway does not support that functionality.' => 'Dieser Gateway unterstützt diese Funktionalität nicht.', + 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Dies wird durch die {setting}-Konfigurationseinstellung in `config/{file}.php` überschrieben.', + 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Dies ist die Adresse Ihres Geschäfts. Sie kann von mehreren Plug-ins dazu verwendet werden, um Dinge wie Lieferung und Steuern festzulegen. Sie kann auch in PDF-Belegen verwendet werden.', + 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Das ist die Standard-PDF, die erstellt wird, wenn die Bestellungs-PDF angefordert wird.', + 'This is the last location for the {store} store.' => 'Dies ist der letzte Standort für den Shop {store}.', + 'This month' => 'Dieser Monat', + 'This order has unsaved changes.' => 'Diese Bestellung hat ungespeicherte Änderungen.', + 'This week' => 'Diese Woche', + 'This year' => 'Dieses Jahr', + 'Times Used' => 'Häufigkeit des Gebrauchs', + 'Title' => 'Titel', + 'To' => 'An', + 'Today' => 'Heute', + 'Too many variants for this product.' => 'Zu viele Varianten für dieses Produkt.', + 'Top Customers by Average Order' => 'Beste Kunden nach durchschnittlicher Bestellung', + 'Top Customers by Total Revenue' => 'Top Kunden nach Gesamtumsatz', + 'Top Customers' => 'Top-Kunden', + 'Top Product Types by Qty Sold' => 'Top Produkttypen nach verkaufter Menge', + 'Top Product Types by Revenue' => 'Top Produkttypen nach Umsatz', + 'Top Product Types' => 'Top-Produkttypen', + 'Top Products by Qty Sold' => 'Top Produkte nach verkaufter Menge', + 'Top Products by Revenue' => 'Top Produkte nach Umsatz', + 'Top Products' => 'Top Produkte', + 'Top Purchasables by Qty Sold' => 'Top-Kaufoptionen nach verkaufter Menge', + 'Top Purchasables by Revenue' => 'Top Kaufoptionen nach Umsatz', + 'Top Purchasables' => 'Top-Kaufoptionen', + 'Total ' => 'Gesamt ', + 'Total Discount Use Limit' => 'Gesamter Rabatt-Nutzungslimit', + 'Total Discount' => 'Gesamtrabatt', + 'Total Included Tax' => 'Vollständige enthaltene Steuer', + 'Total Orders by Billing Country' => 'Gesamtbestellungen nach Rechnungsland', + 'Total Orders by Country' => 'Gesamtbestellungen nach Land', + 'Total Orders by Shipping Country' => 'Gesamtbestellungen nach Lieferland', + 'Total Orders' => 'Bestellungen insgesamt', + 'Total Paid' => 'Insgesamt bezahlt', + 'Total Price' => 'Gesamtpreis', + 'Total Qty' => 'Gesamtmenge', + 'Total Revenue' => 'Gesamteinnahmen', + 'Total Shipping' => 'Gesamtversandkosten', + 'Total Tax' => 'Steuern gesamt', + 'Total Weight' => 'Gesamtgewicht', + 'Total' => 'Insgesamt', + 'Track Inventory' => 'Lagerbestand verfolgen', + 'Transaction Hash' => 'Transaktions-Hashwert', + 'Transaction ID' => 'Transaktions-ID', + 'Transaction captured successfully: {message}' => 'Transaktion erfolgreich erfasst: {message}', + 'Transaction refunded successfully: {message}' => 'Transaktion erfolgreich erstattet: {message}', + 'Transactions' => 'Transaktionen', + 'Transfer Fields' => 'Übertragungsfelder', + 'Transfer Items' => 'Übertragungsbestandsposten', + 'Transfer Settings' => 'Übertragungseinstellungen', + 'Transfer Status' => 'Übertragungsstatus', + 'Transfer fields saved.' => 'Die Übertragungsfelder wurden gespeichert.', + 'Transfer must have at least one item.' => 'Die Übertragung muss mindestens einen Lagerbestandsposten enthalten.', + 'Transfer' => 'Übertragung', + 'Transfers' => 'Übertragungen', + 'Trial days credited' => 'Testtage gutgeschrieben', + 'Trial expiration' => 'Ablauf der Testphase', + 'Trial expiry date' => 'Ablaufdatum der Testphase', + 'Type not in allowed options.' => 'Typ ist nicht in erlaubten Optionen.', + 'Type' => 'Typ', + 'URI' => 'URI', + 'Unable to cancel subscription at this time.' => 'Zurzeit kann das Abonnement nicht storniert werden.', + 'Unable to complete order: another request is already in progress.' => 'Die Bestellung kann nicht abgeschlossen werden: Eine andere Anfrage wird bereits bearbeitet.', + 'Unable to find variant.' => 'Variante nicht gefunden.', + 'Unable to generate coupon codes: {message}' => 'Gutscheincodes können nicht erzeugt werden: {message}', + 'Unable to make payment at this time.' => 'Die Zahlung kann zurzeit nicht durchgeführt werden.', + 'Unable to modify subscription at this time.' => 'Das Abonnement kann zurzeit nicht verändert werden.', + 'Unable to reactivate subscription at this time.' => 'Zurzeit kann das Abonnement nicht wieder aktiviert werden.', + 'Unable to reassign orders.' => 'Bestellungen können nicht neu zugewiesen werden.', + 'Unable to remove order data.' => 'Die Bestelldaten konnten nicht gelöscht werden.', + 'Unable to retrieve Sale and Purchasable.' => 'Aktion und Kaufoptionen konnten nicht abgerufen werden.', + 'Unable to retrieve cart.' => 'Warenkorb konnte nicht abgerufen werden.', + 'Unable to retrieve customer.' => 'Kunde konnte nicht abgerufen werden.', + 'Unable to retrieve load cart URL' => 'URL zum Laden vom Warenkorb konnte nicht abgerufen werden', + 'Unable to retrieve payment source.' => 'Zahlungsquelle konnte nicht abgerufen werden.', + 'Unable to set default shipping category.' => 'Standardversandkategorie konnte nicht eingestellt werden.', + 'Unable to set default tax category.' => 'Standardsteuerkategorie konnte nicht eingestellt werden.', + 'Unable to set primary payment source.' => 'Primäre Zahlungsquelle konnte nicht festgelegt werden.', + 'Unable to start the subscription. Please check your payment details.' => 'Das Abonnement kann nicht gestartet werden. Bitte überprüfen Sie Ihre Zahlungsinformationen.', + 'Unable to subscribe at this time.' => 'Zurzeit kann kein Abonnement abgeschlossen werden.', + 'Unable to update cart.' => 'Warenkorb konnte nicht aktualisiert werden.', + 'Unable to validate address.' => 'Adresse konnte nicht validiert werden.', + 'Unit Price' => 'Einheitspreis', + 'Unit price (minus discounts)' => 'Einheitspreis (abzüglich Rabatte)', + 'Units' => 'Einheiten', + 'Unpaid' => 'Nicht bezahlt', + 'Unsubscribe' => 'Abbestellen', + 'Update Address' => 'Adresse aktualisieren', + 'Update Order Status' => 'Bestellstatus aktualisieren', + 'Update Order Status…' => 'Bestellstatus aktualisieren…', + 'Update order' => 'Bestellung aktualisieren', + 'Update subscription' => 'Abonnement aktualisieren', + 'Update' => 'Aktualisieren', + 'Updated By' => 'Aktualisiert von', + 'Updated committed stock successfully.' => 'Zugewiesener Bestand erfolgreich aktualisiert.', + 'Updated' => 'Aktualisiert', + 'Use Billing Address For Tax' => 'Rechnungsadresse für Steuern verwenden', + 'Use as the primary billing address' => 'Als primäre Zahlungsadresse verwenden', + 'Use as the primary shipping address' => 'Als primäre Versandadresse verwenden', + 'Used By Tax Rates' => 'Verwendet nach Steuersatz', + 'Used by Tax Rates' => 'Verwendet nach Steuersatz', + 'User Groups' => 'Benutzergruppen', + 'User not found.' => 'Benutzer nicht gefunden.', + 'User' => 'Benutzer', + 'Uses' => 'Verwendungen', + 'Validate Business Tax ID as Vat ID' => 'Geschäftliche Steuer-ID als USt-IdNr validieren', + 'Validating condition syntax' => 'Bedingungssyntax wird validiert', + 'Validating formula syntax' => 'Formelsyntax wird validiert', + 'Variant Fields' => 'Variantenfelder', + 'Variant Has Untracked Stock' => 'Variante hat nicht verfolgten Lagerbestand', + 'Variant Price' => 'Preis der Variante', + 'Variant SKU' => 'SKU der Variante', + 'Variant Search' => 'Variantensuche', + 'Variant Stock' => 'Lagerbestand der Variante', + 'Variant Title Format' => 'Format für Variantentitel', + 'Variant Tracks Stock' => 'Variante mit Lagerbestandsverfolgung', + 'Variant UI Label Format' => 'Varianten-UI-Bezeichnung-Format', + 'Variant has no product.' => 'Variante hat kein Produkt.', + 'Variants not restored.' => 'Varianten wurden nicht wiederhergestellt.', + 'Variants restored.' => 'Varianten wurden wiederhergestellt.', + 'Variants' => 'Varianten', + 'View customer' => 'Kunde anzeigen', + 'View order' => 'Bestellung anzeigen', + 'View product type - {productType}' => 'Produkttyp anzeigen - {productType}', + 'View user' => 'Benutzer anzeigen', + 'View' => 'Ansicht', + 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Warnung, das Löschen dieser Währung wird alle Zahlungen und Rückerstattungen in dieser Währung aufhalten. Sind Sie sicher, dass Sie „{name}“ löschen möchten?', + 'Web' => 'Web', + 'Webhook URL' => 'Webhook URL', + 'Weight ({unit})' => 'Gewicht ({unit})', + 'Weight Rate' => 'Gewichtspreis', + 'Weight Unit' => 'Gewichtseinheit', + 'Weight' => 'Gewicht', + 'What product URIs should look like for the site.' => 'Wie Produkt-URIs für diese Website aussehen sollen.', + 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Wie die automatisch generierten Produkttitel aussehen sollten. Sie können auch Schlagwörter nutzen, die Produkteigenschaften ausgeben, wie z. B. {ex1} oder {ex2}. Alle benutzerdefinierten Felder müssen als erforderlich markiert sein.', + 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Wie die automatisch generierten Variantentitel aussehen sollten. Sie können auch Tags nutzen, die Varianteneigenschaften ausgeben, wie z.B. {ex1} oder {ex2}. Alle benutzerdefinierten Felder müssen als erforderlich markiert sein.', + 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Wie der PDF-Dateiname der Bestellung aussehen sollte (ohne Dateiendung). Sie können Tags hinzufügen die Bestelleigenschaften ausgeben, wie etwa {ex1} or {ex2}.', + 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Wie die einzigartige auto-generierte Bestandseinheit (SKU) aussehen sollte, wenn ein Bestandseinheits-Feld ohne Wert abgesendet wurde. Sie können Tags einbeziehen, die Eigenschaften ausgeben, wie z.B. {ex1} oder {ex2)', + 'What this PDF will be called in the control panel.' => 'Wie diese PDF im Control Panel genannt wird.', + 'What this catalog pricing rule will be called in the control panel.' => 'Wie diese Katalogpreisregel im Control Panel genannt wird.', + 'What this discount will be called in the control panel.' => 'Wie dieser Rabatt im Control Panel genannt wird.', + 'What this email will be called in the control panel.' => 'Wie diese E-Mail im Control Panel genannt wird.', + 'What this product type will be called in the control panel.' => 'Wie dieser Produkttyp im Control Panel genannt wird.', + 'What this sale will be called in the control panel.' => 'Wie diese Aktion im Control Panel genannt wird.', + 'What this shipping category will be called in the control panel.' => 'Wie diese Versandkategorie im Control Panel genannt wird.', + 'What this shipping rule will be called in the control panel.' => 'Wie diese Versandregel im Control Panel genannt wird.', + 'What this shipping zone will be called in the control panel.' => 'Wie diese Versandzone im Control Panel genannt wird.', + 'What this status will be called in the control panel.' => 'Wie dieser Status im Control Panel genannt wird.', + 'What this subscription plan will be called in the control panel.' => 'Wie dieser Abonnementplan im Control Panel genannt wird.', + 'What this tax category will be called in the control panel.' => 'Wie diese Steuerklasse im Control Panel genannt wird.', + 'What this tax zone will be called in the control panel.' => 'Wie diese Steuerzone im Control Panel genannt wird.', + 'When this discount is applied to an order, which line items should be discounted?' => 'Wenn dieser Rabatt auf eine Bestellung angewendet wird, welche Einzelposten sollen dann rabattiert werden?', + 'Whether the first available shipping method option should be set automatically on carts.' => 'Ob die erste verfügbare Versandartoption in Warenkörben automatisch gesetzt werden soll.', + 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Ob die primäre Zahlungsquelle des Benutzers bei neuen Warenkörben automatisch festgelegt werden soll.', + 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Ob die primäre Versand- und Rechnungsadresse des Benutzers bei neuen Warenkörben automatisch gesetzt werden soll.', + 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Ob diese Katalogpreisregel unabhängig von anderen Bedingungen genutzt werden kann.', + 'Whether this sale should be available for use, regardless of other conditions.' => 'Ob dieser Verkauf unabhängig von anderen Bedingungen genutzt werden kann.', + 'Which data to display in the name column in the results table.' => 'Welche Daten in der Namensspalte der Ergebnistabelle angezeigt werden.', + 'Which product types should this category be available to?' => 'Für welche Produkttypen sollte diese Kategorie verfügbar sein?', + 'Which template should be loaded when a product’s URL is requested.' => 'Die Vorlage, die bei Anforderung der URL eines Produktes geladen werden soll.', + 'Width ({unit})' => 'Breite ({unit})', + 'Width' => 'Breite', + 'YYYY' => 'JJJJ', + 'Yes' => 'Ja', + 'You are not allowed to add a line item.' => 'Sie dürfen keinen Einzelposten hinzufügen.', + 'You currently have no emails configured to select for this status.' => 'Sie haben aktuell keine E-Mails zur Auswahl für diesen Status konfiguriert.', + 'You do not have permission to load this cart.' => 'Sie haben keine Berechtigung, diesen Warenkorb zu laden.', + 'You must set up at least one gateway that supports subscriptions first.' => 'Sie müssen mindestens einen Gateway einrichten, der zuerst Abonnements unterstützt.', + 'You must be logged in or provide a valid token to load this cart.' => 'Sie müssen angemeldet sein oder ein gültiges Token bereitstellen, um diesen Warenkorb zu laden.', + 'You must be signed in to create a payment source.' => 'Sie müssen angemeldet sein, um eine Zahlungsquelle zu erstellen.', + 'You must be signed in to set a primary payment source.' => 'Sie müssen angemeldet sein, um eine primäre Zahlungsquelle festzulegen.', + 'You must make a payment to complete the order.' => 'Sie müssen eine Zahlung tätigen, um die Bestellung abzuschließen.', + 'Your Cart Recovery Link' => 'Ihr Warenkorb-Wiederherstellungslink', + 'Your Order PDF Download Link' => 'Ihr Bestellung-PDF-Download-Link', + 'Your order is empty' => 'Ihre Bestellungen ist leer', + 'ZIP file' => 'ZIP-Datei', + 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Null - Minimaler Preis ist Null, sollten die Rabatte größer sein als der Wert der Bestellung.', + 'Zip Code' => 'Postleitzahl', + 'all' => 'alle', + 'any' => 'beliebig', + 'average order total' => 'durchschnittlicher Bestellungspreis', + 'billing address' => 'Rechnungsadresse', + 'donation' => 'Spende', + 'donations' => 'Spenden', + 'info' => 'Info', + 'inventory location' => 'Lagerbestandsort', + 'new customers' => 'Neukunden', + 'on hand' => 'auf Lager', + 'only' => 'nur', + 'order' => 'Bestellung', + 'orders' => 'Bestellungen', + 'price' => 'Preis', + 'prices' => 'Preise', + 'product variant' => 'Produktvariante', + 'product variants' => 'Produktvarianten', + 'product' => 'Produkt', + 'products' => 'Produkte', + 'repeat customers' => 'Stammkunden', + 'shipping address' => 'Lieferadresse', + 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'shippingSameAsBilling und billingSameAsShipping können nicht gleichzeitig festgelegt sein.', + 'subscription' => 'Abonnement', + 'subscriptions' => 'Abonnements', + 'to' => 'bis', + 'transfer' => 'übertragung', + 'transfers' => 'übertragungen', + '{amount} included' => '{amount} enthalten', + '{count} Unfulfilled Orders' => '{count} unerfüllte Bestellungen', + '{description} is no longer available.' => '{description} nicht länger verfügbar.', + '{description} only has {stock} in stock.' => '{description} hat nur noch {stock} auf Lager.', + '{from} to {to}' => '{from} nach {to}', + '{name} (Primary)' => '{name} (Hauptname)', + '{name} (Trashed)' => '{name} (Verworfen)', + '{name} catalog price' => '{name} Katalogpreis', + '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, =1{Bestellung} other{Bestellungen}} aktualisiert.', + '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, one {}=1{Bestellung ist} other{Bestellungen sind}} mit {numUsers, plural, one {}=1{dem Nutzer} other{den Nutzern}} verknüpft.', + '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, one {}=1{Abonnement ist} other{Abonnements sind}} für {numUsers, plural, one {}=1{den Nutzer} other{die Nutzer}} aktiviert.', + '{number} more…' => '{number} mehr …', + '{pct} off the discounted item price' => '{pct} Nachlass vom reduzierten Artikelpreis', + '{pct} off the original item price' => '{pct} Nachlass vom ursprünglichen Artikelpreis', + '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, one {}=1{wurde} other{wurden}} keiner Website zugewiesen.', + '{total} in total revenue' => '{total} in Gesamtumsatz', + '{total} orders' => '{total} Bestellungen', + '{total} saleable across {locationCount} location(s)' => '{total} verkaufbar über {locationCount} Standort(e)', + '{uses} uses across {emails} email addresses' => '{uses} Nutzen über {emails} E-Mail-Adressen', + '{uses} uses across {users} users' => '{uses} Verwendungen über {users} Kunden', + '“{description}” is currently out of stock.' => '“{description}” ist aktuell nicht vorrätig.', + '“{key}” has invalid JSON' => '„{key}“ enthält ungültiges JSON', +]; diff --git a/lang/en-GB/commerce.php b/lang/en-GB/commerce.php new file mode 100644 index 0000000000..81217f3ca6 --- /dev/null +++ b/lang/en-GB/commerce.php @@ -0,0 +1,1423 @@ + '(new price)', + '(of original price)' => '(of original price)', + '(off original price)' => '(off original price)', + 'A cart number must be specified.' => 'A cart number must be specified.', + 'A cart recovery link has been sent to {email}.' => 'A cart recovery link has been sent to {email}.', + 'A cart recovery link will be sent to {email}.' => 'A cart recovery link will be sent to {email}.', + 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.', + 'A new download link has been sent to {email}' => 'A new download link has been sent to {email}', + 'A new download link will be sent to {email}' => 'A new download link will be sent to {email}', + 'A valid email is required to create a customer.' => 'A valid email is required to create a customer.', + 'Accept' => 'Accept', + 'Accepted' => 'Accepted', + 'Actions' => 'Actions', + 'Active Carts' => 'Active Carts', + 'Active subscriptions' => 'Active subscriptions', + 'Active' => 'Active', + 'Add Address' => 'Add Address', + 'Add a coupon' => 'Add a coupon', + 'Add a custom line item' => 'Add a custom line item', + 'Add a line item' => 'Add a line item', + 'Add a product' => 'Add a product', + 'Add a variant' => 'Add a variant', + 'Add an adjustment' => 'Add an adjustment', + 'Add an item' => 'Add an item', + 'Add an option' => 'Add an option', + 'Add catalog price' => 'Add catalogue price', + 'Add' => 'Add', + 'Additional Actions' => 'Additional Actions', + 'Additional recipients that should receive this email. Twig code can be used here.' => 'Additional recipients that should receive this email. Twig code can be used here.', + 'Address 1' => 'Address 1', + 'Address 2' => 'Address 2', + 'Address 3' => 'Address 3', + 'Address Line 1' => 'Address Line 1', + 'Address Line 2' => 'Address Line 2', + 'Address Updated.' => 'Address Updated.', + 'Address copied to user.' => 'Address copied to user.', + 'Address not found.' => 'Address not found.', + 'Adjust Quantity' => 'Adjust Quantity', + 'Adjust by' => 'Adjust by', + 'Adjust price when included rate is disqualified?' => 'Adjust price when included rate is disqualified?', + 'Adjustments' => 'Adjustments', + 'Admin Notices' => 'Admin Notices', + 'Administrative Area Code of Origin' => 'Administrative Area Code of Origin', + 'Advanced' => 'Advanced', + 'All Orders' => 'All Orders', + 'All Totals' => 'All Totals', + 'All Transfers' => 'All Transfers', + 'All active subscriptions' => 'All active subscriptions', + 'All customers' => 'All customers', + 'All products' => 'All products', + 'All variants must have a SKU.' => 'All variants must have a SKU.', + 'All' => 'All', + 'Allow Checkout Without Payment' => 'Allow Checkout Without Payment', + 'Allow Empty Cart On Checkout' => 'Allow Empty Cart On Checkout', + 'Allow Partial Payment On Checkout' => 'Allow Partial Payment On Checkout', + 'Allow out of stock purchases' => 'Allow out of stock purchases', + 'Allow' => 'Allow', + 'Allowed Qty' => 'Allowed Qty', + 'Alternative Phone' => 'Alternative Phone', + 'Amount' => 'Amount', + 'An ID must be provided' => 'An ID must be provided', + 'An error occurred while generating this PDF.' => 'An error occurred while generating this PDF.', + 'Any' => 'Any', + 'Anywhere' => 'Anywhere', + 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.', + 'Are you sure you want to capture this transaction?' => 'Are you sure you want to capture this transaction?', + 'Are you sure you want to complete this order?' => 'Are you sure you want to complete this order?', + 'Are you sure you want to delete the selected orders?' => 'Are you sure you want to delete the selected orders?', + 'Are you sure you want to delete the selected product and its variants?' => 'Are you sure you want to delete the selected product and its variants?', + 'Are you sure you want to delete this shipping rule?' => 'Are you sure you want to delete this shipping rule?', + 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.', + 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Are you sure you want to delete “{name}”? This will set all line items with this status to no status.', + 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.', + 'Are you sure you want to overwrite the billing address?' => 'Are you sure you want to overwrite the billing address?', + 'Are you sure you want to overwrite the shipping address?' => 'Are you sure you want to overwrite the shipping address?', + 'Are you sure you want to permanently delete this store and everything in it?' => 'Are you sure you want to permanently delete this store and everything in it?', + 'Are you sure you want to refund this transaction?' => 'Are you sure you want to refund this transaction?', + 'Are you sure you want to remove this customer?' => 'Are you sure you want to remove this customer?', + 'Are you sure you want to save this as a new shipping rule?' => 'Are you sure you want to save this as a new shipping rule?', + 'Are you sure you want to send email: {name}?' => 'Are you sure you want to send email: {name}?', + 'At least one site must be enabled for the product type.' => 'At least one site must be enabled for the product type.', + 'Attempted Payments' => 'Attempted Payments', + 'Attention' => 'Attention', + 'Authorize Only (Manually Capture)' => 'Authorise Only (Manually Capture)', + 'Auto Set Cart Shipping Method Option' => 'Auto Set Cart Shipping Method Option', + 'Auto Set New Cart Addresses' => 'Auto Set New Cart Addresses', + 'Auto Set Payment Source' => 'Auto Set Payment Source', + 'Automatic SKU Format' => 'Automatic SKU Format', + 'Available Shipping Categories' => 'Available Shipping Categories', + 'Available Tax Categories' => 'Available Tax Categories', + 'Available for purchase' => 'Available for purchase', + 'Available for purchase?' => 'Available for purchase?', + 'Available inventory for "{description}" has gone below zero.' => 'Available inventory for "{description}" has gone below zero.', + 'Available to Product Types' => 'Available to Product Types', + 'Available' => 'Available', + 'Available?' => 'Available?', + 'Average Order Total' => 'Average Order Total', + 'Average' => 'Average', + 'BCC’d Recipient' => 'BCC’d Recipient', + 'Bad Request' => 'Bad Request', + 'Bad address ID.' => 'Bad address ID.', + 'Bad order ID.' => 'Bad order ID.', + 'Base Price' => 'Base Price', + 'Base Promotional Price' => 'Base Promotional Price', + 'Base Rate' => 'Base Rate', + 'Base' => 'Base', + 'Bcc' => 'Bcc', + 'Billing Address' => 'Billing Address', + 'Billing Business Name' => 'Billing Business Name', + 'Billing First Name' => 'Billing First Name', + 'Billing Full Name' => 'Billing Full Name', + 'Billing Last Name' => 'Billing Last Name', + 'Billing address required.' => 'Billing address required.', + 'Billing detail update URL' => 'Billing detail update URL', + 'Billing issues' => 'Billing issues', + 'Billing' => 'Billing', + 'Both (Line item price + Line item shipping costs)' => 'Both (Line item price + Line item shipping costs)', + 'Business ID' => 'Business ID', + 'Business Name' => 'Business Name', + 'Business Tax ID' => 'Business Tax ID', + 'CC’d Recipient' => 'CC’d Recipient', + 'CVV' => 'CVV', + 'Can be used as an internal reference.' => 'Can be used as an internal reference.', + 'Can not complete payment for missing transaction.' => 'Cannot complete payment for missing transaction.', + 'Can not create a new order' => 'Cannot create a new order', + 'Can not find an order to pay.' => 'Cannot find an order to pay.', + 'Can not find enabled email.' => 'Cannot find enabled email.', + 'Can not find order' => 'Cannot find order', + 'Can not find order.' => 'Cannot find order.', + 'Can not find the transaction to refund' => 'Cannot find the transaction to refund', + 'Can not move between these inventory types.' => 'Cannot move between these inventory types.', + 'Can not refund amount greater than the remaining amount' => 'Cannot refund amount greater than the remaining amount', + 'Cancel subscription' => 'Cancel subscription', + 'Cancel with gateway now' => 'Cancel with gateway now', + 'Cancel' => 'Cancel', + 'Cancellation date' => 'Cancellation date', + 'Cancellation' => 'Cancellation', + 'Cannot switch plans for this subscription.' => 'Cannot switch plans for this subscription.', + 'Can’t preview this email.' => 'Can’t preview this email.', + 'Capture payment' => 'Capture payment', + 'Capture' => 'Capture', + 'Card Holder' => 'Card Holder', + 'Card Number' => 'Card Number', + 'Card' => 'Card', + 'Cart Recovery Link' => 'Cart Recovery Link', + 'Cart forgotten.' => 'Cart forgotten.', + 'Cart updated.' => 'Cart updated.', + 'Cart {number}' => 'Cart {number}', + 'Catalog Pricing Rule' => 'Catalogue Pricing Rule', + 'Catalog pricing rule description.' => 'Catalogue pricing rule description.', + 'Catalog pricing rule saved.' => 'Catalogue pricing rule saved.', + 'Catalog pricing rules deleted.' => 'Catalogue pricing rules deleted.', + 'Catalog pricing rules updated.' => 'Catalogue pricing rules updated.', + 'Categories Relationship Type' => 'Categories Relationship Type', + 'Categories' => 'Categories', + 'Category Rate Overrides' => 'Category Rate Overrides', + 'Centimeters (cm)' => 'Centimetres (cm)', + 'Changing this value may affect your ability to refund existing transactions.' => 'Changing this value may affect your ability to refund existing transactions.', + 'Choose a color to represent the order’s status' => 'Choose a colour to represent the order’s status', + 'Choose a new customer' => 'Choose a new customer', + 'Choose adjustment values to include when calculating the product revenue total.' => 'Choose adjustment values to include when calculating the product revenue total.', + 'Choose the currency’s ISO code.' => 'Choose the currency’s ISO code.', + 'Choose the destination inventory location for the existing on hand stock.' => 'Choose the destination inventory location for the existing on hand stock.', + 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Choose which sites this product type should be available in, and configure the site-specific settings.', + 'City' => 'City', + 'Clear counter' => 'Clear counter', + 'Clear notices' => 'Clear notices', + 'Close' => 'Close', + 'Code' => 'Code', + 'Collated PDF' => 'Collated PDF', + 'Color' => 'Colour', + 'Commerce Products' => 'Commerce Products', + 'Commerce Settings' => 'Commerce Settings', + 'Commerce Variants' => 'Commerce Variants', + 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Commerce email “{email}” could not be sent for order “{order}”.', + 'Commerce order exports' => 'Commerce order exports', + 'Commerce' => 'Commerce', + 'Committed' => 'Committed', + 'Completed Email' => 'Completed Email', + 'Completed' => 'Completed', + 'Completing order failed.' => 'Failed to complete order.', + 'Condition' => 'Condition', + 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availabililty early or if there are common conditions to all rules for this method.', + 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.', + 'Conditions' => 'Conditions', + 'Contains Purchasables' => 'Contains Purchasables', + 'Control Panel Settings' => 'Control Panel Settings', + 'Control panel' => 'Control panel', + 'Conversion Rate' => 'Conversion Rate', + 'Converted Price' => 'Converted Price', + 'Copied!' => 'Copied!', + 'Copy the URL' => 'Copy the URL', + 'Copy to {location}' => 'Copy to {location}', + 'Copy' => 'Copy', + 'Costs' => 'Costs', + 'Could not archive gateway.' => 'Could not archive gateway.', + 'Could not cancel “{reference}”.' => 'Could not cancel “{reference}”.', + 'Could not create the payment source.' => 'Could not create the payment source.', + 'Could not delete shipping rule' => 'Could not delete shipping rule', + 'Could not delete shipping zone' => 'Could not delete shipping zone', + 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.', + 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.', + 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.', + 'Could not find the email or template.' => 'Could not find the email or template.', + 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}', + 'Could not reactivate “{reference}”.' => 'Could not reactivate “{reference}”.', + 'Could not send email' => 'Could not send email', + 'Could not switch “{reference}” to “{plan}”.' => 'Could not switch “{reference}” to “{plan}”.', + 'Could not update orders address.' => 'Could not update orders address.', + 'Couldn’t archive Line Item Status.' => 'Couldn’t archive Line Item Status.', + 'Couldn’t archive Order Status.' => 'Couldn’t archive Order Status.', + 'Couldn’t capture transaction.' => 'Couldn’t capture transaction.', + 'Couldn’t capture transaction: {message}' => 'Couldn’t capture transaction: {message}', + 'Couldn’t delete email.' => 'Couldn’t delete email.', + 'Couldn’t delete the payment source.' => 'Couldn’t delete the payment source.', + 'Couldn’t get order.' => 'Couldn’t get order.', + 'Couldn’t recalculate order.' => 'Couldn’t recalculate order.', + 'Couldn’t refund transaction.' => 'Couldn’t refund transaction.', + 'Couldn’t refund transaction: {message}' => 'Couldn’t refund transaction: {message}', + 'Couldn’t reorder Line Item Statuses.' => 'Couldn’t reorder Line Item Statuses.', + 'Couldn’t reorder Order Statuses.' => 'Couldn’t reorder Order Statuses.', + 'Couldn’t reorder PDFs.' => 'Couldn’t reorder PDFs.', + 'Couldn’t reorder discounts.' => 'Couldn’t reorder discounts.', + 'Couldn’t reorder gateways.' => 'Couldn’t reorder gateways.', + 'Couldn’t reorder plans.' => 'Couldn’t reorder plans.', + 'Couldn’t reorder rules.' => 'Couldn’t reorder rules.', + 'Couldn’t reorder sale.' => 'Couldn’t reorder sale.', + 'Couldn’t reorder sales.' => 'Couldn’t reorder sales.', + 'Couldn’t reorder statuses.' => 'Couldn’t reorder statuses.', + 'Couldn’t reorder stores.' => 'Couldn’t reorder stores.', + 'Couldn’t save PDF.' => 'Couldn’t save PDF.', + 'Couldn’t save catalog pricing rule.' => 'Couldn’t save catalog pricing rule.', + 'Couldn’t save currency.' => 'Couldn’t save currency.', + 'Couldn’t save discount.' => 'Couldn’t save discount.', + 'Couldn’t save email.' => 'Couldn’t save email.', + 'Couldn’t save gateway.' => 'Couldn’t save gateway.', + 'Couldn’t save inventory location.' => 'Couldn’t save inventory location.', + 'Couldn’t save line item status.' => 'Couldn’t save line item status.', + 'Couldn’t save order fields.' => 'Couldn’t save order fields.', + 'Couldn’t save order status.' => 'Couldn’t save order status.', + 'Couldn’t save order.' => 'Couldn’t save order.', + 'Couldn’t save product type.' => 'Couldn’t save product type.', + 'Couldn’t save sale.' => 'Couldn’t save sale.', + 'Couldn’t save settings.' => 'Couldn’t save settings.', + 'Couldn’t save shipping category.' => 'Couldn’t save shipping category.', + 'Couldn’t save shipping method.' => 'Couldn’t save shipping method.', + 'Couldn’t save shipping rule.' => 'Couldn’t save shipping rule.', + 'Couldn’t save shipping zone.' => 'Couldn’t save shipping zone.', + 'Couldn’t save store.' => 'Could not save store.', + 'Couldn’t save subscription fields.' => 'Couldn’t save subscription fields.', + 'Couldn’t save subscription plan.' => 'Couldn’t save subscription plan.', + 'Couldn’t save subscription.' => 'Couldn’t save subscription.', + 'Couldn’t save tax category.' => 'Couldn’t save tax category.', + 'Couldn’t save tax rate.' => 'Couldn’t save tax rate.', + 'Couldn’t save tax zone.' => 'Couldn’t save tax zone.', + 'Couldn’t save transfer fields.' => 'Couldn’t save transfer fields.', + 'Couldn’t update catalog pricing rule statuses.' => 'Couldn’t update catalogue pricing rules status.', + 'Couldn’t update status.' => 'Couldn’t update status.', + 'Couldn’t updated sales status.' => 'Couldn’t update sales status.', + 'Country Code of Origin' => 'Country Code of Origin', + 'Country List' => 'Country List', + 'Country not allowed.' => 'Country not allowed.', + 'Country' => 'Country', + 'Coupon Code' => 'Coupon Code', + 'Coupon can not apply discount to this order due to address mismatch.' => 'Coupon can not apply discount to this order due to address mismatch.', + 'Coupon can not apply discount to this order due to customer mismatch.' => 'Coupon can not apply discount to this order due to customer mismatch.', + 'Coupon can not apply discount to this order.' => 'Coupon can not apply discount to this order.', + 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Coupon code “{code}” is already in use by discount “{name}”.', + 'Coupon codes cannot be blank.' => 'Coupon codes cannot be blank.', + 'Coupon codes must be unique.' => 'Coupon codes must be unique.', + 'Coupon format is required and must contain at least one `#`.' => 'Coupon format is required and must contain at least one # sign.', + 'Coupon not valid.' => 'Coupon not valid.', + 'Coupon removed: {explanation}' => 'Coupon removed: {explanation}', + 'Coupons' => 'Coupons', + 'Craft Commerce - Administration' => 'Craft Commerce - Administration', + 'Craft Commerce - Inventory' => 'Craft Commerce - Inventory', + 'Craft Commerce - Orders' => 'Craft Commerce - Orders', + 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Product Type - {name}', + 'Craft Commerce - Subscriptions' => 'Craft Commerce - Subscriptions', + 'Create a Discount' => 'Create a Discount', + 'Create a Subscription Plan' => 'Create a Subscription Plan', + 'Create a new PDF' => 'Create a new PDF', + 'Create a new catalog pricing rule' => 'Create a new catalogue pricing rule', + 'Create a new currency' => 'Create a new currency', + 'Create a new email' => 'Create a new email', + 'Create a new gateway' => 'Create a new gateway', + 'Create a new line item status' => 'Create a new line item status', + 'Create a new order status' => 'Create a new order status', + 'Create a new product type' => 'Create a new product type', + 'Create a new sale' => 'Create a new sale', + 'Create a new shipping category' => 'Create a new shipping category', + 'Create a new shipping method' => 'Create a new shipping method', + 'Create a new shipping rule' => 'Create a new shipping rule', + 'Create a new tax category' => 'Create a new tax category', + 'Create a new tax rate' => 'Create a new tax rate', + 'Create a product type' => 'Create a product type', + 'Create a shipping zone' => 'Create a shipping zone', + 'Create a tax zone' => 'Create a tax zone', + 'Create catalog pricing rules' => 'Create catalogue pricing rules', + 'Create customer: “{email}”' => 'Create customer: “{email}”', + 'Create discounts' => 'Create discounts', + 'Create discount…' => 'Create discount…', + 'Create rules that allow this discount to match the order.' => 'Create rules that allow this discount to match the order.', + 'Create rules that allow this discount to match the order’s billing address.' => 'Create rules that allow this discount to match the order’s billing address.', + 'Create rules that allow this discount to match the order’s customer.' => 'Create rules that allow this discount to match the order’s customer.', + 'Create rules that allow this discount to match the order’s shipping address.' => 'Create rules that allow this discount to match the order’s shipping address.', + 'Create rules that allow this gateway to match the billing address.' => 'Create rules that allow this gateway to match the billing address.', + 'Create rules that allow this gateway to match the order.' => 'Create rules that allow this gateway to match the order.', + 'Create rules that allow this gateway to match the shipping address.' => 'Create rules that allow this gateway to match the shipping address.', + 'Create sales' => 'Create sales', + 'Create sale…' => 'Create sale…', + 'Created' => 'Created', + 'Credit Card Payment Type' => 'Credit Card Payment Type', + 'Currency Code' => 'Currency Code', + 'Currency saved.' => 'Currency saved.', + 'Currency' => 'Currency', + 'Current' => 'Current', + 'Custom 1' => 'Custom 1', + 'Custom 2' => 'Custom 2', + 'Custom 3' => 'Custom 3', + 'Custom 4' => 'Custom 4', + 'Custom' => 'Custom', + 'Customer Enabled?' => 'Customer Enabled?', + 'Customer ID is required.' => 'Customer ID is required.', + 'Customer Note' => 'Customer Note', + 'Customer Notices' => 'Customer Notices', + 'Customer data' => 'Customer data', + 'Customer' => 'Customer', + 'Damaged' => 'Damaged', + 'Data shown might be outdated.' => 'Data shown might be outdated.', + 'Date Authorized' => 'Date Authorised', + 'Date Created' => 'Date Created', + 'Date First Paid' => 'Date First Paid', + 'Date Ordered' => 'Date Ordered', + 'Date Paid' => 'Date Paid', + 'Date Updated' => 'Date Updated', + 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Date from which the catalogue pricing rule will be active. Leave blank for unlimited start date', + 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Date from which the discount will be active. Leave blank for unlimited start date', + 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Date from which the sale will be active. Leave blank for unlimited start date', + 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Date when the catalogue pricing rule will be finished. Leave blank for unlimited end date', + 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Date when the discount will be finished. Leave blank for unlimited end date', + 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Date when the sale will be finished. Leave blank for unlimited end date', + 'Date' => 'Date', + 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Default - Allow the price to be negative if discounts are greater than the order value.', + 'Default Category' => 'Default Category', + 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.', + 'Default Order PDF' => 'Default Order PDF', + 'Default Per Item Rate' => 'Default Per Item Rate', + 'Default Percentage Rate' => 'Default Percentage Rate', + 'Default Status?' => 'Default Status?', + 'Default View' => 'Default View', + 'Default Weight Rate' => 'Default Weight Rate', + 'Default Zone' => 'Default Zone', + 'Default status?' => 'Default status?', + 'Default to this tax zone when no billing address is set' => 'Default to this tax zone when no billing address is set', + 'Default to this tax zone when no shipping address is set' => 'Default to this tax zone when no shipping address is set', + 'Default variant updated.' => 'Default variant updated.', + 'Default' => 'Default', + 'Default?' => 'Default?', + 'Delete catalog pricing rules' => 'Delete catalogue pricing rules', + 'Delete discounts' => 'Delete discounts', + 'Delete orders' => 'Delete orders', + 'Delete sales' => 'Delete sales', + 'Delete' => 'Delete', + 'Deleting the {location} location.' => 'Deleting the {location} location.', + 'Describe this rule.' => 'Describe this rule.', + 'Describe this shipping zone.' => 'Describe this shipping zone.', + 'Describe this tax zone.' => 'Describe this tax zone.', + 'Description' => 'Description', + 'Destination Inventory Location' => 'Destination Inventory Location', + 'Destination' => 'Destination', + 'Details' => 'Details', + 'Dimension Unit' => 'Dimension Unit', + 'Dimensions' => 'Dimensions', + 'Disabled' => 'Disabled', + 'Disallow' => 'Disallow', + 'Discount all line items' => 'Discount all line items', + 'Discount description.' => 'Discount description.', + 'Discount is not allowed for the order' => 'Discount is not allowed for order', + 'Discount is out of date.' => 'Discount is out of date.', + 'Discount saved.' => 'Discount saved.', + 'Discount the matching items only' => 'Discount matching items only', + 'Discount use has reached its limit.' => 'Discount use has reached its limit.', + 'Discount' => 'Discount', + 'Discounted Item Subtotal' => 'Discounted Item Subtotal', + 'Discounted Items' => 'Discounted Items', + 'Discounts deleted.' => 'Discounts deleted.', + 'Discounts reordered.' => 'Discounts reordered.', + 'Discounts updated.' => 'Discounts updated.', + 'Discounts' => 'Discounts', + 'Disqualify with valid business tax ID?' => 'Disqualify with valid business tax ID?', + 'Do not apply subsequent matching sales beyond applying this sale.' => 'Do not apply subsequent matching sales beyond applying this sale.', + 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Do not apply this rate if the order address has any of the selected valid business tax IDs.', + 'Do not attach a PDF to this email' => 'Do not attach a PDF to this email', + 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.', + 'Donation can not be zero.' => 'Donation cannot be zero.', + 'Donation needs to be an amount.' => 'Donation needs to be an amount.', + 'Donation settings saved.' => 'Donation settings saved.', + 'Donation' => 'Donation', + 'Donations' => 'Donations', + 'Done' => 'Done', + 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Don’t apply any subsequent discounts to an order if this discount is applied', + 'Download PDF' => 'Download PDF', + 'Download PDF…' => 'Download PDF…', + 'Download Type' => 'Download Type', + 'Download' => 'Download', + 'Draft' => 'Draft', + 'Dummy gateway payment failed.' => 'Dummy gateway payment failed.', + 'Duplicate options exist' => 'Duplicate options exist', + 'Duration' => 'Duration', + 'EU VAT ID' => 'EU VAT ID', + 'Edit address' => 'Edit address', + 'Edit adjustments' => 'Edit adjustments', + 'Edit catalog pricing rules' => 'Edit catalogue pricing rules', + 'Edit discounts' => 'Edit discounts', + 'Edit options' => 'Edit options', + 'Edit orders' => 'Edit orders', + 'Edit sales' => 'Edit sales', + 'Edit' => 'Edit', + 'Effect' => 'Effect', + 'Either (Default) - The relationship field is on the purchasable or the category' => 'Either (Default) - The relationship field is on the purchasable or the category', + 'Either way' => 'Either way', + 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}', + 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.', + 'Email Subject' => 'Email Subject', + 'Email error. No email address found for order. Order: “{order}”' => 'Email error. No email address found for order. Order: “{order}”', + 'Email is not enabled.' => 'Email is not enabled.', + 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.', + 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email required to make payments on a completed order.' => 'Email required to make payments on a completed order.', + 'Email saved.' => 'Email saved.', + 'Email sent' => 'Email sent', + 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.', + 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email unavailable.' => 'Email unavailable.', + 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}', + 'Email “{email}” for order {order} was cancelled.' => 'Email “{email}” for order {order} was cancelled.', + 'Email' => 'Email', + 'Emails' => 'Emails', + 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.', + 'Enable structure for products of this type' => 'Enable structure for products of this type', + 'Enable this discount' => 'Enable this discount', + 'Enable this rule' => 'Enable this rule', + 'Enable this sale' => 'Enable this sale', + 'Enable this shipping method on the front end' => 'Enable this shipping method on the front end', + 'Enable this shipping rule' => 'Enable this shipping rule', + 'Enable this tax rate' => 'Enable this tax rate', + 'Enabled for customers to select during checkout?' => 'Enabled for customers to select during checkout?', + 'Enabled for customers to select?' => 'Enabled for customers to select?', + 'Enabled' => 'Enabled', + 'Enabled?' => 'Enabled?', + 'End Date' => 'End Date', + 'Enter SKU' => 'Enter SKU', + 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Enter a human-friendly name for this tax rate to be used in the control panel.', + 'Enter a percentage like {ex1} or {ex2}.' => 'Enter a percentage like {ex1} or {ex2}.', + 'Enter coupon code' => 'Enter coupon code', + 'Enter reference' => 'Enter reference', + 'Error refunding transaction: {transactionHash}' => 'Error refunding transaction: {transactionHash}', + 'Every new store must be assigned to at least one site.' => 'Every new store must be assigned to at least one site.', + 'Everywhere' => 'Everywhere', + 'Example' => 'Example', + 'Exclude this discount for products that are already on promotion' => 'Exclude this discount for products that are already on promotion', + 'Expired Link' => 'Expired Link', + 'Expired' => 'Expired', + 'Expiry Date' => 'Expiry Date', + 'Expiry date' => 'Expiry date', + 'Expiry' => 'Expiry', + 'Failed to receive transfer: {error}' => 'Failed to receive transfer: {error}', + 'Failed to send email. Please try again.' => 'Failed to send email. Please try again.', + 'Failed to start' => 'Failed to start', + 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Failed to update {num, plural, one {}=1{order status} other{order statuses}}.', + 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Failed updating order status on {num, plural, one {}=1{order} other{orders}}.', + 'Feet (ft)' => 'Feet (ft)', + 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Filtering conditions which describe which orders this rule is applicable to. Write 0 to skip a condition.', + 'First Name' => 'First Name', + 'Flat Amount Off Order' => 'Flat Amount Off Order', + 'Flat Order Discount Amount Off' => 'Flat Order Discount Amount Off', + 'Free Order Payment Strategy' => 'Free Order Payment Strategy', + 'Free Shipping' => 'Free Shipping', + 'Free orders are processed by the payment gateway' => 'Free orders are processed by the payment gateway', + 'Free orders complete immediately' => 'Free orders complete immediately', + 'Free shipping can only be for whole order or matching items, not both.' => 'Free shipping can only be for either the whole order or matching items, not both.', + 'From Name' => 'From Name', + 'Fulfill' => 'Fulfill', + 'Fulfilled' => 'Fulfilled', + 'Fulfillment' => 'Fulfillment', + 'Full Name' => 'Full Name', + 'Gateway Code' => 'Gateway Code', + 'Gateway Message' => 'Gateway Message', + 'Gateway Reference' => 'Gateway Reference', + 'Gateway Response' => 'Gateway Response', + 'Gateway doesn’t support authorize' => 'Gateway doesn’t support authorise', + 'Gateway doesn’t support partial refunds.' => 'Gateway doesn’t support partial refunds.', + 'Gateway doesn’t support purchase' => 'Gateway doesn’t support purchase', + 'Gateway doesn’t support refunds.' => 'Gateway doesn’t support refunds.', + 'Gateway saved.' => 'Gateway saved.', + 'Gateway' => 'Gateway', + 'Gateways reordered.' => 'Gateways reordered.', + 'Gateways' => 'Gateways', + 'General Settings' => 'General Settings', + 'General' => 'General', + 'Generate' => 'Generate', + 'Generated Coupon Format' => 'Generated Coupon Format', + 'Grams (g)' => 'Grams (g)', + 'Groups for which this sale will be applicable to.' => 'Groups for which this sale will be applicable.', + 'HTML Email Template Path' => 'HTML Email Template Path', + 'Handle' => 'Handle', + 'Harmonized System Code' => 'Harmonized System Code', + 'Has Admin Notices' => 'Has Admin Notices', + 'Has Emails?' => 'Has Emails?', + 'Has Free Shipping' => 'Has Free Shipping', + 'Has Orders' => 'Has Orders', + 'Has Purchasable' => 'Has Purchasable', + 'Has Variants?' => 'Has Variants?', + 'Height ({unit})' => 'Height ({unit})', + 'Height' => 'Height', + 'Hide snapshot' => 'Hide snapshot', + 'History' => 'History', + 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).', + 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.', + 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.', + 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.', + 'How products should be labeled within the control panel.' => 'How products should be labelled within the control panel.', + 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).', + 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}', + 'How this shipping method will be referred to in templates and forms.' => 'How this shipping method will be referred to in templates and forms.', + 'How variants should be labeled within the control panel.' => 'How variants should be labelled within the control panel.', + 'How you’ll refer to this PDF in the templates.' => 'How you’ll refer to this PDF in the templates.', + 'How you’ll refer to this product type in the templates.' => 'How you’ll refer to this product type in the templates.', + 'How you’ll refer to this shipping category in the templates.' => 'How you’ll refer to this shipping category in the templates.', + 'How you’ll refer to this status in the templates.' => 'How you’ll refer to this status in the templates.', + 'How you’ll refer to this subscription plan in the templates.' => 'How you’ll refer to this subscription plan in the templates.', + 'How you’ll refer to this tax category in the templates.' => 'How you’ll refer to this tax category in the templates.', + 'ID' => 'ID', + 'IP Address' => 'IP Address', + 'If disabled, this PDF will not be available or sent with emails.' => 'If disabled, this PDF will not be available or sent with emails.', + 'If disabled, this email will not send.' => 'If disabled, this email will not send.', + 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.', + 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'If set to Authorise Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.', + 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.', + 'Ignore Promotions?' => 'Ignore Promotions?', + 'Ignore previous matching sales if this sale matches.' => 'Ignore previous matching sales if this sale matches.', + 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignore promotional prices when this discount is applied to matching line items', + 'Inactive Carts' => 'Inactive Carts', + 'Inches (in)' => 'Inches (in)', + 'Include built-in line item tax.' => 'Include built-in line item tax.', + 'Include in price?' => 'Include in price?', + 'Include line item discounts.' => 'Include line item discounts.', + 'Include line item shipping costs.' => 'Include line item shipping costs.', + 'Include separate line item tax.' => 'Include separate line item tax.', + 'Included in price?' => 'Included in price?', + 'Included' => 'Included', + 'Incoming transfer from Transfer ID: ' => 'Incoming transfer from Transfer ID: ', + 'Incoming' => 'Incoming', + 'Info' => 'Info', + 'Information linked?' => 'Information linked?', + 'Information' => 'Information', + 'Invalid JSON' => 'Invalid JSON', + 'Invalid Order ID' => 'Invalid Order ID', + 'Invalid VAT ID.' => 'Invalid VAT ID.', + 'Invalid condition syntax' => 'Invalid condition syntax', + 'Invalid email.' => 'Invalid email.', + 'Invalid formula syntax' => 'Invalid formula syntax', + 'Invalid gateway: {value}' => 'Invalid gateway: {value}', + 'Invalid inventory movements.' => 'Invalid inventory movements.', + 'Invalid order condition syntax.' => 'Invalid order condition syntax.', + 'Invalid payment or order. Please review.' => 'Invalid payment or order. Please review.', + 'Invalid payment source ID: {value}' => 'Invalid payment source ID: {value}', + 'Invalid store.' => 'Invalid store.', + 'Invalid user.' => 'Invalid user.', + 'Inventory Item' => 'Inventory Item', + 'Inventory Location' => 'Inventory Location', + 'Inventory Locations' => 'Inventory Locations', + 'Inventory Tracked' => 'Inventory Tracked', + 'Inventory Transfers' => 'Inventory Transfers', + 'Inventory could not be set.' => 'Inventory could not be set.', + 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'Inventory location has committed stock, the order(s) must first be fulfilled.', + 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Inventory location has incoming stock, the transfer(s) must first be completed.', + 'Inventory location is already deactivated.' => 'Inventory location is already deactivated.', + 'Inventory location saved.' => 'Inventory location saved.', + 'Inventory locations not saved.' => 'Inventory locations not saved.', + 'Inventory movement could not be saved.' => 'Inventory movement could not be saved.', + 'Inventory movement saved.' => 'Inventory movement saved.', + 'Inventory updated.' => 'Inventory updated.', + 'Inventory was not updated.' => 'Inventory was not updated.', + 'Inventory' => 'Inventory', + 'Invoice amount' => 'Invoice amount', + 'Invoice date' => 'Invoice date', + 'Is Promotable' => 'Is Promotable', + 'Is Promotional Price?' => 'Is Promotional Price?', + 'Is Shippable' => 'Is Shippable', + 'Is Taxable' => 'Is Taxable', + 'Item Rates' => 'Item Rates', + 'Item Subtotal' => 'Item Subtotal', + 'Item Total' => 'Item Total', + 'Item' => 'Item', + 'Items' => 'Items', + 'Kilograms (kg)' => 'Kilograms (kg)', + 'Label' => 'Label', + 'Landscape' => 'Landscape', + 'Language' => 'Language', + 'Last Name' => 'Last Name', + 'Last Updated' => 'Last Updated', + 'Leave a category rate override blank to use the rate from above.' => 'Leave a category rate override blank to use the rate from above.', + 'Leave blank for unlimited uses.' => 'Leave blank for unlimited uses.', + 'Leave blank if products don’t have URLs' => 'Leave blank if products don’t have URLs', + 'Leave gateway subscription as-is' => 'Leave gateway subscription as-is', + 'Length ({unit})' => 'Length ({unit})', + 'Length' => 'Length', + 'Let each product choose which sites it should be saved to' => 'Let each product choose which sites it should be saved to', + 'Limit which orders this discount applies to based on its line items.' => 'Limit which orders this discount applies to based on its line items.', + 'Limit which purchasables this sale applies to.' => 'Limit which purchasables this sale applies to.', + 'Limit' => 'Limit', + 'Line Item Statuses' => 'Line Item Statuses', + 'Line Item' => 'Line Item', + 'Line Items' => 'Line Items', + 'Line item price (minus discounts)' => 'Line item price (minus discounts)', + 'Line item shipping cost' => 'Line item shipping cost', + 'Line item statuses reordered.' => 'Line item statuses reordered.', + 'Link Duration' => 'Link Duration', + 'Link Sent' => 'Link Sent', + 'Link to a product' => 'Link to a product', + 'Link to a variant' => 'Link to a variant', + 'Link' => 'Link', + 'Live' => 'Live', + 'Location' => 'Location', + 'Locations that should be available for previewing products in this product type.' => 'Locations that should be available for previewing products in this product type.', + 'MM' => 'MM', + 'Make a payment' => 'Make a payment', + 'Make this the primary store' => 'Make this the primary store', + 'Manage Inventory' => 'Manage Inventory', + 'Manage donation settings' => 'Manage donation settings', + 'Manage general store settings' => 'Manage general store settings', + 'Manage inventory locations' => 'Manage inventory locations', + 'Manage inventory stock levels' => 'Manage inventory stock levels', + 'Manage inventory transfers' => 'Manage inventory transfers', + 'Manage orders' => 'Manage orders', + 'Manage payment currencies' => 'Manage payment currencies', + 'Manage promotions' => 'Manage promotions', + 'Manage shipping' => 'Manage shipping', + 'Manage store settings' => 'Manage store settings', + 'Manage subscription plans' => 'Manage subscription plans', + 'Manage subscription' => 'Manage subscription', + 'Manage subscriptions' => 'Manage subscriptions', + 'Manage taxes' => 'Manage taxes', + 'Manage' => 'Manage', + 'Mark as Pending' => 'Mark as Pending', + 'Mark as completed' => 'Mark as completed', + 'Match Billing Address' => 'Match Billing Address', + 'Match Customer' => 'Match Customer', + 'Match Order' => 'Match Order', + 'Match Orders' => 'Match Orders', + 'Match Product' => 'Match Product', + 'Match Purchasable' => 'Match Purchasable', + 'Match Shipping Address' => 'Match Shipping Address', + 'Match Variant' => 'Match Variant', + 'Matching Items' => 'Matching Items', + 'Max Qty' => 'Max Qty', + 'Max Uses' => 'Max Uses', + 'Max Variants' => 'Max Variants', + 'Max quantity must greater than min.' => 'Max quantity must greater than min.', + 'Maximum Purchase Quantity' => 'Maximum Purchase Quantity', + 'Maximum Total Shipping Cost' => 'Maximum Total Shipping Cost', + 'Maximum allowed quantity' => 'Maximum allowed quantity', + 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.', + 'Maximum order quantity for this item is {num}.' => 'Maximum order quantity for this item is {num}.', + 'Message' => 'Message', + 'Meters (m)' => 'Metres (m)', + 'Millimeters (mm)' => 'Millimetres (mm)', + 'Min Qty' => 'Min Qty', + 'Min quantity must be less than max.' => 'Min quantity must be less than max.', + 'Minimum Purchase Quantity' => 'Minimum Purchase Quantity', + 'Minimum Total Price Strategy' => 'Minimum Total Price Strategy', + 'Minimum Total Shipping Cost' => 'Minimum Total Shipping Cost', + 'Minimum allowed quantity' => 'Minimum allowed quantity', + 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Minimum number of matching items that need to be ordered for this discount to apply.', + 'Minimum order quantity for this item is {num}.' => 'Minimum order quantity for this item is {num}.', + 'Missing Gateway' => 'Missing Gateway', + 'Missing a default inventory location.' => 'Missing a default inventory location.', + 'Move Inventory' => 'Move Inventory', + 'Move To' => 'Move To', + 'Move {qty} from {fromType} to {toType}' => 'Move {qty} from {fromType} to {toType}', + 'Move' => 'Move', + 'Movement from deactivated inventory location' => 'Movement from deactivated inventory location', + 'Movement' => 'Movement', + 'Must have at least one variant.' => 'Must have at least one variant.', + 'Name Field' => 'Name Field', + 'Name' => 'Name', + 'New Customer' => 'New Customer', + 'New Customers' => 'New Customers', + 'New Order' => 'New order', + 'New PDF' => 'New PDF', + 'New address' => 'New address', + 'New catalog pricing rule' => 'New catalogue pricing rule', + 'New currency' => 'New currency', + 'New discount' => 'New discount', + 'New email' => 'New email', + 'New gateway' => 'New gateway', + 'New line item status' => 'New line item status', + 'New line items get this status by default when the order is completed' => 'New line items get this status by default when the order is completed', + 'New location' => 'New location', + 'New order status' => 'New order status', + 'New orders get this status by default' => 'New orders get this status by default', + 'New product type' => 'New product type', + 'New product' => 'New product', + 'New product, choose a type' => 'New product, choose a type', + 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'New products default to the first tax category available to them. If none are available, this category will be used.', + 'New sale' => 'New sale', + 'New shipping category' => 'New shipping category', + 'New shipping method' => 'New shipping method', + 'New shipping rule' => 'New shipping rule', + 'New shipping zone' => 'New shipping zone', + 'New subscription plan' => 'New subscription plan', + 'New tax category' => 'New tax category', + 'New tax rate' => 'New tax rate', + 'New tax zone' => 'New tax zone', + 'New transfer' => 'New transfer', + 'New {productType} product' => 'New {productType} product', + 'New' => 'New', + 'Next payment' => 'Next payment', + 'No Address' => 'No Address', + 'No PDFs exist yet.' => 'No PDFs exist yet.', + 'No access given to any specific store management features.' => 'No access given to any specific store management features.', + 'No additional payment currencies exist yet.' => 'No additional payment currencies exist yet.', + 'No address' => 'No address', + 'No billing address' => 'No billing address', + 'No catalog pricing rule exists with the ID “{id}”' => 'No catalogue pricing rule exists with the ID “{id}”', + 'No catalog pricing rules exist yet.' => 'No catalogue pricing rules exist yet.', + 'No currency exists with the ID “{id}”' => 'No currency exists with the ID “{id}”', + 'No customer email address exists on this cart.' => 'No customer email address exists on this cart.', + 'No description' => 'No description', + 'No discount exists with the ID “{id}”' => 'No discount exists with the ID “{id}”', + 'No discounts exist yet.' => 'No discounts exist yet.', + 'No donation amount supplied.' => 'No donation amount supplied.', + 'No emails exist yet.' => 'No emails exist yet.', + 'No inventory changes made.' => 'No inventory changes made.', + 'No inventory found.' => 'No inventory found.', + 'No inventory movements made.' => 'No inventory movements made.', + 'No inventory transactions for this location.' => 'No inventory transactions for this location.', + 'No new customer selected.' => 'No new customer selected.', + 'No order history exists with the ID “{id}”' => 'No order history exists with the ID “{id}”', + 'No order status history items will exist until the cart becomes an order.' => 'No order status history items will exist until the cart becomes an order.', + 'No payment source exists with the ID “{id}”' => 'No payment source exists with the ID “{id}”', + 'No private Note.' => 'No private Note.', + 'No product available.' => 'No product available.', + 'No product types exist yet.' => 'No product types exist yet.', + 'No purchasable available.' => 'No purchasable available.', + 'No sale exists with the ID “{id}”' => 'No sale exists with the ID “{id}”', + 'No sales exist yet.' => 'No sales exist yet.', + 'No shipping address' => 'No shipping address', + 'No shipping category exists with the ID “{id}”' => 'No shipping category exists with the ID “{id}”', + 'No shipping method exists with the ID “{id}”' => 'No shipping method exists with the ID “{id}”', + 'No shipping rule exists with the ID “{id}”' => 'No shipping rule exists with the ID “{id}”', + 'No shipping rules exist yet.' => 'No shipping rules exist yet.', + 'No shipping zone exists with the ID “{id}”' => 'No shipping zone exists with the ID “{id}”', + 'No stats available.' => 'No stats available.', + 'No subscription plan exists with the ID “{id}”' => 'No subscription plan exists with the ID “{id}”', + 'No subscription plans exist yet.' => 'No subscription plans exist yet.', + 'No tax category exists with the ID “{id}”' => 'No tax category exists with the ID “{id}”', + 'No tax rate exists with the ID “{id}”' => 'No tax rate exists with the ID “{id}”', + 'No tax zone exists with the ID “{id}”' => 'No tax zone exists with the ID “{id}”', + 'No transactions exist.' => 'No transactions exist.', + 'No user authenticated.' => 'No user authenticated.', + 'No' => 'No', + 'None on hand' => 'None on hand', + 'None' => 'None', + 'Not a valid address type' => 'Not a valid address type', + 'Not a valid credit card number.' => 'Not a valid credit card number.', + 'Not all SKUs are unique.' => 'Not all SKUs are unique.', + 'Note' => 'Note', + 'Notes' => 'Notes', + 'Number of Coupons' => 'Number of Coupons', + 'Number' => 'Number', + 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Of the enabled sites above, which sites should products in this product type be saved to?', + 'On Hand' => 'On Hand', + 'Only allow this gateway to be used for zero value orders?' => 'Only allow this gateway to be used for zero value orders?', + 'Only match certain purchasables…' => 'Only match certain purchasables…', + 'Only match purchasables related to…' => 'Only match purchasables related to…', + 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Only orders with the following order statuses will be included. Leave blank to include all statuses.', + 'Only save product to the site they were created in' => 'Only save product to the site they were created in', + 'Options' => 'Options', + 'Order Condition Formula' => 'Order Condition Formula', + 'Order Description Format' => 'Order Description Format', + 'Order Details' => 'Order Details', + 'Order Fields' => 'Order Fields', + 'Order PDF Download Link' => 'Order PDF Download Link', + 'Order PDF Filename Format' => 'Order PDF Filename Format', + 'Order Reference Number Format' => 'Order Reference Number Format', + 'Order Settings' => 'Order Settings', + 'Order Site' => 'Order Site', + 'Order Status description.' => 'Order Status description.', + 'Order Status' => 'Order Status', + 'Order Statuses' => 'Order Statuses', + 'Order can not be empty.' => 'Order cannot be empty.', + 'Order count' => 'Order count', + 'Order customer data removed.' => 'Order customer data removed.', + 'Order deleted.' => 'Order deleted.', + 'Order fields saved.' => 'Order fields saved.', + 'Order not found.' => 'Order not found.', + 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.', + 'Order recalculated.' => 'Order recalculated.', + 'Order status saved.' => 'Order status saved.', + 'Order statuses reordered.' => 'Order statuses reordered.', + 'Order total shipping cost' => 'Order total shipping cost', + 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)', + 'Order' => 'Order', + 'Orders (Legacy)' => 'Orders (Legacy)', + 'Orders deleted.' => 'Orders deleted.', + 'Orders not restored.' => 'Orders not restored.', + 'Orders restored.' => 'Orders restored.', + 'Orders' => 'Orders', + 'Organization Name' => 'Organisation Name', + 'Organization Tax ID' => 'Organisation Tax ID', + 'Origin and destination cannot be the same.' => 'Origin and destination cannot be the same.', + 'Origin' => 'Origin', + 'Original Price' => 'Original Price', + 'Original price' => 'Original price', + 'Original promotional price' => 'Original promotional price', + 'Other Languages' => 'Other Languages', + 'Other countries' => 'Other countries', + 'Outgoing transfer from Transfer ID: ' => 'Outgoing transfer from Transfer ID: ', + 'Overpaid' => 'Overpaid', + 'Overrides previous?' => 'Overrides previous?', + 'PDF Attachment' => 'PDF Attachment', + 'PDF Template Path' => 'PDF Template Path', + 'PDF saved.' => 'PDF saved.', + 'PDF' => 'PDF', + 'PDFs & Emails' => 'PDFs & Emails', + 'PDFs' => 'PDFs', + 'Paid Amount' => 'Paid Amount', + 'Paid Status' => 'Paid Status', + 'Paid' => 'Paid', + 'Paper Orientation' => 'Paper Orientation', + 'Paper Size' => 'Paper Size', + 'Partial payment not allowed.' => 'Partial payment not allowed.', + 'Partial' => 'Partial', + 'Past year' => 'Past year', + 'Past {num} days' => 'Past {num} days', + 'Pay {amount} of {currency} on the order.' => 'Pay {amount} {currency} on the order.', + 'Pay' => 'Pay', + 'Payment Amount' => 'Payment Amount', + 'Payment Currencies' => 'Payment Currencies', + 'Payment Gateway' => 'Payment Gateway', + 'Payment Method' => 'Payment Method', + 'Payment error: {message}' => 'Payment error: {message}', + 'Payment method issue' => 'Payment method issue', + 'Payment source created.' => 'Payment source created.', + 'Payment source deleted.' => 'Payment source deleted.', + 'Payments' => 'Payments', + 'Pending' => 'Pending', + 'Per Email Address Discount Limit' => 'Per Email Address Discount Limit', + 'Per Item Amount Off' => 'Per Item Amount Off', + 'Per Item Discount' => 'Per Item Discount', + 'Per Item Percentage Off' => 'Per Item Percentage Off', + 'Per Item Rate' => 'Per Item Rate', + 'Per User Discount Limit' => 'Per User Discount Limit', + 'Percentage Rate' => 'Percentage Rate', + 'Phone (Alt)' => 'Phone (Alt)', + 'Phone' => 'Phone', + 'Pick a plan' => 'Pick a plan', + 'Plain Text Email Template Path' => 'Plain Text Email Template Path', + 'Plan' => 'Plan', + 'Plans reordered.' => 'Plans reordered.', + 'Portrait' => 'Portrait', + 'Post Date' => 'Post Date', + 'Postal Code Formula' => 'Postal Code Formula', + 'Pounds (lb)' => 'Pounds (lb)', + 'Preview' => 'Preview', + 'Previous Status' => 'Previous Status', + 'Price' => 'Price', + 'Prices' => 'Prices', + 'Pricing Rules' => 'Pricing Rules', + 'Pricing jobs are currently running.' => 'Pricing jobs are currently running.', + 'Pricing' => 'Pricing', + 'Primary Billing Address' => 'Primary Billing Address', + 'Primary Shipping Address' => 'Primary Shipping Address', + 'Primary payment source updated.' => 'Primary payment source updated.', + 'Primary' => 'Primary', + 'Private Note' => 'Private Note', + 'Product Fields' => 'Product Fields', + 'Product ID is required.' => 'Product ID is required.', + 'Product Template' => 'Product Template', + 'Product Title Format' => 'Product Title Format', + 'Product Type' => 'Product Type', + 'Product Types' => 'Product Types', + 'Product URI Format' => 'Product URI Format', + 'Product Variant' => 'Product Variant', + 'Product Variants' => 'Product Variants', + 'Product type saved.' => 'Product type saved.', + 'Product type settings' => 'Product type settings', + 'Product' => 'Product', + 'Products and Variants deleted.' => 'Products and Variants deleted.', + 'Products not restored.' => 'Products not restored.', + 'Products restored.' => 'Products restored.', + 'Products' => 'Products', + 'Promotable' => 'Promotable', + 'Promotable?' => 'Promotable?', + 'Promotional Amount' => 'Promotional Amount', + 'Promotional Price' => 'Promotional Price', + 'Purchasable Categories' => 'Purchasable Categories', + 'Purchasable ID and Sale ID are required.' => 'Purchasable ID and Sale ID are required.', + 'Purchasable ID is required.' => 'Purchasable ID is required.', + 'Purchasable Type' => 'Purchasable Type', + 'Purchasable' => 'Purchasable', + 'Purchase (Authorize and Capture Immediately)' => 'Purchase (Authorise and Capture Immediately)', + 'Purchase Total' => 'Purchase Total', + 'Qty' => 'Qty', + 'Quality Control' => 'Quality Control', + 'Quantity' => 'Quantity', + 'Rate' => 'Rate', + 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Reassign {numOrders, plural, =1{order} other{orders}}', + 'Recalculate order' => 'Recalculate order', + 'Receive Inventory' => 'Receive Inventory', + 'Receive Transfer' => 'Receive Transfer', + 'Receive' => 'Receive', + 'Received' => 'Received', + 'Recent Orders' => 'Recent Orders', + 'Recipient' => 'Recipient', + 'Recover Cart' => 'Recover Cart', + 'Reduce price' => 'Reduce price', + 'Reduce the price by a fixed amount' => 'Reduce the price by a fixed amount', + 'Reduce the price by a percentage of the original price' => 'Reduce the price by a percentage of the original price', + 'Reference' => 'Reference', + 'Refresh payment history' => 'Refresh payment history', + 'Refund note' => 'Refund note', + 'Refund payment' => 'Refund payment', + 'Refund' => 'Refund', + 'Reject' => 'Reject', + 'Rejected' => 'Rejected', + 'Relationship Type' => 'Relationship Type', + 'Removable included tax rates are only allowed for the default tax zone.' => 'Removable included tax rates are only allowed for the default tax zone.', + 'Remove address' => 'Remove address', + 'Remove all shipping costs from the order' => 'Remove all shipping costs from the order', + 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below', + 'Remove customer data' => 'Remove customer data', + 'Remove from price?' => 'Remove from price?', + 'Remove shipping costs for matching items only' => 'Remove shipping costs for matching items only', + 'Remove the included tax when a valid organization tax ID is present?' => 'Remove the included tax when a valid organization tax ID is present?', + 'Remove' => 'Remove', + 'Removed' => 'Removed', + 'Repeat Customers' => 'Repeat Customers', + 'Reply To' => 'Reply To', + 'Require Billing Address At Checkout' => 'Require Billing Address At Checkout', + 'Require Coupon Code' => 'Require Coupon Code', + 'Require Shipping Address At Checkout' => 'Require Shipping Address At Checkout', + 'Require Shipping Method Selection At Checkout' => 'Require Shipping Method Selection At Checkout', + 'Require' => 'Require', + 'Reserved' => 'Reserved', + 'Reset usage' => 'Reset usage', + 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.', + 'Revenue Options' => 'Revenue Options', + 'Revenue' => 'Revenue', + 'Rule' => 'Rule', + 'Rules reordered.' => 'Rules reordered.', + 'SKU' => 'SKU', + 'Safety' => 'Safety', + 'Sale Price' => 'Sale Price', + 'Sale description.' => 'Sale description.', + 'Sale reordered.' => 'Sale reordered.', + 'Sale saved.' => 'Sale saved.', + 'Sale' => 'Sale', + 'Sales deleted.' => 'Sales deleted.', + 'Sales updated.' => 'Sales updated.', + 'Sales' => 'Sales', + 'Save and continue editing' => 'Save and continue editing', + 'Save and return to all orders' => 'Save and return to all orders', + 'Save and set rules' => 'Save and set rules', + 'Save as a new rule' => 'Save as a new rule', + 'Save product to all sites enabled for this product type' => 'Save product to all sites enabled for this product type', + 'Save product to other sites in the same site group' => 'Save product to other sites in the same site group', + 'Save product to other sites with the same language' => 'Save product to other sites with the same language', + 'Save' => 'Save', + 'Search customer…' => 'Search customer…', + 'Search inventory' => 'Search inventory', + 'Search or enter customer email…' => 'Search or enter customer email…', + 'Search…' => 'Search…', + 'See Orders' => 'See Orders', + 'Select a gateway' => 'Select a gateway', + 'Select a tax category.' => 'Select a tax category.', + 'Select a tax zone. If empty, this rate will match anywhere.' => 'Select a tax zone. If empty, this rate will match anywhere.', + 'Select address' => 'Select address', + 'Select an item' => 'Select an item', + 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Select how the catalogue pricing rule will be applied to the purchasable(s).', + 'Select how the sale will be applied to the purchasable(s).' => 'Select how the sale will be applied to the purchasable(s).', + 'Select product type' => 'Select product type', + 'Select the emails that will be sent when transitioning to this status.' => 'Select the emails that will be sent when transitioning to this status.', + 'Select what this rate should be applied to.' => 'Select what this rate should be applied to.', + 'Send Email' => 'Send Email', + 'Send to custom recipient' => 'Send to custom recipient', + 'Send to the customer' => 'Send to the customer', + 'Set Quantity' => 'Set Quantity', + 'Set default category' => 'Set default category', + 'Set default variant' => 'Set default variant', + 'Set or Adjust' => 'Set or Adjust', + 'Set price' => 'Set price', + 'Set status' => 'Set status', + 'Set the price to a flat amount' => 'Set the price to a flat amount', + 'Set the price to a percentage of the original price' => 'Set the price to a percentage of the original price', + 'Set the sale price to a flat amount' => 'Set the sale price to a flat amount', + 'Set the sale price to a percentage of the original price' => 'Set the sale price to a percentage of the original price', + 'Set to' => 'Set to', + 'Settings saved.' => 'Settings saved.', + 'Settings' => 'Settings', + 'Share cart…' => 'Share cart…', + 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.', + 'Shipping Address Zone' => 'Shipping Address Zone', + 'Shipping Address' => 'Shipping Address', + 'Shipping Business Name' => 'Shipping Business Name', + 'Shipping Categories' => 'Shipping Categories', + 'Shipping Category Conditions' => 'Shipping Category Conditions', + 'Shipping Category' => 'Shipping Category', + 'Shipping First Name' => 'Shipping First Name', + 'Shipping Full Name' => 'Shipping Full Name', + 'Shipping Last Name' => 'Shipping Last Name', + 'Shipping Method' => 'Shipping Method', + 'Shipping Methods' => 'Shipping Methods', + 'Shipping Rule' => 'Shipping Rule', + 'Shipping Zones' => 'Shipping Zones', + 'Shipping address required.' => 'Shipping address required.', + 'Shipping categories deleted.' => 'Shipping categories deleted.', + 'Shipping category saved.' => 'Shipping category saved.', + 'Shipping category updated.' => 'Shipping category updated.', + 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.', + 'Shipping method saved.' => 'Shipping method saved.', + 'Shipping methods and rules deleted.' => 'Shipping methods and rules deleted.', + 'Shipping methods updated.' => 'Shipping methods updated.', + 'Shipping rule saved.' => 'Shipping rule saved.', + 'Shipping zone saved.' => 'Shipping zone saved.', + 'Shipping' => 'Shipping', + 'Short Number' => 'Short Number', + 'Show Chart?' => 'Show Chart?', + 'Show Order Count?' => 'Show Order Count?', + 'Show all prices' => 'Show all prices', + 'Show archived gateways' => 'Show archived gateways', + 'Show order count line on chart.' => 'Show order count line on chart.', + 'Show related sales' => 'Show related sales', + 'Show rule details' => 'Show rule details', + 'Show the Dimensions and Weight fields for products of this type' => 'Show the Dimensions and Weight fields for products of this type', + 'Show the Title field for products' => 'Show the Title field for products', + 'Show the Title field for variants' => 'Show the Title field for variants', + 'Signed In' => 'Signed In', + 'Site Languages' => 'Site Languages', + 'Site store mapping saved.' => 'Site store mapping saved.', + 'Sites' => 'Sites', + 'Slug' => 'Slug', + 'Snapshot' => 'Snapshot', + 'Snapshots' => 'Snapshots', + 'Some orders restored.' => 'Some orders restored.', + 'Some products restored.' => 'Some products restored.', + 'Some variants restored.' => 'Some variants restored.', + 'Something changed with the order before payment, please review your order and submit payment again.' => 'Something changed with the order before payment, please review your order and submit payment again.', + 'Sorry, no matching options.' => 'Sorry, no matching options.', + 'Source - The purchasable relationship field is on the category' => 'Source - The purchasable relationship field is on the category', + 'Source' => 'Source', + 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)', + 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)', + 'Start Date' => 'Start Date', + 'State' => 'State', + 'Status Email Address' => 'Status Email Address', + 'Status Emails' => 'Status Emails', + 'Status History' => 'Status History', + 'Status Updated.' => 'Status Updated.', + 'Status change message' => 'Status change message', + 'Status' => 'Status', + 'Stock' => 'Stock', + 'Stops Processing?' => 'Stops Processing?', + 'Stops subsequent?' => 'Stops subsequent?', + 'Store Location' => 'Store Location', + 'Store Management' => 'Store Management', + 'Store Markets' => 'Store Markets', + 'Store Rule' => 'Store Rule', + 'Store saved.' => 'Store saved.', + 'Store' => 'Store', + 'Stores & Sites' => 'Stores & Sites', + 'Stores' => 'Stores', + 'Strategy to apply when an order is free or has a zero balance.' => 'Strategy to apply when an order is free or has a zero balance.', + 'Strategy to apply when calculating the minimum order price.' => 'Strategy to apply when calculating the minimum order price.', + 'Subject' => 'Subject', + 'Subscribing user' => 'Subscribing user', + 'Subscription Fields' => 'Subscription Fields', + 'Subscription Plans' => 'Subscription Plans', + 'Subscription Settings' => 'Subscription Settings', + 'Subscription cancelled.' => 'Subscription cancelled.', + 'Subscription date' => 'Subscription date', + 'Subscription fields saved.' => 'Subscription fields saved.', + 'Subscription for {user} to {plan} prevented by a plugin.' => 'Subscription for {user} to {plan} prevented by a plugin.', + 'Subscription plan saved.' => 'Subscription plan saved.', + 'Subscription plan' => 'Subscription plan', + 'Subscription plans' => 'Subscription plans', + 'Subscription reactivated.' => 'Subscription reactivated.', + 'Subscription reference' => 'Subscription reference', + 'Subscription started.' => 'Subscription started.', + 'Subscription switched.' => 'Subscription switched.', + 'Subscription to “{plan}”' => 'Subscription to “{plan}”', + 'Subscription' => 'Subscription', + 'Subscriptions on hold' => 'Subscriptions on hold', + 'Subscriptions' => 'Subscriptions', + 'Suppress emails' => 'Suppress emails', + 'Switch plan' => 'Switch plan', + 'Switch' => 'Switch', + 'System' => 'System', + 'Table Columns' => 'Table Columns', + 'Target - The category relationship field is on the purchasable' => 'Target - The category relationship field is on the purchasable', + 'Tax & Shipping' => 'Tax & Shipping', + 'Tax (inc)' => 'Tax (inc)', + 'Tax Categories' => 'Tax Categories', + 'Tax Category' => 'Tax Category', + 'Tax Rates' => 'Tax Rates', + 'Tax Zone' => 'Tax Zone', + 'Tax Zones' => 'Tax Zones', + 'Tax categories deleted.' => 'Tax categories deleted.', + 'Tax category saved.' => 'Tax category saved.', + 'Tax category updated.' => 'Tax category updated.', + 'Tax rate saved.' => 'Tax rate saved.', + 'Tax rates updated.' => 'Tax rates updated.', + 'Tax zone saved.' => 'Tax zone saved.', + 'Tax' => 'Tax', + 'Taxable Subject' => 'Taxable Subject', + 'Template Path' => 'Template Path', + 'That handle is already in use' => 'That handle is already in use', + 'That handle is already in use.' => 'That handle is already in use.', + 'The PDF to attach to this email.' => 'The PDF to attach to this email.', + 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'The URL to the page where billing details for a subscription can be updated, as well as where 3DS authentication is handled.', + 'The address provided is outside the store’s market.' => 'The address provided is outside the store’s market.', + 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'The discount amount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.', + 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'The base discount can only discount items in the cart to down to zero until it is used up, it cannot make the order negative.', + 'The cart recovery link is invalid. Please request a new one.' => 'The cart recovery link is invalid. Please request a new one.', + 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.', + 'The countries that orders are allowed to be placed from.' => 'The countries that orders can be placed from.', + 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'The coupon "{code}" has exceeded its usage limit of {limit}.', + 'The customer for this order has been deleted.' => 'The customer for this order has been deleted.', + 'The default shipping category is automatically available to all product types.' => 'The default shipping category is automatically available to all product types.', + 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'The discount "{name}" has exceeded its total usage limit of {limit}.', + 'The download link has expired. Please request a new one.' => 'The download link has expired. Please request a new one.', + 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.', + 'The entry that contains the description for this subscription’s plan.' => 'The entry that contains the description for this subscription’s plan.', + 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'The flat value by which each item should be discounted, e.g. “3” for $3 off each item.', + 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'The format used to generate new coupons, e.g. {example}. Any # characters will be replaced with a random letter.', + 'The from and to inventory locations must be different.' => 'The from and to inventory locations must be different.', + 'The inventory locations this store uses.' => 'The inventory locations this store uses.', + 'The item is not enabled for sale.' => 'The item is not enabled for sale.', + 'The language the order was made in.' => 'The language the order was made in.', + 'The language to be used when this email is rendered.' => 'The language to be used when this email is rendered.', + 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'The maximum number of levels this product type can have. Leave blank if you don’t care.', + 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'The maximum the customer should spend on shipping. Set to zero to disable.', + 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'The minimum the customer should spend on shipping. Set to zero to disable.', + 'The order is not valid.' => 'The order is not valid.', + 'The payment gateway that will be used for the subscription plan.' => 'The payment gateway that will be used for the subscription plan.', + 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'The percentage value by which each item should be discounted, e.g. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.', + 'The previously-selected shipping method is no longer available.' => 'The previously-selected shipping method is no longer available.', + 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}', + 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}', + 'The primary currency cannot be changed after orders are placed.' => 'The primary currency cannot be changed after orders are placed.', + 'The purchasable defines the relationship' => 'The purchasable defines the relationship', + 'The purchasable is related by another element' => 'The purchasable is related by another element', + 'The recipient of the email. Twig code can be used here.' => 'The recipient of the email. Twig code can be used here.', + 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'The "reply to" email address. Leave blank for normal "reply to" of email sender. Twig code can be used here.', + 'The site the order was made in.' => 'The site the order was made in.', + 'The site to be used when this email is rendered.' => 'The site to be used when this email is rendered.', + 'The subject line of the email. Twig code can be used here.' => 'The subject line of the email. Twig code can be used here.', + 'The template that the PDF should be generated from.' => 'The template that the PDF should be generated from.', + 'The template to be used for HTML emails.' => 'The template to be used for HTML emails.', + 'The template to be used for plain text emails. Twig code can be used here.' => 'The template to be used for plain text emails. Twig code can be used here.', + 'The template to use when a product’s URL is requested.' => 'The template to use when a product’s URL is requested.', + 'The total number of order adjustments changed.' => 'The total number of order adjustments changed.', + 'The total price of the order changed.' => 'The total price of the order changed.', + 'The total quantity of items within the order changed.' => 'The total quantity of items within the order changed.', + 'The unique SKU of the donation purchasable.' => 'The unique SKU of the donation purchasable.', + 'The unit of measurement that should be used when specifying product dimensions.' => 'The unit of measurement that should be used when specifying product dimensions.', + 'The unit of measurement that should be used when specifying product weights.' => 'The unit of measurement that should be used when specifying product weights.', + 'The webhook URL for this gateway.' => 'The webhook URL for this gateway.', + 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.', + 'There are errors on the order' => 'There are errors on the order', + 'There are only {num} “{description}” items left in stock.' => 'There are only {num} “{description}” items left in stock.', + 'There aren’t any product types to select yet.' => 'There are no product types to select yet.', + 'There is no gateway or payment source available for use with this order.' => 'There is no gateway or payment source available for use with this order.', + 'There is no gateway selected that supports payment sources.' => 'There is no gateway selected that supports payment sources.', + 'There is no shipping method selected for this order.' => 'There is no shipping method selected for this order.', + 'This URL will load the cart into the user’s session, making it the active cart.' => 'This URL will load the cart into the user’s session, making it the active cart.', + 'This action is not allowed for the current user.' => 'This action is not allowed for the current user.', + 'This category will be used as the default for all purchasables in this store.' => 'This category will be used as the default for all purchasables in this store.', + 'This coupon is for registered users and limited to {limit} uses.' => 'This coupon is for registered users and limited to {limit} uses.', + 'This coupon is limited to {limit} uses.' => 'This coupon is limited to {limit} uses.', + 'This coupon requires an email address.' => 'This coupon requires an email address.', + 'This gateway does not support that functionality.' => 'This gateway does not support that functionality.', + 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'This is being overridden by the {setting} config setting in `config/{file}.php`.', + 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.', + 'This is the default PDF that will be rendered when requesting the order PDF.' => 'This is the default PDF that will be rendered when requesting the order PDF.', + 'This is the last location for the {store} store.' => 'This is the last location for the {store} store.', + 'This month' => 'This month', + 'This order has unsaved changes.' => 'This order has unsaved changes.', + 'This week' => 'This week', + 'This year' => 'This year', + 'Times Used' => 'Times Used', + 'Title' => 'Title', + 'To' => 'To', + 'Today' => 'Today', + 'Too many variants for this product.' => 'Too many variants for this product.', + 'Top Customers by Average Order' => 'Top Customers by Average Order', + 'Top Customers by Total Revenue' => 'Top Customers by Total Revenue', + 'Top Customers' => 'Top Customers', + 'Top Product Types by Qty Sold' => 'Top Product Types by Qty Sold', + 'Top Product Types by Revenue' => 'Top Product Types by Revenue', + 'Top Product Types' => 'Top Product Types', + 'Top Products by Qty Sold' => 'Top Products by Qty Sold', + 'Top Products by Revenue' => 'Top Products by Revenue', + 'Top Products' => 'Top Products', + 'Top Purchasables by Qty Sold' => 'Top Purchasables by Qty Sold', + 'Top Purchasables by Revenue' => 'Top Purchasables by Revenue', + 'Top Purchasables' => 'Top Purchasables', + 'Total ' => 'Total ', + 'Total Discount Use Limit' => 'Total Discount Use Limit', + 'Total Discount' => 'Total Discount', + 'Total Included Tax' => 'Total Included Tax', + 'Total Orders by Billing Country' => 'Total Orders by Billing Country', + 'Total Orders by Country' => 'Total Orders by Country', + 'Total Orders by Shipping Country' => 'Total Orders by Shipping Country', + 'Total Orders' => 'Total Orders', + 'Total Paid' => 'Total Paid', + 'Total Price' => 'Total Price', + 'Total Qty' => 'Total Qty', + 'Total Revenue' => 'Total Revenue', + 'Total Shipping' => 'Total Shipping', + 'Total Tax' => 'Total Tax', + 'Total Weight' => 'Total Weight', + 'Total' => 'Total', + 'Track Inventory' => 'Track Inventory', + 'Transaction Hash' => 'Transaction Hash', + 'Transaction ID' => 'Transaction ID', + 'Transaction captured successfully: {message}' => 'Transaction captured successfully: {message}', + 'Transaction refunded successfully: {message}' => 'Transaction refunded successfully: {message}', + 'Transactions' => 'Transactions', + 'Transfer Fields' => 'Transfer Fields', + 'Transfer Items' => 'Transfer Items', + 'Transfer Settings' => 'Transfer Settings', + 'Transfer Status' => 'Transfer Status', + 'Transfer fields saved.' => 'Transfer fields saved.', + 'Transfer must have at least one item.' => 'Transfer must have at least one item.', + 'Transfer' => 'Transfer', + 'Transfers' => 'Transfers', + 'Trial days credited' => 'Trial days credited', + 'Trial expiration' => 'Trial expiration', + 'Trial expiry date' => 'Trial expiry date', + 'Type not in allowed options.' => 'Type not in allowed options.', + 'Type' => 'Type', + 'URI' => 'URI', + 'Unable to cancel subscription at this time.' => 'Unable to cancel subscription at this time.', + 'Unable to complete order: another request is already in progress.' => 'Unable to complete order: another request is already in progress.', + 'Unable to find variant.' => 'Unable to find variant.', + 'Unable to generate coupon codes: {message}' => 'Unable to generate coupon codes: {message}', + 'Unable to make payment at this time.' => 'Unable to make payment at this time.', + 'Unable to modify subscription at this time.' => 'Unable to modify subscription at this time.', + 'Unable to reactivate subscription at this time.' => 'Unable to reactivate subscription at this time.', + 'Unable to reassign orders.' => 'Unable to reassign orders.', + 'Unable to remove order data.' => 'Unable to remove order data.', + 'Unable to retrieve Sale and Purchasable.' => 'Unable to retrieve Sale and Purchasable.', + 'Unable to retrieve cart.' => 'Unable to retrieve cart.', + 'Unable to retrieve customer.' => 'Unable to retrieve customer.', + 'Unable to retrieve load cart URL' => 'Unable to retrieve load cart URL', + 'Unable to retrieve payment source.' => 'Unable to retrieve payment source.', + 'Unable to set default shipping category.' => 'Unable to set default shipping category.', + 'Unable to set default tax category.' => 'Unable to set default tax category.', + 'Unable to set primary payment source.' => 'Unable to set primary payment source.', + 'Unable to start the subscription. Please check your payment details.' => 'Unable to start the subscription. Please check your payment details.', + 'Unable to subscribe at this time.' => 'Unable to subscribe at this time.', + 'Unable to update cart.' => 'Unable to update cart.', + 'Unable to validate address.' => 'Unable to validate address.', + 'Unit Price' => 'Unit Price', + 'Unit price (minus discounts)' => 'Unit price (minus discounts)', + 'Units' => 'Units', + 'Unpaid' => 'Unpaid', + 'Unsubscribe' => 'Unsubscribe', + 'Update Address' => 'Update Address', + 'Update Order Status' => 'Update Order Status', + 'Update Order Status…' => 'Update Order Status…', + 'Update order' => 'Update order', + 'Update subscription' => 'Update subscription', + 'Update' => 'Update', + 'Updated By' => 'Updated By', + 'Updated committed stock successfully.' => 'Updated committed stock successfully.', + 'Updated' => 'Updated', + 'Use Billing Address For Tax' => 'Use Billing Address For Tax', + 'Use as the primary billing address' => 'Use as the primary billing address', + 'Use as the primary shipping address' => 'Use as the primary shipping address', + 'Used By Tax Rates' => 'Used By Tax Rates', + 'Used by Tax Rates' => 'Used by Tax Rates', + 'User Groups' => 'User Groups', + 'User not found.' => 'User not found.', + 'User' => 'User', + 'Uses' => 'Uses', + 'Validate Business Tax ID as Vat ID' => 'Validate Business Tax ID as Vat ID', + 'Validating condition syntax' => 'Validating condition syntax', + 'Validating formula syntax' => 'Validating formula syntax', + 'Variant Fields' => 'Variant Fields', + 'Variant Has Untracked Stock' => 'Variant Has Untracked Stock', + 'Variant Price' => 'Variant Price', + 'Variant SKU' => 'Variant SKU', + 'Variant Search' => 'Variant Search', + 'Variant Stock' => 'Variant Stock', + 'Variant Title Format' => 'Variant Title Format', + 'Variant Tracks Stock' => 'Variant Tracks Stock', + 'Variant UI Label Format' => 'Variant UI Label Format', + 'Variant has no product.' => 'Variant has no product.', + 'Variants not restored.' => 'Variants not restored.', + 'Variants restored.' => 'Variants restored.', + 'Variants' => 'Variants', + 'View customer' => 'View customer', + 'View order' => 'View order', + 'View product type - {productType}' => 'View product type - {productType}', + 'View user' => 'View user', + 'View' => 'View', + 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Warning: deleting this currency will stop all payments and refunds in this currency. Are you sure you want to delete “{name}”?', + 'Web' => 'Web', + 'Webhook URL' => 'Webhook URL', + 'Weight ({unit})' => 'Weight ({unit})', + 'Weight Rate' => 'Weight Rate', + 'Weight Unit' => 'Weight Unit', + 'Weight' => 'Weight', + 'What product URIs should look like for the site.' => 'What product URIs should look like for the site.', + 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.', + 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.', + 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.', + 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}', + 'What this PDF will be called in the control panel.' => 'What this PDF will be called in the control panel.', + 'What this catalog pricing rule will be called in the control panel.' => 'What this catalogue pricing rule will be called in the control panel.', + 'What this discount will be called in the control panel.' => 'What this discount will be called in the control panel.', + 'What this email will be called in the control panel.' => 'What this email will be called in the control panel.', + 'What this product type will be called in the control panel.' => 'What this product type will be called in the control panel.', + 'What this sale will be called in the control panel.' => 'What this sale will be called in the control panel.', + 'What this shipping category will be called in the control panel.' => 'What this shipping category will be called in the control panel.', + 'What this shipping rule will be called in the control panel.' => 'What this shipping rule will be called in the control panel.', + 'What this shipping zone will be called in the control panel.' => 'What this shipping zone will be called in the control panel.', + 'What this status will be called in the control panel.' => 'What this status will be called in the control panel.', + 'What this subscription plan will be called in the control panel.' => 'What this subscription plan will be called in the control panel.', + 'What this tax category will be called in the control panel.' => 'What this tax category will be called in the control panel.', + 'What this tax zone will be called in the control panel.' => 'What this tax zone will be called in the control panel.', + 'When this discount is applied to an order, which line items should be discounted?' => 'When this discount is applied to an order, which line items should be discounted?', + 'Whether the first available shipping method option should be set automatically on carts.' => 'Whether the first available shipping method option should be set automatically on carts.', + 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Whether the user’s primary payment source should be set automatically on new carts.', + 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.', + 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Whether this catalogue pricing rule should be available for use, regardless of other conditions.', + 'Whether this sale should be available for use, regardless of other conditions.' => 'Whether this sale should be available for use, regardless of other conditions.', + 'Which data to display in the name column in the results table.' => 'Which data to display in the name column in the results table.', + 'Which product types should this category be available to?' => 'Which product types should this category be available to?', + 'Which template should be loaded when a product’s URL is requested.' => 'Which template should be loaded when a product’s URL is requested.', + 'Width ({unit})' => 'Width ({unit})', + 'Width' => 'Width', + 'YYYY' => 'YYYY', + 'Yes' => 'Yes', + 'You are not allowed to add a line item.' => 'You are not allowed to add a line item.', + 'You currently have no emails configured to select for this status.' => 'You currently have no emails configured to select for this status.', + 'You do not have permission to load this cart.' => 'You do not have permission to load this cart.', + 'You must set up at least one gateway that supports subscriptions first.' => 'You must set up at least one gateway that supports subscriptions first.', + 'You must be logged in or provide a valid token to load this cart.' => 'You must be logged in or provide a valid token to load this cart.', + 'You must be signed in to create a payment source.' => 'You must be signed in to create a payment source.', + 'You must be signed in to set a primary payment source.' => 'You must be signed in to set a primary payment source.', + 'You must make a payment to complete the order.' => 'You must make a payment to complete the order.', + 'Your Cart Recovery Link' => 'Your Cart Recovery Link', + 'Your Order PDF Download Link' => 'Your Order PDF Download Link', + 'Your order is empty' => 'Your order is empty', + 'ZIP file' => 'ZIP file', + 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Zero - Minimum price is zero if discounts are greater than the order value.', + 'Zip Code' => 'Zip Code', + 'all' => 'all', + 'any' => 'any', + 'average order total' => 'average order total', + 'billing address' => 'billing address', + 'donation' => 'donation', + 'donations' => 'donations', + 'info' => 'info', + 'inventory location' => 'inventory location', + 'new customers' => 'new customers', + 'on hand' => 'on hand', + 'only' => 'only', + 'order' => 'order', + 'orders' => 'orders', + 'price' => 'price', + 'prices' => 'prices', + 'product variant' => 'product variant', + 'product variants' => 'product variants', + 'product' => 'product', + 'products' => 'products', + 'repeat customers' => 'repeat customers', + 'shipping address' => 'shipping address', + 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'shippingSameAsBilling and billingSameAsShipping can’t both be set.', + 'subscription' => 'subscription', + 'subscriptions' => 'subscriptions', + 'to' => 'to', + 'transfer' => 'transfer', + 'transfers' => 'transfers', + '{amount} included' => '{amount} included', + '{count} Unfulfilled Orders' => '{count} Unfulfilled Orders', + '{description} is no longer available.' => '{description} is no longer available.', + '{description} only has {stock} in stock.' => '{description} only has {stock} in stock.', + '{from} to {to}' => '{from} to {to}', + '{name} (Primary)' => '{name} (Primary)', + '{name} (Trashed)' => '{name} (Trashed)', + '{name} catalog price' => '{name} catalog price', + '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, one {}=1{order} other{orders}} updated.', + '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.', + '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.', + '{number} more…' => '{number} more…', + '{pct} off the discounted item price' => '{pct} off the discounted item price', + '{pct} off the original item price' => '{pct} off the original item price', + '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.', + '{total} in total revenue' => '{total} in total revenue', + '{total} orders' => '{total} orders', + '{total} saleable across {locationCount} location(s)' => '{total} saleable across {locationCount} location(s)', + '{uses} uses across {emails} email addresses' => '{uses} uses across {emails} email addresses', + '{uses} uses across {users} users' => '{uses} uses across {users} users', + '“{description}” is currently out of stock.' => '“{description}” is currently out of stock.', + '“{key}” has invalid JSON' => '“{key}” has invalid JSON', +]; diff --git a/lang/en/commerce.php b/lang/en/commerce.php new file mode 100644 index 0000000000..1c9643e9d6 --- /dev/null +++ b/lang/en/commerce.php @@ -0,0 +1,1425 @@ + '(new price)', + '(of original price)' => '(of original price)', + '(off original price)' => '(off original price)', + 'A cart number must be specified.' => 'A cart number must be specified.', + 'A cart recovery link has been sent to {email}.' => 'A cart recovery link has been sent to {email}.', + 'A cart recovery link will be sent to {email}.' => 'A cart recovery link will be sent to {email}.', + 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.', + 'A new download link has been sent to {email}' => 'A new download link has been sent to {email}', + 'A new download link will be sent to {email}' => 'A new download link will be sent to {email}', + 'A valid email is required to create a customer.' => 'A valid email is required to create a customer.', + 'Accept' => 'Accept', + 'Accepted' => 'Accepted', + 'Actions' => 'Actions', + 'Active Carts' => 'Active Carts', + 'Active subscriptions' => 'Active subscriptions', + 'Active' => 'Active', + 'Add Address' => 'Add Address', + 'Add a coupon' => 'Add a coupon', + 'Add a custom line item' => 'Add a custom line item', + 'Add a line item' => 'Add a line item', + 'Add a product' => 'Add a product', + 'Add a variant' => 'Add a variant', + 'Add an adjustment' => 'Add an adjustment', + 'Add an item' => 'Add an item', + 'Add an option' => 'Add an option', + 'Add catalog price' => 'Add catalog price', + 'Add' => 'Add', + 'Additional Actions' => 'Additional Actions', + 'Additional recipients that should receive this email. Twig code can be used here.' => 'Additional recipients that should receive this email. Twig code can be used here.', + 'Address 1' => 'Address 1', + 'Address 2' => 'Address 2', + 'Address 3' => 'Address 3', + 'Address Line 1' => 'Address Line 1', + 'Address Line 2' => 'Address Line 2', + 'Address Updated.' => 'Address Updated.', + 'Address copied to user.' => 'Address copied to user.', + 'Address not found.' => 'Address not found.', + 'Adjust Quantity' => 'Adjust Quantity', + 'Adjust by' => 'Adjust by', + 'Adjust price when included rate is disqualified?' => 'Adjust price when included rate is disqualified?', + 'Adjustments' => 'Adjustments', + 'Admin Notices' => 'Admin Notices', + 'Administrative Area Code of Origin' => 'Administrative Area Code of Origin', + 'Advanced' => 'Advanced', + 'All Orders' => 'All Orders', + 'All Totals' => 'All Totals', + 'All Transfers' => 'All Transfers', + 'All active subscriptions' => 'All active subscriptions', + 'All customers' => 'All customers', + 'All products' => 'All products', + 'All variants must have a SKU.' => 'All variants must have a SKU.', + 'All' => 'All', + 'Allow Checkout Without Payment' => 'Allow Checkout Without Payment', + 'Allow Empty Cart On Checkout' => 'Allow Empty Cart On Checkout', + 'Allow Partial Payment On Checkout' => 'Allow Partial Payment On Checkout', + 'Allow out of stock purchases' => 'Allow out of stock purchases', + 'Allow' => 'Allow', + 'Allowed Qty' => 'Allowed Qty', + 'Alternative Phone' => 'Alternative Phone', + 'Amount' => 'Amount', + 'An ID must be provided' => 'An ID must be provided', + 'An error occurred while generating this PDF.' => 'An error occurred while generating this PDF.', + 'Any' => 'Any', + 'Anywhere' => 'Anywhere', + 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.', + 'Are you sure you want to capture this transaction?' => 'Are you sure you want to capture this transaction?', + 'Are you sure you want to complete this order?' => 'Are you sure you want to complete this order?', + 'Are you sure you want to delete the selected orders?' => 'Are you sure you want to delete the selected orders?', + 'Are you sure you want to delete the selected product and its variants?' => 'Are you sure you want to delete the selected product and its variants?', + 'Are you sure you want to delete this shipping rule?' => 'Are you sure you want to delete this shipping rule?', + 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.', + 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?', + 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.', + 'Are you sure you want to overwrite the billing address?' => 'Are you sure you want to overwrite the billing address?', + 'Are you sure you want to overwrite the shipping address?' => 'Are you sure you want to overwrite the shipping address?', + 'Are you sure you want to permanently delete this store and everything in it?' => 'Are you sure you want to permanently delete this store and everything in it?', + 'Are you sure you want to refund this transaction?' => 'Are you sure you want to refund this transaction?', + 'Are you sure you want to remove this customer?' => 'Are you sure you want to remove this customer?', + 'Are you sure you want to save this as a new shipping rule?' => 'Are you sure you want to save this as a new shipping rule?', + 'Are you sure you want to send email: {name}?' => 'Are you sure you want to send email: {name}?', + 'At least one site must be enabled for the product type.' => 'At least one site must be enabled for the product type.', + 'Attempted Payments' => 'Attempted Payments', + 'Attention' => 'Attention', + 'Authorize Only (Manually Capture)' => 'Authorize Only (Manually Capture)', + 'Auto Set Cart Shipping Method Option' => 'Auto Set Cart Shipping Method Option', + 'Auto Set New Cart Addresses' => 'Auto Set New Cart Addresses', + 'Auto Set Payment Source' => 'Auto Set Payment Source', + 'Automatic SKU Format' => 'Automatic SKU Format', + 'Available Shipping Categories' => 'Available Shipping Categories', + 'Available Tax Categories' => 'Available Tax Categories', + 'Available for purchase' => 'Available for purchase', + 'Available for purchase?' => 'Available for purchase?', + 'Available inventory for "{description}" has gone below zero.' => 'Available inventory for "{description}" has gone below zero.', + 'Available to Product Types' => 'Available to Product Types', + 'Available' => 'Available', + 'Available?' => 'Available?', + 'Average Order Total' => 'Average Order Total', + 'Average' => 'Average', + 'BCC’d Recipient' => 'BCC’d Recipient', + 'Bad Request' => 'Bad Request', + 'Bad address ID.' => 'Bad address ID.', + 'Bad order ID.' => 'Bad order ID.', + 'Base Price' => 'Base Price', + 'Base Promotional Price' => 'Base Promotional Price', + 'Base Rate' => 'Base Rate', + 'Base' => 'Base', + 'Bcc' => 'Bcc', + 'Billing Address' => 'Billing Address', + 'Billing Business Name' => 'Billing Business Name', + 'Billing First Name' => 'Billing First Name', + 'Billing Full Name' => 'Billing Full Name', + 'Billing Last Name' => 'Billing Last Name', + 'Billing address required.' => 'Billing address required.', + 'Billing detail update URL' => 'Billing detail update URL', + 'Billing issues' => 'Billing issues', + 'Billing' => 'Billing', + 'Both (Line item price + Line item shipping costs)' => 'Both (Line item price + Line item shipping costs)', + 'Business ID' => 'Business ID', + 'Business Name' => 'Business Name', + 'Business Tax ID' => 'Business Tax ID', + 'CC’d Recipient' => 'CC’d Recipient', + 'CVV' => 'CVV', + 'Can be used as an internal reference.' => 'Can be used as an internal reference.', + 'Can not complete payment for missing transaction.' => 'Can not complete payment for missing transaction.', + 'Can not create a new order' => 'Can not create a new order', + 'Can not find an order to pay.' => 'Can not find an order to pay.', + 'Can not find enabled email.' => 'Can not find enabled email.', + 'Can not find order' => 'Can not find order', + 'Can not find order.' => 'Can not find order.', + 'Can not find the transaction to refund' => 'Can not find the transaction to refund', + 'Can not move between these inventory types.' => 'Can not move between these inventory types.', + 'Can not refund amount greater than the remaining amount' => 'Can not refund amount greater than the remaining amount', + 'Cancel subscription' => 'Cancel subscription', + 'Cancel with gateway now' => 'Cancel with gateway now', + 'Cancel' => 'Cancel', + 'Cancellation date' => 'Cancellation date', + 'Cancellation' => 'Cancellation', + 'Cannot switch plans for this subscription.' => 'Cannot switch plans for this subscription.', + 'Can’t preview this email.' => 'Can’t preview this email.', + 'Capture payment' => 'Capture payment', + 'Capture' => 'Capture', + 'Card Holder' => 'Card Holder', + 'Card Number' => 'Card Number', + 'Card' => 'Card', + 'Cart Recovery Link' => 'Cart Recovery Link', + 'Cart forgotten.' => 'Cart forgotten.', + 'Cart updated.' => 'Cart updated.', + 'Cart {number}' => 'Cart {number}', + 'Catalog Pricing Rule' => 'Catalog Pricing Rule', + 'Catalog pricing rule description.' => 'Catalog pricing rule description.', + 'Catalog pricing rule saved.' => 'Catalog pricing rule saved.', + 'Catalog pricing rules deleted.' => 'Catalog pricing rules deleted.', + 'Catalog pricing rules updated.' => 'Catalog pricing rules updated.', + 'Categories Relationship Type' => 'Categories Relationship Type', + 'Categories' => 'Categories', + 'Category Rate Overrides' => 'Category Rate Overrides', + 'Centimeters (cm)' => 'Centimeters (cm)', + 'Changing this value may affect your ability to refund existing transactions.' => 'Changing this value may affect your ability to refund existing transactions.', + 'Choose a color to represent the order’s status' => 'Choose a color to represent the order’s status', + 'Choose a new customer' => 'Choose a new customer', + 'Choose adjustment values to include when calculating the product revenue total.' => 'Choose adjustment values to include when calculating the product revenue total.', + 'Choose the currency’s ISO code.' => 'Choose the currency’s ISO code.', + 'Choose the destination inventory location for the existing on hand stock.' => 'Choose the destination inventory location for the existing on hand stock.', + 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Choose which sites this product type should be available in, and configure the site-specific settings.', + 'City' => 'City', + 'Clear counter' => 'Clear counter', + 'Clear notices' => 'Clear notices', + 'Close' => 'Close', + 'Code' => 'Code', + 'Collated PDF' => 'Collated PDF', + 'Color' => 'Color', + 'Commerce Products' => 'Commerce Products', + 'Commerce Settings' => 'Commerce Settings', + 'Commerce Variants' => 'Commerce Variants', + 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Commerce email “{email}” could not be sent for order “{order}”.', + 'Commerce order exports' => 'Commerce order exports', + 'Commerce' => 'Commerce', + 'Committed' => 'Committed', + 'Completed Email' => 'Completed Email', + 'Completed' => 'Completed', + 'Completing order failed.' => 'Completing order failed.', + 'Condition' => 'Condition', + 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.', + 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.', + 'Conditions' => 'Conditions', + 'Contains Purchasables' => 'Contains Purchasables', + 'Control Panel Settings' => 'Control Panel Settings', + 'Control panel' => 'Control panel', + 'Conversion Rate' => 'Conversion Rate', + 'Converted Price' => 'Converted Price', + 'Copied!' => 'Copied!', + 'Copy the URL' => 'Copy the URL', + 'Copy to {location}' => 'Copy to {location}', + 'Copy' => 'Copy', + 'Costs' => 'Costs', + 'Could not archive gateway.' => 'Could not archive gateway.', + 'Could not cancel “{reference}”.' => 'Could not cancel “{reference}”.', + 'Could not create the payment source.' => 'Could not create the payment source.', + 'Could not delete shipping rule' => 'Could not delete shipping rule', + 'Could not delete shipping zone' => 'Could not delete shipping zone', + 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.', + 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.', + 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.', + 'Could not find the email or template.' => 'Could not find the email or template.', + 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}', + 'Could not reactivate “{reference}”.' => 'Could not reactivate “{reference}”.', + 'Could not send email' => 'Could not send email', + 'Could not switch “{reference}” to “{plan}”.' => 'Could not switch “{reference}” to “{plan}”.', + 'Could not update orders address.' => 'Could not update orders address.', + 'Couldn’t archive Line Item Status.' => 'Couldn’t archive Line Item Status.', + 'Couldn’t archive Order Status.' => 'Couldn’t archive Order Status.', + 'Couldn’t capture transaction.' => 'Couldn’t capture transaction.', + 'Couldn’t capture transaction: {message}' => 'Couldn’t capture transaction: {message}', + 'Couldn’t delete email.' => 'Couldn’t delete email.', + 'Couldn’t delete the payment source.' => 'Couldn’t delete the payment source.', + 'Couldn’t get order.' => 'Couldn’t get order.', + 'Couldn’t recalculate order.' => 'Couldn’t recalculate order.', + 'Couldn’t refund transaction.' => 'Couldn’t refund transaction.', + 'Couldn’t refund transaction: {message}' => 'Couldn’t refund transaction: {message}', + 'Couldn’t reorder Line Item Statuses.' => 'Couldn’t reorder Line Item Statuses.', + 'Couldn’t reorder Order Statuses.' => 'Couldn’t reorder Order Statuses.', + 'Couldn’t reorder PDFs.' => 'Couldn’t reorder PDFs.', + 'Couldn’t reorder discounts.' => 'Couldn’t reorder discounts.', + 'Couldn’t reorder gateways.' => 'Couldn’t reorder gateways.', + 'Couldn’t reorder plans.' => 'Couldn’t reorder plans.', + 'Couldn’t reorder rules.' => 'Couldn’t reorder rules.', + 'Couldn’t reorder sale.' => 'Couldn’t reorder sale.', + 'Couldn’t reorder sales.' => 'Couldn’t reorder sales.', + 'Couldn’t reorder statuses.' => 'Couldn’t reorder statuses.', + 'Couldn’t reorder stores.' => 'Couldn’t reorder stores.', + 'Couldn’t save PDF.' => 'Couldn’t save PDF.', + 'Couldn’t save catalog pricing rule.' => 'Couldn’t save catalog pricing rule.', + 'Couldn’t save currency.' => 'Couldn’t save currency.', + 'Couldn’t save discount.' => 'Couldn’t save discount.', + 'Couldn’t save email.' => 'Couldn’t save email.', + 'Couldn’t save gateway.' => 'Couldn’t save gateway.', + 'Couldn’t save inventory location.' => 'Couldn’t save inventory location.', + 'Couldn’t save line item status.' => 'Couldn’t save line item status.', + 'Couldn’t save order fields.' => 'Couldn’t save order fields.', + 'Couldn’t save order status.' => 'Couldn’t save order status.', + 'Couldn’t save order.' => 'Couldn’t save order.', + 'Couldn’t save product type.' => 'Couldn’t save product type.', + 'Couldn’t save sale.' => 'Couldn’t save sale.', + 'Couldn’t save settings.' => 'Couldn’t save settings.', + 'Couldn’t save shipping category.' => 'Couldn’t save shipping category.', + 'Couldn’t save shipping method.' => 'Couldn’t save shipping method.', + 'Couldn’t save shipping rule.' => 'Couldn’t save shipping rule.', + 'Couldn’t save shipping zone.' => 'Couldn’t save shipping zone.', + 'Couldn’t save store location address.' => 'Couldn’t save store location address.', + 'Couldn’t save store.' => 'Couldn’t save store.', + 'Couldn’t save subscription fields.' => 'Couldn’t save subscription fields.', + 'Couldn’t save subscription plan.' => 'Couldn’t save subscription plan.', + 'Couldn’t save subscription.' => 'Couldn’t save subscription.', + 'Couldn’t save tax category.' => 'Couldn’t save tax category.', + 'Couldn’t save tax rate.' => 'Couldn’t save tax rate.', + 'Couldn’t save tax zone.' => 'Couldn’t save tax zone.', + 'Couldn’t save transfer fields.' => 'Couldn’t save transfer fields.', + 'Couldn’t update catalog pricing rule statuses.' => 'Couldn’t update catalog pricing rule statuses.', + 'Couldn’t update status.' => 'Couldn’t update status.', + 'Couldn’t updated sales status.' => 'Couldn’t updated sales status.', + 'Country Code of Origin' => 'Country Code of Origin', + 'Country List' => 'Country List', + 'Country not allowed.' => 'Country not allowed.', + 'Country' => 'Country', + 'Coupon Code' => 'Coupon Code', + 'Coupon can not apply discount to this order due to address mismatch.' => 'Coupon can not apply discount to this order due to address mismatch.', + 'Coupon can not apply discount to this order due to customer mismatch.' => 'Coupon can not apply discount to this order due to customer mismatch.', + 'Coupon can not apply discount to this order.' => 'Coupon can not apply discount to this order.', + 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Coupon code “{code}” is already in use by discount “{name}”.', + 'Coupon codes cannot be blank.' => 'Coupon codes cannot be blank.', + 'Coupon codes must be unique.' => 'Coupon codes must be unique.', + 'Coupon format is required and must contain at least one `#`.' => 'Coupon format is required and must contain at least one `#`.', + 'Coupon not valid.' => 'Coupon not valid.', + 'Coupon removed: {explanation}' => 'Coupon removed: {explanation}', + 'Coupons' => 'Coupons', + 'Craft Commerce - Administration' => 'Craft Commerce - Administration', + 'Craft Commerce - Inventory' => 'Craft Commerce - Inventory', + 'Craft Commerce - Orders' => 'Craft Commerce - Orders', + 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Product Type - {name}', + 'Craft Commerce - Subscriptions' => 'Craft Commerce - Subscriptions', + 'Create a Discount' => 'Create a Discount', + 'Create a Subscription Plan' => 'Create a Subscription Plan', + 'Create a new PDF' => 'Create a new PDF', + 'Create a new catalog pricing rule' => 'Create a new catalog pricing rule', + 'Create a new currency' => 'Create a new currency', + 'Create a new email' => 'Create a new email', + 'Create a new gateway' => 'Create a new gateway', + 'Create a new line item status' => 'Create a new line item status', + 'Create a new order status' => 'Create a new order status', + 'Create a new product type' => 'Create a new product type', + 'Create a new sale' => 'Create a new sale', + 'Create a new shipping category' => 'Create a new shipping category', + 'Create a new shipping method' => 'Create a new shipping method', + 'Create a new shipping rule' => 'Create a new shipping rule', + 'Create a new tax category' => 'Create a new tax category', + 'Create a new tax rate' => 'Create a new tax rate', + 'Create a product type' => 'Create a product type', + 'Create a shipping zone' => 'Create a shipping zone', + 'Create a tax zone' => 'Create a tax zone', + 'Create catalog pricing rules' => 'Create catalog pricing rules', + 'Create customer: “{email}”' => 'Create customer: “{email}”', + 'Create discounts' => 'Create discounts', + 'Create discount…' => 'Create discount…', + 'Create rules that allow this discount to match the order.' => 'Create rules that allow this discount to match the order.', + 'Create rules that allow this discount to match the order’s billing address.' => 'Create rules that allow this discount to match the order’s billing address.', + 'Create rules that allow this discount to match the order’s customer.' => 'Create rules that allow this discount to match the order’s customer.', + 'Create rules that allow this discount to match the order’s shipping address.' => 'Create rules that allow this discount to match the order’s shipping address.', + 'Create rules that allow this gateway to match the billing address.' => 'Create rules that allow this gateway to match the billing address.', + 'Create rules that allow this gateway to match the order.' => 'Create rules that allow this gateway to match the order.', + 'Create rules that allow this gateway to match the shipping address.' => 'Create rules that allow this gateway to match the shipping address.', + 'Create sales' => 'Create sales', + 'Create sale…' => 'Create sale…', + 'Created' => 'Created', + 'Credit Card Payment Type' => 'Credit Card Payment Type', + 'Currency Code' => 'Currency Code', + 'Currency saved.' => 'Currency saved.', + 'Currency' => 'Currency', + 'Current' => 'Current', + 'Custom 1' => 'Custom 1', + 'Custom 2' => 'Custom 2', + 'Custom 3' => 'Custom 3', + 'Custom 4' => 'Custom 4', + 'Custom' => 'Custom', + 'Customer Enabled?' => 'Customer Enabled?', + 'Customer ID is required.' => 'Customer ID is required.', + 'Customer Note' => 'Customer Note', + 'Customer Notices' => 'Customer Notices', + 'Customer data' => 'Customer data', + 'Customer' => 'Customer', + 'Damaged' => 'Damaged', + 'Data shown might be outdated.' => 'Data shown might be outdated.', + 'Date Authorized' => 'Date Authorized', + 'Date Created' => 'Date Created', + 'Date First Paid' => 'Date First Paid', + 'Date Ordered' => 'Date Ordered', + 'Date Paid' => 'Date Paid', + 'Date Updated' => 'Date Updated', + 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date', + 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Date from which the discount will be active. Leave blank for unlimited start date', + 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Date from which the sale will be active. Leave blank for unlimited start date', + 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date', + 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Date when the discount will be finished. Leave blank for unlimited end date', + 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Date when the sale will be finished. Leave blank for unlimited end date', + 'Date' => 'Date', + 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Default - Allow the price to be negative if discounts are greater than the order value.', + 'Default Category' => 'Default Category', + 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.', + 'Default Order PDF' => 'Default Order PDF', + 'Default Per Item Rate' => 'Default Per Item Rate', + 'Default Percentage Rate' => 'Default Percentage Rate', + 'Default Status?' => 'Default Status?', + 'Default View' => 'Default View', + 'Default Weight Rate' => 'Default Weight Rate', + 'Default Zone' => 'Default Zone', + 'Default status?' => 'Default status?', + 'Default to this tax zone when no billing address is set' => 'Default to this tax zone when no billing address is set', + 'Default to this tax zone when no shipping address is set' => 'Default to this tax zone when no shipping address is set', + 'Default variant updated.' => 'Default variant updated.', + 'Default' => 'Default', + 'Default?' => 'Default?', + 'Delete catalog pricing rules' => 'Delete catalog pricing rules', + 'Delete discounts' => 'Delete discounts', + 'Delete orders' => 'Delete orders', + 'Delete sales' => 'Delete sales', + 'Delete' => 'Delete', + 'Deleting the {location} location.' => 'Deleting the {location} location.', + 'Describe this rule.' => 'Describe this rule.', + 'Describe this shipping zone.' => 'Describe this shipping zone.', + 'Describe this tax zone.' => 'Describe this tax zone.', + 'Description' => 'Description', + 'Destination Inventory Location' => 'Destination Inventory Location', + 'Destination' => 'Destination', + 'Details' => 'Details', + 'Dimension Unit' => 'Dimension Unit', + 'Dimensions' => 'Dimensions', + 'Disabled' => 'Disabled', + 'Disallow' => 'Disallow', + 'Discount all line items' => 'Discount all line items', + 'Discount description.' => 'Discount description.', + 'Discount is not allowed for the order' => 'Discount is not allowed for the order', + 'Discount is out of date.' => 'Discount is out of date.', + 'Discount saved.' => 'Discount saved.', + 'Discount the matching items only' => 'Discount the matching items only', + 'Discount use has reached its limit.' => 'Discount use has reached its limit.', + 'Discount' => 'Discount', + 'Discounted Item Subtotal' => 'Discounted Item Subtotal', + 'Discounted Items' => 'Discounted Items', + 'Discounts deleted.' => 'Discounts deleted.', + 'Discounts reordered.' => 'Discounts reordered.', + 'Discounts updated.' => 'Discounts updated.', + 'Discounts' => 'Discounts', + 'Disqualify with valid business tax ID?' => 'Disqualify with valid business tax ID?', + 'Do not apply subsequent matching sales beyond applying this sale.' => 'Do not apply subsequent matching sales beyond applying this sale.', + 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Do not apply this rate if the order address has any of the selected valid business tax IDs.', + 'Do not attach a PDF to this email' => 'Do not attach a PDF to this email', + 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.', + 'Donation can not be zero.' => 'Donation can not be zero.', + 'Donation needs to be an amount.' => 'Donation needs to be an amount.', + 'Donation settings saved.' => 'Donation settings saved.', + 'Donation' => 'Donation', + 'Donations' => 'Donations', + 'Done' => 'Done', + 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Don’t apply any subsequent discounts to an order if this discount is applied', + 'Download PDF' => 'Download PDF', + 'Download PDF…' => 'Download PDF…', + 'Download Type' => 'Download Type', + 'Download' => 'Download', + 'Draft' => 'Draft', + 'Dummy gateway payment failed.' => 'Dummy gateway payment failed.', + 'Duplicate options exist' => 'Duplicate options exist', + 'Duration' => 'Duration', + 'EU VAT ID' => 'EU VAT ID', + 'Edit address' => 'Edit address', + 'Edit adjustments' => 'Edit adjustments', + 'Edit catalog pricing rules' => 'Edit catalog pricing rules', + 'Edit discounts' => 'Edit discounts', + 'Edit options' => 'Edit options', + 'Edit orders' => 'Edit orders', + 'Edit sales' => 'Edit sales', + 'Edit' => 'Edit', + 'Effect' => 'Effect', + 'Either (Default) - The relationship field is on the purchasable or the category' => 'Either (Default) - The relationship field is on the purchasable or the category', + 'Either way' => 'Either way', + 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}', + 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.', + 'Email Subject' => 'Email Subject', + 'Email error. No email address found for order. Order: “{order}”' => 'Email error. No email address found for order. Order: “{order}”', + 'Email is not enabled.' => 'Email is not enabled.', + 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.', + 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email required to make payments on a completed order.' => 'Email required to make payments on a completed order.', + 'Email saved.' => 'Email saved.', + 'Email sent' => 'Email sent', + 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.', + 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}', + 'Email unavailable.' => 'Email unavailable.', + 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}', + 'Email “{email}” for order {order} was cancelled.' => 'Email “{email}” for order {order} was cancelled.', + 'Email' => 'Email', + 'Emails' => 'Emails', + 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.', + 'Enable structure for products of this type' => 'Enable structure for products of this type', + 'Enable this discount' => 'Enable this discount', + 'Enable this rule' => 'Enable this rule', + 'Enable this sale' => 'Enable this sale', + 'Enable this shipping method on the front end' => 'Enable this shipping method on the front end', + 'Enable this shipping rule' => 'Enable this shipping rule', + 'Enable this tax rate' => 'Enable this tax rate', + 'Enabled for customers to select during checkout?' => 'Enabled for customers to select during checkout?', + 'Enabled for customers to select?' => 'Enabled for customers to select?', + 'Enabled' => 'Enabled', + 'Enabled?' => 'Enabled?', + 'End Date' => 'End Date', + 'Enter SKU' => 'Enter SKU', + 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Enter a human-friendly name for this tax rate to be used in the control panel.', + 'Enter a percentage like {ex1} or {ex2}.' => 'Enter a percentage like {ex1} or {ex2}.', + 'Enter coupon code' => 'Enter coupon code', + 'Enter reference' => 'Enter reference', + 'Error refunding transaction: {transactionHash}' => 'Error refunding transaction: {transactionHash}', + 'Every new store must be assigned to at least one site.' => 'Every new store must be assigned to at least one site.', + 'Everywhere' => 'Everywhere', + 'Example' => 'Example', + 'Exclude this discount for products that are already on promotion' => 'Exclude this discount for products that are already on promotion', + 'Expired Link' => 'Expired Link', + 'Expired' => 'Expired', + 'Expiry Date' => 'Expiry Date', + 'Expiry date' => 'Expiry date', + 'Expiry' => 'Expiry', + 'Failed to receive transfer: {error}' => 'Failed to receive transfer: {error}', + 'Failed to send email. Please try again.' => 'Failed to send email. Please try again.', + 'Failed to start' => 'Failed to start', + 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Failed to update {num, plural, =1{order status} other{order statuses}}.', + 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Failed updating order status on {num, plural, =1{order} other{orders}}.', + 'Feet (ft)' => 'Feet (ft)', + 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.', + 'First Name' => 'First Name', + 'Flat Amount Off Order' => 'Flat Amount Off Order', + 'Flat Order Discount Amount Off' => 'Flat Order Discount Amount Off', + 'Free Order Payment Strategy' => 'Free Order Payment Strategy', + 'Free Shipping' => 'Free Shipping', + 'Free orders are processed by the payment gateway' => 'Free orders are processed by the payment gateway', + 'Free orders complete immediately' => 'Free orders complete immediately', + 'Free shipping can only be for whole order or matching items, not both.' => 'Free shipping can only be for whole order or matching items, not both.', + 'From Name' => 'From Name', + 'Fulfill' => 'Fulfill', + 'Fulfilled' => 'Fulfilled', + 'Fulfillment' => 'Fulfillment', + 'Full Name' => 'Full Name', + 'Gateway Code' => 'Gateway Code', + 'Gateway Message' => 'Gateway Message', + 'Gateway Reference' => 'Gateway Reference', + 'Gateway Response' => 'Gateway Response', + 'Gateway doesn’t support authorize' => 'Gateway doesn’t support authorize', + 'Gateway doesn’t support partial refunds.' => 'Gateway doesn’t support partial refunds.', + 'Gateway doesn’t support purchase' => 'Gateway doesn’t support purchase', + 'Gateway doesn’t support refunds.' => 'Gateway doesn’t support refunds.', + 'Gateway saved.' => 'Gateway saved.', + 'Gateway' => 'Gateway', + 'Gateways reordered.' => 'Gateways reordered.', + 'Gateways' => 'Gateways', + 'General Settings' => 'General Settings', + 'General' => 'General', + 'Generate' => 'Generate', + 'Generated Coupon Format' => 'Generated Coupon Format', + 'Grams (g)' => 'Grams (g)', + 'Groups for which this sale will be applicable to.' => 'Groups for which this sale will be applicable to.', + 'HTML Email Template Path' => 'HTML Email Template Path', + 'Handle' => 'Handle', + 'Harmonized System Code' => 'Harmonized System Code', + 'Has Admin Notices' => 'Has Admin Notices', + 'Has Emails?' => 'Has Emails?', + 'Has Free Shipping' => 'Has Free Shipping', + 'Has Orders' => 'Has Orders', + 'Has Purchasable' => 'Has Purchasable', + 'Has Variants?' => 'Has Variants?', + 'Height ({unit})' => 'Height ({unit})', + 'Height' => 'Height', + 'Hide snapshot' => 'Hide snapshot', + 'History' => 'History', + 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).', + 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.', + 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.', + 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.', + 'How products should be labeled within the control panel.' => 'How products should be labeled within the control panel.', + 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).', + 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}', + 'How this shipping method will be referred to in templates and forms.' => 'How this shipping method will be referred to in templates and forms.', + 'How variants should be labeled within the control panel.' => 'How variants should be labeled within the control panel.', + 'How you’ll refer to this PDF in the templates.' => 'How you’ll refer to this PDF in the templates.', + 'How you’ll refer to this product type in the templates.' => 'How you’ll refer to this product type in the templates.', + 'How you’ll refer to this shipping category in the templates.' => 'How you’ll refer to this shipping category in the templates.', + 'How you’ll refer to this status in the templates.' => 'How you’ll refer to this status in the templates.', + 'How you’ll refer to this subscription plan in the templates.' => 'How you’ll refer to this subscription plan in the templates.', + 'How you’ll refer to this tax category in the templates.' => 'How you’ll refer to this tax category in the templates.', + 'ID' => 'ID', + 'IP Address' => 'IP Address', + 'If disabled, this PDF will not be available or sent with emails.' => 'If disabled, this PDF will not be available or sent with emails.', + 'If disabled, this email will not send.' => 'If disabled, this email will not send.', + 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.', + 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.', + 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.', + 'Ignore Promotions?' => 'Ignore Promotions?', + 'Ignore previous matching sales if this sale matches.' => 'Ignore previous matching sales if this sale matches.', + 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignore promotional prices when this discount is applied to matching line items', + 'Inactive Carts' => 'Inactive Carts', + 'Inches (in)' => 'Inches (in)', + 'Include built-in line item tax.' => 'Include built-in line item tax.', + 'Include in price?' => 'Include in price?', + 'Include line item discounts.' => 'Include line item discounts.', + 'Include line item shipping costs.' => 'Include line item shipping costs.', + 'Include separate line item tax.' => 'Include separate line item tax.', + 'Included in price?' => 'Included in price?', + 'Included' => 'Included', + 'Incoming transfer from Transfer ID: ' => 'Incoming transfer from Transfer ID: ', + 'Incoming' => 'Incoming', + 'Info' => 'Info', + 'Information linked?' => 'Information linked?', + 'Information' => 'Information', + 'Invalid JSON' => 'Invalid JSON', + 'Invalid Order ID' => 'Invalid Order ID', + 'Invalid VAT ID.' => 'Invalid VAT ID.', + 'Invalid condition syntax' => 'Invalid condition syntax', + 'Invalid email.' => 'Invalid email.', + 'Invalid formula syntax' => 'Invalid formula syntax', + 'Invalid gateway: {value}' => 'Invalid gateway: {value}', + 'Invalid inventory movements.' => 'Invalid inventory movements.', + 'Invalid order condition syntax.' => 'Invalid order condition syntax.', + 'Invalid payment or order. Please review.' => 'Invalid payment or order. Please review.', + 'Invalid payment source ID: {value}' => 'Invalid payment source ID: {value}', + 'Invalid store.' => 'Invalid store.', + 'Invalid user.' => 'Invalid user.', + 'Inventory Item' => 'Inventory Item', + 'Inventory Location' => 'Inventory Location', + 'Inventory Locations' => 'Inventory Locations', + 'Inventory Tracked' => 'Inventory Tracked', + 'Inventory Transfers' => 'Inventory Transfers', + 'Inventory could not be set.' => 'Inventory could not be set.', + 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'Inventory location has committed stock, the order(s) must first be fulfilled.', + 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Inventory location has incoming stock, the transfer(s) must first be completed.', + 'Inventory location is already deactivated.' => 'Inventory location is already deactivated.', + 'Inventory location saved.' => 'Inventory location saved.', + 'Inventory locations not saved.' => 'Inventory locations not saved.', + 'Inventory movement could not be saved.' => 'Inventory movement could not be saved.', + 'Inventory movement saved.' => 'Inventory movement saved.', + 'Inventory updated.' => 'Inventory updated.', + 'Inventory was not updated.' => 'Inventory was not updated.', + 'Inventory' => 'Inventory', + 'Invoice amount' => 'Invoice amount', + 'Invoice date' => 'Invoice date', + 'Is Promotable' => 'Is Promotable', + 'Is Promotional Price?' => 'Is Promotional Price?', + 'Is Shippable' => 'Is Shippable', + 'Is Taxable' => 'Is Taxable', + 'Item Rates' => 'Item Rates', + 'Item Subtotal' => 'Item Subtotal', + 'Item Total' => 'Item Total', + 'Item' => 'Item', + 'Items' => 'Items', + 'Kilograms (kg)' => 'Kilograms (kg)', + 'Label' => 'Label', + 'Landscape' => 'Landscape', + 'Language' => 'Language', + 'Last Name' => 'Last Name', + 'Last Updated' => 'Last Updated', + 'Leave a category rate override blank to use the rate from above.' => 'Leave a category rate override blank to use the rate from above.', + 'Leave blank for unlimited uses.' => 'Leave blank for unlimited uses.', + 'Leave blank if products don’t have URLs' => 'Leave blank if products don’t have URLs', + 'Leave gateway subscription as-is' => 'Leave gateway subscription as-is', + 'Length ({unit})' => 'Length ({unit})', + 'Length' => 'Length', + 'Let each product choose which sites it should be saved to' => 'Let each product choose which sites it should be saved to', + 'Limit which orders this discount applies to based on its line items.' => 'Limit which orders this discount applies to based on its line items.', + 'Limit which purchasables this sale applies to.' => 'Limit which purchasables this sale applies to.', + 'Limit' => 'Limit', + 'Line Item Statuses' => 'Line Item Statuses', + 'Line Item' => 'Line Item', + 'Line Items' => 'Line Items', + 'Line item price (minus discounts)' => 'Line item price (minus discounts)', + 'Line item shipping cost' => 'Line item shipping cost', + 'Line item statuses reordered.' => 'Line item statuses reordered.', + 'Link Duration' => 'Link Duration', + 'Link Sent' => 'Link Sent', + 'Link to a product' => 'Link to a product', + 'Link to a variant' => 'Link to a variant', + 'Link' => 'Link', + 'Live' => 'Live', + 'Location' => 'Location', + 'Locations that should be available for previewing products in this product type.' => 'Locations that should be available for previewing products in this product type.', + 'MM' => 'MM', + 'Make a payment' => 'Make a payment', + 'Make this the primary store' => 'Make this the primary store', + 'Manage Inventory' => 'Manage Inventory', + 'Manage donation settings' => 'Manage donation settings', + 'Manage general store settings' => 'Manage general store settings', + 'Manage inventory locations' => 'Manage inventory locations', + 'Manage inventory stock levels' => 'Manage inventory stock levels', + 'Manage inventory transfers' => 'Manage inventory transfers', + 'Manage orders' => 'Manage orders', + 'Manage payment currencies' => 'Manage payment currencies', + 'Manage promotions' => 'Manage promotions', + 'Manage shipping' => 'Manage shipping', + 'Manage store settings' => 'Manage store settings', + 'Manage subscription plans' => 'Manage subscription plans', + 'Manage subscription' => 'Manage subscription', + 'Manage subscriptions' => 'Manage subscriptions', + 'Manage taxes' => 'Manage taxes', + 'Manage' => 'Manage', + 'Mark as Pending' => 'Mark as Pending', + 'Mark as completed' => 'Mark as completed', + 'Match Billing Address' => 'Match Billing Address', + 'Match Customer' => 'Match Customer', + 'Match Order' => 'Match Order', + 'Match Orders' => 'Match Orders', + 'Match Product' => 'Match Product', + 'Match Purchasable' => 'Match Purchasable', + 'Match Shipping Address' => 'Match Shipping Address', + 'Match Variant' => 'Match Variant', + 'Matching Items' => 'Matching Items', + 'Max Qty' => 'Max Qty', + 'Max Uses' => 'Max Uses', + 'Max Variants' => 'Max Variants', + 'Max quantity must greater than min.' => 'Max quantity must greater than min.', + 'Maximum Purchase Quantity' => 'Maximum Purchase Quantity', + 'Maximum Total Shipping Cost' => 'Maximum Total Shipping Cost', + 'Maximum allowed quantity' => 'Maximum allowed quantity', + 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.', + 'Maximum order quantity for this item is {num}.' => 'Maximum order quantity for this item is {num}.', + 'Message' => 'Message', + 'Meters (m)' => 'Meters (m)', + 'Millimeters (mm)' => 'Millimeters (mm)', + 'Min Qty' => 'Min Qty', + 'Min quantity must be less than max.' => 'Min quantity must be less than max.', + 'Minimum Purchase Quantity' => 'Minimum Purchase Quantity', + 'Minimum Total Price Strategy' => 'Minimum Total Price Strategy', + 'Minimum Total Shipping Cost' => 'Minimum Total Shipping Cost', + 'Minimum allowed quantity' => 'Minimum allowed quantity', + 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Minimum number of matching items that need to be ordered for this discount to apply.', + 'Minimum order quantity for this item is {num}.' => 'Minimum order quantity for this item is {num}.', + 'Missing Gateway' => 'Missing Gateway', + 'Missing a default inventory location.' => 'Missing a default inventory location.', + 'Move Inventory' => 'Move Inventory', + 'Move To' => 'Move To', + 'Move {qty} from {fromType} to {toType}' => 'Move {qty} from {fromType} to {toType}', + 'Move' => 'Move', + 'Movement from deactivated inventory location' => 'Movement from deactivated inventory location', + 'Movement' => 'Movement', + 'Must have at least one variant.' => 'Must have at least one variant.', + 'Name Field' => 'Name Field', + 'Name' => 'Name', + 'New Customer' => 'New Customer', + 'New Customers' => 'New Customers', + 'New Order' => 'New order', + 'New PDF' => 'New PDF', + 'New address' => 'New address', + 'New catalog pricing rule' => 'New catalog pricing rule', + 'New currency' => 'New currency', + 'New discount' => 'New discount', + 'New email' => 'New email', + 'New gateway' => 'New gateway', + 'New line item status' => 'New line item status', + 'New line items get this status by default when the order is completed' => 'New line items get this status by default when the order is completed', + 'New location' => 'New location', + 'New order status' => 'New order status', + 'New orders get this status by default' => 'New orders get this status by default', + 'New product type' => 'New product type', + 'New product' => 'New product', + 'New product, choose a type' => 'New product, choose a type', + 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'New products default to the first tax category available to them. If none are available, this category will be used.', + 'New sale' => 'New sale', + 'New shipping category' => 'New shipping category', + 'New shipping method' => 'New shipping method', + 'New shipping rule' => 'New shipping rule', + 'New shipping zone' => 'New shipping zone', + 'New subscription plan' => 'New subscription plan', + 'New tax category' => 'New tax category', + 'New tax rate' => 'New tax rate', + 'New tax zone' => 'New tax zone', + 'New transfer' => 'New transfer', + 'New {productType} product' => 'New {productType} product', + 'New' => 'New', + 'Next payment' => 'Next payment', + 'No Address' => 'No Address', + 'No PDFs exist yet.' => 'No PDFs exist yet.', + 'No access given to any specific store management features.' => 'No access given to any specific store management features.', + 'No additional payment currencies exist yet.' => 'No additional payment currencies exist yet.', + 'No address' => 'No address', + 'No billing address' => 'No billing address', + 'No catalog pricing rule exists with the ID “{id}”' => 'No catalog pricing rule exists with the ID “{id}”', + 'No catalog pricing rules exist yet.' => 'No catalog pricing rules exist yet.', + 'No currency exists with the ID “{id}”' => 'No currency exists with the ID “{id}”', + 'No customer email address exists on this cart.' => 'No customer email address exists on this cart.', + 'No description' => 'No description', + 'No discount exists with the ID “{id}”' => 'No discount exists with the ID “{id}”', + 'No discounts exist yet.' => 'No discounts exist yet.', + 'No donation amount supplied.' => 'No donation amount supplied.', + 'No emails exist yet.' => 'No emails exist yet.', + 'No inventory changes made.' => 'No inventory changes made.', + 'No inventory found.' => 'No inventory found.', + 'No inventory movements made.' => 'No inventory movements made.', + 'No inventory transactions for this location.' => 'No inventory transactions for this location.', + 'No new customer selected.' => 'No new customer selected.', + 'No order history exists with the ID “{id}”' => 'No order history exists with the ID “{id}”', + 'No order status history items will exist until the cart becomes an order.' => 'No order status history items will exist until the cart becomes an order.', + 'No payment source exists with the ID “{id}”' => 'No payment source exists with the ID “{id}”', + 'No private Note.' => 'No private Note.', + 'No product available.' => 'No product available.', + 'No product types exist yet.' => 'No product types exist yet.', + 'No purchasable available.' => 'No purchasable available.', + 'No sale exists with the ID “{id}”' => 'No sale exists with the ID “{id}”', + 'No sales exist yet.' => 'No sales exist yet.', + 'No shipping address' => 'No shipping address', + 'No shipping category exists with the ID “{id}”' => 'No shipping category exists with the ID “{id}”', + 'No shipping method exists with the ID “{id}”' => 'No shipping method exists with the ID “{id}”', + 'No shipping rule exists with the ID “{id}”' => 'No shipping rule exists with the ID “{id}”', + 'No shipping rules exist yet.' => 'No shipping rules exist yet.', + 'No shipping zone exists with the ID “{id}”' => 'No shipping zone exists with the ID “{id}”', + 'No stats available.' => 'No stats available.', + 'No subscription plan exists with the ID “{id}”' => 'No subscription plan exists with the ID “{id}”', + 'No subscription plans exist yet.' => 'No subscription plans exist yet.', + 'No tax category exists with the ID “{id}”' => 'No tax category exists with the ID “{id}”', + 'No tax rate exists with the ID “{id}”' => 'No tax rate exists with the ID “{id}”', + 'No tax zone exists with the ID “{id}”' => 'No tax zone exists with the ID “{id}”', + 'No transactions exist.' => 'No transactions exist.', + 'No user authenticated.' => 'No user authenticated.', + 'No' => 'No', + 'None on hand' => 'None on hand', + 'None' => 'None', + 'Not a valid address type' => 'Not a valid address type', + 'Not a valid credit card number.' => 'Not a valid credit card number.', + 'Not all SKUs are unique.' => 'Not all SKUs are unique.', + 'Note' => 'Note', + 'Notes' => 'Notes', + 'Number of Coupons' => 'Number of Coupons', + 'Number' => 'Number', + 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Of the enabled sites above, which sites should products in this product type be saved to?', + 'On Hand' => 'On Hand', + 'Only allow this gateway to be used for zero value orders?' => 'Only allow this gateway to be used for zero value orders?', + 'Only match certain purchasables…' => 'Only match certain purchasables…', + 'Only match purchasables related to…' => 'Only match purchasables related to…', + 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Only orders with the following order statuses will be included. Leave blank to include all statuses.', + 'Only save product to the site they were created in' => 'Only save product to the site they were created in', + 'Options' => 'Options', + 'Order Condition Formula' => 'Order Condition Formula', + 'Order Description Format' => 'Order Description Format', + 'Order Details' => 'Order Details', + 'Order Fields' => 'Order Fields', + 'Order PDF Download Link' => 'Order PDF Download Link', + 'Order PDF Filename Format' => 'Order PDF Filename Format', + 'Order Reference Number Format' => 'Order Reference Number Format', + 'Order Settings' => 'Order Settings', + 'Order Site' => 'Order Site', + 'Order Status description.' => 'Order Status description.', + 'Order Status' => 'Order Status', + 'Order Statuses' => 'Order Statuses', + 'Order can not be empty.' => 'Order can not be empty.', + 'Order count' => 'Order count', + 'Order customer data removed.' => 'Order customer data removed.', + 'Order deleted.' => 'Order deleted.', + 'Order fields saved.' => 'Order fields saved.', + 'Order not found.' => 'Order not found.', + 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.', + 'Order recalculated.' => 'Order recalculated.', + 'Order status saved.' => 'Order status saved.', + 'Order statuses reordered.' => 'Order statuses reordered.', + 'Order total shipping cost' => 'Order total shipping cost', + 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)', + 'Order' => 'Order', + 'Orders (Legacy)' => 'Orders (Legacy)', + 'Orders deleted.' => 'Orders deleted.', + 'Orders not restored.' => 'Orders not restored.', + 'Orders restored.' => 'Orders restored.', + 'Orders' => 'Orders', + 'Organization Name' => 'Organization Name', + 'Organization Tax ID' => 'Organization Tax ID', + 'Origin and destination cannot be the same.' => 'Origin and destination cannot be the same.', + 'Origin' => 'Origin', + 'Original Price' => 'Original Price', + 'Original price' => 'Original price', + 'Original promotional price' => 'Original promotional price', + 'Other Languages' => 'Other Languages', + 'Other countries' => 'Other countries', + 'Outgoing transfer from Transfer ID: ' => 'Outgoing transfer from Transfer ID: ', + 'Overpaid' => 'Overpaid', + 'Overrides previous?' => 'Overrides previous?', + 'PDF Attachment' => 'PDF Attachment', + 'PDF Template Path' => 'PDF Template Path', + 'PDF saved.' => 'PDF saved.', + 'PDF' => 'PDF', + 'PDFs & Emails' => 'PDFs & Emails', + 'PDFs' => 'PDFs', + 'Paid Amount' => 'Paid Amount', + 'Paid Status' => 'Paid Status', + 'Paid' => 'Paid', + 'Paper Orientation' => 'Paper Orientation', + 'Paper Size' => 'Paper Size', + 'Partial payment not allowed.' => 'Partial payment not allowed.', + 'Partial' => 'Partial', + 'Past year' => 'Past year', + 'Past {num} days' => 'Past {num} days', + 'Pay {amount} of {currency} on the order.' => 'Pay {amount} of {currency} on the order.', + 'Pay' => 'Pay', + 'Payment Amount' => 'Payment Amount', + 'Payment Currencies' => 'Payment Currencies', + 'Payment Gateway' => 'Payment Gateway', + 'Payment Method' => 'Payment Method', + 'Payment error: {message}' => 'Payment error: {message}', + 'Payment method issue' => 'Payment method issue', + 'Payment source created.' => 'Payment source created.', + 'Payment source deleted.' => 'Payment source deleted.', + 'Payments' => 'Payments', + 'Pending' => 'Pending', + 'Per Email Address Discount Limit' => 'Per Email Address Discount Limit', + 'Per Item Amount Off' => 'Per Item Amount Off', + 'Per Item Discount' => 'Per Item Discount', + 'Per Item Percentage Off' => 'Per Item Percentage Off', + 'Per Item Rate' => 'Per Item Rate', + 'Per User Discount Limit' => 'Per User Discount Limit', + 'Percentage Rate' => 'Percentage Rate', + 'Phone (Alt)' => 'Phone (Alt)', + 'Phone' => 'Phone', + 'Pick a plan' => 'Pick a plan', + 'Plain Text Email Template Path' => 'Plain Text Email Template Path', + 'Plan' => 'Plan', + 'Plans reordered.' => 'Plans reordered.', + 'Portrait' => 'Portrait', + 'Post Date' => 'Post Date', + 'Postal Code Formula' => 'Postal Code Formula', + 'Pounds (lb)' => 'Pounds (lb)', + 'Preview' => 'Preview', + 'Previous Status' => 'Previous Status', + 'Price' => 'Price', + 'Prices' => 'Prices', + 'Pricing Rules' => 'Pricing Rules', + 'Pricing jobs are currently running.' => 'Pricing jobs are currently running.', + 'Pricing' => 'Pricing', + 'Primary Billing Address' => 'Primary Billing Address', + 'Primary Shipping Address' => 'Primary Shipping Address', + 'Primary payment source updated.' => 'Primary payment source updated.', + 'Primary' => 'Primary', + 'Private Note' => 'Private Note', + 'Product Fields' => 'Product Fields', + 'Product ID is required.' => 'Product ID is required.', + 'Product Template' => 'Product Template', + 'Product Title Format' => 'Product Title Format', + 'Product Type' => 'Product Type', + 'Product Types' => 'Product Types', + 'Product URI Format' => 'Product URI Format', + 'Product Variant' => 'Product Variant', + 'Product Variants' => 'Product Variants', + 'Product type saved.' => 'Product type saved.', + 'Product type settings' => 'Product type settings', + 'Product' => 'Product', + 'Products and Variants deleted.' => 'Products and Variants deleted.', + 'Products not restored.' => 'Products not restored.', + 'Products restored.' => 'Products restored.', + 'Products' => 'Products', + 'Promotable' => 'Promotable', + 'Promotable?' => 'Promotable?', + 'Promotional Amount' => 'Promotional Amount', + 'Promotional Price' => 'Promotional Price', + 'Purchasable Categories' => 'Purchasable Categories', + 'Purchasable ID and Sale ID are required.' => 'Purchasable ID and Sale ID are required.', + 'Purchasable ID is required.' => 'Purchasable ID is required.', + 'Purchasable Type' => 'Purchasable Type', + 'Purchasable' => 'Purchasable', + 'Purchase (Authorize and Capture Immediately)' => 'Purchase (Authorize and Capture Immediately)', + 'Purchase Total' => 'Purchase Total', + 'Qty' => 'Qty', + 'Quality Control' => 'Quality Control', + 'Quantity' => 'Quantity', + 'Rate' => 'Rate', + 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Reassign {numOrders, plural, =1{order} other{orders}}', + 'Recalculate order' => 'Recalculate order', + 'Receive Inventory' => 'Receive Inventory', + 'Receive Transfer' => 'Receive Transfer', + 'Receive' => 'Receive', + 'Received' => 'Received', + 'Recent Orders' => 'Recent Orders', + 'Recipient' => 'Recipient', + 'Recover Cart' => 'Recover Cart', + 'Reduce price' => 'Reduce price', + 'Reduce the price by a fixed amount' => 'Reduce the price by a fixed amount', + 'Reduce the price by a percentage of the original price' => 'Reduce the price by a percentage of the original price', + 'Reference' => 'Reference', + 'Refresh payment history' => 'Refresh payment history', + 'Refund note' => 'Refund note', + 'Refund payment' => 'Refund payment', + 'Refund' => 'Refund', + 'Reject' => 'Reject', + 'Rejected' => 'Rejected', + 'Relationship Type' => 'Relationship Type', + 'Removable included tax rates are only allowed for the default tax zone.' => 'Removable included tax rates are only allowed for the default tax zone.', + 'Remove address' => 'Remove address', + 'Remove all shipping costs from the order' => 'Remove all shipping costs from the order', + 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below', + 'Remove customer data' => 'Remove customer data', + 'Remove from price?' => 'Remove from price?', + 'Remove shipping costs for matching items only' => 'Remove shipping costs for matching items only', + 'Remove the included tax when a valid organization tax ID is present?' => 'Remove the included tax when a valid organization tax ID is present?', + 'Remove' => 'Remove', + 'Removed' => 'Removed', + 'Repeat Customers' => 'Repeat Customers', + 'Reply To' => 'Reply To', + 'Require Billing Address At Checkout' => 'Require Billing Address At Checkout', + 'Require Coupon Code' => 'Require Coupon Code', + 'Require Shipping Address At Checkout' => 'Require Shipping Address At Checkout', + 'Require Shipping Method Selection At Checkout' => 'Require Shipping Method Selection At Checkout', + 'Require' => 'Require', + 'Reserved' => 'Reserved', + 'Reset usage' => 'Reset usage', + 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.', + 'Revenue Options' => 'Revenue Options', + 'Revenue' => 'Revenue', + 'Rule' => 'Rule', + 'Rules reordered.' => 'Rules reordered.', + 'SKU' => 'SKU', + 'Safety' => 'Safety', + 'Sale Price' => 'Sale Price', + 'Sale description.' => 'Sale description.', + 'Sale reordered.' => 'Sale reordered.', + 'Sale saved.' => 'Sale saved.', + 'Sale' => 'Sale', + 'Sales deleted.' => 'Sales deleted.', + 'Sales updated.' => 'Sales updated.', + 'Sales' => 'Sales', + 'Save and continue editing' => 'Save and continue editing', + 'Save and return to all orders' => 'Save and return to all orders', + 'Save and set rules' => 'Save and set rules', + 'Save as a new rule' => 'Save as a new rule', + 'Save product to all sites enabled for this product type' => 'Save product to all sites enabled for this product type', + 'Save product to other sites in the same site group' => 'Save product to other sites in the same site group', + 'Save product to other sites with the same language' => 'Save product to other sites with the same language', + 'Save' => 'Save', + 'Search customer…' => 'Search customer…', + 'Search inventory' => 'Search inventory', + 'Search or enter customer email…' => 'Search or enter customer email…', + 'Search…' => 'Search…', + 'See Orders' => 'See Orders', + 'Select a gateway' => 'Select a gateway', + 'Select a tax category.' => 'Select a tax category.', + 'Select a tax zone. If empty, this rate will match anywhere.' => 'Select a tax zone. If empty, this rate will match anywhere.', + 'Select address' => 'Select address', + 'Select an item' => 'Select an item', + 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Select how the catalog pricing rule will be applied to the purchasable(s).', + 'Select how the sale will be applied to the purchasable(s).' => 'Select how the sale will be applied to the purchasable(s).', + 'Select product type' => 'Select product type', + 'Select the emails that will be sent when transitioning to this status.' => 'Select the emails that will be sent when transitioning to this status.', + 'Select what this rate should be applied to.' => 'Select what this rate should be applied to.', + 'Send Email' => 'Send Email', + 'Send to custom recipient' => 'Send to custom recipient', + 'Send to the customer' => 'Send to the customer', + 'Set Quantity' => 'Set Quantity', + 'Set default category' => 'Set default category', + 'Set default variant' => 'Set default variant', + 'Set or Adjust' => 'Set or Adjust', + 'Set price' => 'Set price', + 'Set status' => 'Set status', + 'Set the price to a flat amount' => 'Set the price to a flat amount', + 'Set the price to a percentage of the original price' => 'Set the price to a percentage of the original price', + 'Set the sale price to a flat amount' => 'Set the sale price to a flat amount', + 'Set the sale price to a percentage of the original price' => 'Set the sale price to a percentage of the original price', + 'Set to' => 'Set to', + 'Settings saved.' => 'Settings saved.', + 'Settings' => 'Settings', + 'Share cart…' => 'Share cart…', + 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.', + 'Shipping Address Zone' => 'Shipping Address Zone', + 'Shipping Address' => 'Shipping Address', + 'Shipping Business Name' => 'Shipping Business Name', + 'Shipping Categories' => 'Shipping Categories', + 'Shipping Category Conditions' => 'Shipping Category Conditions', + 'Shipping Category' => 'Shipping Category', + 'Shipping First Name' => 'Shipping First Name', + 'Shipping Full Name' => 'Shipping Full Name', + 'Shipping Last Name' => 'Shipping Last Name', + 'Shipping Method' => 'Shipping Method', + 'Shipping Methods' => 'Shipping Methods', + 'Shipping Rule' => 'Shipping Rule', + 'Shipping Zones' => 'Shipping Zones', + 'Shipping address required.' => 'Shipping address required.', + 'Shipping categories deleted.' => 'Shipping categories deleted.', + 'Shipping category saved.' => 'Shipping category saved.', + 'Shipping category updated.' => 'Shipping category updated.', + 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.', + 'Shipping method saved.' => 'Shipping method saved.', + 'Shipping methods and rules deleted.' => 'Shipping methods and rules deleted.', + 'Shipping methods updated.' => 'Shipping methods updated.', + 'Shipping rule deleted.' => 'Shipping rule deleted.', + 'Shipping rule saved.' => 'Shipping rule saved.', + 'Shipping zone saved.' => 'Shipping zone saved.', + 'Shipping' => 'Shipping', + 'Short Number' => 'Short Number', + 'Show Chart?' => 'Show Chart?', + 'Show Order Count?' => 'Show Order Count?', + 'Show all prices' => 'Show all prices', + 'Show archived gateways' => 'Show archived gateways', + 'Show order count line on chart.' => 'Show order count line on chart.', + 'Show related sales' => 'Show related sales', + 'Show rule details' => 'Show rule details', + 'Show the Dimensions and Weight fields for products of this type' => 'Show the Dimensions and Weight fields for products of this type', + 'Show the Title field for products' => 'Show the Title field for products', + 'Show the Title field for variants' => 'Show the Title field for variants', + 'Signed In' => 'Signed In', + 'Site Languages' => 'Site Languages', + 'Site store mapping saved.' => 'Site store mapping saved.', + 'Sites' => 'Sites', + 'Slug' => 'Slug', + 'Snapshot' => 'Snapshot', + 'Snapshots' => 'Snapshots', + 'Some orders restored.' => 'Some orders restored.', + 'Some products restored.' => 'Some products restored.', + 'Some variants restored.' => 'Some variants restored.', + 'Something changed with the order before payment, please review your order and submit payment again.' => 'Something changed with the order before payment, please review your order and submit payment again.', + 'Sorry, no matching options.' => 'Sorry, no matching options.', + 'Source - The purchasable relationship field is on the category' => 'Source - The purchasable relationship field is on the category', + 'Source' => 'Source', + 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)', + 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)', + 'Start Date' => 'Start Date', + 'State' => 'State', + 'Status Email Address' => 'Status Email Address', + 'Status Emails' => 'Status Emails', + 'Status History' => 'Status History', + 'Status Updated.' => 'Status Updated.', + 'Status change message' => 'Status change message', + 'Status' => 'Status', + 'Stock' => 'Stock', + 'Stops Processing?' => 'Stops Processing?', + 'Stops subsequent?' => 'Stops subsequent?', + 'Store Location' => 'Store Location', + 'Store Management' => 'Store Management', + 'Store Markets' => 'Store Markets', + 'Store Rule' => 'Store Rule', + 'Store saved.' => 'Store saved.', + 'Store' => 'Store', + 'Stores & Sites' => 'Stores & Sites', + 'Stores' => 'Stores', + 'Strategy to apply when an order is free or has a zero balance.' => 'Strategy to apply when an order is free or has a zero balance.', + 'Strategy to apply when calculating the minimum order price.' => 'Strategy to apply when calculating the minimum order price.', + 'Subject' => 'Subject', + 'Subscribing user' => 'Subscribing user', + 'Subscription Fields' => 'Subscription Fields', + 'Subscription Plans' => 'Subscription Plans', + 'Subscription Settings' => 'Subscription Settings', + 'Subscription cancelled.' => 'Subscription cancelled.', + 'Subscription date' => 'Subscription date', + 'Subscription fields saved.' => 'Subscription fields saved.', + 'Subscription for {user} to {plan} prevented by a plugin.' => 'Subscription for {user} to {plan} prevented by a plugin.', + 'Subscription plan saved.' => 'Subscription plan saved.', + 'Subscription plan' => 'Subscription plan', + 'Subscription plans' => 'Subscription plans', + 'Subscription reactivated.' => 'Subscription reactivated.', + 'Subscription reference' => 'Subscription reference', + 'Subscription started.' => 'Subscription started.', + 'Subscription switched.' => 'Subscription switched.', + 'Subscription to “{plan}”' => 'Subscription to “{plan}”', + 'Subscription' => 'Subscription', + 'Subscriptions on hold' => 'Subscriptions on hold', + 'Subscriptions' => 'Subscriptions', + 'Suppress emails' => 'Suppress emails', + 'Switch plan' => 'Switch plan', + 'Switch' => 'Switch', + 'System' => 'System', + 'Table Columns' => 'Table Columns', + 'Target - The category relationship field is on the purchasable' => 'Target - The category relationship field is on the purchasable', + 'Tax & Shipping' => 'Tax & Shipping', + 'Tax (inc)' => 'Tax (inc)', + 'Tax Categories' => 'Tax Categories', + 'Tax Category' => 'Tax Category', + 'Tax Rates' => 'Tax Rates', + 'Tax Zone' => 'Tax Zone', + 'Tax Zones' => 'Tax Zones', + 'Tax categories deleted.' => 'Tax categories deleted.', + 'Tax category saved.' => 'Tax category saved.', + 'Tax category updated.' => 'Tax category updated.', + 'Tax rate saved.' => 'Tax rate saved.', + 'Tax rates updated.' => 'Tax rates updated.', + 'Tax zone saved.' => 'Tax zone saved.', + 'Tax' => 'Tax', + 'Taxable Subject' => 'Taxable Subject', + 'Template Path' => 'Template Path', + 'That handle is already in use' => 'That handle is already in use', + 'That handle is already in use.' => 'That handle is already in use.', + 'The PDF to attach to this email.' => 'The PDF to attach to this email.', + 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.', + 'The address provided is outside the store’s market.' => 'The address provided is outside the store’s market.', + 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.', + 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.', + 'The cart recovery link is invalid. Please request a new one.' => 'The cart recovery link is invalid. Please request a new one.', + 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.', + 'The countries that orders are allowed to be placed from.' => 'The countries that orders are allowed to be placed from.', + 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'The coupon "{code}" has exceeded its usage limit of {limit}.', + 'The customer for this order has been deleted.' => 'The customer for this order has been deleted.', + 'The default shipping category is automatically available to all product types.' => 'The default shipping category is automatically available to all product types.', + 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'The discount "{name}" has exceeded its total usage limit of {limit}.', + 'The download link has expired. Please request a new one.' => 'The download link has expired. Please request a new one.', + 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.', + 'The entry that contains the description for this subscription’s plan.' => 'The entry that contains the description for this subscription’s plan.', + 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'The flat value which should discount each item. i.e “3” for $3 off each item.', + 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.', + 'The from and to inventory locations must be different.' => 'The from and to inventory locations must be different.', + 'The inventory locations this store uses.' => 'The inventory locations this store uses.', + 'The item is not enabled for sale.' => 'The item is not enabled for sale.', + 'The language the order was made in.' => 'The language the order was made in.', + 'The language to be used when this email is rendered.' => 'The language to be used when this email is rendered.', + 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'The maximum number of levels this product type can have. Leave blank if you don’t care.', + 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'The maximum the customer should spend on shipping. Set to zero to disable.', + 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'The minimum the customer should spend on shipping. Set to zero to disable.', + 'The order is not valid.' => 'The order is not valid.', + 'The payment gateway that will be used for the subscription plan.' => 'The payment gateway that will be used for the subscription plan.', + 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.', + 'The previously-selected shipping method is no longer available.' => 'The previously-selected shipping method is no longer available.', + 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}', + 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}', + 'The primary currency cannot be changed after orders are placed.' => 'The primary currency cannot be changed after orders are placed.', + 'The purchasable defines the relationship' => 'The purchasable defines the relationship', + 'The purchasable is related by another element' => 'The purchasable is related by another element', + 'The recipient of the email. Twig code can be used here.' => 'The recipient of the email. Twig code can be used here.', + 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.', + 'The site the order was made in.' => 'The site the order was made in.', + 'The site to be used when this email is rendered.' => 'The site to be used when this email is rendered.', + 'The subject line of the email. Twig code can be used here.' => 'The subject line of the email. Twig code can be used here.', + 'The template that the PDF should be generated from.' => 'The template that the PDF should be generated from.', + 'The template to be used for HTML emails.' => 'The template to be used for HTML emails.', + 'The template to be used for plain text emails. Twig code can be used here.' => 'The template to be used for plain text emails. Twig code can be used here.', + 'The template to use when a product’s URL is requested.' => 'The template to use when a product’s URL is requested.', + 'The total number of order adjustments changed.' => 'The total number of order adjustments changed.', + 'The total price of the order changed.' => 'The total price of the order changed.', + 'The total quantity of items within the order changed.' => 'The total quantity of items within the order changed.', + 'The unique SKU of the donation purchasable.' => 'The unique SKU of the donation purchasable.', + 'The unit of measurement that should be used when specifying product dimensions.' => 'The unit of measurement that should be used when specifying product dimensions.', + 'The unit of measurement that should be used when specifying product weights.' => 'The unit of measurement that should be used when specifying product weights.', + 'The webhook URL for this gateway.' => 'The webhook URL for this gateway.', + 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.', + 'There are errors on the order' => 'There are errors on the order', + 'There are only {num} “{description}” items left in stock.' => 'There are only {num} “{description}” items left in stock.', + 'There aren’t any product types to select yet.' => 'There aren’t any product types to select yet.', + 'There is no gateway or payment source available for use with this order.' => 'There is no gateway or payment source available for use with this order.', + 'There is no gateway selected that supports payment sources.' => 'There is no gateway selected that supports payment sources.', + 'There is no shipping method selected for this order.' => 'There is no shipping method selected for this order.', + 'This URL will load the cart into the user’s session, making it the active cart.' => 'This URL will load the cart into the user’s session, making it the active cart.', + 'This action is not allowed for the current user.' => 'This action is not allowed for the current user.', + 'This category will be used as the default for all purchasables in this store.' => 'This category will be used as the default for all purchasables in this store.', + 'This coupon is for registered users and limited to {limit} uses.' => 'This coupon is for registered users and limited to {limit} uses.', + 'This coupon is limited to {limit} uses.' => 'This coupon is limited to {limit} uses.', + 'This coupon requires an email address.' => 'This coupon requires an email address.', + 'This gateway does not support that functionality.' => 'This gateway does not support that functionality.', + 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'This is being overridden by the {setting} config setting in `config/{file}.php`.', + 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.', + 'This is the default PDF that will be rendered when requesting the order PDF.' => 'This is the default PDF that will be rendered when requesting the order PDF.', + 'This is the last location for the {store} store.' => 'This is the last location for the {store} store.', + 'This month' => 'This month', + 'This order has unsaved changes.' => 'This order has unsaved changes.', + 'This week' => 'This week', + 'This year' => 'This year', + 'Times Used' => 'Times Used', + 'Title' => 'Title', + 'To' => 'To', + 'Today' => 'Today', + 'Too many variants for this product.' => 'Too many variants for this product.', + 'Top Customers by Average Order' => 'Top Customers by Average Order', + 'Top Customers by Total Revenue' => 'Top Customers by Total Revenue', + 'Top Customers' => 'Top Customers', + 'Top Product Types by Qty Sold' => 'Top Product Types by Qty Sold', + 'Top Product Types by Revenue' => 'Top Product Types by Revenue', + 'Top Product Types' => 'Top Product Types', + 'Top Products by Qty Sold' => 'Top Products by Qty Sold', + 'Top Products by Revenue' => 'Top Products by Revenue', + 'Top Products' => 'Top Products', + 'Top Purchasables by Qty Sold' => 'Top Purchasables by Qty Sold', + 'Top Purchasables by Revenue' => 'Top Purchasables by Revenue', + 'Top Purchasables' => 'Top Purchasables', + 'Total ' => 'Total ', + 'Total Discount Use Limit' => 'Total Discount Use Limit', + 'Total Discount' => 'Total Discount', + 'Total Included Tax' => 'Total Included Tax', + 'Total Orders by Billing Country' => 'Total Orders by Billing Country', + 'Total Orders by Country' => 'Total Orders by Country', + 'Total Orders by Shipping Country' => 'Total Orders by Shipping Country', + 'Total Orders' => 'Total Orders', + 'Total Paid' => 'Total Paid', + 'Total Price' => 'Total Price', + 'Total Qty' => 'Total Qty', + 'Total Revenue' => 'Total Revenue', + 'Total Shipping' => 'Total Shipping', + 'Total Tax' => 'Total Tax', + 'Total Weight' => 'Total Weight', + 'Total' => 'Total', + 'Track Inventory' => 'Track Inventory', + 'Transaction Hash' => 'Transaction Hash', + 'Transaction ID' => 'Transaction ID', + 'Transaction captured successfully: {message}' => 'Transaction captured successfully: {message}', + 'Transaction refunded successfully: {message}' => 'Transaction refunded successfully: {message}', + 'Transactions' => 'Transactions', + 'Transfer Fields' => 'Transfer Fields', + 'Transfer Items' => 'Transfer Items', + 'Transfer Settings' => 'Transfer Settings', + 'Transfer Status' => 'Transfer Status', + 'Transfer fields saved.' => 'Transfer fields saved.', + 'Transfer must have at least one item.' => 'Transfer must have at least one item.', + 'Transfer' => 'Transfer', + 'Transfers' => 'Transfers', + 'Trial days credited' => 'Trial days credited', + 'Trial expiration' => 'Trial expiration', + 'Trial expiry date' => 'Trial expiry date', + 'Type not in allowed options.' => 'Type not in allowed options.', + 'Type' => 'Type', + 'URI' => 'URI', + 'Unable to cancel subscription at this time.' => 'Unable to cancel subscription at this time.', + 'Unable to complete order: another request is already in progress.' => 'Unable to complete order: another request is already in progress.', + 'Unable to find variant.' => 'Unable to find variant.', + 'Unable to generate coupon codes: {message}' => 'Unable to generate coupon codes: {message}', + 'Unable to make payment at this time.' => 'Unable to make payment at this time.', + 'Unable to modify subscription at this time.' => 'Unable to modify subscription at this time.', + 'Unable to reactivate subscription at this time.' => 'Unable to reactivate subscription at this time.', + 'Unable to reassign orders.' => 'Unable to reassign orders.', + 'Unable to remove order data.' => 'Unable to remove order data.', + 'Unable to retrieve Sale and Purchasable.' => 'Unable to retrieve Sale and Purchasable.', + 'Unable to retrieve cart.' => 'Unable to retrieve cart.', + 'Unable to retrieve customer.' => 'Unable to retrieve customer.', + 'Unable to retrieve load cart URL' => 'Unable to retrieve load cart URL', + 'Unable to retrieve payment source.' => 'Unable to retrieve payment source.', + 'Unable to set default shipping category.' => 'Unable to set default shipping category.', + 'Unable to set default tax category.' => 'Unable to set default tax category.', + 'Unable to set primary payment source.' => 'Unable to set primary payment source.', + 'Unable to start the subscription. Please check your payment details.' => 'Unable to start the subscription. Please check your payment details.', + 'Unable to subscribe at this time.' => 'Unable to subscribe at this time.', + 'Unable to update cart.' => 'Unable to update cart.', + 'Unable to validate address.' => 'Unable to validate address.', + 'Unit Price' => 'Unit Price', + 'Unit price (minus discounts)' => 'Unit price (minus discounts)', + 'Units' => 'Units', + 'Unpaid' => 'Unpaid', + 'Unsubscribe' => 'Unsubscribe', + 'Update Address' => 'Update Address', + 'Update Order Status' => 'Update Order Status', + 'Update Order Status…' => 'Update Order Status…', + 'Update order' => 'Update order', + 'Update subscription' => 'Update subscription', + 'Update' => 'Update', + 'Updated By' => 'Updated By', + 'Updated committed stock successfully.' => 'Updated committed stock successfully.', + 'Updated' => 'Updated', + 'Use Billing Address For Tax' => 'Use Billing Address For Tax', + 'Use as the primary billing address' => 'Use as the primary billing address', + 'Use as the primary shipping address' => 'Use as the primary shipping address', + 'Used By Tax Rates' => 'Used By Tax Rates', + 'Used by Tax Rates' => 'Used by Tax Rates', + 'User Groups' => 'User Groups', + 'User not found.' => 'User not found.', + 'User' => 'User', + 'Uses' => 'Uses', + 'Validate Business Tax ID as Vat ID' => 'Validate Business Tax ID as Vat ID', + 'Validating condition syntax' => 'Validating condition syntax', + 'Validating formula syntax' => 'Validating formula syntax', + 'Variant Fields' => 'Variant Fields', + 'Variant Has Untracked Stock' => 'Variant Has Untracked Stock', + 'Variant Price' => 'Variant Price', + 'Variant SKU' => 'Variant SKU', + 'Variant Search' => 'Variant Search', + 'Variant Stock' => 'Variant Stock', + 'Variant Title Format' => 'Variant Title Format', + 'Variant Tracks Stock' => 'Variant Tracks Stock', + 'Variant UI Label Format' => 'Variant UI Label Format', + 'Variant has no product.' => 'Variant has no product.', + 'Variants not restored.' => 'Variants not restored.', + 'Variants restored.' => 'Variants restored.', + 'Variants' => 'Variants', + 'View customer' => 'View customer', + 'View order' => 'View order', + 'View product type - {productType}' => 'View product type - {productType}', + 'View user' => 'View user', + 'View' => 'View', + 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?', + 'Web' => 'Web', + 'Webhook URL' => 'Webhook URL', + 'Weight ({unit})' => 'Weight ({unit})', + 'Weight Rate' => 'Weight Rate', + 'Weight Unit' => 'Weight Unit', + 'Weight' => 'Weight', + 'What product URIs should look like for the site.' => 'What product URIs should look like for the site.', + 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.', + 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.', + 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.', + 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}', + 'What this PDF will be called in the control panel.' => 'What this PDF will be called in the control panel.', + 'What this catalog pricing rule will be called in the control panel.' => 'What this catalog pricing rule will be called in the control panel.', + 'What this discount will be called in the control panel.' => 'What this discount will be called in the control panel.', + 'What this email will be called in the control panel.' => 'What this email will be called in the control panel.', + 'What this product type will be called in the control panel.' => 'What this product type will be called in the control panel.', + 'What this sale will be called in the control panel.' => 'What this sale will be called in the control panel.', + 'What this shipping category will be called in the control panel.' => 'What this shipping category will be called in the control panel.', + 'What this shipping rule will be called in the control panel.' => 'What this shipping rule will be called in the control panel.', + 'What this shipping zone will be called in the control panel.' => 'What this shipping zone will be called in the control panel.', + 'What this status will be called in the control panel.' => 'What this status will be called in the control panel.', + 'What this subscription plan will be called in the control panel.' => 'What this subscription plan will be called in the control panel.', + 'What this tax category will be called in the control panel.' => 'What this tax category will be called in the control panel.', + 'What this tax zone will be called in the control panel.' => 'What this tax zone will be called in the control panel.', + 'When this discount is applied to an order, which line items should be discounted?' => 'When this discount is applied to an order, which line items should be discounted?', + 'Whether the first available shipping method option should be set automatically on carts.' => 'Whether the first available shipping method option should be set automatically on carts.', + 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Whether the user’s primary payment source should be set automatically on new carts.', + 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.', + 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Whether this catalog pricing rule should be available for use, regardless of other conditions.', + 'Whether this sale should be available for use, regardless of other conditions.' => 'Whether this sale should be available for use, regardless of other conditions.', + 'Which data to display in the name column in the results table.' => 'Which data to display in the name column in the results table.', + 'Which product types should this category be available to?' => 'Which product types should this category be available to?', + 'Which template should be loaded when a product’s URL is requested.' => 'Which template should be loaded when a product’s URL is requested.', + 'Width ({unit})' => 'Width ({unit})', + 'Width' => 'Width', + 'YYYY' => 'YYYY', + 'Yes' => 'Yes', + 'You are not allowed to add a line item.' => 'You are not allowed to add a line item.', + 'You currently have no emails configured to select for this status.' => 'You currently have no emails configured to select for this status.', + 'You do not have permission to load this cart.' => 'You do not have permission to load this cart.', + 'You must set up at least one gateway that supports subscriptions first.' => 'You must set up at least one gateway that supports subscriptions first.', + 'You must be logged in or provide a valid token to load this cart.' => 'You must be logged in or provide a valid token to load this cart.', + 'You must be signed in to create a payment source.' => 'You must be signed in to create a payment source.', + 'You must be signed in to set a primary payment source.' => 'You must be signed in to set a primary payment source.', + 'You must make a payment to complete the order.' => 'You must make a payment to complete the order.', + 'Your Cart Recovery Link' => 'Your Cart Recovery Link', + 'Your Order PDF Download Link' => 'Your Order PDF Download Link', + 'Your order is empty' => 'Your order is empty', + 'ZIP file' => 'ZIP file', + 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Zero - Minimum price is zero if discounts are greater than the order value.', + 'Zip Code' => 'Zip Code', + 'all' => 'all', + 'any' => 'any', + 'average order total' => 'average order total', + 'billing address' => 'billing address', + 'donation' => 'donation', + 'donations' => 'donations', + 'info' => 'info', + 'inventory location' => 'inventory location', + 'new customers' => 'new customers', + 'on hand' => 'on hand', + 'only' => 'only', + 'order' => 'order', + 'orders' => 'orders', + 'price' => 'price', + 'prices' => 'prices', + 'product variant' => 'product variant', + 'product variants' => 'product variants', + 'product' => 'product', + 'products' => 'products', + 'repeat customers' => 'repeat customers', + 'shipping address' => 'shipping address', + 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'shippingSameAsBilling and billingSameAsShipping can’t both be set.', + 'subscription' => 'subscription', + 'subscriptions' => 'subscriptions', + 'to' => 'to', + 'transfer' => 'transfer', + 'transfers' => 'transfers', + '{amount} included' => '{amount} included', + '{count} Unfulfilled Orders' => '{count} Unfulfilled Orders', + '{description} is no longer available.' => '{description} is no longer available.', + '{description} only has {stock} in stock.' => '{description} only has {stock} in stock.', + '{from} to {to}' => '{from} to {to}', + '{name} (Primary)' => '{name} (Primary)', + '{name} (Trashed)' => '{name} (Trashed)', + '{name} catalog price' => '{name} catalog price', + '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, =1{order} other{orders}} updated.', + '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.', + '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.', + '{number} more…' => '{number} more…', + '{pct} off the discounted item price' => '{pct} off the discounted item price', + '{pct} off the original item price' => '{pct} off the original item price', + '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.', + '{total} in total revenue' => '{total} in total revenue', + '{total} orders' => '{total} orders', + '{total} saleable across {locationCount} location(s)' => '{total} saleable across {locationCount} location(s)', + '{uses} uses across {emails} email addresses' => '{uses} uses across {emails} email addresses', + '{uses} uses across {users} users' => '{uses} uses across {users} users', + '“{description}” is currently out of stock.' => '“{description}” is currently out of stock.', + '“{key}” has invalid JSON' => '“{key}” has invalid JSON', +]; diff --git a/lang/fr-CA/commerce.php b/lang/fr-CA/commerce.php new file mode 100644 index 0000000000..28b0cda5e6 --- /dev/null +++ b/lang/fr-CA/commerce.php @@ -0,0 +1,1423 @@ + '(nouveau prix)', + '(of original price)' => '(du prix initial)', + '(off original price)' => '(en moins sur le prix initial)', + 'A cart number must be specified.' => 'Un numéro de panier doit être indiqué.', + 'A cart recovery link has been sent to {email}.' => 'Un lien de récupération de panier a été envoyé à {email}.', + 'A cart recovery link will be sent to {email}.' => 'Un lien de récupération de panier sera envoyé à {email}.', + 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Un numéro de référence simple sera généré sur la base de ce format lors de la finalisation d’un panier et de sa conversion en commande. Par exemple {ex1}, ou
{ex2}. Le résultat de ce format doit être unique.', + 'A new download link has been sent to {email}' => 'Un nouveau lien de téléchargement a été envoyé à {email}.', + 'A new download link will be sent to {email}' => 'Un nouveau lien de téléchargement sera envoyé à {email}.', + 'A valid email is required to create a customer.' => 'Un courriel valide est requis pour créer un client.', + 'Accept' => 'Accepter', + 'Accepted' => 'Accepté', + 'Actions' => 'Actions', + 'Active Carts' => 'Paniers actifs', + 'Active subscriptions' => 'Abonnements actifs', + 'Active' => 'Actif', + 'Add Address' => 'Ajouter une adresse', + 'Add a coupon' => 'Ajouter un coupon', + 'Add a custom line item' => 'Ajouter un article personnalisé', + 'Add a line item' => 'Ajouter un article', + 'Add a product' => 'Ajouter un produit', + 'Add a variant' => 'Ajouter une variation', + 'Add an adjustment' => 'Ajouter un ajustement', + 'Add an item' => 'Ajouter un article', + 'Add an option' => 'Ajouter une option', + 'Add catalog price' => 'Ajouter un prix catalogue', + 'Add' => 'Ajouter', + 'Additional Actions' => 'Actions supplémentaires', + 'Additional recipients that should receive this email. Twig code can be used here.' => 'Destinataires supplémentaires qui devraient recevoir ce courriel. Du code Twig peut être utilisé ici.', + 'Address 1' => 'Adresse 1', + 'Address 2' => 'Adresse 2', + 'Address 3' => 'Adresse 3', + 'Address Line 1' => 'Adresse ligne 1', + 'Address Line 2' => 'Adresse ligne 2', + 'Address Updated.' => 'Adresse mise à jour.', + 'Address copied to user.' => 'Adresse copiée pour l\'utilisateur.', + 'Address not found.' => 'Adresse non trouvée.', + 'Adjust Quantity' => 'Ajuster la quantité', + 'Adjust by' => 'Ajuster par', + 'Adjust price when included rate is disqualified?' => 'Ajuster le prix lorsque le taux de taxe inclus est disqualifié?', + 'Adjustments' => 'Ajustements', + 'Admin Notices' => 'Avis de l\'administrateur', + 'Administrative Area Code of Origin' => 'Code d\'origine Zone administrative ', + 'Advanced' => 'Avancé', + 'All Orders' => 'Toutes les commandes', + 'All Totals' => 'Tous les totaux', + 'All Transfers' => 'Tous les transferts', + 'All active subscriptions' => 'Tous les abonnements actifs', + 'All customers' => 'Tous les clients', + 'All products' => 'Tous les produits', + 'All variants must have a SKU.' => 'Toutes les variantes doivent avoir une UGS.', + 'All' => 'Tout', + 'Allow Checkout Without Payment' => 'Autoriser le passage à la caisse sans paiement', + 'Allow Empty Cart On Checkout' => 'Autoriser les paniers vides à la fin du processus de paiement', + 'Allow Partial Payment On Checkout' => 'Autoriser les paiements partiels à la sortie', + 'Allow out of stock purchases' => 'Permettre les achats de produits en rupture de stock', + 'Allow' => 'Autoriser', + 'Allowed Qty' => 'Quantité autorisée', + 'Alternative Phone' => 'Autre téléphone', + 'Amount' => 'Montant', + 'An ID must be provided' => 'Un identifiant doit être fourni', + 'An error occurred while generating this PDF.' => 'Une erreur est survenue lors la création de ce fichier PDF.', + 'Any' => 'Chaque', + 'Anywhere' => 'Partout', + 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Voulez-vous vraiment archiver le plan d’abonnement « {name} »? Cela N’ANNULERA PAS les abonnements existants.', + 'Are you sure you want to capture this transaction?' => 'Êtes-vous certain(e) de vouloir saisir cette transaction?', + 'Are you sure you want to complete this order?' => 'Êtes-vous sûr de vouloir terminer cette commande?', + 'Are you sure you want to delete the selected orders?' => 'Voulez-vous vraiment supprimer les commandes sélectionnées?', + 'Are you sure you want to delete the selected product and its variants?' => 'Voulez-vous vraiment supprimer le produit sélectionné et ses variantes?', + 'Are you sure you want to delete this shipping rule?' => 'Êtes-vous sûr de vouloir supprimer cette règle d\'expédition?', + 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Êtes-vous certain(e) de vouloir effacer “{name}” et tout ses produits? Veuillez-vous assurer que vous avez une copie de sauvegarde de votre base de données avant de compléter cette action.', + 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Voulez-vous vraiment supprimer « {name} »? Cela définira tous les articles avec ce statut comme n\'ayant aucun statut.', + 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Voulez-vous vraiment marquer ce transfert comme étant en attente? Il apparaîtra comme entrant à la destination.', + 'Are you sure you want to overwrite the billing address?' => 'Êtes-vous sûr de vouloir écraser l\'adresse de facturation?', + 'Are you sure you want to overwrite the shipping address?' => 'Êtes-vous sûr de vouloir écraser l\'adresse d\'expédition?', + 'Are you sure you want to permanently delete this store and everything in it?' => 'Voulez-vous vraiment supprimer ce magasin et tout ce qu\'il contient?', + 'Are you sure you want to refund this transaction?' => 'Êtes-vous certain(e) de vouloir rembourser cette transaction?', + 'Are you sure you want to remove this customer?' => 'Êtes-vous sûr de vouloir supprimer ce client?', + 'Are you sure you want to save this as a new shipping rule?' => 'Êtes-vous sûr de vouloir enregistrer cette règle comme nouvelle règle d\'expédition?', + 'Are you sure you want to send email: {name}?' => 'Êtes-vous sûr de vouloir envoyer le courriel : {name}?', + 'At least one site must be enabled for the product type.' => 'Au moins un site doit être activé pour le type de produit.', + 'Attempted Payments' => 'Tentatives de paiement', + 'Attention' => 'Attention', + 'Authorize Only (Manually Capture)' => 'Autoriser uniquement (collecter manuellement)', + 'Auto Set Cart Shipping Method Option' => 'Définir automatiquement l\'option de méthode d\'expédition du panier', + 'Auto Set New Cart Addresses' => 'Définir automatiquement les adresses des nouveaux paniers', + 'Auto Set Payment Source' => 'Source de paiement auto-définie', + 'Automatic SKU Format' => 'Format automatique d\'UGS', + 'Available Shipping Categories' => 'Catégories d’expédition disponibles', + 'Available Tax Categories' => 'Catégories de taxes disponibles', + 'Available for purchase' => 'Disponible à l’achat', + 'Available for purchase?' => 'Disponible à l’achat?', + 'Available inventory for "{description}" has gone below zero.' => 'Le stock disponible pour « {description} » est désormais inférieur à zéro.', + 'Available to Product Types' => 'Disponibles dans Types de produits', + 'Available' => 'Disponible', + 'Available?' => 'Disponible?', + 'Average Order Total' => 'Total de commande moyen', + 'Average' => 'Moyenne', + 'BCC’d Recipient' => 'Copie conforme invisible envoyée au destinataire', + 'Bad Request' => 'Requête incorrecte', + 'Bad address ID.' => 'Mauvais identifiant d\'adresse.', + 'Bad order ID.' => 'Identifiant de commande incorrect.', + 'Base Price' => 'Prix de base', + 'Base Promotional Price' => 'Prix promotionnel de base', + 'Base Rate' => 'Taux de base', + 'Base' => 'Base', + 'Bcc' => 'CCI', + 'Billing Address' => 'Adresse de facturation', + 'Billing Business Name' => 'Nom de l’entreprise de facturation', + 'Billing First Name' => 'Nom pour la facturation', + 'Billing Full Name' => 'Nom complet pour la facturation', + 'Billing Last Name' => 'Nom pour la facturation', + 'Billing address required.' => 'Adresse de facturation requise.', + 'Billing detail update URL' => 'URL de mise à jour des informations de facturation', + 'Billing issues' => 'Problèmes de facturation', + 'Billing' => 'Facturation', + 'Both (Line item price + Line item shipping costs)' => 'Les deux (prix de l\'article + frais de livraison de l\'article)', + 'Business ID' => 'Numéro d’entreprise', + 'Business Name' => 'Nom de l’entreprise', + 'Business Tax ID' => 'Numéro d’entreprise', + 'CC’d Recipient' => 'Destinataire en copie', + 'CVV' => 'CVV', + 'Can be used as an internal reference.' => 'Peut être utilisé comme référence interne.', + 'Can not complete payment for missing transaction.' => 'Impossible de terminer le paiement pour la transaction manquante.', + 'Can not create a new order' => 'Impossible de créer une nouvelle commande', + 'Can not find an order to pay.' => 'Impossible de trouver une commande à payer.', + 'Can not find enabled email.' => 'Impossible de trouver le courriel activé.', + 'Can not find order' => 'Impossible de trouver la commande', + 'Can not find order.' => 'Impossible de trouver la commande.', + 'Can not find the transaction to refund' => 'Impossible de trouver la transaction à rembourser', + 'Can not move between these inventory types.' => 'Il n\'est pas possible de passer d\'un type de stocks à l\'autre.', + 'Can not refund amount greater than the remaining amount' => 'Impossible de rembourser un montant supérieur au montant restant', + 'Cancel subscription' => 'Annuler l’abonnement', + 'Cancel with gateway now' => 'Annuler avec la passerelle maintenant', + 'Cancel' => 'Annuler', + 'Cancellation date' => 'Date d’annulation', + 'Cancellation' => 'Annulation', + 'Cannot switch plans for this subscription.' => 'Impossible de changer de plan pour cet abonnement.', + 'Can’t preview this email.' => 'Impossible de prévisualiser ce courriel.', + 'Capture payment' => 'Collecter le paiement', + 'Capture' => 'Saisir', + 'Card Holder' => 'Titulaire de la carte', + 'Card Number' => 'Numéro de carte', + 'Card' => 'Carte', + 'Cart Recovery Link' => 'Lien de récupération du panier', + 'Cart forgotten.' => 'Panier oublié.', + 'Cart updated.' => 'Panier mis à jour.', + 'Cart {number}' => 'Panier {number}', + 'Catalog Pricing Rule' => 'Règle de tarification du catalogue', + 'Catalog pricing rule description.' => 'Description de la règle de tarification du catalogue.', + 'Catalog pricing rule saved.' => 'Règle de tarification du catalogue sauvegardée.', + 'Catalog pricing rules deleted.' => 'Les règles de tarification du catalogue sont supprimées.', + 'Catalog pricing rules updated.' => 'Mise à jour des règles de tarification du catalogue.', + 'Categories Relationship Type' => 'Type de relation des catégories', + 'Categories' => 'Catégories', + 'Category Rate Overrides' => 'Remplacements du taux de catégorie', + 'Centimeters (cm)' => 'Centimètres (cm)', + 'Changing this value may affect your ability to refund existing transactions.' => 'La modification de cette valeur peut affecter votre capacité à rembourser les transactions existantes.', + 'Choose a color to represent the order’s status' => 'Choisissez une couleur pour représenter le statut de la commande', + 'Choose a new customer' => 'Choisir un nouveau client', + 'Choose adjustment values to include when calculating the product revenue total.' => 'Choisissez les valeurs d\'ajustement à inclure lors du calcul du total des revenus du produit.', + 'Choose the currency’s ISO code.' => 'Sélectionner le code ISO de la devise.', + 'Choose the destination inventory location for the existing on hand stock.' => 'Sélectionnez l\'emplacement de destination des stocks pour les stocks disponibles existants.', + 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Choisir les sites pour lesquels ce type de produit sera disponible et configurer les paramètres spécifiques aux sites.', + 'City' => 'Ville', + 'Clear counter' => 'Réinitialiser le compteur', + 'Clear notices' => 'Effacer les avis', + 'Close' => 'Fermer', + 'Code' => 'Code', + 'Collated PDF' => 'PDF unique', + 'Color' => 'Couleur', + 'Commerce Products' => 'Produits de Commerce', + 'Commerce Settings' => 'Paramètres commerciaux', + 'Commerce Variants' => 'Variantes de Commerce', + 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Le courriel Commerce « {email} » n’a pas été envoyé pour la commande « {order} ».', + 'Commerce order exports' => 'Exportations de commandes Commerce', + 'Commerce' => 'Commerce', + 'Committed' => 'Validé', + 'Completed Email' => 'Adresse courriel indiquée', + 'Completed' => 'Terminé', + 'Completing order failed.' => 'Échec de l\'exécution de la commande.', + 'Condition' => 'Condition', + 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Les conditions sont comparées à un ordre avant d\'examiner les règles. Cette fonction est utile si vous souhaitez vérifier la disponibilité d\'une méthode à un stade précoce ou s\'il existe des conditions communes à toutes les règles relatives à cette méthode.', + 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Les conditions sont comparées à la commande du client avant d\'examiner les règles. Cette fonction est utile si vous souhaitez vérifier la disponibilité d\'une méthode à un stade précoce ou s\'il existe des conditions communes à toutes les règles relatives à cette méthode.', + 'Conditions' => 'Conditions', + 'Contains Purchasables' => 'Contient des articles achetables', + 'Control Panel Settings' => 'Réglages du panneau de configuration', + 'Control panel' => 'Panneau de configuration', + 'Conversion Rate' => 'Taux de conversion', + 'Converted Price' => 'Prix converti', + 'Copied!' => 'Copié!', + 'Copy the URL' => 'Copier l\'URL', + 'Copy to {location}' => 'Copier vers {location}', + 'Copy' => 'Copier', + 'Costs' => 'Frais', + 'Could not archive gateway.' => 'Impossible d’archiver la passerelle.', + 'Could not cancel “{reference}”.' => 'Échec de l\'annulation de « {reference} ».', + 'Could not create the payment source.' => 'Impossible de créer la source de paiement.', + 'Could not delete shipping rule' => 'Impossible de supprimer la règle de livraison', + 'Could not delete shipping zone' => 'Impossible de supprimer la zone de livraison', + 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Impossible de supprimer {count, number} {count, plural,one{catégorie} other{catégories}} d\'expédition.', + 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Impossible de supprimer {count, number} {count, plural,one{mode} other{modes}} et règles d\'expédition.', + 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Impossible de supprimer {count, number} {count, plural,one{catégorie} other{catégories}} de taxes.', + 'Could not find the email or template.' => 'Courriel ou modèle introuvable.', + 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Impossible de marquer la commande {number} comme finalisée. L’enregistrement de la commande a échoué au cours de la finalisation avec des erreurs : {order}', + 'Could not reactivate “{reference}”.' => 'Échec de la réactivation de « {reference} ».', + 'Could not send email' => 'Impossible d\'envoyer le courriel', + 'Could not switch “{reference}” to “{plan}”.' => 'Impossible de passer « {reference} » à « {plan} ».', + 'Could not update orders address.' => 'Impossible de mettre à jour l\'adresse des commandes.', + 'Couldn’t archive Line Item Status.' => 'Impossible d\'archiver le statut de l\'article.', + 'Couldn’t archive Order Status.' => 'Impossible d\'archiver le statut de la commande.', + 'Couldn’t capture transaction.' => 'Impossible de collecter la transaction.', + 'Couldn’t capture transaction: {message}' => 'Impossible de collecter la transaction : {message}', + 'Couldn’t delete email.' => 'Impossible de supprimer le courriel.', + 'Couldn’t delete the payment source.' => 'Impossible de supprimer la source de paiement.', + 'Couldn’t get order.' => 'Impossible d\'obtenir la commande.', + 'Couldn’t recalculate order.' => 'Impossible de recalculer la commande.', + 'Couldn’t refund transaction.' => 'Impossible de rembourser la transaction.', + 'Couldn’t refund transaction: {message}' => 'Impossible de rembourser la transaction : {message}', + 'Couldn’t reorder Line Item Statuses.' => 'Impossible de réorganiser les statuts d\'article.', + 'Couldn’t reorder Order Statuses.' => 'Impossible de réorganiser les statuts des commandes.', + 'Couldn’t reorder PDFs.' => 'Impossible de réorganiser les PDF.', + 'Couldn’t reorder discounts.' => 'Impossible de réorganiser les remises.', + 'Couldn’t reorder gateways.' => 'Impossible de réorganiser les passerelles.', + 'Couldn’t reorder plans.' => 'Impossible de réorganiser les projets.', + 'Couldn’t reorder rules.' => 'Impossible de réorganiser les règles.', + 'Couldn’t reorder sale.' => 'Impossible de réorganiser la promotion.', + 'Couldn’t reorder sales.' => 'Impossible de réorganiser les promotions.', + 'Couldn’t reorder statuses.' => 'Impossible de réorganiser les statuts.', + 'Couldn’t reorder stores.' => 'Impossible de commander à nouveau dans les magasins.', + 'Couldn’t save PDF.' => 'Impossible d’enregistrer le PDF.', + 'Couldn’t save catalog pricing rule.' => 'Impossible d\'enregistrer la règle de tarification du catalogue.', + 'Couldn’t save currency.' => 'Impossible d’enregistrer la devise.', + 'Couldn’t save discount.' => 'Impossible d\'enregistrer le rabais.', + 'Couldn’t save email.' => 'Impossible d\'enregistrer le courriel.', + 'Couldn’t save gateway.' => 'Impossible d’enregistrer la passerelle.', + 'Couldn’t save inventory location.' => 'Impossible d\'enregistrer l\'emplacement des stocks.', + 'Couldn’t save line item status.' => 'Impossible d\'enregistrer le statut de l\'article.', + 'Couldn’t save order fields.' => 'Impossible d’enregistrer les champs de commande.', + 'Couldn’t save order status.' => 'Impossible d\'enregistrer le statut de commande.', + 'Couldn’t save order.' => 'Impossible d\'enregistrer la commande.', + 'Couldn’t save product type.' => 'Impossible d\'enregistrer le type de produit.', + 'Couldn’t save sale.' => 'Impossible d\'enregistrer la promotion.', + 'Couldn’t save settings.' => 'Impossible d’enregistrer les paramètres.', + 'Couldn’t save shipping category.' => 'Impossible d\'enregistrer cette catégorie de livraison.', + 'Couldn’t save shipping method.' => 'Impossible d\'enregistrer la méthode d\'expédition.', + 'Couldn’t save shipping rule.' => 'Impossible d’enregistrer la règle d\'expédition.', + 'Couldn’t save shipping zone.' => 'Impossible d\'enregistrer la zone de livraison.', + 'Couldn’t save store.' => 'Impossible d\'enregistrer la boutique.', + 'Couldn’t save subscription fields.' => 'Impossible d’enregistrer les champs d\'abonnement.', + 'Couldn’t save subscription plan.' => 'Impossible d’enregistrer le plan d’abonnement.', + 'Couldn’t save subscription.' => 'Impossible d’enregistrer l’abonnement.', + 'Couldn’t save tax category.' => 'Impossible d\'enregistrer la catégorie de taxe.', + 'Couldn’t save tax rate.' => 'Impossible d\'enregistrer le taux de taxe.', + 'Couldn’t save tax zone.' => 'Impossible d’enregistrer la zone de taxes.', + 'Couldn’t save transfer fields.' => 'Impossible d’enregistrer les champs de transfert.', + 'Couldn’t update catalog pricing rule statuses.' => 'Impossible de mettre à jour le statut des règles de tarification du catalogue.', + 'Couldn’t update status.' => 'Impossible de mettre à jour l\'état.', + 'Couldn’t updated sales status.' => 'Impossible de mettre à jour le statut des ventes.', + 'Country Code of Origin' => 'Code d\'origine Pays', + 'Country List' => 'Liste des pays', + 'Country not allowed.' => 'Pays non autorisé.', + 'Country' => 'Pays', + 'Coupon Code' => 'Code de coupon', + 'Coupon can not apply discount to this order due to address mismatch.' => 'Le coupon ne peut pas être utilisé pour appliquer une remise à cette commande, car l\'adresse ne correspond pas.', + 'Coupon can not apply discount to this order due to customer mismatch.' => 'Le coupon ne peut pas être utilisé pour appliquer une remise à cette commande, car le client ne correspond pas.', + 'Coupon can not apply discount to this order.' => 'Le coupon ne peut pas être utilisé pour appliquer une remise à cette commande.', + 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Le code promotionnel « {code} » est déjà utilisé par la réduction « {name} ».', + 'Coupon codes cannot be blank.' => 'Les codes de coupons ne peuvent pas être vides.', + 'Coupon codes must be unique.' => 'Les codes de coupons doivent être uniques.', + 'Coupon format is required and must contain at least one `#`.' => 'Le format du coupon est requis et doit contenir au moins un « # ».', + 'Coupon not valid.' => 'Coupon non valide.', + 'Coupon removed: {explanation}' => 'Coupon supprimé : {explanation}', + 'Coupons' => 'Coupons', + 'Craft Commerce - Administration' => 'Craft Commerce - Administration', + 'Craft Commerce - Inventory' => 'Craft Commerce - Stocks', + 'Craft Commerce - Orders' => 'Craft Commerce - Commandes', + 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Type de produit - {name}', + 'Craft Commerce - Subscriptions' => 'Craft Commerce - Abonnements', + 'Create a Discount' => 'Créer un rabais', + 'Create a Subscription Plan' => 'Créer un abonnement', + 'Create a new PDF' => 'Créer un nouveau PDF', + 'Create a new catalog pricing rule' => 'Créer une nouvelle règle de tarification pour le catalogue', + 'Create a new currency' => 'Créer une nouvelle devise', + 'Create a new email' => 'Créer un nouveau courriel', + 'Create a new gateway' => 'Créer une nouvelle passerelle', + 'Create a new line item status' => 'Créer un nouveau statut d\'article', + 'Create a new order status' => 'Créer un nouveau statut de commande', + 'Create a new product type' => 'Créer un nouveau type de produit', + 'Create a new sale' => 'Créer une nouvelle promotion', + 'Create a new shipping category' => 'Créer une nouvelle catégorie d’expédition', + 'Create a new shipping method' => 'Créer une nouvelle méthode d\'expédition', + 'Create a new shipping rule' => 'Créer une nouvelle règle de livraison', + 'Create a new tax category' => 'Créer une nouvelle catégorie de taxes', + 'Create a new tax rate' => 'Créer un nouveau taux de taxes', + 'Create a product type' => 'Créer un type de produit', + 'Create a shipping zone' => 'Créer une zone de livraison', + 'Create a tax zone' => 'Créer une zone de taxes', + 'Create catalog pricing rules' => 'Créer des règles de tarification pour les catalogues', + 'Create customer: “{email}”' => 'Créer le client : « {email} »', + 'Create discounts' => 'Créer des remises', + 'Create discount…' => 'Créer un rabais…', + 'Create rules that allow this discount to match the order.' => 'Créez des règles qui permettent à ce rabais de correspondre à la commande.', + 'Create rules that allow this discount to match the order’s billing address.' => 'Créez des règles qui permettent à ce rabais de correspondre à l\'adresse de facturation de la commande.', + 'Create rules that allow this discount to match the order’s customer.' => 'Créez des règles qui permettent à ce rabais de correspondre au client de la commande.', + 'Create rules that allow this discount to match the order’s shipping address.' => 'Créez des règles qui permettent à ce rabais de correspondre à l\'adresse d\'expédition de la commande.', + 'Create rules that allow this gateway to match the billing address.' => 'Créez des règles qui permettent à cette passerelle de faire correspondre l\'adresse de facturation.', + 'Create rules that allow this gateway to match the order.' => 'Créez des règles qui permettent à ce portail de correspondre à la commande.', + 'Create rules that allow this gateway to match the shipping address.' => 'Créez des règles qui permettent à cette passerelle de faire correspondre l\'adresse de livraison.', + 'Create sales' => 'Créer des promotions', + 'Create sale…' => 'Créer une vente…', + 'Created' => 'Créé', + 'Credit Card Payment Type' => 'Type de paiement par carte de crédit', + 'Currency Code' => 'Code de devise', + 'Currency saved.' => 'Devise enregistrée.', + 'Currency' => 'Devise', + 'Current' => 'Actuel', + 'Custom 1' => 'Personnalisé 1', + 'Custom 2' => 'Personnalisé 2', + 'Custom 3' => 'Personnalisé 3', + 'Custom 4' => 'Personnalisé 4', + 'Custom' => 'Personnalisé', + 'Customer Enabled?' => 'Activée pour les clients ?', + 'Customer ID is required.' => 'Un identifiant client est requis.', + 'Customer Note' => 'Note client', + 'Customer Notices' => 'Avis aux clients', + 'Customer data' => 'Données de clients', + 'Customer' => 'Client', + 'Damaged' => 'Endommagé', + 'Data shown might be outdated.' => 'Les données présentées peuvent être obsolètes.', + 'Date Authorized' => 'Date d\'autorisation', + 'Date Created' => 'Date de création', + 'Date First Paid' => 'Date du premier paiement', + 'Date Ordered' => 'Date de commande', + 'Date Paid' => 'Date de paiement', + 'Date Updated' => 'Date de mise à jour', + 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Date à partir de laquelle la règle de tarification du catalogue sera active. Laisser vide pour une date de début illimitée', + 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Date à partir de laquelle le rabais sera actif. Laisser vide pour une date de début illimitée', + 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Date à partir de laquelle la vente sera active. Laisser vide pour une date de début illimitée', + 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Date à laquelle la règle de tarification du catalogue sera terminée. Laisser vide pour une date de fin illimitée', + 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Date à laquelle le rabais sera terminé. Laisser vide pour une date de fin illimitée', + 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Date à laquelle la vente sera terminée. Laisser vide pour une date de fin illimitée', + 'Date' => 'Date', + 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Par défaut - Permet au prix d\'être négatif si les rabais dépassent la valeur de la commande.', + 'Default Category' => 'Catégorie par défaut', + 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Vue par défaut du panneau de contrôle de Commerce. Si l\'utilisateur n\'a pas la permission, il se rabattra sur un emplacement auquel il peut accéder.', + 'Default Order PDF' => 'PDF de commande par défaut', + 'Default Per Item Rate' => 'Taux par article par défaut', + 'Default Percentage Rate' => 'Pourcentage par défaut', + 'Default Status?' => 'État par défaut?', + 'Default View' => 'Vue par défaut', + 'Default Weight Rate' => 'Taux par poids par défaut', + 'Default Zone' => 'Zone par défaut ', + 'Default status?' => 'Statut par défaut?', + 'Default to this tax zone when no billing address is set' => 'Définir cette zone de taxes comme zone par défaut lorsqu’aucune adresse de facturation n’est spécifiée', + 'Default to this tax zone when no shipping address is set' => 'Zone de taxes par défaut si aucune adresse de livraison n’est définie', + 'Default variant updated.' => 'Mise à jour de la variante par défaut.', + 'Default' => 'Valeur par défaut', + 'Default?' => 'Par défaut?', + 'Delete catalog pricing rules' => 'Supprimer les règles de tarification du catalogue', + 'Delete discounts' => 'Supprimer les rabais', + 'Delete orders' => 'Supprimer les commandes', + 'Delete sales' => 'Supprimer les promotions', + 'Delete' => 'Supprimer', + 'Deleting the {location} location.' => 'Suppression de l\'emplacement {location}.', + 'Describe this rule.' => 'Décrivez cette règle.', + 'Describe this shipping zone.' => 'Décrivez cette zone d’expédition.', + 'Describe this tax zone.' => 'Décrivez cette zone de taxes.', + 'Description' => 'Description', + 'Destination Inventory Location' => 'Emplacement de destination des stocks', + 'Destination' => 'Destination', + 'Details' => 'Détails', + 'Dimension Unit' => 'Unités de dimensions', + 'Dimensions' => 'Dimensions', + 'Disabled' => 'Désactivé', + 'Disallow' => 'Ne pas autoriser', + 'Discount all line items' => 'Effectuer un rabais sur tous les articles', + 'Discount description.' => 'Description du rabais.', + 'Discount is not allowed for the order' => 'Aucun rabais n\'est autorisé pour cette commande', + 'Discount is out of date.' => 'Le rabais est échu.', + 'Discount saved.' => 'Rabais enregistré.', + 'Discount the matching items only' => 'Ne faire de rabais que sur les articles correspondants', + 'Discount use has reached its limit.' => 'L’utilisation du rabais a atteint sa limite.', + 'Discount' => 'Rabais', + 'Discounted Item Subtotal' => 'Sous-total de l\'article avec remise', + 'Discounted Items' => 'Articles en réduction', + 'Discounts deleted.' => 'Rabais supprimés.', + 'Discounts reordered.' => 'Rabais réorganisés.', + 'Discounts updated.' => 'Rabais mis à jour.', + 'Discounts' => 'Rabais', + 'Disqualify with valid business tax ID?' => 'Disqualifier avec un identifiant fiscal d\'entreprise valide?', + 'Do not apply subsequent matching sales beyond applying this sale.' => 'N’appliquez pas les ventes correspondantes ultérieures après avoir appliqué cette vente.', + 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Ne pas appliquer ce taux si l\'adresse de la commande comporte l\'un des identifiants de taxe professionnelle valides sélectionnés.', + 'Do not attach a PDF to this email' => 'Ne pas attacher de PDF à ce courriel', + 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Ne pas calculer de nouveau la commande (Numéro : {orderNumber}) si des erreurs sont présentes.', + 'Donation can not be zero.' => 'Un don ne peut être nul.', + 'Donation needs to be an amount.' => 'Le don doit être un montant.', + 'Donation settings saved.' => 'Paramètres de don enregistrés.', + 'Donation' => 'Don', + 'Donations' => 'Dons', + 'Done' => 'Terminé', + 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Ne pas appliquer de rabais supplémentaires à cette commande si ce rabais est appliqué', + 'Download PDF' => 'Télécharger le PDF', + 'Download PDF…' => 'Télécharger le PDF…', + 'Download Type' => 'Type de téléchargement', + 'Download' => 'Télécharger', + 'Draft' => 'Brouillon', + 'Dummy gateway payment failed.' => 'Le paiement par la passerelle factice a échoué.', + 'Duplicate options exist' => 'Il y a des options en double', + 'Duration' => 'Durée', + 'EU VAT ID' => 'N° TVA DE L\'UE', + 'Edit address' => 'Modifier l’adresse', + 'Edit adjustments' => 'Modifier les ajustements', + 'Edit catalog pricing rules' => 'Modifier les règles de tarification du catalogue', + 'Edit discounts' => 'Modifier les rabais', + 'Edit options' => 'Modifier les options', + 'Edit orders' => 'Modifier les commandes', + 'Edit sales' => 'Modifier les promotions', + 'Edit' => 'Modifier', + 'Effect' => 'Effet', + 'Either (Default) - The relationship field is on the purchasable or the category' => 'Soit (par défaut) - Le champ de relation est dans le champ achetable ou la catégorie', + 'Either way' => 'L\'un ou l\'autre', + 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Erreur de génération de PDF de courriel pour le courriel « {email} ». Commande : « {order} ». Erreur de modèle PDF : « {message} » {file} : {line}', + 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'Le modèle au format PDF du courriel n’existe pas à l’emplacement « {templatePath} » pour le courriel « {email} ». Commande : « {order} ».', + 'Email Subject' => 'Objet du courriel', + 'Email error. No email address found for order. Order: “{order}”' => 'Erreur de courriel. Aucune adresse courriel n’a été trouvée pour la commande. Commande : « {order} »', + 'Email is not enabled.' => 'Les courriels ne sont pas activés.', + 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Comme il n’existe pas de modèle de courriel en texte clair à l’emplacement « {templatePath} », ce qui a entraîné « {templateParsedPath} » pour le courriel « {email} ». Commande : « {order} ».', + 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de texte clair de courriel pour le courriel « {email} ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du chemin d\'accès au modèle de texte clair pour le courriel « {email} » dans « Chemin du modèle ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email required to make payments on a completed order.' => 'Courriel requis pour effectuer des paiements sur une commande finalisée.', + 'Email saved.' => 'Courriel enregistré.', + 'Email sent' => 'Courriel envoyé', + 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Le modèle de courriel n’existe pas à l’emplacement « {templatePath} », ce qui a entraîné « {templateParsedPath} » pour le courriel « {email} ». Commande : « {order} ».', + 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel personnalisé « {email} » dans « À : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel pour le courriel {email} dans « Cci : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel pour le courriel {email} dans « CC : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel pour le courriel {email} dans « Répondre à : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel pour le courriel « {email} » dans « Objet : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel pour le « {email} ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel pour le courriel « {email} » dans « Chemin du modèle ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email unavailable.' => 'Courriel non disponible.', + 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'Le courriel « {email} » n\'a pu être envoyé pour la commande « {order} ». Erreur : {error} {file} : {line}', + 'Email “{email}” for order {order} was cancelled.' => 'Le courriel « {email} » pour la commande « {order} » a été annulé.', + 'Email' => 'Courriel', + 'Emails' => 'Courriels', + 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Activer si ce taux doit être intégré au prix de l\'objet imposable au lieu d\'ajouter un coût à la commande.', + 'Enable structure for products of this type' => 'Activer la structure pour les produits de ce type', + 'Enable this discount' => 'Activer ce rabais', + 'Enable this rule' => 'Activer cette règle', + 'Enable this sale' => 'Activer cette vente', + 'Enable this shipping method on the front end' => 'Activer cette méthode d’expédition à l’accueil', + 'Enable this shipping rule' => 'Activer cette règle d’expédition', + 'Enable this tax rate' => 'Activer ce taux de taxe', + 'Enabled for customers to select during checkout?' => 'Possibilité pour les clients de sélectionner au moment de passer à la caisse?', + 'Enabled for customers to select?' => 'Activé pour permettre la sélection par les clients?', + 'Enabled' => 'Activé', + 'Enabled?' => 'Activé?', + 'End Date' => 'Date de fin', + 'Enter SKU' => 'Entrer l\'UGS', + 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Saisir un nom convivial pour ce taux d\'imposition, qui sera utilisé dans le panneau de configuration.', + 'Enter a percentage like {ex1} or {ex2}.' => 'Entrer un pourcentage comme {ex1} ou {ex2}.', + 'Enter coupon code' => 'Entrer le code du coupon', + 'Enter reference' => 'Entrer la référence', + 'Error refunding transaction: {transactionHash}' => 'Erreur lors du remboursement de la transaction : {transactionHash}', + 'Every new store must be assigned to at least one site.' => 'Chaque nouveau point de vente doit être affecté à au moins un site.', + 'Everywhere' => 'Partout', + 'Example' => 'Exemple', + 'Exclude this discount for products that are already on promotion' => 'Exclure cette remise pour les produits déjà en promotion', + 'Expired Link' => 'Lien expiré', + 'Expired' => 'Expiré', + 'Expiry Date' => 'Date d’expiration', + 'Expiry date' => 'Date d’expiration', + 'Expiry' => 'Expiration', + 'Failed to receive transfer: {error}' => 'Échec de la réception du transfert : {error}', + 'Failed to send email. Please try again.' => 'Échec de l\'envoi du courriel. Veuillez réessayer.', + 'Failed to start' => 'Échec du démarrage', + 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Échec de la mise à jour {num, plural, =1{du statut de la commande} other{des statuts des commandes}}.', + 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Échec de la mise à jour du statut {num, plural, =1{de la commande} other{des commandes}}.', + 'Feet (ft)' => 'Pieds (pi)', + 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Les conditions de filtrage qui définissent à quelles commandes cette règle s’applique. Écrire 0 pour ignorer cette condition.', + 'First Name' => 'Prénom', + 'Flat Amount Off Order' => 'Montant fixe de remise sur la commande', + 'Flat Order Discount Amount Off' => 'Montant du rabais forfaitaire sur la commande', + 'Free Order Payment Strategy' => 'Stratégie de paiement des commandes gratuites', + 'Free Shipping' => 'Expédition gratuite', + 'Free orders are processed by the payment gateway' => 'Les commandes gratuites sont traitées à travers la passerelle de paiement', + 'Free orders complete immediately' => 'Les commandes gratuites s\'exécutent immédiatement', + 'Free shipping can only be for whole order or matching items, not both.' => 'La livraison gratuite ne peut concerner que l\'ensemble de la commande ou les articles correspondants, pas les deux.', + 'From Name' => 'Nom de l’expéditeur', + 'Fulfill' => 'Réaliser', + 'Fulfilled' => 'Réalisé', + 'Fulfillment' => 'Réalisation', + 'Full Name' => 'Nom complet', + 'Gateway Code' => 'Code de la passerelle', + 'Gateway Message' => 'Message de passerelle', + 'Gateway Reference' => 'Référence de la passerelle', + 'Gateway Response' => 'Réponse de la passerelle', + 'Gateway doesn’t support authorize' => 'La passerelle ne prend pas en charge l’autorisation', + 'Gateway doesn’t support partial refunds.' => 'La passerelle ne prend pas en charge les remboursements partiels.', + 'Gateway doesn’t support purchase' => 'La passerelle de paiement ne supporte pas les achats', + 'Gateway doesn’t support refunds.' => 'La passerelle ne prend pas en charge les remboursements.', + 'Gateway saved.' => 'Passerelle enregistrée.', + 'Gateway' => 'Passerelle', + 'Gateways reordered.' => 'Passerelles réorganisées.', + 'Gateways' => 'Passerelles', + 'General Settings' => 'Paramètres généraux', + 'General' => 'Général', + 'Generate' => 'Générer', + 'Generated Coupon Format' => 'Format des coupons générés', + 'Grams (g)' => 'Grammes (g)', + 'Groups for which this sale will be applicable to.' => 'Groupes auxquels cette promotion s’appliquera.', + 'HTML Email Template Path' => 'Chemin du modèle de courriel HTML', + 'Handle' => 'Identifiant', + 'Harmonized System Code' => 'Code du système harmonisé', + 'Has Admin Notices' => 'Contient des avis de l\'administrateur', + 'Has Emails?' => 'A des courriels?', + 'Has Free Shipping' => 'Possède la livraison gratuite', + 'Has Orders' => 'Comprend des commandes', + 'Has Purchasable' => 'Comprend des achetables', + 'Has Variants?' => 'Possède des variantes?', + 'Height ({unit})' => 'Hauteur ({unit})', + 'Height' => 'Hauteur', + 'Hide snapshot' => 'Masquer l\'instantané', + 'History' => 'Historique', + 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Combien de temps (en secondes) un lien de téléchargement PDF doit rester valide avant d\'expirer. La valeur par défaut est 86 400 (24 heures).', + 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Combien de fois une adresse courriel peut utiliser ce rabais. Ceci est applicable à toutes les commandes précédentes, par des invités ou des utilisateurs. Indiquez zéro pour une utilisation illimitée par invités ou utilisateurs.', + 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Combien de fois un utilisateur est autorisé à utiliser cette remise. Si cette valeur est différente de zéro, la remise ne sera disponible que pour les utilisateurs connectés.', + 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Le nombre de fois maximum que cette réduction peut être utilisée par des invités ou des utilisateurs inscrits. Mettez zéro pour une utilisation illimitée.', + 'How products should be labeled within the control panel.' => 'Comment les produits doivent être étiquetés dans le panneau de contrôle.', + 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'La relation entre les biens à acheter et les catégories, qui détermine les articles correspondants. Voir [Terminologie des relations]({link}).', + 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'Comment ce produit sera-t-il décrit dans une ligne d’article dans une commande. Vous pouvez inclure des étiquettes qui indiquent les propriétés, telles que {ex1} ou {ex2}', + 'How this shipping method will be referred to in templates and forms.' => 'Comment s’appellera cette méthode dans les modèles et les formulaires.', + 'How variants should be labeled within the control panel.' => 'Comment les variantes doivent être étiquetées dans le panneau de contrôle.', + 'How you’ll refer to this PDF in the templates.' => 'La façon dont vous allez faire référence à ce PDF dans les modèles.', + 'How you’ll refer to this product type in the templates.' => 'Comment s’appellera ce type de produit dans les modèles.', + 'How you’ll refer to this shipping category in the templates.' => 'Comment vous faites référence à cette catégorie d’expédition dans les modèles.', + 'How you’ll refer to this status in the templates.' => 'Comment s’appellera cet état dans les modèles', + 'How you’ll refer to this subscription plan in the templates.' => 'La façon dont vous désignerez ce plan d’abonnement dans les modèles.', + 'How you’ll refer to this tax category in the templates.' => 'Comment s’appellera cette catégorie de taxes dans les modèles.', + 'ID' => 'ID', + 'IP Address' => 'Adresse IP', + 'If disabled, this PDF will not be available or sent with emails.' => 'S\'il est désactivé, ce PDF ne sera pas disponible ou envoyé avec les courriels.', + 'If disabled, this email will not send.' => 'S’il est désactivé, ce courriel ne sera pas envoyé.', + 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Si cette option est activée et que ce tarif ne correspond pas à la commande, le taux sera supprimé du prix de l\'article dans le panier.', + 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Si cette valeur est réglée à « Autorisation seulement », il faut manuellement saisir les paiements avant que les fonds ne soient transférés à votre compte. La passerelle a besoin d’accepter l’option sélectionnée.', + 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Si vous choisissez le pourcentage pour qu\'il corresponde au « rabais sur l\'article en promotion », cela inclura le « montant par article » ainsi que tout autre rabais appliqué avant celui-ci.', + 'Ignore Promotions?' => 'Ignorer les promotions?', + 'Ignore previous matching sales if this sale matches.' => 'Ignorez les ventes correspondantes précédentes si cette vente correspond.', + 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignorer les prix promotionnels lorsque cette remise est appliquée à des postes correspondants.', + 'Inactive Carts' => 'Paniers inactifs', + 'Inches (in)' => 'Pouces (po)', + 'Include built-in line item tax.' => 'Inclure la taxe intégrée sur le type d\'article.', + 'Include in price?' => 'Inclure dans le prix?', + 'Include line item discounts.' => 'Inclure les rabais par type d\'article.', + 'Include line item shipping costs.' => 'Inclure les coûts d\'envoi par type d\'article.', + 'Include separate line item tax.' => 'Inclure une ligne distincte pour la taxe par type d\'article.', + 'Included in price?' => 'Inclus dans le prix?', + 'Included' => 'Inclus', + 'Incoming transfer from Transfer ID: ' => 'Transfert entrant à partir de l\'ID de transfert : ', + 'Incoming' => 'À venir', + 'Info' => 'Infos', + 'Information linked?' => 'Des renseignements sont-ils associés?', + 'Information' => 'Informations', + 'Invalid JSON' => 'JSON invalide', + 'Invalid Order ID' => 'ID de commande invalide', + 'Invalid VAT ID.' => 'ID TVA invalide.', + 'Invalid condition syntax' => 'Syntaxe conditionnelle invalide', + 'Invalid email.' => 'L\'adresse courriel est invalide.', + 'Invalid formula syntax' => 'Syntaxe de formule non valide', + 'Invalid gateway: {value}' => 'Passerelle non valide : {value}', + 'Invalid inventory movements.' => 'Mouvements des stocks non valides.', + 'Invalid order condition syntax.' => 'Syntaxe conditionnelle de commande invalide.', + 'Invalid payment or order. Please review.' => 'Paiement ou commande non valide. Veuillez vérifier votre saisie.', + 'Invalid payment source ID: {value}' => 'Identifiant de la source de paiement non valide : {value}', + 'Invalid store.' => 'Magasin non valide.', + 'Invalid user.' => 'Utilisateur non valide.', + 'Inventory Item' => 'Article d\'inventaire', + 'Inventory Location' => 'Emplacement des stocks', + 'Inventory Locations' => 'Emplacements des stocks', + 'Inventory Tracked' => 'Stocks tracés', + 'Inventory Transfers' => 'Transferts d\'inventaire', + 'Inventory could not be set.' => 'Les stocks n\'ont pas pu être définis.', + 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'L\'emplacement des stocks a validé des stocks, la ou les commandes doivent d\'abord être réalisées.', + 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Si l\'emplacement des stocks a des stocks entrants, le(s) transfert(s) doit(vent) d\'abord être effectué(s).', + 'Inventory location is already deactivated.' => 'L\'emplacement des stocks est déjà désactivé.', + 'Inventory location saved.' => 'Emplacement des stocks enregistré.', + 'Inventory locations not saved.' => 'Emplacements des stocks non enregistrés.', + 'Inventory movement could not be saved.' => 'Le mouvement de stocks n\'a pas pu être enregistré.', + 'Inventory movement saved.' => 'Mouvement de stocks enregistré.', + 'Inventory updated.' => 'Stocks mis à jour.', + 'Inventory was not updated.' => 'Les stocks n\'ont pas été mis à jour.', + 'Inventory' => 'Stocks', + 'Invoice amount' => 'Montant de la facture', + 'Invoice date' => 'Date de facturation', + 'Is Promotable' => 'Peut être promu', + 'Is Promotional Price?' => 'Est-ce un prix promotionnel?', + 'Is Shippable' => 'Peut être expédié', + 'Is Taxable' => 'Peut être taxé', + 'Item Rates' => 'Taux de l’item', + 'Item Subtotal' => 'Sous-total de l\'article', + 'Item Total' => 'Total de l\'article', + 'Item' => 'Article', + 'Items' => 'Articles', + 'Kilograms (kg)' => 'Kilogrammes (kg)', + 'Label' => 'Étiquette', + 'Landscape' => 'Landscape', + 'Language' => 'Langue', + 'Last Name' => 'Nom de famille', + 'Last Updated' => 'Dernière mise à jour', + 'Leave a category rate override blank to use the rate from above.' => 'Laissez un taux de catégorie vide pour utiliser le taux ci-dessus.', + 'Leave blank for unlimited uses.' => 'Laisser vide pour une utilisation illimitée.', + 'Leave blank if products don’t have URLs' => 'Laisser vide si les produits n’ont pas d’URL', + 'Leave gateway subscription as-is' => 'Conserver l\'abonnement à la passerelle tel quel', + 'Length ({unit})' => 'Longueur ({unit})', + 'Length' => 'Longueur', + 'Let each product choose which sites it should be saved to' => 'Laisser chaque produit choisir les sites sur lesquels il doit être enregistré', + 'Limit which orders this discount applies to based on its line items.' => 'Limiter les commandes auxquelles ce rabais s\'applique en fonction de leurs articles.', + 'Limit which purchasables this sale applies to.' => 'Limiter les produits achetables auxquels cette promotion s\'applique.', + 'Limit' => 'Limite', + 'Line Item Statuses' => 'Statuts de l\'article', + 'Line Item' => 'Article', + 'Line Items' => 'Articles', + 'Line item price (minus discounts)' => 'Prix de l\'article (moins les remises)', + 'Line item shipping cost' => 'Frais de livraison de l’article', + 'Line item statuses reordered.' => 'Articles réorganisés.', + 'Link Duration' => 'Durée du lien', + 'Link Sent' => 'Lien envoyé', + 'Link to a product' => 'Lien vers un produit', + 'Link to a variant' => 'Lier à une variante', + 'Link' => 'Lien', + 'Live' => 'En direct', + 'Location' => 'Emplacement', + 'Locations that should be available for previewing products in this product type.' => 'Emplacements à proposer pour la prévisualisation des produits de ce type de produit.', + 'MM' => 'MM', + 'Make a payment' => 'Effectuer un paiement', + 'Make this the primary store' => 'En faire le magasin principal', + 'Manage Inventory' => 'Gérer les stocks', + 'Manage donation settings' => 'Gérer les paramètres de don', + 'Manage general store settings' => 'Gérer les paramètres généraux du magasin', + 'Manage inventory locations' => 'Gérer les emplacements des stocks', + 'Manage inventory stock levels' => 'Gérer les niveaux de stocks', + 'Manage inventory transfers' => 'Gérer les transferts d\'inventaire', + 'Manage orders' => 'Gérer les commandes', + 'Manage payment currencies' => 'Gérer les devises de paiement', + 'Manage promotions' => 'Gérer des promotions', + 'Manage shipping' => 'Gérer l\'expédition', + 'Manage store settings' => 'Gérer les paramètres du magasin', + 'Manage subscription plans' => 'Gérer les abonnements', + 'Manage subscription' => 'Gérer l’abonnement', + 'Manage subscriptions' => 'Gérer les abonnements', + 'Manage taxes' => 'Gérer les taxes', + 'Manage' => 'Gérer', + 'Mark as Pending' => 'Marquer comme en attente', + 'Mark as completed' => 'Marquer comme terminé', + 'Match Billing Address' => 'Faire correspondre l\'adresse de facturation', + 'Match Customer' => 'Faire correspondre le client', + 'Match Order' => 'Faire correspondre la commande', + 'Match Orders' => 'Faire correspondre les commandes', + 'Match Product' => 'Faire correspondre le produit', + 'Match Purchasable' => 'Faire correspondre achetables', + 'Match Shipping Address' => 'Faire correspondre l\'adresse de livraison', + 'Match Variant' => 'Faire correspondre la variante', + 'Matching Items' => 'Articles correspondants', + 'Max Qty' => 'Qté max.', + 'Max Uses' => 'Nombre max d\'utilisations', + 'Max Variants' => 'Variantes max.', + 'Max quantity must greater than min.' => 'La quantité maximale doit être supérieure à la quantité minimale.', + 'Maximum Purchase Quantity' => 'Quantité d’achat maximum', + 'Maximum Total Shipping Cost' => 'Frais d’expédition totaux maximum', + 'Maximum allowed quantity' => 'Quantité maximale autorisée', + 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Le nombre maximum d’éléments correspondants qui peuvent être commandés pour que cette réduction s’applique. Une valeur zéro ici évitera cette condition.', + 'Maximum order quantity for this item is {num}.' => 'La quantité maximale pouvant être commandée pour cet article est {num}.', + 'Message' => 'Message', + 'Meters (m)' => 'Mètres (m)', + 'Millimeters (mm)' => 'Millimètres (mm)', + 'Min Qty' => 'Qté min.', + 'Min quantity must be less than max.' => 'La quantité minimale doit être inférieure à la quantité maximale.', + 'Minimum Purchase Quantity' => 'Quantité d’achat minimale', + 'Minimum Total Price Strategy' => 'Stratégie de prix minimum', + 'Minimum Total Shipping Cost' => 'Coût d’expédition minimum total', + 'Minimum allowed quantity' => 'Quantité minimale autorisée', + 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Nombre minimum d’articles correspondants qui doivent être commandés pour que cette réduction s’applique.', + 'Minimum order quantity for this item is {num}.' => 'La quantité minimale devant être commandée pour cet article est {num}.', + 'Missing Gateway' => 'Passerelle manquante', + 'Missing a default inventory location.' => 'Il manque un emplacement de stocks par défaut.', + 'Move Inventory' => 'Déplacer les stocks', + 'Move To' => 'Déplacer vers', + 'Move {qty} from {fromType} to {toType}' => 'Déplacer {qty} de {fromType} vers {toType}', + 'Move' => 'Déplacer', + 'Movement from deactivated inventory location' => 'Mouvement à partir d\'un lieu de stocks désactivé', + 'Movement' => 'Mouvement', + 'Must have at least one variant.' => 'Doit avoir au moins une variante.', + 'Name Field' => 'Champ de nom', + 'Name' => 'Nom', + 'New Customer' => 'Nouveau client', + 'New Customers' => 'Nouveaux clients', + 'New Order' => 'Nouvelle commande', + 'New PDF' => 'Nouveau PDF', + 'New address' => 'Nouvelle adresse', + 'New catalog pricing rule' => 'Nouvelle règle de tarification des catalogues', + 'New currency' => 'Nouvelle devise', + 'New discount' => 'Nouveau rabais', + 'New email' => 'Nouveau courriel', + 'New gateway' => 'Nouvelle passerelle', + 'New line item status' => 'Nouveau statut d\'article', + 'New line items get this status by default when the order is completed' => 'Les nouveaux articles obtiennent ce statut par défaut lorsque la commande est terminée', + 'New location' => 'Nouvel emplacement', + 'New order status' => 'Nouvel état de commande', + 'New orders get this status by default' => 'Les nouvelles commandes reçoivent cet état par défaut', + 'New product type' => 'Nouveau type de produit', + 'New product' => 'Nouveau produit', + 'New product, choose a type' => 'Nouveau produit, choisir un type', + 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Les nouveaux produits passent par défaut à la première catégorie de taxe disponible. Si aucune n\'est disponible, cette catégorie sera utilisée.', + 'New sale' => 'Nouvelle vente', + 'New shipping category' => 'Nouvelle catégorie d’expédition', + 'New shipping method' => 'Nouvelle méthode d’expédition', + 'New shipping rule' => 'Nouvelle règle d’expédition', + 'New shipping zone' => 'Nouvelle zone d’expédition', + 'New subscription plan' => 'Nouveau plan d’abonnement', + 'New tax category' => 'Nouvelle catégorie de taxes', + 'New tax rate' => 'Nouveau taux de taxes', + 'New tax zone' => 'Nouvelle zone de taxes', + 'New transfer' => 'Nouveau transfert', + 'New {productType} product' => 'Nouveau produit {productType}', + 'New' => 'Nouveau', + 'Next payment' => 'Prochain paiement', + 'No Address' => 'Pas d\adresse', + 'No PDFs exist yet.' => 'Il n\'existe pour l\'instant aucun PDF.', + 'No access given to any specific store management features.' => 'Aucun accès n\'est donné à des fonctions spécifiques de gestion de magasin.', + 'No additional payment currencies exist yet.' => 'Il n’existe plus aucune monnaie de paiement supplémentaire.', + 'No address' => 'Aucune adresse', + 'No billing address' => 'Aucune adresse de facturation', + 'No catalog pricing rule exists with the ID “{id}”' => 'Aucune règle de tarification du catalogue n\'existe avec l\'ID « {id} »', + 'No catalog pricing rules exist yet.' => 'Il n\'existe pas encore de règles de tarification pour les catalogues.', + 'No currency exists with the ID “{id}”' => 'Il n\'existe aucune devise avec l’identifiant « {id} »', + 'No customer email address exists on this cart.' => 'Aucune adresse courriel de client n’existe dans ce panier.', + 'No description' => 'Aucune description', + 'No discount exists with the ID “{id}”' => 'Il n’existe aucun rabais avec l’identifiant « {id} »', + 'No discounts exist yet.' => 'Aucun rabais n’a été créé.', + 'No donation amount supplied.' => 'Aucun montant de don fourni.', + 'No emails exist yet.' => 'Aucun courriel n’a été créé.', + 'No inventory changes made.' => 'Aucune modification de stocks n\'a été effectuée.', + 'No inventory found.' => 'Aucun stock n\'a été trouvé.', + 'No inventory movements made.' => 'Aucun mouvement de stocks n\'a été effectué.', + 'No inventory transactions for this location.' => 'Aucune transaction de stocks pour ce site.', + 'No new customer selected.' => 'Aucun nouveau client sélectionné.', + 'No order history exists with the ID “{id}”' => 'Il n’existe aucun historique de commande avec l’identifiant « {id} »', + 'No order status history items will exist until the cart becomes an order.' => 'Aucun élément n’existe dans l’historique des statuts de la commande jusqu’à ce que le panier soit converti en commande.', + 'No payment source exists with the ID “{id}”' => 'Il n\'existe aucune source de paiement avec l’identifiant « {id} »', + 'No private Note.' => 'Aucune note privée.', + 'No product available.' => 'Aucun produit disponible.', + 'No product types exist yet.' => 'Aucun type de produit n’a été créé.', + 'No purchasable available.' => 'Aucun achetable disponible.', + 'No sale exists with the ID “{id}”' => 'Il n’existe aucune promotion avec l’identifiant « {id} »', + 'No sales exist yet.' => 'Aucune vente n’a été créée.', + 'No shipping address' => 'Aucune adresse de livraison', + 'No shipping category exists with the ID “{id}”' => 'Il n\'existe aucune catégorie de livraison possédant l\'identifiant « {id} »', + 'No shipping method exists with the ID “{id}”' => 'Il n\'existe aucune méthode d\'expédition avec l\'identifiant « {id} »', + 'No shipping rule exists with the ID “{id}”' => 'Il n\'existe pas de règle de livraison avec l\'identifiant « {id} »', + 'No shipping rules exist yet.' => 'Aucune règle d’expédition n’a été créée.', + 'No shipping zone exists with the ID “{id}”' => 'Aucune zone de livraison avec l\'identifiant « {id} »', + 'No stats available.' => 'Aucune statistique disponible.', + 'No subscription plan exists with the ID “{id}”' => 'Il n\'existe aucun d’abonnement avec l’identifiant « {id} »', + 'No subscription plans exist yet.' => 'Aucun plan d’abonnement n’existe encore.', + 'No tax category exists with the ID “{id}”' => 'Il n’existe aucune catégorie de taxes avec l’identifiant « {id} »', + 'No tax rate exists with the ID “{id}”' => 'Il n\'existe aucun taux de taxe avec l\'identifiant « {id} »', + 'No tax zone exists with the ID “{id}”' => 'Il n\'existe aucune zone de taxe avec l\'identifiant « {id} »', + 'No transactions exist.' => 'Aucune transaction existante.', + 'No user authenticated.' => 'Aucun utilisateur authentifié.', + 'No' => 'Non', + 'None on hand' => 'Aucun à disposition', + 'None' => 'Aucun', + 'Not a valid address type' => 'N\'est pas un type d\'adresse valide', + 'Not a valid credit card number.' => 'Numéro de carte non valide.', + 'Not all SKUs are unique.' => 'Toutes les UGS ne sont pas uniques.', + 'Note' => 'Remarque', + 'Notes' => 'Notes', + 'Number of Coupons' => 'Nombre de coupons', + 'Number' => 'Numéro', + 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Parmi les sites activés ci-dessus, sur quels sites les produits de ce type de produit doivent-ils être enregistrés?', + 'On Hand' => 'Sur place', + 'Only allow this gateway to be used for zero value orders?' => 'Permettre uniquement l’utilisation de cette passerelle pour les commandes dont la valeur est zéro?', + 'Only match certain purchasables…' => 'Ne faire correspondre qu\'à certains produits achetables…', + 'Only match purchasables related to…' => 'Ne faire correspondre que les produits achetables liés à…', + 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Seules les commandes ayant les états de commande suivants seront incluses. Laissez vide pour inclure tous les états.', + 'Only save product to the site they were created in' => 'N’enregistrer les produits que sur le site où ils ont été créés', + 'Options' => 'Options', + 'Order Condition Formula' => 'Formule de la condition de commande', + 'Order Description Format' => 'Format de la description de la commande', + 'Order Details' => 'Détails de la commande', + 'Order Fields' => 'Champs de la commande', + 'Order PDF Download Link' => 'Lien de téléchargement du PDF de la commande', + 'Order PDF Filename Format' => 'Format du nom de fichier du PDF de commande', + 'Order Reference Number Format' => 'Format du numéro de référence de la commande', + 'Order Settings' => 'Paramètres de la commande', + 'Order Site' => 'Site de commande', + 'Order Status description.' => 'Description du statut de la commande.', + 'Order Status' => 'État de la commande', + 'Order Statuses' => 'États des commandes', + 'Order can not be empty.' => 'La commande ne peut pas être vide.', + 'Order count' => 'Nombre de commandes', + 'Order customer data removed.' => 'Données des clients supprimées des commandes.', + 'Order deleted.' => 'Commande supprimée.', + 'Order fields saved.' => 'Champs de commande enregistrés.', + 'Order not found.' => 'Commande non trouvée.', + 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Le solde de paiement de la commande est {outstandingBalanceAsCurrency}. Il s\'agit du montant maximum facturé.', + 'Order recalculated.' => 'Commande recalculée.', + 'Order status saved.' => 'Statut de commande enregistré.', + 'Order statuses reordered.' => 'Statuts des commandes réorganisés.', + 'Order total shipping cost' => 'Frais de livraison totaux de la commande', + 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Total taxable de la commande (sous-total des articles + total des rabais + total des frais de livraison)', + 'Order' => 'Commande', + 'Orders (Legacy)' => 'Commandes (Legacy)', + 'Orders deleted.' => 'Commandes supprimées.', + 'Orders not restored.' => 'Commandes non restaurées.', + 'Orders restored.' => 'Commandes restaurées.', + 'Orders' => 'Commandes', + 'Organization Name' => 'Nom de l\'organisation', + 'Organization Tax ID' => 'ID fiscal de l\'organisation', + 'Origin and destination cannot be the same.' => 'L\'origine et la destination ne peuvent pas être les mêmes.', + 'Origin' => 'Origine', + 'Original Price' => 'Prix d’origine', + 'Original price' => 'Prix original', + 'Original promotional price' => 'Prix promotionnel original', + 'Other Languages' => 'Autres langues', + 'Other countries' => 'Autres pays', + 'Outgoing transfer from Transfer ID: ' => 'Transfert sortant à partir de l\'ID de transfert : ', + 'Overpaid' => 'Surpayé', + 'Overrides previous?' => 'Remplace le précédent?', + 'PDF Attachment' => 'Fichier PDF joint', + 'PDF Template Path' => 'Chemin du modèle au format PDF', + 'PDF saved.' => 'PDF enregistré.', + 'PDF' => 'PDF', + 'PDFs & Emails' => 'PDF et courriels', + 'PDFs' => 'Fichiers PDF', + 'Paid Amount' => 'Montant payé', + 'Paid Status' => 'État du paiement', + 'Paid' => 'Payé', + 'Paper Orientation' => 'Orientation du papier', + 'Paper Size' => 'Format du papier', + 'Partial payment not allowed.' => 'Paiement partiel non autorisé.', + 'Partial' => 'Partiel', + 'Past year' => 'L\'année dernière', + 'Past {num} days' => '{num} derniers jours', + 'Pay {amount} of {currency} on the order.' => 'Payer {amount} {currency} sur la commande.', + 'Pay' => 'Payer', + 'Payment Amount' => 'Montant du paiement', + 'Payment Currencies' => 'Devises de paiement', + 'Payment Gateway' => 'Portail de paiement', + 'Payment Method' => 'Méthode de paiement', + 'Payment error: {message}' => 'Erreur de paiement : {message}', + 'Payment method issue' => 'Problème de moyen de paiement', + 'Payment source created.' => 'Source de paiement créée.', + 'Payment source deleted.' => 'Source de paiement supprimée.', + 'Payments' => 'Paiements', + 'Pending' => 'En cours', + 'Per Email Address Discount Limit' => 'Limite de rabais par adresse courriel', + 'Per Item Amount Off' => 'Montant de la remise par article', + 'Per Item Discount' => 'Rabais par article', + 'Per Item Percentage Off' => 'Montant de remise par article', + 'Per Item Rate' => 'Taux par article', + 'Per User Discount Limit' => 'Limite de rabais par utilisateur', + 'Percentage Rate' => 'Taux en pourcentage', + 'Phone (Alt)' => 'Téléphone (autre)', + 'Phone' => 'Numéro de téléphone', + 'Pick a plan' => 'Choisir un plan', + 'Plain Text Email Template Path' => 'Chemin du modèle de courriel de texte brut', + 'Plan' => 'Projet', + 'Plans reordered.' => 'Plans réorganisés.', + 'Portrait' => 'Portrait', + 'Post Date' => 'Date de publication', + 'Postal Code Formula' => 'Formule du code postal', + 'Pounds (lb)' => 'Livres (lb)', + 'Preview' => 'Aperçu', + 'Previous Status' => 'État précédent', + 'Price' => 'Prix', + 'Prices' => 'Prix', + 'Pricing Rules' => 'Règles de tarification', + 'Pricing jobs are currently running.' => 'Des travaux de tarification sont actuellement en cours.', + 'Pricing' => 'Tarification', + 'Primary Billing Address' => 'Adresse de facturation principale', + 'Primary Shipping Address' => 'Adresse de livraison principale', + 'Primary payment source updated.' => 'Source de paiement principale mise à jour.', + 'Primary' => 'Principal', + 'Private Note' => 'Note privée', + 'Product Fields' => 'Champs produit', + 'Product ID is required.' => 'Un identifiant produit est requis.', + 'Product Template' => 'Modèle du produit', + 'Product Title Format' => 'Format du titre de produit', + 'Product Type' => 'Type de produit', + 'Product Types' => 'Types de produits', + 'Product URI Format' => 'Format d\'URI produit', + 'Product Variant' => 'Variante de produit', + 'Product Variants' => 'Variantes de produit', + 'Product type saved.' => 'Type de produit enregistré.', + 'Product type settings' => 'Paramètres du type de produit', + 'Product' => 'Produit', + 'Products and Variants deleted.' => 'Produits et variantes supprimés.', + 'Products not restored.' => 'Produits non restaurés.', + 'Products restored.' => 'Produits restaurés.', + 'Products' => 'Produits', + 'Promotable' => 'Promouvable', + 'Promotable?' => 'Promouvable?', + 'Promotional Amount' => 'Montant promotionnel', + 'Promotional Price' => 'Prix promotionnel', + 'Purchasable Categories' => 'Catégories achetables', + 'Purchasable ID and Sale ID are required.' => 'Des identifiants achetable et de vente sont requis.', + 'Purchasable ID is required.' => 'Un identifiant achetable est requis.', + 'Purchasable Type' => 'Type d\'achat', + 'Purchasable' => 'Achetable', + 'Purchase (Authorize and Capture Immediately)' => 'Acheter (autoriser et collecter immédiatement)', + 'Purchase Total' => 'Total d’achat', + 'Qty' => 'Qté', + 'Quality Control' => 'Contrôle de la qualité', + 'Quantity' => 'Quantité', + 'Rate' => 'Taux', + 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Réaffecter {numOrders, plural, =1{la commande} other{les commandes}}', + 'Recalculate order' => 'Recalculer la commande', + 'Receive Inventory' => 'Recevoir l\'inventaire', + 'Receive Transfer' => 'Recevoir le transfert', + 'Receive' => 'Recevoir', + 'Received' => 'Reçu', + 'Recent Orders' => 'Commandes récentes', + 'Recipient' => 'Destinataire', + 'Recover Cart' => 'Récupérer le panier', + 'Reduce price' => 'Diminuer le prix', + 'Reduce the price by a fixed amount' => 'Diminuer le prix d’un montant fixe', + 'Reduce the price by a percentage of the original price' => 'Réduire le prix selon un pourcentage du prix initial', + 'Reference' => 'Référence', + 'Refresh payment history' => 'Actualiser l\'historique de paiement', + 'Refund note' => 'Note de remboursement', + 'Refund payment' => 'Rembourser le paiement', + 'Refund' => 'Remboursement', + 'Reject' => 'Rejeter', + 'Rejected' => 'Rejeté', + 'Relationship Type' => 'Type de relation', + 'Removable included tax rates are only allowed for the default tax zone.' => 'Les taux de taxes inclus supprimables sont uniquement autorisés pour la zone de taxes par défaut.', + 'Remove address' => 'Supprimer l\'adresse', + 'Remove all shipping costs from the order' => 'Retirer tous les coûts d\'expédition de la commande', + 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Supprimer l\'association avec le client et l\'adresse courriel {numOrders, plural, =1{de la commande} other{des commandes}}. Vous pouvez également sélectionner d\'autres données de clients à supprimer ci-dessous', + 'Remove customer data' => 'Supprimer les données de clients', + 'Remove from price?' => 'Retirer du prix?', + 'Remove shipping costs for matching items only' => 'Retirer les coûts d\'expédition uniquement pour les articles correspondants', + 'Remove the included tax when a valid organization tax ID is present?' => 'Supprimer la taxe incluse lorsqu\'un identifiant fiscal d\'organisation valide est présent?', + 'Remove' => 'Supprimer', + 'Removed' => 'Supprimé', + 'Repeat Customers' => 'Clients réguliers', + 'Reply To' => 'Répondre à', + 'Require Billing Address At Checkout' => 'Exiger l\'adresse de facturation au moment du paiement', + 'Require Coupon Code' => 'Demander un code de promotionnel', + 'Require Shipping Address At Checkout' => 'Exiger l\'adresse de livraison au moment du paiement', + 'Require Shipping Method Selection At Checkout' => 'Exiger le choix du mode d\'expédition au moment de la validation de la commande', + 'Require' => 'Demander', + 'Reserved' => 'Réservée', + 'Reset usage' => 'Réinitialiser l\'utilisation', + 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Restreindre le rabais aux commandes où le client a acheté une valeur totale minimale d’articles correspondants.', + 'Revenue Options' => 'Options de revenus', + 'Revenue' => 'Revenu', + 'Rule' => 'Règle', + 'Rules reordered.' => 'Règles réorganisées.', + 'SKU' => 'UGS', + 'Safety' => 'Sécurité', + 'Sale Price' => 'Prix en promotion', + 'Sale description.' => 'Description de la vente.', + 'Sale reordered.' => 'Promotion réorganisée.', + 'Sale saved.' => 'Promotion enregistrée.', + 'Sale' => 'Vente', + 'Sales deleted.' => 'Ventes supprimées.', + 'Sales updated.' => 'Ventes mises à jour.', + 'Sales' => 'Ventes', + 'Save and continue editing' => 'Enregistrer et continuer la modification', + 'Save and return to all orders' => 'Enregistrer et revenir à l\'ensemble des commandes', + 'Save and set rules' => 'Enregistrer et appliquer les règles', + 'Save as a new rule' => 'Enregistrer en tant que nouvelle règle', + 'Save product to all sites enabled for this product type' => 'Enregistrer les produits sur tous les sites activés pour ce type de produit', + 'Save product to other sites in the same site group' => 'Enregistrer le produit sur les autres sites du même groupe de sites', + 'Save product to other sites with the same language' => 'Enregistrer le produit sur les autres sites ayant la même langue', + 'Save' => 'Enregistrer', + 'Search customer…' => 'Rechercher un client…', + 'Search inventory' => 'Rechercher dans les stocks', + 'Search or enter customer email…' => 'Rechercher ou saisir un courriel client…', + 'Search…' => 'Rechercher…', + 'See Orders' => 'Voir les commandes', + 'Select a gateway' => 'Sélectionner une passerelle', + 'Select a tax category.' => 'Sélectionner une catégorie de taxes.', + 'Select a tax zone. If empty, this rate will match anywhere.' => 'Sélectionner une zone fiscale. Si aucune zone n\'est sélectionnée, ce taux s\'appliquera partout.', + 'Select address' => 'Sélectionner l\'adresse', + 'Select an item' => 'Sélectionner un article', + 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Sélectionnez la manière dont la règle de tarification du catalogue sera appliquée au(x) produit(s) acheté(s).', + 'Select how the sale will be applied to the purchasable(s).' => 'Indiquez comment la promotion sera appliquée aux articles en vente.', + 'Select product type' => 'Sélectionner le type de produit', + 'Select the emails that will be sent when transitioning to this status.' => 'Sélectionner les courriels qui seront envoyés lors de la transition à cet état.', + 'Select what this rate should be applied to.' => 'Sélectionner ce à quoi ce taux doit être appliqué.', + 'Send Email' => 'Envoyer le courriel', + 'Send to custom recipient' => 'Envoyer au destinataire personnalisé', + 'Send to the customer' => 'Envoyer au client', + 'Set Quantity' => 'Définir Quantité', + 'Set default category' => 'Définir la catégorie par défaut', + 'Set default variant' => 'Définir la variante par défaut', + 'Set or Adjust' => 'Régler ou ajuster', + 'Set price' => 'Définir le prix', + 'Set status' => 'Définir le statut', + 'Set the price to a flat amount' => 'Fixer le prix à un montant forfaitaire', + 'Set the price to a percentage of the original price' => 'Fixer le prix à un pourcentage du prix d\'origine', + 'Set the sale price to a flat amount' => 'Définir le prix en promotion à l\'aide d\'un montant fixe', + 'Set the sale price to a percentage of the original price' => 'Définir le prix en promotion comme un pourcentage du prix original', + 'Set to' => 'Définir sur', + 'Settings saved.' => 'Paramètres enregistrés.', + 'Settings' => 'Paramètres', + 'Share cart…' => 'Partager le panier…', + 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Expédition - Le coût minimum est le coût d\'expédition, si le prix de la commande est inférieur au coût d\'expédition.', + 'Shipping Address Zone' => 'Zone d\'adresse de livraison', + 'Shipping Address' => 'Adresse de livraison', + 'Shipping Business Name' => 'Nom de l\'entreprise pour la livraison', + 'Shipping Categories' => 'Catégories d’expédition', + 'Shipping Category Conditions' => 'Conditions de la catégorie d’expédition', + 'Shipping Category' => 'Catégorie d’expédition', + 'Shipping First Name' => 'Nom pour la livraison', + 'Shipping Full Name' => 'Nom complet pour la livraison', + 'Shipping Last Name' => 'Nom pour la livraison', + 'Shipping Method' => 'Méthode d’expédition', + 'Shipping Methods' => 'Méthodes d’expédition', + 'Shipping Rule' => 'Règle de livraison', + 'Shipping Zones' => 'Zones d’expédition', + 'Shipping address required.' => 'Adresse de livraison requise.', + 'Shipping categories deleted.' => 'Catégories d\'expédition supprimées.', + 'Shipping category saved.' => 'Catégorie d’expédition enregistrée.', + 'Shipping category updated.' => 'Catégorie de livraison mise à jour.', + 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Coûts d\'expédition ajoutés à la commande en un tout avant qu\'un pourcentage, un article et des tarifs au poids ne s\'appliquent. Régler à zéro pour désactiver ce taux. La règle entière, y compris ce taux de base, ne répondra pas et ne s\'appliquera pas si le panier ne contient que des articles non expédiables comme les produits numériques.', + 'Shipping method saved.' => 'Méthode d’expédition enregistrée.', + 'Shipping methods and rules deleted.' => 'Modes et règles d\'expédition supprimés.', + 'Shipping methods updated.' => 'Modes de livraison mis à jour.', + 'Shipping rule saved.' => 'Règle de livraison enregistrée.', + 'Shipping zone saved.' => 'Zone d’expédition enregistrée.', + 'Shipping' => 'Expédition', + 'Short Number' => 'Numéro court', + 'Show Chart?' => 'Afficher le graphique?', + 'Show Order Count?' => 'Afficher le nombre de commandes?', + 'Show all prices' => 'Afficher tous les prix', + 'Show archived gateways' => 'Afficher les portails archivés', + 'Show order count line on chart.' => 'Afficher la ligne de décompte des commandes sur le graphique.', + 'Show related sales' => 'Ventes liées au salon', + 'Show rule details' => 'Afficher les détails de la règle', + 'Show the Dimensions and Weight fields for products of this type' => 'Montrer les champs des dimensions et du poids pour les produits de ce type', + 'Show the Title field for products' => 'Afficher le champ Titre pour les produits', + 'Show the Title field for variants' => 'Montrer le champ titre pour les variantes', + 'Signed In' => 'Connexion effectuée', + 'Site Languages' => 'Langues du site', + 'Site store mapping saved.' => 'Cartographie du magasin du site enregistrée.', + 'Sites' => 'Sites', + 'Slug' => 'Identificateur', + 'Snapshot' => 'Instantané', + 'Snapshots' => 'Instantanés', + 'Some orders restored.' => 'Certaines commandes ont été restaurées.', + 'Some products restored.' => 'Certains produits ont été restaurés.', + 'Some variants restored.' => 'Certaines variantes ont été restaurées.', + 'Something changed with the order before payment, please review your order and submit payment again.' => 'Quelque chose a changé dans la commande avant le paiement, merci de la vérifier puis de soumettre le paiement de nouveau.', + 'Sorry, no matching options.' => 'Désolé, aucune option ne correspond.', + 'Source - The purchasable relationship field is on the category' => 'Source - Le champ de la relation d\'achat se trouve dans la catégorie', + 'Source' => 'Source', + 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Préciser une condition Twig déterminant si la réduction doit s\'appliquer à une commande donnée (la commande peut être référencée via la variable `order`).', + 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Préciser une condition Twig déterminant si la règle d\'expédition doit s\'appliquer à une commande donnée (la commande peut être référencée via la variable `order`).', + 'Start Date' => 'Date de début', + 'State' => 'État', + 'Status Email Address' => 'Adresse courriel d’état', + 'Status Emails' => 'Courriels d’état', + 'Status History' => 'Historique des statuts', + 'Status Updated.' => 'Statut mis à jour.', + 'Status change message' => 'Message de changement d\état', + 'Status' => 'État', + 'Stock' => 'Inventaire', + 'Stops Processing?' => 'Arrête le traitement?', + 'Stops subsequent?' => 'Arrête le suivant?', + 'Store Location' => 'Emplacement du magasin', + 'Store Management' => 'Gestion de magasin', + 'Store Markets' => 'Marchés de la boutique', + 'Store Rule' => 'Règle de magasin', + 'Store saved.' => 'Boutique enregistrée.', + 'Store' => 'Magasin', + 'Stores & Sites' => 'Magasins et sites', + 'Stores' => 'Magasins', + 'Strategy to apply when an order is free or has a zero balance.' => 'Stratégie à appliquer lorsqu\'une commande est gratuite ou a un solde nul.', + 'Strategy to apply when calculating the minimum order price.' => 'Stratégie à employer lors du calcul du prix de la commande minimale.', + 'Subject' => 'Objet', + 'Subscribing user' => 'Utilisateur abonné', + 'Subscription Fields' => 'Champs d’abonnement', + 'Subscription Plans' => 'Plans d’abonnement', + 'Subscription Settings' => 'Paramètres d\'abonnement', + 'Subscription cancelled.' => 'Abonnement annulé.', + 'Subscription date' => 'Date d’abonnement', + 'Subscription fields saved.' => 'Champs d’abonnement enregistrés.', + 'Subscription for {user} to {plan} prevented by a plugin.' => 'Un plugiciel a empêché l’abonnement de {user} à {plan}.', + 'Subscription plan saved.' => 'Abonnement enregistré.', + 'Subscription plan' => 'Plan d’abonnement', + 'Subscription plans' => 'Plans d’abonnement', + 'Subscription reactivated.' => 'Abonnement réactivé.', + 'Subscription reference' => 'Référence de l’abonnement', + 'Subscription started.' => 'Abonnement démarré.', + 'Subscription switched.' => 'Abonnement modifié.', + 'Subscription to “{plan}”' => 'Abonnement à « {plan} »', + 'Subscription' => 'Abonnement', + 'Subscriptions on hold' => 'Abonnements en attente', + 'Subscriptions' => 'Abonnements', + 'Suppress emails' => 'Réduire les courriels', + 'Switch plan' => 'Changer de plan', + 'Switch' => 'Changer', + 'System' => 'Système', + 'Table Columns' => 'Colonnes du tableau', + 'Target - The category relationship field is on the purchasable' => 'Cible - Le champ de la relation de catégorie se trouve sur la liste des produits achetables.', + 'Tax & Shipping' => 'Taxes et expédition', + 'Tax (inc)' => 'Taxe (incl.)', + 'Tax Categories' => 'Catégories de taxes', + 'Tax Category' => 'Catégorie de taxes', + 'Tax Rates' => 'Taux de taxes', + 'Tax Zone' => 'Zone de taxes', + 'Tax Zones' => 'Zones de taxes', + 'Tax categories deleted.' => 'Catégories de taxe supprimées.', + 'Tax category saved.' => 'Catégorie de taxes enregistrée.', + 'Tax category updated.' => 'Catégorie de taxe mise à jour.', + 'Tax rate saved.' => 'Taux de taxes enregistré.', + 'Tax rates updated.' => 'Taux de taxe mis à jour.', + 'Tax zone saved.' => 'Zone de taxe enregistrée.', + 'Tax' => 'Taxe', + 'Taxable Subject' => 'Objet taxable', + 'Template Path' => 'Chemin du modèle', + 'That handle is already in use' => 'Cet identificateur est déjà utilisé', + 'That handle is already in use.' => 'Cet identificateur est déjà utilisé.', + 'The PDF to attach to this email.' => 'Le PDF à attacher à ce courriel.', + 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'L\'URL de la page permettant de mettre à jour les détails de facturation d\'un abonnement, ainsi que de gérer l\'authentification 3DS.', + 'The address provided is outside the store’s market.' => 'L\'adresse fournie est en dehors du marché de la boutique.', + 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'Le montant du rabais qui est appliqué à l\'ensemble de la commande. Ce montant est réparti sur les articles dans l\'ordre, du prix le plus élevé au prix le plus bas, jusqu\'à la fin du rabais.', + 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'Le rabais de base ne peut réduire le prix des articles du panier à zéro ni rendre le montant de la commande négatif.', + 'The cart recovery link is invalid. Please request a new one.' => 'Le lien de récupération du panier n\'est pas valide. Veuillez en demander un nouveau.', + 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Le taux de conversion qui sera utilisé lors de la conversion d’un montant en cette devise. Par exemple, si un article coût {amount1}, un taux de conversion de {rate} aboutira à {amount2} dans la devise alternative.', + 'The countries that orders are allowed to be placed from.' => 'Les pays depuis lesquels il est possible de passer une commande.', + 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'Le coupon « {code} » a dépassé la limite de {limit} utilisations.', + 'The customer for this order has been deleted.' => 'Le client associé à cette commande a été supprimé.', + 'The default shipping category is automatically available to all product types.' => 'La catégorie d\'expédition par défaut est automatiquement disponible pour tous les types de produits.', + 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'La remise « {name} » a dépassé sa limite d\'utilisation totale de {limit}.', + 'The download link has expired. Please request a new one.' => 'Le lien de téléchargement a expiré. Veuillez en demander un nouveau.', + 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'L’adresse courriel à partir de laquelle les courriels d’état de commande sont envoyés. Laisser vide pour utiliser l’adresse courriel de système définie dans les paramètres généraux de Craft.', + 'The entry that contains the description for this subscription’s plan.' => 'L’entrée qui contient la description de ce plan d’abonnement.', + 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'La remise forfaitaire appliquée à chaque article. Par ex : « 3 » pour 3 $ de remise par article.', + 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'Le format utilisé pour générer de nouveaux coupons, par exemple {example}. Tout caractère « # » sera remplacé par une lettre aléatoire.', + 'The from and to inventory locations must be different.' => 'Les emplacements des stocks de départ et d\'arrivée doivent être différents.', + 'The inventory locations this store uses.' => 'Les emplacements des stocks utilisés par ce magasin.', + 'The item is not enabled for sale.' => 'Cet article n’est pas activé pour la vente.', + 'The language the order was made in.' => 'La langue dans laquelle la commande a été passée.', + 'The language to be used when this email is rendered.' => 'La langue à utiliser lors de l\'affichage de ce courriel.', + 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Le nombre maximum de niveaux que ce type de produit peut avoir. Laisser vide si cela n\'est pas pertinent.', + 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Le maximum que le client doit dépenser sur l’expédition. Mettez à zéro pour désactiver.', + 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Le minimum que le client doit dépenser sur l’expédition. Mettez à zéro pour désactiver.', + 'The order is not valid.' => 'La commande n\'est pas valide.', + 'The payment gateway that will be used for the subscription plan.' => 'Quelle passerelle de paiement sera utilisée pour le plan d’abonnement?', + 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'La valeur du pourcentage de rabais qui doit être appliqué à chaque article, par exemple {ex1} pour une remise de {ex2}. Les pourcentages sont arrondis à 2 décimales.', + 'The previously-selected shipping method is no longer available.' => 'La méthode d\'expédition précédemment sélectionnée n\'est plus disponible.', + 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Le prix de {description} a augmenté, il est passé de {originalSalePriceAsCurrency} à {newSalePriceAsCurrency}', + 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Le prix de {description} a baissé, il est passé de {originalSalePriceAsCurrency} à {newSalePriceAsCurrency}', + 'The primary currency cannot be changed after orders are placed.' => 'La devise principale ne peut être modifiée après la validation des commandes.', + 'The purchasable defines the relationship' => 'Le produit achetable définit la relation', + 'The purchasable is related by another element' => 'Le produit achetable est lié par un autre élément', + 'The recipient of the email. Twig code can be used here.' => 'Le destinataire du courriel. Du code Twig peut être utilisé ici.', + 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'L\'adresse courriel de réponse. Laisser vide pour une réponse normale à l\'expéditeur du courriel. Du code Twig peut être utilisé ici.', + 'The site the order was made in.' => 'Le site sur lequel la commande a été passée.', + 'The site to be used when this email is rendered.' => 'Le site à utiliser lorsque ce courriel est affiché.', + 'The subject line of the email. Twig code can be used here.' => 'L\'objet du courriel. Du code Twig peut être utilisé ici.', + 'The template that the PDF should be generated from.' => 'Le modèle à partir duquel le PDF doit être généré.', + 'The template to be used for HTML emails.' => 'Le modèle à utiliser pour les courriels HTML.', + 'The template to be used for plain text emails. Twig code can be used here.' => 'Le modèle à utiliser pour les courriels en texte brut. Du code Twig peut être utilisé ici.', + 'The template to use when a product’s URL is requested.' => 'Le modèle à utiliser lorsque l’URL d’un produit est demandé.', + 'The total number of order adjustments changed.' => 'Le nombre total d’ajustements de commande a changé.', + 'The total price of the order changed.' => 'Le montant total de la commande a été modifié.', + 'The total quantity of items within the order changed.' => 'La quantité totale d’articles dans la commande a changé.', + 'The unique SKU of the donation purchasable.' => 'L\'unique UGS de don pouvant être achetée.', + 'The unit of measurement that should be used when specifying product dimensions.' => 'L’unité de mesure qui devrait être utilisée pour indiquer les dimensions d’un produit.', + 'The unit of measurement that should be used when specifying product weights.' => 'L’unité de mesure qui devrait être utilisée pour indiquer les poids d’un produit.', + 'The webhook URL for this gateway.' => 'L\'URL du webhook pour cette passerelle.', + 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'Le nom de l’expéditeur à utiliser lors de l’envoi des courriels d’état de commande. Laisser vide pour utiliser le nom d’expéditeur défini dans les paramètres généraux de Craft.', + 'There are errors on the order' => 'Il y a des erreurs dans la commande', + 'There are only {num} “{description}” items left in stock.' => 'Il ne reste que {num} articles « {description} » en stock.', + 'There aren’t any product types to select yet.' => 'Il n\'y a pas encore de types de produits à sélectionner.', + 'There is no gateway or payment source available for use with this order.' => 'Aucun portail ou source de paiement disponible pour cette commande.', + 'There is no gateway selected that supports payment sources.' => 'Aucune passerelle prenant en charge les modes de paiement n’est sélectionnée.', + 'There is no shipping method selected for this order.' => 'Aucune méthode d\'expédition sélectionnée pour cette commande.', + 'This URL will load the cart into the user’s session, making it the active cart.' => 'Cette URL chargera le panier dans la session de l\'utilisateur, ce qui en fera le panier actif.', + 'This action is not allowed for the current user.' => 'Cette action n\'est pas autorisée pour l\'utilisateur actuel.', + 'This category will be used as the default for all purchasables in this store.' => 'Cette catégorie sera utilisée par défaut pour tous les articles pouvant être achetés dans ce magasin.', + 'This coupon is for registered users and limited to {limit} uses.' => 'Ce coupon est limité à {limit} utilisations pour les utilisateurs inscrits.', + 'This coupon is limited to {limit} uses.' => 'Ce coupon est limité à {limit} utilisations.', + 'This coupon requires an email address.' => 'Ce rabais nécessite une adresse courriel.', + 'This gateway does not support that functionality.' => 'Cette passerelle ne prend pas en charge cette fonctionnalité.', + 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Cela est annulé par le paramètre de configuration {setting} dans `config/{file}.php`.', + 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Il s’agit de l’adresse physique de votre magasin. Elle peut être utilisée par divers plugiciels pour déterminer des éléments comme la livraison et les taxes. Elle peut aussi être utilisée dans les reçus PDF.', + 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Ceci est le PDF par défaut qui sera fourni lors de la demande du PDF de commande.', + 'This is the last location for the {store} store.' => 'Il s\'agit du dernier emplacement pour le magasin {store}.', + 'This month' => 'Ce mois-ci', + 'This order has unsaved changes.' => 'Cette commande a des modifications non enregistrées.', + 'This week' => 'Cette semaine', + 'This year' => 'Cette année', + 'Times Used' => 'Nombre de fois utilisé', + 'Title' => 'Titre', + 'To' => 'À', + 'Today' => 'Aujourd’hui', + 'Too many variants for this product.' => 'Trop de variantes pour ce produit.', + 'Top Customers by Average Order' => 'Meilleurs clients par commande moyenne', + 'Top Customers by Total Revenue' => 'Meilleurs clients par recettes totales', + 'Top Customers' => 'Meilleurs clients', + 'Top Product Types by Qty Sold' => 'Meilleurs types de produits par Qté vendue', + 'Top Product Types by Revenue' => 'Meilleurs types de produits par recettes', + 'Top Product Types' => 'Meilleurs types de produits', + 'Top Products by Qty Sold' => 'Meilleurs produits par Qté vendue', + 'Top Products by Revenue' => 'Meilleurs produits par recettes', + 'Top Products' => 'Meilleurs produits', + 'Top Purchasables by Qty Sold' => 'Meilleurs achetables par Qté vendue', + 'Top Purchasables by Revenue' => 'Meilleurs achetables par recettes', + 'Top Purchasables' => 'Meilleurs achetables', + 'Total ' => 'Total ', + 'Total Discount Use Limit' => 'Limite d\'utilisation totale des rabais', + 'Total Discount' => 'Rabais total', + 'Total Included Tax' => 'Taxes totales incluses', + 'Total Orders by Billing Country' => 'Total des commandes par pays de facturation', + 'Total Orders by Country' => 'Total des commandes par pays', + 'Total Orders by Shipping Country' => 'Total des commandes par pays de livraison', + 'Total Orders' => 'Nombre total de commandes', + 'Total Paid' => 'Total payé', + 'Total Price' => 'Prix total', + 'Total Qty' => 'Qté totale', + 'Total Revenue' => 'Total des recettes', + 'Total Shipping' => 'Total de la livraison', + 'Total Tax' => 'Total des taxes', + 'Total Weight' => 'Poids total', + 'Total' => 'Total', + 'Track Inventory' => 'Suivi des stocks', + 'Transaction Hash' => 'Hachage de la transaction', + 'Transaction ID' => 'ID de la transaction', + 'Transaction captured successfully: {message}' => 'Transaction collectée avec succès : {message}', + 'Transaction refunded successfully: {message}' => 'Transaction remboursée avec succès : {message}', + 'Transactions' => 'Transactions', + 'Transfer Fields' => 'Champs de transfert', + 'Transfer Items' => 'Articles de transfert', + 'Transfer Settings' => 'Paramètres de transfert', + 'Transfer Status' => 'Statut du transfert', + 'Transfer fields saved.' => 'Champs de transfert enregistrés.', + 'Transfer must have at least one item.' => 'Le transfert doit comporter au moins un article.', + 'Transfer' => 'Transférer', + 'Transfers' => 'Transferts', + 'Trial days credited' => 'Jours d’essai crédités', + 'Trial expiration' => 'Expiration de l\'essai', + 'Trial expiry date' => 'Date d’expiration de la version d’essai', + 'Type not in allowed options.' => 'Type non autorisé dans les options.', + 'Type' => 'Type', + 'URI' => 'URI', + 'Unable to cancel subscription at this time.' => 'Impossible d\'annuler l’abonnement actuellement.', + 'Unable to complete order: another request is already in progress.' => 'Impossible de terminer la commande : une autre demande est déjà en cours.', + 'Unable to find variant.' => 'Impossible de trouver la variante.', + 'Unable to generate coupon codes: {message}' => 'Impossible de générer des codes de réduction : {message}', + 'Unable to make payment at this time.' => 'Impossible d’effectuer le paiement pour le moment.', + 'Unable to modify subscription at this time.' => 'Impossible de modifier l’abonnement actuellement.', + 'Unable to reactivate subscription at this time.' => 'Impossible de réactiver l’abonnement pour le moment.', + 'Unable to reassign orders.' => 'Impossible de réattribuer les commandes.', + 'Unable to remove order data.' => 'Impossible de supprimer les données de la commande.', + 'Unable to retrieve Sale and Purchasable.' => 'Impossible de récupérer les promotions et les achetables.', + 'Unable to retrieve cart.' => 'Impossible de récupérer le panier.', + 'Unable to retrieve customer.' => 'Impossible de récupérer le client.', + 'Unable to retrieve load cart URL' => 'Impossible de récupérer et charger l\'URL du panier', + 'Unable to retrieve payment source.' => 'Impossible de récupérer la source de paiement.', + 'Unable to set default shipping category.' => 'Impossible de définir la catégorie d\'expédition par défaut.', + 'Unable to set default tax category.' => 'Impossible de définir la catégorie de taxe par défaut.', + 'Unable to set primary payment source.' => 'Impossible de définir la source de paiement principale.', + 'Unable to start the subscription. Please check your payment details.' => 'Impossible de démarrer l’abonnement. Veuillez vérifier vos informations de paiement.', + 'Unable to subscribe at this time.' => 'Impossible de s’abonner pour le moment.', + 'Unable to update cart.' => 'Impossible de mettre à jour le panier.', + 'Unable to validate address.' => 'Impossible de valider l’adresse.', + 'Unit Price' => 'Prix unitaire', + 'Unit price (minus discounts)' => 'Prix unitaire (moins les remises)', + 'Units' => 'Unités', + 'Unpaid' => 'Non payé', + 'Unsubscribe' => 'Annuler l’abonnement', + 'Update Address' => 'Mettre à jour l’adresse', + 'Update Order Status' => 'Mettre à jour le statut de la commande', + 'Update Order Status…' => 'Mettre à jour le statut de la commande…', + 'Update order' => 'Mettre à jour la commande', + 'Update subscription' => 'Mettre à jour l\'abonnement', + 'Update' => 'Mettre à jour', + 'Updated By' => 'Mis à jour par', + 'Updated committed stock successfully.' => 'Validation des stocks engagés réussie.', + 'Updated' => 'Mis à jour', + 'Use Billing Address For Tax' => 'Utiliser l\'adresse de facturation pour les taxes', + 'Use as the primary billing address' => 'Utiliser comme adresse de facturation principale', + 'Use as the primary shipping address' => 'Utiliser comme adresse de livraison principale', + 'Used By Tax Rates' => 'Utilisé par les taux de taxes', + 'Used by Tax Rates' => 'Utilisé par le taux de taxe', + 'User Groups' => 'Groupes d’utilisateurs', + 'User not found.' => 'Utilisateur non trouvé.', + 'User' => 'Utilisateur', + 'Uses' => 'Utilisations', + 'Validate Business Tax ID as Vat ID' => 'Valider l\'ID de la taxe d\'affaires en tant qu\'ID de TPS/TVQ', + 'Validating condition syntax' => 'Validation de la syntaxe conditionnelle', + 'Validating formula syntax' => 'Validation de la syntaxe de la formule', + 'Variant Fields' => 'Champs de variante', + 'Variant Has Untracked Stock' => 'La variante a un stock non tracé', + 'Variant Price' => 'Prix de la variante', + 'Variant SKU' => 'UGS de la variante', + 'Variant Search' => 'Rechercher une variante', + 'Variant Stock' => 'Stock de la variante', + 'Variant Title Format' => 'Format de titre de variante', + 'Variant Tracks Stock' => 'La variante suit les stocks', + 'Variant UI Label Format' => 'Format des étiquettes de l\'interface de variantes', + 'Variant has no product.' => 'La variante n\'a pas de produit.', + 'Variants not restored.' => 'Variantes non restaurées.', + 'Variants restored.' => 'Variantes restaurées.', + 'Variants' => 'Variantes', + 'View customer' => 'Voir le client', + 'View order' => 'Afficher la commande', + 'View product type - {productType}' => 'Voir le type de produit - {productType}', + 'View user' => 'Voir l\'utilisateur', + 'View' => 'Voir', + 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Attention, la suppression de cette devise entraînera l\'arrêt de tous les paiements et remboursements dans cette devise, êtes-vous sûr de vouloir supprimer « {name} »?', + 'Web' => 'Web', + 'Webhook URL' => 'URL du webhook', + 'Weight ({unit})' => 'Poids ({unit})', + 'Weight Rate' => 'Taux de poids', + 'Weight Unit' => 'Unité de poids', + 'Weight' => 'Poids', + 'What product URIs should look like for the site.' => 'Ce à quoi les URIs de produits devraient ressembler pour le site.', + 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Ce à quoi devraient ressembler les titres de produits générés automatiquement. Vous pouvez inclure des balises qui produisent des propriétés de produits, comme {ex1} ou {ex2}. Tous les champs personnalisés utilisés doivent être définis comme étant obligatoires.', + 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Ce à quoi les titres de variante auto-générés devraient ressembler. Vous pouvez inclure des étiquettes qui sélectionnent les propriétés de la variante, comme {ex1} ou {ex2}. Tous les champs personnalisés utilisés doivent être exigés.', + 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Ce à quoi le nom de fichier PDF de commande devrait ressembler (sans extension). Vous pouvez inclure des étiquettes représentant certaines caractéristiques de la commande, telles que {ex1} ou {ex2}.', + 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Ce à quoi les UGS auto-générés devraient ressembler, lorsqu’un champ d\'UGS est soumis à vide. Vous pouvez inclure des étiquettes qui sélectionnent les propriétés, comme {ex1} ou {ex2}.', + 'What this PDF will be called in the control panel.' => 'Nom de ce PDF dans le panneau de configuration.', + 'What this catalog pricing rule will be called in the control panel.' => 'Le nom de cette règle de tarification du catalogue dans le panneau de configuration.', + 'What this discount will be called in the control panel.' => 'Nom de ce rabais dans le panneau de configuration.', + 'What this email will be called in the control panel.' => 'Nom de ce courriel dans le panneau de configuration.', + 'What this product type will be called in the control panel.' => 'Nom de ce type de produit dans le panneau de configuration.', + 'What this sale will be called in the control panel.' => 'Nom de cette promotion dans le panneau de configuration.', + 'What this shipping category will be called in the control panel.' => 'Nom de cette catégorie de livraison dans le panneau de configuration.', + 'What this shipping rule will be called in the control panel.' => 'Nom de cette règle de livraison dans le panneau de configuration.', + 'What this shipping zone will be called in the control panel.' => 'Nom de cette zone de livraison dans le panneau de configuration.', + 'What this status will be called in the control panel.' => 'Nom de ce statut dans le panneau de configuration.', + 'What this subscription plan will be called in the control panel.' => 'Nom de cet abonnement dans le panneau de configuration.', + 'What this tax category will be called in the control panel.' => 'Nom de cette catégorie de taxe dans le panneau de configuration.', + 'What this tax zone will be called in the control panel.' => 'Nom de cette zone de taxe dans le panneau de configuration.', + 'When this discount is applied to an order, which line items should be discounted?' => 'Lorsque ce rabais est appliqué à une commande, quels postes doivent faire l\'objet d\'un rabais?', + 'Whether the first available shipping method option should be set automatically on carts.' => 'Indique si la première option de méthode d\'expédition disponible doit être définie automatiquement dans les paniers.', + 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Indique si la source de paiement principale de l\'utilisateur doit être définie automatiquement pour les nouveaux paniers.', + 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Indique si les adresses principales de livraison et de facturation de l\'utilisateur doivent être définies automatiquement pour les nouveaux paniers.', + 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Indique si cette règle de tarification du catalogue doit pouvoir être utilisée, indépendamment d\'autres conditions.', + 'Whether this sale should be available for use, regardless of other conditions.' => 'Si cette vente doit être disponible ou non, indépendamment des autres conditions.', + 'Which data to display in the name column in the results table.' => 'Les données à afficher dans la colonne des noms dans le tableau des résultats.', + 'Which product types should this category be available to?' => 'Dans quels types de produits cette catégorie devrait-elle être offerte?', + 'Which template should be loaded when a product’s URL is requested.' => 'Quel modèle devrait être chargé quand l’URL d’un produit est demandée.', + 'Width ({unit})' => 'Largeur ({unit})', + 'Width' => 'Largeur', + 'YYYY' => 'AAAA', + 'Yes' => 'Oui', + 'You are not allowed to add a line item.' => 'Vous n\'êtes pas autorisé à ajouter un article.', + 'You currently have no emails configured to select for this status.' => 'Vous n\'avez actuellement aucune adresse courriel configurée à sélectionner pour ce statut.', + 'You do not have permission to load this cart.' => 'Vous n\'êtes pas autorisé à charger ce panier.', + 'You must set up at least one gateway that supports subscriptions first.' => 'Vous devez définir au moins une passerelle qui prend d’abord en charge les abonnements.', + 'You must be logged in or provide a valid token to load this cart.' => 'Vous devez être connecté ou fournir un jeton valide pour charger ce panier.', + 'You must be signed in to create a payment source.' => 'Vous devez être inscrit pour créer une source de paiement.', + 'You must be signed in to set a primary payment source.' => 'Vous devez être inscrit pour définir une source de paiement principale.', + 'You must make a payment to complete the order.' => 'Vous devez effectuer un paiement pour terminer la commande.', + 'Your Cart Recovery Link' => 'Votre lien de récupération de panier', + 'Your Order PDF Download Link' => 'Lien de téléchargement du PDF de votre commande', + 'Your order is empty' => 'Votre commande est vide', + 'ZIP file' => 'Fichier ZIP', + 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Zéro - Le prix minimum est zéro si les rabais dépassent la valeur de la commande.', + 'Zip Code' => 'Code postal', + 'all' => 'tous', + 'any' => 'n\'importe quel', + 'average order total' => 'total de commande moyen', + 'billing address' => 'adresse de facturation', + 'donation' => 'don', + 'donations' => 'dons', + 'info' => 'infos', + 'inventory location' => 'emplacement des stocks', + 'new customers' => 'nouveaux clients', + 'on hand' => 'disponible', + 'only' => 'seulement', + 'order' => 'commande', + 'orders' => 'commandes', + 'price' => 'prix', + 'prices' => 'prix', + 'product variant' => 'variante du produit', + 'product variants' => 'variantes du produit', + 'product' => 'produit', + 'products' => 'produits', + 'repeat customers' => 'clients réguliers', + 'shipping address' => 'adresse de livraison', + 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'Les valeurs shippingSameAsBilling et billingSameAsShipping ne peuvent être définies.', + 'subscription' => 'abonnement', + 'subscriptions' => 'abonnements', + 'to' => 'à', + 'transfer' => 'transférer', + 'transfers' => 'transferts', + '{amount} included' => '{amount} inclus', + '{count} Unfulfilled Orders' => '{count} commandes non satisfaites', + '{description} is no longer available.' => '{description} n\'est plus disponible.', + '{description} only has {stock} in stock.' => '{description} n\'a que {stock} unités en stock.', + '{from} to {to}' => '{from} à {to}', + '{name} (Primary)' => '{name} (primaire)', + '{name} (Trashed)' => '{name} (mis à la poubelle)', + '{name} catalog price' => 'prix catalogue {name}', + '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, one {}=1{commande mise à jour} other{commandes mises à jour}}.', + '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, =1{commande est associée} other{commandes sont associées}} {numUsers, plural, =1{à l\'utilisateur} other{aux utilisateurs}}.', + '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, =1{abonnement est activé} other{abonnements sont activés}} pour {numUsers, plural, =1{l\'utilisateur} other{les utilisateurs}}.', + '{number} more…' => '{number} plus…', + '{pct} off the discounted item price' => '{pct} sur le prix de l\'article incluant le rabais', + '{pct} off the original item price' => '{pct} sur le prix original de l\'article', + '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, =1{n\'a pas été assigné} other{n\'ont pas été assignés}} à un site.', + '{total} in total revenue' => '{total} dans les recettes totales', + '{total} orders' => '{total} commandes', + '{total} saleable across {locationCount} location(s)' => '{total} vendables parmi {locationCount} emplacement(s).', + '{uses} uses across {emails} email addresses' => '{uses} utilisations pour {emails} adresses courriel', + '{uses} uses across {users} users' => '{uses} utilisations pour {users} utilisateurs', + '“{description}” is currently out of stock.' => '« {description} » est actuellement épuisé.', + '“{key}” has invalid JSON' => '« {key} » possède une valeur JSON non valide', +]; diff --git a/lang/fr/commerce.php b/lang/fr/commerce.php new file mode 100644 index 0000000000..9fa6bcfeac --- /dev/null +++ b/lang/fr/commerce.php @@ -0,0 +1,1423 @@ + '(nouveau prix)', + '(of original price)' => '(par rapport au prix d’origine)', + '(off original price)' => '(en moins par rapport au prix d’origine)', + 'A cart number must be specified.' => 'Un numéro de panier doit être spécifié.', + 'A cart recovery link has been sent to {email}.' => 'Un lien de récupération du panier a été envoyé à {email}.', + 'A cart recovery link will be sent to {email}.' => 'Un lien de récupération du panier sera envoyé à {email}.', + 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Un numéro de référence simple sera généré sur la base de ce format lors de la finalisation d’un panier et de sa conversion en commande. Par exemple {ex1}, ou
{ex2}. Le résultat de ce formatage doit être unique.', + 'A new download link has been sent to {email}' => 'Un nouveau lien de téléchargement a été envoyé à {email}', + 'A new download link will be sent to {email}' => 'Un nouveau lien de téléchargement sera envoyé à {email}', + 'A valid email is required to create a customer.' => 'Un e-mail valide est requis pour créer un client.', + 'Accept' => 'Accepter', + 'Accepted' => 'Accepté', + 'Actions' => 'Actions', + 'Active Carts' => 'Paniers actifs', + 'Active subscriptions' => 'Abonnements actifs', + 'Active' => 'Actif', + 'Add Address' => 'Ajouter l’adresse', + 'Add a coupon' => 'Ajouter un coupon', + 'Add a custom line item' => 'Ajouter un article personnalité', + 'Add a line item' => 'Ajouter un article', + 'Add a product' => 'Ajouter un produit', + 'Add a variant' => 'Ajouter une variante', + 'Add an adjustment' => 'Ajouter un ajustement', + 'Add an item' => 'Ajouter un article', + 'Add an option' => 'Ajouter une option', + 'Add catalog price' => 'Ajouter le prix de catalogue', + 'Add' => 'Ajouter', + 'Additional Actions' => 'Actions supplémentaires', + 'Additional recipients that should receive this email. Twig code can be used here.' => 'Destinataires additionnels qui devraient recevoir cet e-mail. Du code Twig peut être utilisé ici.', + 'Address 1' => 'Adresse 1', + 'Address 2' => 'Adresse 2', + 'Address 3' => 'Adresse 3', + 'Address Line 1' => 'Adresse ligne 1', + 'Address Line 2' => 'Adresse ligne 2', + 'Address Updated.' => 'Adresse mise à jour.', + 'Address copied to user.' => 'Adresse copiée pour l\'utilisateur.', + 'Address not found.' => 'Adresse non trouvée.', + 'Adjust Quantity' => 'Ajuster la quantité', + 'Adjust by' => 'Ajuster par', + 'Adjust price when included rate is disqualified?' => 'Ajuster le prix lorsque le taux de taxe inclus est disqualifié ?', + 'Adjustments' => 'Ajustements', + 'Admin Notices' => 'Avis de l\'administrateur', + 'Administrative Area Code of Origin' => 'Code d\'origine de la zone administrative', + 'Advanced' => 'Avancé', + 'All Orders' => 'Toutes les commandes', + 'All Totals' => 'Tous les totaux', + 'All Transfers' => 'Tous les transferts', + 'All active subscriptions' => 'Tous les abonnements actifs', + 'All customers' => 'Tous les clients', + 'All products' => 'Tous les produits', + 'All variants must have a SKU.' => 'Toutes les variantes doivent avoir un SKU.', + 'All' => 'Tous', + 'Allow Checkout Without Payment' => 'Autoriser le passage de commande sans paiement', + 'Allow Empty Cart On Checkout' => 'Autoriser le panier vide au passage de commande', + 'Allow Partial Payment On Checkout' => 'Autoriser le paiement partiel au passage de commande', + 'Allow out of stock purchases' => 'Permettre les achats de produits en rupture de stock', + 'Allow' => 'Autoriser', + 'Allowed Qty' => 'Quantité autorisée', + 'Alternative Phone' => 'Autre téléphone', + 'Amount' => 'Montant', + 'An ID must be provided' => 'Un identifiant doit être fourni', + 'An error occurred while generating this PDF.' => 'Une erreur est survenue lors la création de ce fichier PDF.', + 'Any' => 'N\'importe quel', + 'Anywhere' => 'Partout', + 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Souhaitez-vous réellement archiver l’abonnement "{name}" ? Cette opération N’ANNULERA PAS les abonnements existants.', + 'Are you sure you want to capture this transaction?' => 'Êtes-vous sûr de vouloir collecter cette transaction ?', + 'Are you sure you want to complete this order?' => 'Êtes-vous sûr de vouloir terminer cette commande ?', + 'Are you sure you want to delete the selected orders?' => 'Êtes-vous sûr de vouloir supprimer les commandes sélectionnées ?', + 'Are you sure you want to delete the selected product and its variants?' => 'Êtes-vous sûr de vouloir supprimer le produit sélectionné et ses variantes ?', + 'Are you sure you want to delete this shipping rule?' => 'Êtes-vous sûr de vouloir supprimer cette règle de livraison ?', + 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Êtes-vous sûr de vouloir supprimer « {name} » et tous ses produits ? Veuillez vous assurer que vous avez effectué une sauvegarde de votre base de données avant de réaliser cette action destructrice.', + 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Voulez-vous vraiment supprimer « {name} » ? Cela définira tous les articles avec ce statut sur aucun statut.', + 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Voulez-vous vraiment marquer ce transfert comme étant en attente ? Il apparaîtra comme entrant à la destination.', + 'Are you sure you want to overwrite the billing address?' => 'Êtes-vous sûr de vouloir écraser les adresses de facturation ?', + 'Are you sure you want to overwrite the shipping address?' => 'Êtes-vous sûr de vouloir écraser les adresses d\'expédition ?', + 'Are you sure you want to permanently delete this store and everything in it?' => 'Voulez-vous vraiment définitivement supprimer ce magasin et tout ce qu\'il contient ?', + 'Are you sure you want to refund this transaction?' => 'Êtes-vous sûr de vouloir rembourser cette transaction ?', + 'Are you sure you want to remove this customer?' => 'Êtes-vous sûr de vouloir supprimer ce client ?', + 'Are you sure you want to save this as a new shipping rule?' => 'Êtes-vous sûr de vouloir enregistrer cette règle comme nouvelle règle de livraison ?', + 'Are you sure you want to send email: {name}?' => 'Êtes-vous sûr de vouloir envoyer l\'e-mail : {name} ?', + 'At least one site must be enabled for the product type.' => 'Au moins un site doit être activé pour le type de produit.', + 'Attempted Payments' => 'Tentatives de paiement effectuées', + 'Attention' => 'Attention', + 'Authorize Only (Manually Capture)' => 'Autoriser uniquement (collecter manuellement)', + 'Auto Set Cart Shipping Method Option' => 'Définir automatiquement l\'option du mode d\'expédition', + 'Auto Set New Cart Addresses' => 'Définir automatiquement les nouvelles adresses du panier', + 'Auto Set Payment Source' => 'Définir automatiquement la source du paiement', + 'Automatic SKU Format' => 'Formatage automatique du code article interne', + 'Available Shipping Categories' => 'Catégories de livraison disponibles', + 'Available Tax Categories' => 'Catégories de taxes disponibles', + 'Available for purchase' => 'Disponible à l’achat', + 'Available for purchase?' => 'Disponible à l’achat ?', + 'Available inventory for "{description}" has gone below zero.' => 'L\'inventaire disponible pour « {description} » est passé en dessous de zéro.', + 'Available to Product Types' => 'Disponible dans types de produits', + 'Available' => 'Disponible', + 'Available?' => 'Disponible ?', + 'Average Order Total' => 'Total de commande moyen', + 'Average' => 'Moyenne', + 'BCC’d Recipient' => 'Destinataire en copie cachée', + 'Bad Request' => 'Requête incorrecte', + 'Bad address ID.' => 'Mauvais identifiant d\'adresse.', + 'Bad order ID.' => 'Mauvais identifiant de commande.', + 'Base Price' => 'Prix de base', + 'Base Promotional Price' => 'Prix promotionnel de base', + 'Base Rate' => 'Frais de base', + 'Base' => 'Base', + 'Bcc' => 'Cci', + 'Billing Address' => 'Adresse de facturation', + 'Billing Business Name' => 'Nom commercial pour la facture', + 'Billing First Name' => 'Nom pour la facturation', + 'Billing Full Name' => 'Nom complet pour la facture', + 'Billing Last Name' => 'Nom pour la facturation', + 'Billing address required.' => 'Adresse de facturation requise.', + 'Billing detail update URL' => 'URL de mise à jour des informations de facturation', + 'Billing issues' => 'Problèmes de facturation', + 'Billing' => 'Facturation', + 'Both (Line item price + Line item shipping costs)' => 'Les deux (Prix de l’article + Frais de livraison de l’article)', + 'Business ID' => 'Numéro d\'identification de l\'entreprise', + 'Business Name' => 'Nom commercial', + 'Business Tax ID' => 'Identifiant fiscal de l\'entreprise (ex. numéro de TVA intracommunautaire)', + 'CC’d Recipient' => 'Destinataire en copie', + 'CVV' => 'CVV', + 'Can be used as an internal reference.' => 'Peut être utilisé comme référence interne.', + 'Can not complete payment for missing transaction.' => 'Impossible de finaliser le paiement pour la transaction manquante.', + 'Can not create a new order' => 'Impossible de créer une nouvelle commande', + 'Can not find an order to pay.' => 'Impossible de trouver une commande à payer.', + 'Can not find enabled email.' => 'Impossible de trouver l\'e-mail activé.', + 'Can not find order' => 'Impossible de trouver la commande', + 'Can not find order.' => 'Impossible de trouver la commande.', + 'Can not find the transaction to refund' => 'Impossible de trouver la transaction à rembourser', + 'Can not move between these inventory types.' => 'Impossible de se déplacer entre ces types d\'inventaire.', + 'Can not refund amount greater than the remaining amount' => 'Impossible de rembourser un montant supérieur au montant restant', + 'Cancel subscription' => 'Annuler l’abonnement', + 'Cancel with gateway now' => 'Annuler avec la passerelle maintenant', + 'Cancel' => 'Annuler', + 'Cancellation date' => 'Date d’annulation', + 'Cancellation' => 'Annulation', + 'Cannot switch plans for this subscription.' => 'Cet abonnement ne peut pas être basculé vers un autre abonnement.', + 'Can’t preview this email.' => 'Impossible de prévisualiser cet e-mail.', + 'Capture payment' => 'Capturer le paiement', + 'Capture' => 'Collecter', + 'Card Holder' => 'Titulaire de la carte', + 'Card Number' => 'Numéro de carte', + 'Card' => 'Carte', + 'Cart Recovery Link' => 'Lien de récupération du panier', + 'Cart forgotten.' => 'Panier oublié.', + 'Cart updated.' => 'Panier mis à jour.', + 'Cart {number}' => 'Panier {number}', + 'Catalog Pricing Rule' => 'Règle de tarification du catalogue', + 'Catalog pricing rule description.' => 'Description de la règle de tarification du catalogue.', + 'Catalog pricing rule saved.' => 'Règle de tarification du catalogue enregistrée.', + 'Catalog pricing rules deleted.' => 'Règles de tarification du catalogue supprimées.', + 'Catalog pricing rules updated.' => 'Mise à jour des règles de tarification du catalogue.', + 'Categories Relationship Type' => 'Type de relation des catégories', + 'Categories' => 'Catégories', + 'Category Rate Overrides' => 'Dépassements des taux de catégorie', + 'Centimeters (cm)' => 'Centimètres (cm)', + 'Changing this value may affect your ability to refund existing transactions.' => 'La modification de cette valeur peut affecter votre capacité à rembourser les transactions existantes.', + 'Choose a color to represent the order’s status' => 'Choisissez une couleur pour représenter le statut de la commande', + 'Choose a new customer' => 'Choisissez un nouveau client', + 'Choose adjustment values to include when calculating the product revenue total.' => 'Choisissez les valeurs d\'ajustement à inclure lors du calcul du total des revenus du produit.', + 'Choose the currency’s ISO code.' => 'Sélectionner le code ISO de la devise.', + 'Choose the destination inventory location for the existing on hand stock.' => 'Choisissez l\'emplacement de l\'inventaire de destination pour le stock existant.', + 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Choisir les sites pour lesquels ce type de produit sera disponible et configurer les paramètres spécifiques aux sites.', + 'City' => 'Ville', + 'Clear counter' => 'Effacer le compteur', + 'Clear notices' => 'Effacer les avis', + 'Close' => 'Fermer', + 'Code' => 'Code', + 'Collated PDF' => 'PDF unique', + 'Color' => 'Couleur', + 'Commerce Products' => 'Produits de Commerce', + 'Commerce Settings' => 'Paramètres de Craft Commerce', + 'Commerce Variants' => 'Variantes Commerce', + 'Commerce email “{email}” could not be sent for order “{order}”.' => 'L’e-mail de Commerce « {email} » n’a pas pu être envoyé pour la commande « {order} ».', + 'Commerce order exports' => 'Exportations de commandes Commerce', + 'Commerce' => 'Commerce', + 'Committed' => 'Validé', + 'Completed Email' => 'E-mail terminé', + 'Completed' => 'Terminé', + 'Completing order failed.' => 'Échec de l\'exécution de la commande.', + 'Condition' => 'Condition', + 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Les conditions sont comparées à une commande avant d\'examiner les règles. Cette fonction est utile si vous souhaitez vérifier la disponibilité d\'une méthode à un stade précoce ou s\'il existe des conditions communes à toutes les règles relatives à cette méthode.', + 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Les conditions sont comparées à la commande du client avant d\'examiner les règles. Cette fonction est utile si vous souhaitez vérifier la disponibilité d\'une méthode à un stade précoce ou s\'il existe des conditions communes à toutes les règles relatives à cette méthode.', + 'Conditions' => 'Conditions', + 'Contains Purchasables' => 'Contient des articles disponibles à l\'achat', + 'Control Panel Settings' => 'Paramètres du panneau de contrôle', + 'Control panel' => 'Panneau de contrôle', + 'Conversion Rate' => 'Taux de conversion', + 'Converted Price' => 'Prix converti', + 'Copied!' => 'Copié !', + 'Copy the URL' => 'Copier l\'URL', + 'Copy to {location}' => 'Copier vers {location}', + 'Copy' => 'Copier', + 'Costs' => 'Coûts', + 'Could not archive gateway.' => 'Impossible d’archiver le portail.', + 'Could not cancel “{reference}”.' => 'Échec de l\'annulation de « {reference} ».', + 'Could not create the payment source.' => 'Impossible de créer la source de paiement.', + 'Could not delete shipping rule' => 'Impossible de supprimer la règle de livraison', + 'Could not delete shipping zone' => 'Impossible de supprimer la zone de livraison', + 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Impossible de supprimer {count, number} {count, plural,one{catégorie} other{catégories}} de livraison.', + 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Impossible de supprimer {count, number} {count, plural,one{mode} other{modes}} et règles de livraison.', + 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Impossible de supprimer {count, number} {count, plural,one{catégorie} other{catégories}} de taxes.', + 'Could not find the email or template.' => 'E-mail ou modèle introuvable.', + 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Impossible de marquer la commande {number} comme finalisée. L’enregistrement de la commande a échoué au cours de la finalisation avec des erreurs : {order}', + 'Could not reactivate “{reference}”.' => 'Échec de la réactivation de « {reference} ».', + 'Could not send email' => 'Impossible d\'envoyer l\'e-mail', + 'Could not switch “{reference}” to “{plan}”.' => 'Impossible de passe « {reference} » à « {plan} ».', + 'Could not update orders address.' => 'Impossible de mettre à jour l\'adresse des commandes.', + 'Couldn’t archive Line Item Status.' => 'Impossible d\'archiver le statut de l\'article.', + 'Couldn’t archive Order Status.' => 'Impossible d\'archiver le statut de commande.', + 'Couldn’t capture transaction.' => 'Impossible de collecter la transaction.', + 'Couldn’t capture transaction: {message}' => 'Impossible de collecter la transaction : {message}', + 'Couldn’t delete email.' => 'Impossible de supprimer l\'e-mail.', + 'Couldn’t delete the payment source.' => 'Impossible de supprimer la source de paiement.', + 'Couldn’t get order.' => 'Impossible d\'obtenir la commande.', + 'Couldn’t recalculate order.' => 'Impossible de recalculer la commande.', + 'Couldn’t refund transaction.' => 'Impossible de rembourser la transaction.', + 'Couldn’t refund transaction: {message}' => 'Impossible de rembourser la transaction : {message}', + 'Couldn’t reorder Line Item Statuses.' => 'Nous n\'avons pas pu réorganiser le statut des articles.', + 'Couldn’t reorder Order Statuses.' => 'Nous n\'avons pas pu réorganiser le statut des commandes.', + 'Couldn’t reorder PDFs.' => 'Impossible de réorganiser les PDF.', + 'Couldn’t reorder discounts.' => 'Nous n\'avons pas pu réorganiser les réductions.', + 'Couldn’t reorder gateways.' => 'Impossible de réorganiser les portails.', + 'Couldn’t reorder plans.' => 'Impossible de réorganiser les abonnements.', + 'Couldn’t reorder rules.' => 'Impossible de réorganiser les règles.', + 'Couldn’t reorder sale.' => 'Impossible de réorganiser la promotion.', + 'Couldn’t reorder sales.' => 'Impossible de réorganiser les ventes.', + 'Couldn’t reorder statuses.' => 'Impossible de réorganiser les statuts.', + 'Couldn’t reorder stores.' => 'Impossible de commander à nouveau dans les magasins.', + 'Couldn’t save PDF.' => 'Impossible d’enregistrer le PDF.', + 'Couldn’t save catalog pricing rule.' => 'Impossible d\'enregistrer la règle de tarification du catalogue.', + 'Couldn’t save currency.' => 'La devise n\'a pas pu être enregistrée.', + 'Couldn’t save discount.' => 'Impossible d\'enregistrer la remise.', + 'Couldn’t save email.' => 'Impossible d\'enregistrer l\'e-mail.', + 'Couldn’t save gateway.' => 'Impossible d’enregistrer le portail.', + 'Couldn’t save inventory location.' => 'Impossible d\'enregistrer l\'emplacement de l\'inventaire.', + 'Couldn’t save line item status.' => 'Impossible d\'enregistrer le statut de l\'article.', + 'Couldn’t save order fields.' => 'Impossible d’enregistrer les champs de commande.', + 'Couldn’t save order status.' => 'Impossible d\'enregistrer le statut de commande.', + 'Couldn’t save order.' => 'Impossible d\'enregistrer la commande.', + 'Couldn’t save product type.' => 'Impossible d\'enregistrer le type de produit.', + 'Couldn’t save sale.' => 'Impossible d\'enregistrer la promotion.', + 'Couldn’t save settings.' => 'Impossible d\'enregistrer les paramètres.', + 'Couldn’t save shipping category.' => 'Nous n\'avons pas pu enregistrer cette catégorie de livraison.', + 'Couldn’t save shipping method.' => 'Impossible d\'enregistrer le mode de livraison.', + 'Couldn’t save shipping rule.' => 'Impossible d\'enregistrer la règle de livraison.', + 'Couldn’t save shipping zone.' => 'Impossible d\'enregistrer la zone de livraison.', + 'Couldn’t save store.' => 'Impossible d\'enregistrer la boutique.', + 'Couldn’t save subscription fields.' => 'Impossible d’enregistrer les champs d\'abonnement.', + 'Couldn’t save subscription plan.' => 'Impossible d’enregistrer l’abonnement.', + 'Couldn’t save subscription.' => 'Impossible d’enregistrer l’abonnement.', + 'Couldn’t save tax category.' => 'Impossible d\'enregistrer la catégorie de taxe.', + 'Couldn’t save tax rate.' => 'Impossible d\'enregistrer le taux de taxe.', + 'Couldn’t save tax zone.' => 'Impossible d\'enregistrer la zone de taxe.', + 'Couldn’t save transfer fields.' => 'Impossible d’enregistrer les champs de transfert.', + 'Couldn’t update catalog pricing rule statuses.' => 'Impossible de mettre à jour le statut des règles de tarification du catalogue.', + 'Couldn’t update status.' => 'Impossible de mettre à jour l\'état.', + 'Couldn’t updated sales status.' => 'Impossible de mettre à jour l\'état des ventes.', + 'Country Code of Origin' => 'Code du pays d\'origine', + 'Country List' => 'Liste des pays', + 'Country not allowed.' => 'Pays non autorisé.', + 'Country' => 'Pays', + 'Coupon Code' => 'Code promotionnel', + 'Coupon can not apply discount to this order due to address mismatch.' => 'Le coupon ne peut pas être utilisé pour appliquer une réduction à cette commande, car l\'adresse ne correspond pas.', + 'Coupon can not apply discount to this order due to customer mismatch.' => 'Le coupon ne peut pas être utilisé pour appliquer une réduction à cette commande, car le client ne correspond pas.', + 'Coupon can not apply discount to this order.' => 'Le coupon ne peut pas être utilisé pour appliquer une réduction à cette commande.', + 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Le code promotionnel « {code} » est déjà utilisé par la réduction « {name} ».', + 'Coupon codes cannot be blank.' => 'Les codes de coupons ne peuvent pas être vides.', + 'Coupon codes must be unique.' => 'Les codes de coupons doivent être uniques.', + 'Coupon format is required and must contain at least one `#`.' => 'Le format du coupon est requis et doit contenir au moins un « # ».', + 'Coupon not valid.' => 'Coupon invalide.', + 'Coupon removed: {explanation}' => 'Coupon supprimé : {explanation}', + 'Coupons' => 'Coupons', + 'Craft Commerce - Administration' => 'Craft Commerce - Administration', + 'Craft Commerce - Inventory' => 'Craft Commerce - Inventaire', + 'Craft Commerce - Orders' => 'Craft Commerce - Commandes', + 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Type de produit - {name}', + 'Craft Commerce - Subscriptions' => 'Craft Commerce - Abonnements', + 'Create a Discount' => 'Créer une remise', + 'Create a Subscription Plan' => 'Créer un abonnement', + 'Create a new PDF' => 'Créer un nouveau PDF', + 'Create a new catalog pricing rule' => 'Créer une nouvelle règle de tarification du catalogue', + 'Create a new currency' => 'Créer une nouvelle devise', + 'Create a new email' => 'Créer un nouvel e-mail', + 'Create a new gateway' => 'Créer un nouveau portail', + 'Create a new line item status' => 'Créer un nouveau statut d\'article', + 'Create a new order status' => 'Créer un nouveau statut de commande', + 'Create a new product type' => 'Créer un nouveau type de produit', + 'Create a new sale' => 'Créer une nouvelle promotion', + 'Create a new shipping category' => 'Créer une nouvelle catégorie de livraison', + 'Create a new shipping method' => 'Créer un nouveau mode de livraison', + 'Create a new shipping rule' => 'Créer une nouvelle règle de livraison', + 'Create a new tax category' => 'Créer une nouvelle catégorie de taxe', + 'Create a new tax rate' => 'Créer un nouveau taux de taxe', + 'Create a product type' => 'Créer un type de produit', + 'Create a shipping zone' => 'Créer une zone de livraison', + 'Create a tax zone' => 'Créer une nouvelle zone de taxe', + 'Create catalog pricing rules' => 'Créer des règles de tarification du catalogue', + 'Create customer: “{email}”' => 'Créer le client : « {email} »', + 'Create discounts' => 'Créer des remises', + 'Create discount…' => 'Créer une remise…', + 'Create rules that allow this discount to match the order.' => 'Créez des règles qui permettent à cette remise de correspondre à la commande.', + 'Create rules that allow this discount to match the order’s billing address.' => 'Créez des règles qui permettent à cette remise de correspondre à l\'adresse de facturation de la commande.', + 'Create rules that allow this discount to match the order’s customer.' => 'Créez des règles qui permettent à cette remise de correspondre au client de la commande.', + 'Create rules that allow this discount to match the order’s shipping address.' => 'Créez des règles qui permettent à cette remise de correspondre à l\'adresse d\'expédition de la commande.', + 'Create rules that allow this gateway to match the billing address.' => 'Créez des règles qui permettent à ce portail de faire correspondre l\'adresse de facturation.', + 'Create rules that allow this gateway to match the order.' => 'Créez des règles qui permettent à ce portail de correspondre à la commande.', + 'Create rules that allow this gateway to match the shipping address.' => 'Créez des règles qui permettent à ce portail de faire correspondre l\'adresse de livraison.', + 'Create sales' => 'Créer des promotions', + 'Create sale…' => 'Créer une promotion…', + 'Created' => 'Créé', + 'Credit Card Payment Type' => 'Type de paiement par carte de crédit', + 'Currency Code' => 'Code monétaire', + 'Currency saved.' => 'Devise enregistrée.', + 'Currency' => 'Devise', + 'Current' => 'Actuel', + 'Custom 1' => 'Personnalisé 1', + 'Custom 2' => 'Personnalisé 2', + 'Custom 3' => 'Personnalisé 3', + 'Custom 4' => 'Personnalisé 4', + 'Custom' => 'Personnalisé', + 'Customer Enabled?' => 'Activé pour le client ?', + 'Customer ID is required.' => 'Un ID client est obligatoire.', + 'Customer Note' => 'Note client ', + 'Customer Notices' => 'Avis aux clients', + 'Customer data' => 'Données de clients', + 'Customer' => 'Client', + 'Damaged' => 'Endommagé', + 'Data shown might be outdated.' => 'Les données présentées peuvent être obsolètes.', + 'Date Authorized' => 'Date d\'autorisation', + 'Date Created' => 'Date de création', + 'Date First Paid' => 'Date du premier paiement', + 'Date Ordered' => 'Date de la commande', + 'Date Paid' => 'Date du paiement', + 'Date Updated' => 'Date de mise à jour', + 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Date à partir de laquelle la règle de tarification du catalogue sera active. Laissez l\'espace vide si vous voulez que la date de début ne soit pas définie', + 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Date à partir de laquelle la remise sera active. Laisser vide si vous voulez que la date de départ ne soit pas définie.', + 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Date à partir de laquelle la promotion sera active. Laisser vide si vous voulez que la date de départ ne soit pas définie.', + 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Date à laquelle la règle de tarification du catalogue sera terminée. Laissez l\'espace vide si vous voulez que la date de fin ne soit pas définie', + 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Date à laquelle la remise ne sera plus effective. Laisser vide si vous voulez que la date de fin ne soit pas définie', + 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Date à laquelle la promotion sera terminée. Laisser vide si vous voulez que la date de fin ne soit pas définie.', + 'Date' => 'Date', + 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Par défaut - Permet au prix d\'être négatif si les remises sont supérieures à la valeur de la commande.', + 'Default Category' => 'Catégorie par défaut', + 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Vue par défaut du panneau de contrôle de Commerce. Si l\'utilisateur n\'a pas la permission, il se rabattra sur un emplacement auquel il peut accéder.', + 'Default Order PDF' => 'PDF de commande par défaut', + 'Default Per Item Rate' => 'Frais par article par défaut', + 'Default Percentage Rate' => 'Frais en pourcentage par défaut', + 'Default Status?' => 'Statut par défaut ?', + 'Default View' => 'Vue par défaut', + 'Default Weight Rate' => 'Frais par poids par défaut', + 'Default Zone' => 'Zone par défaut ', + 'Default status?' => 'Statut par défaut ?', + 'Default to this tax zone when no billing address is set' => 'Définir cette zone fiscale comme zone par défaut en l’absence d’adresse de facturation', + 'Default to this tax zone when no shipping address is set' => 'Utiliser par défaut cette zone de taxe lorsqu\'aucune adresse de livraison n\'est définie', + 'Default variant updated.' => 'Variante par défaut mise à jour.', + 'Default' => 'Par défaut', + 'Default?' => 'Par défaut ?', + 'Delete catalog pricing rules' => 'Supprimer les règles de tarification du catalogue', + 'Delete discounts' => 'Supprimer les remises', + 'Delete orders' => 'Supprimer les commandes', + 'Delete sales' => 'Supprimer les promotions', + 'Delete' => 'Supprimer', + 'Deleting the {location} location.' => 'Suppression de l\'emplacement {location}.', + 'Describe this rule.' => 'Décrire cette règle.', + 'Describe this shipping zone.' => 'Décrire cette zone de livraison.', + 'Describe this tax zone.' => 'Décrire cette zone de taxe.', + 'Description' => 'Description', + 'Destination Inventory Location' => 'Emplacement de l\'inventaire de destination', + 'Destination' => 'Destination', + 'Details' => 'Détails', + 'Dimension Unit' => 'Unité de mesure', + 'Dimensions' => 'Dimensions', + 'Disabled' => 'Désactivé', + 'Disallow' => 'Refuser', + 'Discount all line items' => 'Faire une réduction sur tous les articles', + 'Discount description.' => 'Description de la remise', + 'Discount is not allowed for the order' => 'La réduction n\'est pas autorisée pour cette commande', + 'Discount is out of date.' => 'La remise est échue.', + 'Discount saved.' => 'Remise enregistrée.', + 'Discount the matching items only' => 'Ne faire de remise que sur les articles correspondants', + 'Discount use has reached its limit.' => 'L’utilisation de la remise a atteint son plafond.', + 'Discount' => 'Remise', + 'Discounted Item Subtotal' => 'Sous-total des articles à prix réduit', + 'Discounted Items' => 'Articles en réduction', + 'Discounts deleted.' => 'Rabais supprimés.', + 'Discounts reordered.' => 'Remises réordonnées.', + 'Discounts updated.' => 'Remises mises à jour.', + 'Discounts' => 'Remises', + 'Disqualify with valid business tax ID?' => 'Disqualifier avec un numéro fiscal d\'entreprise valide ?', + 'Do not apply subsequent matching sales beyond applying this sale.' => 'Ne pas appliquer d’autres ventes correspondantes après l’application de cette vente.', + 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Ne pas appliquer ce taux si l\'adresse de la commande comporte l\'un des numéros de taxe professionnelle valides sélectionnés.', + 'Do not attach a PDF to this email' => 'Ne pas attacher de PDF à cet e-mail', + 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Ne pas recalculer la commande (Numéro : {orderNumber}) si des erreurs sont présentes.', + 'Donation can not be zero.' => 'Un don ne peut pas être nul.', + 'Donation needs to be an amount.' => 'Le don doit être un montant.', + 'Donation settings saved.' => 'Paramètres de don enregistrés.', + 'Donation' => 'Don', + 'Donations' => 'Dons', + 'Done' => 'Terminé', + 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Ne pas appliquer de réductions supplémentaires à cette commande si cette réduction est elle-même appliquée', + 'Download PDF' => 'Télécharger le PDF', + 'Download PDF…' => 'Télécharger le PDF…', + 'Download Type' => 'Type de téléchargement', + 'Download' => 'Télécharger', + 'Draft' => 'Brouillon', + 'Dummy gateway payment failed.' => 'Le paiement de la passerelle factice a échoué.', + 'Duplicate options exist' => 'Il y a des options en double', + 'Duration' => 'Durée', + 'EU VAT ID' => 'N° TVA DE L\'UE', + 'Edit address' => 'Éditer l\'adresse', + 'Edit adjustments' => 'Modifier les ajustements', + 'Edit catalog pricing rules' => 'Modifier les règles de tarification du catalogue', + 'Edit discounts' => 'Modifier les remises', + 'Edit options' => 'Modifier les options', + 'Edit orders' => 'Modifier les commandes', + 'Edit sales' => 'Modifier les promotions', + 'Edit' => 'Modifier', + 'Effect' => 'Effet', + 'Either (Default) - The relationship field is on the purchasable or the category' => 'Soit (par défaut) - Le champ de relation est dans le champ achetable ou la catégorie', + 'Either way' => 'Dans tous les cas', + 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Erreur de génération de PDF d\'e-mail pour l\'e-mail « {email} ». Commande : « {order} ». Erreur de modèle PDF : « {message} » {file} : {line}', + 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'Il n’existe pas de template PDF d’e-mail à l’emplacement « {templatePath} » pour l’e-mail « {email} ». Commande : « {order} ».', + 'Email Subject' => 'Objet de l\'email', + 'Email error. No email address found for order. Order: “{order}”' => 'Erreur d\'e-mail. Aucune adresse e-mail n\'a été trouvée pour cette commande. Commande : « {order} »', + 'Email is not enabled.' => 'L\'e-mail n\'est pas activé.', + 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Comme il n’existe pas de modèle d’e-mail en texte brut à l’emplacement « {templatePath} », le chemin « {templateParsedPath} » a donc été généré pour l’e-mail « {email} ». Commande : « {order} ».', + 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail en texte brut « {email} ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail pour l\'e-mail « {email} » dans « Chemin du modèle ». Commande : « {order} ». Erreur de modèles : « {message} » {file} : {line}', + 'Email required to make payments on a completed order.' => 'E-mail requis pour effectuer des paiements sur une commande finalisée.', + 'Email saved.' => 'E-mail enregistré.', + 'Email sent' => 'E-mail envoyé', + 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Comme il n’existe pas de modèle d’e-mail à l’emplacement « {templatePath} », le chemin « {templateParsedPath} » a donc été généré pour l’e-mail « {email} ». Commande : « {order} ».', + 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail personnalisé « {email} » dans « À : » « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail pour l\'e-mail {email} dans « Cci : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail pour l\'e-mail {email} dans « CC : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail pour l\'e-mail {email} dans « Répondre à : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail pour l\'e-mail {email} dans « Objet : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail « {email} ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', + 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail pour l\'e-mail « {email} » dans « Chemin du modèle ». Commande : « {order} ». Erreur de modèles : « {message} » {file} : {line}', + 'Email unavailable.' => 'E-mail indisponible.', + 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'L\'e-mail « {email} » n\'a pu être envoyé pour la commande « {order} ». Erreur : {error} {file} : {line}', + 'Email “{email}” for order {order} was cancelled.' => 'L\'e-mail « {email} » pour la commande « {order} » a été annulé.', + 'Email' => 'E-mail', + 'Emails' => 'E-mails', + 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Activer si ce taux doit être intégré au prix de l\'objet imposable au lieu d\'ajouter un coût à la commande.', + 'Enable structure for products of this type' => 'Activer la structure pour les produits de ce type', + 'Enable this discount' => 'Activer cette remise', + 'Enable this rule' => 'Activer cette règle', + 'Enable this sale' => 'Activer cette promotion', + 'Enable this shipping method on the front end' => 'Activer ce mode de livraison sur le site web', + 'Enable this shipping rule' => 'Activer cette règle de livraison', + 'Enable this tax rate' => 'Activer ce taux de taxe', + 'Enabled for customers to select during checkout?' => 'Activé pour une sélection par les clients lors du paiement ?', + 'Enabled for customers to select?' => 'Possibilité de sélection par les clients ?', + 'Enabled' => 'Activé', + 'Enabled?' => 'Actif ?', + 'End Date' => 'Date de fin', + 'Enter SKU' => 'Entrer le code article interne', + 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Saisir un nom convivial pour ce taux d\'imposition, qui sera utilisé dans le panneau de configuration.', + 'Enter a percentage like {ex1} or {ex2}.' => 'Entrer un pourcentage comme {ex1} ou {ex2}.', + 'Enter coupon code' => 'Entrer le code du coupon', + 'Enter reference' => 'Entrer la référence', + 'Error refunding transaction: {transactionHash}' => 'Erreur lors du remboursement de la transaction : {transactionHash}', + 'Every new store must be assigned to at least one site.' => 'Chaque nouveau point de vente doit être affecté à au moins un site.', + 'Everywhere' => 'Partout', + 'Example' => 'Exemple', + 'Exclude this discount for products that are already on promotion' => 'Exclure cette remise pour les produits déjà en promotion', + 'Expired Link' => 'Lien expiré', + 'Expired' => 'A expiré', + 'Expiry Date' => 'Date d\'échéance', + 'Expiry date' => 'Date d’expiration', + 'Expiry' => 'Expiration', + 'Failed to receive transfer: {error}' => 'Échec de la réception du transfert : {error}', + 'Failed to send email. Please try again.' => 'Échec de l\'envoi de l\'e-mail. Veuillez réessayer.', + 'Failed to start' => 'Échec du démarrage', + 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Échec de la mise à jour {num, plural, =1{du statut de la commande} other{des statuts des commandes}}.', + 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Échec de la mise à jour du statut de {num, plural, =1{la commande} other{des commandes}}.', + 'Feet (ft)' => 'Pieds (ft)', + 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Les conditions de filtrage qui décrivent pour quelles commandes cette règle est applicable. Entrez 0 pour ignorer une condition.', + 'First Name' => 'Prénom', + 'Flat Amount Off Order' => 'Montant fixe de remise sur la commande', + 'Flat Order Discount Amount Off' => 'Montant de la remise forfaitaire sur la commande', + 'Free Order Payment Strategy' => 'Stratégie de paiement des commandes gratuites', + 'Free Shipping' => 'Livraison gratuite', + 'Free orders are processed by the payment gateway' => 'Les commandes gratuites sont traitées par le portail de paiement', + 'Free orders complete immediately' => 'Les commandes gratuites s\'exécutent immédiatement', + 'Free shipping can only be for whole order or matching items, not both.' => 'La livraison gratuite ne peut concerner que l\'ensemble de la commande ou les articles correspondants, pas les deux.', + 'From Name' => 'Nom de l\'expéditeur', + 'Fulfill' => 'Réaliser', + 'Fulfilled' => 'Réalisé', + 'Fulfillment' => 'Réalisation', + 'Full Name' => 'Nom complet', + 'Gateway Code' => 'Code portail', + 'Gateway Message' => 'Message du portail', + 'Gateway Reference' => 'Référence de portail', + 'Gateway Response' => 'Réponse du portail', + 'Gateway doesn’t support authorize' => 'La passerelle de paiement ne supporte pas l\'autorisation', + 'Gateway doesn’t support partial refunds.' => 'Le portail ne prend pas en charge les remboursements partiels.', + 'Gateway doesn’t support purchase' => 'La passerelle de paiement ne supporte pas l\'achat', + 'Gateway doesn’t support refunds.' => 'Le portail ne prend pas en charge les remboursements.', + 'Gateway saved.' => 'Portail enregistré.', + 'Gateway' => 'Passerelle de paiement', + 'Gateways reordered.' => 'Portails réordonnés.', + 'Gateways' => 'Portails', + 'General Settings' => 'Paramètres généraux', + 'General' => 'Général', + 'Generate' => 'Générer', + 'Generated Coupon Format' => 'Format des coupons générés', + 'Grams (g)' => 'Grammes (g)', + 'Groups for which this sale will be applicable to.' => 'Groupes auxquels cette promotion s’appliquera.', + 'HTML Email Template Path' => 'Chemin du modèle d\'email HTML', + 'Handle' => 'Traiter', + 'Harmonized System Code' => 'Code du système harmonisé', + 'Has Admin Notices' => 'Contient des avis de l\'administrateur', + 'Has Emails?' => 'A des emails ?', + 'Has Free Shipping' => 'Possède la livraison gratuite', + 'Has Orders' => 'Comprend des commandes', + 'Has Purchasable' => 'Comprend des articles achetables', + 'Has Variants?' => 'Possède des variantes ?', + 'Height ({unit})' => 'Hauteur ({unit})', + 'Height' => 'Hauteur', + 'Hide snapshot' => 'Masquer l\'instantané', + 'History' => 'Historique', + 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Combien de temps (en secondes) un lien de téléchargement PDF doit rester valide avant d\'expirer. La valeur par défaut est 86 400 (24 heures).', + 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Le nombre de fois qu\'une adresse email donnée peut utiliser cette remise. Cela s\'applique à toutes les commandes précédentes, passées comme invité ou comme utilisateur. Mettez zéro (0) pour une utilisation illimitée par les invités ou les utilisateurs enregistrés.', + 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Combien de fois un utilisateur est autorisé à utiliser cette remise. Si cette valeur est différente de zéro, la remise ne sera disponible que pour les utilisateurs connectés.', + 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Le nombre maximal d\'utilisations de cette remise par des invités ou des utilisateurs connectés. Mettez cette valeur à zéro pour permettre une utilisation illimitée.', + 'How products should be labeled within the control panel.' => 'Comment les produits doivent être étiquetés dans le panneau de contrôle.', + 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'La relation entre les biens à acheter et les catégories, qui détermine les articles correspondants. Voir [Terminologie relations]({link}).', + 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'La façon dont ce produit sera décrit sur le poste d\'une commande. Vous pouvez inclure des tags qui décrivent des propriétés comme {ex1} ou {ex2}', + 'How this shipping method will be referred to in templates and forms.' => 'La façon dont vous allez faire référence à ce mode de livraison dans les modèles et formulaires.', + 'How variants should be labeled within the control panel.' => 'Comment les variantes doivent être étiquetées dans le panneau de contrôle.', + 'How you’ll refer to this PDF in the templates.' => 'La façon dont vous allez faire référence à ce PDF dans les modèles.', + 'How you’ll refer to this product type in the templates.' => 'La façon dont vous allez faire référence à ce type de produit dans les modèles.', + 'How you’ll refer to this shipping category in the templates.' => 'La façon dont vous vous référerez à cette catégorie de livraison dans les modèles.', + 'How you’ll refer to this status in the templates.' => 'La façon dont vous allez faire référence à ce statut dans les modèles.', + 'How you’ll refer to this subscription plan in the templates.' => 'Indique comment vous allez faire référence à cet abonnement dans les templates.', + 'How you’ll refer to this tax category in the templates.' => 'La façon dont vous allez faire référence à cette catégorie de taxe dans les modèles.', + 'ID' => 'Identifiant', + 'IP Address' => 'Adresse IP', + 'If disabled, this PDF will not be available or sent with emails.' => 'S\'il est désactivé, ce PDF ne sera pas disponible ou envoyé avec des e-mails.', + 'If disabled, this email will not send.' => 'S’il est désactivé, cet email ne sera pas envoyé.', + 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Si cette option est activée et que ce tarif ne correspond pas à la commande, le taux sera supprimé du prix de l\'article dans le panier.', + 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Si configuré sur Autoriser Uniquement, vous devrez collecter les règlements manuellement avant que les fonds ne soient transférés sur votre compte. La passerelle de paiement doit supporter l\'option sélectionnée.', + 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Si vous choisissez le pourcentage « de réduction sur l\'article en promotion », cela inclura le « montant par article » ainsi que toute autre promotion appliquée avant celle-ci.', + 'Ignore Promotions?' => 'Ignorer les promotions ?', + 'Ignore previous matching sales if this sale matches.' => 'Ignorer les ventes correspondantes précédentes si cette vente correspond.', + 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignorer les prix promotionnels lorsque cette remise est appliquée aux articles correspondants', + 'Inactive Carts' => 'Paniers inactifs', + 'Inches (in)' => 'Pouces (in)', + 'Include built-in line item tax.' => 'Inclure la taxe intégrée sur le type d\'article.', + 'Include in price?' => 'Inclure dans le prix ?', + 'Include line item discounts.' => 'Inclure les rabais par type d\'article.', + 'Include line item shipping costs.' => 'Inclure les coûts d\'envoi par type d\'article.', + 'Include separate line item tax.' => 'Inclure une ligne distincte pour la taxe par type d\'article.', + 'Included in price?' => 'Inclus dans le prix ?', + 'Included' => 'Inclus', + 'Incoming transfer from Transfer ID: ' => 'Transfert entrant à partir de l\'ID de transfert : ', + 'Incoming' => 'Entrant', + 'Info' => 'Info', + 'Information linked?' => 'Des informations sont-elles associées ?', + 'Information' => 'Informations', + 'Invalid JSON' => 'JSON invalide', + 'Invalid Order ID' => 'ID de commande invalide', + 'Invalid VAT ID.' => 'ID TVA invalide.', + 'Invalid condition syntax' => 'Syntaxe conditionnelle invalide', + 'Invalid email.' => 'E-mail invalide.', + 'Invalid formula syntax' => 'Syntaxe de formule invalide', + 'Invalid gateway: {value}' => 'Portail non valide : {value}', + 'Invalid inventory movements.' => 'Mouvements d\'inventaire non valides.', + 'Invalid order condition syntax.' => 'Syntaxe conditionnelle de commande invalide.', + 'Invalid payment or order. Please review.' => 'Paiement ou commande non valide. Veuillez vérifier votre saisie.', + 'Invalid payment source ID: {value}' => 'ID de la source de paiement non valide : {value}', + 'Invalid store.' => 'Magasin non valide.', + 'Invalid user.' => 'Utilisateur non valide.', + 'Inventory Item' => 'Article d\'inventaire', + 'Inventory Location' => 'Emplacement de l\'inventaire', + 'Inventory Locations' => 'Emplacements de l\'inventaire', + 'Inventory Tracked' => 'Inventaire suivi', + 'Inventory Transfers' => 'Transferts d\'inventaire', + 'Inventory could not be set.' => 'L\'inventaire n\'a pas pu être établi.', + 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'L\'emplacement de l\'inventaire a un stock validé, la commande ou les commandes doivent d\'abord être exécutées.', + 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'L\'emplacement de l\'inventaire a un stock entrant, le ou les transferts doivent d\'abord être effectués.', + 'Inventory location is already deactivated.' => 'L\'emplacement de l\'inventaire est déjà désactivé.', + 'Inventory location saved.' => 'Emplacement de l\'inventaire enregistré.', + 'Inventory locations not saved.' => 'Emplacements de l\'inventaire non enregistrés.', + 'Inventory movement could not be saved.' => 'Le mouvement de l\'inventaire n\'a pas pu être enregistré.', + 'Inventory movement saved.' => 'Mouvement d\'inventaire enregistré.', + 'Inventory updated.' => 'Inventaire mis à jour.', + 'Inventory was not updated.' => 'L\'inventaire n\'a pas été mis à jour.', + 'Inventory' => 'Inventaire', + 'Invoice amount' => 'Montant de la facture', + 'Invoice date' => 'Date de la facture', + 'Is Promotable' => 'Peut être promu', + 'Is Promotional Price?' => 'S\'agit-il d\'un prix promotionnel ?', + 'Is Shippable' => 'Peut être expédié', + 'Is Taxable' => 'Peut être taxé', + 'Item Rates' => 'Taux de l\'article', + 'Item Subtotal' => 'Sous-total de l\'article', + 'Item Total' => 'Total de l\'article', + 'Item' => 'Article', + 'Items' => 'Articles', + 'Kilograms (kg)' => 'Kilogrammes (kg)', + 'Label' => 'Étiquette', + 'Landscape' => 'Paysage', + 'Language' => 'Langue', + 'Last Name' => 'Nom de famille', + 'Last Updated' => 'Dernière mise à jour', + 'Leave a category rate override blank to use the rate from above.' => 'Laissez un taux de catégorie vide pour utiliser le taux ci-dessus.', + 'Leave blank for unlimited uses.' => 'Laisser vide pour une utilisation illimitée.', + 'Leave blank if products don’t have URLs' => 'Laissez la zone vide si les produits n’ont pas d’URL', + 'Leave gateway subscription as-is' => 'Laisser l\'abonnement au portail tel quel', + 'Length ({unit})' => 'Longueur ({unit})', + 'Length' => 'Longueur', + 'Let each product choose which sites it should be saved to' => 'Laissez chaque produit choisir dans quels sites ils devront être enregistrées', + 'Limit which orders this discount applies to based on its line items.' => 'Limiter les commandes auxquelles cette remise s\'applique en fonction de leurs articles.', + 'Limit which purchasables this sale applies to.' => 'Limiter les produits achetables auxquels cette promotion s\'applique.', + 'Limit' => 'Limite', + 'Line Item Statuses' => 'Statuts de l\'article', + 'Line Item' => 'Article de ligne', + 'Line Items' => 'Articles de ligne', + 'Line item price (minus discounts)' => 'Prix de l\'article (moins les remises)', + 'Line item shipping cost' => 'Frais de livraison de l’article', + 'Line item statuses reordered.' => 'Articles réordonnés.', + 'Link Duration' => 'Durée du lien', + 'Link Sent' => 'Lien envoyé', + 'Link to a product' => 'Lien vers un produit', + 'Link to a variant' => 'Lien vers une variante', + 'Link' => 'Lien', + 'Live' => 'Live', + 'Location' => 'Emplacement', + 'Locations that should be available for previewing products in this product type.' => 'Emplacements à proposer pour la prévisualisation des produits de ce type de produit.', + 'MM' => 'MM', + 'Make a payment' => 'Effectuer un paiement', + 'Make this the primary store' => 'En faire le magasin principal', + 'Manage Inventory' => 'Gérer l\'inventaire', + 'Manage donation settings' => 'Gérer les paramètres de don', + 'Manage general store settings' => 'Gérer les paramètres généraux du magasin', + 'Manage inventory locations' => 'Gérer les emplacements de l\'inventaire', + 'Manage inventory stock levels' => 'Gérer les niveaux de l\'inventaire', + 'Manage inventory transfers' => 'Gérer les transferts d\'inventaire', + 'Manage orders' => 'Gérez les commandes', + 'Manage payment currencies' => 'Gérer les devises de paiement', + 'Manage promotions' => 'Gérez les promotions', + 'Manage shipping' => 'Gérer l\'expédition', + 'Manage store settings' => 'Gérer les paramètres du magasin', + 'Manage subscription plans' => 'Gérer les abonnements', + 'Manage subscription' => 'Gérer l’abonnement', + 'Manage subscriptions' => 'Gérer les abonnements', + 'Manage taxes' => 'Gérer les taxes', + 'Manage' => 'Gérer', + 'Mark as Pending' => 'Marquer comme en attente', + 'Mark as completed' => 'Marquer comme terminé', + 'Match Billing Address' => 'Faire correspondre l\'adresse de facturation', + 'Match Customer' => 'Faire correspondre le client', + 'Match Order' => 'Faire correspondre la commande', + 'Match Orders' => 'Faire correspondre les commandes', + 'Match Product' => 'Faire correspondre le produit', + 'Match Purchasable' => 'Faire correspondre les produits achetables', + 'Match Shipping Address' => 'Faire correspondre l\'adresse de livraison', + 'Match Variant' => 'Faire correspondre la variante', + 'Matching Items' => 'Articles correspondants', + 'Max Qty' => 'Qté max.', + 'Max Uses' => 'Nombre max d\'utilisations', + 'Max Variants' => 'Variantes max.', + 'Max quantity must greater than min.' => 'La quantité maximale doit être supérieure à min.', + 'Maximum Purchase Quantity' => 'Quantité maximum d\'achat', + 'Maximum Total Shipping Cost' => 'Coût total maximum de la livraison', + 'Maximum allowed quantity' => 'Quantité maximale autorisée', + 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Nombre maximum d\'articles correspondants qui peuvent être commandés pour que cette réduction soit appliquée. La valeur zéro indique que ce critère n\'est pas pris en compte.', + 'Maximum order quantity for this item is {num}.' => 'La quantité maximale de commande pour cet article est {num}.', + 'Message' => 'Message', + 'Meters (m)' => 'Mètres (m)', + 'Millimeters (mm)' => 'Millimètres (mm)', + 'Min Qty' => 'Qté min.', + 'Min quantity must be less than max.' => 'La quantité minimale doit être inférieure à max.', + 'Minimum Purchase Quantity' => 'Quantité minimum d\'achat', + 'Minimum Total Price Strategy' => 'Stratégie de prix minimum', + 'Minimum Total Shipping Cost' => 'Coût total minimum de la livraison', + 'Minimum allowed quantity' => 'Quantité minimale autorisée', + 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Nombre minimum d\'articles correspondants qui doivent être commandés pour que cette réduction puisse s\'appliquer.', + 'Minimum order quantity for this item is {num}.' => 'La quantité minimale de commande pour cet article est {num}.', + 'Missing Gateway' => 'Portail manquant', + 'Missing a default inventory location.' => 'Il manque un emplacement d\'inventaire par défaut.', + 'Move Inventory' => 'Déplacer l\'inventaire', + 'Move To' => 'Déplacer vers', + 'Move {qty} from {fromType} to {toType}' => 'Déplacer {qty} de {fromType} vers {toType}', + 'Move' => 'Déplacer', + 'Movement from deactivated inventory location' => 'Mouvement à partir d\'un emplacement d\'inventaire désactivé', + 'Movement' => 'Mouvement', + 'Must have at least one variant.' => 'Doit avoir au moins une variante.', + 'Name Field' => 'Champ de nom', + 'Name' => 'Nom', + 'New Customer' => 'Nouveau client', + 'New Customers' => 'Nouveaux clients', + 'New Order' => 'Nouvelle commande', + 'New PDF' => 'Nouveau PDF', + 'New address' => 'Nouvelle adresse', + 'New catalog pricing rule' => 'Nouvelle règle de tarification du catalogue', + 'New currency' => 'Nouvelle devise', + 'New discount' => 'Nouvelle remise', + 'New email' => 'Nouvel email', + 'New gateway' => 'Nouveau portail', + 'New line item status' => 'Nouveau statut d\'article', + 'New line items get this status by default when the order is completed' => 'Les nouveaux articles obtiennent ce statut par défaut lorsque la commande est terminée', + 'New location' => 'Nouvel emplacement', + 'New order status' => 'Nouveau statut de commande', + 'New orders get this status by default' => 'Statut par défaut des nouvelles commandes', + 'New product type' => 'Nouveau type de produit', + 'New product' => 'Nouveau produit', + 'New product, choose a type' => 'Nouveau produit, choisissez un type', + 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Les nouveaux produits passent par défaut dans la première catégorie fiscale disponible. Si aucune n\'est disponible, cette catégorie sera utilisée.', + 'New sale' => 'Nouvelle promotion', + 'New shipping category' => 'Nouvelle catégorie de livraison', + 'New shipping method' => 'Nouveau mode de livraison', + 'New shipping rule' => 'Nouvelle règle de livraison', + 'New shipping zone' => 'Nouvelle zone de livraison', + 'New subscription plan' => 'Nouvel abonnement', + 'New tax category' => 'Nouvelle catégorie de taxe', + 'New tax rate' => 'Nouveau taux de taxe', + 'New tax zone' => 'Nouvelle zone de taxe', + 'New transfer' => 'Nouveau transfert', + 'New {productType} product' => 'Nouveau produit {productType}', + 'New' => 'Nouveau', + 'Next payment' => 'Prochain paiement', + 'No Address' => 'Aucune adresse', + 'No PDFs exist yet.' => 'Il n\'existe pas encore de PDF.', + 'No access given to any specific store management features.' => 'Aucun accès n\'est donné à des fonctions spécifiques de gestion de magasin.', + 'No additional payment currencies exist yet.' => 'Il n\'existe pas encore de devise pour les paiements supplémentaires.', + 'No address' => 'Aucune adresse', + 'No billing address' => 'Aucune adresse de facturation', + 'No catalog pricing rule exists with the ID “{id}”' => 'Aucune règle de tarification du catalogue n\'existe avec l\'ID « {id} »', + 'No catalog pricing rules exist yet.' => 'Il n\'existe pas encore de règles de tarification du catalogue.', + 'No currency exists with the ID “{id}”' => 'Il n\'existe aucune devise avec l\'identifiant « {id} »', + 'No customer email address exists on this cart.' => 'Aucune adresse e-mail de client n\'existe dans ce panier.', + 'No description' => 'Aucune description', + 'No discount exists with the ID “{id}”' => 'Il n\'existe aucune remise avec l\'identifiant « {id} »', + 'No discounts exist yet.' => 'Il n\'existe pas encore de remise.', + 'No donation amount supplied.' => 'Aucun montant de don fourni.', + 'No emails exist yet.' => 'Il n\'existe pas encore d\'email.', + 'No inventory changes made.' => 'Aucune modification d\'inventaire n\'a été effectuée.', + 'No inventory found.' => 'Aucun inventaire n\'a été trouvé.', + 'No inventory movements made.' => 'Aucun mouvement d\'inventaire n\'a été effectué.', + 'No inventory transactions for this location.' => 'Aucune transaction d\'inventaire pour cet emplacement.', + 'No new customer selected.' => 'Aucun nouveau client sélectionné.', + 'No order history exists with the ID “{id}”' => 'Il n\'existe aucun historique de commande avec l\'identifiant « {id} »', + 'No order status history items will exist until the cart becomes an order.' => 'Il n’y aura aucun élément d’historique du statut de la commande jusqu’à ce que le panier devienne une commande.', + 'No payment source exists with the ID “{id}”' => 'Aucune source de paiement n’existe avec l’ID « {id} »', + 'No private Note.' => 'Aucune note privée.', + 'No product available.' => 'Aucun produit disponible.', + 'No product types exist yet.' => 'Il n\'existe pas encore de type de produit.', + 'No purchasable available.' => 'Aucun achetable disponible.', + 'No sale exists with the ID “{id}”' => 'Il n\'existe aucune promotion avec l\'identifiant « {id} »', + 'No sales exist yet.' => 'Il n\'existe pas encore de promotion.', + 'No shipping address' => 'Aucune adresse de livraison', + 'No shipping category exists with the ID “{id}”' => 'Il n\'existe pas de catégorie de livraison possédant l\'identifiant « {id} »', + 'No shipping method exists with the ID “{id}”' => 'Il n\'existe pas de mode de livraison avec l\'identifiant « {id} »', + 'No shipping rule exists with the ID “{id}”' => 'Il n\'existe pas de règle de livraison avec l\'identifiant « {id} »', + 'No shipping rules exist yet.' => 'Il n\'y a pas encore de règle de livraison.', + 'No shipping zone exists with the ID “{id}”' => 'Aucune zone de livraison ne porte l\'identifiant « {id} »', + 'No stats available.' => 'Aucune statistique disponible.', + 'No subscription plan exists with the ID “{id}”' => 'Aucun abonnement n’existe avec l’ID « {id} »', + 'No subscription plans exist yet.' => 'Il n’existe pas encore d’abonnement.', + 'No tax category exists with the ID “{id}”' => 'Il n\'existe aucune catégorie de taxe avec l\'identifiant « {id} »', + 'No tax rate exists with the ID “{id}”' => 'Il n\'existe pas de taux de taxe avec l\'identifiant « {id} »', + 'No tax zone exists with the ID “{id}”' => 'Il n\'existe pas de zone de taxe avec l\'identifiant « {id} »', + 'No transactions exist.' => 'Aucune transaction existante.', + 'No user authenticated.' => 'Aucun utilisateur authentifié.', + 'No' => 'Non', + 'None on hand' => 'Aucun à disposition', + 'None' => 'Aucun', + 'Not a valid address type' => 'N\'est pas un type d\'adresse valide', + 'Not a valid credit card number.' => 'Numéro de carte non valide.', + 'Not all SKUs are unique.' => 'Tous les numéros d’articles (SKU) ne sont pas uniques.', + 'Note' => 'Remarque', + 'Notes' => 'Notes', + 'Number of Coupons' => 'Nombre de coupons', + 'Number' => 'Numéro', + 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Parmi les sites activés ci-dessus, sur quels sites les produits de ce type de produit doivent-ils être enregistrées ?', + 'On Hand' => 'Disponible', + 'Only allow this gateway to be used for zero value orders?' => 'Autoriser l’utilisation de ce portail uniquement pour les commandes de valeur nulle ?', + 'Only match certain purchasables…' => 'Ne faire correspondre que certains produits achetables…', + 'Only match purchasables related to…' => 'Ne faire correspondre que les produits achetables liés à…', + 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Seules les commandes ayant les statuts suivants seront incluses. Laissez ce champ vide pour inclure tous les statuts.', + 'Only save product to the site they were created in' => 'N’enregistrer les produits que sur le site où ils ont été créés', + 'Options' => 'Options', + 'Order Condition Formula' => 'Formule de la condition de commande', + 'Order Description Format' => 'Format de la description de la commande', + 'Order Details' => 'Détails de la commande', + 'Order Fields' => 'Champs de commande', + 'Order PDF Download Link' => 'Lien de téléchargement du PDF de la commande', + 'Order PDF Filename Format' => 'Format de nom de fichier PDF de commandes', + 'Order Reference Number Format' => 'Format du numéro de référence de la commande', + 'Order Settings' => 'Paramètres de commande', + 'Order Site' => 'Site de commande', + 'Order Status description.' => 'Description du statut de la commande.', + 'Order Status' => 'Statut de commande', + 'Order Statuses' => 'Statuts de la commande', + 'Order can not be empty.' => 'La commande ne peut pas être vide.', + 'Order count' => 'Nombre de commandes', + 'Order customer data removed.' => 'Données des clients supprimées des commandes.', + 'Order deleted.' => 'Commande supprimée.', + 'Order fields saved.' => 'Champs commandes enregistrés.', + 'Order not found.' => 'Commande non trouvée.', + 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Le solde de paiement de la commande est {outstandingBalanceAsCurrency}. Il s\'agit du montant maximum facturé.', + 'Order recalculated.' => 'Commande recalculée.', + 'Order status saved.' => 'Statut de commande enregistré.', + 'Order statuses reordered.' => 'Statuts des commandes réordonnés.', + 'Order total shipping cost' => 'Coût total de livraison', + 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Prix total taxable de la commande (sous-total des articles individuels + total des remises + total des frais de livraison)', + 'Order' => 'Commande', + 'Orders (Legacy)' => 'Commandes (Legacy)', + 'Orders deleted.' => 'Commandes supprimées.', + 'Orders not restored.' => 'Commandes non restaurées.', + 'Orders restored.' => 'Commandes restaurées.', + 'Orders' => 'Commandes', + 'Organization Name' => 'Nom de l\'organisation', + 'Organization Tax ID' => 'ID fiscal de l\'organisation', + 'Origin and destination cannot be the same.' => 'L\'origine et la destination ne peuvent pas être les mêmes.', + 'Origin' => 'Origine', + 'Original Price' => 'Prix d’origine', + 'Original price' => 'Prix d\'origine', + 'Original promotional price' => 'Prix promotionnel d\'origine', + 'Other Languages' => 'Autres langues', + 'Other countries' => 'Autres pays', + 'Outgoing transfer from Transfer ID: ' => 'Transfert sortant à partir de l\'ID de transfert : ', + 'Overpaid' => 'Surpayé', + 'Overrides previous?' => 'Remplace la précédente ?', + 'PDF Attachment' => 'Pièce jointe PDF', + 'PDF Template Path' => 'Chemin du template PDF', + 'PDF saved.' => 'PDF enregistré.', + 'PDF' => 'PDF', + 'PDFs & Emails' => 'PDF et e-mails', + 'PDFs' => 'PDF', + 'Paid Amount' => 'Montant payé', + 'Paid Status' => 'Statut « Payé »', + 'Paid' => 'Payé', + 'Paper Orientation' => 'Orientation du papier', + 'Paper Size' => 'Format du papier', + 'Partial payment not allowed.' => 'Paiement partiel non autorisé.', + 'Partial' => 'Partiellement', + 'Past year' => 'L\'année dernière', + 'Past {num} days' => '{num} derniers jours', + 'Pay {amount} of {currency} on the order.' => 'Payer {amount} {currency} à la commande.', + 'Pay' => 'Payer', + 'Payment Amount' => 'Montant du paiement', + 'Payment Currencies' => 'Devises du paiement', + 'Payment Gateway' => 'Portail de paiement', + 'Payment Method' => 'Moyen de paiement', + 'Payment error: {message}' => 'Erreur de paiement : {message}', + 'Payment method issue' => 'Problème de moyen de paiement', + 'Payment source created.' => 'Source de paiement créée.', + 'Payment source deleted.' => 'Source de paiement supprimée.', + 'Payments' => 'Paiements', + 'Pending' => 'En cours', + 'Per Email Address Discount Limit' => 'Limite de réductions par adresse e-mail', + 'Per Item Amount Off' => 'Montant de réduction par article', + 'Per Item Discount' => 'Remise par article', + 'Per Item Percentage Off' => 'Montant de réduction par article', + 'Per Item Rate' => 'Frais par article', + 'Per User Discount Limit' => 'Limite de réductions par utilisateur', + 'Percentage Rate' => 'Frais en pourcentage', + 'Phone (Alt)' => 'Téléphone (Alt)', + 'Phone' => 'Téléphone', + 'Pick a plan' => 'Choisir un abonnement', + 'Plain Text Email Template Path' => 'Chemin du modèle d\'e-mail texte brut', + 'Plan' => 'Abonnement', + 'Plans reordered.' => 'Plans réordonnés.', + 'Portrait' => 'Portrait', + 'Post Date' => 'Date de publication', + 'Postal Code Formula' => 'Formule du code postal', + 'Pounds (lb)' => 'Livres (lb)', + 'Preview' => 'Aperçu', + 'Previous Status' => 'Statut précédent', + 'Price' => 'Prix', + 'Prices' => 'Prix', + 'Pricing Rules' => 'Règles de tarification', + 'Pricing jobs are currently running.' => 'Des travaux de tarification sont actuellement en cours.', + 'Pricing' => 'Tarification', + 'Primary Billing Address' => 'Adresse de facturation principale', + 'Primary Shipping Address' => 'Adresse de livraison principale', + 'Primary payment source updated.' => 'Source de paiement principale mise à jour.', + 'Primary' => 'Principal', + 'Private Note' => 'Note privée', + 'Product Fields' => 'Champs produit', + 'Product ID is required.' => 'Un ID produit est requis.', + 'Product Template' => 'Modèle du produit', + 'Product Title Format' => 'Format du titre de produit', + 'Product Type' => 'Type de produit', + 'Product Types' => 'Types de produits', + 'Product URI Format' => 'Format d\'URI produit', + 'Product Variant' => 'Variante du produit', + 'Product Variants' => 'Variantes du produit', + 'Product type saved.' => 'Type de produit enregistré.', + 'Product type settings' => 'Paramètres du type de produit', + 'Product' => 'Produit', + 'Products and Variants deleted.' => 'Produits et variantes supprimées.', + 'Products not restored.' => 'Produits non restaurés.', + 'Products restored.' => 'Produits restaurés.', + 'Products' => 'Produits', + 'Promotable' => 'Pouvant être promu', + 'Promotable?' => 'Peut-être mis en promotion ?', + 'Promotional Amount' => 'Montant promotionnel', + 'Promotional Price' => 'Prix promotionnel', + 'Purchasable Categories' => 'Catégories de produits achetables', + 'Purchasable ID and Sale ID are required.' => 'Un ID achetable et de vente est requis.', + 'Purchasable ID is required.' => 'Un ID achetable est requis.', + 'Purchasable Type' => 'Type de produit achetable', + 'Purchasable' => 'Produit achetable', + 'Purchase (Authorize and Capture Immediately)' => 'Acheter (autoriser et collecter immédiatement)', + 'Purchase Total' => 'Total des achats', + 'Qty' => 'Qté', + 'Quality Control' => 'Contrôle de la qualité', + 'Quantity' => 'Quantité', + 'Rate' => 'Taux', + 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Réaffecter {numOrders, plural, =1{la commande} other{les commandes}}', + 'Recalculate order' => 'Recalculer la commande', + 'Receive Inventory' => 'Recevoir l\'inventaire', + 'Receive Transfer' => 'Recevoir le transfert', + 'Receive' => 'Recevoir', + 'Received' => 'Reçu', + 'Recent Orders' => 'Commandes récentes', + 'Recipient' => 'Destinataire', + 'Recover Cart' => 'Récupérer le panier', + 'Reduce price' => 'Diminuer le prix', + 'Reduce the price by a fixed amount' => 'Diminuer le prix d’un montant fixe', + 'Reduce the price by a percentage of the original price' => 'Diminuer le prix d’un certain pourcentage du prix d’origine', + 'Reference' => 'Référence', + 'Refresh payment history' => 'Actualiser l\'historique de paiement', + 'Refund note' => 'Note de remboursement', + 'Refund payment' => 'Rembourser le paiement', + 'Refund' => 'Rembourser', + 'Reject' => 'Rejeter', + 'Rejected' => 'Rejeté', + 'Relationship Type' => 'Type de relation', + 'Removable included tax rates are only allowed for the default tax zone.' => 'Les taux de taxes inclus supprimables sont uniquement autorisés pour la zone de taxes par défaut.', + 'Remove address' => 'Supprimer l\'adresse', + 'Remove all shipping costs from the order' => 'Retirer tous les coûts de livraison de la commande', + 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Retirer l\'association au client et à l\'adresse e-mail {numOrders, plural, =1{de la commande} other{des commandes}}. Vous pouvez également sélectionner d\'autres données de clients à supprimer ci-dessous', + 'Remove customer data' => 'Supprimer les données de clients', + 'Remove from price?' => 'Retirer du prix ?', + 'Remove shipping costs for matching items only' => 'Retirer les coûts d\'expédition pour les articles correspondants seulement', + 'Remove the included tax when a valid organization tax ID is present?' => 'Supprimer la taxe incluse lorsqu\'un identifiant fiscal d\'organisation valide est présent ?', + 'Remove' => 'Supprimer', + 'Removed' => 'Supprimé', + 'Repeat Customers' => 'Clients réguliers', + 'Reply To' => 'Répondre à', + 'Require Billing Address At Checkout' => 'Exiger l\'adresse de facturation lors du passage de la commande', + 'Require Coupon Code' => 'Exiger un code promotionnel', + 'Require Shipping Address At Checkout' => 'Exiger l\'adresse d\'expédition lors du passage de la commande', + 'Require Shipping Method Selection At Checkout' => 'Exiger le choix du mode d\'expédition au moment du passage de la commande', + 'Require' => 'Demander', + 'Reserved' => 'Réservé', + 'Reset usage' => 'Réinitialiser l\'utilisation', + 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Restreignez la remise aux commandes pour lesquelles le client a acheté une valeur totale minimale d\'articles correspondants.', + 'Revenue Options' => 'Options de revenus', + 'Revenue' => 'Recettes', + 'Rule' => 'Règle', + 'Rules reordered.' => 'Règles réordonnées.', + 'SKU' => 'Code article interne (SKU)', + 'Safety' => 'Sécurité', + 'Sale Price' => 'Prix de vente', + 'Sale description.' => 'Description de la promotion.', + 'Sale reordered.' => 'Promotion réordonnée.', + 'Sale saved.' => 'Promotion enregistrée.', + 'Sale' => 'Promotion/solde', + 'Sales deleted.' => 'Ventes supprimées.', + 'Sales updated.' => 'Ventes mises à jour.', + 'Sales' => 'Promos & Soldes', + 'Save and continue editing' => 'Enregistrer et continuer l\'édition', + 'Save and return to all orders' => 'Enregistrer et revenir à toutes les commandes', + 'Save and set rules' => 'Enregistrer et définir les règles', + 'Save as a new rule' => 'Enregistrer en tant que nouvelle règle', + 'Save product to all sites enabled for this product type' => 'Enregistrer les produits sur tous les sites activés pour ce type de produit', + 'Save product to other sites in the same site group' => 'Enregistrer le produit sur les autres sites du même groupe de sites', + 'Save product to other sites with the same language' => 'Enregistrer le produit sur les autres sites ayant la même langue', + 'Save' => 'Enregistrer', + 'Search customer…' => 'Rechercher un client…', + 'Search inventory' => 'Rechercher dans l\'inventaire', + 'Search or enter customer email…' => 'Rechercher ou saisir un e-mail client…', + 'Search…' => 'Rechercher…', + 'See Orders' => 'Voir les commandes', + 'Select a gateway' => 'Sélectionner un portail', + 'Select a tax category.' => 'Choisir une catégorie de taxe.', + 'Select a tax zone. If empty, this rate will match anywhere.' => 'Sélectionner une zone fiscale. Si aucune zone n\'est sélectionnée, ce taux s\'appliquera partout.', + 'Select address' => 'Sélectionner l\'adresse', + 'Select an item' => 'Sélectionner un article', + 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Sélectionnez la façon dont la règle de tarification du catalogue sera appliquée aux produits achetables.', + 'Select how the sale will be applied to the purchasable(s).' => 'Indiquez comment la promotion sera appliquée aux articles en vente.', + 'Select product type' => 'Sélectionner le type de produit', + 'Select the emails that will be sent when transitioning to this status.' => 'Sélectionnez les emails qui seront envoyés lors du basculement sur ce statut.', + 'Select what this rate should be applied to.' => 'Sélectionner ce à quoi ce taux doit être appliqué.', + 'Send Email' => 'Envoyer l\'e-mail', + 'Send to custom recipient' => 'Envoyer à un destinataire spécifique', + 'Send to the customer' => 'Envoyer au client', + 'Set Quantity' => 'Définir la quantité', + 'Set default category' => 'Définir la catégorie par défaut', + 'Set default variant' => 'Définir la variante par défaut', + 'Set or Adjust' => 'Définir ou ajuster', + 'Set price' => 'Définir le prix', + 'Set status' => 'Définir le statut', + 'Set the price to a flat amount' => 'Définir le prix comme un montant fixe', + 'Set the price to a percentage of the original price' => 'Définir le prix comme un pourcentage du prix original', + 'Set the sale price to a flat amount' => 'Définir le prix soldé comme un montant fixe', + 'Set the sale price to a percentage of the original price' => 'Définir le prix soldé comme un pourcentage du prix original', + 'Set to' => 'Définir sur', + 'Settings saved.' => 'Paramètres enregistrés.', + 'Settings' => 'Paramètres', + 'Share cart…' => 'Partager le panier…', + 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Expédition - Le coût minimum est le coût d\'expédition, si le prix de la commande est inférieur au coût d\'expédition.', + 'Shipping Address Zone' => 'Zone d\'adresse de livraison', + 'Shipping Address' => 'Adresse de livraison', + 'Shipping Business Name' => 'Nom commercial pour la livraison', + 'Shipping Categories' => 'Catégories de livraison', + 'Shipping Category Conditions' => 'Conditions pour les différentes catégories de livraison', + 'Shipping Category' => 'Catégorie de livraison', + 'Shipping First Name' => 'Nom pour la livraison', + 'Shipping Full Name' => 'Nom complet pour la livraison', + 'Shipping Last Name' => 'Nom pour la livraison', + 'Shipping Method' => 'Mode de livraison', + 'Shipping Methods' => 'Modes de livraison', + 'Shipping Rule' => 'Règle de livraison', + 'Shipping Zones' => 'Zones de livraison', + 'Shipping address required.' => 'Adresse de livraison requise.', + 'Shipping categories deleted.' => 'Catégories de livraison supprimées.', + 'Shipping category saved.' => 'Catégorie de livraison enregistrée.', + 'Shipping category updated.' => 'Catégorie de livraison mise à jour.', + 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Les frais d\'expédition sont ajoutés à l\'ensemble de la commande avant l\'application des taux de pourcentage, d\'article et de poids. Réglez à zéro pour désactiver ce taux. Toute la règle, y compris ce taux de base, ne correspondra pas et ne s\'appliquera pas si le panier ne contient que des articles non expédiables comme des produits numériques.', + 'Shipping method saved.' => 'Mode de livraison enregistré.', + 'Shipping methods and rules deleted.' => 'Modes et règles de livraison supprimés.', + 'Shipping methods updated.' => 'Modes de livraison mis à jour.', + 'Shipping rule saved.' => 'Règle de livraison enregistrée.', + 'Shipping zone saved.' => 'Zone de livraison enregistrée.', + 'Shipping' => 'Livraison', + 'Short Number' => 'Numéro court', + 'Show Chart?' => 'Afficher le graphique ?', + 'Show Order Count?' => 'Afficher le nombre de commandes ?', + 'Show all prices' => 'Afficher tous les prix', + 'Show archived gateways' => 'Afficher les portails archivés', + 'Show order count line on chart.' => 'Afficher la ligne de décompte des commandes sur le graphique.', + 'Show related sales' => 'Afficher les ventes connexes', + 'Show rule details' => 'Afficher les détails de la règle', + 'Show the Dimensions and Weight fields for products of this type' => 'Afficher les champs Dimensions et Poids pour les produits de ce type', + 'Show the Title field for products' => 'Afficher le champ Titre pour les produits', + 'Show the Title field for variants' => 'Afficher le champ Titre pour les variantes', + 'Signed In' => 'Connexion effectuée', + 'Site Languages' => 'Langues du site', + 'Site store mapping saved.' => 'Mappage du site du magasin enregistré.', + 'Sites' => 'Sites', + 'Slug' => 'Identificateur', + 'Snapshot' => 'Instantané', + 'Snapshots' => 'Instantanés', + 'Some orders restored.' => 'Certaines commandes ont été restaurées.', + 'Some products restored.' => 'Certains produits ont été restaurés.', + 'Some variants restored.' => 'Certaines variantes ont été restaurées.', + 'Something changed with the order before payment, please review your order and submit payment again.' => 'Quelque chose a changé dans la commande avant le paiement, merci de la vérifier puis de soumettre de nouveau votre règlement.', + 'Sorry, no matching options.' => 'Désolé, aucune option ne correspond.', + 'Source - The purchasable relationship field is on the category' => 'Source - Le champ de relation du produit achetable est sur la catégorie', + 'Source' => 'Source', + 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Précisez une condition Twig déterminant si la réduction doit s\'appliquer à une commande donnée. (La commande peut être référencée via la variable `order`.)', + 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Précisez une condition Twig déterminant si la règle d\'expédition doit s\'appliquer à une commande donnée. (La commande peut être référencée via la variable `order`.)', + 'Start Date' => 'Date de début', + 'State' => 'État/Province', + 'Status Email Address' => 'Adresse email du statut', + 'Status Emails' => 'Emails de statut', + 'Status History' => 'Historique du statut', + 'Status Updated.' => 'Statut mis à jour.', + 'Status change message' => 'Message de changement de statut', + 'Status' => 'Statut', + 'Stock' => 'Stock', + 'Stops Processing?' => 'Cela arrête-t-il le traitement ?', + 'Stops subsequent?' => 'Cela arrête-t-il les éléments suivants ?', + 'Store Location' => 'Emplacement des magasins', + 'Store Management' => 'Gestion du magasin', + 'Store Markets' => 'Marchés de la boutique', + 'Store Rule' => 'Règle du magasin', + 'Store saved.' => 'Boutique enregistrée.', + 'Store' => 'Magasin', + 'Stores & Sites' => 'Magasins et sites', + 'Stores' => 'Magasins', + 'Strategy to apply when an order is free or has a zero balance.' => 'Stratégie à appliquer lorsqu\'une commande est gratuite ou a un solde nul.', + 'Strategy to apply when calculating the minimum order price.' => 'Stratégie à employer lors du calcul du prix de la commande minimale.', + 'Subject' => 'Objet', + 'Subscribing user' => 'Utilisateur abonné', + 'Subscription Fields' => 'Champs d’abonnement', + 'Subscription Plans' => 'Abonnements', + 'Subscription Settings' => 'Paramètres d\'abonnement', + 'Subscription cancelled.' => 'Abonnement annulé.', + 'Subscription date' => 'Date d’abonnement', + 'Subscription fields saved.' => 'Champs d’abonnement enregistrés.', + 'Subscription for {user} to {plan} prevented by a plugin.' => 'La souscription de l’utilisateur {user} à l’abonnement {plan} a été empêchée par un plug-in.', + 'Subscription plan saved.' => 'Abonnement enregistré.', + 'Subscription plan' => 'Abonnement', + 'Subscription plans' => 'Abonnements', + 'Subscription reactivated.' => 'Abonnement réactivé.', + 'Subscription reference' => 'Référence de l’abonnement', + 'Subscription started.' => 'Abonnement démarré.', + 'Subscription switched.' => 'Abonnement changé.', + 'Subscription to “{plan}”' => 'Abonnement à « {plan} »', + 'Subscription' => 'Abonnement', + 'Subscriptions on hold' => 'Abonnements en attente', + 'Subscriptions' => 'Abonnements', + 'Suppress emails' => 'Réduire les e-mails', + 'Switch plan' => 'Changer d’abonnement', + 'Switch' => 'Basculer', + 'System' => 'Système', + 'Table Columns' => 'Colonnes du tableau', + 'Target - The category relationship field is on the purchasable' => 'Cible - Le champ de relation de la catégorie est sur l\'achetable', + 'Tax & Shipping' => 'Taxes et expédition', + 'Tax (inc)' => 'Taxe (inc)', + 'Tax Categories' => 'Catégories de taxe', + 'Tax Category' => 'Catégorie de taxe', + 'Tax Rates' => 'Taux de taxe', + 'Tax Zone' => 'Zone de taxe', + 'Tax Zones' => 'Zones de taxe', + 'Tax categories deleted.' => 'Catégories de taxe supprimées.', + 'Tax category saved.' => 'Catégorie de taxe enregistrée.', + 'Tax category updated.' => 'Catégorie de taxe mise à jour.', + 'Tax rate saved.' => 'Taux de taxe enregistré.', + 'Tax rates updated.' => 'Taux de taxe mis à jour.', + 'Tax zone saved.' => 'Zone de taxe enregistrée.', + 'Tax' => 'Taxe', + 'Taxable Subject' => 'Ce qui doit être taxé', + 'Template Path' => 'Chemin du modèle', + 'That handle is already in use' => 'Cet identificateur est déjà utilisé', + 'That handle is already in use.' => 'Cet identificateur est déjà utilisé.', + 'The PDF to attach to this email.' => 'Le PDF à attacher à cet e-mail.', + 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'L\'URL de la page permettant de mettre à jour les détails de facturation d\'un abonnement, ainsi que de gérer l\'authentification 3DS.', + 'The address provided is outside the store’s market.' => 'L\'adresse fournie est en dehors du marché de la boutique.', + 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'Le montant de la remise qui est appliquée à l\'ensemble de la commande. Ce montant est réparti sur les objets dans l\'ordre du prix le plus élevé au prix le plus bas, jusqu\'à épuisement de la remise.', + 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'La remise de base ne peut réduire le prix des articles du panier à zéro, elle ne peut pas rendre la commande négative.', + 'The cart recovery link is invalid. Please request a new one.' => 'Le lien de récupération du panier n\'est pas valide. Veuillez en demander un nouveau.', + 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Le taux de conversion qui sera utilisé lorsqu\'un montant sera converti dans cette devise. Par exemple, si un article coûte {amount1}, vous obtiendrez le montant de {amount2} dans l\'autre devise si le taux de conversion est de {rate}.', + 'The countries that orders are allowed to be placed from.' => 'Les pays depuis lesquels il est possible de passer une commande.', + 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'Le coupon « {code} » a dépassé la limite de {limit} utilisations.', + 'The customer for this order has been deleted.' => 'Le client pour cette commande a été supprimé.', + 'The default shipping category is automatically available to all product types.' => 'La catégorie d\'expédition par défaut est automatiquement disponible pour tous les types de produits.', + 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'La réduction « {name} » a dépassé la limite de {limit} utilisations au total.', + 'The download link has expired. Please request a new one.' => 'Le lien de téléchargement a expiré. Veuillez en demander un nouveau.', + 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'L\'adresse à partir de laquelle les emails de statut de commande sont envoyés. Laisser vide pour utiliser l\'adresse email du système définie dans les Paramètres Généraux de Craft.', + 'The entry that contains the description for this subscription’s plan.' => 'Entrée contenant la description de cet abonnement.', + 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'La remise forfaitaire appliquée à chaque article. Par ex : « 3 » pour 3€ de réduction par article.', + 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'Le format utilisé pour générer de nouveaux coupons, par exemple {example}. Tout caractère « # » sera remplacé par une lettre aléatoire.', + 'The from and to inventory locations must be different.' => 'Les emplacements de départ et de retour de l\'inventaire doivent être différents.', + 'The inventory locations this store uses.' => 'Les emplacements d\'inventaire utilisés par ce magasin.', + 'The item is not enabled for sale.' => 'Cet article n’est pas autorisé à la vente.', + 'The language the order was made in.' => 'La langue dans laquelle la commande a été passée.', + 'The language to be used when this email is rendered.' => 'La langue à utiliser lors de l\'affichage de cet e-mail.', + 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Le nombre maximum de niveaux que ce type de produit peut avoir. Laissez vide si cela n\'est pas pertinent.', + 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Le maximum que le client devra payer pour la livraison. Mettre sur zéro pour désactiver.', + 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Le minimum que le client devra payer pour la livraison. Mettre sur zéro pour désactiver.', + 'The order is not valid.' => 'La commande est invalide.', + 'The payment gateway that will be used for the subscription plan.' => 'Indique quel portail de paiement sera utilisé pour l’abonnement.', + 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'La valeur du pourcentage de réduction qui doit être appliqué à chaque article, par exemple {ex1} pour une remise de {ex2}. Les pourcentages sont arrondis à 2 décimales.', + 'The previously-selected shipping method is no longer available.' => 'La méthode d\'expédition précédemment sélectionnée n\'est plus disponible.', + 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Le prix de {description} a augmenté, il est passé de {originalSalePriceAsCurrency} à {newSalePriceAsCurrency}', + 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Le prix de {description} a baissé, il est passé de {originalSalePriceAsCurrency} à {newSalePriceAsCurrency}', + 'The primary currency cannot be changed after orders are placed.' => 'La devise principale ne peut être modifiée après la validation des commandes.', + 'The purchasable defines the relationship' => 'Le produit achetable définit la relation', + 'The purchasable is related by another element' => 'Le produit achetable est lié par un autre élément', + 'The recipient of the email. Twig code can be used here.' => 'Le destinataire de l\'e-mail. Du code Twig peut être utilisé ici.', + 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'L\'adresse e-mail de réponse. Laissez vide pour la réponse normale à l\'expéditeur de l\'e-mail. Du code Twig peut être utilisé ici.', + 'The site the order was made in.' => 'Le site sur lequel la commande a été passée.', + 'The site to be used when this email is rendered.' => 'Le site à utiliser lors de l\'affichage de cet e-mail.', + 'The subject line of the email. Twig code can be used here.' => 'La ligne d\'objet de l\'e-mail. Du code Twig peut être utilisé ici.', + 'The template that the PDF should be generated from.' => 'Le modèle à partir duquel le PDF doit être généré.', + 'The template to be used for HTML emails.' => 'Le modèle utilisé pour les emails HTML', + 'The template to be used for plain text emails. Twig code can be used here.' => 'Le modèle à utiliser pour les e-mails en texte brut. Du code Twig peut être utilisé ici.', + 'The template to use when a product’s URL is requested.' => 'Le modèle à utiliser lorsque l\'URL d\'un produit est requise.', + 'The total number of order adjustments changed.' => 'Le nombre total de modifications de commandes qui ont été modifiées.', + 'The total price of the order changed.' => 'Le montant total de la commande qui a été modifié.', + 'The total quantity of items within the order changed.' => 'Le nombre total d\'articles dans la commande qui ont été modifiés.', + 'The unique SKU of the donation purchasable.' => 'L\'unique référence du don pouvant être acheté.', + 'The unit of measurement that should be used when specifying product dimensions.' => 'L\'unité de mesure qui doit être utilisée lorsque vous indiquez les dimensions du produits.', + 'The unit of measurement that should be used when specifying product weights.' => 'L\'unité de mesure qui doit être utilisée lorsque vous indiquez le poids du produit.', + 'The webhook URL for this gateway.' => 'L\'URL du webhook pour cette passerelle.', + 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'Le nom de l\'expéditeur utilisé lors de l\'envoi d\'emails de statut de commande. Laissez vide pour utiliser le nom de l\'expéditeur défini dans les Paramètres Généraux de Craft.', + 'There are errors on the order' => 'Il y a des erreurs dans la commande', + 'There are only {num} “{description}” items left in stock.' => 'Il reste seulement {num} « {description} » articles en stock.', + 'There aren’t any product types to select yet.' => 'Il n\'y a pas encore de types de produits à sélectionner.', + 'There is no gateway or payment source available for use with this order.' => 'Aucun portail ou source de paiement disponible pour cette commande.', + 'There is no gateway selected that supports payment sources.' => 'Aucun portail prenant en charge les sources de paiement n’a été sélectionné.', + 'There is no shipping method selected for this order.' => 'Aucun mode de livraison sélectionné pour cette commande.', + 'This URL will load the cart into the user’s session, making it the active cart.' => 'Cette URL chargera le panier dans la session de l\'utilisateur, ce qui en fera le panier actif.', + 'This action is not allowed for the current user.' => 'Cette action n\'est pas autorisée pour l\'utilisateur actuel.', + 'This category will be used as the default for all purchasables in this store.' => 'Cette catégorie sera utilisée par défaut pour tous les articles pouvant être achetés dans ce magasin.', + 'This coupon is for registered users and limited to {limit} uses.' => 'Ce coupon est limité à {limit} utilisation(s) pour les utilisateurs inscrits.', + 'This coupon is limited to {limit} uses.' => 'Ce coupon est limité à {limit} utilisation(s).', + 'This coupon requires an email address.' => 'Ce rabais nécessite une adresse e-mail.', + 'This gateway does not support that functionality.' => 'Ce portail ne prend pas en charge cette fonctionnalité.', + 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Cela est outrepassé par le paramètre de configuration {setting} dans `config/{file}.php`.', + 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Il s’agit de l’adresse physique de votre magasin. Elle peut être utilisée par différents plug-ins pour déterminer des éléments tels que le mode de livraison ou les taxes applicables. Elle peut également figurer sur les reçus au format PDF.', + 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Ceci est le PDF par défaut qui sera rendu lors de la demande du PDF de commande.', + 'This is the last location for the {store} store.' => 'Il s\'agit du dernier emplacement pour le magasin {store}.', + 'This month' => 'Ce mois-ci', + 'This order has unsaved changes.' => 'Cette commande a des modifications non enregistrées.', + 'This week' => 'Cette semaine', + 'This year' => 'Cette année', + 'Times Used' => 'Nombre de fois où elle a été utilisée', + 'Title' => 'Titre', + 'To' => 'À', + 'Today' => 'Aujourd’hui', + 'Too many variants for this product.' => 'Trop de variantes pour ce produit.', + 'Top Customers by Average Order' => 'Meilleurs clients par commande moyenne', + 'Top Customers by Total Revenue' => 'Meilleurs clients par recette totale', + 'Top Customers' => 'Meilleurs clients', + 'Top Product Types by Qty Sold' => 'Meilleurs types de produits par Qté vendue', + 'Top Product Types by Revenue' => 'Meilleurs types de produits par recettes', + 'Top Product Types' => 'Meilleurs type de produit', + 'Top Products by Qty Sold' => 'Meilleurs produits par Qté vendue', + 'Top Products by Revenue' => 'Meilleurs produits par recettes', + 'Top Products' => 'Meilleurs produits', + 'Top Purchasables by Qty Sold' => 'Meilleurs achetables par Qté vendue', + 'Top Purchasables by Revenue' => 'Meilleurs achetables par recettes', + 'Top Purchasables' => 'Meilleurs achetables', + 'Total ' => 'Total ', + 'Total Discount Use Limit' => 'Limite d\'utilisation totale des remises', + 'Total Discount' => 'Remise totale', + 'Total Included Tax' => 'Total TTC', + 'Total Orders by Billing Country' => 'Total des commandes par pays de facturation', + 'Total Orders by Country' => 'Total des commandes par pays', + 'Total Orders by Shipping Country' => 'Total des commandes par pays de livraison', + 'Total Orders' => 'Total des commandes', + 'Total Paid' => 'Total payé', + 'Total Price' => 'Montant total', + 'Total Qty' => 'Qté totale', + 'Total Revenue' => 'Total des recettes', + 'Total Shipping' => 'Total des frais de port', + 'Total Tax' => 'Total des taxes', + 'Total Weight' => 'Poids total', + 'Total' => 'Total', + 'Track Inventory' => 'Suivre l\'inventaire', + 'Transaction Hash' => 'Hash de la transaction', + 'Transaction ID' => 'ID de la transaction', + 'Transaction captured successfully: {message}' => 'Transaction capturée avec succès : {message}', + 'Transaction refunded successfully: {message}' => 'Transaction remboursée avec succès : {message}', + 'Transactions' => 'Transactions', + 'Transfer Fields' => 'Champs de transfert', + 'Transfer Items' => 'Articles de transfert', + 'Transfer Settings' => 'Paramètres de transfert', + 'Transfer Status' => 'Statut du transfert', + 'Transfer fields saved.' => 'Champs de transfert enregistrés.', + 'Transfer must have at least one item.' => 'Le transfert doit comporter au moins un article.', + 'Transfer' => 'Transférer', + 'Transfers' => 'Transferts', + 'Trial days credited' => 'Jours d’essai crédités', + 'Trial expiration' => 'Expiration de l\'essai', + 'Trial expiry date' => 'Date d’expiration de la version d’essai', + 'Type not in allowed options.' => 'Type non autorisé dans les options.', + 'Type' => 'Type', + 'URI' => 'URI', + 'Unable to cancel subscription at this time.' => 'Impossible d\'annuler l’abonnement actuellement.', + 'Unable to complete order: another request is already in progress.' => 'Impossible de terminer la commande : une autre demande est déjà en cours.', + 'Unable to find variant.' => 'Impossible de trouver la variante.', + 'Unable to generate coupon codes: {message}' => 'Impossible de générer des codes de réduction : {message}', + 'Unable to make payment at this time.' => 'Impossible d’effectuer le paiement actuellement.', + 'Unable to modify subscription at this time.' => 'Impossible de modifier l’abonnement actuellement.', + 'Unable to reactivate subscription at this time.' => 'Impossible de réactiver l’abonnement actuellement.', + 'Unable to reassign orders.' => 'Impossible de réattribuer les commandes.', + 'Unable to remove order data.' => 'Impossible de supprimer les données de commande.', + 'Unable to retrieve Sale and Purchasable.' => 'Impossible de récupérer les promotions et les achetables.', + 'Unable to retrieve cart.' => 'Impossible de récupérer le panier.', + 'Unable to retrieve customer.' => 'Impossible de récupérer le client.', + 'Unable to retrieve load cart URL' => 'Impossible de récupérer et charger l\'URL du panier', + 'Unable to retrieve payment source.' => 'Impossible de récupérer la source de paiement.', + 'Unable to set default shipping category.' => 'Impossible de définir la catégorie d\'expédition par défaut.', + 'Unable to set default tax category.' => 'Impossible de définir la catégorie de taxe par défaut.', + 'Unable to set primary payment source.' => 'Impossible de définir la source de paiement principale.', + 'Unable to start the subscription. Please check your payment details.' => 'Impossible de démarrer l’abonnement. Veuillez vérifier vos informations de paiement.', + 'Unable to subscribe at this time.' => 'Impossible de souscrire actuellement.', + 'Unable to update cart.' => 'Impossible de mettre à jour le panier.', + 'Unable to validate address.' => 'Impossible de valider l’adresse.', + 'Unit Price' => 'Prix unitaire', + 'Unit price (minus discounts)' => 'Prix unitaire (moins les remises)', + 'Units' => 'Unités', + 'Unpaid' => 'Non payés', + 'Unsubscribe' => 'Se désabonner', + 'Update Address' => 'Mettre à jour lʼadresse', + 'Update Order Status' => 'Mettre à jour le statut de la commande', + 'Update Order Status…' => 'Mise à jour du statut de commande…', + 'Update order' => 'Mettre à jour la commande', + 'Update subscription' => 'Mettre à jour l\'abonnement', + 'Update' => 'Mettre à jour', + 'Updated By' => 'Mis à jour par', + 'Updated committed stock successfully.' => 'Stock validé mis à jour avec succès.', + 'Updated' => 'Mis à jour', + 'Use Billing Address For Tax' => 'Utiliser l\'adresse de facturation pour les taxes', + 'Use as the primary billing address' => 'Utiliser comme adresse de facturation principale', + 'Use as the primary shipping address' => 'Utiliser comme adresse de livraison principale', + 'Used By Tax Rates' => 'Utilisé par le taux d\'imposition', + 'Used by Tax Rates' => 'Utilisé par le taux de taxe', + 'User Groups' => 'Groupes d\'utilisateurs', + 'User not found.' => 'Utilisateur non trouvé.', + 'User' => 'Utilisateur', + 'Uses' => 'Utilisations', + 'Validate Business Tax ID as Vat ID' => 'Valider l\'ID de taxe professionnelle en tant qu\'ID de TVA', + 'Validating condition syntax' => 'Validation de la syntaxe conditionnelle', + 'Validating formula syntax' => 'Validation de la syntaxe de la formule', + 'Variant Fields' => 'Champs variante', + 'Variant Has Untracked Stock' => 'La variante a un stock non suivi', + 'Variant Price' => 'Prix de la variante', + 'Variant SKU' => 'SKU de la variante', + 'Variant Search' => 'Rechercher une variante', + 'Variant Stock' => 'Stock de la variante', + 'Variant Title Format' => 'Format du titre de variante', + 'Variant Tracks Stock' => 'La variante suit le stock', + 'Variant UI Label Format' => 'Format des étiquettes de l\'interface de variantes', + 'Variant has no product.' => 'La variante n\'a pas de produit.', + 'Variants not restored.' => 'Variantes non restaurées.', + 'Variants restored.' => 'Variantes restaurées.', + 'Variants' => 'Variantes', + 'View customer' => 'Voir le client', + 'View order' => 'Afficher la commande', + 'View product type - {productType}' => 'Voir le type de produit - {productType}', + 'View user' => 'Voir l\'utilisateur', + 'View' => 'Voir', + 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Attention, la suppression de cette devise entraînera l\'arrêt de tous les paiements et remboursements dans cette devise, êtes-vous sûr de vouloir supprimer « {name} » ?', + 'Web' => 'Web', + 'Webhook URL' => 'URL du webhook', + 'Weight ({unit})' => 'Poids ({unit})', + 'Weight Rate' => 'Frais selon le poids', + 'Weight Unit' => 'Unité de poids', + 'Weight' => 'Poids', + 'What product URIs should look like for the site.' => 'À quoi les URIs de produits devraient ressembler pour le site.', + 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'À quoi devraient ressembler les titres de produits générés automatiquement. Vous pouvez inclure des balises qui produisent des propriétés des produits, comme {ex1} ou {ex2}. Tous les champs personnalisés utilisés doivent être obligatoires.', + 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'À quoi devraient ressembler les titres de variantes générés automatiquement. Vous pouvez inclure des balises qui produisent des propriétés des variantes, comme {ex1} ou {ex2}. Tous les champs personnalisés utilisés doivent être obligatoires.', + 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Ce à quoi le nom de fichier PDF de commandes devrait ressembler (sans extension). Vous pouvez inclure des étiquettes qui représentent certaines propriétés de la commande, telles que {ex1} ou {ex2}.', + 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'À quoi devraient ressembler les codes article internes (SKU) uniques générés automatiquement, lorsqu\'un champ SKU est soumis sans valeur. Vous pouvez inclure des balises qui produisent des propriétés, comme {ex1} ou {ex2}.', + 'What this PDF will be called in the control panel.' => 'Nom de ce PDF dans le panneau de contrôle.', + 'What this catalog pricing rule will be called in the control panel.' => 'Le nom de cette règle de tarification du catalogue dans le panneau de contrôle.', + 'What this discount will be called in the control panel.' => 'Nom de cette remise dans le panneau de contrôle.', + 'What this email will be called in the control panel.' => 'Nom de cet e-mail dans le panneau de configuration.', + 'What this product type will be called in the control panel.' => 'Nom de ce type de produit dans le panneau de contrôle.', + 'What this sale will be called in the control panel.' => 'Nom de cette promotion dans le panneau de contrôle.', + 'What this shipping category will be called in the control panel.' => 'Nom de cette catégorie de livraison dans le panneau de contrôle.', + 'What this shipping rule will be called in the control panel.' => 'Nom de cette règle de livraison dans le panneau de contrôle.', + 'What this shipping zone will be called in the control panel.' => 'Nom de cette zone de livraison dans le panneau de contrôle.', + 'What this status will be called in the control panel.' => 'Nom de ce statut dans le panneau de contrôle.', + 'What this subscription plan will be called in the control panel.' => 'Nom de cet abonnement dans le panneau de configuration.', + 'What this tax category will be called in the control panel.' => 'Nom de cette catégorie de taxe dans le panneau de contrôle.', + 'What this tax zone will be called in the control panel.' => 'Nom de cette zone de taxe dans le panneau de contrôle.', + 'When this discount is applied to an order, which line items should be discounted?' => 'Lorsque cette réduction est appliquée à une commande, quels postes doivent faire l\'objet d\'une réduction ?', + 'Whether the first available shipping method option should be set automatically on carts.' => 'Indique si la première option du mode d\'expédition disponible doit être définie automatiquement dans les paniers.', + 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Indique si la source de paiement principale de l\'utilisateur doit être définie automatiquement pour les nouveaux paniers.', + 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Indique si les adresses principales de livraison et de facturation de l\'utilisateur doivent être définies automatiquement pour les nouveaux paniers.', + 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Indique si cette règle de tarification du catalogue doit pouvoir être utilisée, indépendamment d\'autres conditions.', + 'Whether this sale should be available for use, regardless of other conditions.' => 'Indique si cette vente doit être utilisable, indépendamment des autres conditions.', + 'Which data to display in the name column in the results table.' => 'Les données à afficher dans la colonne des noms dans le tableau des résultats.', + 'Which product types should this category be available to?' => 'Dans quels types de produits cette catégorie devrait-elle être offerte ?', + 'Which template should be loaded when a product’s URL is requested.' => 'Quel template devrait être chargé quand l’URL d’un produit est demandée.', + 'Width ({unit})' => 'Largeur ({unit})', + 'Width' => 'Largeur', + 'YYYY' => 'AAAA', + 'Yes' => 'Oui', + 'You are not allowed to add a line item.' => 'Vous n\'avez pas le droit d’ajouter un article.', + 'You currently have no emails configured to select for this status.' => 'Vous n\'avez aucune adresse e-mail actuellement configurée à sélectionner pour ce statut.', + 'You do not have permission to load this cart.' => 'Vous n\'avez pas l\'autorisation de charger ce panier.', + 'You must set up at least one gateway that supports subscriptions first.' => 'Vous devez d’abord définir au moins un portail qui prend en charge les abonnements.', + 'You must be logged in or provide a valid token to load this cart.' => 'Vous devez être connecté ou fournir un jeton valide pour charger ce panier.', + 'You must be signed in to create a payment source.' => 'Vous devez être connecté(e) pour créer une source de paiement.', + 'You must be signed in to set a primary payment source.' => 'Vous devez être connecté(e) pour définir une source de paiement principale.', + 'You must make a payment to complete the order.' => 'Vous devez effectuer un paiement pour terminer la commande.', + 'Your Cart Recovery Link' => 'Lien de récupération de votre panier', + 'Your Order PDF Download Link' => 'Lien de téléchargement du PDF de votre commande', + 'Your order is empty' => 'Votre commande est vide', + 'ZIP file' => 'Fichier ZIP', + 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Zéro - Le prix minimum est zéro si les remises sont supérieures à la valeur de la commande.', + 'Zip Code' => 'code postal', + 'all' => 'tous', + 'any' => 'n\'importe quel', + 'average order total' => 'total de commande moyen', + 'billing address' => 'adresse de facturation', + 'donation' => 'don', + 'donations' => 'dons', + 'info' => 'infos', + 'inventory location' => 'emplacement de l\'inventaire', + 'new customers' => 'nouveaux clients', + 'on hand' => 'disponible', + 'only' => 'seulement', + 'order' => 'commande', + 'orders' => 'commandes', + 'price' => 'prix', + 'prices' => 'prix', + 'product variant' => 'variante du produit', + 'product variants' => 'variantes du produit', + 'product' => 'produit', + 'products' => 'produits', + 'repeat customers' => 'clients réguliers', + 'shipping address' => 'adresse de livraison', + 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'Impossible de définir à la fois shippingSameAsBilling et billingSameAsShipping.', + 'subscription' => 'abonnement', + 'subscriptions' => 'abonnements', + 'to' => 'à', + 'transfer' => 'transférer', + 'transfers' => 'transferts', + '{amount} included' => '{amount} inclut', + '{count} Unfulfilled Orders' => '{count} commandes non réalisées', + '{description} is no longer available.' => '{description} n\'est plus disponible.', + '{description} only has {stock} in stock.' => '{description} n\'a que {stock} unités en stock.', + '{from} to {to}' => '{from} à {to}', + '{name} (Primary)' => '{name} (Primaire)', + '{name} (Trashed)' => '{name} (mis à la corbeille)', + '{name} catalog price' => 'prix de catalogue {name}', + '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, one {}=1{commande mise à jour} other{commandes mises à jour}}.', + '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, =1{commande est associée} other{commandes sont associées}} {numUsers, plural, =1{à l\'utilisateur} other{aux utilisateurs}}.', + '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, =1{abonnement est activé} other{abonnements sont activés}} pour {numUsers, plural, =1{l\'utilisateur} other{les utilisateurs}}.', + '{number} more…' => '{number} de plus…', + '{pct} off the discounted item price' => '{pct} sur le prix remisé de l\'article', + '{pct} off the original item price' => '{pct} sur le prix original de l\'article', + '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, =1{n\'a pas été assigné} other{n\'ont pas été assignés}} à un site.', + '{total} in total revenue' => '{total} dans les recettes totales', + '{total} orders' => '{total} commandes', + '{total} saleable across {locationCount} location(s)' => '{total} vendables parmi {locationCount} emplacement(s)', + '{uses} uses across {emails} email addresses' => '{uses} utilisations sur {emails} adresses e-mail', + '{uses} uses across {users} users' => '{uses} utilisations pour {users} utilisateurs', + '“{description}” is currently out of stock.' => '« {description} » est actuellement épuisé.', + '“{key}” has invalid JSON' => '« {key} » possède du JSON invalide', +]; diff --git a/lang/it/commerce.php b/lang/it/commerce.php new file mode 100644 index 0000000000..a2d95831cc --- /dev/null +++ b/lang/it/commerce.php @@ -0,0 +1,1423 @@ + '(nuovo prezzo)', + '(of original price)' => '(del prezzo originale)', + '(off original price)' => '(in meno rispetto al prezzo originale)', + 'A cart number must be specified.' => 'È necessario specificare un numero di carrello.', + 'A cart recovery link has been sent to {email}.' => 'Un link per il recupero del carrello è stato inviato all\'indirizzo {email}.', + 'A cart recovery link will be sent to {email}.' => 'Un link per il recupero del carrello sarà inviato all\'indirizzo {email}.', + 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Un numero di riferimento di facile consultazione verrà generato in base a questo formato al completamento del carrello e alla sua trasformazione in ordine. Per esempio {ex1} o
{ex2}. Il risultato di questo formato deve essere univoco.', + 'A new download link has been sent to {email}' => 'Un nuovo link di download è stato inviato a {email}', + 'A new download link will be sent to {email}' => 'Un nuovo link di download sarà inviato a {email}', + 'A valid email is required to create a customer.' => 'Per creare un cliente è necessario fornire un\'e-mail valida.', + 'Accept' => 'Accetta', + 'Accepted' => 'Accettato', + 'Actions' => 'Azioni', + 'Active Carts' => 'Carrelli attivi', + 'Active subscriptions' => 'Sottoscrizioni attive', + 'Active' => 'Attivo', + 'Add Address' => 'Aggiungi indirizzo', + 'Add a coupon' => 'Aggiungi un coupon', + 'Add a custom line item' => 'Aggiungi una voce personalizzata', + 'Add a line item' => 'Aggiungi una voce', + 'Add a product' => 'Aggiungi prodotto', + 'Add a variant' => 'Aggiungi variante', + 'Add an adjustment' => 'Aggiungi una rettifica', + 'Add an item' => 'Aggiungi un articolo', + 'Add an option' => 'Aggiungi un\'opzione', + 'Add catalog price' => 'Aggiungi prezzo in catalogo', + 'Add' => 'Aggiungi', + 'Additional Actions' => 'Azioni aggiuntive', + 'Additional recipients that should receive this email. Twig code can be used here.' => 'Destinatari aggiuntivi che dovrebbero ricevere questa e-mail. Qui è possibile utilizzare il codice Twig.', + 'Address 1' => 'Indirizzo 1', + 'Address 2' => 'Indirizzo 2', + 'Address 3' => 'Indirizzo 3', + 'Address Line 1' => 'Indirizzo (linea 1)', + 'Address Line 2' => 'Indirizzo (linea 2)', + 'Address Updated.' => 'Indirizzo aggiornato.', + 'Address copied to user.' => 'Indirizzo copiato su utente.', + 'Address not found.' => 'Indirizzo non trovato.', + 'Adjust Quantity' => 'Rettifica quantità', + 'Adjust by' => 'Regola per', + 'Adjust price when included rate is disqualified?' => 'Rettificare il prezzo quando l\'aliquota inclusa non è valida?', + 'Adjustments' => 'Rettifiche', + 'Admin Notices' => 'Avvisi dell\'amministratore', + 'Administrative Area Code of Origin' => 'Codice dell\'area amministrativa di provenienza', + 'Advanced' => 'Impostazioni avanzate', + 'All Orders' => 'Tutti gli ordini', + 'All Totals' => 'Tutti i totali', + 'All Transfers' => 'Tutti i trasferimenti', + 'All active subscriptions' => 'Tutte le sottoscrizioni attive', + 'All customers' => 'Tutti i clienti', + 'All products' => 'Tutti i prodotti', + 'All variants must have a SKU.' => 'Tutte le varianti devono avere uno SKU.', + 'All' => 'Tutti', + 'Allow Checkout Without Payment' => 'Consenti il checkout senza pagamento', + 'Allow Empty Cart On Checkout' => 'Consenti il carrello vuoto al checkout', + 'Allow Partial Payment On Checkout' => 'Consenti il pagamento parziale al checkout', + 'Allow out of stock purchases' => 'Consenti acquisti fuori stock', + 'Allow' => 'Consenti', + 'Allowed Qty' => 'Qtà consentita', + 'Alternative Phone' => 'N. telefono alternativo', + 'Amount' => 'Importo', + 'An ID must be provided' => 'È necessario fornire un documento d\'identità', + 'An error occurred while generating this PDF.' => 'Si è verificato un errore durante la generazione di questo PDF.', + 'Any' => 'Qualsiasi', + 'Anywhere' => 'Dovunque', + 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Sei sicuro di voler archiviare il piano di sottoscrizione “{name}”? Questa operazione NON annullerà le sottoscrizioni esistenti.', + 'Are you sure you want to capture this transaction?' => 'Sei sicuro di voler acquisire questa transazione?', + 'Are you sure you want to complete this order?' => 'Sei sicuro di voler completare questo ordine?', + 'Are you sure you want to delete the selected orders?' => 'Sei sicuro di voler eliminare gli ordini selezionati?', + 'Are you sure you want to delete the selected product and its variants?' => 'Sei sicuro di voler eliminare il prodotto selezionato e le relative varianti?', + 'Are you sure you want to delete this shipping rule?' => 'Sei sicuro di voler eliminare questa regola di spedizione?', + 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Sei sicuro di voler eliminare “{name}” e tutti i relativi prodotti? Assicurati di avere un backup del tuo database prima di eseguire questa azione distruttiva.', + 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Sei sicuro di voler eliminare "{name}"? Tutte le voci con questo stato verranno impostate su nessuno stato.', + 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Contrassegnare questo trasferimento come in sospeso? Verrà visualizzato come in arrivo nella destinazione.', + 'Are you sure you want to overwrite the billing address?' => 'Sei sicuro di voler sovrascrivere l\'indirizzo di fatturazione?', + 'Are you sure you want to overwrite the shipping address?' => 'Sei sicuro di voler sovrascrivere l\'indirizzo di spedizione?', + 'Are you sure you want to permanently delete this store and everything in it?' => 'Sei sicuro di voler eliminare definitivamente questo store e tutto ciò che contiene?', + 'Are you sure you want to refund this transaction?' => 'Sei sicuro di voler rimborsare questa transazione?', + 'Are you sure you want to remove this customer?' => 'Sei sicuro di voler rimuovere questo cliente?', + 'Are you sure you want to save this as a new shipping rule?' => 'Sei sicuro di voler salvare questa immissione come nuova regola di spedizione?', + 'Are you sure you want to send email: {name}?' => 'Sei sicuro di voler inviare l\'email: {name}?', + 'At least one site must be enabled for the product type.' => 'Almeno un sito deve essere abilitato per il tipo di prodotto.', + 'Attempted Payments' => 'Tentativi di pagamento', + 'Attention' => 'Attenzione', + 'Authorize Only (Manually Capture)' => 'Autorizza solo (acquisizione manuale)', + 'Auto Set Cart Shipping Method Option' => 'Impostazione automatica del metodo di spedizione del carrello', + 'Auto Set New Cart Addresses' => 'Impostazione automatica dei nuovi indirizzi del carrello', + 'Auto Set Payment Source' => 'Impostazione automatica della fonte di pagamento', + 'Automatic SKU Format' => 'Formato SKU automatico', + 'Available Shipping Categories' => 'Categorie di spedizione disponibili', + 'Available Tax Categories' => 'Categorie fiscali disponibili', + 'Available for purchase' => 'Disponibile all’acquisto', + 'Available for purchase?' => 'Disponibile all’acquisto?', + 'Available inventory for "{description}" has gone below zero.' => 'Le scorte disponibili per “{description}” sono pari a zero.', + 'Available to Product Types' => 'Disponibile per tipi di prodotto', + 'Available' => 'Disponibile', + 'Available?' => 'Disponibile?', + 'Average Order Total' => 'Totale medio degli ordini', + 'Average' => 'Media', + 'BCC’d Recipient' => 'Destinatario in Ccn', + 'Bad Request' => 'Richiesta non valida', + 'Bad address ID.' => 'ID indirizzo non valido.', + 'Bad order ID.' => 'ID ordine non valido.', + 'Base Price' => 'Prezzo base', + 'Base Promotional Price' => 'Prezzo promozionale base', + 'Base Rate' => 'Tasso base', + 'Base' => 'Base', + 'Bcc' => 'Ccn', + 'Billing Address' => 'Indirizzo di fatturazione', + 'Billing Business Name' => 'Nome azienda per la fatturazione', + 'Billing First Name' => 'Nome per la fatturazione', + 'Billing Full Name' => 'Nome completo per la fatturazione', + 'Billing Last Name' => 'Cognome per la fatturazione', + 'Billing address required.' => 'Indirizzo di fatturazione obbligatorio.', + 'Billing detail update URL' => 'URL di aggiornamento dei dettagli di fatturazione', + 'Billing issues' => 'Problemi di fatturazione', + 'Billing' => 'Fatturazione', + 'Both (Line item price + Line item shipping costs)' => 'Entrambi (prezzo per voce + costi di spedizione per voce)', + 'Business ID' => 'ID azienda', + 'Business Name' => 'Nome azienda', + 'Business Tax ID' => 'Partita IVA aziendale', + 'CC’d Recipient' => 'Destinatario in Cc', + 'CVV' => 'CVV', + 'Can be used as an internal reference.' => 'Può essere utilizzato come riferimento interno.', + 'Can not complete payment for missing transaction.' => 'Impossibile completare il pagamento per transazione assente.', + 'Can not create a new order' => 'Impossibile creare un nuovo ordine', + 'Can not find an order to pay.' => 'Impossibile trovare un ordine da pagare.', + 'Can not find enabled email.' => 'Impossibile trovare l\'email abilitata.', + 'Can not find order' => 'Impossibile trovare l\'ordine', + 'Can not find order.' => 'Impossibile trovare l\'ordine.', + 'Can not find the transaction to refund' => 'Impossibile trovare la transazione da rimborsare', + 'Can not move between these inventory types.' => 'Impossibile spostarsi tra questi tipi di inventario.', + 'Can not refund amount greater than the remaining amount' => 'Impossibile rimborsare un importo maggiore rispetto all\'importo rimanente', + 'Cancel subscription' => 'Annulla sottoscrizione', + 'Cancel with gateway now' => 'Annulla ora tramite gateway', + 'Cancel' => 'Annulla', + 'Cancellation date' => 'Data annullamento', + 'Cancellation' => 'Annullamento', + 'Cannot switch plans for this subscription.' => 'Impossibile cambiare piano per questa sottoscrizione.', + 'Can’t preview this email.' => 'Impossibile visualizzare l\'anteprima di questa e-mail.', + 'Capture payment' => 'Acquisisci pagamento', + 'Capture' => 'Acquisisci', + 'Card Holder' => 'Proprietario della carta', + 'Card Number' => 'Numero di carta', + 'Card' => 'Carta', + 'Cart Recovery Link' => 'Link per il recupero del carrello', + 'Cart forgotten.' => 'Carrello dimenticato.', + 'Cart updated.' => 'Carrello aggiornato.', + 'Cart {number}' => 'Carrello {number}', + 'Catalog Pricing Rule' => 'Regola di prezzo del catalogo', + 'Catalog pricing rule description.' => 'Descrizione della regola di prezzo del catalogo.', + 'Catalog pricing rule saved.' => 'Regola di prezzo del catalogo salvata.', + 'Catalog pricing rules deleted.' => 'Regole di prezzo del catalogo eliminate.', + 'Catalog pricing rules updated.' => 'Regole di prezzo del catalogo aggiornate.', + 'Categories Relationship Type' => 'Tipo di rapporto delle categorie', + 'Categories' => 'Categorie', + 'Category Rate Overrides' => 'Ignora tariffa categoria', + 'Centimeters (cm)' => 'Centimetri (cm)', + 'Changing this value may affect your ability to refund existing transactions.' => 'Modificare questo valore può influenzare la tua capacità di rimborsare le transazioni esistenti.', + 'Choose a color to represent the order’s status' => 'Scegli un colore per rappresentare lo stato dell\'ordine', + 'Choose a new customer' => 'Scegli un nuovo cliente', + 'Choose adjustment values to include when calculating the product revenue total.' => 'Scegli i valori di rettifica da includere nel calcolo del ricavo totale del prodotto.', + 'Choose the currency’s ISO code.' => 'Scegliere il codice ISO della valuta.', + 'Choose the destination inventory location for the existing on hand stock.' => 'Scegli la sede di destinazione dell\'inventario per le scorte esistenti.', + 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Scegli i siti in cui questo tipo di prodotto deve essere disponibile e configura le impostazioni specifiche del sito.', + 'City' => 'Città', + 'Clear counter' => 'Azzera contatore', + 'Clear notices' => 'Cancella notifiche', + 'Close' => 'Chiudi', + 'Code' => 'Codice', + 'Collated PDF' => 'PDF fascicolati', + 'Color' => 'Colore', + 'Commerce Products' => 'Prodotti Commerce', + 'Commerce Settings' => 'Impostazioni Commerce', + 'Commerce Variants' => 'Varianti di Commerce', + 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Impossibile inviare l’email di Commerce “{email}” per l’ordine “{order}”.', + 'Commerce order exports' => 'Esportazioni ordini da Commerce', + 'Commerce' => 'Commerce', + 'Committed' => 'Impegnato', + 'Completed Email' => 'E-mail completa', + 'Completed' => 'Completato', + 'Completing order failed.' => 'Completamento dell\'ordine fallito.', + 'Condition' => 'Condizione', + 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Le condizioni vengono confrontate con un ordine prima di esaminare le regole. Ciò è utile se si desidera qualificare in anticipo la disponibilità di un metodo o se esistono condizioni comuni per tutte le regole per questo metodo.', + 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Le condizioni vengono confrontate con il cliente dell\'ordine prima di esaminare le regole. Ciò è utile se si desidera qualificare in anticipo la disponibilità di un metodo o se esistono condizioni comuni per tutte le regole per questo metodo.', + 'Conditions' => 'Condizioni', + 'Contains Purchasables' => 'Contiene prodotti disponibili all’acquisto', + 'Control Panel Settings' => 'Impostazioni del pannello di controllo', + 'Control panel' => 'Pannello di controllo', + 'Conversion Rate' => 'Tasso di conversione', + 'Converted Price' => 'Prezzo convertito', + 'Copied!' => 'Copiato!', + 'Copy the URL' => 'Copia l\'URL', + 'Copy to {location}' => 'Copia in {location}', + 'Copy' => 'Copia', + 'Costs' => 'Costi', + 'Could not archive gateway.' => 'Impossibile archiviare il gateway.', + 'Could not cancel “{reference}”.' => 'Impossibile cancellare “{reference}”.', + 'Could not create the payment source.' => 'Impossibile creare la fonte di pagamento.', + 'Could not delete shipping rule' => 'Impossibile eliminare le regole di spedizione', + 'Could not delete shipping zone' => 'Impossibile eliminare la zona di spedizione', + 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Impossibile eliminare {count, number} {count, plural, one{categoria} other{categorie}} di spedizione.', + 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Impossibile eliminare {count, number} {count, plural, one{metodo} other{metodi}} e regole di spedizione.', + 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Impossibile eliminare {count, number} {count, plural, one{categoria} other{categorie}} fiscale/i.', + 'Could not find the email or template.' => 'Non è stato possibile trovare l\'email o il template.', + 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Impossibile contrassegnare l’ordine {number} come completato. Il salvataggio non è andato a buon fine in fase di completamento dell’ordine con errori: {order}', + 'Could not reactivate “{reference}”.' => 'Impossibile riattivare “{reference}”.', + 'Could not send email' => 'Non è stato possibile inviare l’email', + 'Could not switch “{reference}” to “{plan}”.' => 'Impossibile passare da “{reference}” a “{plan}”.', + 'Could not update orders address.' => 'Non è stato possibile aggiornare gli indirizzi degli ordini.', + 'Couldn’t archive Line Item Status.' => 'Non è stato possibile salvare lo stato della voce.', + 'Couldn’t archive Order Status.' => 'Non è stato possibile archiviare lo stato dell’ordine.', + 'Couldn’t capture transaction.' => 'Non è stato possibile acquisire la transazione.', + 'Couldn’t capture transaction: {message}' => 'Non è stato possibile acquisire la transazione: {message}', + 'Couldn’t delete email.' => 'Impossibile eliminare l\'e-mail.', + 'Couldn’t delete the payment source.' => 'Impossibile eliminare la fonte di pagamento.', + 'Couldn’t get order.' => 'Non è stato possibile recuperare l’ordine.', + 'Couldn’t recalculate order.' => 'Non è stato possibile ricalcolare l’ordine.', + 'Couldn’t refund transaction.' => 'Non è stato possibile rimborsare la transazione.', + 'Couldn’t refund transaction: {message}' => 'Non è stato possibile rimborsare la transazione: {message}', + 'Couldn’t reorder Line Item Statuses.' => 'Non è stato possibile riordinare gli stati delle voci.', + 'Couldn’t reorder Order Statuses.' => 'Non è stato possibile riordinare gli stati ordine.', + 'Couldn’t reorder PDFs.' => 'Impossibile riordinare i PDF.', + 'Couldn’t reorder discounts.' => 'Non è stato possibile riordinare gli sconti.', + 'Couldn’t reorder gateways.' => 'Impossibile riordinare i gateway.', + 'Couldn’t reorder plans.' => 'Non è stato possibile riordinare i piani.', + 'Couldn’t reorder rules.' => 'Impossibile riordinare le regole.', + 'Couldn’t reorder sale.' => 'Impossibile riordinare la vendita promozionale.', + 'Couldn’t reorder sales.' => 'Impossibile riordinare le vendite.', + 'Couldn’t reorder statuses.' => 'Impossibile riordinare gli stati.', + 'Couldn’t reorder stores.' => 'Impossibile riordinare gli store.', + 'Couldn’t save PDF.' => 'Non è stato possibile salvare il PDF.', + 'Couldn’t save catalog pricing rule.' => 'Non è stato possibile salvare la regola di prezzo in catalogo.', + 'Couldn’t save currency.' => 'Non è stato possibile salvare la valuta.', + 'Couldn’t save discount.' => 'Non è stato possibile salvare lo sconto.', + 'Couldn’t save email.' => 'Non è stato possibile salvare l’email.', + 'Couldn’t save gateway.' => 'Impossibile salvare il gateway.', + 'Couldn’t save inventory location.' => 'Impossibile salvare la sede dell\'inventario.', + 'Couldn’t save line item status.' => 'Non è stato possibile salvare lo stato della voce.', + 'Couldn’t save order fields.' => 'Impossibile salvare i campi dell\'ordine.', + 'Couldn’t save order status.' => 'Non è stato possibile salvare lo stato dell’ordine.', + 'Couldn’t save order.' => 'Non è stato possibile salvare l’ordine.', + 'Couldn’t save product type.' => 'Non è stato possibile salvare il tipo di prodotto.', + 'Couldn’t save sale.' => 'Non è stato possibile salvare la vendita promozionale.', + 'Couldn’t save settings.' => 'Non è stato possibile salvare le impostazioni.', + 'Couldn’t save shipping category.' => 'Non è stato possibile salvare la categoria di spedizione.', + 'Couldn’t save shipping method.' => 'Non è stato possibile salvare il metodo di spedizione.', + 'Couldn’t save shipping rule.' => 'Non è stato possibile salvare la regola di spedizione.', + 'Couldn’t save shipping zone.' => 'Non è stato possibile salvare l’area di spedizione.', + 'Couldn’t save store.' => 'Impossibile salvare store.', + 'Couldn’t save subscription fields.' => 'Impossibile salvare i campi di sottoscrizione.', + 'Couldn’t save subscription plan.' => 'Impossibile salvare il piano di sottoscrizione.', + 'Couldn’t save subscription.' => 'Non è stato possibile salvare la sottoscrizione.', + 'Couldn’t save tax category.' => 'Non è stato possibile salvare la categoria fiscale.', + 'Couldn’t save tax rate.' => 'Non è stato possibile salvare l’aliquota fiscale.', + 'Couldn’t save tax zone.' => 'Non è stato possibile salvare la zona fiscale.', + 'Couldn’t save transfer fields.' => 'Impossibile salvare i campi del trasferimento.', + 'Couldn’t update catalog pricing rule statuses.' => 'Non è stato possibile aggiornare le regole di prezzo in catalogo.', + 'Couldn’t update status.' => 'Impossibile aggiornare lo stato.', + 'Couldn’t updated sales status.' => 'Non è stato possibile aggiornare lo stato della vendita promozionale.', + 'Country Code of Origin' => 'Codice del Paese di origine', + 'Country List' => 'Elenco di Paesi', + 'Country not allowed.' => 'Paese non consentito.', + 'Country' => 'Paese', + 'Coupon Code' => 'Codice promozionale', + 'Coupon can not apply discount to this order due to address mismatch.' => 'Impossibile applicare lo sconto a questo ordine tramite il codice promozionale a causa della mancata corrispondenza dell\'indirizzo.', + 'Coupon can not apply discount to this order due to customer mismatch.' => 'Impossibile applicare lo sconto a questo ordine tramite il codice promozionale a causa della mancata corrispondenza del cliente.', + 'Coupon can not apply discount to this order.' => 'Impossibile applicare lo sconto a questo ordine tramite il codice promozionale.', + 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Il codice coupon “{code}” è già utilizzato dallo sconto “{name}”.', + 'Coupon codes cannot be blank.' => 'I codici promozionali non possono essere vuoti.', + 'Coupon codes must be unique.' => 'I codici promozionali devono essere univoci.', + 'Coupon format is required and must contain at least one `#`.' => 'Il formato dei codici promozionali è obbligatorio e deve contenere almeno un `#`.', + 'Coupon not valid.' => 'Coupon non valido.', + 'Coupon removed: {explanation}' => 'Coupon rimosso: {explanation}', + 'Coupons' => 'Codici promozionali', + 'Craft Commerce - Administration' => 'Craft Commerce - Amministrazione', + 'Craft Commerce - Inventory' => 'Craft Commerce - Inventario', + 'Craft Commerce - Orders' => 'Craft Commerce - Ordini', + 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Tipo di prodotto - {name}', + 'Craft Commerce - Subscriptions' => 'Craft Commerce - Abbonamenti', + 'Create a Discount' => 'Crea uno sconto', + 'Create a Subscription Plan' => 'Crea un piano di sottoscrizione', + 'Create a new PDF' => 'Crea nuovo PDF', + 'Create a new catalog pricing rule' => 'Crea una nuova regola di prezzo in catalogo', + 'Create a new currency' => 'Crea una nuova valuta', + 'Create a new email' => 'Crea una nuova email', + 'Create a new gateway' => 'Crea un nuovo gateway', + 'Create a new line item status' => 'Crea un nuovo stato voce', + 'Create a new order status' => 'Crea un nuovo stato ordine', + 'Create a new product type' => 'Crea un nuovo tipo di prodotto', + 'Create a new sale' => 'Crea una nuova vendita promozionale', + 'Create a new shipping category' => 'Crea una nuova categoria di spedizione', + 'Create a new shipping method' => 'Crea un nuovo metodo di spedizione', + 'Create a new shipping rule' => 'Crea una nuova regola di spedizione', + 'Create a new tax category' => 'Crea una nuova categoria fiscale', + 'Create a new tax rate' => 'Crea una nuova aliquota fiscale', + 'Create a product type' => 'Crea un tipo di prodotto', + 'Create a shipping zone' => 'Crea un’area di spedizione', + 'Create a tax zone' => 'Crea una zona fiscale', + 'Create catalog pricing rules' => 'Crea regole di prezzo in catalogo', + 'Create customer: “{email}”' => 'Crea cliente: “{email}”', + 'Create discounts' => 'Crea sconti', + 'Create discount…' => 'Creazione sconto in corso...', + 'Create rules that allow this discount to match the order.' => 'Crea regole che permettono a questo sconto di corrispondere all\'ordine.', + 'Create rules that allow this discount to match the order’s billing address.' => 'Crea regole che permettono a questo sconto di corrispondere all\'indirizzo di fatturazione dell\'ordine.', + 'Create rules that allow this discount to match the order’s customer.' => 'Crea regole che permettono a questo sconto di corrispondere al cliente dell\'ordine.', + 'Create rules that allow this discount to match the order’s shipping address.' => 'Crea regole che permettono a questo sconto di corrispondere all\'indirizzo di spedizione dell\'ordine.', + 'Create rules that allow this gateway to match the billing address.' => 'Crea delle regole che consentano a questo gateway di corrispondere all\'indirizzo di fatturazione.', + 'Create rules that allow this gateway to match the order.' => 'Crea regole che permettono a questo gateway di corrispondere all\'ordine.', + 'Create rules that allow this gateway to match the shipping address.' => 'Crea delle regole che consentano a questo gateway di corrispondere all\'indirizzo di spedizione.', + 'Create sales' => 'Crea vendite promozionali', + 'Create sale…' => 'Creazione vendita promozionale in corso...', + 'Created' => 'Creato', + 'Credit Card Payment Type' => 'Tipo di pagamento con carta di credito', + 'Currency Code' => 'Codice valuta', + 'Currency saved.' => 'Valuta salvata.', + 'Currency' => 'Valuta', + 'Current' => 'Attuale', + 'Custom 1' => 'Personalizzato 1', + 'Custom 2' => 'Personalizzato 2', + 'Custom 3' => 'Personalizzato 3', + 'Custom 4' => 'Personalizzato 4', + 'Custom' => 'Personalizzato', + 'Customer Enabled?' => 'Cliente abilitato?', + 'Customer ID is required.' => 'È richiesto un ID cliente.', + 'Customer Note' => 'Nota del cliente', + 'Customer Notices' => 'Notifiche del cliente', + 'Customer data' => 'Dati dei clienti', + 'Customer' => 'Cliente', + 'Damaged' => 'Danneggiato', + 'Data shown might be outdated.' => 'I dati riportati potrebbero essere obsoleti.', + 'Date Authorized' => 'Data di autorizzazione', + 'Date Created' => 'Data creazione', + 'Date First Paid' => 'Data primo pagamento', + 'Date Ordered' => 'Data ordine', + 'Date Paid' => 'Data pagamento', + 'Date Updated' => 'Data aggiornamento', + 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Data in cui la regola di prezzo in catalogo verrà attivata. Lasciare vuoto per data di inizio illimitata', + 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Data in cui lo sconto verrà attivato. Lasciare vuoto per data di inizio illimitata', + 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Data in cui la vendita promozionale verrà attivata. Lasciare vuoto per data di inizio illimitata', + 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Data in cui la regola di prezzo in catalogo terminerà. Lasciare vuoto per data di fine illimitata', + 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Data in cui lo sconto terminerà. Lasciare vuoto per data di fine illimitata', + 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Data in cui la vendita promozionale terminerà. Lasciare vuoto per data di fine illimitata', + 'Date' => 'Data', + 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Predefinito - Consente un prezzo negativo se gli sconti sono di importo superiore al valore dell\'ordine.', + 'Default Category' => 'Categoria predefinita', + 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Visualizzazione predefinita del pannello di controllo di Commerce. Se non dispone del permesso, l\'utente tornerà a una posizione a cui può accedere.', + 'Default Order PDF' => 'PDF ordine predefinito', + 'Default Per Item Rate' => 'Tariffa in base ad articolo predefinita', + 'Default Percentage Rate' => 'Tariffa in base a percentuale predefinita', + 'Default Status?' => 'Stato predefinito?', + 'Default View' => 'Vista predefinita', + 'Default Weight Rate' => 'Tariffa in base al peso predefinita', + 'Default Zone' => 'Zona predefinita', + 'Default status?' => 'Stato predefinito?', + 'Default to this tax zone when no billing address is set' => 'Tasso di imposta predefinito se non viene impostato nessun indirizzo di fatturazione', + 'Default to this tax zone when no shipping address is set' => 'Zona fiscale predefinita se non viene impostato nessun indirizzo di spedizione', + 'Default variant updated.' => 'Variante predefinita aggiornata.', + 'Default' => 'Impostazione predefinita', + 'Default?' => 'Ripristinare le impostazioni predefinite?', + 'Delete catalog pricing rules' => 'Elimina regole di prezzo in catalogo', + 'Delete discounts' => 'Elimina sconti', + 'Delete orders' => 'Elimina ordini', + 'Delete sales' => 'Elimina vendite promozionali', + 'Delete' => 'Elimina', + 'Deleting the {location} location.' => 'Eliminazione della sede {location}.', + 'Describe this rule.' => 'Descrivere questa regola.', + 'Describe this shipping zone.' => 'Descrivere quest’area di spedizione.', + 'Describe this tax zone.' => 'Descrivere questa zona fiscale.', + 'Description' => 'Descrizione', + 'Destination Inventory Location' => 'Sede dell\'inventario di destinazione', + 'Destination' => 'Destinazione', + 'Details' => 'Dettagli', + 'Dimension Unit' => 'Unità dimensioni', + 'Dimensions' => 'Dimensioni', + 'Disabled' => 'Disabilitato', + 'Disallow' => 'Nega', + 'Discount all line items' => 'Sconta tutte le voci', + 'Discount description.' => 'Descrizione sconto.', + 'Discount is not allowed for the order' => 'Sconto non consentito per l\'ordine', + 'Discount is out of date.' => 'Sconto scaduto.', + 'Discount saved.' => 'Sconto salvato.', + 'Discount the matching items only' => 'Sconta solo gli articoli corrispondenti', + 'Discount use has reached its limit.' => 'Limite raggiunto per uso sconti.', + 'Discount' => 'Sconto', + 'Discounted Item Subtotal' => 'Totale parziale dell\'articolo scontato', + 'Discounted Items' => 'Articoli scontati', + 'Discounts deleted.' => 'Sconti eliminati.', + 'Discounts reordered.' => 'Sconti riordinati.', + 'Discounts updated.' => 'Sconti aggiornati.', + 'Discounts' => 'Sconti', + 'Disqualify with valid business tax ID?' => 'Annullare con partita IVA aziendale valida?', + 'Do not apply subsequent matching sales beyond applying this sale.' => 'Non applicare vendite corrispondenti successive oltre all’applicazione di questa vendita.', + 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Non applicare questa aliquota se l\'indirizzo dell\'ordine ha una delle partite IVA aziendali valide selezionate.', + 'Do not attach a PDF to this email' => 'Non allegare un PDF a questa email', + 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Non richiamare il ricalcolo dell\'ordine (Numero: {orderNumber}) in presenza di errori.', + 'Donation can not be zero.' => 'La donazione non può essere pari a zero.', + 'Donation needs to be an amount.' => 'La donazione deve essere un importo numerico.', + 'Donation settings saved.' => 'Impostazioni di donazione salvate.', + 'Donation' => 'Donazione', + 'Donations' => 'Donazioni', + 'Done' => 'Fine', + 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Non applicare sconti successivi a un ordine se viene applicato questo sconto', + 'Download PDF' => 'Scarica PDF', + 'Download PDF…' => 'Scarica PDF…', + 'Download Type' => 'Tipo di download', + 'Download' => 'Scarica', + 'Draft' => 'Bozza', + 'Dummy gateway payment failed.' => 'Pagamento gateway provvisorio non riuscito.', + 'Duplicate options exist' => 'Esistono opzioni duplicate', + 'Duration' => 'Durata', + 'EU VAT ID' => 'Partita IVA europea', + 'Edit address' => 'Modifica indirizzo', + 'Edit adjustments' => 'Modifica rettifiche', + 'Edit catalog pricing rules' => 'Modifica regole di prezzo in catalogo', + 'Edit discounts' => 'Modifica sconti', + 'Edit options' => 'Modifica opzioni', + 'Edit orders' => 'Modifica ordini', + 'Edit sales' => 'Modifica vendite promozionali', + 'Edit' => 'Modifica', + 'Effect' => 'Effetto', + 'Either (Default) - The relationship field is on the purchasable or the category' => 'Qualsiasi (predefinito) - Il campo relazione è sul campo disponibile all\'acquisto o categoria', + 'Either way' => 'Entrambi i casi', + 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Errore di generazione del PDF dell\'email per l\'email “{email}”. Ordine: “{order}”. Errore template PDF: “{message}” {file}:{line}', + 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'Il template PDF dell’email non esiste in “{templatePath}” per l’email “{email}”. Ordine: “{order}”.', + 'Email Subject' => 'Oggetto dell’email', + 'Email error. No email address found for order. Order: “{order}”' => 'Errore email. Nessun indirizzo email trovato per l’ordine. Ordine: “{order}”', + 'Email is not enabled.' => 'Email non abilitata.', + 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Il template dell’email di testo semplice non esiste in “{templatePath}” ed è risultato essere “{templateParsedPath}” per l’email “{email}”. Ordine: “{order}”.', + 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email di testo semplice per l\'email “{email}”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', + 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del percorso al template email di testo semplice per l\'email “{email}” in “Template Path”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', + 'Email required to make payments on a completed order.' => 'Email necessaria per effettuare i pagamenti per un ordine completato.', + 'Email saved.' => 'Email salvata.', + 'Email sent' => 'Email inviata', + 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Il template dell’email non esiste in “{templatePath}” ed è risultato essere “{templateParsedPath}” per l’email “{email}”. Ordine: “{order}”.', + 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email per l\'email personalizzata “{email}” in “To:”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email per l\'email “{email}” in “BCC:”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email per l\'email “{email}” in “CC:”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email per l\'email “{email}” in “ReplyTo:”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email per l\'email “{email}” in “Subject:”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', + 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email per l\'email “{email}”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', + 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del percorso al template email per l\'email “{email}” in “Template Path”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', + 'Email unavailable.' => 'E-mail non disponibile.', + 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'Non è stato possibile inviare l\'email “{email}” per l\'ordine “{order}”. Errore: {error} {file}:{line}', + 'Email “{email}” for order {order} was cancelled.' => 'L’email “{email}” per l’ordine {order} è stata cancellata.', + 'Email' => 'Email', + 'Emails' => 'Email', + 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Abilita l\'opzione se questa aliquota deve essere inclusa nel prezzo dell\'imponibile invece di aggiungere un costo all\'ordine.', + 'Enable structure for products of this type' => 'Abilita struttura per i prodotti di questo tipo', + 'Enable this discount' => 'Abilita questo sconto', + 'Enable this rule' => 'Abilita questa regola', + 'Enable this sale' => 'Abilita questa vendita promozionale', + 'Enable this shipping method on the front end' => 'Abilita questo metodo di spedizione in front end', + 'Enable this shipping rule' => 'Abilita questa regola di spedizione', + 'Enable this tax rate' => 'Abilita questa aliquota fiscale', + 'Enabled for customers to select during checkout?' => 'Abilitato per la selezione da parte dei clienti in fase di pagamento?', + 'Enabled for customers to select?' => 'Abilitato per la selezione da parte dei clienti?', + 'Enabled' => 'Pubblicato', + 'Enabled?' => 'Abilitato?', + 'End Date' => 'Data di fine', + 'Enter SKU' => 'Inserisci SKU', + 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Inserisci un nome facilmente comprensibile per questa aliquota fiscale da usare nel pannello di controllo.', + 'Enter a percentage like {ex1} or {ex2}.' => 'Inserisci una percentuale come {ex1} o {ex2}.', + 'Enter coupon code' => 'Inserisci il codice coupon', + 'Enter reference' => 'Inserisci riferimento', + 'Error refunding transaction: {transactionHash}' => 'Errore di rimborso della transazione: {transactionHash}', + 'Every new store must be assigned to at least one site.' => 'Ogni nuovo store deve essere assegnato ad almeno un sito.', + 'Everywhere' => 'Ovunque', + 'Example' => 'Esempio', + 'Exclude this discount for products that are already on promotion' => 'Escludi questo sconto per i prodotti che sono già in promozione', + 'Expired Link' => 'Link scaduto', + 'Expired' => 'Scaduto', + 'Expiry Date' => 'Data di scadenza', + 'Expiry date' => 'Data di scadenza', + 'Expiry' => 'Scadenza', + 'Failed to receive transfer: {error}' => 'Impossibile ricevere il trasferimento: {error}', + 'Failed to send email. Please try again.' => 'Impossibile inviare l\'email. Riprova.', + 'Failed to start' => 'Avvio fallito', + 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Impossibile aggiornare {num, plural, one {}=1{stato dell\'ordine} other{stati degli ordini}}.', + 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Impossibile aggiornare lo stato dell\'ordine su {num, plural, one {}=1{ordine} other{ordini}}.', + 'Feet (ft)' => 'Piedi (ft)', + 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Condizioni di applicazione filtri che descrivono a quali ordini si applica questa regola. Scrivere 0 per saltare una condizione.', + 'First Name' => 'Nome', + 'Flat Amount Off Order' => 'Importo forfettario di sconto sull\'ordine', + 'Flat Order Discount Amount Off' => 'Importo di sconto per ordine forfettario', + 'Free Order Payment Strategy' => 'Strategia di pagamento per ordini gratuiti', + 'Free Shipping' => 'Spedizione gratuita', + 'Free orders are processed by the payment gateway' => 'Gli ordini gratuiti vengono elaborati dal gateway di pagamento', + 'Free orders complete immediately' => 'Ordini gratuiti da completare immediatamente', + 'Free shipping can only be for whole order or matching items, not both.' => 'La spedizione gratuita può essere applicata solo all\'intero ordine o agli articoli abbinati, non a entrambi.', + 'From Name' => 'Nome provenienza', + 'Fulfill' => 'Evadi', + 'Fulfilled' => 'Evaso', + 'Fulfillment' => 'Evasione', + 'Full Name' => 'Nome completo', + 'Gateway Code' => 'Codice del gateway', + 'Gateway Message' => 'Messaggio del gateway', + 'Gateway Reference' => 'Riferimento del gateway', + 'Gateway Response' => 'Risposta del gateway', + 'Gateway doesn’t support authorize' => 'Il gateway non supporta l’autorizzazione', + 'Gateway doesn’t support partial refunds.' => 'Il gateway non supporta rimborsi parziali.', + 'Gateway doesn’t support purchase' => 'Il gateway non supporta l’acquisto', + 'Gateway doesn’t support refunds.' => 'Il gateway non supporta i rimborsi.', + 'Gateway saved.' => 'Gateway salvato.', + 'Gateway' => 'Gateway', + 'Gateways reordered.' => 'Gateway riordinati.', + 'Gateways' => 'Gateway', + 'General Settings' => 'Impostazioni generali', + 'General' => 'Generale', + 'Generate' => 'Genera', + 'Generated Coupon Format' => 'Formato codice promozionale generato', + 'Grams (g)' => 'Grammi (g)', + 'Groups for which this sale will be applicable to.' => 'Gruppi a cui questa vendita promozionale sarà applicabile.', + 'HTML Email Template Path' => 'Percorso template email HTML', + 'Handle' => 'Puntatore', + 'Harmonized System Code' => 'Codice sistema armonizzato', + 'Has Admin Notices' => 'Contiene avvisi dell\'amministratore', + 'Has Emails?' => 'Ha delle email?', + 'Has Free Shipping' => 'Spedizione gratuita disponibile', + 'Has Orders' => 'Ha ordini', + 'Has Purchasable' => 'Ha prodotti disponibili all’acquisto', + 'Has Variants?' => 'Ha delle varianti?', + 'Height ({unit})' => 'Altezza ({unit})', + 'Height' => 'Altezza', + 'Hide snapshot' => 'Nascondi snapshot', + 'History' => 'Cronologia', + 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Per quanto tempo (in secondi) un link di download PDF deve rimanere valido prima di scadere. Il valore predefinito è 86.400 (24 ore).', + 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Quante volte un indirizzo email può utilizzare questo sconto. Si applica a tutti gli ordini precedenti, sia di ospiti che di utenti. Impostare su zero per l’uso illimitato da parte di ospiti e di utenti.', + 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Quante volte un utente può usare questo sconto. Se questa opzione è impostata a un valore non nullo, lo sconto sarà disponibile solo agli utenti registrati.', + 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Quante volte gli ospiti o gli utenti registrati potranno utilizzare questo sconto in totale. Impostare su zero per l’uso illimitato.', + 'How products should be labeled within the control panel.' => 'Come devono essere etichettati i prodotti nel pannello di controllo.', + 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'Come sono correlati gli articolo disponibili all\'acquisto e le categorie, il che determina gli articoli corrispondenti. Vedi [Terminologia delle relazioni]({link}).', + 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'Come verrà descritto questo prodotto in una voce di un ordine. È possibile includere tag che producono proprietà, come {ex1} o {ex2}', + 'How this shipping method will be referred to in templates and forms.' => 'In che modo si farà riferimento a questo metodo di spedizione nei template e nei moduli.', + 'How variants should be labeled within the control panel.' => 'Come devono essere etichettate le varianti nel pannello di controllo.', + 'How you’ll refer to this PDF in the templates.' => 'Come farai riferimento a questo PDF nei template.', + 'How you’ll refer to this product type in the templates.' => 'Come farai riferimento a questo tipo di prodotto nei template.', + 'How you’ll refer to this shipping category in the templates.' => 'Come farai riferimento a questa categoria di spedizione nei template.', + 'How you’ll refer to this status in the templates.' => 'Come farai riferimento a questo stato nei template.', + 'How you’ll refer to this subscription plan in the templates.' => 'Come ti riferirai a questa sottoscrizione nei template.', + 'How you’ll refer to this tax category in the templates.' => 'Come farai riferimento a questa categoria fiscale nei template.', + 'ID' => 'ID', + 'IP Address' => 'Indirizzo IP', + 'If disabled, this PDF will not be available or sent with emails.' => 'Se disattivato, questo PDF non sarà disponibile o inviato con le e-mail.', + 'If disabled, this email will not send.' => 'Se la voce è disabilitata, questa email non verrà inviata.', + 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Se l\'opzione è abilitata e l\'aliquota non corrisponde all\'ordine, l\'importo dell\'aliquota sarà rimosso dal prezzo dell\'imponibile nel carrello.', + 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Se impostato su Autorizza solo, sarà necessario acquisire manualmente i pagamenti prima che i fondi vengano trasferiti sul proprio conto. Il gateway deve supportare l’opzione selezionata.', + 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Se selezioni la percentuale di "riduzione sul prezzo dell\'articolo scontato", essa includerà l\'"Importo per articolo" così come qualsiasi altro sconto applicato prima di questo.', + 'Ignore Promotions?' => 'Ignora promozioni?', + 'Ignore previous matching sales if this sale matches.' => 'Ignora vendite precedenti corrispondenti se questa vendita corrisponde.', + 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignora i prezzi promozionali quando questo sconto viene applicato agli articoli corrispondenti', + 'Inactive Carts' => 'Carrelli non attivi', + 'Inches (in)' => 'Pollici (in)', + 'Include built-in line item tax.' => 'Includi imposta integrata nelle voci.', + 'Include in price?' => 'Includere nel prezzo?', + 'Include line item discounts.' => 'Includi sconti delle voci.', + 'Include line item shipping costs.' => 'Includi costi di spedizione delle voci.', + 'Include separate line item tax.' => 'Includi imposta separata delle voci.', + 'Included in price?' => 'Incluso nel prezzo?', + 'Included' => 'Incluso', + 'Incoming transfer from Transfer ID: ' => 'Trasferimento in arrivo dall\'ID di trasferimento: ', + 'Incoming' => 'In arrivo', + 'Info' => 'Informazioni', + 'Information linked?' => 'Informazioni correlate?', + 'Information' => 'Informazioni', + 'Invalid JSON' => 'JSON non valido', + 'Invalid Order ID' => 'ID ordine non valido', + 'Invalid VAT ID.' => 'Partita IVA non valida.', + 'Invalid condition syntax' => 'Sintassi della condizione non valida', + 'Invalid email.' => 'Email non valida.', + 'Invalid formula syntax' => 'Sintassi della formula non valida', + 'Invalid gateway: {value}' => 'Gateway non valido: {value}', + 'Invalid inventory movements.' => 'Movimenti di inventario non validi.', + 'Invalid order condition syntax.' => 'Sintassi della condizione dell\'ordine non valida.', + 'Invalid payment or order. Please review.' => 'Ordine o pagamento non valido. Controlla i dati.', + 'Invalid payment source ID: {value}' => 'ID fonte di pagamento non valido: {value}', + 'Invalid store.' => 'Store non valido.', + 'Invalid user.' => 'Utente non valido.', + 'Inventory Item' => 'Articolo dell\'inventario', + 'Inventory Location' => 'Sede dell\'inventario', + 'Inventory Locations' => 'Sedi dell\'inventario', + 'Inventory Tracked' => 'Inventario tracciato', + 'Inventory Transfers' => 'Trasferimenti dell\'inventario', + 'Inventory could not be set.' => 'Non è stato possibile impostare l\'inventario.', + 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'La sede dell\'inventario ha impegnato lo stock, l\'ordine o gli ordini devono prima essere evasi.', + 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'La sede dell\'inventario ha stock in arrivo, il trasferimento o i trasferimenti devono prima essere completati.', + 'Inventory location is already deactivated.' => 'La sede dell\'inventario è già disattivata.', + 'Inventory location saved.' => 'Sede dell\'inventario salvata.', + 'Inventory locations not saved.' => 'Sedi dell\'inventario non salvate.', + 'Inventory movement could not be saved.' => 'Non è stato possibile salvare il movimento di inventario.', + 'Inventory movement saved.' => 'Movimento di inventario salvato.', + 'Inventory updated.' => 'Inventario aggiornato.', + 'Inventory was not updated.' => 'Inventario non aggiornato.', + 'Inventory' => 'Inventario', + 'Invoice amount' => 'Importo fattura', + 'Invoice date' => 'Data fattura', + 'Is Promotable' => 'Promozione applicabile', + 'Is Promotional Price?' => 'È il prezzo promozionale?', + 'Is Shippable' => 'Spedizione disponibile', + 'Is Taxable' => 'Tassazione disponibile', + 'Item Rates' => 'Tariffe per articolo', + 'Item Subtotal' => 'Subtotale articolo', + 'Item Total' => 'Totale articolo', + 'Item' => 'Elemento', + 'Items' => 'Articoli', + 'Kilograms (kg)' => 'Chilogrammi (kg)', + 'Label' => 'Etichetta', + 'Landscape' => 'Orizzontale', + 'Language' => 'Lingua', + 'Last Name' => 'Cognome', + 'Last Updated' => 'Ultimo aggiornamento', + 'Leave a category rate override blank to use the rate from above.' => 'Lascia vuota l\'opzione di override del tasso di categoria per utilizzare il tasso di cui sopra.', + 'Leave blank for unlimited uses.' => 'Lascia in bianco per indicare utilizzo illimitato.', + 'Leave blank if products don’t have URLs' => 'Lascia il campo vuoto se ai prodotti non sono associati URL', + 'Leave gateway subscription as-is' => 'Lascia invariato l\'abbonamento al gateway', + 'Length ({unit})' => 'Lunghezza ({unit})', + 'Length' => 'Lunghezza', + 'Let each product choose which sites it should be saved to' => 'Lascia scegliere a ogni prodotto su quali siti deve essere salvato', + 'Limit which orders this discount applies to based on its line items.' => 'Limita a quali ordini applicare questo sconto in base ai relativi articoli.', + 'Limit which purchasables this sale applies to.' => 'Limita a quali articoli disponibili all\'acquisto applicare questa offerta.', + 'Limit' => 'Limite', + 'Line Item Statuses' => 'Stati delle voci', + 'Line Item' => 'Voce', + 'Line Items' => 'Voci', + 'Line item price (minus discounts)' => 'Prezzo per articolo (meno gli sconti)', + 'Line item shipping cost' => 'Costo di spedizione per voce', + 'Line item statuses reordered.' => 'Stati delle voci riordinati.', + 'Link Duration' => 'Durata link', + 'Link Sent' => 'Link inviato', + 'Link to a product' => 'Collega a un prodotto', + 'Link to a variant' => 'Collega a una variante', + 'Link' => 'Collega', + 'Live' => 'Pubblicato', + 'Location' => 'Sede', + 'Locations that should be available for previewing products in this product type.' => 'Posizioni che dovrebbero essere disponibili per l\'anteprima dei prodotti in questo tipo di prodotto.', + 'MM' => 'MM', + 'Make a payment' => 'Effettua un pagamento', + 'Make this the primary store' => 'Renderlo lo store principale', + 'Manage Inventory' => 'Gestisci inventario', + 'Manage donation settings' => 'Gestisci impostazioni di donazione', + 'Manage general store settings' => 'Gestisci impostazioni dello store generale', + 'Manage inventory locations' => 'Gestisci sedi dell\'inventario', + 'Manage inventory stock levels' => 'Gestisci livelli di stock dell\'inventario', + 'Manage inventory transfers' => 'Gestisci trasferimenti dell\'inventario', + 'Manage orders' => 'Gestisci ordini', + 'Manage payment currencies' => 'Gestisci valute di pagamento', + 'Manage promotions' => 'Gestisci promozioni', + 'Manage shipping' => 'Gestisci spedizione', + 'Manage store settings' => 'Gestisci impostazioni dello store', + 'Manage subscription plans' => 'Gestisci piani di sottoscrizione', + 'Manage subscription' => 'Gestisci sottoscrizione', + 'Manage subscriptions' => 'Gestisci sottoscrizioni', + 'Manage taxes' => 'Gestisci imposte', + 'Manage' => 'Gestisci', + 'Mark as Pending' => 'Contrassegna come in attesa', + 'Mark as completed' => 'Contrassegna come completato', + 'Match Billing Address' => 'Sconto basato su indirizzo di fatturazione', + 'Match Customer' => 'Sconto basato sul cliente', + 'Match Order' => 'Sconto basato sull\'ordine', + 'Match Orders' => 'Corrispondenza ordini', + 'Match Product' => 'Abbina prodotto', + 'Match Purchasable' => 'Abbina articoli disponibili per l\'acquisto', + 'Match Shipping Address' => 'Sconto basato su indirizzo di spedizione', + 'Match Variant' => 'Abbina variante', + 'Matching Items' => 'Articoli corrispondenti', + 'Max Qty' => 'Quantità massima', + 'Max Uses' => 'Numero max. utilizzi', + 'Max Variants' => 'Varianti massime', + 'Max quantity must greater than min.' => 'La quantità massima deve essere maggiore di quella minima.', + 'Maximum Purchase Quantity' => 'Quantità massima acquistabile', + 'Maximum Total Shipping Cost' => 'Costi di spedizione totali massimi', + 'Maximum allowed quantity' => 'Quantità massima consentita', + 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Numero massimo di articoli corrispondenti ordinabili per applicare questo sconto. Se questo valore è pari a zero, questa condizione verrà saltata.', + 'Maximum order quantity for this item is {num}.' => 'La quantità massima ordinabile per questo articolo è pari a {num}.', + 'Message' => 'Messaggio', + 'Meters (m)' => 'Metri (m)', + 'Millimeters (mm)' => 'Millimetri (mm)', + 'Min Qty' => 'Quantità min.', + 'Min quantity must be less than max.' => 'La quantità minima deve essere minore di quella massima.', + 'Minimum Purchase Quantity' => 'Quantità minima acquistabile', + 'Minimum Total Price Strategy' => 'Strategia prezzo totale minimo', + 'Minimum Total Shipping Cost' => 'Costi di spedizione totali minimi', + 'Minimum allowed quantity' => 'Quantità minima consentita', + 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Numero minimo di articoli corrispondenti che devono essere ordinati per applicare questo sconto.', + 'Minimum order quantity for this item is {num}.' => 'La quantità minima ordinabile per questo articolo è pari a {num}.', + 'Missing Gateway' => 'Gateway mancante', + 'Missing a default inventory location.' => 'Sede predefinita dell\'inventario mancante.', + 'Move Inventory' => 'Sposta inventario', + 'Move To' => 'Sposta in', + 'Move {qty} from {fromType} to {toType}' => 'Sposta {qty} da {fromType} a {toType}', + 'Move' => 'Sposta', + 'Movement from deactivated inventory location' => 'Spostamento dalla sede dell\'inventario disattivata', + 'Movement' => 'Spostamento', + 'Must have at least one variant.' => 'Deve avere almeno una variante.', + 'Name Field' => 'Campo Nome', + 'Name' => 'Nome', + 'New Customer' => 'Nuovo cliente', + 'New Customers' => 'Nuovi clienti', + 'New Order' => 'Nuovo ordine', + 'New PDF' => 'Nuovo PDF', + 'New address' => 'Nuovo indirizzo', + 'New catalog pricing rule' => 'Nuova regola di prezzo in catalogo', + 'New currency' => 'Nuova valuta', + 'New discount' => 'Nuovo sconto', + 'New email' => 'Nuova email', + 'New gateway' => 'Nuovo gateway', + 'New line item status' => 'Nuovo stato voce', + 'New line items get this status by default when the order is completed' => 'Le nuove voci ricevono questo stato per impostazione predefinita al completamento dell\'ordine', + 'New location' => 'Nuova sede', + 'New order status' => 'Nuovo stato ordine', + 'New orders get this status by default' => 'I nuovi ordini passano a questo stato per impostazione predefinita', + 'New product type' => 'Nuovo tipo di prodotto', + 'New product' => 'Nuovo prodotto', + 'New product, choose a type' => 'Nuovo prodotto, scegli un tipo', + 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'I nuovi prodotti sono impostati come predefinito sulla prima categoria fiscale disponibile. Se nessuna è disponibile, è usata questa categoria.', + 'New sale' => 'Nuova vendita promozionale', + 'New shipping category' => 'Nuova categoria di spedizione', + 'New shipping method' => 'Nuovo metodo di spedizione', + 'New shipping rule' => 'Nuova regola di spedizione', + 'New shipping zone' => 'Nuova area di spedizione', + 'New subscription plan' => 'Nuovo piano di sottoscrizione', + 'New tax category' => 'Nuova categoria fiscale', + 'New tax rate' => 'Nuova aliquota fiscale', + 'New tax zone' => 'Nuova zona fiscale', + 'New transfer' => 'Nuovo trasferimento', + 'New {productType} product' => 'Nuovo prodotto {productType}', + 'New' => 'Nuovo', + 'Next payment' => 'Pagamento successivo', + 'No Address' => 'Nessun indirizzo', + 'No PDFs exist yet.' => 'Non esiste ancora nessun PDF.', + 'No access given to any specific store management features.' => 'Non è consentito l\'accesso ad alcuna funzione specifica di gestione dello store.', + 'No additional payment currencies exist yet.' => 'Non esiste ancora nessuna valuta di pagamento aggiuntiva.', + 'No address' => 'Nessun indirizzo', + 'No billing address' => 'Nessun indirizzo di fatturazione', + 'No catalog pricing rule exists with the ID “{id}”' => 'Nessuna regola di prezzo in catalogo esistente con ID “{id}”', + 'No catalog pricing rules exist yet.' => 'Non esiste ancora nessuna regola di prezzo in catalogo.', + 'No currency exists with the ID “{id}”' => 'Nessuna valuta esistente con ID “{id}”', + 'No customer email address exists on this cart.' => 'Non esiste nessun indirizzo email cliente in questo carrello.', + 'No description' => 'Nessuna descrizione', + 'No discount exists with the ID “{id}”' => 'Nessuno sconto esistente con ID “{id}”', + 'No discounts exist yet.' => 'Non esiste ancora nessuno sconto.', + 'No donation amount supplied.' => 'Nessun importo di donazione fornito.', + 'No emails exist yet.' => 'Non esiste ancora nessuna email.', + 'No inventory changes made.' => 'Non sono state apportate modifiche all\'inventario.', + 'No inventory found.' => 'Non è stato trovato alcun inventario.', + 'No inventory movements made.' => 'Non sono stati effettuati spostamenti in inventario.', + 'No inventory transactions for this location.' => 'Nessuna transazione di inventario per questa sede.', + 'No new customer selected.' => 'Non è stato selezionato alcun nuovo cliente.', + 'No order history exists with the ID “{id}”' => 'Nessuno storico ordini esistente con ID “{id}”', + 'No order status history items will exist until the cart becomes an order.' => 'Non esisterà alcuna cronologia di stato dell’ordine fino a quando il carrello non diventerà un ordine.', + 'No payment source exists with the ID “{id}”' => 'Nessuna fonte di pagamento esistente con ID “{id}”', + 'No private Note.' => 'Nessuna nota privata.', + 'No product available.' => 'Nessun prodotto disponibile.', + 'No product types exist yet.' => 'Non esiste ancora nessun tipo di prodotto.', + 'No purchasable available.' => 'Nessun prodotto disponibile all’acquisto.', + 'No sale exists with the ID “{id}”' => 'Nessuna vendita promozionale con ID “{id}”', + 'No sales exist yet.' => 'Non esiste ancora nessuna vendita promozionale.', + 'No shipping address' => 'Nessun indirizzo di spedizione', + 'No shipping category exists with the ID “{id}”' => 'Nessuna categoria di spedizione esistente con ID “{id}”', + 'No shipping method exists with the ID “{id}”' => 'Nessun metodo di spedizione esistente con ID “{id}”', + 'No shipping rule exists with the ID “{id}”' => 'Nessuna regola di spedizione esistente con ID “{id}”', + 'No shipping rules exist yet.' => 'Non esiste ancora nessuna regola di spedizione.', + 'No shipping zone exists with the ID “{id}”' => 'Nessuna area di spedizione esistente con ID “{id}”', + 'No stats available.' => 'Statistiche indisponibili.', + 'No subscription plan exists with the ID “{id}”' => 'Nessun piano di sottoscrizione esistente con ID “{id}”', + 'No subscription plans exist yet.' => 'Non esiste ancora nessun piano di sottoscrizione.', + 'No tax category exists with the ID “{id}”' => 'Nessuna categoria fiscale esistente con ID “{id}”', + 'No tax rate exists with the ID “{id}”' => 'Nessuna aliquota fiscale esistente con ID “{id}”', + 'No tax zone exists with the ID “{id}”' => 'Nessuna zona fiscale esistente con ID “{id}”', + 'No transactions exist.' => 'Nessuna transazione esistente.', + 'No user authenticated.' => 'Nessun utente autenticato.', + 'No' => 'No', + 'None on hand' => 'Nessuno a disposizione', + 'None' => 'Nessuno', + 'Not a valid address type' => 'Tipo di indirizzo non valido', + 'Not a valid credit card number.' => 'Numero di carta di credito non valido.', + 'Not all SKUs are unique.' => 'Non tutti gli SKU sono univoci.', + 'Note' => 'Nota', + 'Notes' => 'Note', + 'Number of Coupons' => 'Numero di codici promozionali', + 'Number' => 'Numero', + 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Dei siti abilitati riportati sopra, in quali siti devono essere salvati i prodotti in questo tipo di prodotto?', + 'On Hand' => 'Disponibile', + 'Only allow this gateway to be used for zero value orders?' => 'Consenti l’utilizzo di questo gateway solo per gli ordini di valore zero?', + 'Only match certain purchasables…' => 'Solo corrispondenza con determinati articoli disponibili all\'acquisto…', + 'Only match purchasables related to…' => 'Solo corrispondenza con articoli disponibili all\'acquisto relativi a…', + 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Solo gli ordini con il seguente stato saranno inclusi. Lasciare vuoto per consentire tutti gli stati.', + 'Only save product to the site they were created in' => 'Salva prodotti esclusivamente sul sito di creazione', + 'Options' => 'Opzioni', + 'Order Condition Formula' => 'Formula con condizione di ordine', + 'Order Description Format' => 'Formato descrizione ordine', + 'Order Details' => 'Dettagli ordine', + 'Order Fields' => 'Campi ordine', + 'Order PDF Download Link' => 'Link di download PDF ordine', + 'Order PDF Filename Format' => 'Formato nome file PDF ordine', + 'Order Reference Number Format' => 'Formato numero di riferimento ordine', + 'Order Settings' => 'Impostazioni ordine', + 'Order Site' => 'Sito dell\'ordine', + 'Order Status description.' => 'Descrizione stato ordine.', + 'Order Status' => 'Stato dell’ordine', + 'Order Statuses' => 'Stati ordine', + 'Order can not be empty.' => 'Il campo dell\'ordine non può essere vuoto.', + 'Order count' => 'Conteggio dell\'ordine', + 'Order customer data removed.' => 'Dati del cliente dell\'ordine rimossi.', + 'Order deleted.' => 'Ordine eliminato.', + 'Order fields saved.' => 'Campi dell\'ordine salvati.', + 'Order not found.' => 'Ordine non trovato.', + 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Il saldo del pagamento dell\'ordine è {outstandingBalanceAsCurrency}. Questo è il valore massimo che verrà addebitato.', + 'Order recalculated.' => 'Ordine ricalcolato.', + 'Order status saved.' => 'Stato dell’ordine salvato.', + 'Order statuses reordered.' => 'Stati degli ordini riordinati.', + 'Order total shipping cost' => 'Costi di spedizione totali dell’ordine', + 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Prezzo imponibile totale ordine (Subtotale articoli + Sconti totali + Spedizione totale)', + 'Order' => 'Ordine', + 'Orders (Legacy)' => 'Ordini (Legacy)', + 'Orders deleted.' => 'Ordini eliminati.', + 'Orders not restored.' => 'Ordini non ripristinati.', + 'Orders restored.' => 'Ordini ripristinati.', + 'Orders' => 'Ordini', + 'Organization Name' => 'Nome organizzazione', + 'Organization Tax ID' => 'Partita IVA organizzazione', + 'Origin and destination cannot be the same.' => 'L\'origine e la destinazione non possono essere identiche.', + 'Origin' => 'Origine', + 'Original Price' => 'Prezzo originale', + 'Original price' => 'Prezzo originale', + 'Original promotional price' => 'Prezzo promozionale originale', + 'Other Languages' => 'Altre lingue', + 'Other countries' => 'Altri Paesi', + 'Outgoing transfer from Transfer ID: ' => 'Trasferimento in uscita dall\'ID di trasferimento: ', + 'Overpaid' => 'Pagamento in eccesso', + 'Overrides previous?' => 'Sostituisce i dati precedenti?', + 'PDF Attachment' => 'Allegato PDF', + 'PDF Template Path' => 'Percorso al template PDF', + 'PDF saved.' => 'PDF salvato.', + 'PDF' => 'PDF', + 'PDFs & Emails' => 'PDF e e-mail', + 'PDFs' => 'PDF', + 'Paid Amount' => 'Importo pagato', + 'Paid Status' => 'Stato pagamenti effettuati', + 'Paid' => 'Pagato', + 'Paper Orientation' => 'Orientamento foglio', + 'Paper Size' => 'Dimensioni foglio', + 'Partial payment not allowed.' => 'Pagamento parziale non consentito.', + 'Partial' => 'Parziale', + 'Past year' => 'L\'anno scorso', + 'Past {num} days' => '{num} giorni trascorsi', + 'Pay {amount} of {currency} on the order.' => 'Paga {amount} in {currency} sull\'ordine.', + 'Pay' => 'Paga', + 'Payment Amount' => 'Importo pagato', + 'Payment Currencies' => 'Valute di pagamento', + 'Payment Gateway' => 'Gateway di pagamento', + 'Payment Method' => 'Metodo di pagamento', + 'Payment error: {message}' => 'Errore di pagamento: {message}', + 'Payment method issue' => 'Problema con il metodo di pagamento', + 'Payment source created.' => 'Fonte di pagamento creata.', + 'Payment source deleted.' => 'Fonte di pagamento eliminata.', + 'Payments' => 'Pagamenti', + 'Pending' => 'In attesa', + 'Per Email Address Discount Limit' => 'Limite di sconto per indirizzo email', + 'Per Item Amount Off' => 'Importo di sconto per articolo', + 'Per Item Discount' => 'Sconto per articolo', + 'Per Item Percentage Off' => 'Percentuale di sconto per articolo', + 'Per Item Rate' => 'Tariffa in base ad articolo', + 'Per User Discount Limit' => 'Limite di sconto per utente', + 'Percentage Rate' => 'Tariffa in base a percentuale', + 'Phone (Alt)' => 'N. telefono (alt)', + 'Phone' => 'N. telefono', + 'Pick a plan' => 'Scegli un piano', + 'Plain Text Email Template Path' => 'Percorso template email di testo semplice', + 'Plan' => 'Piano', + 'Plans reordered.' => 'Piani riordinati.', + 'Portrait' => 'Verticale', + 'Post Date' => 'Data di pubblicazione', + 'Postal Code Formula' => 'Formula codice postale', + 'Pounds (lb)' => 'Libbre (lb)', + 'Preview' => 'Anteprima', + 'Previous Status' => 'Stato precedente', + 'Price' => 'Prezzo', + 'Prices' => 'Prezzi', + 'Pricing Rules' => 'Regole di prezzo', + 'Pricing jobs are currently running.' => 'Le attività di prezzo sono attualmente in corso.', + 'Pricing' => 'Prezzo', + 'Primary Billing Address' => 'Indirizzo di fatturazione principale', + 'Primary Shipping Address' => 'Indirizzo di spedizione principale', + 'Primary payment source updated.' => 'Fonte di pagamento principale aggiornata.', + 'Primary' => 'Principale', + 'Private Note' => 'Nota privata', + 'Product Fields' => 'Campi prodotto', + 'Product ID is required.' => 'È richiesto un ID prodotto.', + 'Product Template' => 'Template prodotto', + 'Product Title Format' => 'Formato titolo prodotto', + 'Product Type' => 'Tipo di prodotto', + 'Product Types' => 'Tipi di prodotti', + 'Product URI Format' => 'Formato URL prodotto', + 'Product Variant' => 'Variante prodotto', + 'Product Variants' => 'Varianti prodotto', + 'Product type saved.' => 'Tipo di prodotto salvato.', + 'Product type settings' => 'Impostazioni tipo di prodotto', + 'Product' => 'Prodotto', + 'Products and Variants deleted.' => 'Prodotti e Varianti eliminati.', + 'Products not restored.' => 'Prodotti non ripristinati.', + 'Products restored.' => 'Prodotti ripristinati.', + 'Products' => 'Prodotti', + 'Promotable' => 'Promozione applicabile', + 'Promotable?' => 'È possibile applicare promozioni?', + 'Promotional Amount' => 'Importo promozionale', + 'Promotional Price' => 'Prezzo promozionale', + 'Purchasable Categories' => 'Categorie disponibili per l\'acquisto', + 'Purchasable ID and Sale ID are required.' => 'Sono richiesti un ID disponibile all’acquisto e un ID di vendita promozionale.', + 'Purchasable ID is required.' => 'È richiesto un ID disponibile all’acquisto.', + 'Purchasable Type' => 'Tipo disponibile per l’acquisto', + 'Purchasable' => 'Disponibile per l’acquisto', + 'Purchase (Authorize and Capture Immediately)' => 'Acquista (autorizza e acquisisci immediatamente)', + 'Purchase Total' => 'Totale acquisto', + 'Qty' => 'Qtà', + 'Quality Control' => 'Controllo qualità', + 'Quantity' => 'Quantità', + 'Rate' => 'Tasso', + 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Riassegna {numOrders, plural, one {}=1{ordine} other{ordini}}', + 'Recalculate order' => 'Ricalcola ordine', + 'Receive Inventory' => 'Ricevi inventario', + 'Receive Transfer' => 'Ricevi trasferimento', + 'Receive' => 'Ricevi', + 'Received' => 'Ricevuto', + 'Recent Orders' => 'Ordini recenti', + 'Recipient' => 'Destinatario', + 'Recover Cart' => 'Recupera carrello', + 'Reduce price' => 'Riduci prezzo', + 'Reduce the price by a fixed amount' => 'Riduci il prezzo di un importo fisso', + 'Reduce the price by a percentage of the original price' => 'Riduci il prezzo originale di una determinata percentuale', + 'Reference' => 'Riferimento', + 'Refresh payment history' => 'Aggiorna cronologia pagamenti', + 'Refund note' => 'Nota di rimborso', + 'Refund payment' => 'Rimborsa pagamento', + 'Refund' => 'Rimborso', + 'Reject' => 'Rifiuta', + 'Rejected' => 'Rifiutato', + 'Relationship Type' => 'Tipo di relazione', + 'Removable included tax rates are only allowed for the default tax zone.' => 'Le aliquote fiscali rimovibili incluse sono consentite solo per la zona fiscale predefinita.', + 'Remove address' => 'Rimuovi indirizzo', + 'Remove all shipping costs from the order' => 'Rimuovi tutti i costi di spedizione dall\'ordine', + 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Rimuovi l\'associazione al cliente e l\'indirizzo e-mail da {numOrders, plural, one {}=1{ordine} other{ordini}}. Puoi scegliere di selezionare ulteriori dati del cliente da rimuovere qui sotto', + 'Remove customer data' => 'Rimuovi dati dei clienti', + 'Remove from price?' => 'Rimuovere dal prezzo?', + 'Remove shipping costs for matching items only' => 'Rimuovi i costi di spedizione solo per gli articoli corrispondenti', + 'Remove the included tax when a valid organization tax ID is present?' => 'Rimuovere l\'aliquota inclusa quando una partita IVA aziendale valida è presente?', + 'Remove' => 'Rimuovi', + 'Removed' => 'Rimosso', + 'Repeat Customers' => 'Clienti abituali', + 'Reply To' => 'Rispondi a', + 'Require Billing Address At Checkout' => 'Richiedi l\'indirizzo di fatturazione al checkout', + 'Require Coupon Code' => 'Richiedi codice coupon', + 'Require Shipping Address At Checkout' => 'Richiedi l\'indirizzo di spedizione al checkout', + 'Require Shipping Method Selection At Checkout' => 'Richiedi la selezione del metodo di spedizione al checkout', + 'Require' => 'Richiedi', + 'Reserved' => 'Riservato', + 'Reset usage' => 'Ripristina utilizzo', + 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Limita questo sconto solo agli ordini in cui il cliente ha acquistato un valore totale minimo di articoli corrispondenti.', + 'Revenue Options' => 'Opzioni di ricavo', + 'Revenue' => 'Ricavo', + 'Rule' => 'Regola', + 'Rules reordered.' => 'Regole riordinate.', + 'SKU' => 'SKU', + 'Safety' => 'Sicurezza', + 'Sale Price' => 'Prezzo di vendita promozionale', + 'Sale description.' => 'Descrizione vendita promozionale.', + 'Sale reordered.' => 'Vendita promozionale riordinata.', + 'Sale saved.' => 'Vendita promozionale salvata.', + 'Sale' => 'Vendita promozionale', + 'Sales deleted.' => 'Vendite promozionali eliminate.', + 'Sales updated.' => 'Vendite promozionali aggiornate.', + 'Sales' => 'Vendite promozionali', + 'Save and continue editing' => 'Salva e continua modifiche', + 'Save and return to all orders' => 'Salva e torna a tutti gli ordini', + 'Save and set rules' => 'Salva e imposta regole', + 'Save as a new rule' => 'Salva come nuova regola', + 'Save product to all sites enabled for this product type' => 'Salva il prodotto in tutti i siti abilitati per questo tipo di prodotto', + 'Save product to other sites in the same site group' => 'Salva il prodotto su altri siti nello stesso gruppo di siti', + 'Save product to other sites with the same language' => 'Salva il prodotto su altri siti con la stessa lingua', + 'Save' => 'Salva', + 'Search customer…' => 'Cerca cliente…', + 'Search inventory' => 'Ricerca in inventario', + 'Search or enter customer email…' => 'Cerca o immetti l\'e-mail del cliente...', + 'Search…' => 'Ricerca...', + 'See Orders' => 'Visualizza ordini', + 'Select a gateway' => 'Seleziona un gateway', + 'Select a tax category.' => 'Selezionare una categoria fiscale.', + 'Select a tax zone. If empty, this rate will match anywhere.' => 'Seleziona una zona fiscale. Se l\'opzione è lasciata vuota, il tasso corrisponderà ovunque.', + 'Select address' => 'Seleziona indirizzo', + 'Select an item' => 'Seleziona un articolo', + 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Seleziona la modalità di applicazione della regola di prezzo in catalogo al/i prodotto/i disponibile/i per l\'acquisto.', + 'Select how the sale will be applied to the purchasable(s).' => 'Scegli come applicare la vendita all’oggetto acquistabile/agli oggetti acquistabili.', + 'Select product type' => 'Seleziona tipo di prodotto', + 'Select the emails that will be sent when transitioning to this status.' => 'Selezionare le email per l’invio durante la transizione a questo stato.', + 'Select what this rate should be applied to.' => 'Seleziona dove applicare questa aliquota.', + 'Send Email' => 'Invia email', + 'Send to custom recipient' => 'Invia a destinatario personalizzato', + 'Send to the customer' => 'Invia al cliente', + 'Set Quantity' => 'Imposta quantità', + 'Set default category' => 'Imposta categoria predefinita', + 'Set default variant' => 'Imposta variante predefinita', + 'Set or Adjust' => 'Imposta o rettifica', + 'Set price' => 'Imposta prezzo', + 'Set status' => 'Imposta stato', + 'Set the price to a flat amount' => 'Imposta il prezzo su un importo forfettario', + 'Set the price to a percentage of the original price' => 'Imposta il prezzo su una percentuale del prezzo originale', + 'Set the sale price to a flat amount' => 'Imposta il prezzo di vendita su un importo forfettario', + 'Set the sale price to a percentage of the original price' => 'Imposta il prezzo di vendita su una percentuale del prezzo originale', + 'Set to' => 'Imposta su', + 'Settings saved.' => 'Impostazioni salvate.', + 'Settings' => 'Impostazioni', + 'Share cart…' => 'Condividi carrello...', + 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Spedizione - Il costo minimo è il costo di spedizione applicato se il prezzo dell\'ordine è inferiore al costo di spedizione.', + 'Shipping Address Zone' => 'Zona dell\'indirizzo di spedizione', + 'Shipping Address' => 'Indirizzo di spedizione', + 'Shipping Business Name' => 'Nome azienda per la spedizione', + 'Shipping Categories' => 'Categorie di spedizione', + 'Shipping Category Conditions' => 'Condizioni della categoria di spedizione', + 'Shipping Category' => 'Categoria di spedizione', + 'Shipping First Name' => 'Nome di spedizione', + 'Shipping Full Name' => 'Nome completo di spedizione', + 'Shipping Last Name' => 'Cognome di spedizione', + 'Shipping Method' => 'Metodo di spedizione', + 'Shipping Methods' => 'Metodi di spedizione', + 'Shipping Rule' => 'Regola di spedizione', + 'Shipping Zones' => 'Aree di spedizione', + 'Shipping address required.' => 'Indirizzo di spedizione obbligatorio.', + 'Shipping categories deleted.' => 'Categorie di spedizione eliminate.', + 'Shipping category saved.' => 'Categoria di spedizione salvata.', + 'Shipping category updated.' => 'Categoria di spedizione aggiornata.', + 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Costi di spedizione aggiunti all\'ordine come totale prima dell\'applicazione delle percentuali, dell\'articolo e delle tariffe in base al peso. Imposta su zero per disabilitare questa tariffa. L\'intera regola, tasso base incluso, non è valida e applicata se il carrello contiene esclusivamente articoli non soggetti a spedizione, come ad esempio prodotti digitali.', + 'Shipping method saved.' => 'Metodo di spedizione salvato.', + 'Shipping methods and rules deleted.' => 'Metodi di spedizione e regole eliminati.', + 'Shipping methods updated.' => 'Metodi di spedizione aggiornati.', + 'Shipping rule saved.' => 'Regola di spedizione salvata.', + 'Shipping zone saved.' => 'Area di spedizione salvata.', + 'Shipping' => 'Spedizione', + 'Short Number' => 'Numero breve', + 'Show Chart?' => 'Mostrare il grafico?', + 'Show Order Count?' => 'Mostrare il conteggio dell\'ordine?', + 'Show all prices' => 'Mostra tutti i prezzi', + 'Show archived gateways' => 'Mostra gateway archiviati', + 'Show order count line on chart.' => 'Mostra la riga di conteggio dell\'ordine sul grafico.', + 'Show related sales' => 'Mostra vendite collegate', + 'Show rule details' => 'Mostra dettagli della regola', + 'Show the Dimensions and Weight fields for products of this type' => 'Mostra i campi Dimensioni e Peso per i prodotti di questo tipo', + 'Show the Title field for products' => 'Mostra il campo Titolo per i prodotti', + 'Show the Title field for variants' => 'Mostra il campo Titolo per le varianti', + 'Signed In' => 'Accesso riuscito', + 'Site Languages' => 'Lingue del sito', + 'Site store mapping saved.' => 'Mappatura dello store del sito salvata.', + 'Sites' => 'Siti', + 'Slug' => 'Slug', + 'Snapshot' => 'Snapshot', + 'Snapshots' => 'Snapshot', + 'Some orders restored.' => 'Alcuni ordini ripristinati.', + 'Some products restored.' => 'Alcuni prodotti ripristinati.', + 'Some variants restored.' => 'Alcune varianti ripristinate.', + 'Something changed with the order before payment, please review your order and submit payment again.' => 'È cambiato qualcosa nell’ordine prima del pagamento. Controllare l’ordine e inviare nuovamente il pagamento.', + 'Sorry, no matching options.' => 'Spiacente, nessuna opzione corrispondente.', + 'Source - The purchasable relationship field is on the category' => 'Fonte - Il campo relazione prodotti disponibili all\'acquisto si trova sulla categoria', + 'Source' => 'Fonte', + 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Specifica una condizione Twig che determina se lo sconto deve essere applicato a un determinato ordine. (L\'ordine può essere referenziato tramite una variabile `order`.)', + 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Specifica una condizione Twig che determina se le regole di spedizione devono essere applicate a un determinato ordine. (L\'ordine può essere referenziato tramite una variabile `order`.)', + 'Start Date' => 'Data di inizio', + 'State' => 'Stato', + 'Status Email Address' => 'Indirizzo email stato', + 'Status Emails' => 'Email stato', + 'Status History' => 'Cronologia stato', + 'Status Updated.' => 'Stato aggiornato.', + 'Status change message' => 'Messaggio modifica stato', + 'Status' => 'Stato', + 'Stock' => 'Stock', + 'Stops Processing?' => 'Interrompe l’elaborazione?', + 'Stops subsequent?' => 'Interrompe i dati successivi?', + 'Store Location' => 'Posizione store', + 'Store Management' => 'Gestione dello store', + 'Store Markets' => 'Paesi in cui lo store vende', + 'Store Rule' => 'Regola dello store', + 'Store saved.' => 'Store salvato.', + 'Store' => 'Store', + 'Stores & Sites' => 'Store e siti', + 'Stores' => 'Store', + 'Strategy to apply when an order is free or has a zero balance.' => 'Strategia da applicare quando un ordine è gratuito o ha un saldo pari a zero.', + 'Strategy to apply when calculating the minimum order price.' => 'Strategia da applicare durante il calcolo del prezzo d\'ordine minimo.', + 'Subject' => 'Oggetto', + 'Subscribing user' => 'Utente che effettua la sottoscrizione', + 'Subscription Fields' => 'Campi della sottoscrizione', + 'Subscription Plans' => 'Piani di sottoscrizione', + 'Subscription Settings' => 'Impostazioni di sottoscrizione', + 'Subscription cancelled.' => 'Sottoscrizione annullata.', + 'Subscription date' => 'Data sottoscrizione', + 'Subscription fields saved.' => 'Campi di sottoscrizione salvati.', + 'Subscription for {user} to {plan} prevented by a plugin.' => 'Sottoscrizione per {user} a {plan} impedita da un plug-in.', + 'Subscription plan saved.' => 'Piano di sottoscrizione salvato.', + 'Subscription plan' => 'Piano di sottoscrizione', + 'Subscription plans' => 'Piani di sottoscrizione', + 'Subscription reactivated.' => 'Sottoscrizione riattivata.', + 'Subscription reference' => 'Riferimento della sottoscrizione', + 'Subscription started.' => 'Sottoscrizione avviata.', + 'Subscription switched.' => 'Sottoscrizione modificata.', + 'Subscription to “{plan}”' => 'Sottoscrizione di “{plan}”', + 'Subscription' => 'Sottoscrizione', + 'Subscriptions on hold' => 'Sottoscrizioni in sospeso', + 'Subscriptions' => 'Sottoscrizioni', + 'Suppress emails' => 'Nascondi email', + 'Switch plan' => 'Cambia piano', + 'Switch' => 'Cambia', + 'System' => 'Sistema', + 'Table Columns' => 'Colonne tabella', + 'Target - The category relationship field is on the purchasable' => 'Obiettivo - Il campo della relazione di categoria è sul prodotto disponibile all\'acquisto', + 'Tax & Shipping' => 'Imposta e spedizione', + 'Tax (inc)' => 'Imposta (inc.)', + 'Tax Categories' => 'Categorie fiscali', + 'Tax Category' => 'Categoria fiscale', + 'Tax Rates' => 'Aliquote fiscali', + 'Tax Zone' => 'Zona fiscale', + 'Tax Zones' => 'Zone fiscali', + 'Tax categories deleted.' => 'Categorie fiscali eliminate.', + 'Tax category saved.' => 'Categoria fiscale salvata.', + 'Tax category updated.' => 'Categoria fiscale aggiornata.', + 'Tax rate saved.' => 'Aliquota fiscale salvata.', + 'Tax rates updated.' => 'Aliquote fiscali aggiornate.', + 'Tax zone saved.' => 'Zona fiscale salvata.', + 'Tax' => 'Imposta', + 'Taxable Subject' => 'Imponibile', + 'Template Path' => 'Percorso al template', + 'That handle is already in use' => 'Handle già in uso', + 'That handle is already in use.' => 'Handle già in uso.', + 'The PDF to attach to this email.' => 'Il PDF da allegare a questa e-mail.', + 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'L\'url della pagina contenente la sezione di aggiornamento dei dettagli di fatturazione di una sottoscrizione e di gestione dell\'autenticazione 3DS.', + 'The address provided is outside the store’s market.' => 'L\'indirizzo fornito è al di fuori del mercato dello store.', + 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'L\'importo dello sconto che viene applicato all\'intero ordine. Questo importo è ripartito tra le varie voci in ordine di prezzo da quello più elevato a quello più ridotto, fino ad esaurimento dello sconto.', + 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'Lo sconto di base può solo scontare fino a zero gli articoli nel carrello fino ad esaurimento; non può rendere negativo l\'ordine.', + 'The cart recovery link is invalid. Please request a new one.' => 'Il link per il recupero del carrello non è valido. Richiedine uno nuovo.', + 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Tasso di conversione che sarà usato per convertire un importo in questa valuta. Per esempio, se un articolo costa {amount1}, un tasso di conversione del {rate} darà come risultato {amount2} nella valuta alternativa.', + 'The countries that orders are allowed to be placed from.' => 'Paesi da cui è consentito inviare ordini.', + 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'Il coupon “{code}” ha superato il limite di utilizzo di {limit}.', + 'The customer for this order has been deleted.' => 'Il cliente associato a questo ordine è stato eliminato.', + 'The default shipping category is automatically available to all product types.' => 'La categoria di spedizione predefinita è automaticamente disponibile per tutti i tipi di prodotto.', + 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'Lo sconto “{name}” ha superato il limite totale di utilizzo di {limit}.', + 'The download link has expired. Please request a new one.' => 'Il link di download è scaduto. Richiedine uno nuovo.', + 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'Indirizzo email da cui vengono inviate le email di stato dell’ordine. Lasciare vuoto per utilizzare l’indirizzo email di sistema definito nelle Impostazioni generali di Craft.', + 'The entry that contains the description for this subscription’s plan.' => 'La Voce che contiene la descrizione di questo piano di sottoscrizione.', + 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'Importo forfettario corrispondente allo sconto applicabile a ciascun articolo, ovvero “3” per uno sconto di 3 $ su ciascun articolo.', + 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'Formato usato per generare nuovi codici promozionali, ad es., {example}. Eventuali caratteri `#` saranno sostituiti da lettere casuali.', + 'The from and to inventory locations must be different.' => 'Le sedi dell\'inventario da e verso devono essere diverse.', + 'The inventory locations this store uses.' => 'Le sedi dell\'inventario utilizzate da questo store.', + 'The item is not enabled for sale.' => 'L’articolo non è abilitato alla vendita.', + 'The language the order was made in.' => 'La lingua in cui è stato effettuato l\'ordine.', + 'The language to be used when this email is rendered.' => 'La lingua da utilizzare quando viene visualizzata questa email.', + 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Il numero massimo di livelli che questo tipo di prodotto può avere. Lasciare in bianco se non ha importanza.', + 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Importo massimo che il cliente dovrebbe spendere per la spedizione. Impostare su zero per disabilitare.', + 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Importo minimo che il cliente dovrebbe spendere per la spedizione. Impostare su zero per disabilitare.', + 'The order is not valid.' => 'L\'ordine non è valido.', + 'The payment gateway that will be used for the subscription plan.' => 'Quale gateway di pagamento verrà utilizzato per il piano di sottoscrizione.', + 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'Il valore percentile da scontare per ogni articolo, cioè {ex1} per il {ex2} di sconto. Le percentuali sono arrotondate a 2 decimali.', + 'The previously-selected shipping method is no longer available.' => 'Il metodo di spedizione selezionato in precedenza non è più disponibile.', + 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Il prezzo di {description} è aumentato da {originalSalePriceAsCurrency} a {newSalePriceAsCurrency}', + 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Il prezzo di {description} è diminuito da {originalSalePriceAsCurrency} a {newSalePriceAsCurrency}', + 'The primary currency cannot be changed after orders are placed.' => 'La valuta principale non può essere modificata dopo il completamento degli ordini.', + 'The purchasable defines the relationship' => 'L\'articolo disponibile all\'acquisto definisce la relazione', + 'The purchasable is related by another element' => 'L\'articolo disponibile all\'acquisto è correlato a un altro elemento', + 'The recipient of the email. Twig code can be used here.' => 'Il destinatario dell\'email. Qui è possibile utilizzare il codice Twig.', + 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'L\'indirizzo e-mail di risposta. Lasciare vuoto questo campo per rispondere con l\'indirizzo standard al mittente dell\'email. Qui è possibile utilizzare il codice Twig.', + 'The site the order was made in.' => 'Il sito in cui è stato effettuato l\'ordine.', + 'The site to be used when this email is rendered.' => 'Il sito da utilizzare quando viene visualizzata questa email.', + 'The subject line of the email. Twig code can be used here.' => 'L\'oggetto dell\'email. Qui è possibile utilizzare il codice Twig.', + 'The template that the PDF should be generated from.' => 'Il template da cui deve essere generato il PDF.', + 'The template to be used for HTML emails.' => 'Template da usare per le email HTML.', + 'The template to be used for plain text emails. Twig code can be used here.' => 'Il template da usare per le email di testo semplice. Qui è possibile utilizzare il codice Twig.', + 'The template to use when a product’s URL is requested.' => 'Template da usare quando è richiesto l’URL di un prodotto.', + 'The total number of order adjustments changed.' => 'Numero totale di rettifiche ordine modificate.', + 'The total price of the order changed.' => 'Prezzo totale dell’ordine modificato.', + 'The total quantity of items within the order changed.' => 'Quantità totale di articoli nell’ordine modificata.', + 'The unique SKU of the donation purchasable.' => 'SKU univoco della donazione disponibile all\'acquisto.', + 'The unit of measurement that should be used when specifying product dimensions.' => 'Unità di misura che dovrebbe essere utilizzata quando si specificano le dimensioni dei prodotti.', + 'The unit of measurement that should be used when specifying product weights.' => 'Unità di misura che dovrebbe essere utilizzata quando si specificano i pesi dei prodotti.', + 'The webhook URL for this gateway.' => 'L’URL del webhook per questo gateway.', + 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'Nome “Da” che verrà utilizzato per l’invio delle email di stato dell’ordine. Lasciare vuoto per usare il nome del mittente definito nelle Impostazioni generali di Craft.', + 'There are errors on the order' => 'L\'ordine contiene degli errori', + 'There are only {num} “{description}” items left in stock.' => 'Ci sono solo {num} articoli “{description}” in stock.', + 'There aren’t any product types to select yet.' => 'Non ci sono ancora tipi di prodotto da selezionare.', + 'There is no gateway or payment source available for use with this order.' => 'Non è disponibile un gateway o una fonte di pagamento utilizzabile con questo ordine.', + 'There is no gateway selected that supports payment sources.' => 'Non è stato selezionato nessun gateway che supporti le fonti di pagamento.', + 'There is no shipping method selected for this order.' => 'Non è stato selezionato alcun metodo di spedizione per questo ordine.', + 'This URL will load the cart into the user’s session, making it the active cart.' => 'Questo URL caricherà il carrello nella sessione dell\'utente, rendendolo il carrello attivo.', + 'This action is not allowed for the current user.' => 'Azione non consentita per l’utente corrente.', + 'This category will be used as the default for all purchasables in this store.' => 'Questa categoria verrà utilizzata come predefinita per tutti gli articoli disponibili all’acquisto in questo store.', + 'This coupon is for registered users and limited to {limit} uses.' => 'Questo coupon è riservato agli utenti registrati e limitato a {limit} utilizzi.', + 'This coupon is limited to {limit} uses.' => 'Questo coupon è limitato a {limit} utilizzi.', + 'This coupon requires an email address.' => 'Questo coupon richiede un indirizzo e-mail.', + 'This gateway does not support that functionality.' => 'Questo gateway non supporta tale funzionalità.', + 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Questo è stato escluso dalle impostazioni di configurazione {setting} in `config/{file}.php`.', + 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Questo è l’indirizzo del tuo store. Potrebbe essere utilizzato da vari plug-in per la determinazione, ad esempio, della spedizione e delle imposte. Potrebbe anche essere utilizzato nelle ricevute PDF.', + 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Questo è il PDF predefinito che verrà visualizzato quando si richiede il PDF dell\'ordine.', + 'This is the last location for the {store} store.' => 'Questa è l\'ultima sede dello store {store}.', + 'This month' => 'Questo mese', + 'This order has unsaved changes.' => 'Quest\'ordine ha modifiche non salvate.', + 'This week' => 'Questa settimana', + 'This year' => 'Quest\'anno', + 'Times Used' => 'Numero di utilizzi', + 'Title' => 'Titolo', + 'To' => 'A', + 'Today' => 'Oggi', + 'Too many variants for this product.' => 'Troppe varianti per questo prodotto.', + 'Top Customers by Average Order' => 'Principali clienti per ordine medio', + 'Top Customers by Total Revenue' => 'Principali clienti per ricavi totali', + 'Top Customers' => 'Clienti principali', + 'Top Product Types by Qty Sold' => 'Tipi di prodotti principali più venduti per qtà vendute', + 'Top Product Types by Revenue' => 'Principali tipi di prodotti per ricavo', + 'Top Product Types' => 'Principali tipi di prodotti', + 'Top Products by Qty Sold' => 'Prodotti principali per qtà vendute', + 'Top Products by Revenue' => 'Principali prodotti per ricavo', + 'Top Products' => 'Prodotti principali', + 'Top Purchasables by Qty Sold' => 'Principali prodotti disponibili all\'acquisto per qtà vendute', + 'Top Purchasables by Revenue' => 'Principali prodotti disponibili all\'acquisto per ricavo', + 'Top Purchasables' => 'Principali prodotti disponibili all’acquisto', + 'Total ' => 'Totale ', + 'Total Discount Use Limit' => 'Limite totale di utilizzo sconto', + 'Total Discount' => 'Sconto totale', + 'Total Included Tax' => 'Totale imposta inclusa', + 'Total Orders by Billing Country' => 'Totale ordini per Paese di spedizione', + 'Total Orders by Country' => 'Totale ordini per Paese', + 'Total Orders by Shipping Country' => 'Totale ordini per Paese di spedizione', + 'Total Orders' => 'Ordini totali', + 'Total Paid' => 'Totale pagato', + 'Total Price' => 'Prezzo totale', + 'Total Qty' => 'Quantità totale', + 'Total Revenue' => 'Ricavi totali', + 'Total Shipping' => 'Totale spedizione', + 'Total Tax' => 'Totale imposta', + 'Total Weight' => 'Peso totale', + 'Total' => 'Totale', + 'Track Inventory' => 'Traccia inventario', + 'Transaction Hash' => 'Hash di transazione', + 'Transaction ID' => 'ID transazione', + 'Transaction captured successfully: {message}' => 'Transazione acquisita correttamente: {message}', + 'Transaction refunded successfully: {message}' => 'Transazione rimborsata correttamente: {message}', + 'Transactions' => 'Transazioni', + 'Transfer Fields' => 'Campi di trasferimento', + 'Transfer Items' => 'Voci di trasferimento', + 'Transfer Settings' => 'Impostazioni di trasferimento', + 'Transfer Status' => 'Stato di trasferimento', + 'Transfer fields saved.' => 'Campi di trasferimento salvati.', + 'Transfer must have at least one item.' => 'Il trasferimento deve avere almeno una voce.', + 'Transfer' => 'Trasferimento', + 'Transfers' => 'Trasferimenti', + 'Trial days credited' => 'Giorni di prova concessi', + 'Trial expiration' => 'Scadenza prova', + 'Trial expiry date' => 'Data di scadenza del periodo di prova', + 'Type not in allowed options.' => 'Tipo non incluso nelle opzioni consentite.', + 'Type' => 'Tipo', + 'URI' => 'URI', + 'Unable to cancel subscription at this time.' => 'Al momento non è possibile annullare la sottoscrizione.', + 'Unable to complete order: another request is already in progress.' => 'Impossibile completare l\'ordine: è già in corso un\'altra richiesta.', + 'Unable to find variant.' => 'Impossibile trovare la variante.', + 'Unable to generate coupon codes: {message}' => 'Impossibile generare codici promozionali: {message}', + 'Unable to make payment at this time.' => 'Al momento non è possibile effettuare il pagamento.', + 'Unable to modify subscription at this time.' => 'Al momento non è possibile modificare la sottoscrizione.', + 'Unable to reactivate subscription at this time.' => 'Al momento non è possibile riattivare la sottoscrizione.', + 'Unable to reassign orders.' => 'Impossibile riassegnare gli ordini.', + 'Unable to remove order data.' => 'Impossibile eliminare i dati dell\'ordine.', + 'Unable to retrieve Sale and Purchasable.' => 'Impossibile recuperare Vendita promozionale e Disponibile all’acquisto.', + 'Unable to retrieve cart.' => 'Impossibile recuperare il carrello.', + 'Unable to retrieve customer.' => 'Impossibile recuperare il cliente.', + 'Unable to retrieve load cart URL' => 'Impossibile recuperare l\'URL del carrello caricato', + 'Unable to retrieve payment source.' => 'Impossibile recuperare fonte di pagamento.', + 'Unable to set default shipping category.' => 'Impossibile impostare la categoria di spedizione predefinita.', + 'Unable to set default tax category.' => 'Impossibile impostare la categoria d\'imposta predefinita.', + 'Unable to set primary payment source.' => 'Impossibile creare fonte di pagamento principale.', + 'Unable to start the subscription. Please check your payment details.' => 'Impossibile avviare la sottoscrizione. Controllare i dati di pagamento.', + 'Unable to subscribe at this time.' => 'Al momento non è possibile effettuare la sottoscrizione.', + 'Unable to update cart.' => 'Impossibile aggiornare il carrello.', + 'Unable to validate address.' => 'Impossibile convalidare l’indirizzo.', + 'Unit Price' => 'Prezzo unitario', + 'Unit price (minus discounts)' => 'Prezzo unitario (meno gli sconti)', + 'Units' => 'Unità', + 'Unpaid' => 'Non pagato', + 'Unsubscribe' => 'Annulla sottoscrizione', + 'Update Address' => 'Aggiorna indirizzo', + 'Update Order Status' => 'Aggiorna stato ordine', + 'Update Order Status…' => 'Aggiornamento stato ordine in corso...', + 'Update order' => 'Aggiorna ordine', + 'Update subscription' => 'Aggiorna sottoscrizione', + 'Update' => 'Aggiorna', + 'Updated By' => 'Aggiornato da', + 'Updated committed stock successfully.' => 'Aggiornamento dello stock impegnato completato correttamente.', + 'Updated' => 'Aggiornato', + 'Use Billing Address For Tax' => 'Usa l\'indirizzo di fatturazione per le imposte', + 'Use as the primary billing address' => 'Usa come indirizzo di fatturazione principale', + 'Use as the primary shipping address' => 'Usa come indirizzo di spedizione principale', + 'Used By Tax Rates' => 'Utilizzato per aliquote fiscali', + 'Used by Tax Rates' => 'Utilizzato per aliquote fiscali', + 'User Groups' => 'Gruppi di utenti', + 'User not found.' => 'Utente non trovato.', + 'User' => 'Utente', + 'Uses' => 'Utilizzi', + 'Validate Business Tax ID as Vat ID' => 'Convalida l\'ID fiscale dell\'azienda come ID IVA', + 'Validating condition syntax' => 'Convalida della sintassi della condizione', + 'Validating formula syntax' => 'Convalida della sintassi della formula', + 'Variant Fields' => 'Campi varianti', + 'Variant Has Untracked Stock' => 'La variante ha stock non tracciato', + 'Variant Price' => 'Prezzo variante', + 'Variant SKU' => 'SKU variante', + 'Variant Search' => 'Ricerca variante', + 'Variant Stock' => 'Stock variante', + 'Variant Title Format' => 'Formato titolo variante', + 'Variant Tracks Stock' => 'La variante traccia lo stock', + 'Variant UI Label Format' => 'Formato etichetta UI variante', + 'Variant has no product.' => 'La variante non ha prodotti.', + 'Variants not restored.' => 'Varianti non ripristinate.', + 'Variants restored.' => 'Varianti ripristinate.', + 'Variants' => 'Varianti', + 'View customer' => 'Visualizza cliente', + 'View order' => 'Visualizza ordine', + 'View product type - {productType}' => 'Visualizza il tipo di prodotto - {productType}', + 'View user' => 'Visualizza utente', + 'View' => 'Mostra', + 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Attenzione, eliminando questa valuta verranno bloccati tutti i pagamenti e i rimborsi in questa valuta, sei sicuro di voler eliminare "{name}"?', + 'Web' => 'Web', + 'Webhook URL' => 'URL webhook', + 'Weight ({unit})' => 'Peso ({unit})', + 'Weight Rate' => 'Tariffa in base al peso', + 'Weight Unit' => 'Unità di peso', + 'Weight' => 'Peso', + 'What product URIs should look like for the site.' => 'L\'aspetto degli URL del prodotto per il sito.', + 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Aspetto dei titoli dei prodotti auto-generati. È possibile includere tag che producono le proprietà dei prodotti, come {ex1} o {ex2}. Tutti i campi personalizzati utilizzati devono essere impostati come obbligatori.', + 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Aspetto dei titoli delle varianti auto-generati. È possibile includere tag che producono le proprietà delle varanti, come {ex1} o {ex2}. Tutti i campi personalizzati utilizzati devono essere impostati come obbligatori.', + 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Come dovrebbe apparire il nome del file PDF dell\'ordine (senza estensione). È possibile includere tag che producono le proprietà dell\'ordine, come {ex1} o {ex2}.', + 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Aspetto degli SKU univoci auto-generati, quando un campo SKU viene inviato senza un valore. È possibile includere tag che producono proprietà, come {ex1} o {ex2}.', + 'What this PDF will be called in the control panel.' => 'Nome di questo PDF nel pannello di controllo.', + 'What this catalog pricing rule will be called in the control panel.' => 'Come si chiamerà questa regola di prezzo in catalogo nel pannello di controllo.', + 'What this discount will be called in the control panel.' => 'Nome di questo sconto nel pannello di controllo.', + 'What this email will be called in the control panel.' => 'Nome di questa email nel pannello di controllo.', + 'What this product type will be called in the control panel.' => 'Nome di questo tipo di prodotto nel pannello di controllo.', + 'What this sale will be called in the control panel.' => 'Nome di questa vendita promozionale nel pannello di controllo.', + 'What this shipping category will be called in the control panel.' => 'Come si chiamerà questa categoria di spedizione nel pannello di controllo.', + 'What this shipping rule will be called in the control panel.' => 'Come si chiamerà questa regola di spedizione nel pannello di controllo.', + 'What this shipping zone will be called in the control panel.' => 'Come si chiamerà questa zona di spedizione nel pannello di controllo.', + 'What this status will be called in the control panel.' => 'Nome di questo stato nel pannello di controllo.', + 'What this subscription plan will be called in the control panel.' => 'Nome di questo piano di sottoscrizione nel pannello di controllo.', + 'What this tax category will be called in the control panel.' => 'Come si chiamerà questa categoria fiscale nel pannello di controllo.', + 'What this tax zone will be called in the control panel.' => 'Come si chiamerà questa zona fiscale nel pannello di controllo.', + 'When this discount is applied to an order, which line items should be discounted?' => 'Quando questo sconto è applicato a un ordine, quali articoli è necessario scontare?', + 'Whether the first available shipping method option should be set automatically on carts.' => 'Definisce se la prima opzione di metodo di spedizione disponibile debba essere impostata automaticamente sui carrelli.', + 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Definisce se il metodo di pagamento principale dell\'utente debba essere impostato automaticamente sui nuovi carrelli.', + 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Definisce se gli indirizzi principali di spedizione e fatturazione dell\'utente debbano essere impostati automaticamente sui nuovi carrelli.', + 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Se questa regola di prezzo in catalogo deve essere disponibile all’uso, indipendentemente da altre condizioni.', + 'Whether this sale should be available for use, regardless of other conditions.' => 'Se questa vendita deve essere disponibile all’uso, indipendentemente da altre condizioni.', + 'Which data to display in the name column in the results table.' => 'Quali dati visualizzare nella colonna del nome nella tabella dei risultati.', + 'Which product types should this category be available to?' => 'Per quali tipi di prodotto deve essere disponibile questa categoria?', + 'Which template should be loaded when a product’s URL is requested.' => 'Il template da caricare quando viene richiesto l\'URL di un prodotto.', + 'Width ({unit})' => 'Larghezza ({unit})', + 'Width' => 'Larghezza', + 'YYYY' => 'AAAA', + 'Yes' => 'Sì', + 'You are not allowed to add a line item.' => 'Non è consentito aggiungere una voce.', + 'You currently have no emails configured to select for this status.' => 'Attualmente non sono presenti e-mail configurate selezionabili per questo stato.', + 'You do not have permission to load this cart.' => 'Non disponi delle autorizzazioni necessarie per caricare questo carrello.', + 'You must set up at least one gateway that supports subscriptions first.' => 'Prima di tutto è necessario impostare almeno un gateway che supporti le sottoscrizioni.', + 'You must be logged in or provide a valid token to load this cart.' => 'Per caricare questo carrello è necessario effettuare l’accesso o presentare un token valido.', + 'You must be signed in to create a payment source.' => 'Per creare una fonte di pagamento è necessario effettuare l’accesso.', + 'You must be signed in to set a primary payment source.' => 'Per creare una fonte di pagamento principale è necessario effettuare l’accesso.', + 'You must make a payment to complete the order.' => 'È necessario effettuare un pagamento per completare l\'ordine.', + 'Your Cart Recovery Link' => 'Il tuo link per il recupero del carrello', + 'Your Order PDF Download Link' => 'Il tuo link di download PDF ordine', + 'Your order is empty' => 'Il tuo ordine è vuoto', + 'ZIP file' => 'File ZIP', + 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Zero - Il prezzo minimo è zero se gli sconti sono di importo superiore al valore dell\'ordine.', + 'Zip Code' => 'CAP', + 'all' => 'tutti', + 'any' => 'qualsiasi', + 'average order total' => 'totale medio degli ordini', + 'billing address' => 'indirizzo di fatturazione', + 'donation' => 'donazione', + 'donations' => 'donazioni', + 'info' => 'informazioni', + 'inventory location' => 'sede dell\'inventario', + 'new customers' => 'nuovi clienti', + 'on hand' => 'disponibile', + 'only' => 'solo', + 'order' => 'ordine', + 'orders' => 'ordini', + 'price' => 'prezzo', + 'prices' => 'prezzi', + 'product variant' => 'variante prodotto', + 'product variants' => 'varianti prodotto', + 'product' => 'prodotto', + 'products' => 'prodotti', + 'repeat customers' => 'clienti abituali', + 'shipping address' => 'indirizzo di spedizione', + 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'È impossibile impostare sia shippingSameAsBilling che billingSameAsShipping.', + 'subscription' => 'sottoscrizione', + 'subscriptions' => 'sottoscrizioni', + 'to' => 'a', + 'transfer' => 'trasferisci', + 'transfers' => 'trasferimenti', + '{amount} included' => '{amount} incluso', + '{count} Unfulfilled Orders' => '{count} ordini non evasi', + '{description} is no longer available.' => '{description} non è più disponibile.', + '{description} only has {stock} in stock.' => 'Sono disponibili solo {stock} {description} a magazzino.', + '{from} to {to}' => 'Da {from} a {to}', + '{name} (Primary)' => '{name} (Primario)', + '{name} (Trashed)' => '{name} (Spostato nel cestino)', + '{name} catalog price' => '{name} prezzo in catalogo', + '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, one {}=1{ordine aggiornato} other{ordini aggiornati}}.', + '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, one {}=1{ordine è} other{ordini sono}} associato/i {numUsers, plural, one {}=1{all\'utente} other{agli utenti}}.', + '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, one {}=1{abbonamento è} other{abbonamenti sono}} attivato/i per {numUsers, plural, one {}=1{l\'utente} other{gli utenti}}.', + '{number} more…' => '{number} in più…', + '{pct} off the discounted item price' => '{pct} di sconto sul prezzo scontato dell\'articolo', + '{pct} off the original item price' => '{pct} di sconto sul prezzo originale dell\'articolo', + '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, one {}=1{non è stato assegnato} other{non sono stati assegnati}} a un sito.', + '{total} in total revenue' => '{total} in ricavi totali', + '{total} orders' => '{total} ordini', + '{total} saleable across {locationCount} location(s)' => '{total} vendibile in {locationCount} sede/i', + '{uses} uses across {emails} email addresses' => '{uses} utilizzi su {emails} indirizzi email', + '{uses} uses across {users} users' => '{uses} utilizzi da parte dei {users} clienti', + '“{description}” is currently out of stock.' => '“{description}” è attualmente esaurito.', + '“{key}” has invalid JSON' => '“{key}” ha un JSON non valido', +]; diff --git a/lang/ja/commerce.php b/lang/ja/commerce.php new file mode 100644 index 0000000000..bc1704e385 --- /dev/null +++ b/lang/ja/commerce.php @@ -0,0 +1,1423 @@ + '(新しい価格)', + '(of original price)' => '(値引き)', + '(off original price)' => '(元の価格からの割引)', + 'A cart number must be specified.' => 'カート番号を指定してください。', + 'A cart recovery link has been sent to {email}.' => 'カート復元リンクを{email}宛に送信しました。', + 'A cart recovery link will be sent to {email}.' => 'カート復元リンクを{email}宛に送信します。', + 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'カートが注文に変わると、このフォーマットに基づいて管理しやすい参照番号が生成されます。たとえば、{ex1}、または
{ex2}。このフォーマットの結果は一意でなければなりません。', + 'A new download link has been sent to {email}' => '新しいダウンロードリンクを {email} 宛に送信しました', + 'A new download link will be sent to {email}' => '新しいダウンロードリンクを {email} 宛に送信します', + 'A valid email is required to create a customer.' => '顧客を作成するために有効なメールが必要です。', + 'Accept' => '受け入れる', + 'Accepted' => '受け入れ済み', + 'Actions' => 'アクション', + 'Active Carts' => 'アクティブなカート', + 'Active subscriptions' => 'アクティブな定期支払い', + 'Active' => '有効', + 'Add Address' => '住所を追加', + 'Add a coupon' => 'クーポンを追加', + 'Add a custom line item' => 'カスタムラインアイテムを追加', + 'Add a line item' => 'ラインアイテムを追加', + 'Add a product' => '商品を追加', + 'Add a variant' => 'バリアントを追加', + 'Add an adjustment' => 'アジャストメントを追加', + 'Add an item' => 'アイテムを追加', + 'Add an option' => 'オプションを追加', + 'Add catalog price' => 'カテゴリ価格を追加', + 'Add' => '追加', + 'Additional Actions' => '追加のアクション', + 'Additional recipients that should receive this email. Twig code can be used here.' => 'このメールを受信するその他の受信者。ここに Twig コードを使用できます。', + 'Address 1' => '住所1', + 'Address 2' => '住所2', + 'Address 3' => '住所3', + 'Address Line 1' => '住所欄1', + 'Address Line 2' => '住所欄2', + 'Address Updated.' => '住所が更新されました。', + 'Address copied to user.' => 'アドレスをユーザーにコピーしました。', + 'Address not found.' => '住所が見つかりません。', + 'Adjust Quantity' => '数量を調整', + 'Adjust by' => '調整方法', + 'Adjust price when included rate is disqualified?' => '税込料金が不適格である場合、価格を調整しますか?', + 'Adjustments' => 'アジャストメント', + 'Admin Notices' => '管理者通知', + 'Administrative Area Code of Origin' => '原産地の行政区画のエリアコード', + 'Advanced' => '高度', + 'All Orders' => 'すべての注文', + 'All Totals' => 'すべての合計', + 'All Transfers' => 'すべての移動', + 'All active subscriptions' => 'すべての有効な定期支払い', + 'All customers' => 'すべての顧客', + 'All products' => 'すべての商品', + 'All variants must have a SKU.' => 'すべてのバリアントにはSKUが必要です。', + 'All' => 'すべて', + 'Allow Checkout Without Payment' => '支払いなしでのチェックアウトを許可する', + 'Allow Empty Cart On Checkout' => 'チェックアウト時の空のカートを許可する', + 'Allow Partial Payment On Checkout' => 'チェックアウトで一部支払いを許可する', + 'Allow out of stock purchases' => '在庫切れ商品の購入を許可する', + 'Allow' => '許可', + 'Allowed Qty' => '許可する数量', + 'Alternative Phone' => '予備の電話', + 'Amount' => '量', + 'An ID must be provided' => 'ID を指定してください', + 'An error occurred while generating this PDF.' => 'この PDF を生成中にエラーが発生しました。', + 'Any' => 'すべて', + 'Anywhere' => '指定なし', + 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => '定期支払いプラン「{name}」をアーカイブしてもよろしいですか?既存の定期支払いはキャンセルされません。', + 'Are you sure you want to capture this transaction?' => 'この取引をキャプチャしてもよろしいですか?', + 'Are you sure you want to complete this order?' => 'この注文を完了してもよろしいですか?', + 'Are you sure you want to delete the selected orders?' => '選択した注文を削除してもよろしいですか?', + 'Are you sure you want to delete the selected product and its variants?' => '選択した商品とそのバリアントを削除してもよろしいですか?', + 'Are you sure you want to delete this shipping rule?' => 'この配送ルールを削除してもよろしいですか?', + 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => '「{name}」とそのすべての商品を削除してもよろしいですか?この取り消しできない処理を実行する前に、データベースのバックアップがあることを確認してください。', + 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => '「{name}」を削除してよろしいですか? このステータスのあるすべてのラインアイテムがステータス無しに設定されます。', + 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'この移動を保留中としてマークしてもよろしいですか?これは移動先で入庫予定として表示されます。', + 'Are you sure you want to overwrite the billing address?' => 'この請求先住所を上書きしてよろしいですか?', + 'Are you sure you want to overwrite the shipping address?' => 'この配送先住所を上書きしてよろしいですか?', + 'Are you sure you want to permanently delete this store and everything in it?' => 'このストアとストアのすべてを完全に削除してもよろしいですか?', + 'Are you sure you want to refund this transaction?' => 'この取引を払い戻ししてもよろしいですか?', + 'Are you sure you want to remove this customer?' => 'この顧客を削除してもよろしいですか?', + 'Are you sure you want to save this as a new shipping rule?' => '新しい配送ルールとして保存してもよろしいですか?', + 'Are you sure you want to send email: {name}?' => '「{name}」メールを送信してもよろしいですか?', + 'At least one site must be enabled for the product type.' => '商品タイプには少なくとも1つのサイトを有効にする必要があります。', + 'Attempted Payments' => '試行された支払い', + 'Attention' => '部署名', + 'Authorize Only (Manually Capture)' => '承認のみ(手動キャプチャ)', + 'Auto Set Cart Shipping Method Option' => 'カート配送方法オプションを自動設定する', + 'Auto Set New Cart Addresses' => '新規カート住所を自動設定する', + 'Auto Set Payment Source' => '支払い元を自動設定する', + 'Automatic SKU Format' => 'SKUの自動フォーマット', + 'Available Shipping Categories' => '利用可能な配送カテゴリ', + 'Available Tax Categories' => '利用可能な税カテゴリ', + 'Available for purchase' => '購入可能', + 'Available for purchase?' => '購入可能にしますか?', + 'Available inventory for "{description}" has gone below zero.' => '「{description}」の利用可能在庫が0を下回っています。', + 'Available to Product Types' => '商品タイプで利用可能', + 'Available' => '利用可能', + 'Available?' => '購入可能?', + 'Average Order Total' => '注文合計平均', + 'Average' => '平均', + 'BCC’d Recipient' => 'BCC宛先', + 'Bad Request' => '不正なリクエスト', + 'Bad address ID.' => '不正な住所 ID です。', + 'Bad order ID.' => '不正な注文 ID です。', + 'Base Price' => '最安価格', + 'Base Promotional Price' => 'ベース販売促進価格', + 'Base Rate' => '基本料金', + 'Base' => '基本', + 'Bcc' => 'Bcc', + 'Billing Address' => '請求先住所', + 'Billing Business Name' => '請求先会社名', + 'Billing First Name' => '請求先の名', + 'Billing Full Name' => '請求先氏名', + 'Billing Last Name' => '請求先の姓', + 'Billing address required.' => '請求先住所が必要です。', + 'Billing detail update URL' => '課金詳細更新URL', + 'Billing issues' => '請求に関する問題', + 'Billing' => '請求先', + 'Both (Line item price + Line item shipping costs)' => '両方(ラインアイテム価格 + ラインアイテム配送料)', + 'Business ID' => '事業者ID', + 'Business Name' => '事業名', + 'Business Tax ID' => '事業税ID', + 'CC’d Recipient' => 'CC宛先', + 'CVV' => 'CVV', + 'Can be used as an internal reference.' => '内部で参照するために使用されます。', + 'Can not complete payment for missing transaction.' => '取引が見つからないため、支払いを完了できません。', + 'Can not create a new order' => '新しい注文を作成できません', + 'Can not find an order to pay.' => '支払う注文が見つかりません。', + 'Can not find enabled email.' => '有効なメールが見つかりません。', + 'Can not find order' => '注文が見つかりません', + 'Can not find order.' => '注文が見つかりません.', + 'Can not find the transaction to refund' => '払い戻しする取引が見つかりません', + 'Can not move between these inventory types.' => 'これらの在庫タイプ間は移動できません。', + 'Can not refund amount greater than the remaining amount' => '残金を上回る金額を払戻しできません', + 'Cancel subscription' => '定期支払いをキャンセルする', + 'Cancel with gateway now' => '今すぐゲートウェイでキャンセル', + 'Cancel' => 'キャンセル', + 'Cancellation date' => 'キャンセルした日付', + 'Cancellation' => 'キャンセル', + 'Cannot switch plans for this subscription.' => 'この定期支払いのプランを切り替えることはできません。', + 'Can’t preview this email.' => 'このメールはプレビューできません。', + 'Capture payment' => '支払いをキャプチャ', + 'Capture' => 'キャプチャ', + 'Card Holder' => 'カード名義人', + 'Card Number' => 'カード番号', + 'Card' => 'カード', + 'Cart Recovery Link' => 'カート復元リンク', + 'Cart forgotten.' => '忘れられたカート。', + 'Cart updated.' => 'カートが更新されました。', + 'Cart {number}' => 'カート{number}', + 'Catalog Pricing Rule' => 'カタログ価格ルール', + 'Catalog pricing rule description.' => 'カタログ価格ルールの説明。', + 'Catalog pricing rule saved.' => 'カタログ価格ルールを保存しました。', + 'Catalog pricing rules deleted.' => 'カタログ価格ルールを削除しました。', + 'Catalog pricing rules updated.' => 'カタログ価格ルールを更新しました。', + 'Categories Relationship Type' => 'カテゴリの関係付けタイプ', + 'Categories' => 'カテゴリ', + 'Category Rate Overrides' => 'カテゴリの料金上書き', + 'Centimeters (cm)' => 'センチメートル(cm)', + 'Changing this value may affect your ability to refund existing transactions.' => 'この値を変更すると、既存の取引の払い戻し機能に影響する場合があります。', + 'Choose a color to represent the order’s status' => '注文ステータスのカラーを選んでください', + 'Choose a new customer' => '新しい顧客を選択', + 'Choose adjustment values to include when calculating the product revenue total.' => '商品の合計収益を計算する際に含める調整値を選択してください。', + 'Choose the currency’s ISO code.' => 'この国のISOコードを選択してください。', + 'Choose the destination inventory location for the existing on hand stock.' => '宛先の既存の手持ち在庫の在庫場所を選択してください。', + 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'このセクションを表示可能にする商品タイプを選択して、サイト固有の設定を行ってください。', + 'City' => '市区町村', + 'Clear counter' => 'カウントをリセットする', + 'Clear notices' => '通知をクリア', + 'Close' => '閉じる', + 'Code' => 'コード', + 'Collated PDF' => '照合済みの PDF', + 'Color' => 'カラー', + 'Commerce Products' => 'Commerce 商品', + 'Commerce Settings' => 'コマース設定', + 'Commerce Variants' => 'Commerce バリアント', + 'Commerce email “{email}” could not be sent for order “{order}”.' => '注文「{order}」の Commerce メール「{email}」を送信できませんでした。', + 'Commerce order exports' => 'Commerce 注文のエクスポート', + 'Commerce' => 'Commerce', + 'Committed' => 'コミット済み', + 'Completed Email' => '完了したメール', + 'Completed' => '完了', + 'Completing order failed.' => '注文の完了に失敗しました。', + 'Condition' => '条件', + 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'ここでの条件は、ルールを調べる前に注文に対して照合されます。これは、前もってメソッドの可用性を評価する場合、またはこのメソッドに対してすべてのルールに共通の条件がある場合に役立ちます。', + 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'ここで設定した条件は、ルールを確認する前に注文の顧客情報と照合されます。これは、メソッドの利用可否を早い段階で判断したい場合や、このメソッドに適用されるすべてのルールに共通する条件を設けたい場合に便利です。', + 'Conditions' => '条件', + 'Contains Purchasables' => '購入可能商品を含む', + 'Control Panel Settings' => 'コントロールパネルの設定', + 'Control panel' => 'コントロールパネル', + 'Conversion Rate' => '換算レート', + 'Converted Price' => '換算価格', + 'Copied!' => 'コピーしました!', + 'Copy the URL' => 'URL をコピー', + 'Copy to {location}' => '{location}へコピー', + 'Copy' => 'コピー', + 'Costs' => '料金', + 'Could not archive gateway.' => 'ゲートウェイをアーカイブできませんでした。', + 'Could not cancel “{reference}”.' => '「{reference}」をキャンセルできませんでした。', + 'Could not create the payment source.' => '支払い元を作成できませんでした。', + 'Could not delete shipping rule' => '配送ルールを削除できませんでした', + 'Could not delete shipping zone' => '配送地域を削除できませんでした', + 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => '{count, number}件の配送{count, plural, one{カテゴリ} other{カテゴリ}}を削除できませんでした。', + 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => '{count, number}件の配送{count, plural, one{方法} other{方法}}とルールを削除できませんでした。', + 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => '{count, number}件の税{count, plural, one{カテゴリ} other{カテゴリ}}を削除できませんでした。', + 'Could not find the email or template.' => 'メールまたはテンプレートが見つかりませんでした。', + 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => '注文 {number} を完了としてマークできませんでした。注文完了中、注文の保存はエラーにより失敗しました: {order}', + 'Could not reactivate “{reference}”.' => '「{reference}」を再度有効にできませんでした。', + 'Could not send email' => 'メールを送信できませんでした', + 'Could not switch “{reference}” to “{plan}”.' => '「{reference}」を「{plan}」に切り替えることができませんでした。', + 'Could not update orders address.' => '注文の住所を更新できませんでした。', + 'Couldn’t archive Line Item Status.' => 'ラインアイテムのステータスをアーカイブできませんでした。', + 'Couldn’t archive Order Status.' => '注文ステータスをアーカイブできませんでした。', + 'Couldn’t capture transaction.' => '取引をキャプチャできませんでした。', + 'Couldn’t capture transaction: {message}' => '取引をキャプチャできませんでした: {message}', + 'Couldn’t delete email.' => 'メールを削除できませんでした。', + 'Couldn’t delete the payment source.' => '支払い元を削除できませんでした。', + 'Couldn’t get order.' => '注文を取得できませんでした。', + 'Couldn’t recalculate order.' => '注文を再計算できませんでした。', + 'Couldn’t refund transaction.' => '取引を払い戻しできませんでした。', + 'Couldn’t refund transaction: {message}' => '取引を払い戻しできませんでした: {message}', + 'Couldn’t reorder Line Item Statuses.' => 'ラインアイテムのステータスを並び替えできませんでした。', + 'Couldn’t reorder Order Statuses.' => '注文ステータスを並び替えできませんでした。', + 'Couldn’t reorder PDFs.' => 'PDF を並び替えできませんでした。', + 'Couldn’t reorder discounts.' => 'ディスカウントを並び替えできませんでした。', + 'Couldn’t reorder gateways.' => 'ゲートウェイを並び替えできませんでした。', + 'Couldn’t reorder plans.' => 'プランを並び替えできませんでした。', + 'Couldn’t reorder rules.' => 'ルールを並び替えできませんでした。', + 'Couldn’t reorder sale.' => 'セールを並び替えできませんでした。', + 'Couldn’t reorder sales.' => 'セールを並び替えできませんでした。', + 'Couldn’t reorder statuses.' => 'ステータスを並び替えできませんでした。', + 'Couldn’t reorder stores.' => 'ストアを並び替えできませんでした。', + 'Couldn’t save PDF.' => 'PDF を保存できませんでした。', + 'Couldn’t save catalog pricing rule.' => 'カタログ価格ルールを保存できませんでした。', + 'Couldn’t save currency.' => '通貨を保存できませんでした。', + 'Couldn’t save discount.' => 'ディスカウントを保存できませんでした。', + 'Couldn’t save email.' => 'メールを保存できませんでした。', + 'Couldn’t save gateway.' => 'ゲートウェイを保存できませんでした。', + 'Couldn’t save inventory location.' => '在庫場所を保存できませんでした。', + 'Couldn’t save line item status.' => 'ラインアイテムのステータスを保存できませんでした。', + 'Couldn’t save order fields.' => '注文フィールドを保存できませんでした。', + 'Couldn’t save order status.' => '注文ステータスを更新できませんでした。', + 'Couldn’t save order.' => '注文を保存できませんでした。', + 'Couldn’t save product type.' => '商品タイプを保存できませんでした。', + 'Couldn’t save sale.' => 'セールを保存できませんでした。', + 'Couldn’t save settings.' => '設定を保存できませんでした。', + 'Couldn’t save shipping category.' => '配送カテゴリを保存できませんでした。', + 'Couldn’t save shipping method.' => '配送方法を保存できませんでした。', + 'Couldn’t save shipping rule.' => '配送ルールを保存できませんでした。', + 'Couldn’t save shipping zone.' => '配送地域を保存できませんでした。', + 'Couldn’t save store.' => 'ストアを保存できませんでした。', + 'Couldn’t save subscription fields.' => '定期支払いフィールドを保存できませんでした。', + 'Couldn’t save subscription plan.' => '定期支払いプランを保存できませんでした。', + 'Couldn’t save subscription.' => '定期支払いを保存できませんでした。', + 'Couldn’t save tax category.' => '税カテゴリを保存できませんでした。', + 'Couldn’t save tax rate.' => '税率を保存できませんでした。', + 'Couldn’t save tax zone.' => '税対象地域を保存できませんでした。', + 'Couldn’t save transfer fields.' => '移動フィールドを保存できませんでした。', + 'Couldn’t update catalog pricing rule statuses.' => 'カタログ価格ルールのステータスを更新できませんでした。', + 'Couldn’t update status.' => 'ステータスを更新できませんでした。', + 'Couldn’t updated sales status.' => 'セールのステータスを更新できませんでした。', + 'Country Code of Origin' => '原産国コード', + 'Country List' => '国リスト', + 'Country not allowed.' => '許可されていない国です。', + 'Country' => '国', + 'Coupon Code' => 'クーポンコード', + 'Coupon can not apply discount to this order due to address mismatch.' => 'アドレスが一致しなかったため、この注文にはクーポンのディスカウントを適用できません。', + 'Coupon can not apply discount to this order due to customer mismatch.' => '顧客が一致しなかったため、この注文にはクーポンのディスカウントを適用できません。', + 'Coupon can not apply discount to this order.' => 'この注文にはクーポンのディスカウントを適用できません。', + 'Coupon code “{code}” is already in use by discount “{name}”.' => 'クーポンコード「{code}」はディスカウント「{name}」によってすでに使用されています。', + 'Coupon codes cannot be blank.' => 'クーポンコードは空白にできません。', + 'Coupon codes must be unique.' => 'クーポンコードは一意でなければなりません。', + 'Coupon format is required and must contain at least one `#`.' => 'クーポンフォーマットが必要で、少なくとも1つの「#」を含んでいなければなりません。', + 'Coupon not valid.' => 'クーポンが有効ではありません。', + 'Coupon removed: {explanation}' => 'クーポンが削除されました:{explanation}', + 'Coupons' => 'クーポン', + 'Craft Commerce - Administration' => 'Craft Commerce - 管理', + 'Craft Commerce - Inventory' => 'Craft Commerce - 在庫', + 'Craft Commerce - Orders' => 'Craft Commerce - 注文', + 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - 商品タイプ - {name}', + 'Craft Commerce - Subscriptions' => 'Craft Commerce - サブスクリプション', + 'Create a Discount' => 'ディスカウントを作成', + 'Create a Subscription Plan' => '定期支払いプランを作成', + 'Create a new PDF' => '新しい PDF を作成', + 'Create a new catalog pricing rule' => '新規カタログ価格ルールを作成', + 'Create a new currency' => '新しい通貨を作成', + 'Create a new email' => '新しいメールを作成', + 'Create a new gateway' => '新しいゲートウェイを作成', + 'Create a new line item status' => '新しいラインアイテムのステータスを作成', + 'Create a new order status' => '新しい注文ステータスを作成', + 'Create a new product type' => '新しい商品タイプを作成する', + 'Create a new sale' => '新しいセールを作成', + 'Create a new shipping category' => '新しい配送カテゴリを作成', + 'Create a new shipping method' => '新しい配送方法を作成', + 'Create a new shipping rule' => '新しい配送ルールを作成', + 'Create a new tax category' => '新しい税カテゴリを作成する', + 'Create a new tax rate' => '新しい税率を作成', + 'Create a product type' => '商品タイプを作成', + 'Create a shipping zone' => '配送地域を作成', + 'Create a tax zone' => '税対象地域を作成', + 'Create catalog pricing rules' => 'カタログ価格ルールを作成', + 'Create customer: “{email}”' => '顧客を作成: 「{email}」', + 'Create discounts' => 'ディスカウントを作成', + 'Create discount…' => 'ディスカウントを作成…', + 'Create rules that allow this discount to match the order.' => 'このディスカウントを注文に一致させるルールを作成します。', + 'Create rules that allow this discount to match the order’s billing address.' => 'このディスカウントを注文の請求先住所に一致させるルールを作成します。', + 'Create rules that allow this discount to match the order’s customer.' => 'このディスカウントを注文の顧客に一致させるルールを作成します。', + 'Create rules that allow this discount to match the order’s shipping address.' => 'このディスカウントを注文の配送先住所に一致させるルールを作成します。', + 'Create rules that allow this gateway to match the billing address.' => 'このゲートウェイを請求先住所と一致させるルールを作成します。', + 'Create rules that allow this gateway to match the order.' => 'このゲートウェイを注文に一致させるルールを作成します。', + 'Create rules that allow this gateway to match the shipping address.' => 'このゲートウェイを配送先住所と一致させるルールを作成します。', + 'Create sales' => 'セールを作成', + 'Create sale…' => 'セールを作成…', + 'Created' => '作成済み', + 'Credit Card Payment Type' => 'クレジットカード決済のタイプ', + 'Currency Code' => '通貨コード', + 'Currency saved.' => '通貨は保存されました。', + 'Currency' => '通貨', + 'Current' => '現在', + 'Custom 1' => 'カスタム1', + 'Custom 2' => 'カスタム2', + 'Custom 3' => 'カスタム3', + 'Custom 4' => 'カスタム4', + 'Custom' => 'カスタム', + 'Customer Enabled?' => '顧客が利用可能?', + 'Customer ID is required.' => '顧客 ID が必要です。', + 'Customer Note' => '顧客ノート', + 'Customer Notices' => '顧客の通知', + 'Customer data' => '顧客データ', + 'Customer' => '顧客', + 'Damaged' => '破損', + 'Data shown might be outdated.' => '表示データは古い可能性があります。', + 'Date Authorized' => 'オーソリ日', + 'Date Created' => '作成日', + 'Date First Paid' => '初回支払い日', + 'Date Ordered' => '注文日', + 'Date Paid' => '支払日', + 'Date Updated' => '更新日', + 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'カタログ価格ルールが有効になる日付。開始日を指定しない場合は空白のままにします。', + 'Date from which the discount will be active. Leave blank for unlimited start date' => 'ディスカウントが有効になる日付。開始日を指定しない場合は空白のままにします。', + 'Date from which the sale will be active. Leave blank for unlimited start date' => 'セールが有効になる日付。開始日を指定しない場合は空白のままにします。', + 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'カタログ価格ルールが終了する日付。終了日を指定しない場合は空白のままにします。', + 'Date when the discount will be finished. Leave blank for unlimited end date' => 'ディスカウントが終了する日付。終了日を指定しない場合は空白のままにします。', + 'Date when the sale will be finished. Leave blank for unlimited end date' => 'セールが終了する日付。終了日を指定しない場合は空白のままにします。', + 'Date' => '日時', + 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'デフォルト - ディスカウントが注文価格より上回る場合は価格を負にすることができます。', + 'Default Category' => 'デフォルトのカテゴリ', + 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'デフォルトの Commerce コントロールパネルビュー。ユーザーに権限がない場合、アクセスできる場所にフォールバックします。', + 'Default Order PDF' => 'デフォルトの注文PDF', + 'Default Per Item Rate' => 'デフォルトのアイテムごとの料金', + 'Default Percentage Rate' => 'デフォルトパーセント率', + 'Default Status?' => 'デフォルトのステータス?', + 'Default View' => 'デフォルトのビュー', + 'Default Weight Rate' => 'デフォルトの重量ごとの料金', + 'Default Zone' => 'デフォルトの地域', + 'Default status?' => 'デフォルトのステータス?', + 'Default to this tax zone when no billing address is set' => '請求先住所が設定されていない場合は、この税対象地域がデフォルトになります', + 'Default to this tax zone when no shipping address is set' => '配送先住所が設定されていない場合は、この税対象地域がデフォルトになります', + 'Default variant updated.' => 'デフォルトバリアントが更新されました。', + 'Default' => 'デフォルト', + 'Default?' => 'デフォルト?', + 'Delete catalog pricing rules' => 'カタログ価格ルールを削除', + 'Delete discounts' => 'ディスカウントを削除', + 'Delete orders' => '注文を削除', + 'Delete sales' => 'セールを削除', + 'Delete' => '削除', + 'Deleting the {location} location.' => '{location}の場所を削除しています。', + 'Describe this rule.' => 'このルールについて説明してください。', + 'Describe this shipping zone.' => 'この配送地域について説明してください。', + 'Describe this tax zone.' => 'この税対象地域について説明してください。', + 'Description' => '説明', + 'Destination Inventory Location' => '宛先の在庫場所', + 'Destination' => '宛先', + 'Details' => '詳細', + 'Dimension Unit' => '寸法の単位', + 'Dimensions' => '寸法', + 'Disabled' => '無効', + 'Disallow' => '許可しない', + 'Discount all line items' => 'すべてのラインアイテムをディスカウント', + 'Discount description.' => 'ディスカウントの説明。', + 'Discount is not allowed for the order' => '注文のディスカウントは許可されていません', + 'Discount is out of date.' => 'ディスカウントは期限切れです。', + 'Discount saved.' => 'ディスカウントは保存されました。', + 'Discount the matching items only' => '一致アイテムのみをディスカウント', + 'Discount use has reached its limit.' => 'ディスカウントの使用回数が限界に達しました。', + 'Discount' => 'ディスカウント', + 'Discounted Item Subtotal' => 'ディスカウント済みアイテムの小計', + 'Discounted Items' => 'ディスカウントされたアイテム', + 'Discounts deleted.' => 'ディスカウントを削除しました。', + 'Discounts reordered.' => 'ディスカウントを並び替えました。', + 'Discounts updated.' => 'ディスカウントが更新されました。', + 'Discounts' => 'ディスカウント', + 'Disqualify with valid business tax ID?' => '有効な事業税 ID で不適格にしますか?', + 'Do not apply subsequent matching sales beyond applying this sale.' => 'このセールを適用した場合、その後のセールを適用しない。', + 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => '注文の住所が選択された有効な事業税 ID のものである場合は、この税率を適用しないでください。', + 'Do not attach a PDF to this email' => 'このメールに PDF を添付しないでください', + 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'エラーがある場合、注文(番号: {orderNumber})の再計算を実行しないでください。', + 'Donation can not be zero.' => '寄付はゼロにできません。', + 'Donation needs to be an amount.' => '寄付は金額です。', + 'Donation settings saved.' => '寄付の設定が保存されました。', + 'Donation' => '寄付', + 'Donations' => '寄付', + 'Done' => '完了', + 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'このディスカウントが適用された場合、その後のディスカウントを注文に適用しない。', + 'Download PDF' => 'PDFをダウンロード', + 'Download PDF…' => 'PDF をダウンロード...', + 'Download Type' => 'ダウンロードタイプ', + 'Download' => 'ダウンロード', + 'Draft' => '下書き', + 'Dummy gateway payment failed.' => 'ダミーゲートウェアの支払いに失敗しました。', + 'Duplicate options exist' => '重複したオプションがあります', + 'Duration' => '期間', + 'EU VAT ID' => 'EU VAT ID', + 'Edit address' => '住所を編集する', + 'Edit adjustments' => '調整を編集', + 'Edit catalog pricing rules' => 'カタログ価格ルールを編集', + 'Edit discounts' => 'ディスカウントを編集', + 'Edit options' => 'オプションを編集する', + 'Edit orders' => '注文を編集', + 'Edit sales' => 'セールを編集', + 'Edit' => '編集', + 'Effect' => '効果', + 'Either (Default) - The relationship field is on the purchasable or the category' => 'いずれか(デフォルト)- 関連フィールドはパーチャサブルまたはカテゴリにあります', + 'Either way' => 'どちらでもいい', + 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'メール「{email}」に対し、メール PDF の生成エラーが発生しました。注文: “{order}”。PDF テンプレートエラー: 「{message}」{file}:{line}', + 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'メール「{email}」のメール PDF のテンプレートは「{templatePath}」に存在しません。注文:「{order}」。', + 'Email Subject' => 'メールの件名', + 'Email error. No email address found for order. Order: “{order}”' => 'メールエラー。注文のメールアドレスがありません。注文: “{order}”', + 'Email is not enabled.' => 'メールが無効です。', + 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'プレーンテキスト形式のメールテンプレートは「{templatePath}」に存在しません。メール「{email}」の「{templateParsedPath}」に解析されています。注文: 「{order}」。', + 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'メール「{email}」に対し、プレーンテキスト形式メールのテンプレートの解析エラーが発生しました。注文: “{order}”。テンプレートエラー: “{message}” {file}:{line}', + 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '「テンプレートパス」のメール「{email}」に対し、プレーンテキスト形式メールのテンプレートパスの解析エラーが発生しました。注文:「{order}」。テンプレートエラー:「{message}」{file}:{line}', + 'Email required to make payments on a completed order.' => '完了した注文に対する支払いに必要なメール。', + 'Email saved.' => 'メールが保存されました。', + 'Email sent' => 'メールが送信されました', + 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'メールテンプレートは「{templatePath}」に存在しません。メール「{email}」の「{templateParsedPath}」に解析されています。注文: 「{order}」。', + 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '「To:」 のカスタムメール 「{email}」に対し、メールテンプレートの解析エラーが発生しました。注文: 「{order}」。テンプレートエラー: 「{message}」{file}:{line}', + 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '「BCC:」 のメール 「{email}」に対し、メールテンプレートの解析エラーが発生しました。注文: 「{order}」。テンプレートエラー: 「{message}」{file}:{line}', + 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '「CC:」 のメール 「{email}」に対し、メールテンプレートの解析エラーが発生しました。注文: 「{order}」。テンプレートエラー: 「{message}」{file}:{line}', + 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '「ReplyTo:」 のメール 「{email}」に対し、メールテンプレートの解析エラーが発生しました。注文: 「{order}」。テンプレートエラー: 「{message}」{file}:{line}', + 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '「Subject:」 のメール 「{email}」に対し、メールテンプレートの解析エラーが発生しました。注文: 「{order}」。テンプレートエラー: 「{message}」{file}:{line}', + 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'メール「{email}」に対し、メールテンプレートの解析エラーが発生しました。注文: “{order}”。テンプレートエラー: “{message}” {file}:{line}', + 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '“Template Path” のメール “{email}” に対し、メールテンプレートのパスの解析エラーが発生しました。注文: “{order}”。テンプレートエラー: “{message}” {file}:{line}', + 'Email unavailable.' => 'メールを利用できません。', + 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => '注文 “{order}” のメール “{email}” を送信できませんでした。エラー: {error} {file}:{line}', + 'Email “{email}” for order {order} was cancelled.' => '注文「{order}」のメール「{email}」はキャンセルされました。', + 'Email' => 'メール', + 'Emails' => 'メール', + 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'コストを注文に追加する代わりに、税率が課税対象価格に組み込まれるかどうかを有効にします。', + 'Enable structure for products of this type' => 'このタイプの商品の構造を有効にする', + 'Enable this discount' => 'このディスカウントを有効にする', + 'Enable this rule' => 'このルールを有効にする', + 'Enable this sale' => 'このセールを有効にする', + 'Enable this shipping method on the front end' => 'この配送方法をフロントエンドで有効にする', + 'Enable this shipping rule' => 'この配送ルールを有効にする', + 'Enable this tax rate' => 'この税率を有効にする', + 'Enabled for customers to select during checkout?' => '支払い時に顧客が選択できるようにしますか?', + 'Enabled for customers to select?' => '顧客が選択できるようにしますか?', + 'Enabled' => '有効', + 'Enabled?' => '有効?', + 'End Date' => '終了日', + 'Enter SKU' => 'SKUを入力してください', + 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'コントロールパネルで使用される、この税率の分かりやすい名前を入力してください。', + 'Enter a percentage like {ex1} or {ex2}.' => '{ex1} や {ex2} などのパーセンテージを入力してください。', + 'Enter coupon code' => 'クーポンコードを入力してください', + 'Enter reference' => '参照を入力してください', + 'Error refunding transaction: {transactionHash}' => '取引の払い戻し中にエラーが発生しました: {transactionHash}', + 'Every new store must be assigned to at least one site.' => 'すべての新規ストアに少なくとも1つのサイトを割り当てる必要があります。', + 'Everywhere' => 'すべての地域', + 'Example' => '実例', + 'Exclude this discount for products that are already on promotion' => 'すでに販売促進が適用された商品はディスカウント対象から除外します', + 'Expired Link' => '期限切れのリンク', + 'Expired' => '期限切れ', + 'Expiry Date' => '有効期限の日付', + 'Expiry date' => '有効期限', + 'Expiry' => '期日', + 'Failed to receive transfer: {error}' => '移動の受け入れに失敗しました: {error}', + 'Failed to send email. Please try again.' => 'メールの送信に失敗しました。もう一度お試しください。', + 'Failed to start' => '開始できませんでした', + 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => '{num, plural, =1{注文ステータス} other{注文ステータス}}の更新に失敗しました。', + 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => '{num, plural, =1{注文} other{注文}}の注文ステータスの更新に失敗しました。', + 'Feet (ft)' => 'フィート(ft)', + 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'このルールが適用される注文の抽出条件。条件をスキップする場合は0に設定します。', + 'First Name' => '名', + 'Flat Amount Off Order' => '定額を差し引いた注文', + 'Flat Order Discount Amount Off' => '注文を定額でディスカウントする', + 'Free Order Payment Strategy' => '無料注文の支払いの方法', + 'Free Shipping' => '無料配送', + 'Free orders are processed by the payment gateway' => '無料の注文はペイメントゲートウェイによって処理されます', + 'Free orders complete immediately' => '無料の注文を即時に完了する', + 'Free shipping can only be for whole order or matching items, not both.' => '配送無料は、注文全体あるいは一致するアイテムのどちらか一方にのみ適用されます。', + 'From Name' => '送信者名', + 'Fulfill' => '履行', + 'Fulfilled' => '履行済み', + 'Fulfillment' => 'フルフィルメント', + 'Full Name' => '氏名', + 'Gateway Code' => 'ゲートウェイコード', + 'Gateway Message' => 'ゲートウェイメッセージ', + 'Gateway Reference' => 'ゲートウェイ参照', + 'Gateway Response' => 'ゲートウェイレスポンス', + 'Gateway doesn’t support authorize' => 'ゲートウェイは認証をサポートしていません', + 'Gateway doesn’t support partial refunds.' => 'ゲートウェイは一部払い戻しをサポートしていません。', + 'Gateway doesn’t support purchase' => 'ゲートウェイは購入をサポートしていません', + 'Gateway doesn’t support refunds.' => 'ゲートウェイは払い戻しをサポートしていません。', + 'Gateway saved.' => 'ゲートウェイは保存されました。', + 'Gateway' => 'ゲートウェイ', + 'Gateways reordered.' => 'ゲートウェイを並び替えました。', + 'Gateways' => 'ゲートウェイ', + 'General Settings' => '一般設定', + 'General' => '一般', + 'Generate' => '生成', + 'Generated Coupon Format' => '生成されたクーポンフォーマット', + 'Grams (g)' => 'グラム(g)', + 'Groups for which this sale will be applicable to.' => 'このセールが適用されるグループ。', + 'HTML Email Template Path' => 'HTMLメールのテンプレートのパス', + 'Handle' => 'ハンドル', + 'Harmonized System Code' => '関税分類システムコード', + 'Has Admin Notices' => '管理者通知あり', + 'Has Emails?' => 'メールがある?', + 'Has Free Shipping' => '無料配送あり', + 'Has Orders' => '注文あり', + 'Has Purchasable' => 'パーチャサブルあり', + 'Has Variants?' => 'バリアントがある?', + 'Height ({unit})' => '高さ({unit})', + 'Height' => '高さ', + 'Hide snapshot' => 'スナップショットを隠す', + 'History' => '履歴', + 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'PDFダウンロードリンクが有効期限切れとなるまでの期間(秒単位)。デフォルトは 86400 秒(24 時間)。', + 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => '個別のメールアドレスがこのディスカウントを利用できる回数。これは、ゲストまたはユーザーに関係なく、以前のすべての注文に適用されます。ゲストまたはユーザーが無制限に利用するにはゼロに設定します。', + 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => '1 人のユーザーがこのディスカウントを使用できる回数。これがゼロ以外に設定されている場合、サインインしたユーザーのみがディスカウントを利用できます。', + 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'このディスカウントをゲストまたはサインインしたユーザーが合計で使用できる回数。無制限に使用するにはゼロを設定します。', + 'How products should be labeled within the control panel.' => 'コントロールパネル内での商品の表示方法。', + 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'マッチングアイテムを設定するための、パーチャサブルとカテゴリの関連付け方法。[リレーション 専門用語]({link}) をご覧ください。', + 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'この商品が注文のラインアイテムとしてどのように記述されるかを指定します。 {ex1}や{ex2}などのプロパティを出力するタグを含めることができます', + 'How this shipping method will be referred to in templates and forms.' => 'この配送方法をテンプレートとフォーム上で参照する方法。', + 'How variants should be labeled within the control panel.' => 'コントロールパネル内でのバリアントの表示方法。', + 'How you’ll refer to this PDF in the templates.' => 'テンプレートでこの PDF を参照する方法。', + 'How you’ll refer to this product type in the templates.' => 'この商品タイプをテンプレート上で参照する方法。', + 'How you’ll refer to this shipping category in the templates.' => 'この配送カテゴリをテンプレート上で参照する方法。', + 'How you’ll refer to this status in the templates.' => 'このステータスをテンプレート上で参照する方法。', + 'How you’ll refer to this subscription plan in the templates.' => 'テンプレート上で使う定期支払いプランの名前。', + 'How you’ll refer to this tax category in the templates.' => 'この税カテゴリをテンプレート上で参照する方法。', + 'ID' => 'ID', + 'IP Address' => 'IPアドレス', + 'If disabled, this PDF will not be available or sent with emails.' => '無効にした場合、このPDFは利用不可になり、メールで送信されません。', + 'If disabled, this email will not send.' => '無効な場合、このメールは送信されません。', + 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => '有効に設定されていて、この税率が注文と一致しない場合、税額はカートの対象価格から削除されます。', + 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'オーソリのみに設定した場合、資金がアカウントに送金される前に、支払いを手動でキャプチャする必要があります。ゲートウェイは選択したオプションをサポートする必要があります。', + 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => '「ディスカウントされたアイテム価格の割引」にパーセント率を選択した場合、「アイテムごとの金額」とこれが適用される前のすべてのディスカウントが含まれます。', + 'Ignore Promotions?' => 'プロモーションを無視しますか?', + 'Ignore previous matching sales if this sale matches.' => 'このセールがマッチする場合は、既存のマッチするセールを無視する。', + 'Ignore promotional prices when this discount is applied to matching line items' => 'このディスカウントがマッチングアイテムに適用される場合は販売促進価格を無視する', + 'Inactive Carts' => '非アクティブなカート', + 'Inches (in)' => 'インチ(in)', + 'Include built-in line item tax.' => '組み込まれているラインアイテムの税金を含む。', + 'Include in price?' => '価格に含めますか?', + 'Include line item discounts.' => 'ラインアイテムディスカウントを含む。', + 'Include line item shipping costs.' => 'ラインアイテムの配送料を含む。', + 'Include separate line item tax.' => '個別のラインアイテムの税金を含む。', + 'Included in price?' => '価格に含めますか?', + 'Included' => '込み', + 'Incoming transfer from Transfer ID: ' => '移動IDから入庫予定: ', + 'Incoming' => '入荷', + 'Info' => '情報', + 'Information linked?' => '情報はリンク済み?', + 'Information' => '詳細', + 'Invalid JSON' => 'JSONが無効です', + 'Invalid Order ID' => '無効な注文 ID', + 'Invalid VAT ID.' => 'VAT IDが無効です。', + 'Invalid condition syntax' => '無効な条件構文', + 'Invalid email.' => 'メールアドレスが無効です。', + 'Invalid formula syntax' => '無効な式構文', + 'Invalid gateway: {value}' => '無効なゲートウェイ: {value}', + 'Invalid inventory movements.' => '在庫の移動が無効です。', + 'Invalid order condition syntax.' => '注文条件の構文が無効です。', + 'Invalid payment or order. Please review.' => '無効な支払いまたは注文です。確認してください。', + 'Invalid payment source ID: {value}' => '無効な支払い元 ID: {value}', + 'Invalid store.' => 'ストアが無効です。', + 'Invalid user.' => '無効なユーザーです。', + 'Inventory Item' => '在庫アイテム', + 'Inventory Location' => '在庫場所', + 'Inventory Locations' => '在庫場所', + 'Inventory Tracked' => '追跡済み在庫', + 'Inventory Transfers' => '在庫移動', + 'Inventory could not be set.' => '在庫を設定できませんでした。', + 'Inventory location has committed stock, the order(s) must first be fulfilled.' => '在庫場所にコミット済みの在庫があります。最初に注文を履行する必要があります。', + 'Inventory location has incoming stock, the transfer(s) must first be completed.' => '在庫場所に入庫予定の在庫があります。最初に移動を完了する必要があります。', + 'Inventory location is already deactivated.' => '在庫場所はすでに無効化されています。', + 'Inventory location saved.' => '在庫場所が保存されました。', + 'Inventory locations not saved.' => '在庫場所は保存されていません。', + 'Inventory movement could not be saved.' => '在庫の移動を保存できませんでした。', + 'Inventory movement saved.' => '在庫の移動が保存されました。', + 'Inventory updated.' => '在庫が更新されました。', + 'Inventory was not updated.' => '在庫が更新されませんでした。', + 'Inventory' => '在庫', + 'Invoice amount' => '請求金額', + 'Invoice date' => '請求日', + 'Is Promotable' => '販売促進可能', + 'Is Promotional Price?' => '販売促進価格ですか?', + 'Is Shippable' => '出荷可能', + 'Is Taxable' => '課税対象', + 'Item Rates' => 'アイテムレート', + 'Item Subtotal' => 'アイテム小計', + 'Item Total' => 'アイテム合計', + 'Item' => 'アイテム', + 'Items' => 'アイテム', + 'Kilograms (kg)' => 'キログラム(kg)', + 'Label' => 'ラベル', + 'Landscape' => '横向き', + 'Language' => '言語', + 'Last Name' => '姓', + 'Last Updated' => '最終更新日', + 'Leave a category rate override blank to use the rate from above.' => 'カテゴリ料金の上書きを空白にすると、上記からの料金を使用します。', + 'Leave blank for unlimited uses.' => '無制限に使用するには空にしてください。', + 'Leave blank if products don’t have URLs' => '商品に URL がない場合は空にしてください。', + 'Leave gateway subscription as-is' => 'ゲートウェイのサブスクリプションを現状のままにします', + 'Length ({unit})' => '長さ({unit})', + 'Length' => '長さ', + 'Let each product choose which sites it should be saved to' => '各商品で保存先のサイトを選択する', + 'Limit which orders this discount applies to based on its line items.' => 'ラインアイテムに基づいてどの注文にこのディスカウントが適用されるかを制限します。', + 'Limit which purchasables this sale applies to.' => 'どのパーチャサブルにこのセールが適用されるかを制限します。', + 'Limit' => 'リミット', + 'Line Item Statuses' => 'ラインアイテムステータス', + 'Line Item' => 'ラインアイテム', + 'Line Items' => 'ラインアイテム', + 'Line item price (minus discounts)' => 'ラインアイテム価格(ディスカウントを差し引いた価格)', + 'Line item shipping cost' => 'ラインアイテムの配送料', + 'Line item statuses reordered.' => 'ラインアイテムステータスを並び替えました。', + 'Link Duration' => 'リンクの有効期限', + 'Link Sent' => 'リンクを送信しました', + 'Link to a product' => '商品にリンク', + 'Link to a variant' => 'バリアントにリンク', + 'Link' => 'リンク', + 'Live' => 'ライブ', + 'Location' => '場所', + 'Locations that should be available for previewing products in this product type.' => 'この商品タイプでプレビュー可能にすべき表示場所。', + 'MM' => 'MM', + 'Make a payment' => '支払いを行う', + 'Make this the primary store' => 'これをプライマリストアにする', + 'Manage Inventory' => '在庫を管理', + 'Manage donation settings' => '寄付設定を管理', + 'Manage general store settings' => '一般ストア設定を管理', + 'Manage inventory locations' => '在庫場所を管理', + 'Manage inventory stock levels' => '在庫レベルを管理', + 'Manage inventory transfers' => '在庫移動を管理', + 'Manage orders' => '注文を管理', + 'Manage payment currencies' => '支払い通貨を管理', + 'Manage promotions' => '販売促進を管理', + 'Manage shipping' => '配送を管理', + 'Manage store settings' => 'ストア設定を管理', + 'Manage subscription plans' => '定期支払いプランを管理', + 'Manage subscription' => '定期支払いを管理する', + 'Manage subscriptions' => '定期支払いを管理', + 'Manage taxes' => '税を管理', + 'Manage' => '管理', + 'Mark as Pending' => '保留中としてマーク', + 'Mark as completed' => '完了としてマーク', + 'Match Billing Address' => '請求先住所に一致', + 'Match Customer' => '顧客の一致', + 'Match Order' => '注文に一致', + 'Match Orders' => '注文に一致', + 'Match Product' => '商品を一致', + 'Match Purchasable' => 'パーチャサブルをマッチ', + 'Match Shipping Address' => '配送先住所に一致', + 'Match Variant' => 'バリアントを一致', + 'Matching Items' => 'マッチングアイテム', + 'Max Qty' => '最大数量', + 'Max Uses' => '最大使用回数', + 'Max Variants' => '最大バリアント', + 'Max quantity must greater than min.' => '最大数量は最小数量よりも大きくする必要があります。', + 'Maximum Purchase Quantity' => '最大購入数量', + 'Maximum Total Shipping Cost' => '最大合計送料', + 'Maximum allowed quantity' => '最大許容数量', + 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'このディスカウントの適用に可能となる、マッチングアイテムの最大数。ここでゼロの値を指定すると、この条件はスキップされます。', + 'Maximum order quantity for this item is {num}.' => 'このアイテムの最大注文数量は {num} です。', + 'Message' => 'メッセージ', + 'Meters (m)' => 'メートル(m)', + 'Millimeters (mm)' => 'ミリメートル(mm)', + 'Min Qty' => '最小数量', + 'Min quantity must be less than max.' => '最小数量は最大数量よりも小さくする必要があります。', + 'Minimum Purchase Quantity' => '最小購入数量', + 'Minimum Total Price Strategy' => '最低合計価格の算出方法', + 'Minimum Total Shipping Cost' => '最小合計送料', + 'Minimum allowed quantity' => '最小許容数量', + 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'このディスカウントの適用に必要となる、マッチングアイテムの最小数。', + 'Minimum order quantity for this item is {num}.' => 'このアイテムの最低注文数量は {num} です。', + 'Missing Gateway' => '見つからないゲートウェイ', + 'Missing a default inventory location.' => 'デフォルトの在庫場所がありません。', + 'Move Inventory' => '在庫を移動', + 'Move To' => '移動先', + 'Move {qty} from {fromType} to {toType}' => '{fromType}から{toType}へ{qty}件を移動', + 'Move' => '移動', + 'Movement from deactivated inventory location' => '無効化された在庫場所からの移動', + 'Movement' => '移動', + 'Must have at least one variant.' => '少なくとも 1 つのバリアントが必要です。', + 'Name Field' => '名前フィールド', + 'Name' => '名前', + 'New Customer' => '新規顧客', + 'New Customers' => '新規顧客', + 'New Order' => '新規注文', + 'New PDF' => '新規PDF', + 'New address' => '新規住所', + 'New catalog pricing rule' => '新規カタログ価格ルール', + 'New currency' => '新規通貨', + 'New discount' => '新規ディスカウント', + 'New email' => '新規メール', + 'New gateway' => '新規ゲートウェイ', + 'New line item status' => '新規ラインアイテムステータス', + 'New line items get this status by default when the order is completed' => '注文が完了すると、新しいラインアイテムにはデフォルトでこのステータスが適用されます', + 'New location' => '新しい場所', + 'New order status' => '新規注文ステータス', + 'New orders get this status by default' => '新しい注文はデフォルトでこのステータスが適用されます。', + 'New product type' => '新規商品タイプ', + 'New product' => '新規商品', + 'New product, choose a type' => '新しい商品、タイプを選択してください', + 'New products default to the first tax category available to them. If none are available, this category will be used.' => '新しい商品のデフォルトの税カテゴリになります。利用可能なカテゴリがない場合はこのカテゴリが使用されます。', + 'New sale' => '新規セール', + 'New shipping category' => '新規配送カテゴリ', + 'New shipping method' => '新規配送方法', + 'New shipping rule' => '新規配送ルール', + 'New shipping zone' => '新規配送地域', + 'New subscription plan' => '新規定期支払いプラン', + 'New tax category' => '新規税カテゴリ', + 'New tax rate' => '新規税率', + 'New tax zone' => '新規税対象地域', + 'New transfer' => '新しい移動', + 'New {productType} product' => '新規「{productType}」商品', + 'New' => '新規', + 'Next payment' => '次の支払い', + 'No Address' => '住所がありません', + 'No PDFs exist yet.' => 'PDFがまだありません。', + 'No access given to any specific store management features.' => '特定のストア管理機能へのアクセスがありません。', + 'No additional payment currencies exist yet.' => '追加の支払い通貨がまだありません。', + 'No address' => '住所がありません', + 'No billing address' => '請求先住所がありません', + 'No catalog pricing rule exists with the ID “{id}”' => 'ID「{id}」のカタログ価格ルールは存在しません', + 'No catalog pricing rules exist yet.' => 'カタログ価格ルールはまだ存在しません。', + 'No currency exists with the ID “{id}”' => 'ID「{id}」の通貨は存在しません', + 'No customer email address exists on this cart.' => 'このカートには顧客のメールアドレスは存在しません。', + 'No description' => '説明がありません', + 'No discount exists with the ID “{id}”' => 'ID「{id}」のディスカウントは存在しません', + 'No discounts exist yet.' => 'ディスカウントがまだありません。', + 'No donation amount supplied.' => '寄付の金額が指定されていません。', + 'No emails exist yet.' => 'メールがまだありません。', + 'No inventory changes made.' => '在庫に変更はありません。', + 'No inventory found.' => '在庫が見つかりません。', + 'No inventory movements made.' => '在庫の移動はありません。', + 'No inventory transactions for this location.' => 'この場所の在庫トランザクションはありません。', + 'No new customer selected.' => '新しい顧客が選択されていません。', + 'No order history exists with the ID “{id}”' => 'ID「{id}」の注文履歴は存在しません', + 'No order status history items will exist until the cart becomes an order.' => '注文ステータス履歴はカートが注文に変わった後に有効になります。', + 'No payment source exists with the ID “{id}”' => 'ID「{id}」の支払い元は存在しません', + 'No private Note.' => 'プライベートノートがありません。', + 'No product available.' => '利用できる商品はありません。', + 'No product types exist yet.' => '商品タイプがまだありません。', + 'No purchasable available.' => '利用できるパーチャサブルはありません。', + 'No sale exists with the ID “{id}”' => 'ID「{id}」のセールは存在しません', + 'No sales exist yet.' => 'セールがまだありません。', + 'No shipping address' => '配送先住所がありません', + 'No shipping category exists with the ID “{id}”' => 'ID「{id}」の配送カテゴリは存在しません', + 'No shipping method exists with the ID “{id}”' => 'ID「{id}」の配送方法は存在しません', + 'No shipping rule exists with the ID “{id}”' => 'ID「{id}」の配送ルールは存在しません', + 'No shipping rules exist yet.' => '配送ルールがまだ存在しません。', + 'No shipping zone exists with the ID “{id}”' => 'ID「{id}」の配送地域は存在しません', + 'No stats available.' => '統計情報はありません。', + 'No subscription plan exists with the ID “{id}”' => 'ID「{id}」の定期支払いプランは存在しません', + 'No subscription plans exist yet.' => 'まだ定期支払いプランがありません。', + 'No tax category exists with the ID “{id}”' => 'ID「{id}」の税カテゴリは存在しません', + 'No tax rate exists with the ID “{id}”' => 'ID「{id}」の税率は存在しません', + 'No tax zone exists with the ID “{id}”' => 'ID「{id}」の税対象地域は存在しません', + 'No transactions exist.' => '取引がありません。', + 'No user authenticated.' => '認証済みのユーザーがいません。', + 'No' => 'いいえ', + 'None on hand' => '在庫なし', + 'None' => 'なし', + 'Not a valid address type' => '有効な住所タイプではありません', + 'Not a valid credit card number.' => '有効なクレジットカード番号ではありません。', + 'Not all SKUs are unique.' => 'すべての SKU が一意とは限りません。', + 'Note' => 'ノート', + 'Notes' => 'ノート', + 'Number of Coupons' => 'クーポン数', + 'Number' => '番号', + 'Of the enabled sites above, which sites should products in this product type be saved to?' => '上記の有効なサイトのうち、この商品タイプの商品をどのサイトに保存しますか?', + 'On Hand' => '手持ち', + 'Only allow this gateway to be used for zero value orders?' => 'このゲートウェイを注文金額がゼロの場合にのみ使用できるようにしますか?', + 'Only match certain purchasables…' => '特定のパーチャサブルのみにマッチ…', + 'Only match purchasables related to…' => '次に関連するパーチャサブルのみにマッチ…', + 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => '次の注文ステータスの注文のみが含まれます。すべてのステータスを含めるには空白のままにします。', + 'Only save product to the site they were created in' => '作成したサイトにのみ商品を保存する', + 'Options' => 'オプション', + 'Order Condition Formula' => '注文の条件式', + 'Order Description Format' => '注文の表示フォーマット', + 'Order Details' => '注文の詳細', + 'Order Fields' => '注文フィールド', + 'Order PDF Download Link' => '注文PDFダウンロードリンク', + 'Order PDF Filename Format' => '注文PDFのファイル名フォーマット', + 'Order Reference Number Format' => '注文参照番号フォーマット', + 'Order Settings' => '注文設定', + 'Order Site' => '注文サイト', + 'Order Status description.' => '注文ステータスの説明。', + 'Order Status' => '注文ステータス', + 'Order Statuses' => '注文ステータス', + 'Order can not be empty.' => '注文を空にできません。', + 'Order count' => '注文数', + 'Order customer data removed.' => '注文の顧客データを削除しました。', + 'Order deleted.' => '注文は削除されました。', + 'Order fields saved.' => '注文フィールドは保存されました。', + 'Order not found.' => '注文が見つかりません。', + 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => '注文支払残高は {outstandingBalanceAsCurrency} です。これが最大請求額になります。', + 'Order recalculated.' => '注文が再計算されました。', + 'Order status saved.' => '注文ステータスは保存されました。', + 'Order statuses reordered.' => '注文ステータスを並び替えました。', + 'Order total shipping cost' => '注文合計の配送料', + 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => '注文合計の課税価格(ラインアイテム小計 + 合計ディスカウント + 合計配送料)', + 'Order' => '注文', + 'Orders (Legacy)' => '注文(レガシー)', + 'Orders deleted.' => '注文は削除されました。', + 'Orders not restored.' => '注文は復元されていません。', + 'Orders restored.' => '注文は復元されました。', + 'Orders' => '注文', + 'Organization Name' => '組織名', + 'Organization Tax ID' => '組織税 ID', + 'Origin and destination cannot be the same.' => '起点と宛先は同じにはできません。', + 'Origin' => '原因', + 'Original Price' => '元の価格', + 'Original price' => '元の価格', + 'Original promotional price' => '元の販売促進価格', + 'Other Languages' => 'その他の言語', + 'Other countries' => 'その他の国', + 'Outgoing transfer from Transfer ID: ' => '移動IDから出庫予定: ', + 'Overpaid' => '過払い', + 'Overrides previous?' => '既存を上書きする?', + 'PDF Attachment' => 'PDF添付', + 'PDF Template Path' => 'PDFテンプレートのパス', + 'PDF saved.' => 'PDF は保存されました。', + 'PDF' => 'PDF', + 'PDFs & Emails' => 'PDFとメール', + 'PDFs' => 'PDF', + 'Paid Amount' => '支払い総額', + 'Paid Status' => '支払いステータス', + 'Paid' => '決済完了', + 'Paper Orientation' => '用紙の向き', + 'Paper Size' => '用紙サイズ', + 'Partial payment not allowed.' => '一部支払はできません。', + 'Partial' => '一部', + 'Past year' => '去年', + 'Past {num} days' => '過去 {num} 日間', + 'Pay {amount} of {currency} on the order.' => '注文金額 {amount} {currency} を支払う。', + 'Pay' => '支払う', + 'Payment Amount' => '支払い金額', + 'Payment Currencies' => '支払い通貨', + 'Payment Gateway' => 'ペイメントゲートウェイ', + 'Payment Method' => '支払い方法', + 'Payment error: {message}' => '支払いエラー: {message}', + 'Payment method issue' => '支払い方法の問題', + 'Payment source created.' => '支払い元が作成されました。', + 'Payment source deleted.' => '支払い元は削除されました。', + 'Payments' => '支払い', + 'Pending' => '保留中', + 'Per Email Address Discount Limit' => 'メールアドレスごとのディスカウント限度', + 'Per Item Amount Off' => 'アイテムごとの料金割引', + 'Per Item Discount' => 'アイテムごとのディスカウント', + 'Per Item Percentage Off' => 'アイテムごとのパーセント率割引', + 'Per Item Rate' => 'アイテムごとの料金', + 'Per User Discount Limit' => 'ユーザーごとのディスカウント限度', + 'Percentage Rate' => 'パーセンテージ料金', + 'Phone (Alt)' => '電話 (予備)', + 'Phone' => '電話', + 'Pick a plan' => 'プランを選択', + 'Plain Text Email Template Path' => 'プレーンテキストメールのテンプレートのパス', + 'Plan' => 'プラン', + 'Plans reordered.' => 'プランを並び替えました。', + 'Portrait' => '縦向き', + 'Post Date' => '投稿日', + 'Postal Code Formula' => '郵便番号の式', + 'Pounds (lb)' => 'ポンド(lb)', + 'Preview' => 'プレビュー', + 'Previous Status' => '変更前のステータス', + 'Price' => '価格', + 'Prices' => '価格', + 'Pricing Rules' => '価格ルール', + 'Pricing jobs are currently running.' => '価格設定ジョブは現在実行中です。', + 'Pricing' => '価格設定', + 'Primary Billing Address' => '既定の請求先住所', + 'Primary Shipping Address' => '既定の配送先住所', + 'Primary payment source updated.' => '主な支払い元が更新されました。', + 'Primary' => 'プライマリ', + 'Private Note' => 'プライベートノート', + 'Product Fields' => '商品フィールド', + 'Product ID is required.' => '商品 ID が必要です。', + 'Product Template' => '商品のテンプレート', + 'Product Title Format' => '商品タイトルフォーマット', + 'Product Type' => '商品タイプ', + 'Product Types' => '商品タイプ', + 'Product URI Format' => '商品の URI フォーマット', + 'Product Variant' => '商品バリアント', + 'Product Variants' => '商品バリアント', + 'Product type saved.' => '商品タイプは保存されました。', + 'Product type settings' => '商品タイプ設定', + 'Product' => '商品', + 'Products and Variants deleted.' => '商品とバリアントは削除されました。', + 'Products not restored.' => '商品は復元されていません。', + 'Products restored.' => '商品は復元されました。', + 'Products' => '商品', + 'Promotable' => '販売促進可能', + 'Promotable?' => '販売促進可能?', + 'Promotional Amount' => '販売促進価格', + 'Promotional Price' => '販売促進価格', + 'Purchasable Categories' => 'パーチャサブルカテゴリ', + 'Purchasable ID and Sale ID are required.' => 'パーチャサブル ID とセール ID が必要です。', + 'Purchasable ID is required.' => 'パーチャサブル ID が必要です。', + 'Purchasable Type' => 'パーチャサブルタイプ', + 'Purchasable' => 'パーチャサブル', + 'Purchase (Authorize and Capture Immediately)' => '購入(即時承認とキャプチャ)', + 'Purchase Total' => '合計購入額', + 'Qty' => '数量', + 'Quality Control' => '品質コントロール', + 'Quantity' => '数量', + 'Rate' => '率', + 'Reassign {numOrders, plural, =1{order} other{orders}}' => '{numOrders, plural, =1{注文} other{注文}}を再割り当て', + 'Recalculate order' => '注文を再計算する', + 'Receive Inventory' => '在庫を受け入れ', + 'Receive Transfer' => '移動を受け入れ', + 'Receive' => '受け入れ', + 'Received' => '受け入れ済み', + 'Recent Orders' => '最近の注文', + 'Recipient' => '宛先', + 'Recover Cart' => 'カートを復元', + 'Reduce price' => '価格を下げる', + 'Reduce the price by a fixed amount' => '固定金額で価格を下げる', + 'Reduce the price by a percentage of the original price' => '元の価格のパーセンテージで値引く', + 'Reference' => '参照', + 'Refresh payment history' => '支払い履歴を更新する', + 'Refund note' => '払い戻しメモ', + 'Refund payment' => '払戻し', + 'Refund' => '返金', + 'Reject' => '拒否', + 'Rejected' => '拒否済み', + 'Relationship Type' => '関係付けタイプ', + 'Removable included tax rates are only allowed for the default tax zone.' => '削除可能な税込料金は、デフォルトの税対象地域にのみ使用できます。', + 'Remove address' => '注所を削除', + 'Remove all shipping costs from the order' => '注文のすべての送料を値引きする', + 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => '{numOrders, plural, =1{注文} other{注文}}から顧客の関連付けとメールアドレスを削除してください。以下から削除する追加の顧客データを任意で選択してください', + 'Remove customer data' => '顧客データを削除', + 'Remove from price?' => '価格から削除しますか?', + 'Remove shipping costs for matching items only' => 'マッチングアイテムの送料のみを値引きする', + 'Remove the included tax when a valid organization tax ID is present?' => '有効な組織税 ID が存在する場合、含まれている税金を削除しますか?', + 'Remove' => '削除', + 'Removed' => '削除済み', + 'Repeat Customers' => 'リピート顧客', + 'Reply To' => '返信先', + 'Require Billing Address At Checkout' => 'チェックアウトでは請求先住所が必要です', + 'Require Coupon Code' => 'クーポンコードが必要です', + 'Require Shipping Address At Checkout' => 'チェックアウトでは配送先住所が必要です', + 'Require Shipping Method Selection At Checkout' => 'チェックアウトでは配送方法の選択が必要です', + 'Require' => '必須', + 'Reserved' => '予約済み', + 'Reset usage' => '利用状況をリセットする', + 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'マッチングアイテムの合計購入額が最低額に達した注文のみにディスカウントを制限します。', + 'Revenue Options' => '収益オプション', + 'Revenue' => '収入', + 'Rule' => 'ルール', + 'Rules reordered.' => 'ルールを並び替えました。', + 'SKU' => 'SKU', + 'Safety' => '安全性', + 'Sale Price' => 'セール価格', + 'Sale description.' => 'セールの説明。', + 'Sale reordered.' => 'セールを並び替えました。', + 'Sale saved.' => 'セールが保存されました。', + 'Sale' => 'セール', + 'Sales deleted.' => 'セールを削除しました。', + 'Sales updated.' => 'セールは更新されました。', + 'Sales' => 'セール', + 'Save and continue editing' => '保存して編集を続く', + 'Save and return to all orders' => '保存してすべての注文に戻る', + 'Save and set rules' => '保存してルールを設定する', + 'Save as a new rule' => '新しいルールとして保存', + 'Save product to all sites enabled for this product type' => 'この商品タイプで有効なすべてのサイトに商品を保存する', + 'Save product to other sites in the same site group' => '同じサイトグループ内の他のサイトに商品を保存する', + 'Save product to other sites with the same language' => '同じ言語のサイトに商品を保存する', + 'Save' => '保存', + 'Search customer…' => '顧客を検索...', + 'Search inventory' => '在庫検索', + 'Search or enter customer email…' => '顧客のメールを検索、または入力してください...', + 'Search…' => '検索…', + 'See Orders' => '注文を表示', + 'Select a gateway' => 'ゲートウェイを選択してください', + 'Select a tax category.' => '税カテゴリを選択してください。', + 'Select a tax zone. If empty, this rate will match anywhere.' => '税対象地域を選択してください。空の場合、この税率はすべての地域に適用されます。', + 'Select address' => '住所を選択する', + 'Select an item' => 'アイテムを選択', + 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'パーチャサブルにカタログ価格ルールを適用する方法を選択します。', + 'Select how the sale will be applied to the purchasable(s).' => 'パーチャサブルにセールを適用する方法を選択します。', + 'Select product type' => '商品タイプを選択', + 'Select the emails that will be sent when transitioning to this status.' => 'このステータスに変化する時に送信されるメールを選択してください。', + 'Select what this rate should be applied to.' => 'この税率が適用されるべきものを選択してください。', + 'Send Email' => 'メールを送信する', + 'Send to custom recipient' => '任意の宛先に送信する', + 'Send to the customer' => '顧客に送信する', + 'Set Quantity' => '数量を設定', + 'Set default category' => 'デフォルトのカテゴリを設定', + 'Set default variant' => 'デフォルトバリアントを設定', + 'Set or Adjust' => '設定または調整', + 'Set price' => '価格を設定', + 'Set status' => 'ステータスの設定', + 'Set the price to a flat amount' => '定額に価格を設定する', + 'Set the price to a percentage of the original price' => '元の価格に対するパーセンテージで価格を設定する', + 'Set the sale price to a flat amount' => 'セール価格を定額に設定する', + 'Set the sale price to a percentage of the original price' => '元の価格に対するパーセンテージで価格を設定する', + 'Set to' => '設定する', + 'Settings saved.' => '設定が保存されました。', + 'Settings' => '設定', + 'Share cart…' => 'カートを共有…', + 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => '配送 - 注文価格が配送料を下回る場合、最低料金は配送料金です。', + 'Shipping Address Zone' => '配送先住所地域', + 'Shipping Address' => '配送先住所', + 'Shipping Business Name' => '配送先会社名', + 'Shipping Categories' => '配送カテゴリ', + 'Shipping Category Conditions' => '配送カテゴリの条件', + 'Shipping Category' => '配送カテゴリ', + 'Shipping First Name' => '配送先の名', + 'Shipping Full Name' => '配送先氏名', + 'Shipping Last Name' => '配送先の姓', + 'Shipping Method' => '配送方法', + 'Shipping Methods' => '配送方法', + 'Shipping Rule' => '配送ルール', + 'Shipping Zones' => '配送地域', + 'Shipping address required.' => '配送先住所が必要です。', + 'Shipping categories deleted.' => '配送カテゴリが削除されました。', + 'Shipping category saved.' => '配送カテゴリは保存されました。', + 'Shipping category updated.' => '配送カテゴリが更新されました。', + 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'パーセンテージ料金、アイテムごとの料金、および重量ごとの料金の適用前に、注文全体に追加される送料。この料金を無効にするには、0に設定します。この基本料金を含むルール全体は、カートにデジタル商品などの出荷不可アイテムのみが含まれる場合は一致せず、適用されません。', + 'Shipping method saved.' => '配送方法は保存されました。', + 'Shipping methods and rules deleted.' => '配送方法とルールが削除されました。', + 'Shipping methods updated.' => '配送方法が更新されました。', + 'Shipping rule saved.' => '配送ルールは保存されました。', + 'Shipping zone saved.' => '配送地域は保存されました。', + 'Shipping' => '配送', + 'Short Number' => '短縮番号', + 'Show Chart?' => 'チャートを表示?', + 'Show Order Count?' => '注文数を表示?', + 'Show all prices' => 'すべての価格を表示', + 'Show archived gateways' => 'アーカイブされたゲートウェイを表示', + 'Show order count line on chart.' => 'チャートに注文数の線を表示します。', + 'Show related sales' => '関連するセールを表示', + 'Show rule details' => 'ルールの詳細を表示', + 'Show the Dimensions and Weight fields for products of this type' => 'このタイプの商品に寸法と重量のフィールドを表示する', + 'Show the Title field for products' => '商品のタイトルフィールドを表示する', + 'Show the Title field for variants' => 'バリアントのタイトルフィールドを表示する', + 'Signed In' => 'サインイン済み', + 'Site Languages' => 'サイト言語', + 'Site store mapping saved.' => 'サイトストアマッピングが保存されました。', + 'Sites' => 'サイト', + 'Slug' => 'スラッグ', + 'Snapshot' => 'スナップショット', + 'Snapshots' => 'スナップショット', + 'Some orders restored.' => '一部の注文は復元されました。', + 'Some products restored.' => '一部の商品は復元されました。', + 'Some variants restored.' => 'バリアントの一部が復元されました。', + 'Something changed with the order before payment, please review your order and submit payment again.' => '支払い前に注文が変更されました。注文内容を確認してからもう一度支払いを行ってください。', + 'Sorry, no matching options.' => '申し訳ありません。一致するオプションはありません。', + 'Source - The purchasable relationship field is on the category' => 'ソース - パーチャサブルの関連フィールドはカテゴリにあります', + 'Source' => 'ソース', + 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'ディスカウントが特定の注文に適用される必要があるかどうかを判断する Twig 条件を指定します。(注文は `order` 変数で参照できます。)', + 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => '配送ルールが特定の注文に適用される必要があるかどうかを判断する Twig 条件を指定します。(注文は `order` 変数で参照できます。)', + 'Start Date' => '開始日', + 'State' => '州/都道府県', + 'Status Email Address' => 'ステータスメールアドレス', + 'Status Emails' => 'ステータスメール', + 'Status History' => 'ステータス履歴', + 'Status Updated.' => 'ステータスが更新されました。', + 'Status change message' => 'ステータス更新メッセージ', + 'Status' => 'ステータス', + 'Stock' => '在庫', + 'Stops Processing?' => '追加の設定を無効にする?', + 'Stops subsequent?' => '追加の設定を無効にする?', + 'Store Location' => 'ストアのロケーション', + 'Store Management' => 'ストア管理', + 'Store Markets' => 'ストア市場', + 'Store Rule' => 'ストアルール', + 'Store saved.' => 'ストアが保存されました。', + 'Store' => 'ストア', + 'Stores & Sites' => 'ストアとサイト', + 'Stores' => 'ストア', + 'Strategy to apply when an order is free or has a zero balance.' => '注文が無料、または合計金額がゼロの場合の支払い方法', + 'Strategy to apply when calculating the minimum order price.' => '最低の注文価格を算出する方法', + 'Subject' => '件名', + 'Subscribing user' => '定期支払い中のユーザー', + 'Subscription Fields' => '定期支払いフィールド', + 'Subscription Plans' => '定期支払いプラン', + 'Subscription Settings' => '定期支払い設定', + 'Subscription cancelled.' => '定期支払いをキャンセルしました。', + 'Subscription date' => '定期支払い作成日', + 'Subscription fields saved.' => '定期支払いフィールドは保存されました。', + 'Subscription for {user} to {plan} prevented by a plugin.' => '{user} の {plan} への定期支払いは、プラグインによって阻止されました。', + 'Subscription plan saved.' => '定期支払いプランは保存されました。', + 'Subscription plan' => '定期支払いプラン', + 'Subscription plans' => '定期支払いプラン', + 'Subscription reactivated.' => '定期支払いを再度有効にしました。', + 'Subscription reference' => '定期支払い参照番号', + 'Subscription started.' => '定期支払いを開始しました。', + 'Subscription switched.' => '定期支払いを切り替えました。', + 'Subscription to “{plan}”' => '「{plan}」の定期支払い', + 'Subscription' => '定期支払い', + 'Subscriptions on hold' => '保留中の定期支払い', + 'Subscriptions' => '定期支払い', + 'Suppress emails' => 'メールを表示しない', + 'Switch plan' => 'プランの切り替え', + 'Switch' => '切り替える', + 'System' => 'システム', + 'Table Columns' => 'テーブル列', + 'Target - The category relationship field is on the purchasable' => 'ターゲット - カテゴリ関連フィールドはパーチャサブルにあります', + 'Tax & Shipping' => '税と配送', + 'Tax (inc)' => '税(込み)', + 'Tax Categories' => '税カテゴリ', + 'Tax Category' => '税カテゴリ', + 'Tax Rates' => '税率', + 'Tax Zone' => '税対象地域', + 'Tax Zones' => '税対象地域', + 'Tax categories deleted.' => '税カテゴリが削除されました。', + 'Tax category saved.' => '税カテゴリは保存されました。', + 'Tax category updated.' => '税カテゴリが更新されました。', + 'Tax rate saved.' => '税率は保存されました。', + 'Tax rates updated.' => '税率が更新されました。', + 'Tax zone saved.' => '税対象地域は保存されました。', + 'Tax' => '税', + 'Taxable Subject' => '課税対象', + 'Template Path' => 'テンプレートのパス', + 'That handle is already in use' => 'このハンドルはすでに使用されています', + 'That handle is already in use.' => 'このハンドルはすでに使用されています。', + 'The PDF to attach to this email.' => 'このメールに添付するPDF。', + 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => '定期支払いの請求先情報を更新、および 3DS 認証を処理するページへの URL です。', + 'The address provided is outside the store’s market.' => '提供された住所はストア市場外です。', + 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => '注文全体に適用される割引額。この金額は、割引を使い果たすまで、最高価格から最低価格の順にラインアイテム間で適用されます。', + 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => '基本ディスカウントはカート内の商品の価格がゼロになるか、割引を使い果たすまで適用することができます。注文の金額をマイナスにすることはできません。', + 'The cart recovery link is invalid. Please request a new one.' => 'カート復元リンクが無効です。新しいリンクをリクエストしてください。', + 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => '金額をこの通貨に換算するときに使用される換算レート。例えば、アイテムの価格が{amount1}の場合、換算レートが{rate}であれば代替通貨では{amount2}になります。', + 'The countries that orders are allowed to be placed from.' => '注文が許可されていない国々', + 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'クーポン「{code}」は利用回数上限({limit})を超えています。', + 'The customer for this order has been deleted.' => 'この注文の顧客情報は削除されました。', + 'The default shipping category is automatically available to all product types.' => 'デフォルトの配送カテゴリは、すべての商品タイプで自動的に利用可能です。', + 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'ディスカウント「{name}」は累計利用回数上限({limit})を超えています。', + 'The download link has expired. Please request a new one.' => 'ダウンロードリンクの有効期限が切れました。新しいリンクをリクエストしてください。', + 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => '注文ステータスメールの送信元のメールアドレス。Craftの一般設定で定義されたシステムメールアドレスを使用するには、空白のままにします。', + 'The entry that contains the description for this subscription’s plan.' => 'この定期支払いプランの説明を記載しているエントリ。', + 'The flat value which should discount each item. i.e “3” for $3 off each item.' => '各アイテムを一律にディスカウントする値です。各アイテムから $3 割引であれば「3」とします。', + 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => '{example}など、新しいクーポンの生成で使用されるフォーマット。すべての「#」番号記号はランダムな文字に置き換えられます。', + 'The from and to inventory locations must be different.' => '在庫元と在庫先は異なる必要があります。', + 'The inventory locations this store uses.' => 'このストアが使用する在庫場所。', + 'The item is not enabled for sale.' => 'アイテムのセールは有効化されていません。', + 'The language the order was made in.' => '注文が行われた言語。', + 'The language to be used when this email is rendered.' => 'このメールをレンダリングする際に使用する言語。', + 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'この商品タイプが持つことのできる最大レベル数。いくつでも構わない場合は空白のままにしておいてください。', + 'The maximum the customer should spend on shipping. Set to zero to disable.' => '顧客が支払う送料の最大額。無効にするにはゼロに設定します', + 'The minimum the customer should spend on shipping. Set to zero to disable.' => '顧客が支払う送料の最小額。無効にするにはゼロに設定します', + 'The order is not valid.' => '注文は有効ではありません。', + 'The payment gateway that will be used for the subscription plan.' => '定期支払いプランに使用されるペイメントゲートウェイ。', + 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => '各アイテムからディスカウントされるパーセンタイル値で、{ex2} 割引の場合は{ex1}とします。パーセント率は小数点 2 桁に丸められます。', + 'The previously-selected shipping method is no longer available.' => '以前に選択した配送方法はご利用いただけません。', + 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => '{description}の価格が {originalSalePriceAsCurrency} から {newSalePriceAsCurrency} に上がりました', + 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => '{description}の金額が {originalSalePriceAsCurrency} から {newSalePriceAsCurrency} に下がりました', + 'The primary currency cannot be changed after orders are placed.' => '注文後に基本通貨を変更できません。', + 'The purchasable defines the relationship' => 'パーチャサブルは関係付けを定義します', + 'The purchasable is related by another element' => 'パーチャサブルが別のエレメントに関連しています', + 'The recipient of the email. Twig code can be used here.' => 'メールの受信者。ここに Twig コードを使用できます。', + 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => '返信先メールアドレス。通常のメール送信者への返信の場合は空白のままにします。ここに Twig コードを使用できます。', + 'The site the order was made in.' => '注文が行われたサイト。', + 'The site to be used when this email is rendered.' => 'このメールをレンダリングする際に使用するサイト。', + 'The subject line of the email. Twig code can be used here.' => 'メールの件名。ここに Twig コードを使用できます。', + 'The template that the PDF should be generated from.' => 'PDF を生成するテンプレート。', + 'The template to be used for HTML emails.' => 'HTML メールに使用されるテンプレート。', + 'The template to be used for plain text emails. Twig code can be used here.' => 'プレーンテキスト形式のメールに使用されるテンプレート。ここに Twig コードを使用できます。', + 'The template to use when a product’s URL is requested.' => '商品の URL が要求された場合に使用するテンプレート。', + 'The total number of order adjustments changed.' => '注文合計数のアジャストメントが変更されました。', + 'The total price of the order changed.' => '注文の合計金額が変更されました。', + 'The total quantity of items within the order changed.' => '注文内のアイテムの合計数量が変更されました。', + 'The unique SKU of the donation purchasable.' => '購入可能な寄付の一意のSKU。', + 'The unit of measurement that should be used when specifying product dimensions.' => '商品の寸法を示すために使用する単位。', + 'The unit of measurement that should be used when specifying product weights.' => '商品の重量を示すために使用する単位。', + 'The webhook URL for this gateway.' => 'このゲートウェイの webhook の URL。', + 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => '注文ステータスのメールを送信するときに使用される「差出人」の名前。 Craftの一般設定で定義された送信者名を使用するには、空白のままにします。', + 'There are errors on the order' => '注文にエラーがあります', + 'There are only {num} “{description}” items left in stock.' => '在庫には {num} 個の「{description}」アイテムしか残っていません。', + 'There aren’t any product types to select yet.' => '選択する商品タイプがまだありません。', + 'There is no gateway or payment source available for use with this order.' => 'この注文に利用できるゲートウェイまたは支払い元がありません。', + 'There is no gateway selected that supports payment sources.' => '支払い元をサポートするゲートウェイが選択されていません。', + 'There is no shipping method selected for this order.' => 'この注文に選択された配送方法はありません。', + 'This URL will load the cart into the user’s session, making it the active cart.' => 'この URL はカートをユーザーセッションにロードし、カートを有効にします。', + 'This action is not allowed for the current user.' => 'これは現在のユーザーに許可されていないアクションです。', + 'This category will be used as the default for all purchasables in this store.' => 'このカテゴリは、このストア内のすべてのパーチャサブルのデフォルトとして使用されます。', + 'This coupon is for registered users and limited to {limit} uses.' => 'このクーポンは登録済みユーザーを対象としており、使用回数は {limit} 回に制限されています。', + 'This coupon is limited to {limit} uses.' => 'このクーポンの使用は {limit} 回に制限されています。', + 'This coupon requires an email address.' => 'このクーポンにはメールアドレスが必要です。', + 'This gateway does not support that functionality.' => 'このゲートウェイはその機能をサポートしていません。', + 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'これは、`config/{file}.php` の {setting} 構成設定によって上書きされています。', + 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'これはあなたのストアの場所を示す住所です。配送料や税金などを決定するために、さまざまなプラグインによって使用される場合があります。PDF領収書でも使用できます。', + 'This is the default PDF that will be rendered when requesting the order PDF.' => 'これは、注文 PDF を要求する際に表示されるデフォルトの PDF です。', + 'This is the last location for the {store} store.' => 'これは{store}ストアの最後の場所です。', + 'This month' => '今月', + 'This order has unsaved changes.' => 'この注文には保存されていない変更があります。', + 'This week' => '今週', + 'This year' => '今年', + 'Times Used' => '利用された回数', + 'Title' => 'タイトル', + 'To' => '宛先', + 'Today' => '今日', + 'Too many variants for this product.' => 'この商品のバリアントが多すぎます。', + 'Top Customers by Average Order' => '平均注文別上位顧客', + 'Top Customers by Total Revenue' => '合計収益別上位顧客', + 'Top Customers' => '上位顧客', + 'Top Product Types by Qty Sold' => '販売数量別上位商品タイプ', + 'Top Product Types by Revenue' => '収益別上位商品タイプ', + 'Top Product Types' => '上位商品タイプ', + 'Top Products by Qty Sold' => '販売数量別上位商品', + 'Top Products by Revenue' => '収益別上位商品', + 'Top Products' => '上位商品', + 'Top Purchasables by Qty Sold' => '販売数量別上位パーチャサブル', + 'Top Purchasables by Revenue' => '収益別上位パーチャサブル', + 'Top Purchasables' => '上位パーチャサブル', + 'Total ' => '合計 ', + 'Total Discount Use Limit' => 'ディスカウントの合計使用制限数', + 'Total Discount' => '合計ディスカウント', + 'Total Included Tax' => '税込合計', + 'Total Orders by Billing Country' => '請求先国別合計注文数', + 'Total Orders by Country' => '国別合計注文数', + 'Total Orders by Shipping Country' => '配送先国別合計注文数', + 'Total Orders' => '合計注文数', + 'Total Paid' => '支払い済み合計', + 'Total Price' => '合計金額', + 'Total Qty' => '合計数量', + 'Total Revenue' => '合計収益', + 'Total Shipping' => '合計配送料', + 'Total Tax' => '税額合計', + 'Total Weight' => '合計重量', + 'Total' => '合計', + 'Track Inventory' => '在庫を追跡', + 'Transaction Hash' => '取引ハッシュ', + 'Transaction ID' => '取引 ID', + 'Transaction captured successfully: {message}' => '取引は正しくキャプチャされました: {message}', + 'Transaction refunded successfully: {message}' => '取引は正しく払い戻されました: {message}', + 'Transactions' => '取引', + 'Transfer Fields' => '転送項目', + 'Transfer Items' => '移動アイテム', + 'Transfer Settings' => '移動設定', + 'Transfer Status' => '移動ステータス', + 'Transfer fields saved.' => '移動フィールドは保存されました。', + 'Transfer must have at least one item.' => '移動には少なくとも1つのアイテムが必要です。', + 'Transfer' => '移動', + 'Transfers' => '移動', + 'Trial days credited' => '付与されたトライアル期間', + 'Trial expiration' => 'トライアル期限', + 'Trial expiry date' => 'トライアルの有効期限', + 'Type not in allowed options.' => 'タイプは許可されるオプションにありません。', + 'Type' => 'タイプ', + 'URI' => 'URI', + 'Unable to cancel subscription at this time.' => '現在、定期支払いをキャンセルできません。', + 'Unable to complete order: another request is already in progress.' => '注文を完了できません:別のリクエストが既に進行中です。', + 'Unable to find variant.' => 'バリアントが見つかりません。', + 'Unable to generate coupon codes: {message}' => 'クーポンコードを生成できません: {message}', + 'Unable to make payment at this time.' => '現在のところ、お支払いできません。', + 'Unable to modify subscription at this time.' => '現在、定期支払いを変更できません。', + 'Unable to reactivate subscription at this time.' => '現在、定期支払いを再開できません。', + 'Unable to reassign orders.' => '注文を再割り当てできません。', + 'Unable to remove order data.' => '注文データを削除できません。', + 'Unable to retrieve Sale and Purchasable.' => 'セールとパーチャサブルを取得できません。', + 'Unable to retrieve cart.' => 'カートを取得できません。', + 'Unable to retrieve customer.' => '顧客を取得できません。', + 'Unable to retrieve load cart URL' => 'ロードするカートの URL を取得できません', + 'Unable to retrieve payment source.' => '支払い元を取得できません。', + 'Unable to set default shipping category.' => 'デフォルトの配送カテゴリを設定できません。', + 'Unable to set default tax category.' => 'デフォルトの税カテゴリを設定できません。', + 'Unable to set primary payment source.' => '主な支払い元を設定できません。', + 'Unable to start the subscription. Please check your payment details.' => '定期支払いを開始できません。支払いの詳細を確認してください。', + 'Unable to subscribe at this time.' => '現在、定期支払いを設定できません。', + 'Unable to update cart.' => 'カートを更新できません。', + 'Unable to validate address.' => '住所を確認できません。', + 'Unit Price' => '単価', + 'Unit price (minus discounts)' => '単価(ディスカウントを差し引いた価格)', + 'Units' => '単位', + 'Unpaid' => '未決済', + 'Unsubscribe' => '定期支払い解除', + 'Update Address' => '住所を更新する', + 'Update Order Status' => '注文ステータスを更新する', + 'Update Order Status…' => '注文ステータスを更新…', + 'Update order' => '注文を更新する', + 'Update subscription' => '定期支払いを更新', + 'Update' => '更新', + 'Updated By' => '更新者', + 'Updated committed stock successfully.' => 'コミット済み在庫を正常に更新しました。', + 'Updated' => '更新済み', + 'Use Billing Address For Tax' => '税請求先住所を使用する', + 'Use as the primary billing address' => 'プライマリ請求先住所として使用', + 'Use as the primary shipping address' => 'プライマリ配送先住所として使用', + 'Used By Tax Rates' => '税率に使用されている', + 'Used by Tax Rates' => '税率に使用されている', + 'User Groups' => 'ユーザーグループ', + 'User not found.' => 'ユーザーが見つかりません。', + 'User' => 'ユーザー', + 'Uses' => '使用回数', + 'Validate Business Tax ID as Vat ID' => '事業税IDをVAT IDとして検証する', + 'Validating condition syntax' => '条件構文を検証中', + 'Validating formula syntax' => '式構文を検証中', + 'Variant Fields' => 'バリアントフィールド', + 'Variant Has Untracked Stock' => 'バリアントに追跡されていない在庫があります', + 'Variant Price' => 'バリアントの価格', + 'Variant SKU' => 'バリアントのSKU', + 'Variant Search' => 'バリアントの検索', + 'Variant Stock' => 'バリアントの在庫', + 'Variant Title Format' => 'バリアントのタイトルフォーマット', + 'Variant Tracks Stock' => 'バリアントは在庫を追跡します', + 'Variant UI Label Format' => 'バリアントUIラベルの表示形式', + 'Variant has no product.' => 'バリアントに商品がありません。', + 'Variants not restored.' => 'バリアントは復元されていません。', + 'Variants restored.' => 'バリアントが復元されました。', + 'Variants' => 'バリアント', + 'View customer' => '顧客を表示', + 'View order' => '注文を表示', + 'View product type - {productType}' => '商品タイプを表示 - {productType}', + 'View user' => 'ユーザーを表示', + 'View' => '表示', + 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => '警告。この通貨を削除すると、この通貨でのすべての支払いと返金が停止されます。「{name}」を削除してもよろしいですか?', + 'Web' => 'ウェブ', + 'Webhook URL' => 'Webhook URL', + 'Weight ({unit})' => '重量({unit})', + 'Weight Rate' => '重量ごとの料金', + 'Weight Unit' => '重量の単位', + 'Weight' => '重量', + 'What product URIs should look like for the site.' => 'サイト向けの商品 URI の形式', + 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => '自動生成されたバリアントタイトルの表示方法。{ex1} や {ex2} など、商品プロパティを出力するタグを含めることができます。使用されるすべてのカスタムフィールドは required に設定する必要があります。', + 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => '自動生成されたバリアントタイトルの表示方法。{ex1} や {ex2} など、バリアントプロパティを出力するタグを含めることができます。使用されるすべてのカスタムフィールドは required に設定する必要があります。', + 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => '注文 PDF ファイル名の表示方法(拡張子無し)。{ex1} または {ex2} など、注文プロパティを出力するタグを含めることができます。', + 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'SKU フィールドに値が指定されずに送信された場合に自動生成される一意の SKU の表示方法。{ex1} や {ex2} といった、プロパティを出力するタグを含めることができます。', + 'What this PDF will be called in the control panel.' => 'コントロールパネルに表示するこの PDF の名前。', + 'What this catalog pricing rule will be called in the control panel.' => 'コントロールパネルに表示するこのカタログ価格ルールの名前。', + 'What this discount will be called in the control panel.' => 'コントロールパネルに表示するこのディスカウントの名前。', + 'What this email will be called in the control panel.' => 'コントロールパネルに表示するこのメールの名前。', + 'What this product type will be called in the control panel.' => 'コントロールパネルに表示するこの商品タイプの名前。', + 'What this sale will be called in the control panel.' => 'コントロールパネルに表示するこのセールの名前。', + 'What this shipping category will be called in the control panel.' => 'コントロールパネルに表示するこの配送カテゴリの名前。', + 'What this shipping rule will be called in the control panel.' => 'コントロールパネルに表示するこの配送ルールの名前。', + 'What this shipping zone will be called in the control panel.' => 'コントロールパネルに表示するこの配送地域の名前。', + 'What this status will be called in the control panel.' => 'コントロールパネルに表示するこのステータスの名前。', + 'What this subscription plan will be called in the control panel.' => 'コントロールパネルに表示するこの定期支払いプランの名前。', + 'What this tax category will be called in the control panel.' => 'コントロールパネルに表示するこの税カテゴリの名前。', + 'What this tax zone will be called in the control panel.' => 'コントロールパネルに表示するこの税対象地域の名前。', + 'When this discount is applied to an order, which line items should be discounted?' => 'このディスカウントが注文に適用される場合、どのラインアイテムにディスカウントを適用しますか?', + 'Whether the first available shipping method option should be set automatically on carts.' => '最初に利用可能な配送方法オプションをカートに自動的に設定するかどうか。', + 'Whether the user’s primary payment source should be set automatically on new carts.' => 'ユーザーのプライマリの支払い元を自動的に新しいカートに設定するかどうか。', + 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'ユーザーのプライマリの配送先住所と請求先住所を自動的に新しいカートに設定するかどうか。', + 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => '他の条件に関係なく、このカタログ価格ルールを使用可能にしますか?', + 'Whether this sale should be available for use, regardless of other conditions.' => '他の条件に関係なく、このセールを使用可能にしますか?', + 'Which data to display in the name column in the results table.' => '結果テーブルの名前列に表示するデータ。', + 'Which product types should this category be available to?' => 'このカテゴリを使用できる商品タイプはどれですか?', + 'Which template should be loaded when a product’s URL is requested.' => '商品の URL がリクエストされた場合にロードするテンプレート。', + 'Width ({unit})' => '幅({unit})', + 'Width' => '幅', + 'YYYY' => 'YYYY', + 'Yes' => 'はい', + 'You are not allowed to add a line item.' => 'ラインアイテムを追加する権限がありません。', + 'You currently have no emails configured to select for this status.' => '現在、このステータスに選択できるメールが構成されていません。', + 'You do not have permission to load this cart.' => 'このカートを読み込む権限がありません。', + 'You must set up at least one gateway that supports subscriptions first.' => 'まず、定期支払いをサポートするゲートウェイを少なくとも 1 つセットアップする必要があります。', + 'You must be logged in or provide a valid token to load this cart.' => 'カートを読み込むにはログインするか、有効なトークンを入力してください。', + 'You must be signed in to create a payment source.' => '支払い元を作成するにはサインインする必要があります。', + 'You must be signed in to set a primary payment source.' => '主な支払い元を設定するにはサインインする必要があります。', + 'You must make a payment to complete the order.' => '注文を完了するには、支払いを行う必要があります。', + 'Your Cart Recovery Link' => 'カート復元リンク', + 'Your Order PDF Download Link' => 'お客様の注文PDFダウンロードリンク', + 'Your order is empty' => '注文内容がありません', + 'ZIP file' => 'ZIP ファイル', + 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'ゼロ - ディスカウントが注文価格を上回る場合、最低料金はゼロです。', + 'Zip Code' => '郵便番号', + 'all' => 'すべて', + 'any' => 'いずれか', + 'average order total' => '注文合計平均', + 'billing address' => '請求先住所', + 'donation' => '寄付', + 'donations' => '寄付', + 'info' => '情報', + 'inventory location' => '在庫場所', + 'new customers' => '新規顧客', + 'on hand' => '手持ち', + 'only' => 'のみ', + 'order' => '注文', + 'orders' => '注文', + 'price' => '価格', + 'prices' => '価格', + 'product variant' => '商品バリアント', + 'product variants' => '商品バリアント', + 'product' => '商品', + 'products' => '商品', + 'repeat customers' => 'リピート顧客', + 'shipping address' => '配送先住所', + 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'shippingSameAsBilling と billingSameAsShipping を同時に設定することはできません。', + 'subscription' => '定期支払い', + 'subscriptions' => '定期支払い', + 'to' => 'から', + 'transfer' => '移動', + 'transfers' => '移動', + '{amount} included' => '{amount} 込み', + '{count} Unfulfilled Orders' => '{count}件の履行されていない注文', + '{description} is no longer available.' => '{description}はご利用いただけません。', + '{description} only has {stock} in stock.' => '{description}の在庫数は {stock} のみです。', + '{from} to {to}' => '{from}から{to}', + '{name} (Primary)' => '{name} (既定)', + '{name} (Trashed)' => '{name} (破棄済み)', + '{name} catalog price' => '{name}カテゴリ価格', + '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, =1{注文} other{注文}}が更新されました。', + '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, =1{件の注文} other{件の注文}}が、{numUsers, plural, =1{ユーザー} other{ユーザー}}に関連付けられています。', + '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, =1{件のサブスクリプション} other{件のサブスクリプション}}が、{numUsers, plural, =1{ユーザー} other{ユーザー}}に対して有効化されています。', + '{number} more…' => 'その他 {number}...', + '{pct} off the discounted item price' => 'ディスカウントされたアイテム価格の {pct} 割引', + '{pct} off the original item price' => '元のアイテム価格の {pct} 割引', + '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, =1{は} other{は}}サイトにまだ割り当てられていません。', + '{total} in total revenue' => '合計収益の {total}', + '{total} orders' => '注文数 {total}', + '{total} saleable across {locationCount} location(s)' => '{locationCount}か所の場所全体での{total}件の販売可能品', + '{uses} uses across {emails} email addresses' => '{emails} 個のメールアドレスで {uses} 回使用', + '{uses} uses across {users} users' => '{users} 人のユーザーで {uses} 回使用', + '“{description}” is currently out of stock.' => '現在「{description}」の在庫はありません。', + '“{key}” has invalid JSON' => '「{key}」には無効なJSONがあります', +]; diff --git a/lang/nb/commerce.php b/lang/nb/commerce.php new file mode 100644 index 0000000000..708815842b --- /dev/null +++ b/lang/nb/commerce.php @@ -0,0 +1,1423 @@ + '(ny pris)', + '(of original price)' => '(av opprinnelig pris)', + '(off original price)' => '(av opprinnelig pris)', + 'A cart number must be specified.' => 'Et handlekurvnummer må spesifiseres.', + 'A cart recovery link has been sent to {email}.' => 'En lenke for gjenoppretting av handlekurv ble sendt til {email}.', + 'A cart recovery link will be sent to {email}.' => 'En lenke for gjenoppretting av handlekurv blir sendt til {email}.', + 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Et vennlig referansenummer blir generert basert på dette formatet når en handlekurv er fullført og blir til en bestilling. F.eks. {ex1}, eller
{ex2}. Resultatet av dette formatet må være unikt.', + 'A new download link has been sent to {email}' => 'En ny lenke for nedlasting er sendt til {email}', + 'A new download link will be sent to {email}' => 'En ny lenke for nedlasting vil bli sendt til {email}', + 'A valid email is required to create a customer.' => 'For å opprette en kunde er det nødvendig med en gyldig e-postadresse.', + 'Accept' => 'Godta', + 'Accepted' => 'Godtatt', + 'Actions' => 'Handlinger', + 'Active Carts' => 'Aktive Handlevogner', + 'Active subscriptions' => 'Aktive abonnementer', + 'Active' => 'Aktiv', + 'Add Address' => 'Legg til adresse', + 'Add a coupon' => 'Legg til en kupong', + 'Add a custom line item' => 'Legg til et tilpasset linjeelement', + 'Add a line item' => 'Legg til en linjevare', + 'Add a product' => 'Legg til et produkt', + 'Add a variant' => 'Legg til en variasjon', + 'Add an adjustment' => 'Legg til en justering', + 'Add an item' => 'Legg til et element', + 'Add an option' => 'Legg til et valg', + 'Add catalog price' => 'Legg til katalogpris', + 'Add' => 'Legg til', + 'Additional Actions' => 'Flere handlinger', + 'Additional recipients that should receive this email. Twig code can be used here.' => 'Flere mottakere av e-posten. Twig-kode kan brukes.', + 'Address 1' => 'Adresse 1', + 'Address 2' => 'Adresse 2', + 'Address 3' => 'Adresse 3', + 'Address Line 1' => 'Adresselinje 1', + 'Address Line 2' => 'Adresselinje 2', + 'Address Updated.' => 'Adresse oppdatert.', + 'Address copied to user.' => 'Adresse kopiert til bruker.', + 'Address not found.' => 'Adresse ikke funnet.', + 'Adjust Quantity' => 'Juster antall', + 'Adjust by' => 'Juster etter', + 'Adjust price when included rate is disqualified?' => 'Justere pris når inkludert sats er diskvalifisert?', + 'Adjustments' => 'Justeringer', + 'Admin Notices' => 'Administratorvarsler', + 'Administrative Area Code of Origin' => 'Opprinnelseskode for administrativt område', + 'Advanced' => 'Avansert', + 'All Orders' => 'Alle ordrer', + 'All Totals' => 'Alle totaler', + 'All Transfers' => 'Alle overføringer', + 'All active subscriptions' => 'Alle aktive abonnementer', + 'All customers' => 'Alle kunder', + 'All products' => 'Alle produkter', + 'All variants must have a SKU.' => 'Alle varianter må ha en SKU.', + 'All' => 'Alle', + 'Allow Checkout Without Payment' => 'Tillat bestilling uten betaling', + 'Allow Empty Cart On Checkout' => 'Tillat tom handlevogn ved bestilling', + 'Allow Partial Payment On Checkout' => 'Tillat delbetaling ved bestilling', + 'Allow out of stock purchases' => 'Tillat kjøp som er utsolgt', + 'Allow' => 'Tillat', + 'Allowed Qty' => 'Tillatt Antall', + 'Alternative Phone' => 'Alternativt telefonnummer', + 'Amount' => 'Mengde', + 'An ID must be provided' => 'Oppgi en ID', + 'An error occurred while generating this PDF.' => 'En feil oppstod under generering av denne PDF-en.', + 'Any' => 'Noen', + 'Anywhere' => 'Overalt', + 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Er du sikker på at du vil arkivere “{name}” abonnementsplan? Den kansellerer IKKE eksisterende abonnementer.', + 'Are you sure you want to capture this transaction?' => 'Er du sikker på at du vil ta denne transaksjonen?', + 'Are you sure you want to complete this order?' => 'Er du sikker på at du vil fullføre ordren?', + 'Are you sure you want to delete the selected orders?' => 'Er du sikker på at du ønsker å slette de valgte ordrene?', + 'Are you sure you want to delete the selected product and its variants?' => 'Er du sikker på at du vil slette det valgte produktet og dets varianter?', + 'Are you sure you want to delete this shipping rule?' => 'Er du sikker på at du vil slette fraktregelen?', + 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Er du sikker på at du ønsker å slette “{name}” og alle tilhørende produkter? Vennligst vær sikker på at du har en sikkerhetskopi av databasen din før du utfører denne ødeleggende handlingen.', + 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Er du sikker på at du vil slette «{name}»? Alle linjevarene med denne statusen vil få ingen status.', + 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Er du sikker på at du vil merke denne overføring som Venter? Dette vil vises som innkommende på destinasjonen.', + 'Are you sure you want to overwrite the billing address?' => 'Er du sikker på at du vil overskrive faktureringsadressen?', + 'Are you sure you want to overwrite the shipping address?' => 'Er du sikker på at du vil overskrive leveringsadressen?', + 'Are you sure you want to permanently delete this store and everything in it?' => 'Er du sikker på at du vil slette denne butikken og alt i den for godt?', + 'Are you sure you want to refund this transaction?' => 'Er du sikker på at du ønsker å refundere denne transaksjonen?', + 'Are you sure you want to remove this customer?' => 'Er du sikker på at du vil fjerne kunden?', + 'Are you sure you want to save this as a new shipping rule?' => 'Er du sikker på at du vil lagre dette som en ny fraktregel?', + 'Are you sure you want to send email: {name}?' => 'Er du sikker på at du vil sende denne e-posten: {name}?', + 'At least one site must be enabled for the product type.' => 'Minst ett nettsted må aktiveres for produkttypen.', + 'Attempted Payments' => 'Forsøkte betalinger', + 'Attention' => 'Vær oppmerksom på', + 'Authorize Only (Manually Capture)' => 'Kun autoriser (ta opp manuelt)', + 'Auto Set Cart Shipping Method Option' => 'Angi fraktalternativ automatisk', + 'Auto Set New Cart Addresses' => 'Angi nye adresser for handlevogn automatisk', + 'Auto Set Payment Source' => 'Angi betalingskilde automatisk', + 'Automatic SKU Format' => 'Automatisk SKU-format', + 'Available Shipping Categories' => 'Tilgjengelige fraktkategorier', + 'Available Tax Categories' => 'Tilgjengelige skattekategorier', + 'Available for purchase' => 'Tilgjengelig for kjøp', + 'Available for purchase?' => 'Tilgjengelig for kjøp?', + 'Available inventory for "{description}" has gone below zero.' => 'Tilgjengelig inventar for «{description}» har gått under null.', + 'Available to Product Types' => 'Tilgjengelig for produkttyper', + 'Available' => 'Tilgjengelig', + 'Available?' => 'Tilgjengelig?', + 'Average Order Total' => 'Gjennomsnittlig ordretotal', + 'Average' => 'Gjennomsnitt', + 'BCC’d Recipient' => 'BCC\'d-mottaker', + 'Bad Request' => 'Problem med forespørsel', + 'Bad address ID.' => 'Feil adresse-ID.', + 'Bad order ID.' => 'Feil ordre-ID.', + 'Base Price' => 'Grunnpris', + 'Base Promotional Price' => 'Grunnpris kampanje', + 'Base Rate' => 'Grunnpris', + 'Base' => 'Base', + 'Bcc' => 'Bcc', + 'Billing Address' => 'Faktureringsadresse', + 'Billing Business Name' => 'Fakturering firmanavn', + 'Billing First Name' => 'Fakturering fornavn', + 'Billing Full Name' => 'Fakturering fullt navn', + 'Billing Last Name' => 'Fakturering etternavn', + 'Billing address required.' => 'Faktureringsadresse nødvendig.', + 'Billing detail update URL' => 'URL for oppdatering av faktureringsinformasjon', + 'Billing issues' => 'Faktureringsproblemer', + 'Billing' => 'Fakturering', + 'Both (Line item price + Line item shipping costs)' => 'Begge (linjepris + forsendelseskostnader for vare i varelinje)', + 'Business ID' => 'Forretnings-ID', + 'Business Name' => 'Forretningsnavn', + 'Business Tax ID' => 'Forretningsskatte-ID', + 'CC’d Recipient' => 'CC\'d-mottaker', + 'CVV' => 'CW', + 'Can be used as an internal reference.' => 'Kan brukes som intern referanse.', + 'Can not complete payment for missing transaction.' => 'Kan ikke fullføre betaling for manglende transaksjon.', + 'Can not create a new order' => 'Kan ikke opprette en ny ordre', + 'Can not find an order to pay.' => 'Kan ikke finne en ordre å betale.', + 'Can not find enabled email.' => 'Kan ikke finne deaktivert e-post.', + 'Can not find order' => 'Kan ikke finne ordre', + 'Can not find order.' => 'Kan ikke finne ordre.', + 'Can not find the transaction to refund' => 'Kan ikke finne transaksjonen som skal refunderes', + 'Can not move between these inventory types.' => 'Kan ikke flyttes mellom disse inventartypene.', + 'Can not refund amount greater than the remaining amount' => 'Kan ikke refundere beløp større enn det opprinnelige beløpet', + 'Cancel subscription' => 'Kansellere abonnement', + 'Cancel with gateway now' => 'Avbryt med gateway nå', + 'Cancel' => 'Kansellere', + 'Cancellation date' => 'Kanselleringsdato', + 'Cancellation' => 'Kansellering', + 'Cannot switch plans for this subscription.' => 'Kan ikke bytte planer for dette abonnementet.', + 'Can’t preview this email.' => 'Kan ikke forhåndsvise e-posten.', + 'Capture payment' => 'Fang betaling', + 'Capture' => 'Ta', + 'Card Holder' => 'Kortholder', + 'Card Number' => 'Kortnummer', + 'Card' => 'Kort', + 'Cart Recovery Link' => 'Lenke for gjenoppretting av handlekurv', + 'Cart forgotten.' => 'Handlevogn glemt.', + 'Cart updated.' => 'Handlevogn oppdatert.', + 'Cart {number}' => 'Handlevogn {number}', + 'Catalog Pricing Rule' => 'Prisregel for katalog', + 'Catalog pricing rule description.' => 'Beskrivelse av prisregel for katalog.', + 'Catalog pricing rule saved.' => 'Prisregel for katalog lagret.', + 'Catalog pricing rules deleted.' => 'Prisregel for katalog slettet.', + 'Catalog pricing rules updated.' => 'Prisregel for katalog oppdatert.', + 'Categories Relationship Type' => 'Kategorienes forholdstype', + 'Categories' => 'Kategorier', + 'Category Rate Overrides' => 'Overstyrer kategorirate', + 'Centimeters (cm)' => 'Centimeter (cm)', + 'Changing this value may affect your ability to refund existing transactions.' => 'Endring av verdien kan ha innvirkning på din mulighet til å tilbakebetale eksisterende transaksjoner.', + 'Choose a color to represent the order’s status' => 'Velg en farge som skal brukes på ordrens status', + 'Choose a new customer' => 'Velg en ny kunde', + 'Choose adjustment values to include when calculating the product revenue total.' => 'Velg justeringsverdier som skal inkluderes under beregning av samlet produktinntekt.', + 'Choose the currency’s ISO code.' => 'Velg ISO-koden for valutaen.', + 'Choose the destination inventory location for the existing on hand stock.' => 'Velg målsted i inventar for eksisterende tilgjengelige lagervarer.', + 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Velg hvilke nettsteder denne produkttypen skal være tilgjengelig på, og konfigurer innstillingene for hvert enkelt nettsted.', + 'City' => 'By', + 'Clear counter' => 'Fjern teller', + 'Clear notices' => 'Fjern merknader', + 'Close' => 'Lukk', + 'Code' => 'Kode', + 'Collated PDF' => 'Sortert PDF', + 'Color' => 'Farge', + 'Commerce Products' => 'Commerce-produkter', + 'Commerce Settings' => 'Commerce innstillinger', + 'Commerce Variants' => 'Salgsvarianter', + 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Salgs-e-post «{email}» kunne ikke sendes for bestilling «{order}».', + 'Commerce order exports' => 'Eksport av handelsordre', + 'Commerce' => 'Handel', + 'Committed' => 'Forpliktet', + 'Completed Email' => 'Fullført e-postadresse', + 'Completed' => 'Fullført', + 'Completing order failed.' => 'Fullføring av ordre mislyktes.', + 'Condition' => 'Tilstand', + 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Her matches betingelsene mot en ordre før reglene blir vurdert. Dette er nyttig hvis du ønsker å kvalifisere en metodes tilgjengelighet tidlig, eller hvis det finnes felles betingelser for alle reglene for denne metoden.', + 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Her matches betingelsene mot ordrens kunde før reglene blir vurdert. Dette er nyttig hvis du ønsker å kvalifisere en metodes tilgjengelighet tidlig, eller hvis det finnes felles betingelser for alle reglene for denne metoden.', + 'Conditions' => 'Betingelser', + 'Contains Purchasables' => 'Inneholder kjøpbare varer', + 'Control Panel Settings' => 'Kontrollpanelinnstillinger', + 'Control panel' => 'Kontrollpanel', + 'Conversion Rate' => 'Konverteringskurs', + 'Converted Price' => 'Konvertert pris', + 'Copied!' => 'Kopiert!', + 'Copy the URL' => 'Kopier URL', + 'Copy to {location}' => 'Kopier til {location}', + 'Copy' => 'Kopier', + 'Costs' => 'Kostnader', + 'Could not archive gateway.' => 'Kunne ikke arkivere portal.', + 'Could not cancel “{reference}”.' => 'Kunne ikke kansellere «{reference}».', + 'Could not create the payment source.' => 'Kunne ikke opprette betalingskilde.', + 'Could not delete shipping rule' => 'Kunne ikke slette regel for forsendelse', + 'Could not delete shipping zone' => 'Kunne ikke slette forsendelsessone', + 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Kunne ikke slette {count, number} frakt{count, plural, one{kategori} other{kategorier}}.', + 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Kunne ikke slette {count, number} frakt{count, plural, one{metode} other{metoder}} og regler.', + 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Kunne ikke slette {count, number} skatte{count, plural, one{kategori} other{kategorier}}.', + 'Could not find the email or template.' => 'Kunne ikke finne e-posten eller malen.', + 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Kunne ikke merke ordren {number} som komplett. Lagring av bestilling mislyktes under fullføring av ordren med feil: {order}', + 'Could not reactivate “{reference}”.' => 'Kunne ikke reaktivere «{reference}».', + 'Could not send email' => 'Kunne ikke sende e-post', + 'Could not switch “{reference}” to “{plan}”.' => 'Kunne ikke bytte «{reference}» til «{plan}».', + 'Could not update orders address.' => 'Kunne ikke oppdatere ordreadresser.', + 'Couldn’t archive Line Item Status.' => 'Kunne ikke arkivere linjevarestatus.', + 'Couldn’t archive Order Status.' => 'Kunne ikke arkivere ordrestatus.', + 'Couldn’t capture transaction.' => 'Kunne ikke fange transaksjon.', + 'Couldn’t capture transaction: {message}' => 'Kunne ikke fange transaksjon: {message}', + 'Couldn’t delete email.' => 'Kunne ikke slette e-post.', + 'Couldn’t delete the payment source.' => 'Kunne ikke slette betalingskilde.', + 'Couldn’t get order.' => 'Kunne ikke hente ordre.', + 'Couldn’t recalculate order.' => 'Kunne ikke beregne ordre på nytt.', + 'Couldn’t refund transaction.' => 'Kunne ikke refundere transaksjon.', + 'Couldn’t refund transaction: {message}' => 'Kunne ikke refundere transaksjon: {message}', + 'Couldn’t reorder Line Item Statuses.' => 'Kunne ikke endre rekkefølgen på linjevarestatuser.', + 'Couldn’t reorder Order Statuses.' => 'Kunne ikke endre rekkefølgen på ordrestatuser.', + 'Couldn’t reorder PDFs.' => 'Kunne ikke endre rekkefølgen på PDF-er.', + 'Couldn’t reorder discounts.' => 'Kunne ikke endre rekkefølgen på rabatter.', + 'Couldn’t reorder gateways.' => 'Kunne ikke bestille portaler på nytt.', + 'Couldn’t reorder plans.' => 'Kunne ikke bestille planer på nytt.', + 'Couldn’t reorder rules.' => 'Kunne ikke endre rekkefølgen på regler.', + 'Couldn’t reorder sale.' => 'Kunne ikke endre rekkefølgen på salg.', + 'Couldn’t reorder sales.' => 'Kunne ikke endre rekkefølgen på salg.', + 'Couldn’t reorder statuses.' => 'Kunne ikke endre rekkefølgen på statuser.', + 'Couldn’t reorder stores.' => 'Kunne ikke endre rekkefølgen på butikker.', + 'Couldn’t save PDF.' => 'Kunne ikke lagre PDF.', + 'Couldn’t save catalog pricing rule.' => 'Kunne ikke lagre prisregel for katalog.', + 'Couldn’t save currency.' => 'Kunne ikke lagre valuta.', + 'Couldn’t save discount.' => 'Kunne ikke lagre rabatt.', + 'Couldn’t save email.' => 'Kunne ikke lagre e-post.', + 'Couldn’t save gateway.' => 'Kunne ikke lagre portal.', + 'Couldn’t save inventory location.' => 'Kunne ikke lagre inventarsted.', + 'Couldn’t save line item status.' => 'Kunne ikke lagre linjevarestatus.', + 'Couldn’t save order fields.' => 'Kunne ikke lagre ordrefelt.', + 'Couldn’t save order status.' => 'Kunne ikke lagre ordrestatus.', + 'Couldn’t save order.' => 'Kunne ikke lagre ordre.', + 'Couldn’t save product type.' => 'Kunne ikke lagre produkttype.', + 'Couldn’t save sale.' => 'Kunne ikke lagre salg.', + 'Couldn’t save settings.' => 'Kunne ikke lagre innstillinger.', + 'Couldn’t save shipping category.' => 'Kunne ikke lagre fraktkategori.', + 'Couldn’t save shipping method.' => 'Kunne ikke lagre fraktmetode.', + 'Couldn’t save shipping rule.' => 'Kunne ikke lagre fraktregel.', + 'Couldn’t save shipping zone.' => 'Kunne ikke lagre fraktsone.', + 'Couldn’t save store.' => 'Kunne ikke lagre butikk.', + 'Couldn’t save subscription fields.' => 'Kunne ikke lagre abonnementsfelter.', + 'Couldn’t save subscription plan.' => 'Kunne ikke lagre abonnementsplan.', + 'Couldn’t save subscription.' => 'Kunne ikke lagre abonnement.', + 'Couldn’t save tax category.' => 'Kunne ikke lagre avgiftskategori.', + 'Couldn’t save tax rate.' => 'Kunne ikke lagre skattesats.', + 'Couldn’t save tax zone.' => 'Kunne ikke lagre avgiftssone.', + 'Couldn’t save transfer fields.' => 'Kunne ikke lagre overføringsfelt.', + 'Couldn’t update catalog pricing rule statuses.' => 'Kunne ikke oppdatere status på prisregel for katalog.', + 'Couldn’t update status.' => 'Kunne ikke oppdatere status.', + 'Couldn’t updated sales status.' => 'Kunne ikke oppdatere salgstatus.', + 'Country Code of Origin' => 'Opprinnelseskode for land', + 'Country List' => 'Landliste', + 'Country not allowed.' => 'Land ikke tillatt.', + 'Country' => 'Land', + 'Coupon Code' => 'Rabattkode', + 'Coupon can not apply discount to this order due to address mismatch.' => 'Kupongen kan ikke brukes til å gi rabatt på denne ordren på grunn av adresseavvik.', + 'Coupon can not apply discount to this order due to customer mismatch.' => 'Kupongen kan ikke brukes til å gi rabatt på denne ordren på grunn av kundeavvik.', + 'Coupon can not apply discount to this order.' => 'Kupongen kan ikke brukes til å gi rabatt på denne ordren.', + 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Kupongkoden «{code}» er allerede i bruk av rabatten «{name}».', + 'Coupon codes cannot be blank.' => 'Kupongkoder kan ikke være blank.', + 'Coupon codes must be unique.' => 'Kupongkoder må være unik.', + 'Coupon format is required and must contain at least one `#`.' => 'Kupongformat kreves og må inneholde minst én \'#\'.', + 'Coupon not valid.' => 'Kupongen er ikke gyldig.', + 'Coupon removed: {explanation}' => 'Kupong fjernet: {explanation}', + 'Coupons' => 'Kuponger', + 'Craft Commerce - Administration' => 'Craft Commerce – Administrasjon', + 'Craft Commerce - Inventory' => 'Craft Commerce – Inventar', + 'Craft Commerce - Orders' => 'Craft Commerce – Ordrer', + 'Craft Commerce - Product Type - {name}' => 'Craft Commerce – Produkttype – {name}', + 'Craft Commerce - Subscriptions' => 'Craft Commerce – Abonnementer', + 'Create a Discount' => 'Opprett en rabatt', + 'Create a Subscription Plan' => 'Opprett en abonnementsplan', + 'Create a new PDF' => 'Opprett en ny PDF', + 'Create a new catalog pricing rule' => 'Opprett en ny prisregel for katalog', + 'Create a new currency' => 'Lag en ny valuta', + 'Create a new email' => 'Opprett en ny e-post', + 'Create a new gateway' => 'Opprett en ny portal', + 'Create a new line item status' => 'Opprett en ny linjevarestatus', + 'Create a new order status' => 'Opprett en ny ordrestatus', + 'Create a new product type' => 'Opprett ny produkttype', + 'Create a new sale' => 'Opprett et nytt salg', + 'Create a new shipping category' => 'Opprett en ny fraktkategori', + 'Create a new shipping method' => 'Opprett en ny fraktmetode', + 'Create a new shipping rule' => 'Opprett en ny fraktregel', + 'Create a new tax category' => 'Opprett en ny avgiftskategori', + 'Create a new tax rate' => 'Opprett en ny skattesats', + 'Create a product type' => 'Opprett en produkttype', + 'Create a shipping zone' => 'Opprett en fraktsone', + 'Create a tax zone' => 'Opprett en avgiftssone', + 'Create catalog pricing rules' => 'Opprett prisregler for katalog', + 'Create customer: “{email}”' => 'Opprett kunde: «{email}»', + 'Create discounts' => 'Opprett rabatter', + 'Create discount…' => 'Opprett rabatt ...', + 'Create rules that allow this discount to match the order.' => 'Opprett regler som lar denne rabatten matche bestillingen.', + 'Create rules that allow this discount to match the order’s billing address.' => 'Opprett regler som lar denne rabatten matche faktureringsadressen.', + 'Create rules that allow this discount to match the order’s customer.' => 'Opprett regler som lar denne rabatten matche kunden.', + 'Create rules that allow this discount to match the order’s shipping address.' => 'Opprett regler som lar denne rabatten matche leveringsadressen.', + 'Create rules that allow this gateway to match the billing address.' => 'Opprett regler som lar denne portalen matche faktureringsadressen.', + 'Create rules that allow this gateway to match the order.' => 'Opprett regler som lar denne portalen matche ordren.', + 'Create rules that allow this gateway to match the shipping address.' => 'Opprett regler som lar denne portalen matche faktureringsadressen.', + 'Create sales' => 'Opprett salg', + 'Create sale…' => 'Opprett salg …', + 'Created' => 'Opprettet', + 'Credit Card Payment Type' => 'Type kredittkortbetaling', + 'Currency Code' => 'Valutakode', + 'Currency saved.' => 'Valuta lagret.', + 'Currency' => 'Valuta', + 'Current' => 'Gjeldende', + 'Custom 1' => 'Kunde 1', + 'Custom 2' => 'Kunde 2', + 'Custom 3' => 'Kunde 3', + 'Custom 4' => 'Kunde 4', + 'Custom' => 'Egendefinert', + 'Customer Enabled?' => 'Kunde aktivert?', + 'Customer ID is required.' => 'Kunde-ID er nødvendig.', + 'Customer Note' => 'Kundemerknad', + 'Customer Notices' => 'Kundemerknader', + 'Customer data' => 'Kundedata', + 'Customer' => 'Kunde', + 'Damaged' => 'Skadet', + 'Data shown might be outdated.' => 'Vist data kan være utdatert.', + 'Date Authorized' => 'Dato for autorisasjon', + 'Date Created' => 'Dato Opprettet', + 'Date First Paid' => 'Første betalingsdato', + 'Date Ordered' => 'Dato Bestilt', + 'Date Paid' => 'Dato Betalt', + 'Date Updated' => 'Dato Oppdatert', + 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Dato når prisregel for katalog vil aktiveres. La stå tomt for ubegrenset startdato', + 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Datoen som rabatten vil aktiveres. La stå tom for ubegrenset startdato', + 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Dato når salget vil aktiveres. La stå tomt for ubegrenset startdato', + 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Dato når prisregel for katalog avsluttes. La stå tom for ubegrenset sluttdato', + 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Dato når rabatten avsluttes. La stå tom for ubegrenset sluttdato', + 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Dato når salget avsluttes. La stå tom for ubegrenset sluttdato', + 'Date' => 'Dato', + 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Standard – la prisen være negativ hvis rabatter er større en ordreverdien.', + 'Default Category' => 'Standardkategori', + 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Standard visning av Commerce-kontrollpanelet. Hvis brukeren ikke har tillatelse, vil de bli tatt tilbake til et sted de har tilgang til.', + 'Default Order PDF' => 'Standard ordre-PDF', + 'Default Per Item Rate' => 'Standard enhetspris', + 'Default Percentage Rate' => 'Standard prosentpris', + 'Default Status?' => 'Standardstatus?', + 'Default View' => 'Standardvisning', + 'Default Weight Rate' => 'Standard vektpris', + 'Default Zone' => 'Standard sone', + 'Default status?' => 'Standardstatus?', + 'Default to this tax zone when no billing address is set' => 'Sett dette som standard skattesone når ingen fakturaadresse er satt', + 'Default to this tax zone when no shipping address is set' => 'Standard til denne avgiftssonen når ingen leveringsadresse er satt', + 'Default variant updated.' => 'Standardvariant oppdatert.', + 'Default' => 'Standard', + 'Default?' => 'Standard?', + 'Delete catalog pricing rules' => 'Slett prisregler for katalog', + 'Delete discounts' => 'Slett rabatter', + 'Delete orders' => 'Slett ordrer', + 'Delete sales' => 'Slett salg', + 'Delete' => 'Slett', + 'Deleting the {location} location.' => 'Sletter stedet {location}.', + 'Describe this rule.' => 'Beskriv denne regelen.', + 'Describe this shipping zone.' => 'Beskriv denne fraktsonen.', + 'Describe this tax zone.' => 'Beskriv denne avgiftssonen.', + 'Description' => 'Beskrivelse', + 'Destination Inventory Location' => 'Målsted i inventar', + 'Destination' => 'Destinasjon', + 'Details' => 'Detaljer', + 'Dimension Unit' => 'Dimensjonsenhet', + 'Dimensions' => 'Dimensjoner', + 'Disabled' => 'Deaktivert', + 'Disallow' => 'Ikke tillat', + 'Discount all line items' => 'Rabatt på alle linjevarer', + 'Discount description.' => 'Beskrivelse av rabatt', + 'Discount is not allowed for the order' => 'Rabatter er ikke tillat for ordren', + 'Discount is out of date.' => 'Rabatt er utdatert.', + 'Discount saved.' => 'Rabatt lagret.', + 'Discount the matching items only' => 'Gi rabatt bare på matchende varer', + 'Discount use has reached its limit.' => 'Rabattbruk har nådd grensen.', + 'Discount' => 'Rabatt', + 'Discounted Item Subtotal' => 'Rabbatert delsum for vare', + 'Discounted Items' => 'Rabatterte artikler', + 'Discounts deleted.' => 'Rabatter slettet.', + 'Discounts reordered.' => 'Rabatter har ny rekkefølge.', + 'Discounts updated.' => 'Rabatter oppdatert.', + 'Discounts' => 'Rabatter', + 'Disqualify with valid business tax ID?' => 'Diskvalifisere med gyldig ID for virksomhetsskatt?', + 'Do not apply subsequent matching sales beyond applying this sale.' => 'Benytt ikke påfølgende overensstemmende salg utover dette salget.', + 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Ikke bruk denne satsen hvis ordreadressen har noen av de valgte gyldige ID-ene for virksomhetsskatt.', + 'Do not attach a PDF to this email' => 'Ikke legg ved en PDF til denne e-posten', + 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Ikke beregn ordre på nytt (nummer: {orderNumber}) hvis det er problemer.', + 'Donation can not be zero.' => 'Donasjon kan ikke være null.', + 'Donation needs to be an amount.' => 'Donasjon må være et beløp.', + 'Donation settings saved.' => 'Innstillinger for donasjon er lagret.', + 'Donation' => 'Donasjon', + 'Donations' => 'Donasjoner', + 'Done' => 'Ferdig', + 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Ikke bruk påfølgende rabatter på en ordre hvis denne rabatten er brukt', + 'Download PDF' => 'Last ned PDF', + 'Download PDF…' => 'Last ned PDF …', + 'Download Type' => 'Type nedlastning', + 'Download' => 'Last ned', + 'Draft' => 'Utkast', + 'Dummy gateway payment failed.' => 'Dummy gateway-betaling mislyktes.', + 'Duplicate options exist' => 'Identiske valg eksisterer', + 'Duration' => 'Varighet', + 'EU VAT ID' => 'EU VAT-ID', + 'Edit address' => 'Rediger adresse', + 'Edit adjustments' => 'Rediger justeringer', + 'Edit catalog pricing rules' => 'Rediger prisregler for katalog', + 'Edit discounts' => 'Rediger rabatter', + 'Edit options' => 'Rediger valg', + 'Edit orders' => 'Rediger ordrer', + 'Edit sales' => 'Rediger salg', + 'Edit' => 'Rediger', + 'Effect' => 'Effekt', + 'Either (Default) - The relationship field is on the purchasable or the category' => 'Både (standard) – forholdsfeltet er på Kan kjøpes-artikkelen eller kategorien', + 'Either way' => 'Uansett', + 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Generering av PDF i e-post parse error for e-post «{email}». Ordre: «{order}». PDF-malfeil: «{message}» {file}:{line}', + 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'E-post PDF-mal finnes ikke på «{templatePath}» for e-post «{email}». Ordre: «{order}».', + 'Email Subject' => 'E-postens emne', + 'Email error. No email address found for order. Order: “{order}”' => 'Feil med e-post. Ingen e-postadresse funnet for bestilling. Bestilling: «{order}»', + 'Email is not enabled.' => 'E-post er ikke aktivert.', + 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'E-post-mal for tekst finnes ikke i «{templatePath}» og resulterte i «{templateParsedPath}» for e-post «{email}». Ordre: «{order}».', + 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal for tekst parse error for e-post «{email}». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', + 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmalbane for tekst parse error for e-post «{email}» i «Malbane». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', + 'Email required to make payments on a completed order.' => 'E-post påkrevet for å foreta betalinger for en fullført bestilling.', + 'Email saved.' => 'E-post lagret.', + 'Email sent' => 'E-post sendt', + 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'E-post-mal finnes ikke i «{templatePath}» og resulterte i «{templateParsedPath}» for e-post «{email}». Ordre: «{order}».', + 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal parse error for tilpasset e-post «{email}» i «Til:». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', + 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal parse error for e-post «{email}» i «BCC:». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', + 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal parse error for e-post «{email}» i «CC:». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', + 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal parse error for e-post «{email}» i «ReplyTo:». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', + 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal parse error for e-post «{email}» i «Emne:». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', + 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal parse error for e-post «{email}». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', + 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmalbane parse error for e-post «{email}» i «Malbane:». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', + 'Email unavailable.' => 'E-post utilgjengelig.', + 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'E-post «{email}» kunne ikke sendes for ordre «{order}». Error: {error} {file}:{line}', + 'Email “{email}” for order {order} was cancelled.' => 'E-post «{email}» for ordre {order} ble kansellert.', + 'Email' => 'Epost', + 'Emails' => 'E-poster', + 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Aktiver hvis denne satsen skal implementeres i den skattbare produktprisen i stedet for å legge et påslag til ordren.', + 'Enable structure for products of this type' => 'Aktiver struktur for produkter av denne typen', + 'Enable this discount' => 'Aktiver denne rabatten', + 'Enable this rule' => 'Deaktiver denne regelen', + 'Enable this sale' => 'Aktiver dette salget', + 'Enable this shipping method on the front end' => 'Aktiver denne leveringsmetoden på frontsiden', + 'Enable this shipping rule' => 'Aktiver denne fraktregelen', + 'Enable this tax rate' => 'Rediger denne skattesatsen', + 'Enabled for customers to select during checkout?' => 'Aktivert slik at kunder kan velge det under utsjekking?', + 'Enabled for customers to select?' => 'Aktivert for å kunne velges av kunder?', + 'Enabled' => 'Aktivert', + 'Enabled?' => 'Aktivert', + 'End Date' => 'Sluttdato', + 'Enter SKU' => 'Angi SKU', + 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Skriv et menneskevennlig navn for denne skattesatsen som skal brukes i kontrollpanelet.', + 'Enter a percentage like {ex1} or {ex2}.' => 'Skriv inn en prosent, som {ex1} eller {ex2}.', + 'Enter coupon code' => 'Skriv inn kupongkode', + 'Enter reference' => 'Skriv inn referanse', + 'Error refunding transaction: {transactionHash}' => 'En feil oppstod under tilbakebetalingstransaksjonen: {transactionHash}', + 'Every new store must be assigned to at least one site.' => 'Alle nye butikker må tilordnes minst ett nettsted.', + 'Everywhere' => 'Overalt', + 'Example' => 'Eksempel', + 'Exclude this discount for products that are already on promotion' => 'Ekskluder denne rabatten for produkter som allerede er med i en kampanje', + 'Expired Link' => 'Utløpt lenke', + 'Expired' => 'Utløpt', + 'Expiry Date' => 'Utløpsdato', + 'Expiry date' => 'Utløpsdato', + 'Expiry' => 'Utløpsdato', + 'Failed to receive transfer: {error}' => 'Kunne ikke hente overføring: {error}', + 'Failed to send email. Please try again.' => 'Kunne ikke sende e-post. Prøv igjen.', + 'Failed to start' => 'Kunne ikke starte', + 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Kunne ikke oppdatere {num, plural, =1{ordrestatus} other{ordrestatuser}}.', + 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Kunne ikke oppdatere ordrestatus for {num, plural, =1{ordre} other{ordrer}}.', + 'Feet (ft)' => 'Fot (ft)', + 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Filterbetingelser som beskriver hvilke ordre denne regelen gjelder. Skriv 0 for å hoppe over en betingelse.', + 'First Name' => 'Fornavn', + 'Flat Amount Off Order' => 'Flatt rabattbeløp på ordre', + 'Flat Order Discount Amount Off' => 'Beløp på flat ordrerabattsats', + 'Free Order Payment Strategy' => 'Betalingsstrategi for gratis ordre', + 'Free Shipping' => 'Gratis levering', + 'Free orders are processed by the payment gateway' => 'Gratisordrer behandles av betalingsportalen', + 'Free orders complete immediately' => 'Gratisordrer fullføres umiddelbart', + 'Free shipping can only be for whole order or matching items, not both.' => 'Gratis frakt kan bare gjelde for hele ordrer eller matchende varer, ikke begge deler.', + 'From Name' => 'Fra Navn', + 'Fulfill' => 'Fullfør', + 'Fulfilled' => 'Fullført', + 'Fulfillment' => 'Fullbyrdelse', + 'Full Name' => 'Fullt navn', + 'Gateway Code' => 'Portalkode', + 'Gateway Message' => 'Portalmelding', + 'Gateway Reference' => 'Portalreferanse', + 'Gateway Response' => 'Portalrespons', + 'Gateway doesn’t support authorize' => 'Systemport støtter ikke autorisering', + 'Gateway doesn’t support partial refunds.' => 'Portalen støtter ikke delvise tilbakebetalinger.', + 'Gateway doesn’t support purchase' => 'Portalen støtter ikke kjøp', + 'Gateway doesn’t support refunds.' => 'Portalen støtter ikke tilbakebetalinger.', + 'Gateway saved.' => 'Portal lagret.', + 'Gateway' => 'Port', + 'Gateways reordered.' => 'Portaler har ny rekkefølge.', + 'Gateways' => 'Portaler', + 'General Settings' => 'Generelle innstillinger', + 'General' => 'Generelt', + 'Generate' => 'Genrerer', + 'Generated Coupon Format' => 'Generert kupongformat', + 'Grams (g)' => 'Gram (g)', + 'Groups for which this sale will be applicable to.' => 'Grupper som dette salget gjelder for.', + 'HTML Email Template Path' => 'HTML E-postmal Vei', + 'Handle' => 'Håndter', + 'Harmonized System Code' => 'Harmonisert systemkode', + 'Has Admin Notices' => 'Har administratorvarsler', + 'Has Emails?' => 'Har e-poster?', + 'Has Free Shipping' => 'Har gratis frakt', + 'Has Orders' => 'Har ordre', + 'Has Purchasable' => 'Har Kan kjøpes-artikler', + 'Has Variants?' => 'Har varianter?', + 'Height ({unit})' => 'Høyde ({unit})', + 'Height' => 'Høyde', + 'Hide snapshot' => 'Skjul snapshot', + 'History' => 'Historikk', + 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Hvor lenge (i sekunder) en PDF-nedlastingslenke skal være gyldig før den utløper. Standard er 86400 sekunder (24 timer).', + 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Hvor mange ganger en e-postadresse har lov til å bruke denne rabatten. Dette gjelder alle tidligere bestillinger, enten for gjest eller bruker. Sett til null for ubegrenset bruk av gjester eller brukere.', + 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Antall ganger en bruker har lov til å bruke denne rabatten. Hvis dette er satt til noe annet enn null, vil rabatten kun være tilgjengelig for påloggede brukere.', + 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Antall ganger denne rabatten kan bli brukt av gjester eller påloggede brukere. Sett til null for ubegrenset bruk.', + 'How products should be labeled within the control panel.' => 'Slik skal oppføringer merkes i kontrollpanelet.', + 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'Måten kjøpbare artikler og kategorier er tilknyttet, noe som fastlegger samsvarende varer. Se [Relasjonsterminologi]({link}).', + 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'Hvordan dette produktet vil bli beskrevet på varelinjen i en bestilling. Du kan inkludere koder som utgangsegeneskaper, slik som {ex1} eller {ex2}', + 'How this shipping method will be referred to in templates and forms.' => 'Hvordan denne leveringsmetoden vil refereres til i malene og skjemaene.', + 'How variants should be labeled within the control panel.' => 'Slik skal varianter merkes i kontrollpanelet.', + 'How you’ll refer to this PDF in the templates.' => 'Måten du henviser til denne PDF-en i malen.', + 'How you’ll refer to this product type in the templates.' => 'Hvordan du henviser til denne produkttypen i malene.', + 'How you’ll refer to this shipping category in the templates.' => 'Hvordan du refererer til denne fraktkategorien i malene.', + 'How you’ll refer to this status in the templates.' => 'Hvordan du henviser til denne statusen i malene.', + 'How you’ll refer to this subscription plan in the templates.' => 'Hvordan du refererer til denne abonnementsplanen i malene.', + 'How you’ll refer to this tax category in the templates.' => 'Hvordan du henviser til denne skattekategorien i malene.', + 'ID' => 'ID', + 'IP Address' => 'IP-adresse', + 'If disabled, this PDF will not be available or sent with emails.' => 'Hvis deaktivert, vil denne PDF-en verken være tilgjengelig eller sendes med e-poster.', + 'If disabled, this email will not send.' => 'Hvis deaktivert, blir denne e-posten ikke sendt.', + 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Hvis aktivert, og denne skattesatsen ikke samsvarer med bestillingen, blir satsbeløpet fjernet fra produktprisen i handlekurven.', + 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Hvis den settes til "Kun ved autorisasjon", må du manuelt hente inn betalinger før beløpet overføres til kontoen din. Gateway må støtte det valgte alternativet.', + 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Hvis du velger at prosenten skal være «avslag på rabattert pris», vil det inkludere «Beløp per vare» i tillegg til andre rabatter som er lagt til tidligere.', + 'Ignore Promotions?' => 'Ignorere kampanjer?', + 'Ignore previous matching sales if this sale matches.' => 'Ignorere forrige samsvarende salg hvis dette salget samsvarer.', + 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignorer kampanjepriser når rabatten er brukt på samsvarende linjevarer', + 'Inactive Carts' => 'Inaktive vogner', + 'Inches (in)' => 'Tommer (")', + 'Include built-in line item tax.' => 'Inkluder innebygd linjevareavgift.', + 'Include in price?' => 'Inkluder i pris?', + 'Include line item discounts.' => 'Inkluder linjevarerabatt.', + 'Include line item shipping costs.' => 'Inkluder forsendelseskostnader for linjevare.', + 'Include separate line item tax.' => 'Inkluder separat linjevareavgift.', + 'Included in price?' => 'Inkludert i pris?', + 'Included' => 'Inkludert', + 'Incoming transfer from Transfer ID: ' => 'Innkommende overføring fra overførings-ID: ', + 'Incoming' => 'Innkommende', + 'Info' => 'Info', + 'Information linked?' => 'Informasjon lenket?', + 'Information' => 'Informasjon', + 'Invalid JSON' => 'Ugyldig JSON', + 'Invalid Order ID' => 'Ugyldig ordre-ID', + 'Invalid VAT ID.' => 'Ugyldig MVA-ID.', + 'Invalid condition syntax' => 'Ugyldig tilstandssyntaks', + 'Invalid email.' => 'Ugyldig e-post.', + 'Invalid formula syntax' => 'Ugyldig formelsyntaks', + 'Invalid gateway: {value}' => 'Ugyldig portal: {value}', + 'Invalid inventory movements.' => 'Ugyldig inventarforflytninger.', + 'Invalid order condition syntax.' => 'Ugyldig tilstandssyntaks for ordre.', + 'Invalid payment or order. Please review.' => 'Ugyldig betaling eller bestilling. Må endres.', + 'Invalid payment source ID: {value}' => 'Ugyldig betalingskilde-ID: {value}', + 'Invalid store.' => 'Ugyldig butikk.', + 'Invalid user.' => 'Ugyldig bruker.', + 'Inventory Item' => 'Lagervare', + 'Inventory Location' => 'Inventarsted', + 'Inventory Locations' => 'Inventarsteder', + 'Inventory Tracked' => 'Inventar sporet', + 'Inventory Transfers' => 'Lageroverføringer', + 'Inventory could not be set.' => 'Inventar kunne ikke angis.', + 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'Inventarsted har forpliktet lagerbeholdning, ordre(ne) må først fullføres.', + 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Inventarsted har innkommende lagerbeholdning, overføringen(e) må først fullføres.', + 'Inventory location is already deactivated.' => 'Inventarsted er allerede deaktivert.', + 'Inventory location saved.' => 'Inventarsted lagret.', + 'Inventory locations not saved.' => 'Inventarsteder ikke lagret.', + 'Inventory movement could not be saved.' => 'Inventarforflytninger kunne ikke bli lagret.', + 'Inventory movement saved.' => 'Inventarforflytninger lagret.', + 'Inventory updated.' => 'Inventar oppdatert.', + 'Inventory was not updated.' => 'Inventar ble ikke oppdatert.', + 'Inventory' => 'Inventar', + 'Invoice amount' => 'Fakturabeløp', + 'Invoice date' => 'Fakturadato', + 'Is Promotable' => 'Kan promoteres', + 'Is Promotional Price?' => 'Kampanjepris?', + 'Is Shippable' => 'Kan sendes', + 'Is Taxable' => 'Kan skattes', + 'Item Rates' => 'Varepriser', + 'Item Subtotal' => 'Delsum for vare', + 'Item Total' => 'Varetotal', + 'Item' => 'Vare', + 'Items' => 'Elementer', + 'Kilograms (kg)' => 'Kilogram (kg)', + 'Label' => 'Merkelapp', + 'Landscape' => 'Landskap', + 'Language' => 'Språk', + 'Last Name' => 'Etternavn', + 'Last Updated' => 'Sist oppdatert', + 'Leave a category rate override blank to use the rate from above.' => 'La en overstyring av kategorirate stå tom for å bruke en pris ovenfra.', + 'Leave blank for unlimited uses.' => 'La stå tomt for ubegrenset bruk.', + 'Leave blank if products don’t have URLs' => 'Skal stå tomt hvis produktene ikke har nettadresser (URL)', + 'Leave gateway subscription as-is' => 'La gateway-abonnementet være uendret', + 'Length ({unit})' => 'Lengde ({unit})', + 'Length' => 'Lengde', + 'Let each product choose which sites it should be saved to' => 'La hvert produkt velge hvilket nettsted det skal lagres til', + 'Limit which orders this discount applies to based on its line items.' => 'Begrens hvilke ordre denne rabatten gjelder for basert på linjevarer.', + 'Limit which purchasables this sale applies to.' => 'Begrens hvilke «Kan kjøpes»-varer dette salget gjelder for.', + 'Limit' => 'Grense', + 'Line Item Statuses' => 'Linjevarestatuser', + 'Line Item' => 'Linjevare', + 'Line Items' => 'Linjevarer', + 'Line item price (minus discounts)' => 'Varelinjepris (minus rabatter)', + 'Line item shipping cost' => 'Forsendelseskostnader for vare i varelinje', + 'Line item statuses reordered.' => 'Linjevarestatuser har ny rekkefølge.', + 'Link Duration' => 'Lenkens varighet', + 'Link Sent' => 'Lenke sendt', + 'Link to a product' => 'Lag lenke til et produkt', + 'Link to a variant' => 'Lag lenke til en variant', + 'Link' => 'Lenke', + 'Live' => 'Direkte', + 'Location' => 'Sted', + 'Locations that should be available for previewing products in this product type.' => 'Plasseringer som bør være tilgjengelig for forhåndsvisning av produkter av denne produkttypen.', + 'MM' => 'MM', + 'Make a payment' => 'Betal', + 'Make this the primary store' => 'Vil du gjøre dette til primærbutikk', + 'Manage Inventory' => 'Behandle inventar', + 'Manage donation settings' => 'Administrer donasjonsinnstillinger', + 'Manage general store settings' => 'Administrer generelle butikkinnstillinger', + 'Manage inventory locations' => 'Behandle inventarsteder', + 'Manage inventory stock levels' => 'Behandle inventarnivåer', + 'Manage inventory transfers' => 'Administrer lageroverføringer', + 'Manage orders' => 'Administrer ordrer', + 'Manage payment currencies' => 'Administrer betalingsvalutaer', + 'Manage promotions' => 'Administrer kampanjer', + 'Manage shipping' => 'Administrer frakt', + 'Manage store settings' => 'Administrer butikkinnstillinger', + 'Manage subscription plans' => 'Administrer abonnementsplaner', + 'Manage subscription' => 'Administrer abonnement', + 'Manage subscriptions' => 'Administrer abonnementer', + 'Manage taxes' => 'Administrer avgifter', + 'Manage' => 'Administrer', + 'Mark as Pending' => 'Merk som Venter', + 'Mark as completed' => 'Merk som ferdig', + 'Match Billing Address' => 'Match faktureringsadresse', + 'Match Customer' => 'Match kunde', + 'Match Order' => 'Match ordre', + 'Match Orders' => 'Match ordrer', + 'Match Product' => 'Match produkt', + 'Match Purchasable' => 'Sammenlign kan kjøpes-artikler', + 'Match Shipping Address' => 'Match fraktadresse', + 'Match Variant' => 'Match variant', + 'Matching Items' => 'Samsvarende varer', + 'Max Qty' => 'Maks. antall', + 'Max Uses' => 'Maks. antall brukere', + 'Max Variants' => 'Maks. varianter', + 'Max quantity must greater than min.' => 'Maksimal mengde må være større enn min.', + 'Maximum Purchase Quantity' => 'Maksimumskjøp kvantitet', + 'Maximum Total Shipping Cost' => 'Maksimal total fraktkostnad', + 'Maximum allowed quantity' => 'Maksimalt tillatt antall', + 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Maksimalt antall samsvarende varer som kan bestilles for at denne rabatten trer i kraft. Null i verdi vil her hoppe over denne betingelsen.', + 'Maximum order quantity for this item is {num}.' => 'Maksimumsantall som kan bestilles av denne artikkelen er {num}.', + 'Message' => 'Beskjed', + 'Meters (m)' => 'Meter (m)', + 'Millimeters (mm)' => 'Millimeter (mm)', + 'Min Qty' => 'Min. antall', + 'Min quantity must be less than max.' => 'Minimum mengde må være mindre enn maks.', + 'Minimum Purchase Quantity' => 'Minimumskjøp kvantitet', + 'Minimum Total Price Strategy' => 'Strategi for minimum totalpris', + 'Minimum Total Shipping Cost' => 'Minimum på total fraktkostnad', + 'Minimum allowed quantity' => 'Minimum tillatt antall', + 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Minimalt antall samsvarende varer som må bestilles for at denne rabatten skal gjelde.', + 'Minimum order quantity for this item is {num}.' => 'Minimumsantall for denne artikkelen er {num}.', + 'Missing Gateway' => 'Mangler portal', + 'Missing a default inventory location.' => 'Mangler et standard inventarsted.', + 'Move Inventory' => 'Flytt inventar', + 'Move To' => 'Flytt til', + 'Move {qty} from {fromType} to {toType}' => 'Flytt {qty} fra {fromType} til {toType}', + 'Move' => 'Flytt', + 'Movement from deactivated inventory location' => 'Forflytning fra deaktivert inventarsted', + 'Movement' => 'Forflytning', + 'Must have at least one variant.' => 'Må ha minst én variant.', + 'Name Field' => 'Navnfelt', + 'Name' => 'Navn', + 'New Customer' => 'Nyt kunde', + 'New Customers' => 'Nye kunder', + 'New Order' => 'Ny ordre', + 'New PDF' => 'Ny PDF', + 'New address' => 'Ny adresse', + 'New catalog pricing rule' => 'Ny prisregler for katalog', + 'New currency' => 'Ny valuta', + 'New discount' => 'Ny rabatt', + 'New email' => 'Ny e-post', + 'New gateway' => 'Ny portal', + 'New line item status' => 'Ny linjevarestatus', + 'New line items get this status by default when the order is completed' => 'Ny linjevarer får denne statusen som standard når ordren er fullført', + 'New location' => 'Nytt sted', + 'New order status' => 'Ny bestillingsstatus', + 'New orders get this status by default' => 'Nye bestillinger får denne statusen som standard', + 'New product type' => 'Ny betalingsmetode', + 'New product' => 'Nytt produkt', + 'New product, choose a type' => 'Nytt produkt, velg type', + 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Nye produkter oppføres som standard med den første tilgjengelige skattekategorien. Hvis ingen er tilgjengelige, brukes denne kategorien.', + 'New sale' => 'Nytt salg', + 'New shipping category' => 'Ny fraktkategori', + 'New shipping method' => 'Ny fraktmetode', + 'New shipping rule' => 'Ny fraktregel', + 'New shipping zone' => 'Ny fraktsone', + 'New subscription plan' => 'Ny abonnementsplan', + 'New tax category' => 'Ny skattekategori', + 'New tax rate' => 'Ny skattehyppighet', + 'New tax zone' => 'Ny skattesone', + 'New transfer' => 'Ny overføring', + 'New {productType} product' => 'Nytt {productType}-produkt', + 'New' => 'Ny', + 'Next payment' => 'Neste betaling', + 'No Address' => 'Ingen adresse', + 'No PDFs exist yet.' => 'Ingen PDF-er eksisterer ennå.', + 'No access given to any specific store management features.' => 'Ingen tilgang gitt til noen spesifikke butikkadministrasjonsfunksjoner.', + 'No additional payment currencies exist yet.' => 'Ingen ekstra betalingsvaluta finnes ennå', + 'No address' => 'Ingen adresse', + 'No billing address' => 'Ingen faktureringsadresse', + 'No catalog pricing rule exists with the ID “{id}”' => 'Ingen prisregel for katalog eksisterer med ID-en «{id}»', + 'No catalog pricing rules exist yet.' => 'Ingen prisregler for katalog eksisterer ennå.', + 'No currency exists with the ID “{id}”' => 'Ingen valuta finnes med denne ID-en «{id}»', + 'No customer email address exists on this cart.' => 'Det eksisterer ingen kunde-e-postadresse på denne handlekurven.', + 'No description' => 'Ingen beskrivelse', + 'No discount exists with the ID “{id}”' => 'Ingen rabatt eksisterer med ID-en «{id}»', + 'No discounts exist yet.' => 'Ingen rabatter finnes ennå.', + 'No donation amount supplied.' => 'Ingen donasjonsbeløp levert.', + 'No emails exist yet.' => 'Ingen e-poster finnes ennå.', + 'No inventory changes made.' => 'Ingen inventarendringer gjort.', + 'No inventory found.' => 'Ingen inventar funnet.', + 'No inventory movements made.' => 'Ingen inventarforflytninger gjort.', + 'No inventory transactions for this location.' => 'Ingen inventaroverføringer for dette stedet.', + 'No new customer selected.' => 'Ingen ny kunde valgt.', + 'No order history exists with the ID “{id}”' => 'Ingen ordrehistorikk eksisterer med ID-en «{id}»', + 'No order status history items will exist until the cart becomes an order.' => 'Ingen historikk-elementer for ordrestatus eksisterer før handlekurven blir en ordre.', + 'No payment source exists with the ID “{id}”' => 'Det finnes ingen betalingskilde med ID-en «{id}»', + 'No private Note.' => 'Ingen privat merknad.', + 'No product available.' => 'Ingen produkt tilgjengelig.', + 'No product types exist yet.' => 'Ingen produkttyper finnes ennå.', + 'No purchasable available.' => 'Ingen Kan kjøpes-artikler tilgjengelig.', + 'No sale exists with the ID “{id}”' => 'Ingen salg eksisterer med ID-en «{id}»', + 'No sales exist yet.' => 'Ingen salg finnes ennå.', + 'No shipping address' => 'Ingen fraktadresse', + 'No shipping category exists with the ID “{id}”' => 'Ingen fraktkategori finnes med den ID-en «{id}»', + 'No shipping method exists with the ID “{id}”' => 'Ingen fraktmetode eksisterer med ID-en «{id}»', + 'No shipping rule exists with the ID “{id}”' => 'Ingen fraktregel eksisterer med ID-en «{id}»', + 'No shipping rules exist yet.' => 'Ingen fraktregler finnes ennå.', + 'No shipping zone exists with the ID “{id}”' => 'Ingen fraktsone eksisterer med ID-en «{id}»', + 'No stats available.' => 'Ingen statistikk tilgjengelig.', + 'No subscription plan exists with the ID “{id}”' => 'Det finnes ingen abonnementsplan med ID-en «{id}»', + 'No subscription plans exist yet.' => 'Det finnes ingen abonnementsplan ennå.', + 'No tax category exists with the ID “{id}”' => 'Ingen skattekategori eksisterer med ID-en «{id}»', + 'No tax rate exists with the ID “{id}”' => 'Ingen skattesatsen eksisterer med ID-en «{id}»', + 'No tax zone exists with the ID “{id}”' => 'Ingen skattesone eksisterer med ID-en «{id}»', + 'No transactions exist.' => 'Ingen transaksjoner eksisterer.', + 'No user authenticated.' => 'Ingen bruker autentisert.', + 'No' => 'Nei', + 'None on hand' => 'Ingen for hånden', + 'None' => 'Ingen', + 'Not a valid address type' => 'Ikke en gyldig adressetype', + 'Not a valid credit card number.' => 'Ikke et gyldig kredittkortnummer.', + 'Not all SKUs are unique.' => 'Ikke alle produktkoder (SKU) er unike.', + 'Note' => 'Merk', + 'Notes' => 'Merknader', + 'Number of Coupons' => 'Antall kuponger', + 'Number' => 'Nummer', + 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Av de aktiverte sidene ovenfor, til hvilke sider skal produktene i denne produkttypen lagres?', + 'On Hand' => 'Tilgjengelig', + 'Only allow this gateway to be used for zero value orders?' => 'Kun tillate at denne portalen brukes for nullverdi-bestillinger?', + 'Only match certain purchasables…' => 'Kombiner bare bestemte «Kan kjøpes»-artikler …', + 'Only match purchasables related to…' => 'Kombiner bare «Kan kjøpes»-artikler knyttet til …', + 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Kun ordrer med følgende ordrestatuser vil bli inkludert. La stå tomt for å inkludere alle statuser.', + 'Only save product to the site they were created in' => 'Lagre produkter kun til siden de ble laget på', + 'Options' => 'Valg', + 'Order Condition Formula' => 'Formel for ordretilstand', + 'Order Description Format' => 'Ordrebeskrivelse format', + 'Order Details' => 'Ordredetaljer', + 'Order Fields' => 'Bestillingsfelt', + 'Order PDF Download Link' => 'Nedlastingslenke for PDF-ordre', + 'Order PDF Filename Format' => 'Bestillings PDF-filnavn format', + 'Order Reference Number Format' => 'Format for bestillingsreferanse', + 'Order Settings' => 'Ordreinnstillinger', + 'Order Site' => 'Ordreside', + 'Order Status description.' => 'Beskrivelse av ordrestatus.', + 'Order Status' => 'Ordrestatus', + 'Order Statuses' => 'Bestillingsstatuser', + 'Order can not be empty.' => 'Ordre kan ikke stå tomt.', + 'Order count' => 'Antall ordre', + 'Order customer data removed.' => 'Kundedata bestilt fjernet.', + 'Order deleted.' => 'Ordre slettet.', + 'Order fields saved.' => 'Ordrefelt lagret.', + 'Order not found.' => 'Ordre ikke funnet.', + 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Saldoen for ordren er {outstandingBalanceAsCurrency}. Dette er det høyeste beløpet som vil bli belastet.', + 'Order recalculated.' => 'Ordre beregnet på nytt.', + 'Order status saved.' => 'Ordrestatus lagret.', + 'Order statuses reordered.' => 'Ordrestatuser har ny rekkefølge.', + 'Order total shipping cost' => 'Total forsendelseskostnad for ordren', + 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Bestillingens totale avgiftspliktige pris (linjepris delsum + totale rabatter + totale forsendelseskostnader)', + 'Order' => 'Ordre', + 'Orders (Legacy)' => 'Ordrer (eldre)', + 'Orders deleted.' => 'Ordrer slettet.', + 'Orders not restored.' => 'Ordre ble ikke gjenopprettet.', + 'Orders restored.' => 'Bestillinger gjenopprettet.', + 'Orders' => 'Ordre', + 'Organization Name' => 'Organisasjonsnavn', + 'Organization Tax ID' => 'Skatte-ID for organisasjon', + 'Origin and destination cannot be the same.' => 'Opprinnelse og destinasjon kan ikke være det samme.', + 'Origin' => 'Opprinnelse', + 'Original Price' => 'Opprinnelig pris', + 'Original price' => 'Opprinnelig pris', + 'Original promotional price' => 'Opprinnelig kampanjepris', + 'Other Languages' => 'Andre språk', + 'Other countries' => 'Andre land', + 'Outgoing transfer from Transfer ID: ' => 'Utgående overføring fra overførings-ID: ', + 'Overpaid' => 'Overbetalt', + 'Overrides previous?' => 'Overstyrer forrige?', + 'PDF Attachment' => 'PDF-vedlegg', + 'PDF Template Path' => 'Bane for PDF-mal', + 'PDF saved.' => 'PDF lagret.', + 'PDF' => 'PDF', + 'PDFs & Emails' => 'PDF-er og e-poster', + 'PDFs' => 'PDF-er', + 'Paid Amount' => 'Betalt beløp', + 'Paid Status' => 'Betalingsstatus', + 'Paid' => 'Betalt', + 'Paper Orientation' => 'Papirretning', + 'Paper Size' => 'Papirstørrelse', + 'Partial payment not allowed.' => 'Delbetaling ikke tillatt.', + 'Partial' => 'Delvis', + 'Past year' => 'Siste år', + 'Past {num} days' => 'Siste {num} dager', + 'Pay {amount} of {currency} on the order.' => 'Betal {amount} i {currency} på ordren.', + 'Pay' => 'Betal', + 'Payment Amount' => 'Betalingssum', + 'Payment Currencies' => 'Betalingsvalutaer', + 'Payment Gateway' => 'Betalingsportal', + 'Payment Method' => 'Betalingsmetode', + 'Payment error: {message}' => 'Betalingsfeil: {message}', + 'Payment method issue' => 'Problem med betalingsmetode', + 'Payment source created.' => 'Betalingskilde opprettet.', + 'Payment source deleted.' => 'Betalingskilde slettet.', + 'Payments' => 'Betalinger', + 'Pending' => 'Venter', + 'Per Email Address Discount Limit' => 'Rabattgrense per e-postadresse', + 'Per Item Amount Off' => 'Rabattbeløp per vare', + 'Per Item Discount' => 'Rabatt per enhet', + 'Per Item Percentage Off' => 'Rabatt per vare', + 'Per Item Rate' => 'Enhetspris', + 'Per User Discount Limit' => 'Rabattgrense per bruker', + 'Percentage Rate' => 'Prosentpris', + 'Phone (Alt)' => 'Telefonnummer (alt.)', + 'Phone' => 'Telefon', + 'Pick a plan' => 'Velg en plan', + 'Plain Text Email Template Path' => 'Malbane for tekst-e-post', + 'Plan' => 'Plan', + 'Plans reordered.' => 'Planer har ny rekkefølge.', + 'Portrait' => 'Portrett', + 'Post Date' => 'Postdato', + 'Postal Code Formula' => 'Postnummer-formel', + 'Pounds (lb)' => 'Pund (lb)', + 'Preview' => 'Forhåndsvis', + 'Previous Status' => 'Tidligere status', + 'Price' => 'Pris', + 'Prices' => 'Priser', + 'Pricing Rules' => 'Prisregler', + 'Pricing jobs are currently running.' => 'Prisjobber pågår.', + 'Pricing' => 'Prissetting', + 'Primary Billing Address' => 'Primær faktureringsadresse', + 'Primary Shipping Address' => 'Primær fraktadresse', + 'Primary payment source updated.' => 'Primær betalingskilde oppdatert.', + 'Primary' => 'Primær', + 'Private Note' => 'Privat merknad', + 'Product Fields' => 'Produktfelt', + 'Product ID is required.' => 'Produkt-ID nødvendig.', + 'Product Template' => 'Produktmal', + 'Product Title Format' => 'Tittelformat for produkt', + 'Product Type' => 'Produkttype', + 'Product Types' => 'Produkttyper', + 'Product URI Format' => 'URI-format for produkt', + 'Product Variant' => 'Produktvariant', + 'Product Variants' => 'Produktvarianter', + 'Product type saved.' => 'Produkttype lagret.', + 'Product type settings' => 'Innstillinger for produkttype', + 'Product' => 'Produkt', + 'Products and Variants deleted.' => 'Produkter og varianter slettet.', + 'Products not restored.' => 'Produkter ble ikke gjenopprettet.', + 'Products restored.' => 'Produkter gjenopprettet.', + 'Products' => 'Produkter', + 'Promotable' => 'Promoterbar', + 'Promotable?' => 'Promoterbar?', + 'Promotional Amount' => 'Kampanjebeløp', + 'Promotional Price' => 'Kampanjepris', + 'Purchasable Categories' => 'Kategorier som kan kjøpes', + 'Purchasable ID and Sale ID are required.' => 'Kjøpbar- og salg-ID nødvendig.', + 'Purchasable ID is required.' => 'Kjøpbar-ID nødvendig.', + 'Purchasable Type' => 'Typer som kan kjøpes', + 'Purchasable' => 'Kan kjøpes', + 'Purchase (Authorize and Capture Immediately)' => 'Kjøp (autoriser og fang umiddelbart)', + 'Purchase Total' => 'Kjøpstotal', + 'Qty' => 'Ant.', + 'Quality Control' => 'Kvalitetskontroll', + 'Quantity' => 'Kvantitet', + 'Rate' => 'Pris', + 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Tildel {numOrders, plural, =1{ordre} other{ordrer}}', + 'Recalculate order' => 'Beregn ordre på nytt', + 'Receive Inventory' => 'Motta lager', + 'Receive Transfer' => 'Motta overføring', + 'Receive' => 'Motta', + 'Received' => 'Mottatt', + 'Recent Orders' => 'Nylige ordrer', + 'Recipient' => 'Mottaker', + 'Recover Cart' => 'Gjenopprett handlekurv', + 'Reduce price' => 'Reduser pris', + 'Reduce the price by a fixed amount' => 'Reduser pris med et fast beløp', + 'Reduce the price by a percentage of the original price' => 'Reduser prisen med en prosentdel av den opprinnelige prisen', + 'Reference' => 'Referanse', + 'Refresh payment history' => 'Oppdater betalingshistorikk', + 'Refund note' => 'Merknad til tilbakebetaling', + 'Refund payment' => 'Refunder betaling', + 'Refund' => 'Refusjon', + 'Reject' => 'Avslå', + 'Rejected' => 'Avslått', + 'Relationship Type' => 'Forholdstype', + 'Removable included tax rates are only allowed for the default tax zone.' => 'Inkluderte skatter og avgifter som kan fjernes, er kun tillatt for standard skattesone.', + 'Remove address' => 'Fjern adresse', + 'Remove all shipping costs from the order' => 'Fjern alle fraktkostnader fra bestillingen', + 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Fjern kundetilknytning og e-post fra {numOrders, plural, =1{ordre} other{ordrer}}. Valgfritt: velg ytterligere kundedata som skal fjernes nedenfor', + 'Remove customer data' => 'Fjern kundedata', + 'Remove from price?' => 'Fjern fra pris?', + 'Remove shipping costs for matching items only' => 'Fjern fraktkostnader kun for identiske varer', + 'Remove the included tax when a valid organization tax ID is present?' => 'Fjerne inkludert skatt når det er en gyldig organisasjonsskatt?', + 'Remove' => 'Fjern', + 'Removed' => 'Fjernet', + 'Repeat Customers' => 'Tilbakevendende kunder', + 'Reply To' => 'Svar til', + 'Require Billing Address At Checkout' => 'Krev faktureringsadresse ved bestilling', + 'Require Coupon Code' => 'Krever kupongkode', + 'Require Shipping Address At Checkout' => 'Krev fraktadresse ved bestilling', + 'Require Shipping Method Selection At Checkout' => 'Krev valg av leveringsmetode ved bestilling', + 'Require' => 'Krever', + 'Reserved' => 'Reservert', + 'Reset usage' => 'Tilbakestill bruk', + 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Begrens rabatten til kun bestillinger hvor kunden har kjøpt produkter til et minimumsbeløp med matchende varer.', + 'Revenue Options' => 'Alternativer for inntekt', + 'Revenue' => 'Inntekt', + 'Rule' => 'Regel', + 'Rules reordered.' => 'Regler har ny rekkefølge.', + 'SKU' => 'SKU', + 'Safety' => 'Sikkerhet', + 'Sale Price' => 'Salgspris', + 'Sale description.' => 'Salgsbeskrivelse.', + 'Sale reordered.' => 'Salg har ny rekkefølge.', + 'Sale saved.' => 'Salg lagret.', + 'Sale' => 'Salg', + 'Sales deleted.' => 'Salg slettet.', + 'Sales updated.' => 'Salg er oppdatert.', + 'Sales' => 'Salg', + 'Save and continue editing' => 'Lagre og fortsett redigering', + 'Save and return to all orders' => 'Lagre og gå tilbake til alle ordre', + 'Save and set rules' => 'Lagre og fastsett regler', + 'Save as a new rule' => 'Lagre som en ny regel', + 'Save product to all sites enabled for this product type' => 'Lagre produkt på alle sider som er aktivert for denne produkttypen', + 'Save product to other sites in the same site group' => 'Lagre produkt på andre sider i samme sidegruppe', + 'Save product to other sites with the same language' => 'Lagre produkt på andre sider med samme språk', + 'Save' => 'Lagre', + 'Search customer…' => 'Søk på kunde…', + 'Search inventory' => 'Søk i inventar', + 'Search or enter customer email…' => 'Søk eller skriv inn e-post til kunden …', + 'Search…' => 'Søk…', + 'See Orders' => 'Se ordre', + 'Select a gateway' => 'Velg en ny portal', + 'Select a tax category.' => 'Velg en skattekategori.', + 'Select a tax zone. If empty, this rate will match anywhere.' => 'Velg en skattesone. Hvis tomt, vil satsen samsvare med hvor som helst.', + 'Select address' => 'Velg adresse', + 'Select an item' => 'Velg et element', + 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Velg hvordan prisregel for katalog skal anvendes på varen(e).', + 'Select how the sale will be applied to the purchasable(s).' => 'Velg hvordan salget skal anvendes på varen(e).', + 'Select product type' => 'Velg produkttype', + 'Select the emails that will be sent when transitioning to this status.' => 'Selv e-postene som vil sendes ved overgang til denne statusen.', + 'Select what this rate should be applied to.' => 'Velg hva denne satsen skal gjelde for.', + 'Send Email' => 'Send e-post', + 'Send to custom recipient' => 'Send til tilpasset mottaker', + 'Send to the customer' => 'Send til kunden', + 'Set Quantity' => 'Angi antall', + 'Set default category' => 'Angi standardkategori', + 'Set default variant' => 'Angi standardvariant', + 'Set or Adjust' => 'Angi eller juster', + 'Set price' => 'Angi pris', + 'Set status' => 'Sett status', + 'Set the price to a flat amount' => 'Sett prisen til et flatt beløp', + 'Set the price to a percentage of the original price' => 'Sett prisen til en prosentdel av den opprinnelige prisen', + 'Set the sale price to a flat amount' => 'Angi salgsprisen til et flatt beløp', + 'Set the sale price to a percentage of the original price' => 'Angi salgsprisen til en prosentdel av den opprinnelige prisen', + 'Set to' => 'Angi som', + 'Settings saved.' => 'Innstillinger lagret.', + 'Settings' => 'Innstillinger', + 'Share cart…' => 'Del handlekurv …', + 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Frakt – minimumskostnad er fraktkostnaden, hvis ordreprisen er mindre enn fraktkostnaden.', + 'Shipping Address Zone' => 'Fraktadressesone', + 'Shipping Address' => 'Fraktadresse', + 'Shipping Business Name' => 'Frakt firmanavn', + 'Shipping Categories' => 'Fraktkategorier', + 'Shipping Category Conditions' => 'Betingelser for fraktkategori', + 'Shipping Category' => 'Fraktkategori', + 'Shipping First Name' => 'Frakt fornavn', + 'Shipping Full Name' => 'Frakt fullt navn', + 'Shipping Last Name' => 'Frakt etternavn', + 'Shipping Method' => 'Fraktmetode', + 'Shipping Methods' => 'Leveringsmetoder', + 'Shipping Rule' => 'Fraktregel', + 'Shipping Zones' => 'Fraktsoner', + 'Shipping address required.' => 'Forsendelsesadresse nødvendig.', + 'Shipping categories deleted.' => 'Fraktkategorier slettet.', + 'Shipping category saved.' => 'Fraktkategori er lagret.', + 'Shipping category updated.' => 'Fraktkategori oppdatert.', + 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Fraktkostnader lagt til ordren som en helhet før prosent-, vare-, og vektsatser blir brukt. Sett til null for å deaktivere denne satsen. Hele regelen, inkludert denne grunnsatsen, vil ikke samsvare og gjelde hvis handlekurven inneholder elementer som ikke kan sendes, for eksempel digitale produkter.', + 'Shipping method saved.' => 'Fraktmetode lagret.', + 'Shipping methods and rules deleted.' => 'Fraktmetoder og regler slettet.', + 'Shipping methods updated.' => 'Leveringsmetoder oppdatert.', + 'Shipping rule saved.' => 'Fraktregel lagret.', + 'Shipping zone saved.' => 'Fraktsone lagret.', + 'Shipping' => 'Levering', + 'Short Number' => 'Kort nummer', + 'Show Chart?' => 'Vise handlekurv?', + 'Show Order Count?' => 'Vise antall ordre?', + 'Show all prices' => 'Vis alle priser', + 'Show archived gateways' => 'Vis arkiverte portaler', + 'Show order count line on chart.' => 'Vis linje for antall ordre i tabell.', + 'Show related sales' => 'Vis relaterte salg', + 'Show rule details' => 'Vis detaljer om regel', + 'Show the Dimensions and Weight fields for products of this type' => 'Vis mål- og vektfelt for slike produkter', + 'Show the Title field for products' => 'Vis tittelfeltet for produkter', + 'Show the Title field for variants' => 'Vis tittelfeltet for varianter', + 'Signed In' => 'Logget inn', + 'Site Languages' => 'Nettsidespråk', + 'Site store mapping saved.' => 'Kartleggingen av butikk lagret.', + 'Sites' => 'Nettsteder', + 'Slug' => 'Lenke', + 'Snapshot' => 'Snapshot', + 'Snapshots' => 'Snapshots', + 'Some orders restored.' => 'Enkelte ordrer ble gjenopprettet.', + 'Some products restored.' => 'Enkelte produkter ble gjenopprettet.', + 'Some variants restored.' => 'Noen varianter ble gjenopprettet.', + 'Something changed with the order before payment, please review your order and submit payment again.' => 'Noe endret seg ved bestillingen før betaling, vennligst se over bestillingen og send betalingen igjen.', + 'Sorry, no matching options.' => 'Beklager, ingen tilsvarende valg.', + 'Source - The purchasable relationship field is on the category' => 'Kile – forholdsfeltet for Kan kjøpes-artikler er på kategorien', + 'Source' => 'Kilde', + 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Spesifiser en Twig-tilstand som bestemmer om rabatten skal anvendes på en gitt ordre. (Ordren kan henvises til via en `order`-variabel.)', + 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Spesifiser en Twig-tilstand som bestemmer om fraktregelen skal anvendes på en gitt ordre. (Ordren kan henvises til via en `order`-variabel.)', + 'Start Date' => 'Startdato', + 'State' => 'Stat', + 'Status Email Address' => 'E-postadresse-status', + 'Status Emails' => 'E-postadresse-statuser', + 'Status History' => 'Statushistorikk', + 'Status Updated.' => 'Status oppdatert.', + 'Status change message' => 'Melding ved statusendring', + 'Status' => 'Status', + 'Stock' => 'Beholdning', + 'Stops Processing?' => 'Stoppe behandling?', + 'Stops subsequent?' => 'Stopper påfølgende?', + 'Store Location' => 'Stedet for butikk', + 'Store Management' => 'Butikkadministrasjon', + 'Store Markets' => 'Butikkmarkeder', + 'Store Rule' => 'Butikkregel', + 'Store saved.' => 'Butikk lagret.', + 'Store' => 'Butikk', + 'Stores & Sites' => 'Butikker og nettsteder', + 'Stores' => 'Butikker', + 'Strategy to apply when an order is free or has a zero balance.' => 'Strategien som skal brukes, når en ordre er gratis eller har en saldo som står i null.', + 'Strategy to apply when calculating the minimum order price.' => 'Strategi å bruke når du beregner minimumspris for ordre.', + 'Subject' => 'Emne', + 'Subscribing user' => 'Abonnerende bruker', + 'Subscription Fields' => 'Abonnementsfelt', + 'Subscription Plans' => 'Abonnementsplaner', + 'Subscription Settings' => 'Abonnementsinnstillinger', + 'Subscription cancelled.' => 'Abonnement kansellert.', + 'Subscription date' => 'Abonnementsdato', + 'Subscription fields saved.' => 'Abonnementsfelt er lagret.', + 'Subscription for {user} to {plan} prevented by a plugin.' => 'Abonnement for {user} til {plan} forhindret av en programutvidelse.', + 'Subscription plan saved.' => 'Abonnementsplan er lagret.', + 'Subscription plan' => 'Abonnementsplan', + 'Subscription plans' => 'Abonnementsplaner', + 'Subscription reactivated.' => 'Abonnement reaktivert.', + 'Subscription reference' => 'Referanse for abonnement', + 'Subscription started.' => 'Abonnement startet.', + 'Subscription switched.' => 'Abonnement byttet.', + 'Subscription to “{plan}”' => 'Abonnement på «{plan}»', + 'Subscription' => 'Abonnement', + 'Subscriptions on hold' => 'Abonnement på vent', + 'Subscriptions' => 'Abonnementer', + 'Suppress emails' => 'Undertrykk e-poster', + 'Switch plan' => 'Bytte plan', + 'Switch' => 'Bytte', + 'System' => 'System', + 'Table Columns' => 'Tabellkolonner', + 'Target - The category relationship field is on the purchasable' => 'Mål – feltet for kategoriforhold er på Kan kjøpes-artikkelen', + 'Tax & Shipping' => 'Avgift og frakt', + 'Tax (inc)' => 'Avgift (inkludert)', + 'Tax Categories' => 'Skattekategorier', + 'Tax Category' => 'Skattekategori', + 'Tax Rates' => 'Skatteavgifter', + 'Tax Zone' => 'Skattesone', + 'Tax Zones' => 'Skattesoner', + 'Tax categories deleted.' => 'Skattekategorier slettet.', + 'Tax category saved.' => 'Skattekategori lagret.', + 'Tax category updated.' => 'Avgiftskategori oppdatert.', + 'Tax rate saved.' => 'Skattesats lagret.', + 'Tax rates updated.' => 'Skattesatser ble oppdatert.', + 'Tax zone saved.' => 'Skattesone lagret.', + 'Tax' => 'Avgift', + 'Taxable Subject' => 'Skattepliktig enhet', + 'Template Path' => 'Mal-vei', + 'That handle is already in use' => 'Etiketten er allerede i bruk', + 'That handle is already in use.' => 'Etiketten er allerede i bruk.', + 'The PDF to attach to this email.' => 'PDF-en som skal legges til e-posten.', + 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'Lenken til sidenfor oppdatering av faktureringsinformasjon for et abonnement, samt administrering av 3DS-autentisering.', + 'The address provided is outside the store’s market.' => 'Adressen som er oppgitt er utenfor butikkens marked.', + 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'Rabattbeløpet som er brukt på hele ordren. Beløpet er spredt over linjevarene i rekkefølgen fra høyest til lavest pris til rabatten er brukt opp.', + 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'Grunnrabatten gjelder bare for varer i handlekurven ned til null inntil den er brukt opp. Ordren kan ikke bli negativ.', + 'The cart recovery link is invalid. Please request a new one.' => 'Lenken for gjenoppretting av handlekurv er ugyldig. Be om en ny.', + 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Konverteringskursen som vil bli brukt når man konverterer en sum til denne valutaen. For eksempel, hvis en vare koster {amount1}, vil en konverteringskurs på {rate} resultere i {amount2} i den alternative valutaen.', + 'The countries that orders are allowed to be placed from.' => 'Landene det er tillatt å legge inn bestillinger fra.', + 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'Kupongen «{code}» har overskredet bruksgrensen på {limit}.', + 'The customer for this order has been deleted.' => 'Kunden for denne ordren ble slettet.', + 'The default shipping category is automatically available to all product types.' => 'Standard fraktkategori er automatisk tilgjengelig for alle produkttyper.', + 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'Rabatten «{name}» har overskredet bruksgrensen på {limit}.', + 'The download link has expired. Please request a new one.' => 'Nedlastingslenken er utløpt. Be om en ny.', + 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'E-postadressen som e-poster med bestillingsstatus sendes fra. La stå tomt for å bruke systemets e-postadresse som er definert i Crafts generelle innstillinger.', + 'The entry that contains the description for this subscription’s plan.' => 'Oppføringen som inneholder beskrivelse for denne abonnementsplanen.', + 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'Den flate verdien som gir avslag på hver vare, f.eks. «3» for $3 avslag på hver vare.', + 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'Formatet som brukes til å generere nye kuponger, f.eks. {example}. Alle \'#\'-tegn blir byttet ut med en tilfeldig bokstav.', + 'The from and to inventory locations must be different.' => 'Inventarstedene til og fra må være forskjellige.', + 'The inventory locations this store uses.' => 'Inventarstedene denne butikken bruker.', + 'The item is not enabled for sale.' => 'Denne artikkelen er ikke aktivert for salg.', + 'The language the order was made in.' => 'Språket som ordren ble laget i.', + 'The language to be used when this email is rendered.' => 'Språk som skal brukes i e-posten som genereres.', + 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Maksimalt antall nivåer tillatt i produkttypen. La stå tomt hvis det ikke spiller noen rolle.', + 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Det maksimale en kunde skal betale i frakt. Sett til null for å deaktivere.', + 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Det minste kunden skal betale i frakt. Sett til null for å deaktivere.', + 'The order is not valid.' => 'Ugyldig ordre.', + 'The payment gateway that will be used for the subscription plan.' => 'Hvilken betalingsportal skal brukes for abonnementsplanen.', + 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'Den prosentvise verdien som skal brukes som rabatt på hver vare. F.eks. {ex1} for en rabatt på {ex2}. Prosenten rundes av til to desimaler.', + 'The previously-selected shipping method is no longer available.' => 'Tidligere valgt leveringsmetode er ikke lenger tilgjengelig.', + 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Prisen på {description} har økt fra {originalSalePriceAsCurrency} til {newSalePriceAsCurrency}', + 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Prisen på {description} har blitt redusert fra {originalSalePriceAsCurrency} til {newSalePriceAsCurrency}', + 'The primary currency cannot be changed after orders are placed.' => 'Hovedvalutaen kan ikke endres etter at ordren er sendt inn.', + 'The purchasable defines the relationship' => '«Kan kjøpes»-artikkelen definerer forholdet', + 'The purchasable is related by another element' => '«Kan kjøpes»-artikkelen er relatert til et annet element', + 'The recipient of the email. Twig code can be used here.' => 'Mottakeren av e-posten. Twig-kode kan brukes.', + 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'E-postadresser som svar skal sendes til. La stå tomt for vanlig "svar til" fra e-postavsender. Twig-kode kan brukes.', + 'The site the order was made in.' => 'Nettstedet som ordren ble laget på.', + 'The site to be used when this email is rendered.' => 'Nettstedet som skal brukes i e-posten som genereres.', + 'The subject line of the email. Twig code can be used here.' => 'Emnefeltet til e-posten. Twig-kode kan brukes.', + 'The template that the PDF should be generated from.' => 'Malen som PDF-en skal genereres fra.', + 'The template to be used for HTML emails.' => 'Malen som skal brukes for HTML-e-poster.', + 'The template to be used for plain text emails. Twig code can be used here.' => 'Malen som skal brukes for enkle e-poster med tekst. Twig-kode kan brukes.', + 'The template to use when a product’s URL is requested.' => 'Malen som skal brukes når URL-en til et produkt blir forespurt om.', + 'The total number of order adjustments changed.' => 'Det totale antallet ordrejusteringer er endret.', + 'The total price of the order changed.' => 'Totalprisen på bestillingen er endret.', + 'The total quantity of items within the order changed.' => 'Den totale størrelsen på varer i ordren er endret.', + 'The unique SKU of the donation purchasable.' => 'Den unike SKU-en for donasjonen som kan kjøpes.', + 'The unit of measurement that should be used when specifying product dimensions.' => 'Måleenheten som skal brukes ved spesifikasjon av produktmål.', + 'The unit of measurement that should be used when specifying product weights.' => 'Måleenheten som skal brukes ved spesifikasjon av produktets vekt.', + 'The webhook URL for this gateway.' => 'Webhook-URL for gatewayen.', + 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => '“Fra”-navnet som skal brukes ved sending av e-poster med bestillingsstatuser. La stå tomt for å bruke avsendernavnet som er definert i Crafts generelle innstillinger.', + 'There are errors on the order' => 'Det er feil i ordren', + 'There are only {num} “{description}” items left in stock.' => 'Det er kun {num} «{description}»-artikler igjen på lager.', + 'There aren’t any product types to select yet.' => 'Det er ingen produkttyper å velge ennå.', + 'There is no gateway or payment source available for use with this order.' => 'Det er ingen portal eller betalingskilde tilgjengelig for denne ordren.', + 'There is no gateway selected that supports payment sources.' => 'Det er ikke valgt noen portal som støtter betalingskilder.', + 'There is no shipping method selected for this order.' => 'Det er ikke valgt noen leveringsmetode for denne ordren.', + 'This URL will load the cart into the user’s session, making it the active cart.' => 'Denne URL-en laster handlekurven inn i brukerens økt, slik at den blir den aktive handlekurven.', + 'This action is not allowed for the current user.' => 'Denne handlingen er ikke tillatt for nåværende bruker.', + 'This category will be used as the default for all purchasables in this store.' => 'Denne kategorien vil bli brukt som standard for alle varer som kan kjøpes i denne butikken.', + 'This coupon is for registered users and limited to {limit} uses.' => 'Denne kupongen er for registrerte brukere og begrenset til {limit} bruk.', + 'This coupon is limited to {limit} uses.' => 'Denne kupongen er begrenset til {limit} bruk.', + 'This coupon requires an email address.' => 'En e-postadresse er nødvendig for å bruke kupongen.', + 'This gateway does not support that functionality.' => 'Denne portalen støtter ikke den funksjonaliteten.', + 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Dette bli overstyrt av konfigurasjonsinnstillingen {setting} i `config/{file}.php`.', + 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Dette er adressen hvor butikken din ligger. Det kan bli brukt av ulike programutvidelser for å bestemme ting som frakt og skatt. Den kan også brukes i PDF-kvitteringer.', + 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Dette er standard-PDF-en som lages nårman henter fram ordre-PDF-en.', + 'This is the last location for the {store} store.' => 'Dette er siste sted for butikken {store}.', + 'This month' => 'Denne måneden', + 'This order has unsaved changes.' => 'Ordren har endringer som ikke er lagret.', + 'This week' => 'Denne uken', + 'This year' => 'Dette året', + 'Times Used' => 'Tid brukt', + 'Title' => 'Tittel', + 'To' => 'Til', + 'Today' => 'I dag', + 'Too many variants for this product.' => 'For mange varianter for dette produktet.', + 'Top Customers by Average Order' => 'Topp kunder etter gjennomsnittssordre', + 'Top Customers by Total Revenue' => 'Topp kunder etter total inntekt', + 'Top Customers' => 'Topp kunder', + 'Top Product Types by Qty Sold' => 'Topp produkttyper etter antall solgt', + 'Top Product Types by Revenue' => 'Topp produkttyper etter inntekt', + 'Top Product Types' => 'Topp produkttyper', + 'Top Products by Qty Sold' => 'Topp produkter etter antall solgt', + 'Top Products by Revenue' => 'Topp produkttyper etter inntekt', + 'Top Products' => 'Topp produkter', + 'Top Purchasables by Qty Sold' => 'Topp Kan kjøpes-artikler etter antall solgt', + 'Top Purchasables by Revenue' => 'Topp Kan kjøpes-artikler etter inntekt', + 'Top Purchasables' => 'De beste Kan kjøpes-artikler', + 'Total ' => 'Sum ', + 'Total Discount Use Limit' => 'Grense på samlet rabattbruk', + 'Total Discount' => 'Total rabatt', + 'Total Included Tax' => 'Sum inkludert avgift', + 'Total Orders by Billing Country' => 'Totalt antall ordrer etter faktureringsland', + 'Total Orders by Country' => 'Totalt antall ordrer etter land', + 'Total Orders by Shipping Country' => 'Totalt antall ordrer etter fraktland', + 'Total Orders' => 'Totalt antall ordre', + 'Total Paid' => 'Total betalt', + 'Total Price' => 'Totalpris', + 'Total Qty' => 'Totalt antall', + 'Total Revenue' => 'Total inntekt', + 'Total Shipping' => 'Total frakt', + 'Total Tax' => 'Total avgift', + 'Total Weight' => 'Samlet vekt', + 'Total' => 'Sum', + 'Track Inventory' => 'Spor inventar', + 'Transaction Hash' => 'Transaksjons-Hash', + 'Transaction ID' => 'Transaksjons-ID', + 'Transaction captured successfully: {message}' => 'Vellykket fanging av transaksjoner: {message}', + 'Transaction refunded successfully: {message}' => 'Vellykket refusjon av transaksjon: {message}', + 'Transactions' => 'Transaksjoner', + 'Transfer Fields' => 'Overføringsfelt', + 'Transfer Items' => 'Overføringselement', + 'Transfer Settings' => 'Overføringsinnstillinger', + 'Transfer Status' => 'Overføringsstatus', + 'Transfer fields saved.' => 'Overføringsfelt lagret.', + 'Transfer must have at least one item.' => 'Overføringer må ha minst ett element.', + 'Transfer' => 'Overføring', + 'Transfers' => 'Overføringer', + 'Trial days credited' => 'Prøvedager kreditert', + 'Trial expiration' => 'Utløpstid for prøveperiode', + 'Trial expiry date' => 'Utløpsdato for utprøving', + 'Type not in allowed options.' => 'Type ikke med blant tillatte alternativer.', + 'Type' => 'Type', + 'URI' => 'URI', + 'Unable to cancel subscription at this time.' => 'Kan ikke avbryte abonnementet nå.', + 'Unable to complete order: another request is already in progress.' => 'Kunne ikke fullføre ordren: En annen forespørsel er allerede i gang.', + 'Unable to find variant.' => 'Kan ikke finne variant.', + 'Unable to generate coupon codes: {message}' => 'Kan ikke generere kupongkoder: {message}', + 'Unable to make payment at this time.' => 'Kan ikke utføre betaling nå.', + 'Unable to modify subscription at this time.' => 'Kan ikke endre abonnementet nå.', + 'Unable to reactivate subscription at this time.' => 'Kan ikke reaktivere abonnementet nå.', + 'Unable to reassign orders.' => 'Kunne ikke tilordne ordrer på nytt.', + 'Unable to remove order data.' => 'Kunne ikke fjerne ordredata.', + 'Unable to retrieve Sale and Purchasable.' => 'Kan ikke hente salg eller Kan kjøpes-artikler.', + 'Unable to retrieve cart.' => 'Kan ikke hente handlekurv.', + 'Unable to retrieve customer.' => 'Kan ikke hente kunde.', + 'Unable to retrieve load cart URL' => 'Kan ikke hente URL for innlasting av handlekurv', + 'Unable to retrieve payment source.' => 'Kunne ikke hente betalingskilde.', + 'Unable to set default shipping category.' => 'Kan ikke angi standard fraktkategori.', + 'Unable to set default tax category.' => 'Kan ikke angi standard avgiftskategori.', + 'Unable to set primary payment source.' => 'Kunne ikke angi primær betalingskilde.', + 'Unable to start the subscription. Please check your payment details.' => 'Kan ikke starte abonnement. Kontroller dine betalingsdetaljer.', + 'Unable to subscribe at this time.' => 'Kan ikke abonnere nå.', + 'Unable to update cart.' => 'Kan ikke oppdatere handlekurv.', + 'Unable to validate address.' => 'Kan ikke validere adresser.', + 'Unit Price' => 'Enhetspris', + 'Unit price (minus discounts)' => 'Enhetspris (minus rabatter)', + 'Units' => 'Enheter', + 'Unpaid' => 'Ubetalt', + 'Unsubscribe' => 'Avslutte abonnement', + 'Update Address' => 'Oppdater adressen', + 'Update Order Status' => 'Oppdater bestillingsstatus', + 'Update Order Status…' => 'Oppdater ordrestatus ...', + 'Update order' => 'Oppdater ordre', + 'Update subscription' => 'Oppdater abonnement', + 'Update' => 'Oppdater', + 'Updated By' => 'Oppdatert av', + 'Updated committed stock successfully.' => 'Forpliktet beholdning oppdatert.', + 'Updated' => 'Oppdatert', + 'Use Billing Address For Tax' => 'Bruk faktureringsadresse for avgift', + 'Use as the primary billing address' => 'Bruk som den primære faktureringsadressen', + 'Use as the primary shipping address' => 'Bruk som den primære leveringsadressen', + 'Used By Tax Rates' => 'Bruk av skattesatser', + 'Used by Tax Rates' => 'Brukt av skattesats', + 'User Groups' => 'Brukergrupper', + 'User not found.' => 'Bruker ikke funnet.', + 'User' => 'Bruker', + 'Uses' => 'Bruksområder', + 'Validate Business Tax ID as Vat ID' => 'Bekreft ID for virksomhetsskatt som MVA-ID', + 'Validating condition syntax' => 'Validerer tilstandssyntaks', + 'Validating formula syntax' => 'Validerer formelsyntaks', + 'Variant Fields' => 'Variantfelt', + 'Variant Has Untracked Stock' => 'Varianten har lagerbeholdning som ikke er sporet', + 'Variant Price' => 'Variantpris', + 'Variant SKU' => 'Variant-SKU', + 'Variant Search' => 'Variantsøk', + 'Variant Stock' => 'Varantlager', + 'Variant Title Format' => 'Tittelformat for varianter', + 'Variant Tracks Stock' => 'Varianten sporer lagerbeholdning', + 'Variant UI Label Format' => 'Etikettformat for grensesnitt for variant', + 'Variant has no product.' => 'Variant har ingen produkt.', + 'Variants not restored.' => 'Varianter ble ikke gjenopprettet.', + 'Variants restored.' => 'Varianter ble gjenopprettet.', + 'Variants' => 'Varianter', + 'View customer' => 'Vis kunder', + 'View order' => 'Vis ordre', + 'View product type - {productType}' => 'Vis produkttype – {productType}', + 'View user' => 'Vis bruker', + 'View' => 'Vis', + 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Advarsel. Hvis du sletter valutaen, vil det stoppe alle betalinger og tilbakebetalinger i valutaen. Er du sikker på at du vil slette «{name}»?', + 'Web' => 'Web', + 'Webhook URL' => 'Webhook-URL', + 'Weight ({unit})' => 'Vekt ({unit})', + 'Weight Rate' => 'Vektpris', + 'Weight Unit' => 'Vektenhet', + 'Weight' => 'Vekt', + 'What product URIs should look like for the site.' => 'Hvordan produkt-URI-er skal se ut på dette nettstedet.', + 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Hvordan den automatisk genererte produkttittelen skal se ut. Du kan inkludere tags som gir egenskaper til de ulike produktene, slik som {ex1} eller {ex2}. Alle spesialfelt som brukes må settes til påkrevd.', + 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Hvordan den automatisk genererte varianttittelen skal se ut. Du kan inkludere tags som gir egenskaper til de ulike variantene, slik som {ex1} eller {ex2}. Alle spesialfelt som brukes må settes til påkrevd.', + 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Hvordan PDF-filnavnet til ordrene skal se ut (uten utvidelse). Du kan inkludere stikkord med utgående ordreegenskaper, slik som {ex1} eller {ex2}.', + 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Hvordan de unike, autogenererte SKU-ene skal se ut når et SKU-felt sendes inn uten en verdi. Du kan inkludere tags som gir egenskaper, slik som {ex1} eller {ex2}.', + 'What this PDF will be called in the control panel.' => 'Hva denne PDF-en skal hete i kontrollpanelet.', + 'What this catalog pricing rule will be called in the control panel.' => 'Hva denne prisregelen for katalog skal hete i kontrollpanelet.', + 'What this discount will be called in the control panel.' => 'Hva denne rabatten skal hete i kontrollpanelet.', + 'What this email will be called in the control panel.' => 'Hva denne e-posten skal hete i kontrollpanelet.', + 'What this product type will be called in the control panel.' => 'Hva denne produkttypen skal hete i kontrollpanelet.', + 'What this sale will be called in the control panel.' => 'Hva dette salget skal hete i kontrollpanelet.', + 'What this shipping category will be called in the control panel.' => 'Hva denne fraktkategorien skal hete i kontrollpanelet.', + 'What this shipping rule will be called in the control panel.' => 'Hva denne fraktregelen skal hete i kontrollpanelet.', + 'What this shipping zone will be called in the control panel.' => 'Hva denne fraktsonen skal hete i kontrollpanelet.', + 'What this status will be called in the control panel.' => 'Hva denne statusen skal hete i kontrollpanelet.', + 'What this subscription plan will be called in the control panel.' => 'Hva dette abonnementet skal hete i kontrollpanelet.', + 'What this tax category will be called in the control panel.' => 'Hva denne avgiftskategorien skal hete i kontrollpanelet.', + 'What this tax zone will be called in the control panel.' => 'Hva denne skattesonen skal hete i kontrollpanelet.', + 'When this discount is applied to an order, which line items should be discounted?' => 'Når denne rabatten blir brukt på en ordre, hvilke linjevarer skal få rabatt?', + 'Whether the first available shipping method option should be set automatically on carts.' => 'Om det første tilgjengelige fraktalternativet skal angis automatisk i handlekurver.', + 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Om brukerens primære betalingskilde skal angis automatisk i nye handlekurver.', + 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Om brukerens primære frakt- og faktureringsadresse betalingskilde skal angis automatisk i nye handlekurver.', + 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Om denne prisregelen for katalog skal være tilgjengelig for bruk, uansett andre forhold.', + 'Whether this sale should be available for use, regardless of other conditions.' => 'Om dette salget skal være tilgjengelig for bruk, uansett andre forhold.', + 'Which data to display in the name column in the results table.' => 'Hvilken data som skal vises i kolonnen med navn i resultattabellen.', + 'Which product types should this category be available to?' => 'Hvilke produkttyper skal denne kategorien være tilgjengelig for?', + 'Which template should be loaded when a product’s URL is requested.' => 'Hvilken mal som skal lastes når en forespørsel sendes for et produkts URL.', + 'Width ({unit})' => 'Bredde ({unit})', + 'Width' => 'Bredde', + 'YYYY' => 'ÅÅÅÅ', + 'Yes' => 'Ja', + 'You are not allowed to add a line item.' => 'Du har ikke tillatelse til å legge til en linjevare.', + 'You currently have no emails configured to select for this status.' => 'Du har ikke en e-post du kan velge for denne statusen.', + 'You do not have permission to load this cart.' => 'Du har ikke tillatelse til å laste inn denne handlekurven.', + 'You must set up at least one gateway that supports subscriptions first.' => 'Du må sette opp minst en portal som støtter abonnementer først.', + 'You must be logged in or provide a valid token to load this cart.' => 'Du må være innlogget eller oppgi en gyldig nøkkel for å laste inn denne handlekurven.', + 'You must be signed in to create a payment source.' => 'Du må være innlogget for å opprette en betalingskilde.', + 'You must be signed in to set a primary payment source.' => 'Du må være innlogget for å angi en primær betalingskilde.', + 'You must make a payment to complete the order.' => 'Du må betale for å fullføre ordren.', + 'Your Cart Recovery Link' => 'Lenken din for gjenoppretting av handlekurv', + 'Your Order PDF Download Link' => 'Din nedlastingslenke for PDF-ordre', + 'Your order is empty' => 'Ordren er tom', + 'ZIP file' => 'ZIP-fil', + 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Null – minimumspris er null hvis rabattene er større enn ordreverdien.', + 'Zip Code' => 'Postnummer', + 'all' => 'alle', + 'any' => 'noen', + 'average order total' => 'gjennomsnittlig ordrebeløp', + 'billing address' => 'faktureringsadresse', + 'donation' => 'donasjon', + 'donations' => 'donasjoner', + 'info' => 'info', + 'inventory location' => 'inventarsted', + 'new customers' => 'nye kunder', + 'on hand' => 'for hånden', + 'only' => 'kun', + 'order' => 'ordre', + 'orders' => 'ordre', + 'price' => 'pris', + 'prices' => 'priser', + 'product variant' => 'produktvariant', + 'product variants' => 'produktvarianter', + 'product' => 'produkt', + 'products' => 'produkter', + 'repeat customers' => 'tilbakevendende kunder', + 'shipping address' => 'fraktadresse', + 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'Både shippingSameAsBilling og billingSameAsShipping kan ikke settes.', + 'subscription' => 'abonnement', + 'subscriptions' => 'abonnementer', + 'to' => 'til', + 'transfer' => 'overføring', + 'transfers' => 'overføringer', + '{amount} included' => '{amount} inkludert', + '{count} Unfulfilled Orders' => '{count} ufullførte ordrer', + '{description} is no longer available.' => '{description} er ikke tilgjengelig lenger.', + '{description} only has {stock} in stock.' => '{description} har bare {stock} på lager.', + '{from} to {to}' => '{from} til {to}', + '{name} (Primary)' => '{name} (Primære)', + '{name} (Trashed)' => '{name} (forkastet)', + '{name} catalog price' => 'Katalogpris {name}', + '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, one {}=1{ordre} other{ordrer}} oppdatert.', + '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, =1{ordre er} other{ordrer er}} assosiert med {numUsers, plural, =1{bruker} other{brukere}}.', + '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, =1{abonnement er} other{abonnementer er}} aktivert for {numUsers, plural, =1{bruker} other{brukere}}.', + '{number} more…' => '{number} mer…', + '{pct} off the discounted item price' => '{pct} avslag på rabattert pris', + '{pct} off the original item price' => '{pct} avslag på opprinnelig pris', + '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, one {}=1{har} other{har}} ikke blitt tilordnet et nettsted.', + '{total} in total revenue' => '{total} i total inntekt', + '{total} orders' => '{total} ordrer', + '{total} saleable across {locationCount} location(s)' => '{total} salgbare på tvers av {locationCount} sted(er)', + '{uses} uses across {emails} email addresses' => '{uses} antall bruk for {emails} e-postadresser', + '{uses} uses across {users} users' => '{uses} antall bruk for {users} brukere', + '“{description}” is currently out of stock.' => 'Det er for øyeblikket tomt på lageret for «{description}».', + '“{key}” has invalid JSON' => '«{key}» har ugyldig JSON', +]; diff --git a/lang/nl/commerce.php b/lang/nl/commerce.php new file mode 100644 index 0000000000..4474662c23 --- /dev/null +++ b/lang/nl/commerce.php @@ -0,0 +1,1423 @@ + '(nieuwe prijs)', + '(of original price)' => '(van originele prijs)', + '(off original price)' => '(van originele prijs)', + 'A cart number must be specified.' => 'Gelieve een winkelwagennummer op te geven.', + 'A cart recovery link has been sent to {email}.' => 'Er is een link voor het herstellen van de winkelwagen verstuurd naar {email}.', + 'A cart recovery link will be sent to {email}.' => 'Er wordt een link voor het herstellen van de winkelwagen verstuurd naar {email}.', + 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Er wordt een gebruiksvriendelijk referentienummer met dit formaat gegenereerd wanneer een winkelwagen is voltooid en in een bestelling wordt omgezet. Bijvoorbeeld {ex1} of
{ex2}. Het resultaat van dit formaat moet uniek zijn.', + 'A new download link has been sent to {email}' => 'Er is een nieuwe downloadlink verstuurd naar {email}', + 'A new download link will be sent to {email}' => 'Er wordt een nieuwe downloadlink verstuurd naar {email}', + 'A valid email is required to create a customer.' => 'Om een klant aan te maken, is een geldig e-mailadres nodig.', + 'Accept' => 'Accepteren', + 'Accepted' => 'Geaccepteerd', + 'Actions' => 'Acties', + 'Active Carts' => 'Actieve Winkelwagens', + 'Active subscriptions' => 'Actieve abonnementen', + 'Active' => 'Actief', + 'Add Address' => 'Adres toevoegen', + 'Add a coupon' => 'Voeg een coupon toe', + 'Add a custom line item' => 'Een aangepast lijnitem toevoegen', + 'Add a line item' => 'Lijnitem toevoegen', + 'Add a product' => 'Voeg een product toe', + 'Add a variant' => 'Voeg een variant toe', + 'Add an adjustment' => 'Aanpassing toevoegen', + 'Add an item' => 'Een item toevoegen', + 'Add an option' => 'Voeg een optie toe', + 'Add catalog price' => 'Catalogusprijs toevoegen', + 'Add' => 'Toevoegen', + 'Additional Actions' => 'Bijkomende acties', + 'Additional recipients that should receive this email. Twig code can be used here.' => 'Bijkomende ontvangers die deze e-mail moeten ontvangen. Hier kunt u Twig-code gebruiken.', + 'Address 1' => 'Adres 1', + 'Address 2' => 'Adres 2', + 'Address 3' => 'Adres 3', + 'Address Line 1' => 'Adresregel 1', + 'Address Line 2' => 'Adresregel 2', + 'Address Updated.' => 'Adres bijgewerkt.', + 'Address copied to user.' => 'Adres gekopieerd naar gebruiker.', + 'Address not found.' => 'Adres niet gevonden.', + 'Adjust Quantity' => 'Hoeveelheid aanpassen', + 'Adjust by' => 'Aanpassen met', + 'Adjust price when included rate is disqualified?' => 'Prijs aanpassen wanneer belastingtarief onjuist is?', + 'Adjustments' => 'Aanpassingen', + 'Admin Notices' => 'Beheerderskennisgevingen', + 'Administrative Area Code of Origin' => 'Oorsprongscode administratief gebied', + 'Advanced' => 'Geavanceerd', + 'All Orders' => 'Alle bestellingen', + 'All Totals' => 'Alle totalen', + 'All Transfers' => 'Alle overdrachten', + 'All active subscriptions' => 'Alle actieve abonnementen', + 'All customers' => 'Alle klanten', + 'All products' => 'Alle producten', + 'All variants must have a SKU.' => 'Alle varianten moeten een SKU hebben.', + 'All' => 'Alle', + 'Allow Checkout Without Payment' => 'Afrekenen zonder betaling toestaan', + 'Allow Empty Cart On Checkout' => 'Lege winkelwagen bij afrekenen toestaan', + 'Allow Partial Payment On Checkout' => 'Gedeeltelijke betaling bij afrekenen toestaan', + 'Allow out of stock purchases' => 'Aankopen niet op voorraad toestaan', + 'Allow' => 'Toestaan', + 'Allowed Qty' => 'Toegestaan ​​aantal', + 'Alternative Phone' => 'Alternatief telefoonnummer', + 'Amount' => 'Aantal', + 'An ID must be provided' => 'Er moet een ID worden opgegeven', + 'An error occurred while generating this PDF.' => 'Er is een fout opgetreden bij het genereren van deze PDF.', + 'Any' => 'Ieder', + 'Anywhere' => 'Eender waar', + 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Weet u zeker dat u het abonnementsplan “{name}” wilt archiveren? Hierdoor worden uw bestaande abonnementen NIET geannuleerd.', + 'Are you sure you want to capture this transaction?' => 'Weet u zeker dat u deze transactie wilt vastleggen?', + 'Are you sure you want to complete this order?' => 'Weet u zeker dat u deze bestelling wilt voltooien?', + 'Are you sure you want to delete the selected orders?' => 'Weet u zeker dat u de geselecteerde bestellingen wilt verwijderen?', + 'Are you sure you want to delete the selected product and its variants?' => 'Weet u zeker dat u het geselecteerde product en de varianten wilt verwijderen?', + 'Are you sure you want to delete this shipping rule?' => 'Weet u zeker dat u deze verzendingsregel wilt verwijderen?', + 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Weet u zeker dat u “{name}” wilt verwijderen en al haar producten? Zorg ervoor dat u een back-up van uw database heeft voordat u deze destructieve actie uitvoert.', + 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Weet u zeker dat u “{name}” wilt verwijderen? Dit zal alle lijnitems met deze status resetten naar geen status.', + 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Weet u zeker dat u deze overdracht als in afwachting wilt markeren? Deze wordt als inkomend weergegeven op de bestemming.', + 'Are you sure you want to overwrite the billing address?' => 'Weet u zeker dat u het factuuradres wilt overschrijven?', + 'Are you sure you want to overwrite the shipping address?' => 'Weet u zeker dat u het verzendadres wilt overschrijven?', + 'Are you sure you want to permanently delete this store and everything in it?' => 'Weet u zeker dat u deze winkel en alles erin permanent wilt verwijderen?', + 'Are you sure you want to refund this transaction?' => 'Weet u zeker dat u deze transactie wilt terugbetalen?', + 'Are you sure you want to remove this customer?' => 'Weet u zeker dat u deze klant wilt verwijderen?', + 'Are you sure you want to save this as a new shipping rule?' => 'Weet u zeker dat u deze nieuwe verzendingsregel wilt opslaan?', + 'Are you sure you want to send email: {name}?' => 'Weet u zeker dat de e-mail: {name} wilt verzenden?', + 'At least one site must be enabled for the product type.' => 'Er moet minimaal één site zijn ingeschakeld voor het producttype.', + 'Attempted Payments' => 'Betaalpogingen', + 'Attention' => 'Ter attentie van', + 'Authorize Only (Manually Capture)' => 'Alleen autoriseren (Handmatig registreren)', + 'Auto Set Cart Shipping Method Option' => 'Verzendmethodeoptie winkelwagen automatisch instellen', + 'Auto Set New Cart Addresses' => 'Nieuw winkelwagenadres automatisch instellen', + 'Auto Set Payment Source' => 'Betaalbron automatisch instellen', + 'Automatic SKU Format' => 'Automatisch Artikelnummer Formaat', + 'Available Shipping Categories' => 'Beschikbare verzendcategorieën', + 'Available Tax Categories' => 'Beschikbare belastingcategorieën', + 'Available for purchase' => 'Beschikbaar voor aankoop', + 'Available for purchase?' => 'Beschikbaar voor aankoop?', + 'Available inventory for "{description}" has gone below zero.' => 'De beschikbare voorraad voor "{description}" is lager dan nul.', + 'Available to Product Types' => 'Beschikbaar voor producttypen', + 'Available' => 'Beschikbaar', + 'Available?' => 'Beschikbaar?', + 'Average Order Total' => 'Gemiddelde totaalprijs bestellingen', + 'Average' => 'Gemiddelde', + 'BCC’d Recipient' => 'BCC’de ontvanger', + 'Bad Request' => 'Foutieve aanvraag', + 'Bad address ID.' => 'Onjuiste adres-ID.', + 'Bad order ID.' => 'Verkeerde bestelling-ID.', + 'Base Price' => 'Basisprijs', + 'Base Promotional Price' => 'Promotiebasisprijs', + 'Base Rate' => 'Basistarief', + 'Base' => 'Basis', + 'Bcc' => 'Bcc', + 'Billing Address' => 'Factuuradres', + 'Billing Business Name' => 'Bedrijfsnaam voor facturering', + 'Billing First Name' => 'Voornaam voor facturering', + 'Billing Full Name' => 'Volledige naam voor facturering', + 'Billing Last Name' => 'Achternaam voor facturering', + 'Billing address required.' => 'Factuuradres verplicht.', + 'Billing detail update URL' => 'URL om factuurgegevens bij te werken', + 'Billing issues' => 'Factureringsprobleem', + 'Billing' => 'Facturering', + 'Both (Line item price + Line item shipping costs)' => 'Beide (prijs regelartikel + verzendkosten regelartikel)', + 'Business ID' => 'Bedrijfs-id', + 'Business Name' => 'Bedrijfsnaam', + 'Business Tax ID' => 'BTW nummer', + 'CC’d Recipient' => 'Ontvanger in cc', + 'CVV' => 'CVC', + 'Can be used as an internal reference.' => 'Kan worden gebruikt als interne referentie.', + 'Can not complete payment for missing transaction.' => 'Betaling voor ontbrekende transactie voltooien niet mogelijk.', + 'Can not create a new order' => 'Nieuwe bestelling aanmaken niet mogelijk', + 'Can not find an order to pay.' => 'Geen bestelling gevonden om te betalen.', + 'Can not find enabled email.' => 'Kan ingeschakeld e-mail niet vinden.', + 'Can not find order' => 'Bestelling niet gevonden', + 'Can not find order.' => 'Bestelling niet gevonden.', + 'Can not find the transaction to refund' => 'Terug te betalen transactie onvindbaar', + 'Can not move between these inventory types.' => 'Kan niet verplaatsen tussen deze voorraadtypen.', + 'Can not refund amount greater than the remaining amount' => 'Het is niet mogelijk om een bedrag terug te betalen dat hoger is dan het resterend bedrag', + 'Cancel subscription' => 'Abonnement annuleren', + 'Cancel with gateway now' => 'Nu annuleren met gateway', + 'Cancel' => 'Annuleer', + 'Cancellation date' => 'Annuleringsdatum', + 'Cancellation' => 'Annulering', + 'Cannot switch plans for this subscription.' => 'Kan niet overstappen op een ander plan voor dit abonnement.', + 'Can’t preview this email.' => 'Er is geen voorbeeldweergave van deze e-mail beschikbaar.', + 'Capture payment' => 'Betaling registreren', + 'Capture' => 'Vastleggen', + 'Card Holder' => 'Kaarthouder', + 'Card Number' => 'Kaartnummer', + 'Card' => 'Kaart', + 'Cart Recovery Link' => 'Link voor het herstellen van de winkelwagen', + 'Cart forgotten.' => 'Winkelwagen vergeten.', + 'Cart updated.' => 'Winkelwagen bijgewerkt.', + 'Cart {number}' => 'Winkelwagen {number}', + 'Catalog Pricing Rule' => 'Catalogusprijsregel', + 'Catalog pricing rule description.' => 'Beschrijving van catalogusprijsregel.', + 'Catalog pricing rule saved.' => 'Catalogusprijsregel opgeslagen.', + 'Catalog pricing rules deleted.' => 'Catalogusprijsregels verwijderd.', + 'Catalog pricing rules updated.' => 'Catalogusprijsregels bijgewerkt.', + 'Categories Relationship Type' => 'Relatietype categorieën', + 'Categories' => 'Categorieën', + 'Category Rate Overrides' => 'Categorietarief overschrijft', + 'Centimeters (cm)' => 'Centimeter (cm)', + 'Changing this value may affect your ability to refund existing transactions.' => 'Het wijzigen van deze waarde kan gevolgen hebben voor de mogelijkheid om bestaande transacties terug te betalen.', + 'Choose a color to represent the order’s status' => 'Kies een kleur om de status van de bestelling weer te geven', + 'Choose a new customer' => 'Kies een nieuwe klant', + 'Choose adjustment values to include when calculating the product revenue total.' => 'Kies de te gebruiken aanpasswingswaarden bij het berekenen van de totale omzet van het product.', + 'Choose the currency’s ISO code.' => 'Kies de ISO code van de valuta.', + 'Choose the destination inventory location for the existing on hand stock.' => 'Kies de bestemmingsvoorraadlocatie voor de bestaande aanwezige voorraad.', + 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Kies op welke sites dit producttype beschikbaar moet zijn en configureer de sitespecifieke instellingen.', + 'City' => 'Plaats', + 'Clear counter' => 'Teller op nul zetten', + 'Clear notices' => 'Kennisgevingen wissen', + 'Close' => 'Sluiten', + 'Code' => 'Code', + 'Collated PDF' => 'Samengevoegde PDF', + 'Color' => 'Kleur', + 'Commerce Products' => 'Commerce-producten', + 'Commerce Settings' => 'Commerce instellingen', + 'Commerce Variants' => 'Commerce-varianten', + 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Commerce-e-mail “{email}” kan niet worden verzonden voor bestelling “{order}”.', + 'Commerce order exports' => 'Exports van commerce-bestellingen', + 'Commerce' => 'Commerce', + 'Committed' => 'Toegezegde voorraad', + 'Completed Email' => 'Ingevuld e-mailadres', + 'Completed' => 'Voltooid', + 'Completing order failed.' => 'Uitvoering bestelling mislukt.', + 'Condition' => 'Conditie', + 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Voorwaarden worden hier vergeleken met een bestelling voordat de regels worden bekeken. Dit is handig als u de beschikbaarheid van een methode vroegtijdig wilt kwalificeren of als er gemeenschappelijke voorwaarden zijn voor alle regels voor deze methode.', + 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Voorwaarden hier worden vergeleken met de klant van de bestelling voordat de regels worden bekeken. Dit is handig als u de beschikbaarheid van een methode vroegtijdig wilt kwalificeren of als er gemeenschappelijke voorwaarden zijn voor alle regels voor deze methode.', + 'Conditions' => 'Voorwaarden', + 'Contains Purchasables' => 'Bevat koopbare artikelen', + 'Control Panel Settings' => 'Instellingen configuratiescherm', + 'Control panel' => 'Configuratiescherm', + 'Conversion Rate' => 'Conversietarief', + 'Converted Price' => 'Omgezette prijs', + 'Copied!' => 'Gekopieerd!', + 'Copy the URL' => 'Kopieer de URL', + 'Copy to {location}' => 'Kopiëren naar {location}', + 'Copy' => 'Kopiëren', + 'Costs' => 'Kosten', + 'Could not archive gateway.' => 'Gateway archiveren niet mogelijk.', + 'Could not cancel “{reference}”.' => 'Kan niet annuleren “{reference}”.', + 'Could not create the payment source.' => 'Betaalbron aanmaken niet mogelijk.', + 'Could not delete shipping rule' => 'Verzendingsregel verwijderen niet mogelijk', + 'Could not delete shipping zone' => 'Verzendingszone verwijderen niet mogelijk', + 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Kan {count, number} verzend{count, plural, one{categorie} other{categorieën}} niet verwijderen.', + 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Kan {count, number} verzend{count, plural, one{methode} other{methoden}} en regels niet verwijderen.', + 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Kan {count, number} belasting{count, plural, one{categorie} other{categorieën}} niet verwijderen.', + 'Could not find the email or template.' => 'E-mail of sjabloon niet gevonden.', + 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Bestelling {number} kan niet als voltooid worden gemarkeerd. Opslaan bestelling mislukt vanwege fouten tijdens het voltooien van de bestelling: {order}', + 'Could not reactivate “{reference}”.' => 'Kan niet opnieuw activeren “{reference}”.', + 'Could not send email' => 'E-mail verzenden niet mogelijk', + 'Could not switch “{reference}” to “{plan}”.' => 'Kan “{reference}” niet omschakelen naar “{plan}”.', + 'Could not update orders address.' => 'Bijwerken adres bestelling niet mogelijk.', + 'Couldn’t archive Line Item Status.' => 'Status regelartikel archiveren niet mogelijk.', + 'Couldn’t archive Order Status.' => 'Status bestelling archiveren niet mogelijk.', + 'Couldn’t capture transaction.' => 'Transactie registreren niet mogelijk.', + 'Couldn’t capture transaction: {message}' => 'Transactie registreren niet mogelijk: {message}', + 'Couldn’t delete email.' => 'Kon e-mailadres niet verwijderen.', + 'Couldn’t delete the payment source.' => 'Betaalbron verwijderen niet mogelijk.', + 'Couldn’t get order.' => 'Bestelling ophalen lukt niet.', + 'Couldn’t recalculate order.' => 'Bestelling herberekenen was niet mogelijk.', + 'Couldn’t refund transaction.' => 'Transactie terugbetalen niet mogelijk.', + 'Couldn’t refund transaction: {message}' => 'Transactie terugbetalen niet mogelijk: {message}', + 'Couldn’t reorder Line Item Statuses.' => 'Regelartikelstatussen herschikken niet mogelijk.', + 'Couldn’t reorder Order Statuses.' => 'Bestellingsstatussen herschikken niet mogelijk.', + 'Couldn’t reorder PDFs.' => 'PDF\'s herschikken niet mogelijk.', + 'Couldn’t reorder discounts.' => 'Kan kortingen niet opnieuw rangschikken.', + 'Couldn’t reorder gateways.' => 'Kan gateways niet opnieuw bestellen.', + 'Couldn’t reorder plans.' => 'Kan plannen niet opnieuw rangschikken.', + 'Couldn’t reorder rules.' => 'Regels herschikken niet mogelijk.', + 'Couldn’t reorder sale.' => 'Aanbieding herschikken lukt niet.', + 'Couldn’t reorder sales.' => 'Aanbiedingen herschikken niet mogelijk.', + 'Couldn’t reorder statuses.' => 'De statussen herschikken was niet mogelijk.', + 'Couldn’t reorder stores.' => 'Kon winkels niet herordenen.', + 'Couldn’t save PDF.' => 'PDF opslaan niet mogelijk.', + 'Couldn’t save catalog pricing rule.' => 'Kon catalogusprijsregel niet opslaan.', + 'Couldn’t save currency.' => 'Valuta opslaan niet mogelijk.', + 'Couldn’t save discount.' => 'Korting opslaan niet mogelijk.', + 'Couldn’t save email.' => 'E-mail opslaan niet mogelijk.', + 'Couldn’t save gateway.' => 'Gateway opslaan niet mogelijk.', + 'Couldn’t save inventory location.' => 'Kon voorraadlocatie niet opslaan.', + 'Couldn’t save line item status.' => 'Status regelartikel opslaan niet mogelijk.', + 'Couldn’t save order fields.' => 'Kon bestellingsvelden niet opslaan.', + 'Couldn’t save order status.' => 'Status bestelling opslaan niet mogelijk.', + 'Couldn’t save order.' => 'Bestelling opslaan niet mogelijk.', + 'Couldn’t save product type.' => 'Producttype opslaan niet mogelijk.', + 'Couldn’t save sale.' => 'Aanbieding opslaan niet mogelijk.', + 'Couldn’t save settings.' => 'Instellingen opslaan niet mogelijk.', + 'Couldn’t save shipping category.' => 'Verzendcategorie opslaan niet mogelijk.', + 'Couldn’t save shipping method.' => 'Verzendmethode opslaan niet mogelijk.', + 'Couldn’t save shipping rule.' => 'Verzendingsregel opslaan niet mogelijk.', + 'Couldn’t save shipping zone.' => 'Verzendingszone opslaan niet mogelijk.', + 'Couldn’t save store.' => 'Kon winkel niet opslaan.', + 'Couldn’t save subscription fields.' => 'Kon abonnementsvelden niet opslaan.', + 'Couldn’t save subscription plan.' => 'Abonnementsplan opslaan niet mogelijk.', + 'Couldn’t save subscription.' => 'Abonnement opslaan niet mogelijk.', + 'Couldn’t save tax category.' => 'Belastingcategorie opslaan niet mogelijk.', + 'Couldn’t save tax rate.' => 'Belastingtarief opslaan niet mogelijk.', + 'Couldn’t save tax zone.' => 'Belastingzone opslaan niet mogelijk.', + 'Couldn’t save transfer fields.' => 'Kan overdrachtsvelden niet opslaan.', + 'Couldn’t update catalog pricing rule statuses.' => 'Kon status catalogusprijsregel niet bijwerken.', + 'Couldn’t update status.' => 'Kan status niet bijwerken.', + 'Couldn’t updated sales status.' => 'Status aanbieding bijwerken niet mogelijk.', + 'Country Code of Origin' => 'Oorsprongscode land', + 'Country List' => 'Landenlijst', + 'Country not allowed.' => 'Land niet toegestaan.', + 'Country' => 'Land', + 'Coupon Code' => 'Kortingscode', + 'Coupon can not apply discount to this order due to address mismatch.' => 'Met deze coupon kan geen korting worden toegepast op deze bestelling omdat het adres niet overeenkomt.', + 'Coupon can not apply discount to this order due to customer mismatch.' => 'Met deze coupon kan geen korting worden toegepast op deze bestelling omdat de klant niet overeenkomt.', + 'Coupon can not apply discount to this order.' => 'Met deze coupon kan geen korting worden toegepast op deze bestelling.', + 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Couponcode "{code}" wordt al gebruikt door korting "{name}".', + 'Coupon codes cannot be blank.' => 'Couponcodes mogen niet leeg zijn.', + 'Coupon codes must be unique.' => 'Couponcodes moeten uniek zijn.', + 'Coupon format is required and must contain at least one `#`.' => 'Couponnotatie is vereist en moet minimaal één \'#\' bevatten.', + 'Coupon not valid.' => 'Ongeldige coupon.', + 'Coupon removed: {explanation}' => 'Coupon verwijderd: {explanation}', + 'Coupons' => 'Coupons', + 'Craft Commerce - Administration' => 'Craft Commerce - Administratie', + 'Craft Commerce - Inventory' => 'Craft Commerce - Voorraad', + 'Craft Commerce - Orders' => 'Craft Commerce - Bestellingen', + 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Producttype - {name}', + 'Craft Commerce - Subscriptions' => 'Craft Commerce - Abonnementen', + 'Create a Discount' => 'Maak een korting aan', + 'Create a Subscription Plan' => 'Een abonnementsplan aanmaken', + 'Create a new PDF' => 'Maak een nieuw PDF-bestand aan', + 'Create a new catalog pricing rule' => 'Maak een nieuwe catalogusprijsregel', + 'Create a new currency' => 'Maak nieuwe valuta aan', + 'Create a new email' => 'Maak een nieuw e-mailadres aan', + 'Create a new gateway' => 'Een nieuwe gateway aanmaken', + 'Create a new line item status' => 'Maak een regelartikelstatus aan', + 'Create a new order status' => 'Maak een bestellingstatus aan', + 'Create a new product type' => 'Maak een nieuw producttype', + 'Create a new sale' => 'Maak een nieuwe aanbieding aan', + 'Create a new shipping category' => 'Maak een nieuwe verzendcategorie aan', + 'Create a new shipping method' => 'Maak een nieuwe verzendmethode aan', + 'Create a new shipping rule' => 'Maak een nieuwe verzendingsregel', + 'Create a new tax category' => 'Maak een nieuwe fiscale categorie aan', + 'Create a new tax rate' => 'Maak een nieuw belastingtarief aan', + 'Create a product type' => 'Maak een producttype aan', + 'Create a shipping zone' => 'Maak een verzendingszone aan', + 'Create a tax zone' => 'Maak een belastingzone aan', + 'Create catalog pricing rules' => 'Catalogusprijsregels maken', + 'Create customer: “{email}”' => 'Klant aanmaken: “{email}”', + 'Create discounts' => 'Kortingen aanmaken', + 'Create discount…' => 'Korting aanmaken ...', + 'Create rules that allow this discount to match the order.' => 'Maak regels waardoor deze korting bij de bestelling past.', + 'Create rules that allow this discount to match the order’s billing address.' => 'Maak regels waardoor deze korting bij het factuuradres van de bestelling past.', + 'Create rules that allow this discount to match the order’s customer.' => 'Maak regels waardoor deze korting bij de klant van de bestelling past.', + 'Create rules that allow this discount to match the order’s shipping address.' => 'Maak regels waardoor deze korting bij het verzendadres van de bestelling past.', + 'Create rules that allow this gateway to match the billing address.' => 'Maak regels waardoor deze gateway bij het factuuradres past.', + 'Create rules that allow this gateway to match the order.' => 'Maak regels waardoor deze gateway overeenkomt met de bestelling.', + 'Create rules that allow this gateway to match the shipping address.' => 'Maak regels waardoor deze gateway bij het verzendadres past.', + 'Create sales' => 'Verkopen aanmaken', + 'Create sale…' => 'Aanbieding aanmaken ...', + 'Created' => 'Gemaakt', + 'Credit Card Payment Type' => 'Creditcard betalingstype', + 'Currency Code' => 'Valutacode', + 'Currency saved.' => 'Valuta opgeslagen.', + 'Currency' => 'Muntsoort', + 'Current' => 'Huidige', + 'Custom 1' => 'Aangepast 1', + 'Custom 2' => 'Aangepast 2', + 'Custom 3' => 'Aangepast 3', + 'Custom 4' => 'Aangepast 4', + 'Custom' => 'Aangepast', + 'Customer Enabled?' => 'Klant ingeschakeld?', + 'Customer ID is required.' => 'Klant-ID is verplicht.', + 'Customer Note' => 'Notitie van klant', + 'Customer Notices' => 'Kennisgevingen aan klanten', + 'Customer data' => 'Klantgegevens', + 'Customer' => 'Klant', + 'Damaged' => 'Beschadigd', + 'Data shown might be outdated.' => 'De weergegeven gegevens zijn mogelijk verouderd.', + 'Date Authorized' => 'Datum geautoriseerd', + 'Date Created' => 'Datum aangemaakt', + 'Date First Paid' => 'Datum eerste betaling', + 'Date Ordered' => 'Datum besteld', + 'Date Paid' => 'Datum van betaling', + 'Date Updated' => 'Datum van update', + 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Datum vanaf wanneer de catalogusprijsregel actief zal zijn. Laat leeg voor onbeperkte startdatum', + 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Datum vanaf wanneer de korting actief zal zijn. Laat leeg voor onbeperkte startdatum', + 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Datum vanaf wanneer de verkoop actief zal zijn. Laat leeg voor onbeperkte startdatum', + 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Datum waarop de catalogusprijsregel zal stoppen. Laat leeg voor onbeperkte einddatum', + 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Datum waarop de korting zal stoppen. Laat leeg voor onbeperkte einddatum', + 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Datum waarop de verkoop zal stoppen. Laat leeg voor onbeperkte einddatum', + 'Date' => 'Datum', + 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Standaard - de prijs mag negatief zijn als kortingen hoger zijn dan de bestelwaarde.', + 'Default Category' => 'Standaardcategorie', + 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'De standaard weergave van het Commerce-configuratiescherm. Als de gebruiker geen rechten heeft, wordt een voor de gebruiker toegankelijke locatie gebruikt.', + 'Default Order PDF' => 'Standaard bestel-pdf', + 'Default Per Item Rate' => 'Standaardkost per item', + 'Default Percentage Rate' => 'Standaardpercentage', + 'Default Status?' => 'Standaard status?', + 'Default View' => 'Standaardweergave', + 'Default Weight Rate' => 'Standaard gewichttarief', + 'Default Zone' => 'Standaard zone', + 'Default status?' => 'Standaardstatus?', + 'Default to this tax zone when no billing address is set' => 'Stel standaard deze belastingzone in als er geen factuuradres is ingesteld', + 'Default to this tax zone when no shipping address is set' => 'Standaard BTW zone met dit verzendadres is ingesteld', + 'Default variant updated.' => 'Standaardvariant bijgewerkt.', + 'Default' => 'Standaard', + 'Default?' => 'Standaard?', + 'Delete catalog pricing rules' => 'Catalogusprijsregels verwijderen', + 'Delete discounts' => 'Kortingen verwijderen', + 'Delete orders' => 'Bestellingen verwijderen', + 'Delete sales' => 'Verkopen verwijderen', + 'Delete' => 'Verwijder', + 'Deleting the {location} location.' => 'Verwijder de locatie {location}.', + 'Describe this rule.' => 'Beschrijf deze regel.', + 'Describe this shipping zone.' => 'Geef een beschrijving voor de verzendingszone', + 'Describe this tax zone.' => 'Beschrijf deze fiscale zone.', + 'Description' => 'Beschrijving', + 'Destination Inventory Location' => 'Bestemmingsvoorraadlocatie', + 'Destination' => 'Bestemming', + 'Details' => 'Details', + 'Dimension Unit' => 'Afmetingen eenheid', + 'Dimensions' => 'Afmeting', + 'Disabled' => 'Uitgeschakeld', + 'Disallow' => 'Niet toestaan', + 'Discount all line items' => 'Korting toepassen op alle regelartikelen', + 'Discount description.' => 'Kortingsbeschrijving.', + 'Discount is not allowed for the order' => 'Korting is niet toegestaan voor de bestelling', + 'Discount is out of date.' => 'De korting is verouderd.', + 'Discount saved.' => 'Korting opgeslagen.', + 'Discount the matching items only' => 'Korting alleen toepassen op overeenstemmende artikelen', + 'Discount use has reached its limit.' => 'De limiet voor het kortinggebruik is bereikt.', + 'Discount' => 'Korting', + 'Discounted Item Subtotal' => 'Subtotaal kortingsartikel', + 'Discounted Items' => 'Artikelen met korting', + 'Discounts deleted.' => 'Kortingen verwijderd.', + 'Discounts reordered.' => 'Kortingen herschikt.', + 'Discounts updated.' => 'Kortingen bijgewerkt.', + 'Discounts' => 'Kortingen', + 'Disqualify with valid business tax ID?' => 'Diskwalificeren met geldig btw-nummer?', + 'Do not apply subsequent matching sales beyond applying this sale.' => 'Pas geen opeenvolgende overeenkomende verkopen toe na het toepassen van deze verkoop.', + 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Pas dit tarief niet toe als het besteladres een of meer van de geselecteerde geldige btw-nummers bevat.', + 'Do not attach a PDF to this email' => 'Voeg geen PDF toe in bijlage bij deze e-mail', + 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Geen herberekening vragen voor bestelling (Nummer: {orderNumber}) als er fouten aanwezig zijn.', + 'Donation can not be zero.' => 'Een donatie mag niet gelijk zijn aan nul.', + 'Donation needs to be an amount.' => 'De donatie moet een bedrag zijn.', + 'Donation settings saved.' => 'Donatie-instellingen opgeslagen.', + 'Donation' => 'Donatie', + 'Donations' => 'Donaties', + 'Done' => 'Klaar', + 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Geen verdere kortingen toepassen voor een bestelling als deze korting wordt toegepast', + 'Download PDF' => 'PDF downloaden', + 'Download PDF…' => 'PDF downloaden…', + 'Download Type' => 'Downloadtype', + 'Download' => 'Downloaden', + 'Draft' => 'Concept', + 'Dummy gateway payment failed.' => 'Dummygatewaybetaling mislukt.', + 'Duplicate options exist' => 'Er zijn dubbele opties', + 'Duration' => 'Duur', + 'EU VAT ID' => 'EU-btw-nummer', + 'Edit address' => 'Adres bewerken', + 'Edit adjustments' => 'Aanpassingen bewerken', + 'Edit catalog pricing rules' => 'Catalogusprijsregels bewerken', + 'Edit discounts' => 'Kortingen bewerken', + 'Edit options' => 'Opties bewerken', + 'Edit orders' => 'Bestellingen bewerken', + 'Edit sales' => 'Verkopen bewerken', + 'Edit' => 'Bewerken', + 'Effect' => 'Effect', + 'Either (Default) - The relationship field is on the purchasable or the category' => 'Eender (standaard) - Het relatieveld bevindt zich op het koopbare artikel of de categorie', + 'Either way' => 'Allebei', + 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Fout bij genereren van e-mail-PDF voor e-mail “{email}”. Bestelling: “{order}”. PDF-sjabloonfout: “{message}” {file}:{line}', + 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'PDF-sjabloon voor e-mail bestaat niet in “{templatePath}” voor e-mail “{email}”. Bestelling: “{order}”.', + 'Email Subject' => 'E-mail onderwerp', + 'Email error. No email address found for order. Order: “{order}”' => 'E-mailfout. Geen e-mailadres gevonden voor bestelling. Bestelling: "{order}"', + 'Email is not enabled.' => 'E-mail is niet ingeschakeld.', + 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'E-mailsjabloon platte tekst bestaat niet in “{templatePath}”. Dit heeft geresulteerd in “{templateParsedPath}” voor e-mail “{email}”. Bestelling: “{order}”.', + 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in platte tekst e-mailsjabloon voor e-mail “{email}”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', + 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in platte tekst e-mailsjabloonpad voor e-mail “{email}” in “Sjabloonpad:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', + 'Email required to make payments on a completed order.' => 'Het e-mailadres is verplicht voor betaling van een voltooide bestelling.', + 'Email saved.' => 'E-mail opgeslagen.', + 'Email sent' => 'E-mail verzonden', + 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'De e-mailsjabloon bestaat niet in “{templatePath}”. Dit heeft geresulteerd in “{templateParsedPath}” voor e-mail “{email}”. Bestelling: “{order}”.', + 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloon voor aangepaste e-mail “{email}” in “Aan:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloon voor e-mail “{email}” in “Bcc:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloon voor e-mail “{email}” in “Cc:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloon voor e-mail “{email}” in “Antwoorden aan:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloon voor e-mail “{email}” in “Onderwerp:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', + 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloon voor e-mail “{email}”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', + 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloonpad voor e-mail “{email}” in “Sjabloonpad:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', + 'Email unavailable.' => 'E-mailadres niet beschikbaar.', + 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'E-mail “{email}” kon niet worden verzonden voor bestelling “{order}”. Fout: {error} {file}:{line}', + 'Email “{email}” for order {order} was cancelled.' => 'E-mail "{email}" voor bestelling "{order}" is geannuleerd.', + 'Email' => 'E-mail', + 'Emails' => 'E-mails', + 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Schakel dit in als dit tarief moet worden opgenomen in de belastbare prijs in plaats van het toevoegen van een kostenpost aan de bestelling.', + 'Enable structure for products of this type' => 'Structuur inschakelen voor producten van dit type', + 'Enable this discount' => 'Schakel deze korting in', + 'Enable this rule' => 'Schakel deze regel in', + 'Enable this sale' => 'Schakel deze korting in', + 'Enable this shipping method on the front end' => 'Activeer deze verzendmethode aan de voorkant', + 'Enable this shipping rule' => 'Deze verzending regel inschakelen', + 'Enable this tax rate' => 'Schakel dit belastingtarief in', + 'Enabled for customers to select during checkout?' => 'Ingeschakeld voor klanten om te selecteren tijdens het afrekenen?', + 'Enabled for customers to select?' => 'Ingeschakeld voor selectie door klanten?', + 'Enabled' => 'Ingeschakeld', + 'Enabled?' => 'Ingeschakeld?', + 'End Date' => 'Einddatum', + 'Enter SKU' => 'Voer Artikelnummer in', + 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Voeg een mensvriendelijke naam in voor dit belastingtarief voor gebruik in het configuratiescherm.', + 'Enter a percentage like {ex1} or {ex2}.' => 'Voer een percentage in, bijvoorbeeld {ex1} of {ex2}.', + 'Enter coupon code' => 'Couponcode invoeren', + 'Enter reference' => 'Referentie invoeren', + 'Error refunding transaction: {transactionHash}' => 'Fout bij terugbetaling transactie: {transactionHash}', + 'Every new store must be assigned to at least one site.' => 'Elke nieuwe winkel moet aan minimaal één site worden toegewezen.', + 'Everywhere' => 'Overal', + 'Example' => 'Voorbeeld', + 'Exclude this discount for products that are already on promotion' => 'Sluit deze korting uit voor producten die al in de promotie zijn', + 'Expired Link' => 'Link verlopen', + 'Expired' => 'Verlopen', + 'Expiry Date' => 'Houdbaarheidsdatum', + 'Expiry date' => 'Vervaldatum', + 'Expiry' => 'Verloop', + 'Failed to receive transfer: {error}' => 'Overdracht ontvangen mislukt: {error}', + 'Failed to send email. Please try again.' => 'E-mail versturen mislukt. Probeer het opnieuw.', + 'Failed to start' => 'Start mislukt', + 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Bijwerken van {num, plural, =1{bestelstatus} other{bestelstatussen}} mislukt.', + 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Bijwerken van bestelstatus mislukt voor {num, plural, =1{bestelling} other{bestellingen}}.', + 'Feet (ft)' => 'Voet (ft)', + 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Aan het filteren op voorwaarden die beschrijven op welke bestelling deze regel van toepassing is. Voer 0 in om een voorwaarde over te slaan.', + 'First Name' => 'Voornaam', + 'Flat Amount Off Order' => 'Vast kortingbedrag voor bestelling', + 'Flat Order Discount Amount Off' => 'Afgetrokken vast kortingbedrag bestelling', + 'Free Order Payment Strategy' => 'Betaalstrategie gratis bestelling', + 'Free Shipping' => 'Gratis verzending', + 'Free orders are processed by the payment gateway' => 'Gratis bestellingen worden verwerkt door de betalingsgateway', + 'Free orders complete immediately' => 'Gratis bestellingen worden onmiddellijk uitgevoerd', + 'Free shipping can only be for whole order or matching items, not both.' => 'Gratis verzending is alleen van toepassing voor de hele bestelling of overeenstemmende artikelen, niet voor beide.', + 'From Name' => 'Van naam', + 'Fulfill' => 'Vervullen', + 'Fulfilled' => 'Vervuld', + 'Fulfillment' => 'Vervulling', + 'Full Name' => 'Volledige naam', + 'Gateway Code' => 'Gatewaycode', + 'Gateway Message' => 'Gatewaybericht', + 'Gateway Reference' => 'Gatewayreferentie', + 'Gateway Response' => 'Antwoord van de gateway', + 'Gateway doesn’t support authorize' => 'De gateway ondersteunt geen autorisatie', + 'Gateway doesn’t support partial refunds.' => 'De gateway ondersteunt geen gedeeltelijke terugbetalingen.', + 'Gateway doesn’t support purchase' => 'De gateway ondersteunt geen aankoop', + 'Gateway doesn’t support refunds.' => 'De gateway ondersteunt geen terugbetalingen.', + 'Gateway saved.' => 'Gateway opgeslagen.', + 'Gateway' => 'Gateway', + 'Gateways reordered.' => 'Gateways herschikt.', + 'Gateways' => 'Gateways', + 'General Settings' => 'Algemene instellingen', + 'General' => 'Algemeen', + 'Generate' => 'Genereren', + 'Generated Coupon Format' => 'Couponnotatie gegenereerd', + 'Grams (g)' => 'Gram (g)', + 'Groups for which this sale will be applicable to.' => 'Groepen waarop deze verkoop van toepassing is.', + 'HTML Email Template Path' => 'HTML e-mail sjabloonpad', + 'Handle' => 'Ingang', + 'Harmonized System Code' => 'Geharmoniseerde systeemcode', + 'Has Admin Notices' => 'Heeft beheerderskennisgevingen', + 'Has Emails?' => 'Heeft e-mails?', + 'Has Free Shipping' => 'Heeft gratis verzending', + 'Has Orders' => 'Heeft bestellingen', + 'Has Purchasable' => 'Heeft koopbare', + 'Has Variants?' => 'Heeft varianten?', + 'Height ({unit})' => 'Hoogte ({unit})', + 'Height' => 'Hoogte', + 'Hide snapshot' => 'Snapshot verbergen', + 'History' => 'Geschiedenis', + 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'De tijd (in seconden) die een pdf-downloadlink geldig moet blijven voordat hij verloopt. Standaard is dit 86.400 (24 uur).', + 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Hoe vaak één e-mailadres is toegestaan om deze korting te gebruiken. Dit geldt voor alle voorgaande bestellingen, voor zowel gast als gebruiker. Voor een nul in voor onbeperkt gebruik door gasten en gebruikers.', + 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Hoe vaak een gebruiker deze korting mag gebruiken. Als dit is ingesteld op iets anders dan nul, is de korting alleen beschikbaar voor aangemelde gebruikers.', + 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Hoe vaak deze korting in totaal kan worden gebruikt door gasten of aangemelde gebruikers. Voer een nul in voor onbeperkt gebruik.', + 'How products should be labeled within the control panel.' => 'Hoe producten op het configuratiescherm gelabeld moeten worden.', + 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'De wijze waarop de aankopen en categorieën met elkaar gerelateerd zijn. Dit bepaalt de overeenstemmende items. Zie [Relatieterminologie]({link}).', + 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'Hoe dit product wordt omschreven bij een lijnitem in een order. Je kunt tags met eigenschappen toevoegen, zoals {ex1} of {ex2}.', + 'How this shipping method will be referred to in templates and forms.' => 'Hoe naar deze verzendmethode in sjablonen en formulieren wordt verwezen.', + 'How variants should be labeled within the control panel.' => 'Hoe varianten op het configuratiescherm gelabeld moeten worden.', + 'How you’ll refer to this PDF in the templates.' => 'Hoe u naar deze pdf verwijst in de sjablonen.', + 'How you’ll refer to this product type in the templates.' => 'Hoe u zult verwijzen naar dit type product in de templates.', + 'How you’ll refer to this shipping category in the templates.' => 'Hoe deze verzendcategorie wordt genoemd in de sjablonen.', + 'How you’ll refer to this status in the templates.' => 'Hoe zult u verwijzen naar deze status in de templates.', + 'How you’ll refer to this subscription plan in the templates.' => 'Manier waarop u in sjablonen naar dit abonnementsplan verwijst.', + 'How you’ll refer to this tax category in the templates.' => 'Hoe zult u verwijzen naar deze fiscale categorie in de sjablonen.', + 'ID' => 'ID', + 'IP Address' => 'IP-adres', + 'If disabled, this PDF will not be available or sent with emails.' => 'Indien dit niet is geselecteerd, zal deze pdf niet beschikbaar zijn of niet verzonden worden met e-mails.', + 'If disabled, this email will not send.' => 'Bij uitschakeling wordt deze e-mail niet verzonden.', + 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Als dit is ingeschakeld en dit tarief niet overeen komt met de bestelling, wordt het tariefbedrag in mindering gebracht op de prijs in de winkelwagen.', + 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Indien ingesteld op \'Alleen met toestemming\', moet u handmatig de betalingen vastleggen voordat het geld zal worden overgemaakt op uw rekening. De Gateway moet de geselecteerde optie ondersteunen.', + 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Als u het percentage selecteert als “aftrekken van de itemprijs met korting”, omvat dit het “Bedrag per item” en alle andere eerder toegepaste kortingen.', + 'Ignore Promotions?' => 'Promoties negeren?', + 'Ignore previous matching sales if this sale matches.' => 'Eerdere overeenkomende verkopen negeren als deze verkoop overeenkomt.', + 'Ignore promotional prices when this discount is applied to matching line items' => 'Promotieprijzen negeren als deze korting is toegepast op overeenstemmende lijnitems', + 'Inactive Carts' => 'Inactieve Winkelwagens', + 'Inches (in)' => 'Inch (in)', + 'Include built-in line item tax.' => 'Ingebouwd lijnitembelasting opnemen.', + 'Include in price?' => 'Opnemen in prijs?', + 'Include line item discounts.' => 'Lijnitemkortingen opnemen.', + 'Include line item shipping costs.' => 'Verzendkosten voor lijnitem opnemen.', + 'Include separate line item tax.' => 'Afzonderlijke lijnitembelasting opnemen.', + 'Included in price?' => 'Opgenomen in prijs?', + 'Included' => 'Inbegrepen', + 'Incoming transfer from Transfer ID: ' => 'Inkomende overdracht van overdracht-ID: ', + 'Incoming' => 'Inkomend', + 'Info' => 'Info', + 'Information linked?' => 'Informatie gekoppeld?', + 'Information' => 'Informatie', + 'Invalid JSON' => 'JSON ongeldig', + 'Invalid Order ID' => 'Ongeldige bestelling-ID', + 'Invalid VAT ID.' => 'Ongeldig btw-nr.', + 'Invalid condition syntax' => 'Ongeldige voorwaardesyntaxis', + 'Invalid email.' => 'Ongeldig e-mailadres.', + 'Invalid formula syntax' => 'Ongeldige formulesyntaxis', + 'Invalid gateway: {value}' => 'Ongeldige gateway: {value}', + 'Invalid inventory movements.' => 'Ongeldige voorraadverplaatsingen.', + 'Invalid order condition syntax.' => 'De voorwaardesyntaxis van de bestelling is ongeldig.', + 'Invalid payment or order. Please review.' => 'Ongeldige betaling of bestelling. Controleer de gegevens.', + 'Invalid payment source ID: {value}' => 'Ongeldige betaalbron-ID: {value}', + 'Invalid store.' => 'Ongeldige winkel.', + 'Invalid user.' => 'Ongeldige gebruiker.', + 'Inventory Item' => 'Voorraaditem', + 'Inventory Location' => 'Voorraadlocatie', + 'Inventory Locations' => 'Voorraadlocaties', + 'Inventory Tracked' => 'Voorraad bijgehouden', + 'Inventory Transfers' => 'Voorraadoverdrachten', + 'Inventory could not be set.' => 'Voorraad kon niet worden ingesteld.', + 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'Voorraadlocatie heeft toegezegde voorraad, de bestelling(en) moet(en) eerst worden vervuld.', + 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Voorraadlocatie heeft inkomende voorraad, de overdracht(en) moet(en) eerst worden vervuld.', + 'Inventory location is already deactivated.' => 'Voorraadlocatie is al gedeactiveerd.', + 'Inventory location saved.' => 'Voorraadlocatie opgeslagen.', + 'Inventory locations not saved.' => 'Voorraadlocaties niet opgeslagen.', + 'Inventory movement could not be saved.' => 'Voorraadverplaatsing kon niet worden opgeslagen.', + 'Inventory movement saved.' => 'Voorraadverplaatsing opgeslagen.', + 'Inventory updated.' => 'Voorraad bijgewerkt.', + 'Inventory was not updated.' => 'Voorraad niet bijgewerkt.', + 'Inventory' => 'Voorraad', + 'Invoice amount' => 'Factuurbedrag', + 'Invoice date' => 'Factuurdatum', + 'Is Promotable' => 'Is promootbaar', + 'Is Promotional Price?' => 'Is promotieprijs?', + 'Is Shippable' => 'Is verzendbaar', + 'Is Taxable' => 'Is belastbaar', + 'Item Rates' => 'Itemtarieven', + 'Item Subtotal' => 'Subtotaal artikel', + 'Item Total' => 'Totaal artikel', + 'Item' => 'Item', + 'Items' => 'Artikelen', + 'Kilograms (kg)' => 'Kilogram (kg)', + 'Label' => 'Label', + 'Landscape' => 'Liggend', + 'Language' => 'Taal', + 'Last Name' => 'Achternaam', + 'Last Updated' => 'Laatst bijgewerkt', + 'Leave a category rate override blank to use the rate from above.' => 'Laat een categorietariefoverschrijving leeg om het tarief van hierboven te gebruiken.', + 'Leave blank for unlimited uses.' => 'Laat leeg voor onbeperkt gebruik.', + 'Leave blank if products don’t have URLs' => 'Leeg laten als de producten geen URL hebben', + 'Leave gateway subscription as-is' => 'Gatewayabonnement ongewijzigd laten', + 'Length ({unit})' => 'Lengte ({unit})', + 'Length' => 'Lengte', + 'Let each product choose which sites it should be saved to' => 'Laat elk product kiezen op welke sites het wordt opgeslagen', + 'Limit which orders this discount applies to based on its line items.' => 'Beperk op welke bestellingen deze korting van toepassing is op basis van de artikelen.', + 'Limit which purchasables this sale applies to.' => 'Beperk op welke koopbare artikelen deze aanbieding van toepassing is.', + 'Limit' => 'Limiet', + 'Line Item Statuses' => 'Lijnitemstatussen', + 'Line Item' => 'Lijnitem', + 'Line Items' => 'Regelartikel', + 'Line item price (minus discounts)' => 'Lijnitemprijs (min kortingen)', + 'Line item shipping cost' => 'Verzendkosten regelartikel', + 'Line item statuses reordered.' => 'Statussen lijnitems herschikt.', + 'Link Duration' => 'Duur van de link', + 'Link Sent' => 'Link verstuurd', + 'Link to a product' => 'Link naar een product', + 'Link to a variant' => 'Link naar een variant', + 'Link' => 'Link', + 'Live' => 'Live', + 'Location' => 'Locatie', + 'Locations that should be available for previewing products in this product type.' => 'Locaties die beschikbaar moeten zijn voor het bekijken van een voorbeeld van producten in dit producttype.', + 'MM' => 'MM', + 'Make a payment' => 'Verricht een betaling', + 'Make this the primary store' => 'Dit de primaire winkel maken', + 'Manage Inventory' => 'Voorraad beheren', + 'Manage donation settings' => 'Donatie-instellingen beheren', + 'Manage general store settings' => 'Algemene winkelinstellingen beheren', + 'Manage inventory locations' => 'Voorraadlocaties beheren', + 'Manage inventory stock levels' => 'Voorraadniveaus beheren', + 'Manage inventory transfers' => 'Voorraadoverdrachten beheren', + 'Manage orders' => 'Bestellingen beheren', + 'Manage payment currencies' => 'Betaalvaluta\'s beheren', + 'Manage promotions' => 'Promoties beheren', + 'Manage shipping' => 'Verzending beheren', + 'Manage store settings' => 'Winkelinstellingen beheren', + 'Manage subscription plans' => 'Abonnementsplannen beheren', + 'Manage subscription' => 'Abonnement beheren', + 'Manage subscriptions' => 'Abonnementen beheren', + 'Manage taxes' => 'Belastingen beheren', + 'Manage' => 'Beheren', + 'Mark as Pending' => 'Markeren als in afwachting', + 'Mark as completed' => 'Markeren als voltooid', + 'Match Billing Address' => 'Factuuradres matchen', + 'Match Customer' => 'Klant matchen', + 'Match Order' => 'Bestelling matchen', + 'Match Orders' => 'Bestellingen matchen', + 'Match Product' => 'Product matchen', + 'Match Purchasable' => 'Koopbare matchen', + 'Match Shipping Address' => 'Verzendadres matchen', + 'Match Variant' => 'Variant matchen', + 'Matching Items' => 'Overeenstemmende items', + 'Max Qty' => 'Maximumhoeveelheid', + 'Max Uses' => 'Max. aantal gebruiken', + 'Max Variants' => 'Max. aantal varianten', + 'Max quantity must greater than min.' => 'Maximumhoeveelheid moet groter zijn dan minimum.', + 'Maximum Purchase Quantity' => 'Maximale orderhoeveelheid', + 'Maximum Total Shipping Cost' => 'Maximale totale verzendkosten', + 'Maximum allowed quantity' => 'Maximaal toegestane hoeveelheid', + 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Maximaal aantal bijpassende items dat kan worden besteld zodat deze account van toepassing is. Met een waarde van nul wordt deze voorwaarde overgeslagen.', + 'Maximum order quantity for this item is {num}.' => 'De maximale bestelhoeveelheid voor dit artikel is {num}.', + 'Message' => 'Bericht', + 'Meters (m)' => 'Meter (m)', + 'Millimeters (mm)' => 'Millimeter (mm)', + 'Min Qty' => 'Minimumhoeveelheid', + 'Min quantity must be less than max.' => 'Minimumhoeveelheid moet keiner zijn dan maximum.', + 'Minimum Purchase Quantity' => 'Minimale bestelhoeveelheid', + 'Minimum Total Price Strategy' => 'Strategie voor minimale totaalprijs', + 'Minimum Total Shipping Cost' => 'Minimale totale verzendkosten', + 'Minimum allowed quantity' => 'Minimaal toegestane hoeveelheid', + 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Minimale aantal bijpassende items dat moet worden besteld om deze korting te krijgen.', + 'Minimum order quantity for this item is {num}.' => 'De minimale bestelhoeveelheid voor dit artikel is {num}.', + 'Missing Gateway' => 'Ontbrekende gateway', + 'Missing a default inventory location.' => 'Standaard voorraadlocatie ontbreekt.', + 'Move Inventory' => 'Voorraad verplaatsen', + 'Move To' => 'Verplaatsen naar', + 'Move {qty} from {fromType} to {toType}' => '{qty} verplaatsen van {fromType} naar {toType}', + 'Move' => 'Verplaatsen', + 'Movement from deactivated inventory location' => 'Verplaatsing van gedeactiveerde voorraadlocatie', + 'Movement' => 'Verplaatsing', + 'Must have at least one variant.' => 'Moet minstens één variant hebben.', + 'Name Field' => 'Naamveld', + 'Name' => 'Naam', + 'New Customer' => 'Nieuwe klant', + 'New Customers' => 'Nieuwe klanten', + 'New Order' => 'Nieuwe bestelling', + 'New PDF' => 'Nieuwe pdf', + 'New address' => 'Nieuw adres', + 'New catalog pricing rule' => 'Nieuwe catalogusprijsregel', + 'New currency' => 'Nieuwe valuta', + 'New discount' => 'Nieuwe korting', + 'New email' => 'Nieuwe e-mail', + 'New gateway' => 'Nieuwe gateway', + 'New line item status' => 'Nieuwe lijnitemstatus', + 'New line items get this status by default when the order is completed' => 'Nieuwe regelartikelen krijgen standaard deze status wanneer de bestelling voltooid is', + 'New location' => 'Nieuwe locatie', + 'New order status' => 'Status van nieuwe order', + 'New orders get this status by default' => 'Nieuwe bestellingen krijgen deze status standaard', + 'New product type' => 'Nieuw producttype', + 'New product' => 'Nieuw product', + 'New product, choose a type' => 'Nieuw product, kies een type', + 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Nieuwe producten worden standaard in de eerste beschikbare belastingcategorie geplaatst. Als er geen beschikbaar is, wordt deze categorie gebruikt.', + 'New sale' => 'Nieuwe korting', + 'New shipping category' => 'Nieuwe verzendcategorie', + 'New shipping method' => 'Nieuwe verzendmethode', + 'New shipping rule' => 'Nieuwe verzendingsregel', + 'New shipping zone' => 'Nieuw verzendingszone', + 'New subscription plan' => 'Nieuw abonnementsplan', + 'New tax category' => 'Nieuwe fiscale categorie', + 'New tax rate' => 'Nieuw BTW tarief', + 'New tax zone' => 'Nieuwe fiscale zone', + 'New transfer' => 'Nieuwe overdracht', + 'New {productType} product' => 'Nieuw {productType}-product', + 'New' => 'Nieuw', + 'Next payment' => 'Volgende betaling', + 'No Address' => 'Geen adres', + 'No PDFs exist yet.' => 'Er bestaan ​​nog geen pdf\'s.', + 'No access given to any specific store management features.' => 'Geen toegang gegeven tot specifieke winkelbeheerfuncties.', + 'No additional payment currencies exist yet.' => 'Er zijn geen aanvullende betaalvaluta\'s.', + 'No address' => 'Geen adres', + 'No billing address' => 'Geen factuuradres', + 'No catalog pricing rule exists with the ID “{id}”' => 'Er bestaat geen catalogusprijsregel met ID “{id}”', + 'No catalog pricing rules exist yet.' => 'Er bestaan nog geen catalogusprijsregels.', + 'No currency exists with the ID “{id}”' => 'Er bestaan geen valuta met ID "{id}"', + 'No customer email address exists on this cart.' => 'Er bestaat geen e-mailadres van een klant voor deze winkelwagen.', + 'No description' => 'Geen beschrijving', + 'No discount exists with the ID “{id}”' => 'Er bestaat geen korting met ID "{id}"', + 'No discounts exist yet.' => 'Er bestaan nog geen kortingen.', + 'No donation amount supplied.' => 'Geen donatiebedrag opgegeven.', + 'No emails exist yet.' => 'Er bestaan nog geen e-mails.', + 'No inventory changes made.' => 'Geen voorraadwijzigingen aangebracht.', + 'No inventory found.' => 'Geen voorraad gevonden.', + 'No inventory movements made.' => 'Geen voorraadverplaatsingen gedaan.', + 'No inventory transactions for this location.' => 'Geen voorraadtransacties voor deze locatie.', + 'No new customer selected.' => 'Geen nieuwe klant geselecteerd.', + 'No order history exists with the ID “{id}”' => 'Er bestaat geen bestegeschiedenis met ID “{id}”', + 'No order status history items will exist until the cart becomes an order.' => 'Er worden pas items aan de geschiedenis van de orderstatus toegevoegd nadat de winkelwagen een order is geworden.', + 'No payment source exists with the ID “{id}”' => 'Er bestaat geen betaalbron met ID “{id}”', + 'No private Note.' => 'Geen privénotitie.', + 'No product available.' => 'Geen product beschikbaar.', + 'No product types exist yet.' => 'Er bestaan nog geen producttypen.', + 'No purchasable available.' => 'Geen koopbare artikelen beschikbaar.', + 'No sale exists with the ID “{id}”' => 'Er bestaat geen aanbieding met ID “{id}”', + 'No sales exist yet.' => 'Er bestaan ​​nog geen verkopen.', + 'No shipping address' => 'Geen verzendadres', + 'No shipping category exists with the ID “{id}”' => 'Er bestaat geen verzendcategorie met ID "{id}"', + 'No shipping method exists with the ID “{id}”' => 'Er bestaat geen verzendmethode met ID “{id}”', + 'No shipping rule exists with the ID “{id}”' => 'Er bestaat geen verzendingsregel met ID “{id}”', + 'No shipping rules exist yet.' => 'Er bestaan nog geen verzendingsregels.', + 'No shipping zone exists with the ID “{id}”' => 'Verzendingszone met ID “{id}” bestaat niet', + 'No stats available.' => 'Geen statistieken beschikbaar.', + 'No subscription plan exists with the ID “{id}”' => 'Er bestaat geen abonnementsplan met ID “{id}”', + 'No subscription plans exist yet.' => 'Er bestaan nog geen abonnementsplannen.', + 'No tax category exists with the ID “{id}”' => 'Er bestaat geen belastingcategorie met ID “{id}”', + 'No tax rate exists with the ID “{id}”' => 'Er bestaat geen belastingtarief met ID “{id}”', + 'No tax zone exists with the ID “{id}”' => 'Er bestaat geen belastingzone met ID “{id}”', + 'No transactions exist.' => 'Er zijn geen transacties.', + 'No user authenticated.' => 'Geen gebruiker geauthenticeerd.', + 'No' => 'Nee', + 'None on hand' => 'Geen aanwezig', + 'None' => 'Geen', + 'Not a valid address type' => 'Geen geldig adrestype', + 'Not a valid credit card number.' => 'Geen geldig creditcardnummer.', + 'Not all SKUs are unique.' => 'Niet alle SKU\'s zijn uniek.', + 'Note' => 'Opmerking', + 'Notes' => 'Notities', + 'Number of Coupons' => 'Aantal coupons', + 'Number' => 'Aantal', + 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Op welke van de bovenstaande ingeschakelde sites moeten producten van dit producttype worden opgeslagen?', + 'On Hand' => 'Aanwezig', + 'Only allow this gateway to be used for zero value orders?' => 'Toestaan dat deze gateway alleen wordt gebruikt voor orders met waarde nul?', + 'Only match certain purchasables…' => 'Alleen bepaalde koopbare artikelen vergelijken…', + 'Only match purchasables related to…' => 'Alleen koopbare artikelen vergelijken gerelateerd aan…', + 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Alleen bestellingen met de volgende bestelstatussen worden meegenomen. Laat dit leeg voor alle statussen.', + 'Only save product to the site they were created in' => 'Product alleen opslaan op de site waarop het is gemaakt', + 'Options' => 'Opties', + 'Order Condition Formula' => 'Formule bestelvoorwaarde', + 'Order Description Format' => 'Bestellingbeschrijvingsformaat', + 'Order Details' => 'Bestellingsgegevens', + 'Order Fields' => 'Bestellingsvelden', + 'Order PDF Download Link' => 'Pdf-downloadlink van de bestelling', + 'Order PDF Filename Format' => 'Bestelling-PDF-Bestandsnaamformaat', + 'Order Reference Number Format' => 'Notatie orderreferentienummer', + 'Order Settings' => 'Bestellingsinstellingen', + 'Order Site' => 'Bestelsite', + 'Order Status description.' => 'Beschrijving bestellingsstatus.', + 'Order Status' => 'Bestelstatus', + 'Order Statuses' => 'Orderstatussen', + 'Order can not be empty.' => 'De bestelling mag niet leeg zijn.', + 'Order count' => 'Aantal bestellingen', + 'Order customer data removed.' => 'Klantgegevens bestellingen verwijderd.', + 'Order deleted.' => 'Bestelling verwijderd.', + 'Order fields saved.' => 'Bestellingsvelden opgeslagen.', + 'Order not found.' => 'Bestelling niet gevonden.', + 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Het saldo voor betaling van de bestelling is {outstandingBalanceAsCurrency}. Dit is de maximumwaarde die wordt aangerekend.', + 'Order recalculated.' => 'Bestelling herberekend.', + 'Order status saved.' => 'Bestelstatus opgeslagen.', + 'Order statuses reordered.' => 'Bestelstatussen herschikt.', + 'Order total shipping cost' => 'Totale verzendkosten bestelling', + 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Totale belastbare prijs bestelling (subtotaal van regelartikel + totale kortingen + totale verzendkosten)', + 'Order' => 'Bestelling', + 'Orders (Legacy)' => 'Bestellingen (Verouders)', + 'Orders deleted.' => 'Bestellingen verwijderd.', + 'Orders not restored.' => 'Bestellingen niet hersteld.', + 'Orders restored.' => 'Bestellingen hersteld.', + 'Orders' => 'Bestellingen', + 'Organization Name' => 'Organisatienaam', + 'Organization Tax ID' => 'Fiscaal nummer organisatie', + 'Origin and destination cannot be the same.' => 'Oorsprong en bestemming kunnen niet hetzelfde zijn.', + 'Origin' => 'Oorsprong', + 'Original Price' => 'Oorspronkelijke prijs', + 'Original price' => 'Oorspronkelijke prijs', + 'Original promotional price' => 'Oorspronkelijke promotieprijs', + 'Other Languages' => 'Andere talen', + 'Other countries' => 'Andere landen', + 'Outgoing transfer from Transfer ID: ' => 'Uitgaande overdracht van overdracht-ID: ', + 'Overpaid' => 'Teveel betaald', + 'Overrides previous?' => 'Vorige overschrijven?', + 'PDF Attachment' => 'Pdf-bijlage', + 'PDF Template Path' => 'Pad van PDF-sjabloon', + 'PDF saved.' => 'PDF opgeslagen.', + 'PDF' => 'PDF', + 'PDFs & Emails' => 'Pdf\'s en e-mails', + 'PDFs' => 'Pdf\'s', + 'Paid Amount' => 'Betaald bedrag', + 'Paid Status' => 'Status Betaald', + 'Paid' => 'Betaald', + 'Paper Orientation' => 'Papierstand', + 'Paper Size' => 'Papierformaat', + 'Partial payment not allowed.' => 'Gedeeltelijke betaling niet toegestaan.', + 'Partial' => 'Gedeeltelijk', + 'Past year' => 'Vorig jaar', + 'Past {num} days' => 'Afgelopen {num} dagen', + 'Pay {amount} of {currency} on the order.' => '{amount} {currency} betalen voor de bestelling.', + 'Pay' => 'Betalen', + 'Payment Amount' => 'Betaalbedrag', + 'Payment Currencies' => 'Betaalvaluta\'s', + 'Payment Gateway' => 'Betalingsgateway', + 'Payment Method' => 'Betaalmethode', + 'Payment error: {message}' => 'Betalingsfout: {message}', + 'Payment method issue' => 'Probleem met betaalmethode', + 'Payment source created.' => 'Betaalbron aangemaakt.', + 'Payment source deleted.' => 'Betaalbron verwijderd.', + 'Payments' => 'Betalingen', + 'Pending' => 'In afwachting', + 'Per Email Address Discount Limit' => 'Kortingslimiet per e-mailadres', + 'Per Item Amount Off' => 'Kortingbedrag per item', + 'Per Item Discount' => 'Korting per artikel', + 'Per Item Percentage Off' => 'Kortingpercentage per item', + 'Per Item Rate' => 'Per stuk prijs', + 'Per User Discount Limit' => 'Kortingslimiet per gebruiker', + 'Percentage Rate' => 'Percentage Tarief', + 'Phone (Alt)' => 'Telefoon (alt)', + 'Phone' => 'Telefoon', + 'Pick a plan' => 'Een plan selecteren', + 'Plain Text Email Template Path' => 'Sjabloonpad e-mail met platte tekst', + 'Plan' => 'Plan', + 'Plans reordered.' => 'Plannen herschikt.', + 'Portrait' => 'Staand', + 'Post Date' => 'Post datum', + 'Postal Code Formula' => 'Postcodeformule', + 'Pounds (lb)' => 'Pond (lb)', + 'Preview' => 'Preview', + 'Previous Status' => 'Vorige status', + 'Price' => 'Prijs', + 'Prices' => 'Prijzen', + 'Pricing Rules' => 'Prijsregels', + 'Pricing jobs are currently running.' => 'Er worden momenteel prijstaken uitgevoerd.', + 'Pricing' => 'Prijzen', + 'Primary Billing Address' => 'Primair factuuradres', + 'Primary Shipping Address' => 'Primair verzendadres', + 'Primary payment source updated.' => 'Primaire betaalbron bijgewerkt.', + 'Primary' => 'Primair', + 'Private Note' => 'Privénotitie', + 'Product Fields' => 'Productvelden', + 'Product ID is required.' => 'De product-ID is verplicht.', + 'Product Template' => 'Product Sjabloon', + 'Product Title Format' => 'Titelformaat product', + 'Product Type' => 'Producttype', + 'Product Types' => 'Producttypen', + 'Product URI Format' => 'URI-formaat product', + 'Product Variant' => 'Productvariant', + 'Product Variants' => 'Productvarianten', + 'Product type saved.' => 'Producttype opgeslagen.', + 'Product type settings' => 'Instellingen voor het producttype', + 'Product' => 'Product', + 'Products and Variants deleted.' => 'Producten en varianten verwijderd.', + 'Products not restored.' => 'Producten niet hersteld.', + 'Products restored.' => 'Producten hersteld.', + 'Products' => 'Producten', + 'Promotable' => 'Promootbaar', + 'Promotable?' => 'Promotie mogelijk?', + 'Promotional Amount' => 'Promotiebedrag', + 'Promotional Price' => 'Promotieprijs', + 'Purchasable Categories' => 'Koopbare categorieën', + 'Purchasable ID and Sale ID are required.' => 'ID koopbaar artikel en ID aanbieding zijn verplicht.', + 'Purchasable ID is required.' => 'ID koopbaar artikel is verplicht.', + 'Purchasable Type' => 'Koopbaar type', + 'Purchasable' => 'Koopbaar', + 'Purchase (Authorize and Capture Immediately)' => 'Kopen (onmiddellijk autoriseren en registreren)', + 'Purchase Total' => 'Ordertotaal', + 'Qty' => 'Aant.', + 'Quality Control' => 'Kwaliteitscontrole', + 'Quantity' => 'Hoeveelheid', + 'Rate' => 'Score', + 'Reassign {numOrders, plural, =1{order} other{orders}}' => '{numOrders, plural, one {}=1{bestelling} other{bestellingen}} opnieuw toewijzen', + 'Recalculate order' => 'Bestelling herberekenen', + 'Receive Inventory' => 'Voorraad ontvangen', + 'Receive Transfer' => 'Overdracht ontvangen', + 'Receive' => 'Ontvangen', + 'Received' => 'Ontvangen', + 'Recent Orders' => 'Recente bestellingen', + 'Recipient' => 'Ontvanger', + 'Recover Cart' => 'Winkelwagen herstellen', + 'Reduce price' => 'Prijs verminderen', + 'Reduce the price by a fixed amount' => 'De prijs verminderen met een vast bedrag', + 'Reduce the price by a percentage of the original price' => 'Verlaag de prijs met een percentage van de originele prijs', + 'Reference' => 'Verwijzing', + 'Refresh payment history' => 'Betalingsgeschiedenis vernieuwen', + 'Refund note' => 'Terugbetalingsnotitie', + 'Refund payment' => 'Betaling terugbetalen', + 'Refund' => 'Terugbetaling', + 'Reject' => 'Weigeren', + 'Rejected' => 'Geweigerd', + 'Relationship Type' => 'Relatietype', + 'Removable included tax rates are only allowed for the default tax zone.' => 'Verwijderbare opgenomen belastingtarieven zijn alleen toegestaan voor de standaard belastingzone.', + 'Remove address' => 'Adres verwijderen', + 'Remove all shipping costs from the order' => 'Alle verzendkosten verwijderen van de bestelling', + 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Verwijder klantassociatie en e-mailadres uit de {numOrders, plural, one {}=1{bestelling} other{bestellingen}}. Selecteer optioneel extra klantgegevens hieronder om te verwijderen', + 'Remove customer data' => 'Klantgegevens verwijderen', + 'Remove from price?' => 'Verwijderen van prijs?', + 'Remove shipping costs for matching items only' => 'Verzendkosten alleen verwijderen voor overeenstemmende items', + 'Remove the included tax when a valid organization tax ID is present?' => 'Inbegrepen belasting verwijderen als er een geldig fiscaal nummer aanwezig is voor een organisatie?', + 'Remove' => 'Verwijderen', + 'Removed' => 'Verwijderd', + 'Repeat Customers' => 'Terugkerende klanten', + 'Reply To' => 'Antwoorden aan', + 'Require Billing Address At Checkout' => 'Factuuradres vereisen bij afrekenen', + 'Require Coupon Code' => 'Couponcode vereisen', + 'Require Shipping Address At Checkout' => 'Verzendadres vereisen bij afrekenen', + 'Require Shipping Method Selection At Checkout' => 'Selectie van verzendmethode vereisen bij afrekenen', + 'Require' => 'Vereisen', + 'Reserved' => 'Gereserveerd', + 'Reset usage' => 'Gebruik resetten', + 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Beperk de korting tot de bestellingen waarbij de klant een minimum aantal waarde aan bijbehorende items heeft gekocht.', + 'Revenue Options' => 'Omzetopties', + 'Revenue' => 'Inkomsten', + 'Rule' => 'Regel', + 'Rules reordered.' => 'Regels herschikt.', + 'SKU' => 'Artikelnummer', + 'Safety' => 'Veiligheid', + 'Sale Price' => 'Prijs aanbieding', + 'Sale description.' => 'Aanbieding omschrijving.', + 'Sale reordered.' => 'Aanbieding herschikt.', + 'Sale saved.' => 'Aanbieding opgeslagen.', + 'Sale' => 'Aanbieding', + 'Sales deleted.' => 'Verkopen verwijderd.', + 'Sales updated.' => 'Aanbiedingen bijgewerkt.', + 'Sales' => 'Aanbiedingen', + 'Save and continue editing' => 'Opslaan en doorgaan met aanpassen', + 'Save and return to all orders' => 'Opslaan en terugkeren naar alle bestellingen', + 'Save and set rules' => 'Opslaan en voer regels in', + 'Save as a new rule' => 'Opslaan als nieuwe regel', + 'Save product to all sites enabled for this product type' => 'Product opslaan op alle sites die zijn ingeschakeld voor dit producttype', + 'Save product to other sites in the same site group' => 'Product opslaan op andere sites in dezelfde sitegroep', + 'Save product to other sites with the same language' => 'Product opslaan op andere sites met dezelfde taal', + 'Save' => 'Opslaan', + 'Search customer…' => 'Klant zoeken ...', + 'Search inventory' => 'Voorraad zoeken', + 'Search or enter customer email…' => 'E-mailadres klant zoeken of invoeren ...', + 'Search…' => 'Zoeken …', + 'See Orders' => 'Bestellingen weergeven', + 'Select a gateway' => 'Een gateway selecteren', + 'Select a tax category.' => 'Selecteer een BTW categorie.', + 'Select a tax zone. If empty, this rate will match anywhere.' => 'Selecteer een belastingzone. Als deze leeg is, is dit tarief overal van toepassing.', + 'Select address' => 'Adres selecteren', + 'Select an item' => 'Selecteer een item', + 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Selecteer hoe de catalogusprijsregel wordt toegepast op de koopbare artikelen.', + 'Select how the sale will be applied to the purchasable(s).' => 'Selecteer hoe de aanbieding wordt toegepast op de te koop aangeboden artikelen.', + 'Select product type' => 'Selecteer producttype', + 'Select the emails that will be sent when transitioning to this status.' => 'Selecteer de e-mails die zullen worden verstuurd wanneer er wordt overgegaan naar deze status.', + 'Select what this rate should be applied to.' => 'Selecteer waarop dit tarief moet worden toegepast.', + 'Send Email' => 'E-mail verzenden', + 'Send to custom recipient' => 'Verzenden naar aangepaste ontvanger', + 'Send to the customer' => 'Verstuur naar de klant', + 'Set Quantity' => 'Hoeveelheid instellen', + 'Set default category' => 'Standaardcategorie instellen', + 'Set default variant' => 'Standaardvariant instellen', + 'Set or Adjust' => 'Instellen of aanpassen', + 'Set price' => 'Prijs instellen', + 'Set status' => 'Status instellen', + 'Set the price to a flat amount' => 'Prijs instellen op vast bedrag', + 'Set the price to a percentage of the original price' => 'Stel de prijs in op een percentage van de oorspronkelijke prijs', + 'Set the sale price to a flat amount' => 'De prijs instellen op een vast bedrag', + 'Set the sale price to a percentage of the original price' => 'De prijs van de aanbieding instellen als een percentage van de oorspronkelijke prijs', + 'Set to' => 'Instellen op', + 'Settings saved.' => 'Instellingen opgeslagen.', + 'Settings' => 'Instellingen', + 'Share cart…' => 'Winkelwagen delen ...', + 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Verzending - de minimumkosten zijn de verzendkosten als de prijs van de bestelling lager is dan de verzendkosten.', + 'Shipping Address Zone' => 'Verzendadreszone', + 'Shipping Address' => 'Verzendadres', + 'Shipping Business Name' => 'Bedrijfsnaam voor verzending', + 'Shipping Categories' => 'Verzendcategorieën', + 'Shipping Category Conditions' => 'Voorwaarden verzendcategorie', + 'Shipping Category' => 'Verzendcategorie', + 'Shipping First Name' => 'Voornaam voor verzending', + 'Shipping Full Name' => 'Volledige naam voor verzending', + 'Shipping Last Name' => 'Achternaam voor verzending', + 'Shipping Method' => 'Verzendmethode', + 'Shipping Methods' => 'Verzend methoden', + 'Shipping Rule' => 'Verzendingsregel', + 'Shipping Zones' => 'Verzendingszones', + 'Shipping address required.' => 'Verzendadres verplicht.', + 'Shipping categories deleted.' => 'Verzendcategorieën verwijderd.', + 'Shipping category saved.' => 'Verzendcategorie opgeslagen.', + 'Shipping category updated.' => 'Verzendcategorie bijgewerkt.', + 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Verzendkosten die aan de bestelling worden toegevoegd als geheel voordat percentage-, item- en gewichtstarieven worden toegepast. Zet dit op nul om dit tarief uit te schakelen. De gehele regel, inclusief dit basistarief, wordt niet toegepast als de winkelwagen alleen niet-verzendbare items zoals digitale producten bevat.', + 'Shipping method saved.' => 'Verzendmethode opgeslagen.', + 'Shipping methods and rules deleted.' => 'Verzendcategorieën en regels verwijderd.', + 'Shipping methods updated.' => 'Verzendmethoden bijgewerkt.', + 'Shipping rule saved.' => 'Verzendingsregel opgeslagen.', + 'Shipping zone saved.' => 'Verzendingszone opgeslagen.', + 'Shipping' => 'Verzending', + 'Short Number' => 'Kort nummer', + 'Show Chart?' => 'Grafiek tonen?', + 'Show Order Count?' => 'Aantal bestellingen tonen?', + 'Show all prices' => 'Alle prijzen weergeven', + 'Show archived gateways' => 'Gearchiveerde gateways weergeven', + 'Show order count line on chart.' => 'Lijn met aantal bestellingen tonen in grafiek.', + 'Show related sales' => 'Gerelateerde verkopen weergeven', + 'Show rule details' => 'Regeldetails tonen', + 'Show the Dimensions and Weight fields for products of this type' => 'Toon de dimensies en gewicht velden voor producten van dit type', + 'Show the Title field for products' => 'Het titelveld tonen voor producten', + 'Show the Title field for variants' => 'Toon het titel veld voor varianten', + 'Signed In' => 'Aangemeld', + 'Site Languages' => 'Talen van de site', + 'Site store mapping saved.' => 'Site-winkelkoppeling opgeslagen.', + 'Sites' => 'Websites', + 'Slug' => 'Slug', + 'Snapshot' => 'Snapshot', + 'Snapshots' => 'Snapshots', + 'Some orders restored.' => 'Sommige bestellingen zijn hersteld.', + 'Some products restored.' => 'Sommige producten zijn hersteld.', + 'Some variants restored.' => 'Sommige verianten zijn hersteld.', + 'Something changed with the order before payment, please review your order and submit payment again.' => 'De bestelling is gewijzigd voorafgaand aan de betaling. Controleer uw bestelling en verricht de betaling opnieuw.', + 'Sorry, no matching options.' => 'Er zijn helaas geen overeenstemmende opties.', + 'Source - The purchasable relationship field is on the category' => 'Bron - het relatieveld van het koopbare artikel bevindt zich in de categorie', + 'Source' => 'Bron', + 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Geef een Twig-voorwaarde op die bepaalt of de korting van toepassing is voor een specifieke bestelling. (Via een `order` variabele kan worden verwezen naar de bestelling.)', + 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Geef een Twig-voorwaarde op die bepaalt of de verzendingsregel van toepassing is voor een specifieke bestelling. (Via een `order` variabele kan worden verwezen naar de bestelling.)', + 'Start Date' => 'Startdatum', + 'State' => 'Provincie', + 'Status Email Address' => 'E-mailadres status', + 'Status Emails' => 'Status e-mail', + 'Status History' => 'Statusgeschiedenis', + 'Status Updated.' => 'Status bijgewerkt.', + 'Status change message' => 'Melding van statuswijziging', + 'Status' => 'Status', + 'Stock' => 'Voorraad', + 'Stops Processing?' => 'Verwerking stoppen?', + 'Stops subsequent?' => 'Volgende stoppen?', + 'Store Location' => 'Winkellocatie', + 'Store Management' => 'Winkelbeheer', + 'Store Markets' => 'Winkelmarkten', + 'Store Rule' => 'Winkelregel', + 'Store saved.' => 'Winkel opgeslagen.', + 'Store' => 'Winkel', + 'Stores & Sites' => 'Winkels en sites', + 'Stores' => 'Winkels', + 'Strategy to apply when an order is free or has a zero balance.' => 'De strategie die moet worden toegepast voor een gratis bestelling of een bestelling met een nulsaldo.', + 'Strategy to apply when calculating the minimum order price.' => 'Toe te passen strategie bij het berekenen van de minimale bestelprijs.', + 'Subject' => 'Onderwerp', + 'Subscribing user' => 'Geabonneerde gebruiker', + 'Subscription Fields' => 'Abonnementsvelden', + 'Subscription Plans' => 'Abonnementsplannen', + 'Subscription Settings' => 'Abonnementsinstellingen', + 'Subscription cancelled.' => 'Abonnement geannuleerd.', + 'Subscription date' => 'Datum van abonnement', + 'Subscription fields saved.' => 'Abonnementsvelden opgeslagen.', + 'Subscription for {user} to {plan} prevented by a plugin.' => 'Abonnement van {user} voor {plan} is door een plug-in voorkomen.', + 'Subscription plan saved.' => 'Abonnementsplan opgeslagen.', + 'Subscription plan' => 'Abonnementsplan', + 'Subscription plans' => 'Abonnementsplannen', + 'Subscription reactivated.' => 'Abonnement opnieuw geactiveerd.', + 'Subscription reference' => 'Abonnementsreferentie', + 'Subscription started.' => 'Abonnement gestart.', + 'Subscription switched.' => 'Abonnement omgeschakeld.', + 'Subscription to “{plan}”' => 'Abonnement op “{plan}”', + 'Subscription' => 'Abonnement', + 'Subscriptions on hold' => 'Abonnementen in wachtstand', + 'Subscriptions' => 'Abonnementen', + 'Suppress emails' => 'E-mails onderdrukken', + 'Switch plan' => 'Ander plan selecteren', + 'Switch' => 'Wisselen', + 'System' => 'Systeem', + 'Table Columns' => 'Tabelkolommen', + 'Target - The category relationship field is on the purchasable' => 'Doel - het relatieveld categorie bevindt zich op het koopbare artikel', + 'Tax & Shipping' => 'Belasting en verzending', + 'Tax (inc)' => 'Belasting (incl.)', + 'Tax Categories' => 'BTW categorieën', + 'Tax Category' => 'Fiscale Categorie', + 'Tax Rates' => 'Belastingtarieven', + 'Tax Zone' => 'Fiscale Zone', + 'Tax Zones' => 'Fiscale zones', + 'Tax categories deleted.' => 'Belastingcategorieën verwijderd.', + 'Tax category saved.' => 'Belastingcategorie opgeslagen.', + 'Tax category updated.' => 'Belastingcategorie bijgewerkt.', + 'Tax rate saved.' => 'Belastingtarief opgeslagen.', + 'Tax rates updated.' => 'Belastingtarieven bijgewerkt.', + 'Tax zone saved.' => 'Belastingzone opgeslagen.', + 'Tax' => 'Belasting', + 'Taxable Subject' => 'Belastingplichtig persoon', + 'Template Path' => 'Template pad', + 'That handle is already in use' => 'Deze ingang is al in gebruik', + 'That handle is already in use.' => 'Deze ingang is al in gebruik.', + 'The PDF to attach to this email.' => 'De pdf die in bijlage wordt toegevoegd aan deze e-mail.', + 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'De URL van de pagina voor het bijwerken van factuurgegevens voor een abonnement en voor 3DS-authenticatie.', + 'The address provided is outside the store’s market.' => 'Het opgegeven adres is buiten het marktgebied van de winkel.', + 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'Het kortingbedrag dat wordt toegepast op de hele bestelling. Dit bedrag is verdeeld over de lijnitems van de hoogste tot de laagste prijs, totdat de korting is opgebruikt.', + 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'De basiskorting kan maar tot nul korting toepassen voor items in de winkelwagen en kan de bestelling niet negatief maken.', + 'The cart recovery link is invalid. Please request a new one.' => 'De link voor het herstellen van de winkelwagen is ongeldig. Vraag een nieuwe aan.', + 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Het conversietarief dat wordt gebruikt bij het omrekenen van een bedrag naar deze valuta. Als een item bijvoorbeeld {amount1} kost, dan is dit met een conversietarief van {rate} een bedrag van {amount2} in de andere valuta.', + 'The countries that orders are allowed to be placed from.' => 'De landen van waaruit bestellingen mogen worden geplaatst.', + 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'De gebruikslimiet van {limit} van de coupon "{code}" is overschreden.', + 'The customer for this order has been deleted.' => 'De klant voor deze bestelling is verwijderd.', + 'The default shipping category is automatically available to all product types.' => 'De standaardverzendcategorie is automatisch beschikbaar voor alle producttypes.', + 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'De gebruikslimiet van {limit} van de korting "{name}" is overschreden.', + 'The download link has expired. Please request a new one.' => 'De downloadlink is verlopen. Vraag een nieuwe aan.', + 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'Het e-mailadres waarvan bestelling status e-mails worden verzonden. Laat leeg om het Systeem E-mailadres vastgesteld in de algemene instellingen van Craft te gebruiken.', + 'The entry that contains the description for this subscription’s plan.' => 'Het item met een beschrijving voor dit abonnementsplan.', + 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'De vaste waarde van de korting voor elk item. Bijv. “3” voor $ 3 korting op elk item.', + 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'De gebruikte notatie om nieuwe coupons te genereren, bijvoorbeeld {example}. \'#\'-tekens worden vervangen door een willekeurige letter.', + 'The from and to inventory locations must be different.' => 'De voorraadlocatie van en naar moeten verschillend zijn.', + 'The inventory locations this store uses.' => 'De inventarisatielocaties die deze winkel gebruikt.', + 'The item is not enabled for sale.' => 'Het artikel is niet ingeschakeld voor verkoop.', + 'The language the order was made in.' => 'De taal waarin de bestelling is geplaatst.', + 'The language to be used when this email is rendered.' => 'De taal die moet worden gebruikt bij het renderen van deze e-mail.', + 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Het maximum aantal niveau\'s dat dit producttype kan hebben. Laat leeg als dit niet uitmaakt.', + 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Het maximumbedrag dat de klant zou moeten uitgeven aan verzending. Stel in op nul om uit te schakelen.', + 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Het minimumbedrag dat de klant zou moeten uitgeven aan verzending. Stel in op nul om uit te schakelen.', + 'The order is not valid.' => 'De bestelling is ongeldig.', + 'The payment gateway that will be used for the subscription plan.' => 'De betaalgateway die voor dit abonnementsplan wordt gebruikt.', + 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'De percentielwaarde van de korting voor elk item. Bijv. {ex1} voor {ex2} korting. Percentages worden afgerond tot op 2 decimalen.', + 'The previously-selected shipping method is no longer available.' => 'De eerder geselecteerde verzendmethode is niet meer beschikbaar.', + 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'De prijs van {description} is verhoogd van {originalSalePriceAsCurrency} naar {newSalePriceAsCurrency}', + 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'De prijs van {description} is gedaald van {originalSalePriceAsCurrency} naar {newSalePriceAsCurrency}', + 'The primary currency cannot be changed after orders are placed.' => 'De primaire valuta kan niet worden gewijzigd na het plaatsen van bestellingen.', + 'The purchasable defines the relationship' => 'Het koopbare artikel bepaalt de relatie', + 'The purchasable is related by another element' => 'Het koopbare artikel is gerelateerd aan een ander element', + 'The recipient of the email. Twig code can be used here.' => 'De ontvanger van de e-mail. Hier kan Twig-code worden gebruikt.', + 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'Het e-mailadres om te antwoorden. Laat dit leeg om het gewone e-mailadres van de afzender te gebruiken. Hier kunt u Twig-code gebruiken.', + 'The site the order was made in.' => 'De site waarop de bestelling is geplaatst.', + 'The site to be used when this email is rendered.' => 'De site die moet worden gebruikt bij het renderen van deze e-mail.', + 'The subject line of the email. Twig code can be used here.' => 'Het onderwerp van de e-mail. Hier kunt u Twig-code gebruiken.', + 'The template that the PDF should be generated from.' => 'De sjabloon op basis waarvan de pdf wordt gemaakt.', + 'The template to be used for HTML emails.' => 'De template die gebruikt moet worden voor HTML e-mail.', + 'The template to be used for plain text emails. Twig code can be used here.' => 'De sjabloon voor e-mails met platte tekst. Hier kunt u Twig-code gebruiken.', + 'The template to use when a product’s URL is requested.' => 'Het sjabloon om te gebruiken wanneer een URL van een product wordt opgevraagd.', + 'The total number of order adjustments changed.' => 'Het totale aantal bestellingsaanpassingen is veranderd.', + 'The total price of the order changed.' => 'De totale prijs van de bestelling is veranderd.', + 'The total quantity of items within the order changed.' => 'De totale hoeveelheid artikelen binnen de bestelling is veranderd.', + 'The unique SKU of the donation purchasable.' => 'De unieke SKU van de te kopen donatie.', + 'The unit of measurement that should be used when specifying product dimensions.' => 'De meet eenheid die gebruikt moet worden wanneer dimensies van een product worden gespecificeerd.', + 'The unit of measurement that should be used when specifying product weights.' => 'De meet eenheid die gebruikt moet worden wanneer gewichten van een product worden gespecificeerd.', + 'The webhook URL for this gateway.' => 'De webhook-URL voor deze gateway.', + 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'De “Van” naam die gebruikt zal worden bij het verzenden van bestelling status e-mails. Laat leeg om de Verzender Naam vastgesteld in de algemene instellingen van Craft te gebruiken.', + 'There are errors on the order' => 'De bestelling bevat fouten', + 'There are only {num} “{description}” items left in stock.' => 'Er zijn nog maar {num} “{description}”-artikelen op voorraad.', + 'There aren’t any product types to select yet.' => 'Er zijn nog geen producttypen om te selecteren.', + 'There is no gateway or payment source available for use with this order.' => 'Er is geen gateway of betalingsbron beschikbaar om te gebruiken bij deze bestelling.', + 'There is no gateway selected that supports payment sources.' => 'Er is geen gateway geselecteerd die betaalbronnen ondersteunt.', + 'There is no shipping method selected for this order.' => 'Voor deze bestelling is geen verzendmethode geselecteerd.', + 'This URL will load the cart into the user’s session, making it the active cart.' => 'Deze URL zal de winkelwagen laden in de sessie van de gebruiker en er de actieve winkelwagen van maken.', + 'This action is not allowed for the current user.' => 'Deze is actie is niet toegestaan voor de huidige gebruiker.', + 'This category will be used as the default for all purchasables in this store.' => 'Deze categorie wordt gebruikt als standaardcategorie voor alle koopbare artikelen in deze winkel.', + 'This coupon is for registered users and limited to {limit} uses.' => 'Deze coupon is voor geregistreerde gebruikers en maximaal {limit} keer bruikbaar.', + 'This coupon is limited to {limit} uses.' => 'Deze coupon mag {limit} keer gebruikt worden.', + 'This coupon requires an email address.' => 'Deze coupon vereist een e-mailadres.', + 'This gateway does not support that functionality.' => 'Deze gateway ondersteunt deze functionaliteit niet.', + 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Dit wordt overschreven door de configuratie-instelling {setting} in `config/{file}.php`.', + 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Dit is het adres waar uw winkel is gevestigd. Dit adres kan door verschillende invoegtoepassingen worden gebruikt, onder andere voor verzending en belastingen. Het kan ook in PDF-bonnen worden gebruikt.', + 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Dit is de standaard pdf die wordt weergegeven bij het opvragen van de bestel-pdf.', + 'This is the last location for the {store} store.' => 'Dit is de laatste locatie voor de winkel {store}.', + 'This month' => 'Deze maand', + 'This order has unsaved changes.' => 'Deze bestelling heeft niet-opgeslagen wijzigingen.', + 'This week' => 'Deze week', + 'This year' => 'Dit jaar', + 'Times Used' => 'Aantal keren gebruikt', + 'Title' => 'Titel', + 'To' => 'Aan', + 'Today' => 'Vandaag', + 'Too many variants for this product.' => 'Te veel varianten voor dit product.', + 'Top Customers by Average Order' => 'Topklanten per gemiddelde bestelling', + 'Top Customers by Total Revenue' => 'Topklanten volgens totale omzet', + 'Top Customers' => 'Topklanten', + 'Top Product Types by Qty Sold' => 'Topproducttypen volgens verkochte hoeveelheid', + 'Top Product Types by Revenue' => 'Top-producttypen volgens omzet', + 'Top Product Types' => 'Top-producttypen', + 'Top Products by Qty Sold' => 'Topproducten volgens verkochte hoeveelheid', + 'Top Products by Revenue' => 'Topproducten volgens omzet', + 'Top Products' => 'Topproducten', + 'Top Purchasables by Qty Sold' => 'Top koopbare artikelen volgens verkochte hoeveelheid', + 'Top Purchasables by Revenue' => 'Top koopbare artikelen volgens omzet', + 'Top Purchasables' => 'Top van de koopbare artikelen', + 'Total ' => 'Totaal ', + 'Total Discount Use Limit' => 'Totale gebruikslimiet korting', + 'Total Discount' => 'Totale korting', + 'Total Included Tax' => 'Totaal inclusief belasting', + 'Total Orders by Billing Country' => 'Totaal aantal bestellingen per land van facturering', + 'Total Orders by Country' => 'Totaal aantal bestellingen per land', + 'Total Orders by Shipping Country' => 'Totaal aantal bestellingen per land van verzending', + 'Total Orders' => 'Totaal aantal bestellingen', + 'Total Paid' => 'Totaal betaald', + 'Total Price' => 'Totale prijs', + 'Total Qty' => 'Totaal aantal', + 'Total Revenue' => 'Totale omzet', + 'Total Shipping' => 'Totale verzendkosten', + 'Total Tax' => 'Totaal belasting', + 'Total Weight' => 'Totaal gewicht', + 'Total' => 'Totaal', + 'Track Inventory' => 'Voorraad bijhouden', + 'Transaction Hash' => 'Transactiehash', + 'Transaction ID' => 'Transactie-ID', + 'Transaction captured successfully: {message}' => 'Transactie succesvol vastgelegd: {message}', + 'Transaction refunded successfully: {message}' => 'Transactie succesvol terugbetaald: {message}', + 'Transactions' => 'Transacties', + 'Transfer Fields' => 'Overdrachtvelden', + 'Transfer Items' => 'Overdrachtsitems', + 'Transfer Settings' => 'Overdrachtsinstellingen', + 'Transfer Status' => 'Overdrachtsstatus', + 'Transfer fields saved.' => 'Overdrachtsvelden opgeslagen.', + 'Transfer must have at least one item.' => 'Overdracht moet minimaal één item bevatten.', + 'Transfer' => 'Overdracht', + 'Transfers' => 'Overdrachten', + 'Trial days credited' => 'Dagen voor proefversie gecrediteerd', + 'Trial expiration' => 'Proefperiode verlopen', + 'Trial expiry date' => 'Vervaldatum proefperiode', + 'Type not in allowed options.' => 'Type maakt geen deel uit van de toegestane opties.', + 'Type' => 'Type', + 'URI' => 'URI', + 'Unable to cancel subscription at this time.' => 'Annulering van het abonnement is momenteel niet mogelijk.', + 'Unable to complete order: another request is already in progress.' => 'Kan bestelling niet voltooien: er loopt al een andere aanvraag.', + 'Unable to find variant.' => 'Kan variant niet vinden.', + 'Unable to generate coupon codes: {message}' => 'Kan couponcodes niet genereren: {message}', + 'Unable to make payment at this time.' => 'Betalen is momenteel niet mogelijk.', + 'Unable to modify subscription at this time.' => 'Wijzigen van het abonnement is momenteel niet mogelijk.', + 'Unable to reactivate subscription at this time.' => 'Opnieuw activeren van het abonnement is momenteel niet mogelijk.', + 'Unable to reassign orders.' => 'Kan bestellingen niet opnieuw toewijzen.', + 'Unable to remove order data.' => 'Kan bestelgegevens niet verwijderen.', + 'Unable to retrieve Sale and Purchasable.' => 'Aanbieding en koopbaar artikel ophalen niet mogelijk.', + 'Unable to retrieve cart.' => 'Winkelwagen ophalen is niet mogelijk.', + 'Unable to retrieve customer.' => 'Klant ophalen niet mogelijk.', + 'Unable to retrieve load cart URL' => 'Kan URL om de winkelwagen te laden niet ophalen', + 'Unable to retrieve payment source.' => 'Kan betaalbron niet ophalen.', + 'Unable to set default shipping category.' => 'Kan de standaard verzendcategorie niet instellen.', + 'Unable to set default tax category.' => 'Kan de standaard belastingcategorie niet instellen.', + 'Unable to set primary payment source.' => 'Kan primaire betaalbron niet instellen.', + 'Unable to start the subscription. Please check your payment details.' => 'Starten van het abonnement is niet mogelijk. Controleer uw betalingsgegevens.', + 'Unable to subscribe at this time.' => 'Abonneren is op dit moment niet mogelijk.', + 'Unable to update cart.' => 'Winkelwagen bijwerken is niet mogelijk.', + 'Unable to validate address.' => 'Kan adres niet valideren.', + 'Unit Price' => 'Eenheidsprijs', + 'Unit price (minus discounts)' => 'Eenheidsprijs (min kortingen)', + 'Units' => 'Eenheden', + 'Unpaid' => 'Niet-betaald', + 'Unsubscribe' => 'Afmelden', + 'Update Address' => 'Adres bijwerken', + 'Update Order Status' => 'Orderstatus bijwerken', + 'Update Order Status…' => 'Bestellingsstatus bijwerken ...', + 'Update order' => 'Bestelling bijwerken', + 'Update subscription' => 'Abonnement bijwerken', + 'Update' => 'Updaten', + 'Updated By' => 'Bijgewerkt door', + 'Updated committed stock successfully.' => 'Toegezegde voorraad bijgewerkt.', + 'Updated' => 'Bijgewerkt', + 'Use Billing Address For Tax' => 'Factuuradres gebruiken voor belasting', + 'Use as the primary billing address' => 'Gebruiken als primair factuuradres', + 'Use as the primary shipping address' => 'Gebruiken als primair verzendadres', + 'Used By Tax Rates' => 'Gebruikt door belastingtarieven', + 'Used by Tax Rates' => 'Gebruikt door belastingtarieven', + 'User Groups' => 'Gebruikersgroepen', + 'User not found.' => 'Gebruiker niet gevonden.', + 'User' => 'Gebruiker', + 'Uses' => 'Aantal gebruiken', + 'Validate Business Tax ID as Vat ID' => 'Fiscaal ondernemingsnummer valideren als btw-nummer', + 'Validating condition syntax' => 'Validatie voorwaardesyntaxis', + 'Validating formula syntax' => 'Validatie formulesyntaxis', + 'Variant Fields' => 'Variantvelden', + 'Variant Has Untracked Stock' => 'Variant heeft niet-bijgehouden voorraad', + 'Variant Price' => 'Variantprijs', + 'Variant SKU' => 'Variant-SKU', + 'Variant Search' => 'Variant zoeken', + 'Variant Stock' => 'Variantvoorraad', + 'Variant Title Format' => 'Variant titel formaat', + 'Variant Tracks Stock' => 'Voorraad van variant wordt bijgehouden', + 'Variant UI Label Format' => 'Variant UI-labelformaat', + 'Variant has no product.' => 'Variant heeft geen product.', + 'Variants not restored.' => 'Varianten niet hersteld.', + 'Variants restored.' => 'Varianten hersteld.', + 'Variants' => 'Varianten', + 'View customer' => 'Klant bekijken', + 'View order' => 'Bestelling bekijken', + 'View product type - {productType}' => 'Producttype bekijken - {productType}', + 'View user' => 'Gebruiker bekijken', + 'View' => 'Weergeven', + 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Waarschuwing: door deze valuta te verwijderen worden alle betalingen en terugbetalingen in deze valuta gestopt. Weet u zeker dat u “{name}” wilt verwijderen?', + 'Web' => 'Web', + 'Webhook URL' => 'Webhook-URL', + 'Weight ({unit})' => 'Gewicht ({unit})', + 'Weight Rate' => 'Gewicht percentage', + 'Weight Unit' => 'Eenheid gewicht', + 'Weight' => 'Gewicht', + 'What product URIs should look like for the site.' => 'Hoe URI\'s van producten eruit moeten zien voor de website.', + 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Hoe de automatisch gegenereerde producttitels eruit moeten zien. U kunt tags bijvoegen die producteigenschappen uitvoeren, zoals {ex1} of {ex2}. Alle gebruikte aangepaste velden moeten worden ingesteld als verplicht.', + 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Hoe de automatisch gegenereerde variant titels eruit moeten zien. U kunt tags bijvoegen die eigenschappen van de variant uitzetten, zoals {ex1} of {ex2}. Alle aangepaste velden die gebruikt worden moeten worden ingesteld als vereist.', + 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Hoe de pdf-bestandsnaam van de bestelling eruit moet zien (zonder extensie). U kunt tags toevoegen die eigenschappen van de bestelling weergeven, zoals {ex1} of {ex2}.', + 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Hoe de unieke automatisch gegenereerde SKU\'s eruit moeten zien, wanneer een SKU veld wordt ingevoerd zonder waarde. U kunt tags bijvoegen die eigenschappen uitzetten, zoals {ex1} of {ex2}.', + 'What this PDF will be called in the control panel.' => 'De naam van deze pdf in het configuratiescherm.', + 'What this catalog pricing rule will be called in the control panel.' => 'De naam van deze catalogusprijsregel in het configuratiescherm.', + 'What this discount will be called in the control panel.' => 'Naam van deze korting in het configuratiescherm.', + 'What this email will be called in the control panel.' => 'De naam van dit e-mailbericht in het configuratiescherm.', + 'What this product type will be called in the control panel.' => 'Naam van dit producttype in het configuratiescherm.', + 'What this sale will be called in the control panel.' => 'De naam van deze verkoop in het configuratiescherm.', + 'What this shipping category will be called in the control panel.' => 'De naam van deze verzendcategorie in het configuratiescherm.', + 'What this shipping rule will be called in the control panel.' => 'De naam van deze verzendingsregel in het configuratiescherm.', + 'What this shipping zone will be called in the control panel.' => 'De naam van deze verzendingszone in het configuratiescherm.', + 'What this status will be called in the control panel.' => 'Naam van deze status in het configuratiescherm.', + 'What this subscription plan will be called in the control panel.' => 'De naam van dit abonnement in het configuratiescherm.', + 'What this tax category will be called in the control panel.' => 'De naam van deze belastingcategorie in het configuratiescherm.', + 'What this tax zone will be called in the control panel.' => 'De naam van deze belastingzone in het configuratiescherm.', + 'When this discount is applied to an order, which line items should be discounted?' => 'Wanneer deze korting wordt toegepast op een bestelling, op welke artikelen moet deze dan worden toegepast?', + 'Whether the first available shipping method option should be set automatically on carts.' => 'Of de eerste beschikbare verzendmethode automatisch moet worden ingesteld voor winkelwagens.', + 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Of de primaire betaalbron van de gebruiker automatisch moet worden ingesteld voor nieuwe winkelwagens.', + 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Of het primaire verzend- en factuuradres automatisch moeten worden ingesteld voor nieuwe winkelwagens.', + 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Aanduiding of deze catalogusprijsregel beschikbaar moet zijn voor gebruik, ongeacht andere voorwaarden.', + 'Whether this sale should be available for use, regardless of other conditions.' => 'Aanduiding of deze verkoop beschikbaar moet zijn voor gebruik, ongeacht andere voorwaarden.', + 'Which data to display in the name column in the results table.' => 'Welke gegevens worden weergegeven in de naamkolom van de resultatentabel.', + 'Which product types should this category be available to?' => 'Voor welke producttypen moet deze categorie beschikbaar zijn?', + 'Which template should be loaded when a product’s URL is requested.' => 'De sjabloon die moet worden geladen bij het opvragen van de URL van een product.', + 'Width ({unit})' => 'Breedte ({unit})', + 'Width' => 'Breedte', + 'YYYY' => 'YYYY', + 'Yes' => 'Ja', + 'You are not allowed to add a line item.' => 'Het is niet toegestaan om een lijnitem toe te voegen.', + 'You currently have no emails configured to select for this status.' => 'U heeft momenteel geen e-mails geconfigureerd die u voor deze status kunt selecteren.', + 'You do not have permission to load this cart.' => 'U heeft geen rechten om deze winkelwagen te laden.', + 'You must set up at least one gateway that supports subscriptions first.' => 'U moet eerst minimaal één gateway instellen die abonnementen ondersteunt.', + 'You must be logged in or provide a valid token to load this cart.' => 'U moet ingelogd zijn of een geldig token verstrekken om deze winkelwagen te laden.', + 'You must be signed in to create a payment source.' => 'U moet zijn aangemeld om een betaalbron aan te maken.', + 'You must be signed in to set a primary payment source.' => 'U moet zijn aangemeld om een primaire betaalbron in te stellen.', + 'You must make a payment to complete the order.' => 'U moet een betaling verrichten om de bestelling te voltooien.', + 'Your Cart Recovery Link' => 'Uw link voor het herstellen van de winkelwagen', + 'Your Order PDF Download Link' => 'De pdf-downloadlink van uw bestelling', + 'Your order is empty' => 'Uw bestelling is leeg', + 'ZIP file' => 'ZIP-bestand', + 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Nul - de minimumprijs is nul als kortingen hoger zijn dan de bestelwaarde.', + 'Zip Code' => 'Postcode', + 'all' => 'alle', + 'any' => 'elke', + 'average order total' => 'gemiddelde totaalprijs bestellingen', + 'billing address' => 'factuuradres', + 'donation' => 'donatie', + 'donations' => 'donaties', + 'info' => 'info', + 'inventory location' => 'voorraadlocatie', + 'new customers' => 'nieuwe klanten', + 'on hand' => 'aanwezig', + 'only' => 'enkel', + 'order' => 'bestelling', + 'orders' => 'bestellingen', + 'price' => 'prijs', + 'prices' => 'prijzen', + 'product variant' => 'productvariant', + 'product variants' => 'productvarianten', + 'product' => 'product', + 'products' => 'producten', + 'repeat customers' => 'terugkerende klanten', + 'shipping address' => 'verzendadres', + 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'shippingSameAsBilling en billingSameAsShipping kunnen niet beide worden ingesteld.', + 'subscription' => 'abonnement', + 'subscriptions' => 'abonnementen', + 'to' => 'aan', + 'transfer' => 'overdracht', + 'transfers' => 'overdrachten', + '{amount} included' => '{amount} inbegrepen', + '{count} Unfulfilled Orders' => '{count} onvervulde bestellingen', + '{description} is no longer available.' => '{description} is niet meer beschikbaar.', + '{description} only has {stock} in stock.' => '{description} heeft maar {stock} op voorraad.', + '{from} to {to}' => '{from} naar {to}', + '{name} (Primary)' => '{name} (primair)', + '{name} (Trashed)' => '{name} (verwijderd)', + '{name} catalog price' => 'Catalogusprijs {name}', + '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, =1{bestelling} other{bestellingen}} bijgewerkt.', + '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, one {}=1{bestelling is} other{bestellingen zijn}} geassocieerd met de {numUsers, plural, one {}=1{gebruiker} other{gebruikers}}.', + '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, one {}=1{abonnement is} other{abonnementen zijn}} geactiveerd voor de {numUsers, plural, one {}=1{gebruiker} other{gebruikers}}.', + '{number} more…' => '{number} extra...', + '{pct} off the discounted item price' => '{pct} korting op de itemprijs met korting', + '{pct} off the original item price' => '{pct} korting op de oorspronkelijke itemprijs', + '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, one {}=1{is} other{zijn}} niet toegevoegd aan een site.', + '{total} in total revenue' => '{total} aan totale omzet', + '{total} orders' => '{total} bestellingen', + '{total} saleable across {locationCount} location(s)' => '{total} verkoopbaar op {locationCount} locatie(s)', + '{uses} uses across {emails} email addresses' => '{uses} gebruiksinstanties voor {emails} e-mailadressen', + '{uses} uses across {users} users' => '{uses} gebruiken voor {users} gebruikers', + '“{description}” is currently out of stock.' => '“{description}” is momenteel niet voorradig.', + '“{key}” has invalid JSON' => '“{key}” bevat ongeldige JSON', +]; diff --git a/lang/pt/commerce.php b/lang/pt/commerce.php new file mode 100644 index 0000000000..5f9f917213 --- /dev/null +++ b/lang/pt/commerce.php @@ -0,0 +1,1423 @@ + '(novo preço)', + '(of original price)' => '(do preço original)', + '(off original price)' => '(de desconto no preço original)', + 'A cart number must be specified.' => 'Deve ser especificado um número de carrinho.', + 'A cart recovery link has been sent to {email}.' => 'Foi enviado um link para recuperar o carrinho para {email}.', + 'A cart recovery link will be sent to {email}.' => 'Será enviado um link para recuperar o carrinho para {email}.', + 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Um número de referência amigável será gerado com base neste formato quando o carrinho for convertido num pedido. Por exemplo {ex1}, ou
{ex2}. O resultado deste formato deve ser único.', + 'A new download link has been sent to {email}' => 'Um novo link para download foi enviado para {email}', + 'A new download link will be sent to {email}' => 'Um novo link para download será enviado para {email}', + 'A valid email is required to create a customer.' => 'É necessário um email válido para criar um cliente.', + 'Accept' => 'Aceitar', + 'Accepted' => 'Aceite', + 'Actions' => 'Ações', + 'Active Carts' => 'Carrinhos Ativos', + 'Active subscriptions' => 'Subscrições ativas', + 'Active' => 'Ativo', + 'Add Address' => 'Adicionar endereço', + 'Add a coupon' => 'Adicionar um cupão', + 'Add a custom line item' => 'Adicionar um item de linha personalizado', + 'Add a line item' => 'Adicionar um item de linha', + 'Add a product' => 'Adicionar um produto', + 'Add a variant' => 'Adicionar um variante', + 'Add an adjustment' => 'Adicionar um ajuste', + 'Add an item' => 'Adicionar um item', + 'Add an option' => 'Adicionar uma opção', + 'Add catalog price' => 'Adicionar preço de catálogo', + 'Add' => 'Adicionar', + 'Additional Actions' => 'Ações adicionais', + 'Additional recipients that should receive this email. Twig code can be used here.' => 'Destinatários adicionais que devem receber este email. O código Twig pode ser usado aqui.', + 'Address 1' => 'Endereço 1', + 'Address 2' => 'Endereço 2', + 'Address 3' => 'Endereço 3', + 'Address Line 1' => 'Linha de Endereço 1', + 'Address Line 2' => 'Endereço linha 2', + 'Address Updated.' => 'Endereço atualizado.', + 'Address copied to user.' => 'Endereço copiado para o utilizador.', + 'Address not found.' => 'Endereço não encontrado.', + 'Adjust Quantity' => 'Ajustar a quantidade', + 'Adjust by' => 'Ajustar por', + 'Adjust price when included rate is disqualified?' => 'Ajustar o preço quando a tarifa incluída for desqualificada?', + 'Adjustments' => 'Ajustes', + 'Admin Notices' => 'Avisos da administração', + 'Administrative Area Code of Origin' => 'Código da área administrativa de origem', + 'Advanced' => 'Avançado', + 'All Orders' => 'Todos os Pedidos', + 'All Totals' => 'Todos os totais', + 'All Transfers' => 'Todas as transferências', + 'All active subscriptions' => 'Todas as subscrições ativas', + 'All customers' => 'Todos os clientes', + 'All products' => 'Todos os produtos', + 'All variants must have a SKU.' => 'Todas as variantes devem ter um SKU.', + 'All' => 'Tudo', + 'Allow Checkout Without Payment' => 'Permitir checkout sem pagamento', + 'Allow Empty Cart On Checkout' => 'Permitir carrinho vazio no checkout', + 'Allow Partial Payment On Checkout' => 'Permitir pagamento parcial no checkout', + 'Allow out of stock purchases' => 'Permitir compras fora de stock', + 'Allow' => 'Permitir', + 'Allowed Qty' => 'Qntd Permitida', + 'Alternative Phone' => 'Telefone alternativo', + 'Amount' => 'Valor', + 'An ID must be provided' => 'Deve ser fornecida uma identificação', + 'An error occurred while generating this PDF.' => 'Ocorreu um erro ao gerar este PDF.', + 'Any' => 'Qualquer', + 'Anywhere' => 'Qualquer sítio', + 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Tem a certeza de que pretende arquivar o plano de subscrição “{name}”? NÃO IRÁ cancelar as subscrições existentes.', + 'Are you sure you want to capture this transaction?' => 'Tem certeza que deseja capturar essa transação?', + 'Are you sure you want to complete this order?' => 'Tem certeza de que deseja concluir este pedido?', + 'Are you sure you want to delete the selected orders?' => 'Tem a certeza que quer eliminar os pedidos selecionados?', + 'Are you sure you want to delete the selected product and its variants?' => 'Tem a certeza que quer eliminar o produto selecionado e seus variantes?', + 'Are you sure you want to delete this shipping rule?' => 'Tem a certeza de que pretende eliminar esta regra de transporte?', + 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Tem certeza que você quer deletar “{name}” e todos os seus produtos? Por favor, faça um backup do seu banco de dados antes de realizar essa ação destrutiva.', + 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Tem a certeza que quer eliminar "{name}"? Isto definirá todos os itens de linha com este estado para sem estado.', + 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Tem a certeza de que deseja marcar esta transferência como pendente? Isto será mostrado como a entrar no destino.', + 'Are you sure you want to overwrite the billing address?' => 'Tem a certeza que deseja substituir este endereço de faturação?', + 'Are you sure you want to overwrite the shipping address?' => 'Tem a certeza que deseja substituir este endereço de envio?', + 'Are you sure you want to permanently delete this store and everything in it?' => 'Tem a certeza de que quer eliminar permanentemente esta loja e tudo o que nela se encontra?', + 'Are you sure you want to refund this transaction?' => 'Tem certeza que você deseja estornar essa transação?', + 'Are you sure you want to remove this customer?' => 'Tem a certeza que deseja eliminar este cliente?', + 'Are you sure you want to save this as a new shipping rule?' => 'Tem a certeza de que pretende guardar esta nova regra de transporte?', + 'Are you sure you want to send email: {name}?' => 'Tem a certeza de que deseja enviar o email: {name}?', + 'At least one site must be enabled for the product type.' => 'Pelo menos um site deve estar ativo para o tipo de produto.', + 'Attempted Payments' => 'Tentativas de pagamento efetuadas', + 'Attention' => 'Atenção', + 'Authorize Only (Manually Capture)' => 'Apenas Autorizar (Capturar Manualmente)', + 'Auto Set Cart Shipping Method Option' => 'Opção de método de envio do carrinho de compras definido automaticamente', + 'Auto Set New Cart Addresses' => 'Definir automaticamente novos endereços de carrinho de compras', + 'Auto Set Payment Source' => 'Definição automática da fonte de pagamento', + 'Automatic SKU Format' => 'Formato SKU Automático', + 'Available Shipping Categories' => 'Categorias de Envio Disponíveis', + 'Available Tax Categories' => 'Categorias de Tributos Disponíveis', + 'Available for purchase' => 'Disponível para compra', + 'Available for purchase?' => 'Disponível para compra?', + 'Available inventory for "{description}" has gone below zero.' => 'O stock disponível de "{description}" ficou abaixo de zero.', + 'Available to Product Types' => 'Disponível para Tipos de Produtos', + 'Available' => 'Disponível', + 'Available?' => 'Disponível?', + 'Average Order Total' => 'Total médio de pedidos', + 'Average' => 'Média', + 'BCC’d Recipient' => 'Cópia oculta para', + 'Bad Request' => 'Pedido inválido', + 'Bad address ID.' => 'ID do endereço inválida.', + 'Bad order ID.' => 'ID de pedido inválido.', + 'Base Price' => 'Preço de base', + 'Base Promotional Price' => 'Preço promocional de base', + 'Base Rate' => 'Custo Base', + 'Base' => 'Base', + 'Bcc' => 'Cco', + 'Billing Address' => 'Endereço de Cobrança', + 'Billing Business Name' => 'Nome da Empresa para Cobrança', + 'Billing First Name' => 'Primeiro nome de Cobrança', + 'Billing Full Name' => 'Nome Completo de Cobrança', + 'Billing Last Name' => 'Último nome de Cobrança', + 'Billing address required.' => 'É necessário o endereço de faturação.', + 'Billing detail update URL' => 'URL de atualização de detalhes de faturação', + 'Billing issues' => 'Questões de faturação', + 'Billing' => 'Faturação', + 'Both (Line item price + Line item shipping costs)' => 'Ambos (preço do item individual + custos de envio do item individual)', + 'Business ID' => 'CNPJ', + 'Business Name' => 'Nome da Empresa', + 'Business Tax ID' => 'CNPJ da Empresa', + 'CC’d Recipient' => 'Destinatário de CC', + 'CVV' => 'CVV', + 'Can be used as an internal reference.' => 'Pode ser utilizado como referência de intervalo.', + 'Can not complete payment for missing transaction.' => 'Não é possível completar o pagamento para a transação não encontrada.', + 'Can not create a new order' => 'Não é possível criar um novo pedido', + 'Can not find an order to pay.' => 'Não foi possível encontrar um pedido por pagar.', + 'Can not find enabled email.' => 'Não foi possível encontrar o email ativo.', + 'Can not find order' => 'Não foi possível encontrar o pedido', + 'Can not find order.' => 'Não foi possível encontrar o pedido.', + 'Can not find the transaction to refund' => 'Não foi possível encontrar a transação a reembolsar', + 'Can not move between these inventory types.' => 'Não é possível mover entre estes tipos de inventário.', + 'Can not refund amount greater than the remaining amount' => 'Não é possível reembolsar uma quantia superior ao valor restante', + 'Cancel subscription' => 'Cancelar subscrição', + 'Cancel with gateway now' => 'Cancelar agora através do gateway', + 'Cancel' => 'Cancelar', + 'Cancellation date' => 'Data de cancelamento', + 'Cancellation' => 'Cancelamento', + 'Cannot switch plans for this subscription.' => 'Não foi possível mudar de plano para esta subscrição.', + 'Can’t preview this email.' => 'Não é possível pré-visualizar este email.', + 'Capture payment' => 'Capturar pagamento', + 'Capture' => 'Capturar', + 'Card Holder' => 'Titular do cartão', + 'Card Number' => 'Número do Cartão', + 'Card' => 'Cartão', + 'Cart Recovery Link' => 'Link para recuperar o carrinho', + 'Cart forgotten.' => 'Carrinho esquecido.', + 'Cart updated.' => 'Carrinho atualizado.', + 'Cart {number}' => 'Carrinho {number}', + 'Catalog Pricing Rule' => 'Regra de determinação de preços de catálogo', + 'Catalog pricing rule description.' => 'Descrição da regra de cálculo do preço de catálogo.', + 'Catalog pricing rule saved.' => 'Regra de determinação de preços de catálogo guardada.', + 'Catalog pricing rules deleted.' => 'Regras de fixação de preços de catálogo suprimidas.', + 'Catalog pricing rules updated.' => 'Regras de fixação de preços do catálogo atualizadas.', + 'Categories Relationship Type' => 'Tipo de relação das categorias', + 'Categories' => 'Categorias', + 'Category Rate Overrides' => 'Sobreposições à Categoria de Tributo', + 'Centimeters (cm)' => 'Centímetros (cm)', + 'Changing this value may affect your ability to refund existing transactions.' => 'Alterar este valor pode afetar a sua capacidade de reembolsar transações existentes.', + 'Choose a color to represent the order’s status' => 'Escolha uma cor para representar o estado do pedido', + 'Choose a new customer' => 'Escolha um novo cliente', + 'Choose adjustment values to include when calculating the product revenue total.' => 'Escolha os valores de ajuste a serem incluídos no cálculo da receita total do produto.', + 'Choose the currency’s ISO code.' => 'Escolha o código ISO da moeda.', + 'Choose the destination inventory location for the existing on hand stock.' => 'Selecione o local de destino do inventário para o stock disponível existente.', + 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Escolha em que sites este tipo de produto deverá estar disponível e configure as definições específicas do site.', + 'City' => 'Cidade', + 'Clear counter' => 'Limpar contador', + 'Clear notices' => 'Limpar os avisos', + 'Close' => 'Fechar', + 'Code' => 'Código', + 'Collated PDF' => 'PDF agrupado', + 'Color' => 'Cor', + 'Commerce Products' => 'Produtos do Commerce', + 'Commerce Settings' => 'Configurações do Commerce', + 'Commerce Variants' => 'Variantes comerciais', + 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Não foi possível enviar o email comercial “{email}” para o pedido “{order}”.', + 'Commerce order exports' => 'Exportações de encomendas comerciais', + 'Commerce' => 'Commerce', + 'Committed' => 'Comprometido', + 'Completed Email' => 'E-mail completo', + 'Completed' => 'Concluído', + 'Completing order failed.' => 'Erro ao concluir o pedido.', + 'Condition' => 'Condição', + 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'As condições são comparadas com uma ordem antes de examinar as regras. É útil para qualificar antecipadamente a disponibilidade de um método ou se houver condições comuns a todas as regras para esse método.', + 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'As condições aqui são comparadas com o cliente do pedido antes de consultar as regras. Isso é útil se quiser qualificar a disponibilidade de um método antecipadamente, ou se houver condições comuns a todas as regras para esse método.', + 'Conditions' => 'Condições', + 'Contains Purchasables' => 'Contém artigos de compra', + 'Control Panel Settings' => 'Definições do painel de controlo', + 'Control panel' => 'Painel de controlo', + 'Conversion Rate' => 'Taxa de Conversão', + 'Converted Price' => 'Preço convertido', + 'Copied!' => 'Copiado!', + 'Copy the URL' => 'Copiar a URL', + 'Copy to {location}' => 'Copiar para {location}', + 'Copy' => 'Copiar', + 'Costs' => 'Custos', + 'Could not archive gateway.' => 'Não foi possível arquivar o gateway.', + 'Could not cancel “{reference}”.' => 'Não foi possível cancelar “{reference}”.', + 'Could not create the payment source.' => 'Não foi possível criar a fonte de pagamento.', + 'Could not delete shipping rule' => 'Não foi possível eliminar a regra de envio', + 'Could not delete shipping zone' => 'Não foi possível eliminar a zona de envio', + 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Não foi possível eliminar {count, number} {count, plural, one{categoria} other{categorias}} de envio.', + 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Não foi possível eliminar {count, number} {count, plural, one{método} other{métodos}} e regras de envio.', + 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Não foi possível eliminar {count, number} {count, plural, one{categoria} other{categorias}} de imposto.', + 'Could not find the email or template.' => 'Não foi possível encontrar o email ou o modelo.', + 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Não foi possível assinalar a encomenda {number} como concluída. Erro ao guardar a encomenda durante a conclusão da encomenda com erros: {order}', + 'Could not reactivate “{reference}”.' => 'Não foi possível reativar “{reference}”.', + 'Could not send email' => 'Não foi possível enviar o email', + 'Could not switch “{reference}” to “{plan}”.' => 'Não foi possível mudar “{reference}” para “{plan}”.', + 'Could not update orders address.' => 'Não foi possível atualizar o endereço dos pedidos.', + 'Couldn’t archive Line Item Status.' => 'Não foi possível arquivar o status do item de linha.', + 'Couldn’t archive Order Status.' => 'Não foi possível arquivar o estado do pedido.', + 'Couldn’t capture transaction.' => 'Não foi possível efetuar a transação.', + 'Couldn’t capture transaction: {message}' => 'Não foi possível efetuar a transação: {message}', + 'Couldn’t delete email.' => 'Não foi possível apagar o e-mail.', + 'Couldn’t delete the payment source.' => 'Não foi possível eliminar a fonte de pagamento.', + 'Couldn’t get order.' => 'Não foi possível obter o pedido.', + 'Couldn’t recalculate order.' => 'Não foi possível recalcular o pedido.', + 'Couldn’t refund transaction.' => 'Não foi possível reembolsar a transação.', + 'Couldn’t refund transaction: {message}' => 'Não foi possível reembolsar a transação: {message}', + 'Couldn’t reorder Line Item Statuses.' => 'Não foi possível reordenar os status de itens de linha.', + 'Couldn’t reorder Order Statuses.' => 'Não foi possível reordenar os Status de Pedido.', + 'Couldn’t reorder PDFs.' => 'Não foi possível reordenar os PDFs.', + 'Couldn’t reorder discounts.' => 'Não foi possível reordenar os descontos.', + 'Couldn’t reorder gateways.' => 'Não foi possível reordenar os gateways.', + 'Couldn’t reorder plans.' => 'Não foi possível pedir novamente os planos.', + 'Couldn’t reorder rules.' => 'Não foi possível reordenar as regras.', + 'Couldn’t reorder sale.' => 'Não foi possível voltar a encomendar a promoção.', + 'Couldn’t reorder sales.' => 'Não foi possível reordenar as ofertas.', + 'Couldn’t reorder statuses.' => 'Não foi possível reordenar os estados.', + 'Couldn’t reorder stores.' => 'Não foi possível voltar a pedir de lojas.', + 'Couldn’t save PDF.' => 'Não foi possível guardar o PDF.', + 'Couldn’t save catalog pricing rule.' => 'Não foi possível guardar a regra de fixação de preços do catálogo.', + 'Couldn’t save currency.' => 'Não foi possível guardar a moeda.', + 'Couldn’t save discount.' => 'Não foi possível guardar o desconto.', + 'Couldn’t save email.' => 'Não foi possível guardar o e-mail.', + 'Couldn’t save gateway.' => 'Não foi possível guardar o gateway.', + 'Couldn’t save inventory location.' => 'Não foi possível guardar a localização do inventário.', + 'Couldn’t save line item status.' => 'Não foi possível guardar o status do item de linha.', + 'Couldn’t save order fields.' => 'Não foi possível guardar os campos do pedido.', + 'Couldn’t save order status.' => 'Não foi possível guardar o status do pedido.', + 'Couldn’t save order.' => 'Não foi possível guardar o pedido.', + 'Couldn’t save product type.' => 'Não foi possível guardar o tipo de produto.', + 'Couldn’t save sale.' => 'Não foi possível guardar a oferta.', + 'Couldn’t save settings.' => 'Não foi possível guardar as definições.', + 'Couldn’t save shipping category.' => 'Não foi possível guardar a categoria de envio.', + 'Couldn’t save shipping method.' => 'Não foi possível guardar o método de envio.', + 'Couldn’t save shipping rule.' => 'Não foi possível guardar a regra de envio.', + 'Couldn’t save shipping zone.' => 'Não foi possível guardar a zona de envio.', + 'Couldn’t save store.' => 'Não foi possível guardar a loja.', + 'Couldn’t save subscription fields.' => 'Não foi possível guardar os campos de subscrição.', + 'Couldn’t save subscription plan.' => 'Não foi possível guardar o plano de subscrição.', + 'Couldn’t save subscription.' => 'Não foi possível guardar a subscrição.', + 'Couldn’t save tax category.' => 'Não foi possível guardar esta categoria de imposto.', + 'Couldn’t save tax rate.' => 'Não foi possível guardar a taxa de imposto.', + 'Couldn’t save tax zone.' => 'Não foi possível guardar a zona fiscal.', + 'Couldn’t save transfer fields.' => 'Não foi possível guardar os campos da transferência.', + 'Couldn’t update catalog pricing rule statuses.' => 'Não foi possível atualizar o estado das regras de fixação de preços do catálogo.', + 'Couldn’t update status.' => 'Não foi possível atualizar o estado.', + 'Couldn’t updated sales status.' => 'Não foi possível atualizar o status das ofertas.', + 'Country Code of Origin' => 'Código do país de origem', + 'Country List' => 'Lista de países', + 'Country not allowed.' => 'O país não é permitido.', + 'Country' => 'País', + 'Coupon Code' => 'Cupom de Desconto', + 'Coupon can not apply discount to this order due to address mismatch.' => 'O cupão não consegue aplicar desconto a este pedido devido a incompatibilidade de endereço.', + 'Coupon can not apply discount to this order due to customer mismatch.' => 'O cupão não consegue aplicar desconto a este pedido devido a incompatibilidade com o cliente.', + 'Coupon can not apply discount to this order.' => 'O cupão não consegue aplicar desconto a este pedido.', + 'Coupon code “{code}” is already in use by discount “{name}”.' => 'O código de cupão “{code}” já está a ser utilizado pelo desconto “{name}”.', + 'Coupon codes cannot be blank.' => 'Os códigos do cupão não podem ficar em branco.', + 'Coupon codes must be unique.' => 'Os códigos de cupão devem ser únicos.', + 'Coupon format is required and must contain at least one `#`.' => 'O formato do cupão é obrigatório e deve conter pelo menos um `#`.', + 'Coupon not valid.' => 'Cupão inválido.', + 'Coupon removed: {explanation}' => 'Cupão removido: {explanation}', + 'Coupons' => 'Cupões', + 'Craft Commerce - Administration' => 'Craft Commerce - Administração', + 'Craft Commerce - Inventory' => 'Craft Commerce - Inventário', + 'Craft Commerce - Orders' => 'Craft Commerce - Pedidos', + 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Tipo de produto - {name}', + 'Craft Commerce - Subscriptions' => 'Craft Commerce - Subscrições', + 'Create a Discount' => 'Criar um Desconto', + 'Create a Subscription Plan' => 'Criar um plano de subscrição', + 'Create a new PDF' => 'Criar um novo PDF', + 'Create a new catalog pricing rule' => 'Criar uma nova regra de determinação de preços do catálogo', + 'Create a new currency' => 'Criar nova moeda', + 'Create a new email' => 'Criar um novo e-mail', + 'Create a new gateway' => 'Criar um novo gateway', + 'Create a new line item status' => 'Criar um novo status de item de linha', + 'Create a new order status' => 'Criar um novo status de pedido', + 'Create a new product type' => 'Criar um novo tipo de produto', + 'Create a new sale' => 'Criar nova oferta', + 'Create a new shipping category' => 'Criar uma nova categoria de envio', + 'Create a new shipping method' => 'Criar um novo método de envio', + 'Create a new shipping rule' => 'Criar uma nova regra de envio', + 'Create a new tax category' => 'Criar uma nova categoria de tributos', + 'Create a new tax rate' => 'Criar uma nova taxa do imposto', + 'Create a product type' => 'Criar um tipo de produto', + 'Create a shipping zone' => 'Criar nova zona de envio', + 'Create a tax zone' => 'Criar uma zona fiscal', + 'Create catalog pricing rules' => 'Criar regras de determinação do preço do catálogo', + 'Create customer: “{email}”' => 'Criar cliente: "{email}"', + 'Create discounts' => 'Criar descontos', + 'Create discount…' => 'Criar desconto…', + 'Create rules that allow this discount to match the order.' => 'Criar regras que permitam que este desconto corresponda ao pedido.', + 'Create rules that allow this discount to match the order’s billing address.' => 'Criar regras que permitam que este desconto corresponda ao endereço de cobrança do pedido.', + 'Create rules that allow this discount to match the order’s customer.' => 'Criar regras que permitam que este desconto corresponda ao pedido do cliente.', + 'Create rules that allow this discount to match the order’s shipping address.' => 'Criar regras que permitam que este desconto corresponda ao endereço de envio do pedido.', + 'Create rules that allow this gateway to match the billing address.' => 'Crie regras que permitam que este gateway corresponda ao endereço de faturação.', + 'Create rules that allow this gateway to match the order.' => 'Crie regras que permitam que este gateway corresponda ao pedido.', + 'Create rules that allow this gateway to match the shipping address.' => 'Crie regras que permitam que este gateway corresponda à morada de entrega.', + 'Create sales' => 'Criar ofertas', + 'Create sale…' => 'Criar oferta…', + 'Created' => 'Criados', + 'Credit Card Payment Type' => 'Tipo de Modalidade do Cartão de Crédito', + 'Currency Code' => 'Código da Moeda', + 'Currency saved.' => 'Moeda guardada.', + 'Currency' => 'Moeda', + 'Current' => 'Atual', + 'Custom 1' => 'Personalizado 1', + 'Custom 2' => 'Personalizado 2', + 'Custom 3' => 'Personalizado 3', + 'Custom 4' => 'Personalizado 4', + 'Custom' => 'Personalizado', + 'Customer Enabled?' => 'Habilitado para Clientes?', + 'Customer ID is required.' => 'A ID do cliente é obrigatória.', + 'Customer Note' => 'Nota de cliente', + 'Customer Notices' => 'Avisos do cliente', + 'Customer data' => 'Dados dos clientes', + 'Customer' => 'Cliente', + 'Damaged' => 'Danificado', + 'Data shown might be outdated.' => 'Os dados apresentados podem estar desatualizados.', + 'Date Authorized' => 'Data da autorização', + 'Date Created' => 'Data de Criação', + 'Date First Paid' => 'Data do primeiro pagamento', + 'Date Ordered' => 'Data do Pedido', + 'Date Paid' => 'Data de Pagamento', + 'Date Updated' => 'Data Atualizada', + 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Data a partir da qual a regra de determinação do preço do catálogo estará ativa. Deixe em branco para uma data de início ilimitada', + 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Data a partir da qual o desconto estará ativo. Deixe em branco para data de início não limitada', + 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Data a partir da qual essa oferta estará ativa. Deixe em branco para data de início não limitada', + 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Data em que a regra de determinação do preço do catálogo será concluída. Deixe em branco para uma data final ilimitada', + 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Data na qual o desconto terminará. Deixe em branco para data final indefinida', + 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Data em que a oferta termina. Deixe em branco para prazo final indeterminado.', + 'Date' => 'Data', + 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Padrão - permite que o preço seja negativo se os descontos forem maiores do que o valor do pedido.', + 'Default Category' => 'Categoria padrão', + 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Visualização padrão do painel de controlo do Commerce. Se o utilizador não tiver permissão, voltará a um local a que possa aceder.', + 'Default Order PDF' => 'PDF de pedido padrão', + 'Default Per Item Rate' => 'Taxa padrão por item', + 'Default Percentage Rate' => 'Taxa percentual padrão', + 'Default Status?' => 'Status Padrão?', + 'Default View' => 'Visão padrão', + 'Default Weight Rate' => 'Taxa padrão de peso', + 'Default Zone' => 'Zona Padrão', + 'Default status?' => 'Estado padrão?', + 'Default to this tax zone when no billing address is set' => 'Predefinir esta zona de imposto quando não for definido um endereço de faturação', + 'Default to this tax zone when no shipping address is set' => 'Zona de tributos padrão para quando nenhum endereço de entrega esteja definido', + 'Default variant updated.' => 'Variante predefinida atualizada.', + 'Default' => 'Padrão', + 'Default?' => 'Padrão?', + 'Delete catalog pricing rules' => 'Eliminar regras de determinação de preços do catálogo', + 'Delete discounts' => 'Eliminar descontos', + 'Delete orders' => 'Eliminar pedidos', + 'Delete sales' => 'Eliminar ofertas', + 'Delete' => 'Deletar', + 'Deleting the {location} location.' => 'Eliminar a localização {location}.', + 'Describe this rule.' => 'Descreva essa regra.', + 'Describe this shipping zone.' => 'Descreva sua zona de envio.', + 'Describe this tax zone.' => 'Descreva essa zona de tributos.', + 'Description' => 'Descrição', + 'Destination Inventory Location' => 'Local de destino do inventário', + 'Destination' => 'Destino', + 'Details' => 'Detalhes', + 'Dimension Unit' => 'Unidade de Dimensão', + 'Dimensions' => 'Dimensões', + 'Disabled' => 'Desabilitado', + 'Disallow' => 'Proibir', + 'Discount all line items' => 'Descontar todos os itens de linha', + 'Discount description.' => 'Descrição do desconto.', + 'Discount is not allowed for the order' => 'Desconto não permitido para o pedido', + 'Discount is out of date.' => 'O desconto está desatualizado.', + 'Discount saved.' => 'Desconto guardado.', + 'Discount the matching items only' => 'Descontar apenas os itens correspondentes', + 'Discount use has reached its limit.' => 'A utilização do desconto atingiu o limite.', + 'Discount' => 'Desconto', + 'Discounted Item Subtotal' => 'Subtotal do artigo com desconto', + 'Discounted Items' => 'Artigos com desconto', + 'Discounts deleted.' => 'Descontos eliminados.', + 'Discounts reordered.' => 'Descontos reordenados.', + 'Discounts updated.' => 'Descontos atualizados.', + 'Discounts' => 'Descontos', + 'Disqualify with valid business tax ID?' => 'Desqualificar com ID fiscal comercial válida?', + 'Do not apply subsequent matching sales beyond applying this sale.' => 'Não aplicar vendas correspondentes subsequentes além da aplicação desta venda.', + 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Não aplicar esta taxa se o endereço do pedido tiver qualquer um dos IDs de imposto comerciais válidos selecionados.', + 'Do not attach a PDF to this email' => 'Não anexe um PDF a este email', + 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Não chame o recálculo no pedido (Número: {orderNumber}) se houver erros.', + 'Donation can not be zero.' => 'A doação não pode ser zero.', + 'Donation needs to be an amount.' => 'A doação tem de ser uma quantia.', + 'Donation settings saved.' => 'Definições de doações guardadas.', + 'Donation' => 'Doação', + 'Donations' => 'Doações', + 'Done' => 'Concluído', + 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Não aplicar nenhum desconto subsequente a um pedido caso esse desconto seja aplicado', + 'Download PDF' => 'Baixar PDF', + 'Download PDF…' => 'Transferir PDF…', + 'Download Type' => 'Tipo de transferência', + 'Download' => 'Transferência', + 'Draft' => 'Rascunho', + 'Dummy gateway payment failed.' => 'O pagamento do gateway fictício falhou.', + 'Duplicate options exist' => 'Existem opções duplicadas', + 'Duration' => 'Duração', + 'EU VAT ID' => 'ID de NIF da UE', + 'Edit address' => 'Editar Endereço', + 'Edit adjustments' => 'Editar ajustes', + 'Edit catalog pricing rules' => 'Editar regras de determinação de preços do catálogo', + 'Edit discounts' => 'Editar descontos', + 'Edit options' => 'Editar opções', + 'Edit orders' => 'Editar pedidos', + 'Edit sales' => 'Editar ofertas', + 'Edit' => 'Editar', + 'Effect' => 'Efeito', + 'Either (Default) - The relationship field is on the purchasable or the category' => 'Qualquer (Padrão) - O campo de relação está no produto para compra ou na categoria', + 'Either way' => 'Ambas', + 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Erro de geração de email com PDF para o email “{email}”. Pedido: “{order}”. Erro de modelo de PDF: “{message}” {file}:{line}', + 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'O modelo do PDF de email não existe em “{templatePath}” para o email “{email}”. Encomenda: “{order}”.', + 'Email Subject' => 'Assunto do E-mail', + 'Email error. No email address found for order. Order: “{order}”' => 'Erro de e-mail. Não foi encontrado um endereço de e-mail para o pedido. Pedido: “{order}”', + 'Email is not enabled.' => 'O email não está ativo.', + 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'O modelo de email de texto simples não existe em “{templatePath}” o que resultou em “{templateParsedPath}” para o email “{email}”. Pedido: “{order}”.', + 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de modelo de email de texto simples para o email “{email}”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', + 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de caminho de modelo de email de texto simples para o email “{email}” em “Caminho de modelo:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', + 'Email required to make payments on a completed order.' => 'Email necessário para fazer pagamentos num pedido concluído.', + 'Email saved.' => 'Email guardado.', + 'Email sent' => 'Email enviado', + 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'O modelo de email não existe em “{templatePath}” o que resultou em “{templateParsedPath}” para o email “{email}”. Encomenda: “{order}”.', + 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de email personalizado para o email “{email}” em “Para:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de modelo de email para o email “{email}” em “BCC:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de modelo de email para o email “{email}” em “CC:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de modelo de email para o email “{email}” em “ReplyTo:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', + 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de modelo de email para o email “{email}” em “Assunto:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', + 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de modelo de email para o email “{email}”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', + 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de caminho de modelo de email para o email “{email}” em “Caminho de modelo:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', + 'Email unavailable.' => 'E-mail indisponível.', + 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'Não foi possível enviar o email “{email}” para o pedido “{order}”. Erro: {error} {file}:{line}', + 'Email “{email}” for order {order} was cancelled.' => 'O email “{email}” do pedido {order} foi cancelado.', + 'Email' => 'E-mail', + 'Emails' => 'E-mails', + 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Ativar se esta taxa deve ser incorporada ao preço do assunto tributável em vez de adicionar um custo ao pedido.', + 'Enable structure for products of this type' => 'Ativar estrutura para produtos deste tipo', + 'Enable this discount' => 'Habilitar esse desconto', + 'Enable this rule' => 'Ativar esta regra', + 'Enable this sale' => 'Habilitar essa oferta', + 'Enable this shipping method on the front end' => 'Habilitar esse método de envio de publicamente', + 'Enable this shipping rule' => 'Habilitar essa regra de envio', + 'Enable this tax rate' => 'Ativar esta taxa de imposto', + 'Enabled for customers to select during checkout?' => 'Habilitar para seleção por clientes durante a compra?', + 'Enabled for customers to select?' => 'Ativado para seleção por parte dos clientes?', + 'Enabled' => 'Habilitado', + 'Enabled?' => 'Habilitado?', + 'End Date' => 'Data Final', + 'Enter SKU' => 'Insira o SKU', + 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Insira um nome simples para esta taxa de imposto para ser usada no painel de controlo.', + 'Enter a percentage like {ex1} or {ex2}.' => 'Insira uma percentagem como {ex1} ou {ex2}.', + 'Enter coupon code' => 'Inserir o código do cupão', + 'Enter reference' => 'Inserir referência', + 'Error refunding transaction: {transactionHash}' => 'Erro ao reembolsar a transação: {transactionHash}', + 'Every new store must be assigned to at least one site.' => 'Cada nova loja deve ser atribuída a pelo menos um local.', + 'Everywhere' => 'Em todo o lado', + 'Example' => 'Exemplo', + 'Exclude this discount for products that are already on promotion' => 'Excluir este desconto para produtos que já estejam em promoção', + 'Expired Link' => 'Link expirado', + 'Expired' => 'Expirado', + 'Expiry Date' => 'Data de Expiração', + 'Expiry date' => 'Data de validade', + 'Expiry' => 'Validade', + 'Failed to receive transfer: {error}' => 'Falha ao receber a transferência: {error}', + 'Failed to send email. Please try again.' => 'Erro ao enviar e-mail. Tente novamente.', + 'Failed to start' => 'Erro ao iniciar', + 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Erro ao atualizar o {num, plural, =1{estado do pedido} other{estado dos pedidos}}.', + 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Erro ao atualizar o estado do pedido em {num, plural, =1{pedido} other{pedidos}}.', + 'Feet (ft)' => 'Pés (ft)', + 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Condições de filtragem que descrevem em quais pedidos essa regra se aplica. Escreva 0 para pular essa condição.', + 'First Name' => 'Nome', + 'Flat Amount Off Order' => 'Valor fixo de desconto no pedido', + 'Flat Order Discount Amount Off' => 'Valor do desconto de pedido fixo', + 'Free Order Payment Strategy' => 'Estratégia de pagamento de pedidos gratuitos', + 'Free Shipping' => 'Envio Grátis', + 'Free orders are processed by the payment gateway' => 'Os pedidos gratuitos são processados pelo gateway de pagamento', + 'Free orders complete immediately' => 'Os pedidos gratuitos completam-se imediatamente', + 'Free shipping can only be for whole order or matching items, not both.' => 'O envio gratuito só pode ser feito no pedido total ou em itens correspondentes, não em ambos.', + 'From Name' => 'Remetente', + 'Fulfill' => 'Cumprir', + 'Fulfilled' => 'Cumprido', + 'Fulfillment' => 'Cumprimento', + 'Full Name' => 'Nome completo', + 'Gateway Code' => 'Código de gateway', + 'Gateway Message' => 'Mensagem do gateway', + 'Gateway Reference' => 'Referência do gateway', + 'Gateway Response' => 'Resposta do gateway', + 'Gateway doesn’t support authorize' => 'O gateway não suporta a autorização', + 'Gateway doesn’t support partial refunds.' => 'O gateway não suporta reembolsos parciais.', + 'Gateway doesn’t support purchase' => 'O Gateway não suporta a compra', + 'Gateway doesn’t support refunds.' => 'O gateway não suporta reembolsos.', + 'Gateway saved.' => 'Gateway guardado.', + 'Gateway' => 'Gateway', + 'Gateways reordered.' => 'Gateways reordenados.', + 'Gateways' => 'Gateways', + 'General Settings' => 'Configurações Gerais', + 'General' => 'Geral', + 'Generate' => 'Gerar', + 'Generated Coupon Format' => 'Formato de cupão gerado', + 'Grams (g)' => 'Gramas (g)', + 'Groups for which this sale will be applicable to.' => 'Grupos aos quais esta venda será aplicável.', + 'HTML Email Template Path' => 'Caminho para Template de E-mail HTML', + 'Handle' => 'Identificador', + 'Harmonized System Code' => 'Código do Sistema Harmonizado', + 'Has Admin Notices' => 'Tem avisos da administração', + 'Has Emails?' => 'Possuí E-mails?', + 'Has Free Shipping' => 'Tem Envio Grátis', + 'Has Orders' => 'Tem pedidos', + 'Has Purchasable' => 'Tem produtos para compra', + 'Has Variants?' => 'Possuí Variantes?', + 'Height ({unit})' => 'Altura ({unit})', + 'Height' => 'Altura', + 'Hide snapshot' => 'Ocultar snapshot', + 'History' => 'Histórico', + 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Quanto tempo (em segundos) deve um link de transferência de PDF permanecer válido antes de expirar. O padrão é 86400 (24 horas).', + 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Quantas vezes um único e-mail pode usar esse desconto. Isso se aplica a todos os pedidos passados, quer seja usuário ou convidado. Defina como zero para uso ilimitado por usuários e convidados.', + 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Quantas vezes é permitido ao utilizador usar este desconto. Se estiver definido para algo além de zero, o desconto estará disponível apenas para utilizadores com sessão iniciada.', + 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Quantas vezes este desconto pode ser usado no total por convidados e utilizadores com sessão iniciada. Defina como zero para uso ilimitado.', + 'How products should be labeled within the control panel.' => 'Como os produtos devem ser rotulados no painel de controlo.', + 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'Como as Compras e Categorias estão relacionadas, o que determina os itens correspondentes. Consulte [Terminologia de relações]({link}).', + 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'Como esse produto será descrito em um item em um pedido. Você pode incluir tags que deem como resposta propriedades, assim como {ex1} ou {ex2}', + 'How this shipping method will be referred to in templates and forms.' => 'Como você vai se referir a esse método de envio nos templates e formulários.', + 'How variants should be labeled within the control panel.' => 'Como as variantes devem ser rotuladas no painel de controlo.', + 'How you’ll refer to this PDF in the templates.' => 'Como se irá referir a este PDF nos modelos.', + 'How you’ll refer to this product type in the templates.' => 'Como você vai se referir a esse tipo de produto nos template', + 'How you’ll refer to this shipping category in the templates.' => 'Como você vai se referir a essa categoria de envio nos templates e formulários.', + 'How you’ll refer to this status in the templates.' => 'Como você vai se referir a esse status nos templates.', + 'How you’ll refer to this subscription plan in the templates.' => 'Como se irá referir a este plano de subscrição nos modelos.', + 'How you’ll refer to this tax category in the templates.' => 'Como você vai se referir a essa categoria de tributos nos templates.', + 'ID' => 'ID', + 'IP Address' => 'Endereço de IP', + 'If disabled, this PDF will not be available or sent with emails.' => 'Se desativado, este PDF não ficará disponível nem será enviado nos emails.', + 'If disabled, this email will not send.' => 'Se desativado, este email não será enviado.', + 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Se ativado e esta taxa não corresponder ao pedido, o valor da taxa será removido do preço em questão no carrinho.', + 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Se definido para Apenas Autorizar, você precisará capturar manualmente os pagamentos antes, para então os fundos serem transferidos para sua conta. O Gateway precisa suportar a opção selecionada.', + 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Se selecionar a percentagem que vai estar "fora do preço do item com desconto", esta vai incluir o "Valor por item" assim como qualquer outro desconto aplicado antes deste.', + 'Ignore Promotions?' => 'Ignorar promoções?', + 'Ignore previous matching sales if this sale matches.' => 'Ignorar vendas correspondentes anteriores se esta venda for correspondente.', + 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignorar preços promocionais quando este desconto é aplicado a artigos de linha correspondentes', + 'Inactive Carts' => 'Carrinhos Inativos', + 'Inches (in)' => 'Polegadas (in)', + 'Include built-in line item tax.' => 'Incluir imposto de item de linha integrado.', + 'Include in price?' => 'Incluir no preço?', + 'Include line item discounts.' => 'Incluir descontos de item de linha.', + 'Include line item shipping costs.' => 'Incluir os custos de envio do item de linha.', + 'Include separate line item tax.' => 'Incluir imposto de item de linha em separado.', + 'Included in price?' => 'Incluído no preço?', + 'Included' => 'Incluído', + 'Incoming transfer from Transfer ID: ' => 'Transferência a entrar do ID de transferência: ', + 'Incoming' => 'A chegar', + 'Info' => 'Informações', + 'Information linked?' => 'Informação disponível numa ligação?', + 'Information' => 'Informação', + 'Invalid JSON' => 'JSON inválido', + 'Invalid Order ID' => 'A ID de pedido não é válida', + 'Invalid VAT ID.' => 'ID de IVA inválido.', + 'Invalid condition syntax' => 'Sintaxe da condição inválida', + 'Invalid email.' => 'Email inválido.', + 'Invalid formula syntax' => 'A sintaxe da fórmula não é válida', + 'Invalid gateway: {value}' => 'Gateway inválido: {value}', + 'Invalid inventory movements.' => 'Movimentos de stock inválidos.', + 'Invalid order condition syntax.' => 'Sintaxe inválida da condição do pedido.', + 'Invalid payment or order. Please review.' => 'Pagamento ou encomenda inválidos. Reveja.', + 'Invalid payment source ID: {value}' => 'ID de fonte de pagamento inválida: {value}', + 'Invalid store.' => 'Loja inválida.', + 'Invalid user.' => 'Utilizador inválido.', + 'Inventory Item' => 'Item de inventário', + 'Inventory Location' => 'Localização do inventário', + 'Inventory Locations' => 'Locais de inventário', + 'Inventory Tracked' => 'Inventário monitorizado', + 'Inventory Transfers' => 'Transferências de inventário', + 'Inventory could not be set.' => 'Não foi possível definir o inventário.', + 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'O local do stock tem stock comprometido, a(s) ordem(ns) deve(m) ser atendida(s) primeiro.', + 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Se o local do inventário tiver entradas de stock, a(s) transferência(s) deve(m) ser concluída(s) primeiro.', + 'Inventory location is already deactivated.' => 'A localização do inventário já está desativada.', + 'Inventory location saved.' => 'Local do inventário guardado.', + 'Inventory locations not saved.' => 'Locais de inventário não guardados.', + 'Inventory movement could not be saved.' => 'Não foi possível guardar o movimento de inventário.', + 'Inventory movement saved.' => 'Movimento de inventário guardado.', + 'Inventory updated.' => 'Inventário atualizado.', + 'Inventory was not updated.' => 'O inventário não foi atualizado.', + 'Inventory' => 'Inventário', + 'Invoice amount' => 'Quantia da fatura', + 'Invoice date' => 'Data da fatura', + 'Is Promotable' => 'É promovível', + 'Is Promotional Price?' => 'É preço promocional?', + 'Is Shippable' => 'É expedível', + 'Is Taxable' => 'É taxável', + 'Item Rates' => 'Custo por Item', + 'Item Subtotal' => 'Subtotal do item', + 'Item Total' => 'Total do artigo', + 'Item' => 'Item', + 'Items' => 'Itens', + 'Kilograms (kg)' => 'Quilogramas (kg)', + 'Label' => 'Etiqueta', + 'Landscape' => 'Paisagem', + 'Language' => 'Idioma', + 'Last Name' => 'Sobrenome', + 'Last Updated' => 'Última atualização', + 'Leave a category rate override blank to use the rate from above.' => 'Deixar em branco a substituição de uma taxa de categoria para utilizar a taxa acima.', + 'Leave blank for unlimited uses.' => 'Deixar em branco para usos ilimitados.', + 'Leave blank if products don’t have URLs' => 'Deixar em branco se os produtos não tiverem URL', + 'Leave gateway subscription as-is' => 'Deixar a subscrição do gateway tal como está', + 'Length ({unit})' => 'Comprimento ({unit})', + 'Length' => 'Comprimento', + 'Let each product choose which sites it should be saved to' => 'Deixar que cada produto escolha os sites em que deve ser guardado', + 'Limit which orders this discount applies to based on its line items.' => 'Limitar a quais pedidos este desconto é aplicável com base nos respetivos itens de linha.', + 'Limit which purchasables this sale applies to.' => 'Limitar a quais produtos para compra se aplica esta oferta.', + 'Limit' => 'Limite', + 'Line Item Statuses' => 'Estados de item de linha', + 'Line Item' => 'Item de linha', + 'Line Items' => 'Itens de linha', + 'Line item price (minus discounts)' => 'Preço do item de linha (menos descontos)', + 'Line item shipping cost' => 'Custo de envio do item individual', + 'Line item statuses reordered.' => 'Estados do item de linha reordenados.', + 'Link Duration' => 'Duração do link', + 'Link Sent' => 'Link enviado', + 'Link to a product' => 'Link para um produto', + 'Link to a variant' => 'Ligar a uma variante', + 'Link' => 'Ligação', + 'Live' => 'Publicado', + 'Location' => 'Local', + 'Locations that should be available for previewing products in this product type.' => 'Locais que devem estar disponíveis para pré-visualização de produtos deste tipo.', + 'MM' => 'MM', + 'Make a payment' => 'Fazer um pagamento', + 'Make this the primary store' => 'Tornar esta a loja principal', + 'Manage Inventory' => 'Gerir inventário', + 'Manage donation settings' => 'Gerir definições de doações', + 'Manage general store settings' => 'Gerir definições gerais da loja', + 'Manage inventory locations' => 'Gerir locais de inventário', + 'Manage inventory stock levels' => 'Gerir os níveis de stock do inventário', + 'Manage inventory transfers' => 'Gerir transferências de inventário', + 'Manage orders' => 'Gerir pedidos', + 'Manage payment currencies' => 'Gerir moedas de pagamento', + 'Manage promotions' => 'Gerir promoções', + 'Manage shipping' => 'Gerir envios', + 'Manage store settings' => 'Gerir definições da loja', + 'Manage subscription plans' => 'Gerir planos de subscrição', + 'Manage subscription' => 'Gerir subscrição', + 'Manage subscriptions' => 'Gerir subscrições', + 'Manage taxes' => 'Gerir impostos', + 'Manage' => 'Gerir', + 'Mark as Pending' => 'Assinalar como pendente', + 'Mark as completed' => 'Assinalar como concluído', + 'Match Billing Address' => 'Fazer corresponder ao endereço de cobrança', + 'Match Customer' => 'Fazer corresponder ao cliente', + 'Match Order' => 'Fazer corresponder ao pedido', + 'Match Orders' => 'Fazer corresponder aos pedidos', + 'Match Product' => 'Corresponder produto', + 'Match Purchasable' => 'Produto para compra correspondente', + 'Match Shipping Address' => 'Fazer corresponder ao endereço de envio', + 'Match Variant' => 'Corresponder variante', + 'Matching Items' => 'Itens correspondentes', + 'Max Qty' => 'Quantidade máxima', + 'Max Uses' => 'Máximo de utilizações', + 'Max Variants' => 'Variantes máximas', + 'Max quantity must greater than min.' => 'A quantidade máxima deve ser superior à mínima.', + 'Maximum Purchase Quantity' => 'Quantidade Máxima por Compra', + 'Maximum Total Shipping Cost' => 'Custo Total Máximo do Envio', + 'Maximum allowed quantity' => 'Quantidade máxima autorizada', + 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'O número máximo de itens correspondentes que podem ser comprados para a aplicação do desconto. Um valor zero aqui ignorará essa condição.', + 'Maximum order quantity for this item is {num}.' => 'A quantidade máxima de pedido para este item é {num}.', + 'Message' => 'Mensagem', + 'Meters (m)' => 'Metros (m)', + 'Millimeters (mm)' => 'Milímetros (mm)', + 'Min Qty' => 'Quantidade mínima', + 'Min quantity must be less than max.' => 'A quantidade mínima deve ser inferior à máxima.', + 'Minimum Purchase Quantity' => 'Quantidade Mínima por Pedido', + 'Minimum Total Price Strategy' => 'Estratégia Mínima de Preço Total', + 'Minimum Total Shipping Cost' => 'Custo Total Mínimo do Envio', + 'Minimum allowed quantity' => 'Quantidade mínima autorizada', + 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Número mínimo de items válidos que precisam ser pedidos para que esse desconto seja aplicado.', + 'Minimum order quantity for this item is {num}.' => 'A quantidade mínima para pedido deste item é {num}.', + 'Missing Gateway' => 'Gateway em falta', + 'Missing a default inventory location.' => 'Falta uma localização de inventário padrão.', + 'Move Inventory' => 'Mover inventário', + 'Move To' => 'Mover para', + 'Move {qty} from {fromType} to {toType}' => 'Mover {qty} de {fromType} para {toType}', + 'Move' => 'Mover', + 'Movement from deactivated inventory location' => 'Movimento a partir do local de inventário desativado', + 'Movement' => 'Movimento', + 'Must have at least one variant.' => 'Deve ter pelo menos uma variante.', + 'Name Field' => 'Campo do nome', + 'Name' => 'Nome', + 'New Customer' => 'Novo cliente', + 'New Customers' => 'Novos clientes', + 'New Order' => 'Novo pedido', + 'New PDF' => 'Novo PDF', + 'New address' => 'Novo endereço', + 'New catalog pricing rule' => 'Nova regra de fixação de preços por catálogo', + 'New currency' => 'Nova moeda', + 'New discount' => 'Novo Desconto', + 'New email' => 'Novo e-mail', + 'New gateway' => 'Novo gateway', + 'New line item status' => 'Novo estado de item de linha', + 'New line items get this status by default when the order is completed' => 'Novos itens de linha obtêm este status por defeito quando o pedido é concluído', + 'New location' => 'Novo local', + 'New order status' => 'Novo status de pedido', + 'New orders get this status by default' => 'Novos pedidos recebem esse status automaticamente', + 'New product type' => 'Novo tipo de produto', + 'New product' => 'Novo produto', + 'New product, choose a type' => 'Novo produto, escolher um tipo', + 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Os novos produtos assumem como padrão a primeira categoria de imposto disponível para eles. Se não houver nenhuma disponível, será usada esta categoria.', + 'New sale' => 'Nova oferta', + 'New shipping category' => 'Nova categoria de envio', + 'New shipping method' => 'Novo método de envio', + 'New shipping rule' => 'Nova regra de envio', + 'New shipping zone' => 'Nova zona de envio', + 'New subscription plan' => 'Novo plano de subscrição', + 'New tax category' => 'Nova categoria de tributos', + 'New tax rate' => 'Novo tributo', + 'New tax zone' => 'Nova zona de tributos', + 'New transfer' => 'Nova transferência', + 'New {productType} product' => 'Novo produto {productType}', + 'New' => 'Novo', + 'Next payment' => 'Próximo pagamento', + 'No Address' => 'Sem endereço', + 'No PDFs exist yet.' => 'Não existem PDFs ainda.', + 'No access given to any specific store management features.' => 'Sem acesso atribuído a quaisquer funcionalidades específicas de gestão de lojas.', + 'No additional payment currencies exist yet.' => 'Ainda não há moedas de pagamento adicionais.', + 'No address' => 'Sem endereço', + 'No billing address' => 'Sem endereço de faturação', + 'No catalog pricing rule exists with the ID “{id}”' => 'Não existe nenhuma regra de fixação de preços do catálogo com o ID "{id}"', + 'No catalog pricing rules exist yet.' => 'Ainda não existem regras de fixação de preços de catálogo.', + 'No currency exists with the ID “{id}”' => 'Não existe moeda com a ID “{id}”', + 'No customer email address exists on this cart.' => 'Não existe nenhum e-mail de cliente neste carrinho.', + 'No description' => 'Sem descrição', + 'No discount exists with the ID “{id}”' => 'Não existe desconto com a ID “{id}”', + 'No discounts exist yet.' => 'Não existem descontos ainda.', + 'No donation amount supplied.' => 'Não foi fornecido nenhum valor de doação.', + 'No emails exist yet.' => 'Nenhum e-mail existe ainda.', + 'No inventory changes made.' => 'Não foram feitas alterações ao inventário.', + 'No inventory found.' => 'Não foi encontrado nenhum inventário.', + 'No inventory movements made.' => 'Não foram feitos movimentos de inventário.', + 'No inventory transactions for this location.' => 'Não há transações de inventário para este local.', + 'No new customer selected.' => 'Não foi selecionado nenhum novo cliente.', + 'No order history exists with the ID “{id}”' => 'Não existe histórico de pedidos com o ID “{id}”', + 'No order status history items will exist until the cart becomes an order.' => 'Não existirão itens do histórico de estado de encomenda até que o carrinho seja convertido numa encomenda.', + 'No payment source exists with the ID “{id}”' => 'Não existem fontes de pagamento com a ID “{id}”', + 'No private Note.' => 'Sem nota privada.', + 'No product available.' => 'Nenhum produto disponível.', + 'No product types exist yet.' => 'Não existe tipos de produto ainda.', + 'No purchasable available.' => 'Não está disponível nenhum produto para compra.', + 'No sale exists with the ID “{id}”' => 'Não existe oferta com a ID “{id}”', + 'No sales exist yet.' => 'Não existem ofertas ainda.', + 'No shipping address' => 'Sem endereço de Envio', + 'No shipping category exists with the ID “{id}”' => 'Não existe categoria de envio com a ID “{id}”', + 'No shipping method exists with the ID “{id}”' => 'Não existe método de envio com a ID “{id}”', + 'No shipping rule exists with the ID “{id}”' => 'Não existe regra de envio com o ID “{id}”', + 'No shipping rules exist yet.' => 'Ainda não existem regras de envio.', + 'No shipping zone exists with the ID “{id}”' => 'Não existe zona de envio com a ID “{id}”', + 'No stats available.' => 'Não há estatísticas disponíveis.', + 'No subscription plan exists with the ID “{id}”' => 'Não existe um plano de subscrição com a ID “{id}”', + 'No subscription plans exist yet.' => 'Ainda não existem planos de subscrição.', + 'No tax category exists with the ID “{id}”' => 'Não existem categorias de tributos com o ID “{id}”', + 'No tax rate exists with the ID “{id}”' => 'Não existem impostos com a ID “{id}”', + 'No tax zone exists with the ID “{id}”' => 'Não existe uma zona fiscal com a ID “{id}”', + 'No transactions exist.' => 'Não há nenhuma transação.', + 'No user authenticated.' => 'Nenhum utilizador autenticado.', + 'No' => 'Não', + 'None on hand' => 'Nenhum disponível', + 'None' => 'Nenhum', + 'Not a valid address type' => 'Tipo de endereço inválido', + 'Not a valid credit card number.' => 'Não é um número de cartão de crédito válido.', + 'Not all SKUs are unique.' => 'Nem todos os SKU são únicos.', + 'Note' => 'Observação', + 'Notes' => 'Notas', + 'Number of Coupons' => 'Número de cupões', + 'Number' => 'Número', + 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Dos sites habilitados acima, em que sites é que os produtos deste tipo de produto devem ser guardados?', + 'On Hand' => 'Disponível', + 'Only allow this gateway to be used for zero value orders?' => 'Permitir que apenas este gateway seja utilizado para encomendas de valor zero?', + 'Only match certain purchasables…' => 'Apenas corresponder a determinados produtos para compra…', + 'Only match purchasables related to…' => 'Apenas corresponder produtos para compra relacionados com…', + 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Só serão incluídos pedidos com os seguintes estados de pedido. Deixe em branco para incluir todos os estados.', + 'Only save product to the site they were created in' => 'Guardar produtos apenas no site em que foram criados', + 'Options' => 'Opções', + 'Order Condition Formula' => 'Formula de condição de pedido', + 'Order Description Format' => 'Formato de Descrição do Pedido', + 'Order Details' => 'Detalhes do pedido', + 'Order Fields' => 'Campos de Pedido', + 'Order PDF Download Link' => 'Link de pedido de transferência de PDF', + 'Order PDF Filename Format' => 'Formato do Nome do Arquivo PDF do Pedido', + 'Order Reference Number Format' => 'Formato do número de referência da encomenda', + 'Order Settings' => 'Configurações de Pedido', + 'Order Site' => 'Site do pedido', + 'Order Status description.' => 'Descrição do estado do pedido.', + 'Order Status' => 'Status do Pedido', + 'Order Statuses' => 'Status de Pedido', + 'Order can not be empty.' => 'O pedido não pode ficar vazio.', + 'Order count' => 'Contagem de pedidos', + 'Order customer data removed.' => 'Solicitar a remoção dos dados do cliente.', + 'Order deleted.' => 'Pedido eliminado.', + 'Order fields saved.' => 'Campos de pedido guardados.', + 'Order not found.' => 'O pedido não foi encontrado.', + 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'O saldo do pagamento do pedido é {outstandingBalanceAsCurrency}. Este é o valor máximo que será cobrado.', + 'Order recalculated.' => 'Pedido recalculado.', + 'Order status saved.' => 'Status do pedido guardado.', + 'Order statuses reordered.' => 'Estados de pedidos reordenados.', + 'Order total shipping cost' => 'Custo de envio total da encomenda', + 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Preço total da encomenda passível de aplicação de impostos (subtotal do item individual + total de descontos + total do envio)', + 'Order' => 'Pedido', + 'Orders (Legacy)' => 'Pedidos (Legacy)', + 'Orders deleted.' => 'Pedidos eliminados.', + 'Orders not restored.' => 'Pedidos não restaurados.', + 'Orders restored.' => 'Pedidos restaurados.', + 'Orders' => 'Pedidos', + 'Organization Name' => 'Nome da empresa', + 'Organization Tax ID' => 'NIF da Empresa', + 'Origin and destination cannot be the same.' => 'A origem e o destino não podem ser o mesmo.', + 'Origin' => 'Origem', + 'Original Price' => 'Preço original', + 'Original price' => 'Preço original', + 'Original promotional price' => 'Preço promocional original', + 'Other Languages' => 'Outros idiomas', + 'Other countries' => 'Outros países', + 'Outgoing transfer from Transfer ID: ' => 'Transferência a sair do ID de transferência: ', + 'Overpaid' => 'Pagamento excessivo', + 'Overrides previous?' => 'Substitui o anterior?', + 'PDF Attachment' => 'Anexo PDF', + 'PDF Template Path' => 'Caminho do modelo de PDF', + 'PDF saved.' => 'PDF guardado.', + 'PDF' => 'PDF', + 'PDFs & Emails' => 'PDFs e E-mails', + 'PDFs' => 'PDFs', + 'Paid Amount' => 'Quantia paga', + 'Paid Status' => 'Estado de pagamento efetuado', + 'Paid' => 'Pago', + 'Paper Orientation' => 'Orientação do papel', + 'Paper Size' => 'Tamanho do papel', + 'Partial payment not allowed.' => 'Não é permitido o pagamento parcial.', + 'Partial' => 'Parcial', + 'Past year' => 'Último ano', + 'Past {num} days' => 'Últimos {num} dias', + 'Pay {amount} of {currency} on the order.' => 'Pagar {amount} de {currency} no pedido.', + 'Pay' => 'Pagar', + 'Payment Amount' => 'Valor do Pagamento', + 'Payment Currencies' => 'Moedas de Pagamento', + 'Payment Gateway' => 'Gateway de pagamento', + 'Payment Method' => 'Método de Pagamento', + 'Payment error: {message}' => 'Erro no pagamento: {message}', + 'Payment method issue' => 'Problema no método de pagamento', + 'Payment source created.' => 'Fonte de pagamento criada.', + 'Payment source deleted.' => 'Fonte de pagamento eliminada.', + 'Payments' => 'Pagamentos', + 'Pending' => 'Pendente', + 'Per Email Address Discount Limit' => 'Limite de desconto por endereço de email', + 'Per Item Amount Off' => 'Desconto por item', + 'Per Item Discount' => 'Desconto por item', + 'Per Item Percentage Off' => 'Percentagem de desconto por item', + 'Per Item Rate' => 'Custo Por Item', + 'Per User Discount Limit' => 'Limite de desconto por utilizador', + 'Percentage Rate' => 'Custo Proporcional', + 'Phone (Alt)' => 'Telefone (Alt)', + 'Phone' => 'Telefone', + 'Pick a plan' => 'Escolher um plano', + 'Plain Text Email Template Path' => 'Caminho para Template de E-mail Simples', + 'Plan' => 'Plano', + 'Plans reordered.' => 'Planos reordenados.', + 'Portrait' => 'Retrato', + 'Post Date' => 'Data de Envio', + 'Postal Code Formula' => 'Fórmula do código postal', + 'Pounds (lb)' => 'Libras (lb)', + 'Preview' => 'Pré-visualização', + 'Previous Status' => 'Status Anterior', + 'Price' => 'Preço', + 'Prices' => 'Preços', + 'Pricing Rules' => 'Regras de preços', + 'Pricing jobs are currently running.' => 'As atividades de fixação de preços estão atualmente em curso.', + 'Pricing' => 'Preços', + 'Primary Billing Address' => 'Endereço de faturação principal', + 'Primary Shipping Address' => 'Endereço de envio principal', + 'Primary payment source updated.' => 'Fonte de pagamento principal atualizada.', + 'Primary' => 'Principal', + 'Private Note' => 'Nota privada', + 'Product Fields' => 'Campos de Produto', + 'Product ID is required.' => 'É necessária a ID do produto.', + 'Product Template' => 'Template do Produto', + 'Product Title Format' => 'Formato do Título do Produto', + 'Product Type' => 'Tipo de produto', + 'Product Types' => 'Tipos de Produto', + 'Product URI Format' => 'Formato do URI do Produto', + 'Product Variant' => 'Variante de produto', + 'Product Variants' => 'Variantes de produto', + 'Product type saved.' => 'Tipo de produto guardado.', + 'Product type settings' => 'Definições do tipo de produto', + 'Product' => 'Produto', + 'Products and Variants deleted.' => 'Produtos e Variantes eliminados.', + 'Products not restored.' => 'Produtos não restaurados.', + 'Products restored.' => 'Produtos restaurados.', + 'Products' => 'Produtos', + 'Promotable' => 'Promovível', + 'Promotable?' => 'Promovível?', + 'Promotional Amount' => 'Montante promocional', + 'Promotional Price' => 'Preço promocional', + 'Purchasable Categories' => 'Categorias para compra', + 'Purchasable ID and Sale ID are required.' => 'É necessária a ID do produto para compra e da oferta.', + 'Purchasable ID is required.' => 'É necessária a ID do produto para compra.', + 'Purchasable Type' => 'Tipo de artigo para compra', + 'Purchasable' => 'Para compra', + 'Purchase (Authorize and Capture Immediately)' => 'Comprar (Autorizar e cobrar imediatamente)', + 'Purchase Total' => 'Total da Compra', + 'Qty' => 'Qtd', + 'Quality Control' => 'Controlo de qualidade', + 'Quantity' => 'Quantidade', + 'Rate' => 'Taxa', + 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Reatribuir {numOrders, plural, one {}=1{pedido} other{pedidos}}', + 'Recalculate order' => 'Recalcular pedido', + 'Receive Inventory' => 'Receber inventário', + 'Receive Transfer' => 'Receber transferência', + 'Receive' => 'Receber', + 'Received' => 'Recebido', + 'Recent Orders' => 'Pedidos Recentes', + 'Recipient' => 'Destinatário', + 'Recover Cart' => 'Recuperar carrinho', + 'Reduce price' => 'Reduzir preço', + 'Reduce the price by a fixed amount' => 'Reduzir um valor fixo no preço', + 'Reduce the price by a percentage of the original price' => 'Reduzir o preço segundo uma percentagem do preço original', + 'Reference' => 'Referência', + 'Refresh payment history' => 'Atualizar histórico de pagamentos', + 'Refund note' => 'Nota de reembolso', + 'Refund payment' => 'Reembolsar pagamento', + 'Refund' => 'Estornar', + 'Reject' => 'Rejeitar', + 'Rejected' => 'Rejeitado', + 'Relationship Type' => 'Tipo de relação', + 'Removable included tax rates are only allowed for the default tax zone.' => 'As taxas de imposto removíveis incluídas só são permitidas na zona de imposto predefinida.', + 'Remove address' => 'Remover endereço', + 'Remove all shipping costs from the order' => 'Remover todos os custos de envio do pedido', + 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Remover a associação do cliente e o endereço de e-mail {numOrders, plural, one {}=1{do pedido} other{dos pedidos}}. Se desejar, selecione abaixo os dados adicionais do cliente que pretende eliminar', + 'Remove customer data' => 'Remover dados dos clientes', + 'Remove from price?' => 'Remover do preço?', + 'Remove shipping costs for matching items only' => 'Remove os custos de envio apenas nos itens correspondentes', + 'Remove the included tax when a valid organization tax ID is present?' => 'Remover o imposto incluído quando está presente um NIF empresarial válido?', + 'Remove' => 'Remover', + 'Removed' => 'Removido', + 'Repeat Customers' => 'Repetir clientes', + 'Reply To' => 'Responder a', + 'Require Billing Address At Checkout' => 'Exigir endereço de faturação no checkout', + 'Require Coupon Code' => 'Requer Código de Cupão', + 'Require Shipping Address At Checkout' => 'Exigir endereço de envio no checkout', + 'Require Shipping Method Selection At Checkout' => 'Exigir a seleção do método de envio no checkout', + 'Require' => 'Exigir', + 'Reserved' => 'Reservado', + 'Reset usage' => 'Restaurar a utilização', + 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Restringir o desconto apenas aos pedidos de clientes que tenham comprado o valor mínimo total em itens válidos.', + 'Revenue Options' => 'Opções de receita', + 'Revenue' => 'Receita', + 'Rule' => 'Regra', + 'Rules reordered.' => 'Regras reordenadas.', + 'SKU' => 'SKU', + 'Safety' => 'Segurança', + 'Sale Price' => 'Preço de venda', + 'Sale description.' => 'Descrição da Oferta.', + 'Sale reordered.' => 'Venda reencomendada.', + 'Sale saved.' => 'Oferta guardada.', + 'Sale' => 'Oferta', + 'Sales deleted.' => 'Promoções eliminadas.', + 'Sales updated.' => 'Ofertas atualizadas.', + 'Sales' => 'Ofertas', + 'Save and continue editing' => 'Salvar e continuar editando', + 'Save and return to all orders' => 'Guardar e devolver a todos os pedidos', + 'Save and set rules' => 'Salvar e definir regras', + 'Save as a new rule' => 'Guardar como nova regra', + 'Save product to all sites enabled for this product type' => 'Guardar produto em todos os sites ativos para este tipo de produto', + 'Save product to other sites in the same site group' => 'Guardar produto noutros sites do mesmo grupo de sites', + 'Save product to other sites with the same language' => 'Guardar produto para outros sites com o mesmo idioma', + 'Save' => 'Salvar', + 'Search customer…' => 'Pesquisar cliente…', + 'Search inventory' => 'Pesquisar inventário', + 'Search or enter customer email…' => 'Pesquisar ou introduzir email do cliente…', + 'Search…' => 'Pesquisar…', + 'See Orders' => 'Ver pedidos', + 'Select a gateway' => 'Selecionar um gateway', + 'Select a tax category.' => 'Selecionar uma categoria de tributos.', + 'Select a tax zone. If empty, this rate will match anywhere.' => 'Selecione uma zona fiscal. Se estiver em branco, esta taxa corresponderá a qualquer lugar.', + 'Select address' => 'Selecionar endereço', + 'Select an item' => 'Selecionar um item', + 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Selecione como a regra de determinação do preço do catálogo será aplicada ao(s) artigo(s) para compra.', + 'Select how the sale will be applied to the purchasable(s).' => 'Selecione como o desconto será aplicado aos itens que podem ser comprados.', + 'Select product type' => 'Selecionar o tipo de produto', + 'Select the emails that will be sent when transitioning to this status.' => 'Selecione os e-mails que serão enviados ao transicionar para esse status.', + 'Select what this rate should be applied to.' => 'Selecione onde aplicar esta taxa.', + 'Send Email' => 'Enviar email', + 'Send to custom recipient' => 'Enviar para um recipiente personalizado', + 'Send to the customer' => 'Enviar para o cliente', + 'Set Quantity' => 'Definir quantidade', + 'Set default category' => 'Definir categoria padrão', + 'Set default variant' => 'Definir variante predefinida', + 'Set or Adjust' => 'Definir ou ajustar', + 'Set price' => 'Definir preço', + 'Set status' => 'Definir status', + 'Set the price to a flat amount' => 'Definir o preço para um montante fixo', + 'Set the price to a percentage of the original price' => 'Definir o preço para uma percentagem do preço original', + 'Set the sale price to a flat amount' => 'Definir uma quantia fixa no preço de venda', + 'Set the sale price to a percentage of the original price' => 'Definir o preço de venda como uma percentagem do preço original', + 'Set to' => 'Definir para', + 'Settings saved.' => 'Definições guardadas.', + 'Settings' => 'Configurações', + 'Share cart…' => 'Partilhar carrinho…', + 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Envio - O custo mínimo é o custo de envio, caso o preço do pedido seja inferior ao custo de envio.', + 'Shipping Address Zone' => 'Zona do endereço de envio', + 'Shipping Address' => 'Endereço de Envio', + 'Shipping Business Name' => 'Nome da Empresa para Envio', + 'Shipping Categories' => 'Categorias de Envio', + 'Shipping Category Conditions' => 'Condições da Categoria de Envio', + 'Shipping Category' => 'Categoria de Envio', + 'Shipping First Name' => 'Primeiro nome de envio', + 'Shipping Full Name' => 'Nome Completo de Envio', + 'Shipping Last Name' => 'Último nome de envio', + 'Shipping Method' => 'Método de Envio', + 'Shipping Methods' => 'Métodos de Envio', + 'Shipping Rule' => 'Regra de Envio', + 'Shipping Zones' => 'Zonas de Envio', + 'Shipping address required.' => 'É necessário um endereço de envio.', + 'Shipping categories deleted.' => 'Categorias de envio eliminadas.', + 'Shipping category saved.' => 'Categoria de envio guardada.', + 'Shipping category updated.' => 'Categoria de envio atualizada.', + 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Custos de envio adicionados ao pedido como um todo antes que as taxas de percentagem, o item e o peso sejam aplicados. Defina como zero para desativar esta taxa. A regra completa, incluindo esta taxa básica, não será igual e só é aplicável se o carrinho contiver apenas itens que não precisam de ser enviados, tais como produtos digitais.', + 'Shipping method saved.' => 'Método de envio guardado.', + 'Shipping methods and rules deleted.' => 'Métodos e regras de envio eliminados.', + 'Shipping methods updated.' => 'Métodos de envio atualizados.', + 'Shipping rule saved.' => 'Regra de envio guardada.', + 'Shipping zone saved.' => 'Zona de envio guardada.', + 'Shipping' => 'Envio', + 'Short Number' => 'Número curto', + 'Show Chart?' => 'Mostrar carrinho?', + 'Show Order Count?' => 'Mostrar contagem de pedidos?', + 'Show all prices' => 'Mostrar todos os preços', + 'Show archived gateways' => 'Mostrar gateways arquivados', + 'Show order count line on chart.' => 'Mostrar a linha de contagem de pedidos no gráfico.', + 'Show related sales' => 'Mostrar vendas relacionadas', + 'Show rule details' => 'Mostrar os detalhes da regra', + 'Show the Dimensions and Weight fields for products of this type' => 'Mostrar os campos de Dimensões e Peso para produtos desse tipo', + 'Show the Title field for products' => 'Mostra o campo Título para produtos', + 'Show the Title field for variants' => 'Mostra o campo Título para variantes', + 'Signed In' => 'Iniciou sessão', + 'Site Languages' => 'Idioma do site', + 'Site store mapping saved.' => 'Mapeamento da loja do site guardado.', + 'Sites' => 'Sites', + 'Slug' => 'Slug', + 'Snapshot' => 'Snapshot', + 'Snapshots' => 'Snapshots', + 'Some orders restored.' => 'Alguns pedidos restaurados.', + 'Some products restored.' => 'Alguns produtos restaurados.', + 'Some variants restored.' => 'Algumas variantes restauradas.', + 'Something changed with the order before payment, please review your order and submit payment again.' => 'Algo mudou no pedido antes do pagamento, por favor reveja-o e faça novamente o pagamento.', + 'Sorry, no matching options.' => 'Lamentamos mas não há opções correspondentes.', + 'Source - The purchasable relationship field is on the category' => 'Fonte - O campo de relação de compra está na categoria', + 'Source' => 'Fonte', + 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Especifique uma condição Twig que determina se o desconto deve ser aplicado a um pedido específico. (O pedido pode ser referenciado através de uma variável `order`.)', + 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Especifique uma condição Twig que determina se a regra de envio deve ser aplicada a um pedido específico. (O pedido pode ser referenciado através de uma variável `order`.)', + 'Start Date' => 'Data Inicial', + 'State' => 'Estado', + 'Status Email Address' => 'Endereço de E-mail de Status', + 'Status Emails' => 'E-mails de Status', + 'Status History' => 'Histórico de estado', + 'Status Updated.' => 'Estado atualizado.', + 'Status change message' => 'Mensagem de alteração de estado', + 'Status' => 'Estado', + 'Stock' => 'Estoque', + 'Stops Processing?' => 'Para o processamento?', + 'Stops subsequent?' => 'Para o subsequente?', + 'Store Location' => 'Localização da loja', + 'Store Management' => 'Gestão de loja', + 'Store Markets' => 'Mercados da loja', + 'Store Rule' => 'Regra da loja', + 'Store saved.' => 'Loja guardada.', + 'Store' => 'Loja', + 'Stores & Sites' => 'Lojas e sites', + 'Stores' => 'Lojas', + 'Strategy to apply when an order is free or has a zero balance.' => 'Estratégia a aplicar quando um pedido é grátis ou tem saldo zero.', + 'Strategy to apply when calculating the minimum order price.' => 'Estratégia a aplicar ao calcular o preço mínimo do pedido.', + 'Subject' => 'Assunto', + 'Subscribing user' => 'Utilizador com subscrição ativa', + 'Subscription Fields' => 'Campos de subscrição', + 'Subscription Plans' => 'Planos de subscrição', + 'Subscription Settings' => 'Definições da subscrição', + 'Subscription cancelled.' => 'Subscrição cancelada.', + 'Subscription date' => 'Data de subscrição', + 'Subscription fields saved.' => 'Campos de subscrição guardados.', + 'Subscription for {user} to {plan} prevented by a plugin.' => 'Subscrição de {user} para {plan} impedida por um plugin.', + 'Subscription plan saved.' => 'Plano de subscrição guardado.', + 'Subscription plan' => 'Plano de subscrição', + 'Subscription plans' => 'Planos de subscrição', + 'Subscription reactivated.' => 'Subscrição reativada.', + 'Subscription reference' => 'Referência da subscrição', + 'Subscription started.' => 'Subscrição iniciada.', + 'Subscription switched.' => 'Subscrição trocada.', + 'Subscription to “{plan}”' => 'Subscrição de “{plan}”', + 'Subscription' => 'Subscrição', + 'Subscriptions on hold' => 'Subscrições em pausa', + 'Subscriptions' => 'Subscrições', + 'Suppress emails' => 'Suprimir emails', + 'Switch plan' => 'Mudar de plano', + 'Switch' => 'Mudar', + 'System' => 'Sistema', + 'Table Columns' => 'Colunas de tabela', + 'Target - The category relationship field is on the purchasable' => 'Destino - O campo da relação da categoria está no produto para compra', + 'Tax & Shipping' => 'Imposto e Envio', + 'Tax (inc)' => 'Taxa (inc)', + 'Tax Categories' => 'Categorias de Tributos', + 'Tax Category' => 'Categorias de Tributos', + 'Tax Rates' => 'Tributos', + 'Tax Zone' => 'Zona de Tributo', + 'Tax Zones' => 'Zonas de Tributos', + 'Tax categories deleted.' => 'Categorias de impostos eliminadas.', + 'Tax category saved.' => 'Categoria de imposto guardada.', + 'Tax category updated.' => 'Categoria de taxa atualizada.', + 'Tax rate saved.' => 'Taxa do imposto guardada.', + 'Tax rates updated.' => 'Taxas de imposto atualizadas.', + 'Tax zone saved.' => 'Zona fiscal guardada.', + 'Tax' => 'Imposto', + 'Taxable Subject' => 'Alvo do Tributo', + 'Template Path' => 'Caminho do Template', + 'That handle is already in use' => 'Essa pega já está a ser usada', + 'That handle is already in use.' => 'Esta pega já está a ser usada.', + 'The PDF to attach to this email.' => 'O PDF a anexar a este email.', + 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'A URL para a página para atualizar os detalhes de cobrança de uma assinatura, bem como gerir a autenticação 3DS.', + 'The address provided is outside the store’s market.' => 'O endereço fornecido está fora do mercado da loja.', + 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'O valor de desconto aplicado a todo o pedido. Esse valor é distribuído pelos itens de linha na ordem do preço mais alto para o preço mais baixo, até que o desconto esteja esgotado.', + 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'O desconto base só pode descontar itens do carrinho para zero até que seja gasto, e não pode tornar o pedido negativo.', + 'The cart recovery link is invalid. Please request a new one.' => 'O link de recuperação do carrinho não é válido. Solicite um novo.', + 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'A taxa de conversão que será aplicada ao converter o valor para essa moeda. Por exemplo, se o item custa {amount1}, uma taxa de conversão de {rate} resultaria em {amount2} na outra moeda.', + 'The countries that orders are allowed to be placed from.' => 'Os países a partir dos quais podem ser feitos pedidos.', + 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'O cupão "{code}" ultrapassou o seu limite de utilização de {limit}.', + 'The customer for this order has been deleted.' => 'O cliente associado a este pedido foi eliminado.', + 'The default shipping category is automatically available to all product types.' => 'A categoria de envio padrão está automaticamente disponível para todos os tipos de produtos.', + 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'O desconto "{name}" ultrapassou o seu limite total de utilização de {limit}.', + 'The download link has expired. Please request a new one.' => 'O link de download expirou. Por favor, solicite um novo.', + 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'O endereço de e-mail do qual os e-mails de status de pedidos são enviados. Deixe em branco para usar o Endereço de E-mail do Sistema definido nas Configurações Gerais do Craft.', + 'The entry that contains the description for this subscription’s plan.' => 'A entrada que contém a descrição deste plano de subscrições.', + 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'O valor fixo que deve ser descontado em cada item. Ex: “3” para $3 de desconto.', + 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'O formato usado para gerar novos cupões, por exemplo {example}. Quaisquer caracteres `#` serão substituídos por uma letra aleatória.', + 'The from and to inventory locations must be different.' => 'Os locais de inventário de origem e destino devem ser diferentes.', + 'The inventory locations this store uses.' => 'Os locais de inventário que esta loja utiliza.', + 'The item is not enabled for sale.' => 'O item não está disponível para venda.', + 'The language the order was made in.' => 'O idioma em que foi feito o pedido.', + 'The language to be used when this email is rendered.' => 'O idioma a usar quando este e-mail for renderizado.', + 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'O número máximo de níveis que este tipo de produto pode ter. Deixe em branco se você não se importa.', + 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'O máximo que o cliente deve gastar com o envio. Defina o valor como zero para desabilitar.', + 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'O mínimo que o cliente deve gastar com o envio. Defina o valor como zero para desabilitar.', + 'The order is not valid.' => 'O pedido não é válido.', + 'The payment gateway that will be used for the subscription plan.' => 'Que gateway de pagamento será utilizado para o plano de subscrição.', + 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'O valor percentual que deve descontar em cada item, ou seja, {ex1} para {ex2} de desconto. As percentagens são arredondadas para duas casas decimais.', + 'The previously-selected shipping method is no longer available.' => 'Já não está disponível o método de envio selecionado anteriormente.', + 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'O preço de {description} subiu de {originalSalePriceAsCurrency} para {newSalePriceAsCurrency}', + 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'O preço de {description} baixou de {originalSalePriceAsCurrency} para {newSalePriceAsCurrency}', + 'The primary currency cannot be changed after orders are placed.' => 'A moeda principal não pode ser alterada depois dos pedidos terem sido feitos.', + 'The purchasable defines the relationship' => 'Os produtos para compra definem a relação', + 'The purchasable is related by another element' => 'Os produtos para compra estão relacionados por outro elemento', + 'The recipient of the email. Twig code can be used here.' => 'O destinatário do email. O código twig pode ser utilizado aqui.', + 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'A resposta ao endereço de email. Deixar em branco para resposta normal de remetente de email. O código Twig pode ser usado aqui.', + 'The site the order was made in.' => 'O site onde o pedido foi feito.', + 'The site to be used when this email is rendered.' => 'O site a utilizar quando este e-mail for apresentado.', + 'The subject line of the email. Twig code can be used here.' => 'A linha de assunto do email. O código twig pode ser utilizado aqui.', + 'The template that the PDF should be generated from.' => 'O modelo a partir do qual o PDF deve ser gerado.', + 'The template to be used for HTML emails.' => 'O template a ser usado para e-mails HTML.', + 'The template to be used for plain text emails. Twig code can be used here.' => 'O modelo a ser utilizado nos emails de texto simples. O código twig pode ser utilizado aqui.', + 'The template to use when a product’s URL is requested.' => 'O template a ser usado quando a URL desse produto for requisitada.', + 'The total number of order adjustments changed.' => 'O número total de ajustes no pedido mudou.', + 'The total price of the order changed.' => 'O preço total do pedido mudou.', + 'The total quantity of items within the order changed.' => 'A quantidade total de itens no pedido mudou.', + 'The unique SKU of the donation purchasable.' => 'O SKU único da doação para compra.', + 'The unit of measurement that should be used when specifying product dimensions.' => 'A unidade de medida que deve ser usada ao especificar as dimensões do produto.', + 'The unit of measurement that should be used when specifying product weights.' => 'A unidade de medida que deve ser usada para especificar o peso do produto.', + 'The webhook URL for this gateway.' => 'O webhook URL para este gateway.', + 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'O nome “de” que será usado quando forem enviados e-mails de status de pedido. Deixe em branco para usar o Nome do Remetente definido nas Configurações Gerais do Craft.', + 'There are errors on the order' => 'Há erros no pedido', + 'There are only {num} “{description}” items left in stock.' => 'Existem apenas {num} “{description}” itens em stock.', + 'There aren’t any product types to select yet.' => 'Ainda não há nenhum tipo de produto para selecionar.', + 'There is no gateway or payment source available for use with this order.' => 'Não há gateway ou fonte de pagamento disponível para usar neste pedido.', + 'There is no gateway selected that supports payment sources.' => 'Não foi selecionado um gateway que suporte as fontes de pagamento.', + 'There is no shipping method selected for this order.' => 'Não foi selecionado um método de envio para este pedido.', + 'This URL will load the cart into the user’s session, making it the active cart.' => 'Este URL irá carregar o carrinho para a sessão do utilizador, transformando-o no carrinho ativo.', + 'This action is not allowed for the current user.' => 'Essa ação não é permitida ao utilizador atual.', + 'This category will be used as the default for all purchasables in this store.' => 'Esta categoria será utilizada como padrão para todas as compras nesta loja.', + 'This coupon is for registered users and limited to {limit} uses.' => 'Este cupão é para utilizadores registados e está limitado a {limit} utilizações.', + 'This coupon is limited to {limit} uses.' => 'Este cupão está limitado a {limit} utilizações.', + 'This coupon requires an email address.' => 'Este cupão requer um endereço de e-mail.', + 'This gateway does not support that functionality.' => 'Este gateway não suporta essa funcionalidade.', + 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Isto está a ser sobreposto pela definição de configuração {setting} em `config/{file}.php`.', + 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Este é o endereço da localização da sua loja. Pode ser utilizado por vários plugins para determinar aspetos como o envio e os impostos. Também pode ser utilizado em recibos PDF.', + 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Este é o PDF padrão que será renderizado ao pedir o PDF do pedido.', + 'This is the last location for the {store} store.' => 'Esta é a última localização da loja {store}.', + 'This month' => 'Este mês', + 'This order has unsaved changes.' => 'Este pedido tem alterações não guardadas.', + 'This week' => 'Esta semana', + 'This year' => 'Este ano', + 'Times Used' => 'Vezes Usado', + 'Title' => 'Título', + 'To' => 'Para', + 'Today' => 'Hoje', + 'Too many variants for this product.' => 'Demasiadas variantes para este produto.', + 'Top Customers by Average Order' => 'Clientes principais por média de pedido', + 'Top Customers by Total Revenue' => 'Clientes principais por rendimento total', + 'Top Customers' => 'Clientes Principais', + 'Top Product Types by Qty Sold' => 'Principais tipos de produtos por quantidade vendida', + 'Top Product Types by Revenue' => 'Principais tipos de produtos por receita', + 'Top Product Types' => 'Principais Tipos de Produtos', + 'Top Products by Qty Sold' => 'Principais produtos por quantidade vendida', + 'Top Products by Revenue' => 'Principais produtos por receita', + 'Top Products' => 'Produtos Principais', + 'Top Purchasables by Qty Sold' => 'Principais produtos para compra por quantidade vendida', + 'Top Purchasables by Revenue' => 'Principais produtos para compra por receita', + 'Top Purchasables' => 'Principais produtos para compra', + 'Total ' => 'Total ', + 'Total Discount Use Limit' => 'Limite Total de Uso do desconto', + 'Total Discount' => 'Desconto Total', + 'Total Included Tax' => 'Imposto total incluído', + 'Total Orders by Billing Country' => 'Total de pedidos por país de faturação', + 'Total Orders by Country' => 'Total de pedidos por país', + 'Total Orders by Shipping Country' => 'Total de pedidos por país de envio', + 'Total Orders' => 'Encomendas totais', + 'Total Paid' => 'Total Pago', + 'Total Price' => 'Preço Total', + 'Total Qty' => 'Quantidade total', + 'Total Revenue' => 'Total de receitas', + 'Total Shipping' => 'Total de Envio', + 'Total Tax' => 'Imposto Total', + 'Total Weight' => 'Peso total', + 'Total' => 'Total', + 'Track Inventory' => 'Monitorizar inventário', + 'Transaction Hash' => 'Hash da transação', + 'Transaction ID' => 'ID da transação', + 'Transaction captured successfully: {message}' => 'Transação realizada com sucesso: {message}', + 'Transaction refunded successfully: {message}' => 'Transação reembolsada com sucesso: {message}', + 'Transactions' => 'Transações', + 'Transfer Fields' => 'Campos de transferência', + 'Transfer Items' => 'Itens da transferência', + 'Transfer Settings' => 'Definições da transferência', + 'Transfer Status' => 'Estado da transferência', + 'Transfer fields saved.' => 'Campos de transferência guardados.', + 'Transfer must have at least one item.' => 'A transferência deve ter pelo menos um item.', + 'Transfer' => 'Transferência', + 'Transfers' => 'Transferências', + 'Trial days credited' => 'Dias de teste creditados', + 'Trial expiration' => 'Data de validade do teste', + 'Trial expiry date' => 'Data de validade do período experimental', + 'Type not in allowed options.' => 'O tipo não está nas opções permitidas.', + 'Type' => 'Tipo', + 'URI' => 'URI', + 'Unable to cancel subscription at this time.' => 'De momento, não é possível cancelar a subscrição.', + 'Unable to complete order: another request is already in progress.' => 'Não foi possível concluir o pedido: já existe outro pedido em curso.', + 'Unable to find variant.' => 'Não foi possível encontrar a variante.', + 'Unable to generate coupon codes: {message}' => 'Não foi possível gerar códigos de cupão: {message}', + 'Unable to make payment at this time.' => 'De momento, não é possível efetuar o pagamento.', + 'Unable to modify subscription at this time.' => 'De momento, não é possível modificar a subscrição.', + 'Unable to reactivate subscription at this time.' => 'De momento, não é possível reativar a subscrição.', + 'Unable to reassign orders.' => 'Não é possível reatribuir pedidos.', + 'Unable to remove order data.' => 'Não foi possível eliminar os dados do pedido.', + 'Unable to retrieve Sale and Purchasable.' => 'Não foi possível encontrar a Oferta e o Produto para compra.', + 'Unable to retrieve cart.' => 'Não foi possível encontrar o carrinho.', + 'Unable to retrieve customer.' => 'Não foi possível encontrar o cliente.', + 'Unable to retrieve load cart URL' => 'Não foi possível carregar o URL do carrinho', + 'Unable to retrieve payment source.' => 'Não foi possível encontrar a fonte de pagamento.', + 'Unable to set default shipping category.' => 'Não foi possível definir a categoria de envio padrão.', + 'Unable to set default tax category.' => 'Não foi possível definir a categoria de taxas padrão.', + 'Unable to set primary payment source.' => 'Não foi possível definir a fonte principal de pagamento.', + 'Unable to start the subscription. Please check your payment details.' => 'Não foi possível iniciar a subscrição. Verifique os seus dados de pagamento.', + 'Unable to subscribe at this time.' => 'De momento, não é possível subscrever.', + 'Unable to update cart.' => 'Não foi possível atualizar o carrinho.', + 'Unable to validate address.' => 'Não foi possível validar a morada.', + 'Unit Price' => 'Preço unitário', + 'Unit price (minus discounts)' => 'Preço unitário (menos descontos)', + 'Units' => 'Unidades', + 'Unpaid' => 'Não pago', + 'Unsubscribe' => 'Anular subscrição', + 'Update Address' => 'Atualizar endereço', + 'Update Order Status' => 'Atualizar o estado da encomenda', + 'Update Order Status…' => 'Atualizar Status do Pedido…', + 'Update order' => 'Atualizar pedido', + 'Update subscription' => 'Atualizar subscrição', + 'Update' => 'Atualizar', + 'Updated By' => 'Atualizado por', + 'Updated committed stock successfully.' => 'Atualizado o stock comprometido com sucesso.', + 'Updated' => 'Atualizado', + 'Use Billing Address For Tax' => 'Utilizar o endereço de faturação para o imposto', + 'Use as the primary billing address' => 'Utilizar como endereço de faturação principal', + 'Use as the primary shipping address' => 'Utilizar como endereço de envio principal', + 'Used By Tax Rates' => 'Usado pelos Tributos', + 'Used by Tax Rates' => 'Usado pelos Tributos', + 'User Groups' => 'Grupos de Usuário', + 'User not found.' => 'O utilizador não foi encontrado.', + 'User' => 'Utilizador', + 'Uses' => 'Utilizações', + 'Validate Business Tax ID as Vat ID' => 'Validar o NIF da empresa como ID do IVA', + 'Validating condition syntax' => 'A validar a sintaxe da condição', + 'Validating formula syntax' => 'A validar a sintaxe da fórmula', + 'Variant Fields' => 'Campos de Variante', + 'Variant Has Untracked Stock' => 'A variante tem stock sem acompanhamento', + 'Variant Price' => 'Preço da variante', + 'Variant SKU' => 'SKU da variante', + 'Variant Search' => 'Pesquisa de variantes', + 'Variant Stock' => 'Stock da variante', + 'Variant Title Format' => 'Formato do Título de Variante', + 'Variant Tracks Stock' => 'Acompanhamento de stock de variantes', + 'Variant UI Label Format' => 'Formato da etiqueta de interface variante', + 'Variant has no product.' => 'A variante não tem produto.', + 'Variants not restored.' => 'Variantes não restauradas.', + 'Variants restored.' => 'Variantes restauradas.', + 'Variants' => 'Variantes', + 'View customer' => 'Ver cliente', + 'View order' => 'Ver pedido', + 'View product type - {productType}' => 'Ver tipo de produto - {productType}', + 'View user' => 'Ver utilizador', + 'View' => 'Ver', + 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Aviso, apagar esta moeda irá parar todos os pagamentos e reembolsos nesta moeda, tem a certeza que quer apagar "{name}"?', + 'Web' => 'Web', + 'Webhook URL' => 'Webhook URL', + 'Weight ({unit})' => 'Peso ({unit})', + 'Weight Rate' => 'Fator Multiplicador por Peso', + 'Weight Unit' => 'Unidade de Peso', + 'Weight' => 'Peso', + 'What product URIs should look like for the site.' => 'Como deverá ser o aspeto dos URI de produtos do site.', + 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Como os títulos de produtos gerados automaticamente devem ser. Pode incluir tags que indiquem propriedades do produto, tais como {ex1} ou {ex2}. Todos os campos personalizados devem ser definidos como obrigatórios.', + 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Como os títulos de variante gerado automaticamente devem ser. Você pode incluir tags que deem como resultado propriedades do variante, como {ex1} ou {ex2}. Todos os campos personalizados devem ser definidos como obrigatórios.', + 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Como os nomes dos ficheiros PDF do pedido devem parecer (sem extensão). Pode incluir tags que emitam propriedades do pedido, como {ex1} ou {ex2}.', + 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Como o SKU único gerado automaticamente deve ser, quando um campo SKU é enviado sem um valor. Você pode incluir tags que deem como resposta propriedades, como {ex1} ou {ex2}', + 'What this PDF will be called in the control panel.' => 'Qual o nome deste PDF no Painel de Controlo.', + 'What this catalog pricing rule will be called in the control panel.' => 'Como será designada esta regra de fixação de preços de catálogo no painel de controlo.', + 'What this discount will be called in the control panel.' => 'Qual o nome deste desconto no Painel de Controlo.', + 'What this email will be called in the control panel.' => 'Nome deste email no Painel de Controlo.', + 'What this product type will be called in the control panel.' => 'Como este tipo de produto será chamado no Painel de Controlo.', + 'What this sale will be called in the control panel.' => 'Qual o nome desta venda no Painel de Controlo.', + 'What this shipping category will be called in the control panel.' => 'Qual o nome desta categoria no Painel de Controlo.', + 'What this shipping rule will be called in the control panel.' => 'Qual o nome desta regra de envio no Painel de Controlo.', + 'What this shipping zone will be called in the control panel.' => 'Qual o nome desta zona de envio no Painel de Controlo.', + 'What this status will be called in the control panel.' => 'Qual o nome deste estado no Painel de Controlo.', + 'What this subscription plan will be called in the control panel.' => 'Qual o nome deste plano de subscrição no Painel de Controlo.', + 'What this tax category will be called in the control panel.' => 'Qual o nome desta categoria de taxa no painel de controlo.', + 'What this tax zone will be called in the control panel.' => 'Qual o nome desta zona de taxa no painel de controlo.', + 'When this discount is applied to an order, which line items should be discounted?' => 'Quando este desconto é aplicado a um pedido, que itens de linha devem ter desconto?', + 'Whether the first available shipping method option should be set automatically on carts.' => 'Se a primeira opção de método de envio disponível deve ser definida automaticamente nos carrinhos.', + 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Se a fonte de pagamento principal do utilizador deve ser definida automaticamente nos novos carrinhos.', + 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Se os endereços principais de envio e faturação do utilizador devem ser definidos automaticamente nos novos carrinhos.', + 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Se esta regra de determinação de preços do catálogo deve estar disponível para utilização, independentemente de outras condições.', + 'Whether this sale should be available for use, regardless of other conditions.' => 'Se a venda deve ou não estar disponível para utilização, independentemente das outras condições.', + 'Which data to display in the name column in the results table.' => 'Qual a data a indicar na coluna de nome na tabela de resultados.', + 'Which product types should this category be available to?' => 'Para que tipos de produto é que esta categoria deve ficar disponível?', + 'Which template should be loaded when a product’s URL is requested.' => 'Que modelo deverá ser carregado quando é solicitado o URL de um produto.', + 'Width ({unit})' => 'Largura ({unit})', + 'Width' => 'Largura', + 'YYYY' => 'AAAA', + 'Yes' => 'Sim', + 'You are not allowed to add a line item.' => 'Não tem permissão para adicionar um item de linha.', + 'You currently have no emails configured to select for this status.' => 'Atualmente não tem emails configurados a selecionar para este estado.', + 'You do not have permission to load this cart.' => 'Não tem permissão para carregar este carrinho.', + 'You must set up at least one gateway that supports subscriptions first.' => 'Primeiro, deve configurar, no mínimo, um gateway que suporte subscrições.', + 'You must be logged in or provide a valid token to load this cart.' => 'Tem de iniciar sessão ou fornecer um token válido para carregar este carrinho.', + 'You must be signed in to create a payment source.' => 'Deve ter sessão iniciada para criar uma fonte de pagamento.', + 'You must be signed in to set a primary payment source.' => 'Deve ter sessão iniciada para definir uma fonte de pagamento principal.', + 'You must make a payment to complete the order.' => 'Tem de fazer o pagamento para concluir o pedido.', + 'Your Cart Recovery Link' => 'Link para recuperar o seu carrinho', + 'Your Order PDF Download Link' => 'O seu link de pedido de transferência de PDF', + 'Your order is empty' => 'O seu pedido está vazio', + 'ZIP file' => 'Ficheiro ZIP', + 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Zero - O preço mínimo é zero caso os descontos sejam maiores do que o valor do pedido.', + 'Zip Code' => 'CEP', + 'all' => 'tudo', + 'any' => 'qualquer', + 'average order total' => 'total médio de pedidos', + 'billing address' => 'endereço de faturação', + 'donation' => 'doação', + 'donations' => 'doações', + 'info' => 'informações', + 'inventory location' => 'localização do inventário', + 'new customers' => 'novos clientes', + 'on hand' => 'disponível', + 'only' => 'apenas', + 'order' => 'pedido', + 'orders' => 'pedidos', + 'price' => 'preço', + 'prices' => 'preços', + 'product variant' => 'variante de produto', + 'product variants' => 'variantes de produto', + 'product' => 'produto', + 'products' => 'produtos', + 'repeat customers' => 'repetir clientes', + 'shipping address' => 'endereço de Envio', + 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'shippingSameAsBilling e billingSameAsShipping não podem ser definidos em simultâneo.', + 'subscription' => 'subscrição', + 'subscriptions' => 'subscrições', + 'to' => 'para', + 'transfer' => 'transferência', + 'transfers' => 'transferências', + '{amount} included' => '{amount} incluído', + '{count} Unfulfilled Orders' => '{count} Ordens não cumpridas', + '{description} is no longer available.' => '{description} já não está disponível.', + '{description} only has {stock} in stock.' => '{description} só tem {stock} em stock.', + '{from} to {to}' => '{from} para {to}', + '{name} (Primary)' => '{name} (Principal)', + '{name} (Trashed)' => '{name} (Movido para a reciclagem)', + '{name} catalog price' => '{name} preço de catálogo', + '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, =1{pedido} other{pedidos}} atualizado(s).', + '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, one {}=1{pedido é} other{pedidos são}} associated with the {numUsers, plural, one {}=1{utilizador} other{utilizadores}}.', + '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, one {}=1{subscrição está} other{subscrições estão}} ativada para {numUsers, plural, one {}=1{utilizador} other{utilizadores}}.', + '{number} more…' => '{number} mais…', + '{pct} off the discounted item price' => '{pct} de desconto no preço do item', + '{pct} off the original item price' => '{pct} de desconto no preço original do item', + '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, one {}=1{não foi atribuído} other{não foram atribuídos}} a um site.', + '{total} in total revenue' => '{total} em receitas totais', + '{total} orders' => '{total} pedidos', + '{total} saleable across {locationCount} location(s)' => '{total} vendável através de {locationCount} local(ais)', + '{uses} uses across {emails} email addresses' => '{uses} utilizações em {emails} endereços de email', + '{uses} uses across {users} users' => '{uses} utilizações em {users} utilizadores', + '“{description}” is currently out of stock.' => '“{description}” está fora de stock de momento.', + '“{key}” has invalid JSON' => '“{key}” tem JSON inválido', +]; diff --git a/lang/sk/commerce.php b/lang/sk/commerce.php new file mode 100644 index 0000000000..a911780be0 --- /dev/null +++ b/lang/sk/commerce.php @@ -0,0 +1,1423 @@ + '(nová cena)', + '(of original price)' => '(z pôvodnej ceny)', + '(off original price)' => '(z pôvodnej ceny)', + 'A cart number must be specified.' => 'Musíte uviesť číslo košíka.', + 'A cart recovery link has been sent to {email}.' => 'Odkaz na obnovenie košíka bol odoslaný na adresu {email}.', + 'A cart recovery link will be sent to {email}.' => 'Odkaz na obnovenie košíka odošleme na adresu {email}.', + 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Po naplnení košíka a jeho premene na objednávku sa na základe tohto formátu vytvorí zrozumiteľné referenčné číslo. Napríklad {ex1} alebo
{ex2}. Výsledok tohto formátovania musí byť jedinečný.', + 'A new download link has been sent to {email}' => 'Nový odkaz na stiahnutie bol odoslaný na adresu {email}', + 'A new download link will be sent to {email}' => 'Nový odkaz na stiahnutie bude zaslaný na adresu {email}', + 'A valid email is required to create a customer.' => 'Pre vytvorenie zákazníka sa vyžaduje platná e-mailová adresa.', + 'Accept' => 'Prijať', + 'Accepted' => 'Prijaté', + 'Actions' => 'Akcie', + 'Active Carts' => 'Aktívne Košíky', + 'Active subscriptions' => 'Aktívne prihlásenia na odber', + 'Active' => 'Aktívny', + 'Add Address' => 'Pridať adresu', + 'Add a coupon' => 'Pridať kupón', + 'Add a custom line item' => 'Pridať vlastnú položku', + 'Add a line item' => 'Pridať riadkovú položku', + 'Add a product' => 'Pridať produkt', + 'Add a variant' => 'Pridať variantu', + 'Add an adjustment' => 'Pridať nastavenie', + 'Add an item' => 'Pridať položku', + 'Add an option' => 'Pridať možnosť', + 'Add catalog price' => 'Pridať katalógovú cenu', + 'Add' => 'Pridať', + 'Additional Actions' => 'Dodatočné úkony', + 'Additional recipients that should receive this email. Twig code can be used here.' => 'Dodatoční príjemcovia, ktorí by mali obdržať tento e-mail. Môže byť použitý Twig kód.', + 'Address 1' => 'Adresa 1', + 'Address 2' => 'Adresa 2', + 'Address 3' => 'Adresa 3', + 'Address Line 1' => 'Adresa, riadok 1', + 'Address Line 2' => 'Adresa, riadok 2', + 'Address Updated.' => 'Adresa aktualizovaná.', + 'Address copied to user.' => 'Adresa skopírovaná pre používateľa.', + 'Address not found.' => 'Adresa nebola nájdená.', + 'Adjust Quantity' => 'Upraviť množstvo', + 'Adjust by' => 'Upraviť podľa', + 'Adjust price when included rate is disqualified?' => 'Upraviť cenu, keď je zahrnutá sadzba diskvalifikovaná?', + 'Adjustments' => 'Nastavenia', + 'Admin Notices' => 'Oznámenia správcu', + 'Administrative Area Code of Origin' => 'Kód administratívnej oblasti pôvodu', + 'Advanced' => 'Pokročilé', + 'All Orders' => 'Všetky objednávky', + 'All Totals' => 'Všetko celkom', + 'All Transfers' => 'Všetky prevody', + 'All active subscriptions' => 'Všetky aktívne prihlásenia na odber', + 'All customers' => 'Všetci zákazníci', + 'All products' => 'Všetky produkty', + 'All variants must have a SKU.' => 'Všetky varianty musia mať SKU.', + 'All' => 'Všetky', + 'Allow Checkout Without Payment' => 'Povoliť kontrolu bez platby', + 'Allow Empty Cart On Checkout' => 'Povoliť prázdny košík pri pokladni', + 'Allow Partial Payment On Checkout' => 'Povoliť čiastočnú platbu pri pokladni', + 'Allow out of stock purchases' => 'Umožniť nákupy tovaru, ktorý nie je na sklade', + 'Allow' => 'Povoliť', + 'Allowed Qty' => 'Povolené Množstvo', + 'Alternative Phone' => 'Alternatívne telefónne číslo', + 'Amount' => 'Množstvo', + 'An ID must be provided' => 'Musí byť poskytnutá identifikácia', + 'An error occurred while generating this PDF.' => 'Pri vytváraní tohto súboru PDF sa vyskytla chyba.', + 'Any' => 'Akékoľvek', + 'Anywhere' => 'Kdekoľvek', + 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Naozaj chcete archivovať plán prihlásení na odber {name}? Táto akcia nezruší existujúce prihlásenia na odber.', + 'Are you sure you want to capture this transaction?' => 'Určite zachytiť túto transakciu?', + 'Are you sure you want to complete this order?' => 'Naozaj chcete dokončiť túto objednávku?', + 'Are you sure you want to delete the selected orders?' => 'Určite zmazať vybrané objednávky?', + 'Are you sure you want to delete the selected product and its variants?' => 'Ste si istí, že chcete vymazať vybraný výrobok a jeho varianty?', + 'Are you sure you want to delete this shipping rule?' => 'Naozaj chcete odstrániť toto pravidlo dopravy?', + 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Určite zmazať „{name}“ a všetky prislúchajúce produkty? Uisti sa prosím, že je spravená záloha databázy pred vykonaním tejto deštruktívnej akcie.', + 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Určite zmazať „{name}“? Všetky riadkové položky s týmto stavom budú zmenené na „žiadny stav“.', + 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Naozaj chcete tento prevod označiť ako čakajúci? V cieli sa zobrazí ako prichádzajúci.', + 'Are you sure you want to overwrite the billing address?' => 'Naozaj chcete prepísať fakturačnú adresu?', + 'Are you sure you want to overwrite the shipping address?' => 'Naozaj chcete prepísať dodaciu adresu?', + 'Are you sure you want to permanently delete this store and everything in it?' => 'Naozaj chcete natrvalo odstrániť tento obchod a všetko, čo obsahuje?', + 'Are you sure you want to refund this transaction?' => 'Určite vrátiť túto transakciu?', + 'Are you sure you want to remove this customer?' => 'Naozaj chcete odstrániť tohto zákazníka?', + 'Are you sure you want to save this as a new shipping rule?' => 'Naozaj chcete uložiť ako nové pravidlo dopravy?', + 'Are you sure you want to send email: {name}?' => 'Naozaj chcete poslať e-mail: {name}?', + 'At least one site must be enabled for the product type.' => 'Pre daný typ produktu musí byť povolená aspoň jedna stránka.', + 'Attempted Payments' => 'Pokusy o platbu', + 'Attention' => 'Pozor', + 'Authorize Only (Manually Capture)' => 'Len autorizovať (ručne zachytiť)', + 'Auto Set Cart Shipping Method Option' => 'Automatické nastavenie spôsobu dopravy v košíku', + 'Auto Set New Cart Addresses' => 'Automatické nastavenie nových adries košíka', + 'Auto Set Payment Source' => 'Automatické nastavenie zdroja platby', + 'Automatic SKU Format' => 'Automatický SKU Formát', + 'Available Shipping Categories' => 'Dostupné kategórie dopravy', + 'Available Tax Categories' => 'Dostupné kategórie daní', + 'Available for purchase' => 'Dostupné na nákup', + 'Available for purchase?' => 'Dostupné na nákup?', + 'Available inventory for "{description}" has gone below zero.' => 'Dostupné zásoby pre položky „{description}“ klesli pod nulu.', + 'Available to Product Types' => 'Dostupné pre typy produktov', + 'Available' => 'Dostupné', + 'Available?' => 'Dostupné?', + 'Average Order Total' => 'Priemerná cena objednávky', + 'Average' => 'Priemerný', + 'BCC’d Recipient' => 'Príjemca Skrytej kópie (BCC)', + 'Bad Request' => 'Zlá požiadavka', + 'Bad address ID.' => 'Zlé ID adresy.', + 'Bad order ID.' => 'Zlé ID objednávky.', + 'Base Price' => 'Základná cena', + 'Base Promotional Price' => 'Základná propagačná cena', + 'Base Rate' => 'Základná Sadzba', + 'Base' => 'Základ', + 'Bcc' => 'Skrytá kópia (Bcc)', + 'Billing Address' => 'Fakturačná adresa', + 'Billing Business Name' => 'Fakturačný obchodný názov', + 'Billing First Name' => 'Meno na fakturácii', + 'Billing Full Name' => 'Celý fakturačný názov', + 'Billing Last Name' => 'Priezvisko na fakturácii', + 'Billing address required.' => 'Požaduje sa fakturačná adresa.', + 'Billing detail update URL' => 'URL pre aktualizáciu platobných údajov', + 'Billing issues' => 'Problémy s platbou', + 'Billing' => 'Účtovanie', + 'Both (Line item price + Line item shipping costs)' => 'Obidve (cena riadkovej položky + náklady na dodanie riadkovej položky)', + 'Business ID' => 'IČO', + 'Business Name' => 'Obchodné Meno', + 'Business Tax ID' => 'DIČ', + 'CC’d Recipient' => 'Príjemca kópie (CC)', + 'CVV' => 'CVV', + 'Can be used as an internal reference.' => 'Je možné použiť ako internú referenciu.', + 'Can not complete payment for missing transaction.' => 'Nemožno dokončiť platbu pre chýbajúcu transakciu.', + 'Can not create a new order' => 'Nepodarilo sa vytvoriť novú objednávku', + 'Can not find an order to pay.' => 'Nie je možné nájsť objednávku na zaplatenie.', + 'Can not find enabled email.' => 'Nebolo možné nájsť povolený e-mail.', + 'Can not find order' => 'Objednávku nebolo možné nájsť', + 'Can not find order.' => 'Objednávku nebolo možné nájsť.', + 'Can not find the transaction to refund' => 'Transakcia na refundáciu sa nenašla', + 'Can not move between these inventory types.' => 'Medzi týmito typmi zásob sa nedá pohybovať.', + 'Can not refund amount greater than the remaining amount' => 'Nie je možné refundovať vyššiu sumu ako je zvyšná suma', + 'Cancel subscription' => 'Zrušiť prihlásenie na odber', + 'Cancel with gateway now' => 'Zrušiť cez platobnú bránu teraz', + 'Cancel' => 'Zrušiť', + 'Cancellation date' => 'Dátum zrušenia', + 'Cancellation' => 'Zrušenie', + 'Cannot switch plans for this subscription.' => 'Nie je možné prepnúť plány pre toto prihlásenie na odber.', + 'Can’t preview this email.' => 'Nie je možné zobraziť náhľad tohto e-mailu.', + 'Capture payment' => 'Zachytiť platbu', + 'Capture' => 'Zachytiť', + 'Card Holder' => 'Držiteľ karty', + 'Card Number' => 'Číslo karty', + 'Card' => 'Karta', + 'Cart Recovery Link' => 'Odkaz na obnovenie košíka', + 'Cart forgotten.' => 'Zabudnutý košík.', + 'Cart updated.' => 'Košík je aktualizovaný.', + 'Cart {number}' => 'Košík {number}', + 'Catalog Pricing Rule' => 'Pravidlo stanovovania cien podľa katalógu', + 'Catalog pricing rule description.' => 'Popis pravidla stanovovania cien podľa katalógu.', + 'Catalog pricing rule saved.' => 'Pravidlo stanovovania cien podľa katalógu bolo uložené.', + 'Catalog pricing rules deleted.' => 'Pravidlá stanovovania cien podľa katalógu boli odstránené.', + 'Catalog pricing rules updated.' => 'Pravidlá stanovovania cien podľa katalógu boli aktualizované.', + 'Categories Relationship Type' => 'Kategórie typov vzťahov', + 'Categories' => 'Kategórie', + 'Category Rate Overrides' => 'Nahradenia sadzieb kategórií', + 'Centimeters (cm)' => 'Centimetre (cm)', + 'Changing this value may affect your ability to refund existing transactions.' => 'Zmena tejto hodnoty môže ovplyvniť možnosť refundácie existujúcich transakcií.', + 'Choose a color to represent the order’s status' => 'Vyberte farbu, ktorá má predstavovať stav objednávky', + 'Choose a new customer' => 'Vyberte nového zákazníka', + 'Choose adjustment values to include when calculating the product revenue total.' => 'Vyberte hodnoty úprav, ktoré sa majú zahrnúť pri výpočte celkových príjmov z produktu.', + 'Choose the currency’s ISO code.' => 'Vyberte k mene kód ISO.', + 'Choose the destination inventory location for the existing on hand stock.' => 'Vyberte cieľové miesto zásob pre existujúce zásoby na sklade.', + 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Vyberte, na ktorých weboch by mala byť tento typ produktu k dispozícii, a nakonfigurujte nastavenia pre konkrétne weby.', + 'City' => 'Mesto', + 'Clear counter' => 'Vynulovať počítadlo', + 'Clear notices' => 'Vymazať upozornenia', + 'Close' => 'Zavrieť', + 'Code' => 'Kód', + 'Collated PDF' => 'Zosumarizované PDF', + 'Color' => 'Farba', + 'Commerce Products' => 'Commerce produkty', + 'Commerce Settings' => 'Commerce Nastavenia', + 'Commerce Variants' => 'Varianty systému Commerce', + 'Commerce email “{email}” could not be sent for order “{order}”.' => 'E-mail systému Commerce „{email}“ pre objednávku „{order}“ sa nedá odoslať.', + 'Commerce order exports' => 'Exporty objednávok systému Commerce', + 'Commerce' => 'Commerce', + 'Committed' => 'Odovzdané', + 'Completed Email' => 'Vyplnený e-mail', + 'Completed' => 'Dokončené', + 'Completing order failed.' => 'Nepodarilo sa dokončiť objednávku.', + 'Condition' => 'Stav', + 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Podmienky sa tu porovnávajú s príkazom pred vyhľadaním pravidiel. To je užitočné, ak chcete predčasne overiť dostupnosť spôsobu alebo ak existujú spoločné podmienky pre všetky pravidlá pre tento spôsob.', + 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Podmienky sa tu porovnávajú so zákazníkom objednávky pred vyhľadaním pravidiel. To je užitočné, ak chcete predčasne overiť dostupnosť metódy alebo ak existujú spoločné podmienky pre všetky pravidlá pre túto metódu.', + 'Conditions' => 'Podmienky', + 'Contains Purchasables' => 'Obsahuje položky na predaj', + 'Control Panel Settings' => 'Nastavenia ovládacieho panela', + 'Control panel' => 'Ovládací panel', + 'Conversion Rate' => 'Konverzný kurz', + 'Converted Price' => 'Konvertovaná cena', + 'Copied!' => 'Skopírované!', + 'Copy the URL' => 'Kopírovať URL', + 'Copy to {location}' => 'Kopírovať do {location}', + 'Copy' => 'Kopírovať', + 'Costs' => 'Náklady', + 'Could not archive gateway.' => 'Brána sa nedá archivovať.', + 'Could not cancel “{reference}”.' => 'Nebolo možné zrušiť „{reference}“.', + 'Could not create the payment source.' => 'Zdroj platby sa nedá vytvoriť.', + 'Could not delete shipping rule' => 'Pravidlo dodania sa nedá odstrániť', + 'Could not delete shipping zone' => 'Dodacia oblasť sa nedá odstrániť', + 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Nepodarilo sa vymazať {count, number} {count, plural, one{kategóriu dopravy} few {kategórie dopravy} many {kategórie dopravy} other{kategórií dopravy}}.', + 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Nepodarilo sa vymazať {count, number} {count, plural, one{spôsob dopravy} few {spôsoby dopravy} many {spôsobu dopravy} other{spôsobov dopravy}} a pravidlá.', + 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Nepodarilo sa vymazať {count, number} {count, plural, one{daňovú kategóriu} few {daňové kategórie} many {daňovej kategórie} other{daňových kategórií}}.', + 'Could not find the email or template.' => 'E-mail alebo šablónu sa nepodarilo nájsť.', + 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Objednávku {number} nebolo možné označiť ako dokončenú. Pri dokončovaní nasledujúcej objednávky zlyhalo jej uloženie s chybami: {order}', + 'Could not reactivate “{reference}”.' => 'Nebolo možné znovu aktivovať „{reference}“.', + 'Could not send email' => 'E-mail nebolo možné odoslať', + 'Could not switch “{reference}” to “{plan}”.' => 'Nebolo možné zmeniť „{reference}“ na „{plan}“.', + 'Could not update orders address.' => 'Adresy objednávok sa nedajú aktualizovať.', + 'Couldn’t archive Line Item Status.' => 'Nebolo možné archivovať stav riadkovej položky.', + 'Couldn’t archive Order Status.' => 'Nebolo možné archivovať stav objednávky.', + 'Couldn’t capture transaction.' => 'Nemožno zachytiť transakciu.', + 'Couldn’t capture transaction: {message}' => 'Nemožno zachytiť transakciu: {message}', + 'Couldn’t delete email.' => 'Nepodarilo sa odstrániť e-mail.', + 'Couldn’t delete the payment source.' => 'Zdroj platby sa nedá odstrániť.', + 'Couldn’t get order.' => 'Nemožno získať objednávku.', + 'Couldn’t recalculate order.' => 'Nemožno prepočítať objednávku.', + 'Couldn’t refund transaction.' => 'Nemožno vrátiť transakciu.', + 'Couldn’t refund transaction: {message}' => 'Transakciu nie je možné vrátiť: {message}', + 'Couldn’t reorder Line Item Statuses.' => 'Nedalo sa zmeniť usporiadanie stavov riadkových položiek.', + 'Couldn’t reorder Order Statuses.' => 'Nedalo sa zmeniť usporiadanie stavov objednávok.', + 'Couldn’t reorder PDFs.' => 'Poradie súborov PDF sa nedá zmeniť.', + 'Couldn’t reorder discounts.' => 'Nedalo sa zmeniť usporiadanie zliav.', + 'Couldn’t reorder gateways.' => 'Poradie brán sa nedá zmeniť.', + 'Couldn’t reorder plans.' => 'Opätovná objednávka plánov sa nepodarila.', + 'Couldn’t reorder rules.' => 'Poradie pravidiel sa nedá zmeniť.', + 'Couldn’t reorder sale.' => 'Poradie výpredaja nebolo možné zmeniť.', + 'Couldn’t reorder sales.' => 'Poradie predajných akcií sa nedá zmeniť.', + 'Couldn’t reorder statuses.' => 'Poradie stavov sa nedá zmeniť.', + 'Couldn’t reorder stores.' => 'Nepodarilo sa zmeniť poradie obchodov.', + 'Couldn’t save PDF.' => 'Nemožno uložiť súbor PDF.', + 'Couldn’t save catalog pricing rule.' => 'Nepodarilo sa uložiť pravidlo stanovovania cien podľa katalógu.', + 'Couldn’t save currency.' => 'Mena sa nedala uložiť.', + 'Couldn’t save discount.' => 'Nemožno uložiť zľavu.', + 'Couldn’t save email.' => 'Nemožno uložiť e-mail.', + 'Couldn’t save gateway.' => 'Brána sa nedá uložiť.', + 'Couldn’t save inventory location.' => 'Nepodarilo sa uložiť umiestnenie zásob.', + 'Couldn’t save line item status.' => 'Nemožno uložiť stav riadkovej položky.', + 'Couldn’t save order fields.' => 'Nemožno uložiť polia objednávky.', + 'Couldn’t save order status.' => 'Nemožno uložiť stav objednávky.', + 'Couldn’t save order.' => 'Nemožno uložiť objednávku.', + 'Couldn’t save product type.' => 'Nemožno uložiť typ produktu.', + 'Couldn’t save sale.' => 'Nemožno uložiť výpredaj.', + 'Couldn’t save settings.' => 'Nemožno uložiť nastavenia.', + 'Couldn’t save shipping category.' => 'Kategória dopravy sa nedala uložiť.', + 'Couldn’t save shipping method.' => 'Nemožno uložiť spôsob dodania.', + 'Couldn’t save shipping rule.' => 'Nemožno uložiť pravidlo dodania.', + 'Couldn’t save shipping zone.' => 'Nie je možné vybrať zónu doručenia.', + 'Couldn’t save store.' => 'Nepodarilo sa uložiť obchod.', + 'Couldn’t save subscription fields.' => 'Polia predplatného sa nepodarilo uložiť.', + 'Couldn’t save subscription plan.' => 'Plán prihlásení na odber sa nedá uložiť.', + 'Couldn’t save subscription.' => 'Predplatné nebolo možné uložiť.', + 'Couldn’t save tax category.' => 'Nemožno uložiť daňovú kategóriu.', + 'Couldn’t save tax rate.' => 'Nemožno uložiť daňovú sadzbu.', + 'Couldn’t save tax zone.' => 'Nemožno uložiť daňovú zónu.', + 'Couldn’t save transfer fields.' => 'Nemožno uložiť polia prevodu.', + 'Couldn’t update catalog pricing rule statuses.' => 'Nepodarilo sa aktualizovať stav pravidla stanovovania cien podľa katalógu.', + 'Couldn’t update status.' => 'Stav sa nepodarilo aktualizovať.', + 'Couldn’t updated sales status.' => 'Nebolo možné aktualizovať stav výpredajov.', + 'Country Code of Origin' => 'Kód krajiny pôvodu', + 'Country List' => 'Zoznam krajín', + 'Country not allowed.' => 'Krajina nie je povolená.', + 'Country' => 'Krajina', + 'Coupon Code' => 'Kupónový Kód', + 'Coupon can not apply discount to this order due to address mismatch.' => 'Kupón nie je možné uplatniť na túto objednávku z dôvodu nesúladu adries.', + 'Coupon can not apply discount to this order due to customer mismatch.' => 'Kupón nie je možné uplatniť na túto objednávku z dôvodu nesúladu zákazníka.', + 'Coupon can not apply discount to this order.' => 'Kupón nie je možné uplatniť na túto objednávku.', + 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Kód kupónu „{code}“ sa už používa pri zľave „{name}“.', + 'Coupon codes cannot be blank.' => 'Kódy kupónov nemôžu byť prázdne.', + 'Coupon codes must be unique.' => 'Kódy kupónov musia byť jedinečné.', + 'Coupon format is required and must contain at least one `#`.' => 'Formát kupónu je povinný a musí obsahovať aspoň jeden znak „#“.', + 'Coupon not valid.' => 'Kupón je neplatný.', + 'Coupon removed: {explanation}' => 'Kupón odstránený: {explanation}', + 'Coupons' => 'Kupóny', + 'Craft Commerce - Administration' => 'Craft Commerce – Správa', + 'Craft Commerce - Inventory' => 'Craft Commerce – Zásoby', + 'Craft Commerce - Orders' => 'Craft Commerce – Objednávky', + 'Craft Commerce - Product Type - {name}' => 'Craft Commerce – Typ výrobku – {name}', + 'Craft Commerce - Subscriptions' => 'Craft Commerce – Predplatné', + 'Create a Discount' => 'Vytvoriť zľavu', + 'Create a Subscription Plan' => 'Vytvorte plán prihlásení na odber', + 'Create a new PDF' => 'Vytvoriť nový súbor PDF', + 'Create a new catalog pricing rule' => 'Vytvoriť nové pravidlo stanovovania cien podľa katalógu', + 'Create a new currency' => 'Vytvoriť novú menu', + 'Create a new email' => 'Vytvoriť nový e-mail', + 'Create a new gateway' => 'Vytvoriť novú bránu', + 'Create a new line item status' => 'Vytvoriť nový stav riadkovej položky', + 'Create a new order status' => 'Vytvoriť nový stav objednávky', + 'Create a new product type' => 'Vytvoriť nový typ produktu', + 'Create a new sale' => 'Vytvoriť nový výpredaj', + 'Create a new shipping category' => 'Vytvoriť novú kategóriu dopravy', + 'Create a new shipping method' => 'Vytvoriť nový spôsob dodania', + 'Create a new shipping rule' => 'Vytvoriť nové pravidlo dodania', + 'Create a new tax category' => 'Vytvoriť novú daňovú kategóriu', + 'Create a new tax rate' => 'Vytvoriť novú daňovú sadzbu', + 'Create a product type' => 'Vytvoriť typ produktu', + 'Create a shipping zone' => 'Vytvoriť zónu doručenia', + 'Create a tax zone' => 'Vytvoriť daňovú zónu', + 'Create catalog pricing rules' => 'Vytvoriť pravidlá stanovovania cien podľa katalógu', + 'Create customer: “{email}”' => 'Vytvoriť zákazníka: „{email}“', + 'Create discounts' => 'Vytvoriť zľavy', + 'Create discount…' => 'Vytvoriť zľavu…', + 'Create rules that allow this discount to match the order.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto zľava zodpovedala objednávke.', + 'Create rules that allow this discount to match the order’s billing address.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto zľava zodpovedala fakturačnej adrese objednávky.', + 'Create rules that allow this discount to match the order’s customer.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto zľava zodpovedala zákazníkovi objednávky.', + 'Create rules that allow this discount to match the order’s shipping address.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto zľava zodpovedala dodacej adrese objednávky.', + 'Create rules that allow this gateway to match the billing address.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto brána zodpovedala fakturačnej adrese.', + 'Create rules that allow this gateway to match the order.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto platobná brána zodpovedala objednávke.', + 'Create rules that allow this gateway to match the shipping address.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto brána zodpovedala dodacej adrese.', + 'Create sales' => 'Vytvoriť výpredaj', + 'Create sale…' => 'Vytvoriť výpredaj…', + 'Created' => 'Vytvorené', + 'Credit Card Payment Type' => 'Typ Platby Kreditnou Kartou', + 'Currency Code' => 'Kód meny', + 'Currency saved.' => 'Mena sa uložila.', + 'Currency' => 'Mena', + 'Current' => 'Súčasný', + 'Custom 1' => 'Vlastný 1', + 'Custom 2' => 'Vlastný 2', + 'Custom 3' => 'Vlastný 3', + 'Custom 4' => 'Vlastný 4', + 'Custom' => 'Vlastný', + 'Customer Enabled?' => 'Povolené pre zákazníkov?', + 'Customer ID is required.' => 'Vyžaduje sa ID zákazníka.', + 'Customer Note' => 'Poznámka zákazníka', + 'Customer Notices' => 'Zákaznícke upozornenia', + 'Customer data' => 'Údaje o zákazníkoch', + 'Customer' => 'Zákazník', + 'Damaged' => 'Poškodené', + 'Data shown might be outdated.' => 'Uvedené údaje môžu byť zastarané.', + 'Date Authorized' => 'Dátum schválenia', + 'Date Created' => 'Dátum Vytvorenia', + 'Date First Paid' => 'Dátum prvej platby', + 'Date Ordered' => 'Dátum Objednania', + 'Date Paid' => 'Dátum Úhrady', + 'Date Updated' => 'Dátum Aktualizácie', + 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Dátum, od ktorého bude pravidlo stanovovania cien podľa katalógu aktívne. Prázdne pre neobmedzený dátum začiatku', + 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Dátum, od ktorého bude zľava aktívna. Prázdne pre neobmedzený dátum začiatku', + 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Dátum, od ktorého bude výpredaj aktívny. Prázdne pre neobmedzený dátum začiatku', + 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Dátum, od ktorého bude pravidlo stanovovania cien podľa katalógu ukončené. Prázdne pre neobmedzený dátum začiatku', + 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Dátum, kedy bude zľava ukončená. Prázdne pre neobmedzený dátum ukončenia', + 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Dátum, kedy bude výpredaj ukončený. Prázdne pre neobmedzený dátum ukončenia', + 'Date' => 'Dátum', + 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Predvolené – Povoliť zápornú cenu v prípade, že zľavy sú vyššie ako hodnota objednávky.', + 'Default Category' => 'Predvolená kategória', + 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Predvolené zobrazenie ovládacieho panela Commerce. Ak používateľ nemá oprávnenie, vráti sa na miesto, ku ktorému má prístup.', + 'Default Order PDF' => 'Predvolená objednávka PDF', + 'Default Per Item Rate' => 'Predvolená sadzba na položku', + 'Default Percentage Rate' => 'Predvolená percentuálna sadzba', + 'Default Status?' => 'Predvolený Stav?', + 'Default View' => 'Predvolené zobrazenie', + 'Default Weight Rate' => 'Predvolená váhová sadzba', + 'Default Zone' => 'Predvolená Zóna', + 'Default status?' => 'Predvolený stav?', + 'Default to this tax zone when no billing address is set' => 'Nastaviť predvolenú hodnotu na túto daňovú oblasť, ak nie je nastavená fakturačná adresa', + 'Default to this tax zone when no shipping address is set' => 'Predvoliť túto daňovú zónu, ak nie je nastavená žiadna dodacia adresa', + 'Default variant updated.' => 'Predvolený variant aktualizovaný.', + 'Default' => 'Predvolené', + 'Default?' => 'Predvolené?', + 'Delete catalog pricing rules' => 'Odstrániť pravidlá stanovovania cien podľa katalógu', + 'Delete discounts' => 'Súvisiace zľavy', + 'Delete orders' => 'Zmazať objednávky', + 'Delete sales' => 'Odstrániť výpredaj', + 'Delete' => 'Zmazať', + 'Deleting the {location} location.' => 'Odstránenie polohy {location}.', + 'Describe this rule.' => 'Popíš toto pravidlo.', + 'Describe this shipping zone.' => 'Popíšte túto zónu doručenia.', + 'Describe this tax zone.' => 'Popis tejto daňovej zóny.', + 'Description' => 'Popis', + 'Destination Inventory Location' => 'Inventarizácia miesta určenia', + 'Destination' => 'Cieľ', + 'Details' => 'Podrobnosti', + 'Dimension Unit' => 'Jednotka Rozmeru', + 'Dimensions' => 'Rozmery', + 'Disabled' => 'Deaktivované', + 'Disallow' => 'Zakázať', + 'Discount all line items' => 'Zľaviť všetky riadkové položky', + 'Discount description.' => 'Popis zľavy.', + 'Discount is not allowed for the order' => 'Na túto objednávku nie je možné uplatniť zľavu', + 'Discount is out of date.' => 'Zľava je premlčaná.', + 'Discount saved.' => 'Zľava uložená.', + 'Discount the matching items only' => 'Zľaviť iba zhodujúce sa položky', + 'Discount use has reached its limit.' => 'Použitie zliav dosiahlo limit.', + 'Discount' => 'Zľava', + 'Discounted Item Subtotal' => 'Medzisúčet zľavnenej položky', + 'Discounted Items' => 'Zľavnené položky', + 'Discounts deleted.' => 'Zľavy odstránené.', + 'Discounts reordered.' => 'Poradie zliav bolo zmenené.', + 'Discounts updated.' => 'Zľavy aktualizované.', + 'Discounts' => 'Zľavy', + 'Disqualify with valid business tax ID?' => 'Diskvalifikovať sa platným daňovým identifikačným číslom?', + 'Do not apply subsequent matching sales beyond applying this sale.' => 'Nepoužívať následné zodpovedajúce predajné akcie nad rámec použitia tejto predajnej akcie.', + 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Túto sadzbu neuplatňujte, ak má adresa objednávky niektoré z vybraných platných DIČ pre podnikateľov.', + 'Do not attach a PDF to this email' => 'K tomuto e-mailu neprikladajte súbor PDF', + 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'V prípade chýb nevolať prepočítanie objednávky (Číslo: {orderNumber}).', + 'Donation can not be zero.' => 'Hodnota daru nemôže byť nula.', + 'Donation needs to be an amount.' => 'Dar musí mať uvedenú hodnotu.', + 'Donation settings saved.' => 'Nastavenia darovania uložené.', + 'Donation' => 'Darovanie', + 'Donations' => 'Dary', + 'Done' => 'Hotovo', + 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Ak sa uplatňuje táto zľava, neuplatňovať žiadne ďalšie zľavy', + 'Download PDF' => 'Stiahnuť PDF', + 'Download PDF…' => 'Stiahnuť PDF…', + 'Download Type' => 'Stiahnuť typ', + 'Download' => 'Stiahnuť', + 'Draft' => 'Koncept', + 'Dummy gateway payment failed.' => 'Platba cez prázdnu bránu zlyhala.', + 'Duplicate options exist' => 'Existuje duplicitná možnosť', + 'Duration' => 'Trvanie', + 'EU VAT ID' => 'DIČ pre EÚ', + 'Edit address' => 'Upraviť adresu', + 'Edit adjustments' => 'Upraviť nastavenia', + 'Edit catalog pricing rules' => 'Upraviť pravidlá stanovovania cien podľa katalógu', + 'Edit discounts' => 'Upraviť zľavy', + 'Edit options' => 'Upraviť možnosti', + 'Edit orders' => 'Upraviť objednávky', + 'Edit sales' => 'Upraviť výpredaj', + 'Edit' => 'Upraviť', + 'Effect' => 'Efekt', + 'Either (Default) - The relationship field is on the purchasable or the category' => 'Oboje (predvolené) - Pole vzťahov je na položke na predaj alebo kategórii', + 'Either way' => 'Obojsmerne', + 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Chyba generovania PDF e-mailu pre e-mail „{email}“. Objednávka: „{order}“. Chyba šablóny PDF: „{message}“ {file}:{line}', + 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'V ceste „{templatePath}“ neexistuje e-mailová šablóna vo formáte PDF pre e-mail „{email}“. Objednávka: „{order}“.', + 'Email Subject' => 'Predmet Emailu', + 'Email error. No email address found for order. Order: “{order}”' => 'Chyba e-mailu. K objednávke nie je priradená žiadna e-mailová adresa. Objednávka:„{order}“', + 'Email is not enabled.' => 'E-mail nie je povolený.', + 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'V ceste „{templatePath}“ neexistuje e-mailová šablóna s obyčajným textom. Výsledkom je cesta „{templateParsedPath}“ pre e-mail „{email}“. Objednávka: „{order}“.', + 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny s obyčajným textom pre e-mail „{email}“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', + 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny s obyčajným textom pre e-mail „{email}“ v „Cesta šablóny“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', + 'Email required to make payments on a completed order.' => 'Na uskutočnenie platieb na základe vyplnenej objednávky sa požaduje e-mail.', + 'Email saved.' => 'E-mail uložený.', + 'Email sent' => 'E-mail odoslaný', + 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'V ceste „{templatePath}“ neexistuje e-mailová šablóna. Výsledkom je cesta „{templateParsedPath}“ pre e-mail „{email}“. Objednávka: „{order}“.', + 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny pre vlastný e-mail „{email}“ v „Adresát:“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', + 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny pre e-mail „{email}“ v „Skrytá kópia:“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', + 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny pre e-mail „{email}“ v „Kópia:“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', + 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny pre e-mail „{email}“ v „Odpoveď:“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', + 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny pre e-mail „{email}“ v „Predmet:“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', + 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny pre e-mail „{email}“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', + 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy cesty e-mailovej šablóny pre e-mail „{email}“ v „Cesta šablóny:“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', + 'Email unavailable.' => 'E-mail nie je k dispozícii.', + 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'Pre objednávku „{order}“ nebolo možné odoslať e-mail „{email}“. Chyba: {error} {file}:{line}', + 'Email “{email}” for order {order} was cancelled.' => 'E-mail „{email}“ k objednávke „{order}“ bol zrušený.', + 'Email' => 'E-mail', + 'Emails' => 'Emaily', + 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Povoľte, ak sa má táto sadzba započítať do ceny zdaniteľného predmetu namiesto pridania nákladov k objednávke.', + 'Enable structure for products of this type' => 'Povoľte štruktúru pre produkty tohto typu', + 'Enable this discount' => 'Povoliť túto zľavu', + 'Enable this rule' => 'Povoliť toto pravidlo', + 'Enable this sale' => 'Povoliť tento výpredaj', + 'Enable this shipping method on the front end' => 'Povoliť tento spôsob dodania na front-ende', + 'Enable this shipping rule' => 'Povoliť toto pravidlo dodania', + 'Enable this tax rate' => 'Povoliť túto daňovú sadzbu', + 'Enabled for customers to select during checkout?' => 'Umožniť zákazníkom vybrať počas overovania?', + 'Enabled for customers to select?' => 'Majú zákazníci povolený výber?', + 'Enabled' => 'Povolené', + 'Enabled?' => 'Povolené?', + 'End Date' => 'Dátum ukončenia', + 'Enter SKU' => 'Zadať SKU', + 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Zadajte názov tejto daňovej sadzby, ktorý sa bude používať v ovládacom paneli.', + 'Enter a percentage like {ex1} or {ex2}.' => 'Zadajte percentuálnu hodnotu, ako napr. {ex1} alebo {ex2}.', + 'Enter coupon code' => 'Zadajte kód kupónu', + 'Enter reference' => 'Zadajte referenciu', + 'Error refunding transaction: {transactionHash}' => 'Chyba pri refundácii transakcie: {transactionHash}', + 'Every new store must be assigned to at least one site.' => 'Každý nový obchod musí byť priradený aspoň k jednej lokalite.', + 'Everywhere' => 'Všade', + 'Example' => 'Príklad', + 'Exclude this discount for products that are already on promotion' => 'Nepoužiť túto zľavu na výrobky, ktoré sú už v akcii', + 'Expired Link' => 'Neplatný odkaz', + 'Expired' => 'Vypršaný', + 'Expiry Date' => 'Dátum vypršania platnosti', + 'Expiry date' => 'Dátum exspirácie', + 'Expiry' => 'Platnosť', + 'Failed to receive transfer: {error}' => 'Nepodarilo sa prijať prevod: {error}', + 'Failed to send email. Please try again.' => 'Odoslanie e-mailu sa nepodarilo. Skúste to znovu.', + 'Failed to start' => 'Nebolo možné spustiť', + 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Nepodarilo sa aktualizovať {num, plural, one {} few {stavy objednávky} many {stavov objednávky}=1{stav objednávky} other{stavov objednávky}}.', + 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Nepodarilo sa aktualizovať stav pre {num, plural, one {} few {objednávky} many {objednávok}=1{objednávku} other{objednávok}}.', + 'Feet (ft)' => 'Stopy (ft)', + 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Filtrovanie podmienok, ktoré popisujú, na ktoré objednávky sa toto pravidlo vzťahuje. Nula pre preskočenie podmienky.', + 'First Name' => 'Krstné Meno', + 'Flat Amount Off Order' => 'Pevná cena z objednávky', + 'Flat Order Discount Amount Off' => 'Fixná čiastka zľavy objednávky', + 'Free Order Payment Strategy' => 'Platobná stratégia pre objednávku zdarma', + 'Free Shipping' => 'Doprava Zdarma', + 'Free orders are processed by the payment gateway' => 'Objednávky zdarma sú spracované platobnou bránou', + 'Free orders complete immediately' => 'Objednávky zdarma sa dokončia ihneď', + 'Free shipping can only be for whole order or matching items, not both.' => 'Doprava zdarma je dostupná len pre celú objednávku alebo zhodné položky, avšak nie pre oboje.', + 'From Name' => 'Meno Od', + 'Fulfill' => 'Splniť', + 'Fulfilled' => 'Splnené', + 'Fulfillment' => 'Splnenie', + 'Full Name' => 'Celé meno', + 'Gateway Code' => 'Kód brány', + 'Gateway Message' => 'Správa z brány', + 'Gateway Reference' => 'Referencia brány', + 'Gateway Response' => 'Odpoveď brány', + 'Gateway doesn’t support authorize' => 'Brána nepodporuje autorizáciu', + 'Gateway doesn’t support partial refunds.' => 'Brána nepodporuje čiastočné refundácie.', + 'Gateway doesn’t support purchase' => 'Brána nepodporuje nákup', + 'Gateway doesn’t support refunds.' => 'Brána nepodporuje refundácie.', + 'Gateway saved.' => 'Brána bola uložená.', + 'Gateway' => 'Brána', + 'Gateways reordered.' => 'Poradie brán bolo zmenené.', + 'Gateways' => 'Brány', + 'General Settings' => 'Všeobecné Nastavenia', + 'General' => 'Všeobecné', + 'Generate' => 'Generovať', + 'Generated Coupon Format' => 'Formát generovaného kupónu', + 'Grams (g)' => 'Gramy (g)', + 'Groups for which this sale will be applicable to.' => 'Skupiny, na ktoré sa tento výpredaj vzťahuje.', + 'HTML Email Template Path' => 'Cesta HTML Emailovej Šablóny', + 'Handle' => 'Identifikátor', + 'Harmonized System Code' => 'Kód harmonizovaného systému', + 'Has Admin Notices' => 'Obsahuje oznámenia správcu', + 'Has Emails?' => 'Má emaily?', + 'Has Free Shipping' => 'Má dopravu zadarmo', + 'Has Orders' => 'Má objednávky', + 'Has Purchasable' => 'Má položky na predaj', + 'Has Variants?' => 'Má Varianty?', + 'Height ({unit})' => 'Výška ({unit})', + 'Height' => 'Výška', + 'Hide snapshot' => 'Skryť snímku', + 'History' => 'História', + 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Ako dlho (v sekundách) má odkaz na stiahnutie PDF zostať platný pred vypršaním platnosti. Predvolená hodnota je 86400 (24 hodín).', + 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Koľkokrát môže byť táto zľava využitá z jednej emailovej adresy. Toto sa týka všetkých predošlých objednávok vykonaných návštevníkmi aj užívateľmi. Pre neobmedzené použitie návštevníkmi alebo užívateľmi nastavte na nula.', + 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Koľkokrát môže jeden používateľ využiť túto zľavu. Ak je táto hodnota nastavená na inú hodnotu ako nula, zľava bude k dispozícii len prihláseným používateľom.', + 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Koľkokrát celkovo môžu túto zľavu využiť hostia alebo prihlásení používatelia. Nastavte nulu pre neobmedzené použitie.', + 'How products should be labeled within the control panel.' => 'Ako by mali byť produkty označené v ovládacom paneli.', + 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'Aký vzťah majú medzi sebou položky na zakúpenie a kategórie. Toto určuje položky zhody. Viď [Terminológia vzťahov]({link}).', + 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'Ako bude výrobok opísaný v riadkovej položke objednávky. Môžete pridať štítky, ktorých výstupom sú vlastnosti, ako napríklad {ex1} alebo {ex2}', + 'How this shipping method will be referred to in templates and forms.' => 'Ako sa bude na tento spôsob dodania odkazovať v šablónach a formulároch.', + 'How variants should be labeled within the control panel.' => 'Ako by mali byť varianty označené v ovládacom paneli.', + 'How you’ll refer to this PDF in the templates.' => 'Názov, akým budete tento PDF súbor nazývať v šablónach.', + 'How you’ll refer to this product type in the templates.' => 'Ako sa bude na tento typ produktu odkazovať v šablónach.', + 'How you’ll refer to this shipping category in the templates.' => 'Ako sa bude táto kategória dopravy označovať v šablónach.', + 'How you’ll refer to this status in the templates.' => 'Ako sa bude na tento stav odkazovať v šablónach.', + 'How you’ll refer to this subscription plan in the templates.' => 'Spôsob, akým sa v šablónach odkazuje na tento plán prihlásení na odber.', + 'How you’ll refer to this tax category in the templates.' => 'Ako sa bude na túto daňovú kategóriu odkazovať v šablónach.', + 'ID' => 'ID', + 'IP Address' => 'Adresa IP', + 'If disabled, this PDF will not be available or sent with emails.' => 'Ak bude zakázaný, nebude tento PDF súbor k dispozícii a nebude možné ho posielať ako prílohu v e-mailoch.', + 'If disabled, this email will not send.' => 'Ak je táto položka zakázaná, tento e-mail sa neodošle.', + 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Ak je táto možnosť povolená a táto sadzba nesúhlasí s objednávkou, bude táto zahrnutá suma sadzby odstránená z ceny predmetu v košíku.', + 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Ak je nastavené na Len Autorizovať, bude treba platby ručne zachytiť než budú finančné prostriedky prevedené na účet. Brána musí vybranú voľbu podporovať.', + 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Ak vyberiete percentuálnu sadzbu „zo zľavnenej ceny položky“, bude obsahovať ako „zľavnenú čiastku na položku“, tak aj akékoľvek iné zľavy, ktoré boli použité pred touto.', + 'Ignore Promotions?' => 'Ignorovať akcie?', + 'Ignore previous matching sales if this sale matches.' => 'Ignorovať predchádzajúce zodpovedajúce predajné akcie, ak táto predajná akcia súhlasí.', + 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignorovať akčné ceny keď je táto zľava použitá na zhodujúce sa riadkové položky', + 'Inactive Carts' => 'Neaktívne Košíky', + 'Inches (in)' => 'Palce (in)', + 'Include built-in line item tax.' => 'Zahrnúť zabudovanú daňovú položku.', + 'Include in price?' => 'Zahrnúť do ceny?', + 'Include line item discounts.' => 'Zahrnúť zľavy z položiek.', + 'Include line item shipping costs.' => 'Zahrnúť náklady na dopravu v jednotlivých položkách.', + 'Include separate line item tax.' => 'Zahrnúť samostatnú daňovú položku.', + 'Included in price?' => 'Zahrnuté v cene?', + 'Included' => 'Vrátane', + 'Incoming transfer from Transfer ID: ' => 'Prichádzajúci prevod z Transfer ID: ', + 'Incoming' => 'Prichádzajúce', + 'Info' => 'Informácie', + 'Information linked?' => 'Je informácia prepojená?', + 'Information' => 'Informácia', + 'Invalid JSON' => 'Neplatné JSON', + 'Invalid Order ID' => 'Neplatné ID objednávky', + 'Invalid VAT ID.' => 'Neplatné IČ DPH.', + 'Invalid condition syntax' => 'Neplatná podmienka syntaxe', + 'Invalid email.' => 'Neplatná e-mailová adresa.', + 'Invalid formula syntax' => 'Neplatná syntax vzorca', + 'Invalid gateway: {value}' => 'Neplatná brána: {value}', + 'Invalid inventory movements.' => 'Neplatné pohyby zásob.', + 'Invalid order condition syntax.' => 'Neplatná syntax podmienky objednávky.', + 'Invalid payment or order. Please review.' => 'Neplatná platba alebo objednávka. Skontrolujte, prosím.', + 'Invalid payment source ID: {value}' => 'Neplatné ID zdroja platby: {value}', + 'Invalid store.' => 'Neplatný obchod.', + 'Invalid user.' => 'Neplatný používateľ.', + 'Inventory Item' => 'Položka inventára', + 'Inventory Location' => 'Umiestnenie zásob', + 'Inventory Locations' => 'Umiestnenia zásob', + 'Inventory Tracked' => 'Sledovanie zásob', + 'Inventory Transfers' => 'Prevody zásob', + 'Inventory could not be set.' => 'Zásoby sa nepodarilo nastaviť.', + 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'Inventárne miesto má viazané zásoby, objednávka musí byť najprv splnená.', + 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Inventúrne miesto má zásoby na prijatie, musí sa najprv dokončiť prevod.', + 'Inventory location is already deactivated.' => 'Umiestnenie zásob je už deaktivované.', + 'Inventory location saved.' => 'Uložené umiestnenie zásob.', + 'Inventory locations not saved.' => 'Umiestnenie zásob nebolo uložené.', + 'Inventory movement could not be saved.' => 'Pohyb zásob nebolo možné uložiť.', + 'Inventory movement saved.' => 'Uložený pohyb zásob.', + 'Inventory updated.' => 'Inventár aktualizovaný.', + 'Inventory was not updated.' => 'Inventár nebol aktualizovaný.', + 'Inventory' => 'Inventár', + 'Invoice amount' => 'Suma faktúry', + 'Invoice date' => 'Dátum faktúry', + 'Is Promotable' => 'Je možné propagovať', + 'Is Promotional Price?' => 'Je propagačná cena?', + 'Is Shippable' => 'Je možné odoslať', + 'Is Taxable' => 'Je zdaniteľné', + 'Item Rates' => 'Sadzby za položky', + 'Item Subtotal' => 'Medzisúčet položky', + 'Item Total' => 'Položka celkom', + 'Item' => 'Položka', + 'Items' => 'Položky', + 'Kilograms (kg)' => 'Kilogramy (kg)', + 'Label' => 'Štítok', + 'Landscape' => 'Krajina', + 'Language' => 'Jazyk', + 'Last Name' => 'Priezvisko', + 'Last Updated' => 'Naposledy aktualizované', + 'Leave a category rate override blank to use the rate from above.' => 'Ak chcete použiť sadzbu z vyššie uvedenej kategórie, ponechajte prázdne miesto.', + 'Leave blank for unlimited uses.' => 'Pre neobmedzené použitie nechajte prázdne.', + 'Leave blank if products don’t have URLs' => 'Ak produkty nemajú adresy URL, ponechajte prázdne', + 'Leave gateway subscription as-is' => 'Pnoechať predplatné brány bez zmien', + 'Length ({unit})' => 'Dĺžka ({unit})', + 'Length' => 'Dĺžka', + 'Let each product choose which sites it should be saved to' => 'Nechať každý produkt zvoliť, na ktoré weby sa má uložiť', + 'Limit which orders this discount applies to based on its line items.' => 'Obmedziť, na ktoré objednávky sa táto zľava vzťahuje na základe ich položiek.', + 'Limit which purchasables this sale applies to.' => 'Obmedzte, na ktorý nákupný tovar sa tento predaj vzťahuje.', + 'Limit' => 'Limit', + 'Line Item Statuses' => 'Stavy riadkových položiek', + 'Line Item' => 'Riadková položka', + 'Line Items' => 'Riadkové položky', + 'Line item price (minus discounts)' => 'Cena položky (znížená o zľavy)', + 'Line item shipping cost' => 'Náklady na dodanie riadkovej položky', + 'Line item statuses reordered.' => 'Stavy riadkových položiek boli zmenené.', + 'Link Duration' => 'Trvanie odkazu', + 'Link Sent' => 'Odkaz bol odoslaný', + 'Link to a product' => 'Odkaz na produkt', + 'Link to a variant' => 'Odkaz na variant', + 'Link' => 'Odkaz', + 'Live' => 'Publikované', + 'Location' => 'Poloha', + 'Locations that should be available for previewing products in this product type.' => 'Umiestnenia, ktoré majú byť dostupné pre náhľad produktov v tomto type produktu.', + 'MM' => 'MM', + 'Make a payment' => 'Vykonať platbu', + 'Make this the primary store' => 'Nastaviť ako hlavný obchod', + 'Manage Inventory' => 'Správa zásob', + 'Manage donation settings' => 'Spravovať nastavenia darovania', + 'Manage general store settings' => 'Spravovať všeobecné nastavenia obchodu', + 'Manage inventory locations' => 'Správa skladových miest', + 'Manage inventory stock levels' => 'Správa úrovne skladových zásob', + 'Manage inventory transfers' => 'Spravovať presuny zásob', + 'Manage orders' => 'Správa objednávok', + 'Manage payment currencies' => 'Spravovať platobné meny', + 'Manage promotions' => 'Spravovať akcie', + 'Manage shipping' => 'Správa prepravy', + 'Manage store settings' => 'Spravovať nastavenia obchodu', + 'Manage subscription plans' => 'Spravovať plány predplatného', + 'Manage subscription' => 'Správa prihlásení na odber', + 'Manage subscriptions' => 'Správa prihlásení na odber', + 'Manage taxes' => 'Správa daní', + 'Manage' => 'Spravovať', + 'Mark as Pending' => 'Označiť ako čakajúce', + 'Mark as completed' => 'Označiť ako dokončené', + 'Match Billing Address' => 'Zhoda fakturačnej adresy', + 'Match Customer' => 'Zhoda zákazníka', + 'Match Order' => 'Zhoda objednávky', + 'Match Orders' => 'Zhoda objednávok', + 'Match Product' => 'Zodpovedajúci produkt', + 'Match Purchasable' => 'Zhoda položiek na predaj', + 'Match Shipping Address' => 'Zhoda dodacej adresy', + 'Match Variant' => 'Zodpovedajúci variant', + 'Matching Items' => 'Položky so zhodou', + 'Max Qty' => 'Maximálne množstvo', + 'Max Uses' => 'Maximálny počet použití', + 'Max Variants' => 'Maximálne varianty', + 'Max quantity must greater than min.' => 'Max. množstvo musí byť väčšie ako min.', + 'Maximum Purchase Quantity' => 'Maximálny počet položiek v nákupe', + 'Maximum Total Shipping Cost' => 'Maximálna celková cena dopravy', + 'Maximum allowed quantity' => 'Maximálne povolené množstvo', + 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Maximálny počet zhodných položiek, ktoré treba objednať, aby zľava platila. Pri zadanej nulovej hodnote sa podmienka preskakuje.', + 'Maximum order quantity for this item is {num}.' => 'Maximálne množstvo objednávky tejto položky je {num}.', + 'Message' => 'Správa', + 'Meters (m)' => 'Metre (m)', + 'Millimeters (mm)' => 'Milimetre (mm)', + 'Min Qty' => 'Minimálne množstvo', + 'Min quantity must be less than max.' => 'Min. množstvo musí byť menšie ako max.', + 'Minimum Purchase Quantity' => 'Minimálny počet položiek v nákupe', + 'Minimum Total Price Strategy' => 'Stratégia pre minimálnu celkovú cenu', + 'Minimum Total Shipping Cost' => 'Minimálna celková cena dopravy', + 'Minimum allowed quantity' => 'Minimálne povolené množstvo', + 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Minimálny počet zhodných položiek, ktoré treba objednať, aby zľava nadobudla platnosť.', + 'Minimum order quantity for this item is {num}.' => 'Minimálne množstvo objednávky tejto položky je {num}.', + 'Missing Gateway' => 'Chýba brána', + 'Missing a default inventory location.' => 'Chýba predvolené umiestnenie inventára.', + 'Move Inventory' => 'Presun inventára', + 'Move To' => 'Presunúť do', + 'Move {qty} from {fromType} to {toType}' => 'Presunúť {qty} z {fromType} do {toType}', + 'Move' => 'Presunúť', + 'Movement from deactivated inventory location' => 'Presun z deaktivovaného inventárneho miesta', + 'Movement' => 'Presun', + 'Must have at least one variant.' => 'Musí mať aspoň jeden variant.', + 'Name Field' => 'Pole s názvom', + 'Name' => 'Meno', + 'New Customer' => 'Nový zákazník', + 'New Customers' => 'Noví zákazníci', + 'New Order' => 'Nová objednávka', + 'New PDF' => 'Nové PDF', + 'New address' => 'Nová adresa', + 'New catalog pricing rule' => 'Nové pravidlo stanovovania cien podľa katalógu', + 'New currency' => 'Nová mena', + 'New discount' => 'Nová zľava', + 'New email' => 'Nový email', + 'New gateway' => 'Nová brána', + 'New line item status' => 'Nový stav riadkovej položky', + 'New line items get this status by default when the order is completed' => 'Keď sa objednávka dokončí, nové riadkové položky budú mať tento stav predvolený', + 'New location' => 'Nové umiestnenie', + 'New order status' => 'Nový stav objednávky', + 'New orders get this status by default' => 'Nové objednávky budú mať tento stav predvolený', + 'New product type' => 'Nový typ produktu', + 'New product' => 'Nový produkt', + 'New product, choose a type' => 'Nový produkt, vyberte typ', + 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Pre nové produkty bude predvolená prvá dostupná daňová kategória. Ak nebude žiadna dostupná, použije sa táto kategória.', + 'New sale' => 'Nový výpredaj', + 'New shipping category' => 'Nová kategória dopravy', + 'New shipping method' => 'Nový spôsob dodania', + 'New shipping rule' => 'Nové pravidlo dodania', + 'New shipping zone' => 'Nová zóna doručenia', + 'New subscription plan' => 'Nový plán prihlásení na odber', + 'New tax category' => 'Nová daňová kategória', + 'New tax rate' => 'Nová daňová sadzba', + 'New tax zone' => 'Nová daňová zóna', + 'New transfer' => 'Nový prevod', + 'New {productType} product' => 'Nový produkt {productType}', + 'New' => 'Nové', + 'Next payment' => 'Ďalšia platba', + 'No Address' => 'Žiadna adresa', + 'No PDFs exist yet.' => 'Neexistuje zatiaľ žiadne PDF.', + 'No access given to any specific store management features.' => 'Nemáte prístup k žiadnym špecifickým funkciám správy obchodu.', + 'No additional payment currencies exist yet.' => 'Zatiaľ neexistujú žiadne doplnkové platobné meny.', + 'No address' => 'Žiadna adresa', + 'No billing address' => 'Žiadna fakturačná adresa', + 'No catalog pricing rule exists with the ID “{id}”' => 'Žiadne pravidlo stanovovania cien podľa katalógu s ID „{id}“ neexistuje', + 'No catalog pricing rules exist yet.' => 'Zatiaľ neexistujú žiadne pravidlá stanovovania cien podľa katalógu.', + 'No currency exists with the ID “{id}”' => 'Mena s identifikátorom „{id}“ neexistuje', + 'No customer email address exists on this cart.' => 'Pre tento košík chýba e-mailová adresa zákazníka.', + 'No description' => 'Žiadny popis', + 'No discount exists with the ID “{id}”' => 'Žiadna zľava s ID „{id}“ neexistuje', + 'No discounts exist yet.' => 'Žiadne zľavy zatiaľ neexistujú.', + 'No donation amount supplied.' => 'Nebola zadaná žiadna hodnota pre darovanie.', + 'No emails exist yet.' => 'Žiadne emaily zatiaľ neexistujú.', + 'No inventory changes made.' => 'Neboli vykonané žiadne zmeny v inventári.', + 'No inventory found.' => 'Nenašiel sa žiadny inventár.', + 'No inventory movements made.' => 'Nevykonali sa žiadne inventúrne pohyby.', + 'No inventory transactions for this location.' => 'Na tomto mieste sa nevykonávajú žiadne inventúrne operácie.', + 'No new customer selected.' => 'Nebol vybraný žiadny nový zákazník.', + 'No order history exists with the ID “{id}”' => 'Žiadna história objednávky s ID „{id}“ neexistuje', + 'No order status history items will exist until the cart becomes an order.' => 'Kým sa obsah košíku nepremení na objednávku, nebudú v histórii stavov objednávky žiadne položky.', + 'No payment source exists with the ID “{id}”' => 'Neexistuje zdroj platby s ID „{id}“', + 'No private Note.' => 'Žiadna súkromná poznámka.', + 'No product available.' => 'Žiadny produkt k dispozícii.', + 'No product types exist yet.' => 'Žiadne typy produktov zatiaľ neexistujú.', + 'No purchasable available.' => 'Žiadne položky na predaj k dispozícii.', + 'No sale exists with the ID “{id}”' => 'Žiadny výpredaj s ID „{id}“ neexistuje', + 'No sales exist yet.' => 'Žiadne zľavy zatiaľ neexistujú.', + 'No shipping address' => 'Žiadna dodacia adresa', + 'No shipping category exists with the ID “{id}”' => 'Kategória dopravy s identifikátorom „{id}“ neexistuje', + 'No shipping method exists with the ID “{id}”' => 'Žiadny spôsob dodania s ID „{id}“ neexistuje', + 'No shipping rule exists with the ID “{id}”' => 'Žiadne pravidlo dodania s ID „{id}“ neexistuje', + 'No shipping rules exist yet.' => 'Žiadne pravidlá dodania zatiaľ neexistujú.', + 'No shipping zone exists with the ID “{id}”' => 'Neexistuje žiadna zóna doručenia s ID „{id}“', + 'No stats available.' => 'Nie sú k dispozícii žiadne štatistiky.', + 'No subscription plan exists with the ID “{id}”' => 'Neexistuje žiadny plán prihlásení na odber s ID „{id}“', + 'No subscription plans exist yet.' => 'Doposiaľ neexistujú žiadne plány prihlásení na odber.', + 'No tax category exists with the ID “{id}”' => 'Žiadna daňová kategória s ID „{id}“ neexistuje', + 'No tax rate exists with the ID “{id}”' => 'Žiadna daňová sadzba s ID „{id}“ neexistuje', + 'No tax zone exists with the ID “{id}”' => 'Žiadna daňová zóna s ID „{id}“ neexistuje', + 'No transactions exist.' => 'Neexistujú žiadne transakcie.', + 'No user authenticated.' => 'Žiadny overený používateľ.', + 'No' => 'Nie', + 'None on hand' => 'Žiadne nie sú k dispozícii', + 'None' => 'Žiadne', + 'Not a valid address type' => 'Typ adresy nie je platný', + 'Not a valid credit card number.' => 'Toto nie je platné číslo platobnej karty.', + 'Not all SKUs are unique.' => 'Nie všetky jednotky SKU sú jedinečné.', + 'Note' => 'Poznámka', + 'Notes' => 'Poznámky', + 'Number of Coupons' => 'Počet kupónov', + 'Number' => 'Číslo', + 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Na ktoré z vyššie uvedených povolených webov by sa mali ukladať produkty tohto typu produktu?', + 'On Hand' => 'Dostupné', + 'Only allow this gateway to be used for zero value orders?' => 'Povoliť použitie tejto brány pre objednávky s nulovou hodnotou?', + 'Only match certain purchasables…' => 'Zhodujú sa len niektoré položky na predaj…', + 'Only match purchasables related to…' => 'Zhodujte sa len s položkami na predaj súvisiacimi s…', + 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Zahrnuté budú len objednávky s nasledujúcimi stavmi objednávok. Ak chcete zahrnúť všetky stavy, nechajte prázdne.', + 'Only save product to the site they were created in' => 'Produkty ukladať len do webov, v ktorých boli vytvorené', + 'Options' => 'Možnosti', + 'Order Condition Formula' => 'Vzorec podmienky objednávky', + 'Order Description Format' => 'Formát opisu objednávky', + 'Order Details' => 'Informácie o objednávke', + 'Order Fields' => 'Polia Objednávky', + 'Order PDF Download Link' => 'Odkaz na stiahnutie PDF objednávky', + 'Order PDF Filename Format' => 'Formát názvu PDF súboru k objednávke', + 'Order Reference Number Format' => 'Formát referenčného čísla objednávky', + 'Order Settings' => 'Nastavenia Objednávky', + 'Order Site' => 'Web objednávok', + 'Order Status description.' => 'Popis stavu výpredaja.', + 'Order Status' => 'Stav Objednávky', + 'Order Statuses' => 'Stavy Objednávok', + 'Order can not be empty.' => 'Objednávka nemôže byť prázdna.', + 'Order count' => 'Počet objednávok', + 'Order customer data removed.' => 'Nariadiť odstránenie údajov o zákazníkoch.', + 'Order deleted.' => 'Objednávka zmazaná.', + 'Order fields saved.' => 'Polia objednávky uložené.', + 'Order not found.' => 'Objednávka sa nenašla.', + 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Čiastka platby za objednávku je {outstandingBalanceAsCurrency}. Toto je maximálna hodnota, ktorá bude účtovaná.', + 'Order recalculated.' => 'Objednávka prepočítaná.', + 'Order status saved.' => 'Stav objednávky uložený.', + 'Order statuses reordered.' => 'Poradie stavu objednávok bolo zmenené.', + 'Order total shipping cost' => 'Celkové náklady na dodanie objednávky', + 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Celková zdaniteľná cena objednávky (medzisúčet riadkovej položky + suma zliav + celkové náklady na dodanie)', + 'Order' => 'Objednávka', + 'Orders (Legacy)' => 'Objednávky (staršia verzia)', + 'Orders deleted.' => 'Objednávky zmazané.', + 'Orders not restored.' => 'Objednávky neboli obnovené.', + 'Orders restored.' => 'Objednávky obnovené.', + 'Orders' => 'Objednávky', + 'Organization Name' => 'Názov organizácie', + 'Organization Tax ID' => 'Daňové identifikačné číslo organizácie', + 'Origin and destination cannot be the same.' => 'Pôvodné a cieľové miesto nemôžu byť rovnaké.', + 'Origin' => 'Pôvod', + 'Original Price' => 'Pôvodná cena', + 'Original price' => 'Pôvodná cena', + 'Original promotional price' => 'Pôvodná propagačná cena', + 'Other Languages' => 'Ostatné jazyky', + 'Other countries' => 'Ostatné krajiny', + 'Outgoing transfer from Transfer ID: ' => 'Odchádzajúci prevod z Transfer ID: ', + 'Overpaid' => 'Preplatené', + 'Overrides previous?' => 'Nahradiť predchádzajúce?', + 'PDF Attachment' => 'Príloha PDF', + 'PDF Template Path' => 'Cesta k šablóne PDF', + 'PDF saved.' => 'Súbor PDF uložený.', + 'PDF' => 'PDF', + 'PDFs & Emails' => 'PDF súbory a e-maily', + 'PDFs' => 'Súbory PDF', + 'Paid Amount' => 'Zaplatená suma', + 'Paid Status' => 'Stav zaplatenia', + 'Paid' => 'Zaplatené', + 'Paper Orientation' => 'Orientácia papiera', + 'Paper Size' => 'Veľkosť papiera', + 'Partial payment not allowed.' => 'Čiastočné platby nie sú povolené.', + 'Partial' => 'Čiastočný', + 'Past year' => 'Minulý rok', + 'Past {num} days' => 'Posledných {num} dní', + 'Pay {amount} of {currency} on the order.' => 'Zaplaťte čiastku {amount} v {currency} za objednávku.', + 'Pay' => 'Zaplatiť', + 'Payment Amount' => 'Suma platby', + 'Payment Currencies' => 'Platobné meny', + 'Payment Gateway' => 'Platobná brána', + 'Payment Method' => 'Spôsob Platby', + 'Payment error: {message}' => 'Chyba platby: {message}', + 'Payment method issue' => 'Problém so spôsobom platby', + 'Payment source created.' => 'Zdroj platby bol vytvorený.', + 'Payment source deleted.' => 'Zdroj platby bol odstránený.', + 'Payments' => 'Platby', + 'Pending' => 'Nevyriešené', + 'Per Email Address Discount Limit' => 'Obmedzenie počtu zliav na e-mailovú adresu', + 'Per Item Amount Off' => 'Zľavnená čiastka na položku', + 'Per Item Discount' => 'Zľava na položku', + 'Per Item Percentage Off' => 'Percentuálna zľava na položku', + 'Per Item Rate' => 'Sadzba Na Položku', + 'Per User Discount Limit' => 'Obmedzenie počtu zliav na osobu', + 'Percentage Rate' => 'Percentuálna Sadzba', + 'Phone (Alt)' => 'Telefón (alt.)', + 'Phone' => 'Telefón', + 'Pick a plan' => 'Zvoľte plán', + 'Plain Text Email Template Path' => 'Cesta k šablóne pre e-mail s obyčajným textom', + 'Plan' => 'Plán', + 'Plans reordered.' => 'Poradie plánov bolo zmenené.', + 'Portrait' => 'Portrét', + 'Post Date' => 'Dátum Príspevku', + 'Postal Code Formula' => 'Vzorec poštového smerovacieho čísla', + 'Pounds (lb)' => 'Libry (lb)', + 'Preview' => 'Náhľad', + 'Previous Status' => 'Predchádzajúci stav', + 'Price' => 'Cena', + 'Prices' => 'Ceny', + 'Pricing Rules' => 'Pravidlá stanovovania cien', + 'Pricing jobs are currently running.' => 'V súčasnosti prebiehajú cenové úlohy.', + 'Pricing' => 'Ceny', + 'Primary Billing Address' => 'Hlavná fakturačná adresa', + 'Primary Shipping Address' => 'Hlavná dodacia adresa', + 'Primary payment source updated.' => 'Primárny zdroj platby aktualizovaný.', + 'Primary' => 'Hlavný', + 'Private Note' => 'Súkromná poznámka', + 'Product Fields' => 'Polia produktu', + 'Product ID is required.' => 'Vyžaduje sa ID produktu.', + 'Product Template' => 'Šablóna Produktu', + 'Product Title Format' => 'Formát Názvu produktu', + 'Product Type' => 'Typ produktu', + 'Product Types' => 'Typy Produktov', + 'Product URI Format' => 'Formát URI produktu', + 'Product Variant' => 'Variant produktu', + 'Product Variants' => 'Varianty produktu', + 'Product type saved.' => 'Typ produktu uložený.', + 'Product type settings' => 'Nastavenia typu produktu', + 'Product' => 'Produkt', + 'Products and Variants deleted.' => 'Produkty a Varianty odstránené.', + 'Products not restored.' => 'Produkty neboli obnovené.', + 'Products restored.' => 'Produkty obnovené.', + 'Products' => 'Produkty', + 'Promotable' => 'Akciový', + 'Promotable?' => 'Akciový?', + 'Promotional Amount' => 'Propagačná čiastka', + 'Promotional Price' => 'Propagačná cena', + 'Purchasable Categories' => 'Zakúpiteľné kategórie', + 'Purchasable ID and Sale ID are required.' => 'Vyžaduje sa ID položky na predaj a ID zľavy.', + 'Purchasable ID is required.' => 'Vyžaduje sa ID položky na predaj.', + 'Purchasable Type' => 'Zakúpiteľný typ', + 'Purchasable' => 'Na predaj', + 'Purchase (Authorize and Capture Immediately)' => 'Nákup (Okamžitá Autorizácia a Zachytenie)', + 'Purchase Total' => 'Nákup Celkom', + 'Qty' => 'Množstvo', + 'Quality Control' => 'Kontrola kvality', + 'Quantity' => 'Množstvo', + 'Rate' => 'Sadzba', + 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Znovu prideliť {numOrders, plural, one {} few {objednávky} many {objednávok}=1{objednávku} other{objednávok}}', + 'Recalculate order' => 'Prepočítať objednávku', + 'Receive Inventory' => 'Prijatie zásob', + 'Receive Transfer' => 'Prijatie prevodu', + 'Receive' => 'Prijať', + 'Received' => 'Prijaté', + 'Recent Orders' => 'Posledné objednávky', + 'Recipient' => 'Príjemca', + 'Recover Cart' => 'Obnovenie košíka', + 'Reduce price' => 'Znížiť cenu', + 'Reduce the price by a fixed amount' => 'Znížiť cenu o pevnú sumu', + 'Reduce the price by a percentage of the original price' => 'Znížiť cenu o percentuálnu hodnotu pôvodnej ceny', + 'Reference' => 'Referencia', + 'Refresh payment history' => 'Obnoviť históriu platieb', + 'Refund note' => 'Poznámka k refundácii', + 'Refund payment' => 'Refundácia platby', + 'Refund' => 'Vrátenie peňazí', + 'Reject' => 'Zamietnuť', + 'Rejected' => 'Zamietnuté', + 'Relationship Type' => 'Typ vzťahu', + 'Removable included tax rates are only allowed for the default tax zone.' => 'Odnímateľné zahrnutie daňových sadzieb je povolené iba pre predvolenú daňovú oblasť.', + 'Remove address' => 'Odstrániť adresu', + 'Remove all shipping costs from the order' => 'Odstrániť všetky náklady na dopravu z objednávky', + 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Odstrániť prepojenie so zákazníkom a e-mailovú adresu z {numOrders, plural, one {} few {objednávok} many {objednávok}=1{objednávky} other{objednávok}}. Podľa potreby nižšie vyberte ďalšie údaje o zákazníkoch, ktoré chcete odstrániť', + 'Remove customer data' => 'Odstrániť údaje o zákazníkoch', + 'Remove from price?' => 'Odstrániť z ceny?', + 'Remove shipping costs for matching items only' => 'Odstrániť náklady za dopravu len pre položky, ktoré sa zhodujú', + 'Remove the included tax when a valid organization tax ID is present?' => 'Odstrániť zahrnutú daň, ak je k dispozícii platné DIČ organizácie?', + 'Remove' => 'Odstrániť', + 'Removed' => 'Odstránené', + 'Repeat Customers' => 'Opakovaní zákazníci', + 'Reply To' => 'Odpovedať', + 'Require Billing Address At Checkout' => 'Vyžadovať fakturačnú adresu pri pokladni', + 'Require Coupon Code' => 'Vyžiadať kód kupónu', + 'Require Shipping Address At Checkout' => 'Vyžadovať adresu prepravy pri pokladni', + 'Require Shipping Method Selection At Checkout' => 'Vyžadovať výber spôsobu dopravy pri pokladni', + 'Require' => 'Vyžadovať', + 'Reserved' => 'Rezervované', + 'Reset usage' => 'Vynulovať počítadlo použitia', + 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Obmedziť zľavu len na tie objednávky, kde zákazník nakúpil za minimálnu celkovú hodnotu zodpovedajúcich prvkov.', + 'Revenue Options' => 'Možnosti príjmov', + 'Revenue' => 'Výnos', + 'Rule' => 'Pravidlo', + 'Rules reordered.' => 'Poradie pravidiel upravené.', + 'SKU' => 'SKU', + 'Safety' => 'Bezpečnosť', + 'Sale Price' => 'Výpredajová cena', + 'Sale description.' => 'Popis výpredaja.', + 'Sale reordered.' => 'Poradie výpredaja bolo zmenené.', + 'Sale saved.' => 'Výpredaj uložený.', + 'Sale' => 'Výpredaj', + 'Sales deleted.' => 'Výpredaje odstránené.', + 'Sales updated.' => 'Zľavy aktualizované.', + 'Sales' => 'Výpredaje', + 'Save and continue editing' => 'Uložiť a pokračovať v úpravách', + 'Save and return to all orders' => 'Uložiť a vrátiť sa na všetky objednávky', + 'Save and set rules' => 'Uložiť a nastaviť pravidlá', + 'Save as a new rule' => 'Uložiť ako nové pravidlo', + 'Save product to all sites enabled for this product type' => 'Uložiť produkt na všetky stránky povolené pre tento typ produktu', + 'Save product to other sites in the same site group' => 'Uložiť produkt do iných webov v rovnakej skupine webov', + 'Save product to other sites with the same language' => 'Uložiť produkt do iných webov s rovnakým jazykom', + 'Save' => 'Uložiť', + 'Search customer…' => 'Vyhľadať zákazníka…', + 'Search inventory' => 'Vyhľadávanie v inventári', + 'Search or enter customer email…' => 'Vyhľadajte alebo zadajte e-mail zákazníka…', + 'Search…' => 'Hľadať…', + 'See Orders' => 'Zobraziť objednávky', + 'Select a gateway' => 'Vyberte bránu', + 'Select a tax category.' => 'Vybrať daňovú kategóriu.', + 'Select a tax zone. If empty, this rate will match anywhere.' => 'Vyberte daňovú zónu. Ak bude prázdna, použije sa táto hodnota všade.', + 'Select address' => 'Vybrať adresu', + 'Select an item' => 'Vyberte položku', + 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Vyberte spôsob aplikovania pravidla stanovovania cien podľa katalógu na položku(y) na zakúpenie.', + 'Select how the sale will be applied to the purchasable(s).' => 'Vyberte spôsob aplikovania predajnej akcie na položku(y) na zakúpenie.', + 'Select product type' => 'Vyberte typ produktu', + 'Select the emails that will be sent when transitioning to this status.' => 'Vybrať emaily, ktoré budú poslané pri prechode na tento stav.', + 'Select what this rate should be applied to.' => 'Vyberte, na čo sa má táto sadzba uplatňovať.', + 'Send Email' => 'Poslať e-mail', + 'Send to custom recipient' => 'Odoslať vlastnému príjemcovi', + 'Send to the customer' => 'Poslať zákazníkovi', + 'Set Quantity' => 'Nastaviť množstvo', + 'Set default category' => 'Nastaviť predvolenú kategóriu', + 'Set default variant' => 'Nastaviť predvolený variant', + 'Set or Adjust' => 'Nastaviť alebo upraviť', + 'Set price' => 'Nastaviť cenu', + 'Set status' => 'Nastaviť stav', + 'Set the price to a flat amount' => 'Nastaviť cenu na pevnú sumu', + 'Set the price to a percentage of the original price' => 'Nastaviť cenu na percentuálnu hodnotu pôvodnej ceny', + 'Set the sale price to a flat amount' => 'Nastaviť cenu výpredaja na pevnú sumu', + 'Set the sale price to a percentage of the original price' => 'Nastaviť cenu výpredaja na percentuálnu hodnotu pôvodnej ceny', + 'Set to' => 'Nastaviť na', + 'Settings saved.' => 'Nastavenia uložené.', + 'Settings' => 'Nastavenia', + 'Share cart…' => 'Zdieľať košík…', + 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Doprava – Minimálna cena za dopravu v prípade, že je hodnota objednávky nižšia ako cena za dopravu.', + 'Shipping Address Zone' => 'Zóna dodacej adresy', + 'Shipping Address' => 'Dodacia Adresa', + 'Shipping Business Name' => 'Expedičný obchodný názov', + 'Shipping Categories' => 'Kategórie dopravy', + 'Shipping Category Conditions' => 'Podmienky kategórie dopravy', + 'Shipping Category' => 'Kategória dopravy', + 'Shipping First Name' => 'Meno na spôsobe dopravy', + 'Shipping Full Name' => 'Celý expedičný názov', + 'Shipping Last Name' => 'Priezvisko na spôsobe dopravy', + 'Shipping Method' => 'Spôsob Dodania', + 'Shipping Methods' => 'Spôsoby Dodania', + 'Shipping Rule' => 'Pravidlo dopravy', + 'Shipping Zones' => 'Zóny doručenia', + 'Shipping address required.' => 'Požaduje sa dodacia adresa.', + 'Shipping categories deleted.' => 'Kategórie dopravy boli vymazané.', + 'Shipping category saved.' => 'Kategória dopravy sa uložila.', + 'Shipping category updated.' => 'Kategória dopravy bola aktualizovaná.', + 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Cena za dopravu pridaná k objednávke ako celok ešte predtým, ako sa použijú percentuálne sadzby, sadzby za položku a hmotnosť. Nastavte na nulu a táto sadzba sa nepoužije. Toto pravidlo, vrátane základnej sadzby, sa nepoužije v prípade, že košík obsahuje položky, ktoré nie je možné dopraviť, napríklad digitálne produkty.', + 'Shipping method saved.' => 'Spôsob dodania uložený.', + 'Shipping methods and rules deleted.' => 'Spôsoby a pravidlá dopravy boli odstránené.', + 'Shipping methods updated.' => 'Spôsob dopravy bol aktualizovaný.', + 'Shipping rule saved.' => 'Pravidlo dodania uložené.', + 'Shipping zone saved.' => 'Zóna doručenia uložená.', + 'Shipping' => 'Dodanie', + 'Short Number' => 'Krátke číslo', + 'Show Chart?' => 'Zobraziť graf?', + 'Show Order Count?' => 'Zobraziť počet objednávok?', + 'Show all prices' => 'Zobraziť všetky ceny', + 'Show archived gateways' => 'Zobraziť archivované brány', + 'Show order count line on chart.' => 'Zobraziť v grafe riadok s počtom objednávok.', + 'Show related sales' => 'Zobraziť súvisiaci výpredaj', + 'Show rule details' => 'Zobraziť podrobnosti pravidla', + 'Show the Dimensions and Weight fields for products of this type' => 'Zobraziť pole Rozmerov a Hmotnosti pre produkty tohto typu', + 'Show the Title field for products' => 'Zobraziť pole Názvu pre produkty', + 'Show the Title field for variants' => 'Zobraziť pole Názvu pre varianty', + 'Signed In' => 'Prihlásený', + 'Site Languages' => 'Jazyky webu', + 'Site store mapping saved.' => 'Mapovanie úložiska je uložené.', + 'Sites' => 'Lokality', + 'Slug' => 'Slug', + 'Snapshot' => 'Snímka', + 'Snapshots' => 'Snímky', + 'Some orders restored.' => 'Niektoré objednávky boli obnovené.', + 'Some products restored.' => 'Niektoré produkty boli obnovené.', + 'Some variants restored.' => 'Niektoré varianty boli obnovené.', + 'Something changed with the order before payment, please review your order and submit payment again.' => 'Pred platbou došlo k zmene v objednávke, skontrolujte, prosím, svoju objednávku a znova vykonajte platbu.', + 'Sorry, no matching options.' => 'Ľutujeme, nenašli sa žiadne zhody.', + 'Source - The purchasable relationship field is on the category' => 'Zdroj – pole nákupného vzťahu sa nachádza v kategórii', + 'Source' => 'Zdroj', + 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Nastavte podmienku Twig, ktorá určí, či sa má pre danú objednávku uplatniť zľava. (Objednávku je možné volať pomocou premennej `order`.)', + 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Nastavte podmienku Twig, ktorá určí, či sa má pre danú objednávku uplatniť pravidlo dopravy. (Objednávku je možné volať pomocou premennej `order`.)', + 'Start Date' => 'Dátum Začiatku', + 'State' => 'Štát', + 'Status Email Address' => 'Emailová Adresa pre Stav', + 'Status Emails' => 'Emaily pre Stavy', + 'Status History' => 'História stavov', + 'Status Updated.' => 'Stav bol aktualizovaný.', + 'Status change message' => 'Správa o zmene stavu', + 'Status' => 'Stav', + 'Stock' => 'Sklad', + 'Stops Processing?' => 'Zastaví spracovanie?', + 'Stops subsequent?' => 'Zastaví následné?', + 'Store Location' => 'Umiestnenie obchodu', + 'Store Management' => 'Manažment obchodu', + 'Store Markets' => 'Obchodné trhy', + 'Store Rule' => 'Pravidlo obchodu', + 'Store saved.' => 'Uložený obchod.', + 'Store' => 'Obchod', + 'Stores & Sites' => 'Obchody a lokality', + 'Stores' => 'Obchody', + 'Strategy to apply when an order is free or has a zero balance.' => 'Ktorú stratégiu použiť v prípade, že je objednávka zdarma alebo má nulový zostatok.', + 'Strategy to apply when calculating the minimum order price.' => 'Ktorú stratégiu použiť pri počítaní minimálnej ceny objednávky.', + 'Subject' => 'Predmet', + 'Subscribing user' => 'Používateľ prihlasujúci odber', + 'Subscription Fields' => 'Polia prihlásenia na odber', + 'Subscription Plans' => 'Plány prihlásení na odber', + 'Subscription Settings' => 'Nastavenie predplatného', + 'Subscription cancelled.' => 'Predplatné zrušené.', + 'Subscription date' => 'Dátum prihlásenia na odber', + 'Subscription fields saved.' => 'Polia prihlásenia na odber uložené.', + 'Subscription for {user} to {plan} prevented by a plugin.' => 'Doplnok zabránil prihláseniu na odber plánu {plan} pre používateľa {user}.', + 'Subscription plan saved.' => 'Plán prihlásení na odber bol uložený.', + 'Subscription plan' => 'Plán prihlásení na odber', + 'Subscription plans' => 'Plány prihlásení na odber', + 'Subscription reactivated.' => 'Predplatné opäť aktivované.', + 'Subscription reference' => 'Referenčné číslo prihlásenia na odber', + 'Subscription started.' => 'Predplatné spustené.', + 'Subscription switched.' => 'Predplatné zmenené.', + 'Subscription to “{plan}”' => 'Prihlásenie na odber plánu „{plan}“', + 'Subscription' => 'Prihlásenie na odber', + 'Subscriptions on hold' => 'Predplatné pozastavené', + 'Subscriptions' => 'Prihlásenia na odber', + 'Suppress emails' => 'Odstrániť e-maily', + 'Switch plan' => 'Prepnúť plán', + 'Switch' => 'Prepnúť', + 'System' => 'Systém', + 'Table Columns' => 'Stĺpce tabuľky', + 'Target - The category relationship field is on the purchasable' => 'Cieľ – Pole kategórie vzťahov je na položke na predaj', + 'Tax & Shipping' => 'Daň a doprava', + 'Tax (inc)' => 'Daň (vr.)', + 'Tax Categories' => 'Daňové Kategórie', + 'Tax Category' => 'Daňová Kategória', + 'Tax Rates' => 'Daňové Sadzby', + 'Tax Zone' => 'Daňová Zóna', + 'Tax Zones' => 'Daňové Zóny', + 'Tax categories deleted.' => 'Daňové kategórie boli vymazané.', + 'Tax category saved.' => 'Daňová kategória uložená.', + 'Tax category updated.' => 'Daňová kategória bola aktualizovaná.', + 'Tax rate saved.' => 'Daňová sadzba uložená.', + 'Tax rates updated.' => 'Daňové sadzby boli aktualizované.', + 'Tax zone saved.' => 'Daňová zóna uložená.', + 'Tax' => 'Daň', + 'Taxable Subject' => 'Zdaniteľný Subjekt', + 'Template Path' => 'Cesta Šablóny', + 'That handle is already in use' => 'Tento popisovač sa už používa', + 'That handle is already in use.' => 'Tento popisovač sa už používa.', + 'The PDF to attach to this email.' => 'PDF pre priloženie do e-mailu.', + 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'Adresa URL stránky na aktualizáciu fakturačných údajov predplatného, ako aj na spracovanie overovania 3DS.', + 'The address provided is outside the store’s market.' => 'Uvedená adresa sa nachádza mimo trhu obchodu.', + 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'Čiastka zľavy, ktorá je použitá na celú objednávku. Táto čiastka sa rozdelí medzi riadkové položky v poradí od najvyššej ceny po najnižšiu, až kým sa nevyužije celá zľava.', + 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'Základná zľava môže zľaviť položky v košíku maximálne na hodnotu nula, až kým nebude celá využitá. Nie je možné dostať objednávku do záporného čísla.', + 'The cart recovery link is invalid. Please request a new one.' => 'Odkaz na obnovenie košíka je neplatný. Požiadajte o nový.', + 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Konverzný kurz, ktorý sa použije pri konverzii nejakej sumy do tejto meny. Napríklad, ak nejaká položka stojí {amount1}, na základe konverzného kurzu {rate} by v druhej mene stála {amount2}.', + 'The countries that orders are allowed to be placed from.' => 'Krajiny, z ktorých je povolené zadávať objednávky.', + 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'Kupón „{code}“ prekročil limit použitia {limit}.', + 'The customer for this order has been deleted.' => 'Zákazník, na ktorého sa táto objednávka vzťahuje, bol odstránený.', + 'The default shipping category is automatically available to all product types.' => 'Predvolená kategória dopravy je automaticky dostupná pre všetky typy produktov.', + 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'Zľava „{name}“ prekročila limit použitia {limit}.', + 'The download link has expired. Please request a new one.' => 'Platnosť odkazu na stiahnutie vypršala. Požiadajte o nový odkaz.', + 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'Emailová adresa, z ktorej sú odosielané emaily o stave objednávky. Prázdne, ak sa má použiť systémová emailová adresa definovaná vo všeobecných nastaveniach Craft-u.', + 'The entry that contains the description for this subscription’s plan.' => 'Záznam, ktorý obsahuje popis tohto plánu prihlásení na odber.', + 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'Fixná hodnota, ktorá má zľaviť každú položku, t.j. „3“ pre zľavu €3 pre každú položku.', + 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'Formát použitý na generovanie nových kupónov, napr. {example}. Akékoľvek znaky „#“ budú nahradené náhodným písmenom.', + 'The from and to inventory locations must be different.' => 'Miesta inventúry z a do sa musia líšiť.', + 'The inventory locations this store uses.' => 'Inventárne miesta, ktoré tento obchod používa.', + 'The item is not enabled for sale.' => 'Túto položku nie je povolené zahrnúť do predaja.', + 'The language the order was made in.' => 'Jazyk, v ktorom bola objednávka uskutočnená.', + 'The language to be used when this email is rendered.' => 'Jazyk, ktorý sa má použiť pri zobrazení tohto e-mailu.', + 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Maximálny počet úrovní, ktoré môže tento typ produktu obsahovať. Ak na tom nezáleží, ponechajte políčko prázdne.', + 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Maximálna suma, ktorú by mal zákazník minúť za dopravu. Vypnete zadaním nuly.', + 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Minimálna suma, ktorú by mal zákazník minúť za dopravu. Vypnete zadaním nuly.', + 'The order is not valid.' => 'Neplatná objednávka.', + 'The payment gateway that will be used for the subscription plan.' => 'Platobná brána, ktorá sa použije pre plán prihlásení na odber.', + 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'Percentuálna hodnota, ktorá má zľaviť každú položku, t.j. {ex1} pre zľavu {ex2} pre každú položku. Percentá sú zaokrúhlené na 2 desatinné miesta.', + 'The previously-selected shipping method is no longer available.' => 'Predtým zvolený spôsob dopravy už nie je dostupný.', + 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Cena {description} sa zvýšila z {originalSalePriceAsCurrency} na {newSalePriceAsCurrency}', + 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Cena {description} sa znížila z {originalSalePriceAsCurrency} na {newSalePriceAsCurrency}', + 'The primary currency cannot be changed after orders are placed.' => 'Hlavná mena nemôže byť po zadaní objednávok zmenená.', + 'The purchasable defines the relationship' => 'Položka na predaj definuje vzťah', + 'The purchasable is related by another element' => 'Položka na predaj je spojená s ďalším prvkom', + 'The recipient of the email. Twig code can be used here.' => 'Príjemca e-mailu. Môže byť použitý Twig kód.', + 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'E-mailová adresa pre odpoveď. Ak chcete použiť normálnu adresu pre odpoveď, ponechajte toto pole prázdne. Môže byť použitý Twig kód.', + 'The site the order was made in.' => 'Miesto, kde bola objednávka zadaná.', + 'The site to be used when this email is rendered.' => 'Miesto, ktoré sa má použiť pri zobrazení tohto e-mailu.', + 'The subject line of the email. Twig code can be used here.' => 'Riadok predmetu e-mailu. Môže byť použitý Twig kód.', + 'The template that the PDF should be generated from.' => 'Šablóna, z ktorej sa má generovať PDF súbor.', + 'The template to be used for HTML emails.' => 'Šablóna ktorá sa má použiť pre HTML emaily.', + 'The template to be used for plain text emails. Twig code can be used here.' => 'Šablóna, ktorá sa má použiť pre e-maily s obyčajným textom. Môže byť použitý Twig kód.', + 'The template to use when a product’s URL is requested.' => 'Šablóna, ktorá sa má použiť pri požiadavke URL adresy produktu.', + 'The total number of order adjustments changed.' => 'Celkový počet úprav objednávky sa zmenil.', + 'The total price of the order changed.' => 'Celková cena objednávky sa zmenila.', + 'The total quantity of items within the order changed.' => 'Celkové množstvo položiek v objednávke sa zmenilo.', + 'The unique SKU of the donation purchasable.' => 'Jedinečné SKU pre predajný dar.', + 'The unit of measurement that should be used when specifying product dimensions.' => 'Merná jednotka, ktorá má byť použitá pri určovaní rozmerov produktu.', + 'The unit of measurement that should be used when specifying product weights.' => 'Merná jednotka, ktorá má byť použitá pri určovaní váhy produktu.', + 'The webhook URL for this gateway.' => 'URL pre webhook pre túto bránu.', + 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'Meno „Od“, ktoré bude použité pri odosielaní emailov o stave objednávky. Prázdne, ak sa má použiť meno odosielateľa definované vo všeobecných nastaveniach Craft-u.', + 'There are errors on the order' => 'Objednávka obsahuje chyby', + 'There are only {num} “{description}” items left in stock.' => 'Počet zvyšných položiek „{description}“ na sklade je {num}.', + 'There aren’t any product types to select yet.' => 'Zatiaľ nie je možné vybrať žiadne typy produktov.', + 'There is no gateway or payment source available for use with this order.' => 'Pre túto objednávku nie je k dispozícii žiadna použiteľná brána alebo zdroj platby.', + 'There is no gateway selected that supports payment sources.' => 'Nie je vybratá žiadna brána, ktorá podporuje zdroje platby.', + 'There is no shipping method selected for this order.' => 'Pre túto objednávku nie je vybraný žiadny spôsob doručenia.', + 'This URL will load the cart into the user’s session, making it the active cart.' => 'Tento odkaz URL nahrá košík do relácie používateľa a aktivuje ho.', + 'This action is not allowed for the current user.' => 'Táto akcia nie je pre aktuálneho používateľa povolená.', + 'This category will be used as the default for all purchasables in this store.' => 'Táto kategória sa bude používať ako predvolená pre všetky nákupy v tomto obchode.', + 'This coupon is for registered users and limited to {limit} uses.' => 'Tento kupón je pre registrovaných používateľov a je obmedzený na {limit} použití.', + 'This coupon is limited to {limit} uses.' => 'Tento kupón je obmedzený na {limit} použití.', + 'This coupon requires an email address.' => 'Tento kupón vyžaduje e-mailovú adresu.', + 'This gateway does not support that functionality.' => 'Táto brána nepodporuje danú funkciu.', + 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Toto je prepísané nastavením konfigurácie {setting} v `config/{file}.php`.', + 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Toto je adresa umiestnenia obchodu. Pomocou nej môžu rôzne doplnky určiť napríklad dodacie podmienky a dane. Používa sa tiež v potvrdenkách vo formáte PDF.', + 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Toto je predvolený súbor PDF, ktorý sa vykreslí pri žiadosti o súbor PDF objednávky.', + 'This is the last location for the {store} store.' => 'Toto je posledné miesto pre obchod {store}.', + 'This month' => 'Tento mesiac', + 'This order has unsaved changes.' => 'Táto objednávka obsahuje neuložené zmeny.', + 'This week' => 'Tento týždeň', + 'This year' => 'Tento rok', + 'Times Used' => 'Počet Použití', + 'Title' => 'Názov', + 'To' => 'Komu', + 'Today' => 'Dnes', + 'Too many variants for this product.' => 'Príliš veľa variantov pre tento produkt.', + 'Top Customers by Average Order' => 'Top zákazníci podľa priemernej objednávky', + 'Top Customers by Total Revenue' => 'Top zákazníci podľa celkového príjmu', + 'Top Customers' => 'Top zákazníci', + 'Top Product Types by Qty Sold' => 'Top typy produktov podľa počtu predaných kusov', + 'Top Product Types by Revenue' => 'Top typy produktov podľa príjmu', + 'Top Product Types' => 'Top typy produktov', + 'Top Products by Qty Sold' => 'Top produkty podľa počtu predaných kusov', + 'Top Products by Revenue' => 'Top produkty podľa príjmu', + 'Top Products' => 'Top produkty', + 'Top Purchasables by Qty Sold' => 'Top položky na predaj podľa počtu predaných kusov', + 'Top Purchasables by Revenue' => 'Top položky na predaj podľa príjmu', + 'Top Purchasables' => 'Top položky na predaj', + 'Total ' => 'Celkom ', + 'Total Discount Use Limit' => 'Celkový limit na využitie zľavy', + 'Total Discount' => 'Celková zľava', + 'Total Included Tax' => 'Celková započítaná daň', + 'Total Orders by Billing Country' => 'Celkový počet objednávok podľa krajiny fakturácie', + 'Total Orders by Country' => 'Celkový počet objednávok podľa krajiny', + 'Total Orders by Shipping Country' => 'Celkový počet objednávok podľa krajiny dopravy', + 'Total Orders' => 'Celkový počet objednávok', + 'Total Paid' => 'Celkom Uhradené', + 'Total Price' => 'Celková Cena', + 'Total Qty' => 'Celkové množstvo', + 'Total Revenue' => 'Celkový príjem', + 'Total Shipping' => 'Celkové náklady na dopravu', + 'Total Tax' => 'Celková daň', + 'Total Weight' => 'Celková hmotnosť', + 'Total' => 'Celkom', + 'Track Inventory' => 'Sledovanie zásob', + 'Transaction Hash' => 'Hodnota hash transakcie', + 'Transaction ID' => 'ID transakcie', + 'Transaction captured successfully: {message}' => 'Transakcia úspešne zachytená: {message}', + 'Transaction refunded successfully: {message}' => 'Transakcia úspešne vrátená: {message}', + 'Transactions' => 'Transakcie', + 'Transfer Fields' => 'Polia na prevod', + 'Transfer Items' => 'Položky na prevod', + 'Transfer Settings' => 'Nastavenia prevodu', + 'Transfer Status' => 'Stav prevodu', + 'Transfer fields saved.' => 'Uložené polia na prevod.', + 'Transfer must have at least one item.' => 'Prevod musí obsahovať aspoň jednu položku.', + 'Transfer' => 'Prevod', + 'Transfers' => 'Prevody', + 'Trial days credited' => 'Priznané dni skúšobnej verzie', + 'Trial expiration' => 'Vypršanie platnosti skúšobnej verzie', + 'Trial expiry date' => 'Dátum exspirácie skúšobnej verzie', + 'Type not in allowed options.' => 'Typ nie je možné použiť v možnostiach.', + 'Type' => 'Typ', + 'URI' => 'URI', + 'Unable to cancel subscription at this time.' => 'Prihlásenie na odber sa momentálne nedá zrušiť.', + 'Unable to complete order: another request is already in progress.' => 'Objednávku nie je možné dokončiť: práve prebieha iná požiadavka.', + 'Unable to find variant.' => 'Nie je možné nájsť variant.', + 'Unable to generate coupon codes: {message}' => 'Nie je možné generovať kódy kupónov: {message}', + 'Unable to make payment at this time.' => 'Platba sa nedá aktuálne uskutočniť.', + 'Unable to modify subscription at this time.' => 'Prihlásenie na odber sa aktuálne nedá upraviť.', + 'Unable to reactivate subscription at this time.' => 'Prihlásenie na odber sa aktuálne nedá znova aktivovať.', + 'Unable to reassign orders.' => 'Objednávky nie je možné prerozdeliť.', + 'Unable to remove order data.' => 'Údaje o objednávke sa nepodarilo odstrániť.', + 'Unable to retrieve Sale and Purchasable.' => 'Nebolo možné obnoviť zľavu a položky na predaj.', + 'Unable to retrieve cart.' => 'Košík sa nedá obnoviť.', + 'Unable to retrieve customer.' => 'Nebolo možné obnoviť zákazníka.', + 'Unable to retrieve load cart URL' => 'Nebolo možné získať odkaz URL pre nahranie košíka', + 'Unable to retrieve payment source.' => 'Nie je možné načítať zdroj platby.', + 'Unable to set default shipping category.' => 'Predvolená kategória dopravy sa nedala nastaviť.', + 'Unable to set default tax category.' => 'Predvolená daňová kategória sa nedala nastaviť.', + 'Unable to set primary payment source.' => 'Nie je možné nastaviť primárny zdroj platby.', + 'Unable to start the subscription. Please check your payment details.' => 'Prihlásenie na odber nie je možné spustiť. Skontrolujte platobné údaje.', + 'Unable to subscribe at this time.' => 'Aktuálne nie je možné prihlásiť odber.', + 'Unable to update cart.' => 'Košík sa nedá aktualizovať.', + 'Unable to validate address.' => 'Nie je možné overiť adresu.', + 'Unit Price' => 'Jednotková cena', + 'Unit price (minus discounts)' => 'Jednotková cena (znížená o zľavy)', + 'Units' => 'Jednotky', + 'Unpaid' => 'Nezaplatené', + 'Unsubscribe' => 'Odhlásiť odber', + 'Update Address' => 'Aktualizovať adresu', + 'Update Order Status' => 'Aktualizovať stav objednávky', + 'Update Order Status…' => 'Aktualizovať stav objednávky…', + 'Update order' => 'Aktualizovať objednávku', + 'Update subscription' => 'Aktualizovať predplatné', + 'Update' => 'Aktualizovať', + 'Updated By' => 'Aktualizoval(a)', + 'Updated committed stock successfully.' => 'Úspešne aktualizované viazané zásoby.', + 'Updated' => 'Aktualizované', + 'Use Billing Address For Tax' => 'Použitie fakturačnej adresy pre daň', + 'Use as the primary billing address' => 'Používajte ako hlavnú fakturačnú adresu', + 'Use as the primary shipping address' => 'Používajte ako hlavnú dodaciu adresu', + 'Used By Tax Rates' => 'Používané daňovými sadzbami', + 'Used by Tax Rates' => 'Používané daňovými sadzbami', + 'User Groups' => 'Skupiny užívateľov', + 'User not found.' => 'Používateľ sa nenašiel.', + 'User' => 'Používateľ', + 'Uses' => 'Používa', + 'Validate Business Tax ID as Vat ID' => 'Overenie daňového identifikačného čísla podniku ako identifikačného čísla DPH', + 'Validating condition syntax' => 'Overuje sa platnosť syntaxe', + 'Validating formula syntax' => 'Overuje sa syntax vzorca', + 'Variant Fields' => 'Polia variantu', + 'Variant Has Untracked Stock' => 'Variant má nesledované zásoby', + 'Variant Price' => 'Cena variantu', + 'Variant SKU' => 'SKU variantu', + 'Variant Search' => 'Vyhľadávanie variantov', + 'Variant Stock' => 'Zásoby variantu', + 'Variant Title Format' => 'Formát Názvu Varianty', + 'Variant Tracks Stock' => 'Variant skladových zásob', + 'Variant UI Label Format' => 'Formát označenia variantu používateľského rozhrania', + 'Variant has no product.' => 'Variant nemá žiadny produkt.', + 'Variants not restored.' => 'Varianty neboli obnovené.', + 'Variants restored.' => 'Varianty boli obnovené.', + 'Variants' => 'Varianty', + 'View customer' => 'Zobraziť zákazníka', + 'View order' => 'Zobraziť objednávku', + 'View product type - {productType}' => 'Zobraziť typ produktu - {productType}', + 'View user' => 'Zobraziť používateľa', + 'View' => 'Zobraziť', + 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Upozornenie. Zmazanie tejto meny pozastaví všetky platby a vrátenie platieb v tejto mene. Naozaj chcete zmazať „{name}“?', + 'Web' => 'Web', + 'Webhook URL' => 'URL pre webhook', + 'Weight ({unit})' => 'Hmotnosť ({unit})', + 'Weight Rate' => 'Váhová Sadzba', + 'Weight Unit' => 'Jednotka Hmotnosti', + 'Weight' => 'Váha', + 'What product URIs should look like for the site.' => 'Ako by mali vyzerať identifikátory URI produktov pre daný web.', + 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Ako majú vyzerať automaticky generované názvy produktov. Možno zahrnúť značky, ktoré zobrazia vlastnosti produktov, ako je {ex1} alebo {ex2}. Všetky vlastné polia musia byť nastavené na povinné.', + 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Ako majú vyzerať automaticky generované názvy variant. Možno zahrnúť značky, ktoré zobrazia vlastnosti variant, ako je {ex1} alebo {ex2}. Všetky vlastné polia musia byť nastavené na povinné.', + 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Ako by mal vyzerať názov PDF súboru k objednávkam (bez prípony). Môžete pridať štítky, ktorých výstupom sú vlastnosti objednávky, ako napríklad {ex1} alebo {ex2}.', + 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Ako má vyzerať jedinečné automaticky generované SKU, keď je pole SKU odoslané prázdne. Možno zahrnúť značky, ktoré zobrazia vlastnosti, ako je {ex1} alebo {ex2}', + 'What this PDF will be called in the control panel.' => 'Ako sa bude tento PDF súbor volať v ovládacom paneli.', + 'What this catalog pricing rule will be called in the control panel.' => 'Ako sa bude toto pravidlo stanovovania cien podľa katalógu volať v ovládacom paneli.', + 'What this discount will be called in the control panel.' => 'Ako sa bude táto zľava volať v ovládacom paneli.', + 'What this email will be called in the control panel.' => 'Ako sa bude tento e-mail volať v ovládacom paneli.', + 'What this product type will be called in the control panel.' => 'Ako sa bude tento typ produktu volať v ovládacom paneli.', + 'What this sale will be called in the control panel.' => 'Ako sa bude tento výpredaj volať v ovládacom paneli.', + 'What this shipping category will be called in the control panel.' => 'Ako sa bude táto kategória dopravy volať v ovládacom paneli.', + 'What this shipping rule will be called in the control panel.' => 'Ako sa bude toto pravidlo dopravy volať v ovládacom paneli.', + 'What this shipping zone will be called in the control panel.' => 'Ako sa bude táto zóna doručenia volať v ovládacom paneli.', + 'What this status will be called in the control panel.' => 'Ako sa bude tento stav volať v ovládacom paneli.', + 'What this subscription plan will be called in the control panel.' => 'Ako sa bude toto predplatné volať v ovládacom paneli.', + 'What this tax category will be called in the control panel.' => 'Ako sa bude táto daňová kategória volať v ovládacom paneli.', + 'What this tax zone will be called in the control panel.' => 'Ako sa bude táto daňová zóna volať v ovládacom paneli.', + 'When this discount is applied to an order, which line items should be discounted?' => 'Keď sa táto zľava uplatní na objednávku, ktoré položky by mali byť zľavnené?', + 'Whether the first available shipping method option should be set automatically on carts.' => 'Či sa má v košíkoch automaticky nastaviť prvá dostupná možnosť spôsobu dopravy.', + 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Či sa má pri nových košíkoch automaticky nastaviť primárny zdroj platby používateľa.', + 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Či sa má v nových košíkoch automaticky nastaviť primárna dodacia a fakturačná adresa používateľa.', + 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Či by toto pravidlo stanovovania cien podľa katalógu malo byť k dispozícii na použitie bez ohľadu na ostatné podmienky.', + 'Whether this sale should be available for use, regardless of other conditions.' => 'Určuje, či má byť táto predajná akcia dostupná na použitie bez ohľadu na ostatné podmienky.', + 'Which data to display in the name column in the results table.' => 'Ktoré údaje zobrazovať v stĺpci s názvom v tabuľke výsledkov.', + 'Which product types should this category be available to?' => 'Pre ktoré typy produktov má byť táto kategória dostupná?', + 'Which template should be loaded when a product’s URL is requested.' => 'Šablóna, ktorá sa ma načítať, keď je vyžiadaná adresa URL produktu.', + 'Width ({unit})' => 'Šírka ({unit})', + 'Width' => 'Šírka', + 'YYYY' => 'RRRR', + 'Yes' => 'Áno', + 'You are not allowed to add a line item.' => 'Nemáte oprávnenie pridať riadkovú položku.', + 'You currently have no emails configured to select for this status.' => 'Pre výber tohto stavu nemáte momentálne nastavený žiadny e-mail.', + 'You do not have permission to load this cart.' => 'Nemáte oprávnenie na načítanie tohto košíka.', + 'You must set up at least one gateway that supports subscriptions first.' => 'Najprv musíte nastaviť aspoň jednu bránu, ktorá podporuje prihlásenia na odber.', + 'You must be logged in or provide a valid token to load this cart.' => 'Na načítanie tohto košíka sa musíte prihlásiť alebo zadať platný token.', + 'You must be signed in to create a payment source.' => 'Ak chcete vytvoriť zdroj platby, musíte byť prihlásení.', + 'You must be signed in to set a primary payment source.' => 'Ak chcete nastaviť primárny zdroj platby, musíte byť prihlásení.', + 'You must make a payment to complete the order.' => 'Ak chcete dokončiť túto objednávku, musíte ju zaplatiť.', + 'Your Cart Recovery Link' => 'Váš odkaz na obnovenie košíka', + 'Your Order PDF Download Link' => 'Odkaz na stiahnutie PDF vašej objednávky', + 'Your order is empty' => 'Vaša objednávka je prázdna', + 'ZIP file' => 'Súbor ZIP', + 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Nula – Minimálna cena je nula v prípade, že sú zľavy vyššie ako hodnota objednávky.', + 'Zip Code' => 'PSČ', + 'all' => 'všetky', + 'any' => 'akékoľvek', + 'average order total' => 'priemerná cena objednávky', + 'billing address' => 'fakturačná adresa', + 'donation' => 'darovanie', + 'donations' => 'dary', + 'info' => 'informácie', + 'inventory location' => 'umiestnenie zásob', + 'new customers' => 'noví zákazníci', + 'on hand' => 'dostupné', + 'only' => 'len', + 'order' => 'objednať', + 'orders' => 'objednávky', + 'price' => 'cena', + 'prices' => 'ceny', + 'product variant' => 'variant produktu', + 'product variants' => 'varianty produktu', + 'product' => 'produkt', + 'products' => 'produkty', + 'repeat customers' => 'opakovaní zákazníci', + 'shipping address' => 'dodacia adresa', + 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'Nie je možné nastaviť súčasne položku shippingSameAsBilling aj položku billingSameAsShipping.', + 'subscription' => 'predplatné', + 'subscriptions' => 'predplatné', + 'to' => 'na', + 'transfer' => 'prevod', + 'transfers' => 'prevody', + '{amount} included' => 'vrátane {amount}', + '{count} Unfulfilled Orders' => 'Nesplnené objednávky: {count}', + '{description} is no longer available.' => '{description} už nie je dostupný.', + '{description} only has {stock} in stock.' => 'Počet kusov {description} na sklade je už len {stock}.', + '{from} to {to}' => '{from} až {to}', + '{name} (Primary)' => '{name} (Hlavná)', + '{name} (Trashed)' => '{name} (Zahodené do koša)', + '{name} catalog price' => '{name} katalógová cena', + '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, one {} few {objednávky} many {objednávok}=1{objednávka} other{objednávok}} aktualizovaná/aktualizovaných.', + '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, one {} few {objednávky sú priradené} many {objednávok je priradených}=1{objednávka je priradená} other{objednávok je priradených}} k {numUsers, plural, one {} few {používateľom} many {používateľom}=1{používateľovi} other{používateľom}}.', + '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, one {} few {predplatné sú aktivované} many {predplatných je aktivovaných}=1{predplatné je aktivované} other{predplatných je aktivovaných}} pre {numUsers, plural, one {} few {používateľov} many {používateľov}=1{používateľa} other{používateľov}}.', + '{number} more…' => 'Ešte {number}…', + '{pct} off the discounted item price' => '{pct} zo zľavnenej ceny položky', + '{pct} off the original item price' => '{pct} z pôvodnej ceny položky', + '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, one {} few {nemajú} many {nemá}=1{nemá} other{nemá}} priradenú lokalitu.', + '{total} in total revenue' => 'celkový príjem {total}', + '{total} orders' => '{total} objednávok', + '{total} saleable across {locationCount} location(s)' => '{total} predajné na {locationCount} mieste (miestach)', + '{uses} uses across {emails} email addresses' => '{uses} použití pre e-mailové adresy {emails}', + '{uses} uses across {users} users' => '{uses} použití pre použivateľov {users}', + '“{description}” is currently out of stock.' => 'Produkt „{description}“ je momentálne vypredaný.', + '“{key}” has invalid JSON' => '„{key}“ obsahuje neplatný JSON', +]; diff --git a/phpstan.neon b/phpstan.neon index 39e7ab9091..ce7b2c4d71 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,42 @@ includes: - - vendor/craftcms/phpstan/phpstan.neon + - vendor/larastan/larastan/extension.neon + - vendor/nesbot/carbon/extension.neon parameters: level: 5 + parallel: + # Parallel workers each independently reflect on classes reached through class_alias() + # chains (the legacy src-yii2/ -> src/ stubs); depending on which worker resolves a given + # alias target first, PHPStan sometimes can/can't trace the chain, making `argument.type`/ + # `method.notFound` errors (and their `@phpstan-ignore-next-line` suppressions) on those + # call sites non-deterministic between otherwise-identical runs. Single-process analysis + # is slower but avoids that race entirely. + maximumNumberOfProcesses: 1 paths: - - src \ No newline at end of file + - src + scanDirectories: + - src-yii2 + scanFiles: + - vendor/craftcms/yii2-adapter/legacy/Craft.php + - vendor/craftcms/yii2-adapter/lib/yii2/Yii.php + - vendor/twig/twig/src/Extension/CoreExtension.php + - vendor/craftcms/cms/src/helpers.php + databaseMigrationsPath: + - database/migrations + stubFiles: + - vendor/craftcms/yii2-adapter/stubs/laravel-ruleset-validation.stub + - vendor/craftcms/yii2-adapter/stubs/_generated.stub + - vendor/craftcms/yii2-adapter/stubs/GraphQL/Type/Definition/FieldDefinition.stub + - vendor/craftcms/yii2-adapter/stubs/GraphQL/Type/Definition/ResolveInfo.stub + - vendor/craftcms/yii2-adapter/stubs/samdark/log/PsrTarget.stub + - vendor/craftcms/yii2-adapter/stubs/yii/base/Component.stub + - vendor/craftcms/yii2-adapter/stubs/yii/base/Event.stub + - vendor/craftcms/yii2-adapter/stubs/yii/base/Module.stub + - vendor/craftcms/yii2-adapter/stubs/yii/BaseYii.stub + - vendor/craftcms/yii2-adapter/stubs/yii/db/BaseActiveRecord.stub + - vendor/craftcms/yii2-adapter/stubs/yii/db/Query.stub + - vendor/craftcms/yii2-adapter/stubs/yii/db/Migration.stub + - vendor/craftcms/yii2-adapter/stubs/yii/di/ServiceLocator.stub + - vendor/craftcms/yii2-adapter/stubs/yii/helpers/BaseArrayHelper.stub + - vendor/craftcms/yii2-adapter/stubs/yii/validators/UniqueValidator.stub + - vendor/craftcms/yii2-adapter/stubs/yii/validators/Validator.stub \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000000..8ee86de9bd --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,42 @@ + + + + + ./tests/Feature + + + ./tests/Unit + + + ./tests/Arch + + + + + + + + + + + + + + + + + + + + + ./src + + + diff --git a/rector.php b/rector.php index 83d0991d94..10464479f9 100644 --- a/rector.php +++ b/rector.php @@ -7,12 +7,5 @@ return RectorConfig::configure() ->withPaths([ __DIR__ . '/src', - __DIR__ . '/tests/unit', - ]) - ->withSkip([ - Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector::class => [ - __DIR__ . '/src/console/controllers/GatewaysController.php', - ], - Rector\Php80\Rector\Class_\StringableForToStringRector::class, - ]) - ->withPhpSets(php80: true); + __DIR__ . '/src-yii2', + ]); diff --git a/routes/actions.php b/routes/actions.php new file mode 100644 index 0000000000..3a8c6e8ef7 --- /dev/null +++ b/routes/actions.php @@ -0,0 +1,278 @@ +group(function () { + Route::get('cart/get-cart', [CartController::class, 'getCart']); + Route::post('cart/update-cart', [CartController::class, 'updateCart']); + Route::match(['get', 'post'], 'cart/load-cart', [CartController::class, 'loadCart']); + Route::post('cart/complete', [CartController::class, 'complete']); +}); + +Route::post('cart/forget-cart', [CartController::class, 'forgetCart']); +Route::get('cart/email-challenge', [CartController::class, 'emailChallenge']); +Route::post('cart/cart-challenge', [CartController::class, 'cartChallenge']) + ->middleware('throttle:' . CartChallengeRateLimiter::NAME); +Route::get('cart/cart-sent', [CartController::class, 'cartSent']); + +// Anonymous by design — guest checkout must be able to pay. Each action gates its own +// order/customer-ownership checks inline (mirrors CartController's own permission model). +Route::post('payments/pay', [PaymentsController::class, 'pay']); +Route::match(['get', 'post'], 'payments/complete-payment', [PaymentsController::class, 'completePayment']); + +Route::post('payment-sources/add', [PaymentSourcesController::class, 'add']); +Route::post('payment-sources/set-primary-payment-source', [PaymentSourcesController::class, 'setPrimaryPaymentSource']); +Route::post('payment-sources/delete', [PaymentSourcesController::class, 'delete']); + +// These are also reachable, unauthenticated, at their site-side action URL (per +// CraftCms\Cms\Plugin\Concerns\HasRoutes::registerActionRoutes()) — the `auth`/`can` +// middleware below is what actually protects them, not the URL prefix. +Route::middleware(['auth', 'can:accessPlugin-commerce', 'can:commerce-manageDonationSettings']) + ->post('donations/save', [DonationsController::class, 'save']); + +Route::middleware(['auth', 'can:accessPlugin-commerce', RequireAdmin::class])->group(function () { + Route::post('gateways/save', [GatewaysController::class, 'save']); + Route::post('gateways/archive', [GatewaysController::class, 'archive']); + Route::post('gateways/reorder', [GatewaysController::class, 'reorder']); + + Route::post('settings/save-settings', [GeneralSettingsController::class, 'saveSettings']); + Route::post('settings/save-transfer-settings', [TransferSettingsController::class, 'saveTransferSettings']); + Route::post('order-settings/save', [OrderSettingsController::class, 'save']); + + Route::post('stores/save-store', [StoresController::class, 'saveStore']); + Route::post('stores/delete-store', [StoresController::class, 'deleteStore']); + Route::post('stores/reorder-stores', [StoresController::class, 'reorderStores']); + Route::post('stores/save-site-stores', [StoresController::class, 'saveSiteStores']); + + Route::post('order-statuses/save', [OrderStatusesController::class, 'save']); + Route::match(['get', 'post'], 'order-statuses/get-order-statuses', [OrderStatusesController::class, 'getOrderStatuses']); + Route::post('order-statuses/reorder', [OrderStatusesController::class, 'reorder']); + Route::post('order-statuses/delete', [OrderStatusesController::class, 'delete']); + + Route::post('line-item-statuses/save', [LineItemStatusesController::class, 'save']); + Route::post('line-item-statuses/reorder', [LineItemStatusesController::class, 'reorder']); + Route::post('line-item-statuses/archive', [LineItemStatusesController::class, 'archive']); + + Route::post('product-types/save-product-type', [ProductTypesController::class, 'saveProductType']); + Route::post('product-types/delete-product-type', [ProductTypesController::class, 'deleteProductType']); +}); + +// BaseStoreManagementController::init() always required commerce-manageStoreSettings, on top of +// whichever more specific permission each feature area's own controller adds — every route in +// this group must check both, matching that legacy compound check exactly. +Route::middleware(['auth', 'can:accessPlugin-commerce', 'can:commerce-manageStoreSettings'])->group(function () { + Route::post('store-management/save', [StoreManagementController::class, 'save']); + Route::post('store-management/render-form', [StoreManagementController::class, 'renderForm']); + + Route::middleware('can:commerce-managePaymentCurrencies')->group(function () { + Route::post('payment-currencies/save', [PaymentCurrenciesController::class, 'save']); + Route::post('payment-currencies/delete', [PaymentCurrenciesController::class, 'delete']); + }); + + Route::middleware('can:commerce-manageShipping')->group(function () { + Route::post('shipping-zones/save', [ShippingZonesController::class, 'save']); + Route::post('shipping-zones/delete', [ShippingZonesController::class, 'delete']); + Route::post('shipping-zones/test-zip', [ShippingZonesController::class, 'testZip']); + + Route::post('shipping-methods/save', [ShippingMethodsController::class, 'save']); + Route::post('shipping-methods/delete', [ShippingMethodsController::class, 'delete']); + Route::post('shipping-methods/update-status', [ShippingMethodsController::class, 'updateStatus']); + + Route::post('shipping-rules/save', [ShippingRulesController::class, 'save']); + Route::post('shipping-rules/duplicate', [ShippingRulesController::class, 'duplicate']); + Route::post('shipping-rules/reorder', [ShippingRulesController::class, 'reorder']); + Route::post('shipping-rules/delete', [ShippingRulesController::class, 'delete']); + Route::post('shipping-rules/render-form', [ShippingRulesController::class, 'renderForm']); + + Route::post('shipping-categories/save', [ShippingCategoriesController::class, 'save']); + Route::post('shipping-categories/delete', [ShippingCategoriesController::class, 'delete']); + Route::post('shipping-categories/set-default-category', [ShippingCategoriesController::class, 'setDefaultCategory']); + Route::post('shipping-categories/render-form', [ShippingCategoriesController::class, 'renderForm']); + }); + + Route::middleware('can:commerce-manageTaxes')->group(function () { + Route::post('tax-zones/save', [TaxZonesController::class, 'save']); + Route::post('tax-zones/delete', [TaxZonesController::class, 'delete']); + Route::post('tax-zones/test-zip', [TaxZonesController::class, 'testZip']); + + Route::post('tax-categories/save', [TaxCategoriesController::class, 'save']); + Route::post('tax-categories/delete', [TaxCategoriesController::class, 'delete']); + Route::post('tax-categories/set-default-category', [TaxCategoriesController::class, 'setDefaultCategory']); + + Route::post('tax-rates/save', [TaxRatesController::class, 'save']); + Route::post('tax-rates/delete', [TaxRatesController::class, 'delete']); + Route::post('tax-rates/update-status', [TaxRatesController::class, 'updateStatus']); + Route::post('tax-rates/render-form', [TaxRatesController::class, 'renderForm']); + }); + + Route::middleware('can:commerce-managePromotions')->group(function () { + Route::post('sales/save', [SalesController::class, 'save']); + Route::post('sales/reorder', [SalesController::class, 'reorder']); + Route::post('sales/delete', [SalesController::class, 'delete']); + Route::match(['get', 'post'], 'sales/get-all-sales', [SalesController::class, 'getAllSales']); + Route::post('sales/get-sales-by-product-id', [SalesController::class, 'getSalesByProductId']); + Route::post('sales/get-sales-by-purchasable-id', [SalesController::class, 'getSalesByPurchasableId']); + Route::post('sales/add-purchasable-to-sale', [SalesController::class, 'addPurchasableToSale']); + Route::post('sales/update-status', [SalesController::class, 'updateStatus']); + + Route::match(['get', 'post'], 'discounts/table-data', [DiscountsController::class, 'tableData']); + Route::post('discounts/save', [DiscountsController::class, 'save']); + Route::post('discounts/reorder', [DiscountsController::class, 'reorder']); + Route::post('discounts/move-to-page', [DiscountsController::class, 'moveToPage']); + Route::post('discounts/delete', [DiscountsController::class, 'delete']); + Route::post('discounts/clear-discount-uses', [DiscountsController::class, 'clearDiscountUses']); + Route::post('discounts/update-status', [DiscountsController::class, 'updateStatus']); + Route::post('discounts/get-discounts-by-purchasable-id', [DiscountsController::class, 'getDiscountsByPurchasableId']); + Route::post('discounts/generate-coupons', [DiscountsController::class, 'generateCoupons']); + + Route::post('catalog-pricing-rules/save', [CatalogPricingRulesController::class, 'save']); + Route::post('catalog-pricing-rules/delete', [CatalogPricingRulesController::class, 'delete']); + Route::post('catalog-pricing-rules/update-status', [CatalogPricingRulesController::class, 'updateStatus']); + + Route::post('catalog-pricing/filter', [CatalogPricingController::class, 'filter']); + Route::post('catalog-pricing/prices', [CatalogPricingController::class, 'prices']); + Route::get('catalog-pricing/queue-status', [CatalogPricingController::class, 'queueStatus']); + Route::post('catalog-pricing/get-catalog-prices', [CatalogPricingController::class, 'getCatalogPrices']); + }); +}); + +// OrdersController extends the plain Yii2 Controller (not BaseCpController) — its init() only +// ever checked commerce-manageOrders, not accessPlugin-commerce. +Route::middleware(['auth', 'can:commerce-manageOrders'])->group(function () { + Route::post('orders/fulfill', [OrdersController::class, 'fulfill']); + Route::get('orders/fulfillment-modal', [OrdersController::class, 'fulfillmentModal']); + Route::post('orders/save', [OrdersController::class, 'save']); + Route::post('orders/delete-order', [OrdersController::class, 'deleteOrder']); + Route::post('orders/refresh', [OrdersController::class, 'refresh']); + Route::post('orders/get-shipping-method-options', [OrdersController::class, 'getShippingMethodOptions']); + Route::get('orders/user-orders-table', [OrdersController::class, 'userOrdersTable']); + Route::get('orders/purchasables-table', [OrdersController::class, 'purchasablesTable']); + Route::get('orders/customer-search', [OrdersController::class, 'customerSearch']); + Route::get('orders/get-customer-addresses', [OrdersController::class, 'getCustomerAddresses']); + Route::get('orders/get-order-address', [OrdersController::class, 'getOrderAddress']); + Route::post('orders/validate-address', [OrdersController::class, 'validateAddress']); + Route::post('orders/create-customer', [OrdersController::class, 'createCustomer']); + Route::get('orders/get-load-cart-url', [OrdersController::class, 'getLoadCartUrl']); + Route::get('orders/send-email', [OrdersController::class, 'sendEmail']); + Route::get('orders/update-order-address', [OrdersController::class, 'updateOrderAddress']); + Route::get('orders/get-index-sources-badge-counts', [OrdersController::class, 'getIndexSourcesBadgeCounts']); + Route::get('orders/get-payment-modal', [OrdersController::class, 'getPaymentModal']); + Route::post('orders/payment-amount-data', [OrdersController::class, 'paymentAmountData']); + + Route::post('orders/copy-address-to-user', [OrdersController::class, 'copyAddressToUser']) + ->middleware('can:editUsers'); + + Route::middleware('can:commerce-capturePayment') + ->post('orders/transaction-capture', [OrdersController::class, 'transactionCapture']); + Route::middleware('can:commerce-refundPayment') + ->post('orders/transaction-refund', [OrdersController::class, 'transactionRefund']); + + Route::middleware([RequireCpRequest::class, 'can:deleteUsers'])->group(function () { + Route::get('orders/reassign-modal', [OrdersController::class, 'reassignModal']); + Route::post('orders/reassign', [OrdersController::class, 'reassign']); + Route::get('orders/remove-customer-data-modal', [OrdersController::class, 'removeCustomerDataModal']); + Route::post('orders/remove-customer-data', [OrdersController::class, 'removeCustomerData']); + }); +}); + +// InventoryController checks commerce-manageInventoryStockLevels inline on every action +// (not via init()) — replicated here as a route-group-wide permission instead. +Route::middleware(['auth', 'can:commerce-manageInventoryStockLevels'])->group(function () { + Route::post('inventory/item-save', [InventoryController::class, 'itemSave']); + Route::get('inventory/inventory-levels-table-data', [InventoryController::class, 'inventoryLevelsTableData']); + Route::post('inventory/update-levels', [InventoryController::class, 'updateLevels']); + Route::get('inventory/edit-update-levels-modal', [InventoryController::class, 'editUpdateLevelsModal']); + Route::post('inventory/save-inventory-movement', [InventoryController::class, 'saveInventoryMovement']); + Route::get('inventory/edit-movement-modal', [InventoryController::class, 'editMovementModal']); + Route::get('inventory/unfulfilled-orders', [InventoryController::class, 'unfulfilledOrders']); +}); + +Route::middleware(['auth', 'can:commerce-manageInventoryLocations'])->group(function () { + Route::post('inventory-locations/save', [InventoryLocationsController::class, 'save']); + Route::get('inventory-locations/inventory-locations-table-data', [InventoryLocationsController::class, 'inventoryLocationsTableData']); + Route::get('inventory-locations/prepare-delete-modal', [InventoryLocationsController::class, 'prepareDeleteModal']); + Route::post('inventory-locations/deactivate', [InventoryLocationsController::class, 'deactivate']); +}); + +Route::middleware(['auth', 'can:commerce-manageInventoryTransfers'])->group(function () { + Route::get('transfers/create', [TransfersController::class, 'create']); + Route::post('transfers/mark-as-pending', [TransfersController::class, 'markAsPending']); + Route::post('transfers/save-settings', [TransfersController::class, 'saveSettings']); + Route::post('transfers/receive-transfer', [TransfersController::class, 'receiveTransfer']); + Route::get('transfers/receive-transfer-screen', [TransfersController::class, 'receiveTransferScreen']); + Route::get('transfers/render-management', [TransfersController::class, 'renderManagement']); +}); + +Route::middleware(['auth', 'can:accessPlugin-commerce', RequireAdmin::class])->group(function () { + Route::post('emails/save', [EmailsController::class, 'save']); + Route::post('emails/delete', [EmailsController::class, 'delete']); + + Route::post('pdfs/save', [PdfsController::class, 'save']); + Route::post('pdfs/delete', [PdfsController::class, 'delete']); + Route::post('pdfs/reorder', [PdfsController::class, 'reorder']); +}); + +Route::middleware(['auth', 'can:accessPlugin-commerce'])->group(function () { + Route::post('formulas/validate-condition', [FormulasController::class, 'validateCondition']); + Route::post('formulas/validate-formula', [FormulasController::class, 'validateFormula']); +}); + +// Rendered inside an iframe from the email edit screen's preview button — admin-only, matching +// the legacy controller's plain `requireAdmin(false)` (it never extended a Commerce base +// controller, so there was never an accessPlugin-commerce check here either). +Route::middleware(RequireAdmin::class)->get('email-preview/render', [EmailPreviewController::class, 'render']); + +// Anonymous by design — customers download/request order PDFs without being logged in. +// pdf-challenge is rate-limited (not auth-gated) to blunt brute-forcing of order numbers/hashes. +Route::get('downloads/pdf', [DownloadsController::class, 'pdf']); +Route::get('downloads/email-challenge', [DownloadsController::class, 'emailChallenge']); +Route::post('downloads/pdf-challenge', [DownloadsController::class, 'pdfChallenge']) + ->middleware('throttle:' . PdfChallengeRateLimiter::NAME); +Route::get('downloads/pdf-sent', [DownloadsController::class, 'pdfSent']); diff --git a/routes/cp.php b/routes/cp.php new file mode 100644 index 0000000000..4130e826a0 --- /dev/null +++ b/routes/cp.php @@ -0,0 +1,210 @@ +group(function () { + Route::middleware('can:commerce-manageDonationSettings') + ->get('commerce/donations', [DonationsController::class, 'edit']); + + Route::middleware(RequireAdmin::class)->group(function () { + Route::get('commerce/settings/gateways', [GatewaysController::class, 'index']); + Route::get('commerce/settings/gateways/new', [GatewaysController::class, 'edit']); + Route::post('commerce/settings/gateways/render-form', [GatewaysController::class, 'renderForm']); + Route::get('commerce/settings/gateways/{id}', [GatewaysController::class, 'edit'])->whereNumber('id'); + + Route::get('commerce/settings/general', [GeneralSettingsController::class, 'edit']); + Route::get('commerce/settings/ordersettings', [OrderSettingsController::class, 'edit']); + Route::get('commerce/settings/transfers', [TransferSettingsController::class, 'editTransferSettings']); + + Route::get('commerce/settings/stores', [StoresController::class, 'storesIndex']); + Route::get('commerce/settings/stores/new', [StoresController::class, 'editStore']); + Route::get('commerce/settings/stores/{storeId}', [StoresController::class, 'editStore'])->whereNumber('storeId'); + Route::get('commerce/settings/sites', [StoresController::class, 'editSiteStores']); + + Route::get('commerce/settings/orderstatuses', [OrderStatusesController::class, 'index']); + Route::get('commerce/settings/orderstatuses/{storeHandle}/new', [OrderStatusesController::class, 'edit']); + Route::get('commerce/settings/orderstatuses/{storeHandle}/{id}', [OrderStatusesController::class, 'edit'])->whereNumber('id'); + + Route::get('commerce/settings/lineitemstatuses', [LineItemStatusesController::class, 'index']); + Route::get('commerce/settings/lineitemstatuses/{storeHandle}/new', [LineItemStatusesController::class, 'edit']); + Route::get('commerce/settings/lineitemstatuses/{storeHandle}/{id}', [LineItemStatusesController::class, 'edit'])->whereNumber('id'); + + Route::get('commerce/settings/producttypes', [ProductTypesController::class, 'productTypeIndex']); + Route::get('commerce/settings/producttypes/new', [ProductTypesController::class, 'editProductType']); + Route::post('commerce/settings/producttypes/render-form', [ProductTypesController::class, 'renderForm']); + Route::get('commerce/settings/producttypes/{productTypeId}', [ProductTypesController::class, 'editProductType'])->whereNumber('productTypeId'); + + Route::get('commerce/settings/emails', [EmailsController::class, 'index']); + Route::get('commerce/settings/emails/{storeHandle}/new', [EmailsController::class, 'edit']); + Route::post('commerce/settings/emails/render-form', [EmailsController::class, 'renderForm']); + Route::get('commerce/settings/emails/{storeHandle}/{id}', [EmailsController::class, 'edit'])->whereNumber('id'); + + Route::get('commerce/settings/pdfs', [PdfsController::class, 'index']); + Route::get('commerce/settings/pdfs/{storeHandle}/new', [PdfsController::class, 'edit']); + Route::get('commerce/settings/pdfs/{storeHandle}/{id}', [PdfsController::class, 'edit'])->whereNumber('id'); + }); + + // ProductsController/VariantsController extend BaseCpController directly (no extra + // permission beyond accessPlugin-commerce) — each additionally guards its own methods with + // an inline "does the user have access to any product type" check. + Route::get('commerce/products/{productType}/new', [ProductsController::class, 'create']); + + // Product/variant/transfer edit screens just resolve the element by {id} — the + // productTypeHandle segment is cosmetic (matches legacy: the old `elements/edit` route + // never checked it against the element either). No extra permission middleware here, + // matching the legacy UrlManager rules — access is enforced by the element's own + // canView()/canSave() (see Product::canView()/Transfer::canView()). + // The product/variant routes must be registered before the {productTypeHandle?} index + // routes below, since a bare numeric segment (`commerce/variants/123`) would otherwise + // match the index route first. + $idSlugParams = [ + 'id' => '\d+', + 'slug' => '(?:-[^\/]*)', + ]; + Route::get('commerce/products/{productTypeHandle}/{id}{slug?}', EditElementController::class)->where($idSlugParams); + Route::get('commerce/variants/{id}{slug?}', EditElementController::class)->where($idSlugParams); + Route::get('commerce/inventory/transfers/{id}{slug?}', EditElementController::class)->where($idSlugParams); + + Route::get('commerce/products/{productTypeHandle?}', [ProductsController::class, 'productIndex']); + Route::get('commerce/variants/{productTypeHandle?}', [VariantsController::class, 'index']); + + // BaseStoreManagementController::init() always required commerce-manageStoreSettings, on + // top of whichever more specific permission each feature area's own controller adds — every + // route in this group must check both, matching that legacy compound check exactly. + Route::middleware('can:commerce-manageStoreSettings')->group(function () { + Route::get('commerce/store-management', [StoreManagementController::class, 'index']); + Route::get('commerce/store-management/{storeHandle}', [StoreManagementController::class, 'edit']); + + Route::middleware('can:commerce-manageShipping') + ->prefix('commerce/store-management/{storeHandle}') + ->group(function () { + Route::get('shippingzones', [ShippingZonesController::class, 'index']); + Route::get('shippingzones/new', [ShippingZonesController::class, 'edit']); + Route::get('shippingzones/{id}', [ShippingZonesController::class, 'edit'])->whereNumber('id'); + + Route::get('shippingcategories', [ShippingCategoriesController::class, 'index']); + Route::get('shippingcategories/new', [ShippingCategoriesController::class, 'edit']); + Route::get('shippingcategories/{id}', [ShippingCategoriesController::class, 'edit'])->whereNumber('id'); + + Route::get('shippingmethods', [ShippingMethodsController::class, 'index']); + Route::get('shippingmethods/new', [ShippingMethodsController::class, 'edit']); + Route::get('shippingmethods/{id}', [ShippingMethodsController::class, 'edit'])->whereNumber('id'); + Route::get('shippingmethods/{methodId}/shippingrules/new', [ShippingRulesController::class, 'edit'])->whereNumber('methodId'); + Route::get('shippingmethods/{methodId}/shippingrules/{ruleId}', [ShippingRulesController::class, 'edit'])->whereNumber(['methodId', 'ruleId']); + }); + + Route::middleware('can:commerce-manageTaxes') + ->prefix('commerce/store-management/{storeHandle}') + ->group(function () { + Route::get('taxcategories', [TaxCategoriesController::class, 'index']); + Route::get('taxcategories/new', [TaxCategoriesController::class, 'edit']); + Route::get('taxcategories/{id}', [TaxCategoriesController::class, 'edit'])->whereNumber('id'); + + Route::get('taxzones', [TaxZonesController::class, 'index']); + Route::get('taxzones/new', [TaxZonesController::class, 'edit']); + Route::get('taxzones/{id}', [TaxZonesController::class, 'edit'])->whereNumber('id'); + + Route::get('taxrates', [TaxRatesController::class, 'index']); + Route::get('taxrates/new', [TaxRatesController::class, 'edit']); + Route::get('taxrates/{id}', [TaxRatesController::class, 'edit'])->whereNumber('id'); + }); + + Route::middleware('can:commerce-managePromotions')->group(function () { + Route::get('commerce/catalog-pricing', [CatalogPricingController::class, 'index']); + + Route::prefix('commerce/store-management/{storeHandle}')->group(function () { + Route::get('sales', [SalesController::class, 'index']); + Route::get('sales/new', [SalesController::class, 'edit']); + Route::get('sales/{id}', [SalesController::class, 'edit'])->whereNumber('id'); + + Route::get('discounts', [DiscountsController::class, 'index']); + Route::get('discounts/new', [DiscountsController::class, 'edit']); + Route::get('discounts/{id}', [DiscountsController::class, 'edit'])->whereNumber('id'); + + Route::get('pricing-rules', [CatalogPricingRulesController::class, 'index']); + Route::get('pricing-rules/new', [CatalogPricingRulesController::class, 'edit']); + Route::get('pricing-rules/{id}', [CatalogPricingRulesController::class, 'edit'])->whereNumber('id'); + }); + }); + + Route::middleware('can:commerce-managePaymentCurrencies') + ->prefix('commerce/store-management/{storeHandle}') + ->group(function () { + Route::get('payment-currencies', [PaymentCurrenciesController::class, 'index']); + Route::get('payment-currencies/new', [PaymentCurrenciesController::class, 'edit']); + Route::get('payment-currencies/{id}', [PaymentCurrenciesController::class, 'edit'])->whereNumber('id'); + }); + }); + + // PromotionsController extends BaseCpController (not BaseStoreManagementController) — it + // only ever needed accessPlugin-commerce, not commerce-manageStoreSettings. + Route::get('commerce/promotions', fn() => redirect('commerce/promotions/sales')); + + // OrdersController extends the plain Yii2 Controller (not BaseCpController) — its init() + // only ever checked commerce-manageOrders, not accessPlugin-commerce. + Route::middleware('can:commerce-manageOrders')->group(function () { + Route::get('commerce/orders/{orderId}', [OrdersController::class, 'editOrder'])->whereNumber('orderId'); + Route::get('commerce/orders/{storeHandle}/create', [OrdersController::class, 'create']); + Route::get('commerce/orders/{orderStatusHandle?}', [OrdersController::class, 'orderIndex']); + }); + + // InventoryController checks commerce-manageInventoryStockLevels inline on every action + // (not via init()) — replicated here as a route-group-wide permission instead. + Route::middleware('can:commerce-manageInventoryStockLevels')->group(function () { + Route::get('commerce/inventory/item/{inventoryItemId}', [InventoryController::class, 'itemEdit'])->whereNumber('inventoryItemId'); + Route::get('commerce/inventory/levels/{inventoryLocationHandle}', [InventoryController::class, 'editLocationLevels']); + Route::get('commerce/inventory/levels', [InventoryController::class, 'editLocationLevels']); + Route::get('commerce/inventory', [InventoryController::class, 'editLocationLevels']); + }); + + Route::middleware('can:commerce-manageInventoryLocations')->group(function () { + Route::get('commerce/inventory-locations', [InventoryLocationsController::class, 'index']); + Route::get('commerce/inventory-locations/new', [InventoryLocationsController::class, 'edit']); + Route::get('commerce/inventory-locations/{inventoryLocationId}', [InventoryLocationsController::class, 'edit'])->whereNumber('inventoryLocationId'); + }); + + Route::middleware('can:commerce-manageInventoryTransfers') + ->get('commerce/inventory/transfers', [TransfersController::class, 'index']); +}); + +// The Commerce screen on the Edit User screen — permission is enforced inline via the +// EditUserScreensResolving listener in src/Plugin.php (only shows the tab/registers the +// screen if the viewer can access Commerce), matching every other Edit User screen's `auth`-only +// route-level requirement. +Route::middleware('auth')->group(function () { + Route::get('myaccount/commerce', [UsersController::class, 'index']); + Route::get('users/{userId}/commerce', [UsersController::class, 'index'])->whereNumber('userId'); +}); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000000..4fe445d8e4 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,7 @@ +whereNumber('gatewayId'); diff --git a/src-yii2/Plugin.php b/src-yii2/Plugin.php new file mode 100755 index 0000000000..59bb2c2bcd --- /dev/null +++ b/src-yii2/Plugin.php @@ -0,0 +1,11 @@ +cartCookie; + } + + public function setCartCookie(array $value): void + { + app(\CraftCms\Commerce\Order\Carts::class)->cartCookie = $value; + } + + /** + * @see \CraftCms\Commerce\Order\Carts::$cartCookieDuration + */ + public function getCartCookieDuration(): int + { + return app(\CraftCms\Commerce\Order\Carts::class)->cartCookieDuration; + } + + public function setCartCookieDuration(int $value): void + { + app(\CraftCms\Commerce\Order\Carts::class)->cartCookieDuration = $value; + } + + public function getCart(bool $forceSave = false): Order + { + return app(\CraftCms\Commerce\Order\Carts::class)->getCart($forceSave); + } + + public function peekCart(): ?Order + { + return app(\CraftCms\Commerce\Order\Carts::class)->peekCart(); + } + + public function forgetCart(): void + { + app(\CraftCms\Commerce\Order\Carts::class)->forgetCart(); + } + + public function generateCartNumber(): string + { + return app(\CraftCms\Commerce\Order\Carts::class)->generateCartNumber(); + } + + public function getActiveCartEdgeDuration(): string + { + return app(\CraftCms\Commerce\Order\Carts::class)->getActiveCartEdgeDuration(); + } + + public function getHasSessionCartNumber(): bool + { + return app(\CraftCms\Commerce\Order\Carts::class)->getHasSessionCartNumber(); + } + + public function setSessionCartNumber(string $cartNumber): void + { + app(\CraftCms\Commerce\Order\Carts::class)->setSessionCartNumber($cartNumber); + } + + public function getLoadCartUrl(Order $cart): string + { + return app(\CraftCms\Commerce\Order\Carts::class)->getLoadCartUrl($cart); + } + + public function restorePreviousCartForCurrentUser(): void + { + app(\CraftCms\Commerce\Order\Carts::class)->restorePreviousCartForCurrentUser(); + } + + public function purgeIncompleteCarts(): int + { + return app(\CraftCms\Commerce\Order\Carts::class)->purgeIncompleteCarts(); + } + + public static function registerEvents(): void + { + Event::listen(CartPurgeEvent::class, static function(CartPurgeEvent $event) { + $legacy = Plugin::getInstance()->getCarts(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_PURGE_INACTIVE_CARTS)) { + $legacy->trigger(self::EVENT_BEFORE_PURGE_INACTIVE_CARTS, $event); + } + }); + } +} diff --git a/src-yii2/services/CatalogPricing.php b/src-yii2/services/CatalogPricing.php new file mode 100755 index 0000000000..cfe7b453c5 --- /dev/null +++ b/src-yii2/services/CatalogPricing.php @@ -0,0 +1,84 @@ +generateCatalogPrices($purchasableIds, $catalogPricingRules, $showConsoleOutput ? new ConsoleOutput() : null, $queue); + } + + public function getCatalogPrice(int $purchasableId, ?int $storeId = null, ?int $userId = null, bool $isPromotionalPrice = false): ?float + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricing::class)->getCatalogPrice($purchasableId, $storeId, $userId, $isPromotionalPrice); + } + + public function getCatalogPricesByPurchasableId(int $purchasableId, ?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricing::class)->getCatalogPricesByPurchasableId($purchasableId, $storeId); + } + + public function getCatalogPrices(int $storeId, ?CatalogPricingCondition $conditionBuilder = null, bool $includeBasePrices = true, ?string $searchText = null, ?int $limit = null, ?int $offset = null): Collection + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricing::class)->getCatalogPrices($storeId, $conditionBuilder, $includeBasePrices, $searchText, $limit, $offset); + } + + public function getCatalogPricesPageInfo(int $storeId, ?CatalogPricingCondition $conditionBuilder = null, bool $includeBasePrices = true, ?string $searchText = null, int $limit = 100, int $offset = 0): mixed + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricing::class)->getCatalogPricesPageInfo($storeId, $conditionBuilder, $includeBasePrices, $searchText, $limit, $offset); + } + + public function markPricesAsUpdatePending(int|array|null $catalogPricingRuleId = null, int|array|null $purchasableId = null, int|array|null $storeId = null): void + { + app(\CraftCms\Commerce\CatalogPricing\CatalogPricing::class)->markPricesAsUpdatePending($catalogPricingRuleId, $purchasableId, $storeId); + } + + public function createCatalogPricingJob(array $config = [], int $priority = 100): void + { + app(\CraftCms\Commerce\CatalogPricing\CatalogPricing::class)->createCatalogPricingJob($config, $priority); + } + + public function areCatalogPricingJobsRunning(): bool + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricing::class)->areCatalogPricingJobsRunning(); + } + + public function reserveCatalogPricingQueueRow(): ?CatalogPricingQueueRecord + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricing::class)->reserveCatalogPricingQueueRow(); + } + + public function releaseCatalogPricingQueueRowById(int $id): void + { + app(\CraftCms\Commerce\CatalogPricing\CatalogPricing::class)->releaseCatalogPricingQueueRowById($id); + } + + public function deleteCatalogPricingQueueRowById(int $id): void + { + app(\CraftCms\Commerce\CatalogPricing\CatalogPricing::class)->deleteCatalogPricingQueueRowById($id); + } + + // TODO: return type will differ (Builder vs craft\db\Query) — update callers when migrated + public function createCatalogPricingQuery(?int $userId = null, int|string|null $storeId = null, ?bool $isPromotionalPrice = null, bool $allPrices = false, ?CatalogPricingCondition $condition = null): mixed + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricing::class)->createCatalogPricingQuery($userId, $storeId, $isPromotionalPrice, $allPrices, $condition); + } + + // TODO: return type will differ (Builder vs craft\db\Query) — update callers when migrated + public function createCatalogPricesQuery(?int $userId = null, int|string|null $storeId = null, bool $allPrices = false, ?CatalogPricingCondition $condition = null): mixed + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricing::class)->createCatalogPricesQuery($userId, $storeId, $allPrices, $condition); + } +} diff --git a/src-yii2/services/CatalogPricingRules.php b/src-yii2/services/CatalogPricingRules.php new file mode 100644 index 0000000000..df0587ee84 --- /dev/null +++ b/src-yii2/services/CatalogPricingRules.php @@ -0,0 +1,83 @@ +hasCatalogPricingRules(); + } + + public function canUseCatalogPricingRules(): bool + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricingRules::class)->canUseCatalogPricingRules(); + } + + public function getCatalogPricingRuleById(int $id, ?int $storeId = null): ?CatalogPricingRule + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricingRules::class)->getCatalogPricingRuleById($id, $storeId); + } + + /** + * @return Collection + */ + public function getAllCatalogPricingRules(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricingRules::class)->getAllCatalogPricingRules($storeId); + } + + /** + * @return Collection + */ + public function getAllCatalogPricingRulesByPurchasableId(int $purchasableId, ?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricingRules::class)->getAllCatalogPricingRulesByPurchasableId($purchasableId, $storeId); + } + + /** + * @return Collection + */ + public function getAllEnabledCatalogPricingRules(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricingRules::class)->getAllEnabledCatalogPricingRules($storeId); + } + + /** + * @return Collection + */ + public function getAllActiveCatalogPricingRules(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricingRules::class)->getAllActiveCatalogPricingRules($storeId); + } + + /** + * @return Collection + */ + public function getAllCatalogPricingRulesWithUserConditions(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricingRules::class)->getAllCatalogPricingRulesWithUserConditions($storeId); + } + + public function generateRulePriceFromPrice(?float $basePrice, ?float $basePromotionalPrice, CatalogPricingRule $catalogPricingRule): ?float + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricingRules::class)->generateRulePriceFromPrice($basePrice, $basePromotionalPrice, $catalogPricingRule); + } + + public function saveCatalogPricingRule(CatalogPricingRule $catalogPricingRule, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricingRules::class)->saveCatalogPricingRule($catalogPricingRule, $runValidation); + } + + public function deleteCatalogPricingRuleById(int $id): bool + { + return app(\CraftCms\Commerce\CatalogPricing\CatalogPricingRules::class)->deleteCatalogPricingRuleById($id); + } +} diff --git a/src-yii2/services/Coupons.php b/src-yii2/services/Coupons.php new file mode 100644 index 0000000000..e6fffd1966 --- /dev/null +++ b/src-yii2/services/Coupons.php @@ -0,0 +1,63 @@ +getAllCodes(); + } + + public function getCouponByCode(string $code): ?Coupon + { + return app(\CraftCms\Commerce\Promotion\Coupons::class)->getCouponByCode($code); + } + + /** + * @return Coupon[] + */ + public function getCouponsByDiscountId(int $discountId): array + { + return app(\CraftCms\Commerce\Promotion\Coupons::class)->getCouponsByDiscountId($discountId); + } + + /** + * @param string[] $existingCodes + * @return string[] + * @throws \Exception + */ + public function generateCouponCodes(int $count = 1, string $format = self::DEFAULT_COUPON_FORMAT, array $existingCodes = []): array + { + return app(\CraftCms\Commerce\Promotion\Coupons::class)->generateCouponCodes($count, $format, $existingCodes); + } + + public function deleteCouponById(int $id): bool + { + return app(\CraftCms\Commerce\Promotion\Coupons::class)->deleteCouponById($id); + } + + public function saveDiscountCoupons(Discount $discount): bool + { + return app(\CraftCms\Commerce\Promotion\Coupons::class)->saveDiscountCoupons($discount); + } + + public function saveCoupon(Coupon $coupon, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Promotion\Coupons::class)->saveCoupon($coupon, $runValidation); + } +} diff --git a/src-yii2/services/Currencies.php b/src-yii2/services/Currencies.php new file mode 100644 index 0000000000..034a021b87 --- /dev/null +++ b/src-yii2/services/Currencies.php @@ -0,0 +1,47 @@ +getTeller($currency); + } + + public function getCurrencyByIso(string $iso): ?Currency + { + return app(\CraftCms\Commerce\Payment\Currencies::class)->getCurrencyByIso($iso); + } + + /** + * @return Collection + */ + public function getAllCurrencies(): Collection + { + return app(\CraftCms\Commerce\Payment\Currencies::class)->getAllCurrencies(); + } + + public function getAllCurrenciesList(): array + { + return app(\CraftCms\Commerce\Payment\Currencies::class)->getAllCurrenciesList(); + } + + public function getSubunitFor(Currency|string $currency): int + { + return app(\CraftCms\Commerce\Payment\Currencies::class)->getSubunitFor($currency); + } + + public function numericCodeFor(Currency|string $currency): int + { + return app(\CraftCms\Commerce\Payment\Currencies::class)->numericCodeFor($currency); + } +} diff --git a/src-yii2/services/Customers.php b/src-yii2/services/Customers.php new file mode 100644 index 0000000000..1bd3b2f475 --- /dev/null +++ b/src-yii2/services/Customers.php @@ -0,0 +1,77 @@ +savePrimaryShippingAddressId($user, $addressId); + } + + public function savePrimaryBillingAddressId(User $user, ?int $addressId): bool + { + return app(\CraftCms\Commerce\Customer\Customers::class)->savePrimaryBillingAddressId($user, $addressId); + } + + public function savePrimaryPaymentSourceId(User $user, ?int $paymentSourceId): bool + { + return app(\CraftCms\Commerce\Customer\Customers::class)->savePrimaryPaymentSourceId($user, $paymentSourceId); + } + + public function loginHandler(): void + { + app(\CraftCms\Commerce\Customer\Customers::class)->loginHandler(); + } + + public function orderCompleteHandler(Order $order): void + { + app(\CraftCms\Commerce\Customer\Customers::class)->orderCompleteHandler($order); + } + + /** + * @param Order[] $orders + * @return Order[] + */ + public function eagerLoadCustomerForOrders(array $orders): array + { + return app(\CraftCms\Commerce\Customer\Customers::class)->eagerLoadCustomerForOrders($orders); + } + + public function ensureCustomer(User $user): CustomerRecord + { + return app(\CraftCms\Commerce\Customer\Customers::class)->ensureCustomer($user); + } + + /** + * @throws ElementNotFoundException + */ + public function transferCustomerData(User $fromCustomer, User $toCustomer): bool + { + return app(\CraftCms\Commerce\Customer\Customers::class)->transferCustomerData($fromCustomer, $toCustomer); + } + + public static function registerEvents(): void + { + Event::listen(UpdatePrimaryPaymentSourceEvent::class, static function(UpdatePrimaryPaymentSourceEvent $event) { + $legacy = Plugin::getInstance()->getCustomers(); + if ($legacy->hasEventHandlers(self::EVENT_UPDATE_PRIMARY_PAYMENT_SOURCE)) { + $legacy->trigger(self::EVENT_UPDATE_PRIMARY_PAYMENT_SOURCE, $event); + } + }); + } +} diff --git a/src-yii2/services/Discounts.php b/src-yii2/services/Discounts.php new file mode 100644 index 0000000000..cdf23e402b --- /dev/null +++ b/src-yii2/services/Discounts.php @@ -0,0 +1,121 @@ +getDiscountById($id, $storeId); + } + + /** + * @return Collection + */ + public function getAllDiscounts(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Promotion\Discounts::class)->getAllDiscounts($storeId); + } + + /** + * @return Discount[] + */ + public function getAllActiveDiscounts(?Order $order = null): array + { + return app(\CraftCms\Commerce\Promotion\Discounts::class)->getAllActiveDiscounts($order); + } + + public function orderCouponAvailable(Order $order, ?string &$explanation = null): bool + { + return app(\CraftCms\Commerce\Promotion\Discounts::class)->orderCouponAvailable($order, $explanation); + } + + public function getDiscountByCode(?string $code, ?int $storeId = null): ?Discount + { + return app(\CraftCms\Commerce\Promotion\Discounts::class)->getDiscountByCode($code, $storeId); + } + + /** + * @return Discount[] + */ + public function getDiscountsRelatedToPurchasable(PurchasableInterface $purchasable): array + { + return app(\CraftCms\Commerce\Promotion\Discounts::class)->getDiscountsRelatedToPurchasable($purchasable); + } + + public function matchLineItem(LineItem $lineItem, Discount $discount, bool $matchOrder = false): bool + { + return app(\CraftCms\Commerce\Promotion\Discounts::class)->matchLineItem($lineItem, $discount, $matchOrder); + } + + public function matchOrder(Order $order, Discount $discount): bool + { + return app(\CraftCms\Commerce\Promotion\Discounts::class)->matchOrder($order, $discount); + } + + public function saveDiscount(Discount $model, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Promotion\Discounts::class)->saveDiscount($model, $runValidation); + } + + public function deleteDiscountById(int $id): bool + { + return app(\CraftCms\Commerce\Promotion\Discounts::class)->deleteDiscountById($id); + } + + public function ensureSortOrder(?int $storeId = null): void + { + app(\CraftCms\Commerce\Promotion\Discounts::class)->ensureSortOrder($storeId); + } + + public function clearCustomerUsageHistoryById(int $id): void + { + app(\CraftCms\Commerce\Promotion\Discounts::class)->clearCustomerUsageHistoryById($id); + } + + public function clearEmailUsageHistoryById(int $id): void + { + app(\CraftCms\Commerce\Promotion\Discounts::class)->clearEmailUsageHistoryById($id); + } + + public function clearDiscountUsesById(int $id): void + { + app(\CraftCms\Commerce\Promotion\Discounts::class)->clearDiscountUsesById($id); + } + + public function reorderDiscounts(array $ids): bool + { + return app(\CraftCms\Commerce\Promotion\Discounts::class)->reorderDiscounts($ids); + } + + public function appendCouponCode(int $discountId, string|Coupon $coupon, ?int $maxUses = null): bool + { + return app(\CraftCms\Commerce\Promotion\Discounts::class)->appendCouponCode($discountId, $coupon, $maxUses); + } + + public function getEmailUsageStatsById(int $id): array + { + return app(\CraftCms\Commerce\Promotion\Discounts::class)->getEmailUsageStatsById($id); + } + + public function getCustomerUsageStatsById(int $id): array + { + return app(\CraftCms\Commerce\Promotion\Discounts::class)->getCustomerUsageStatsById($id); + } + + public function orderCompleteHandler(Order $order): void + { + app(\CraftCms\Commerce\Promotion\Discounts::class)->orderCompleteHandler($order); + } +} diff --git a/src-yii2/services/Emails.php b/src-yii2/services/Emails.php new file mode 100644 index 0000000000..9058d54f2f --- /dev/null +++ b/src-yii2/services/Emails.php @@ -0,0 +1,149 @@ +getEmailById($id, $storeId); + } + + /** + * @return Collection + */ + public function getAllEmails(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Email\Emails::class)->getAllEmails($storeId); + } + + /** + * @return Collection + */ + public function getAllEnabledEmails(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Email\Emails::class)->getAllEnabledEmails($storeId); + } + + public function saveEmail(Email $email, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Email\Emails::class)->saveEmail($email, $runValidation); + } + + /** + * @throws Throwable if reasons + */ + public function handleChangedEmail(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Email\Emails::class)->handleChangedEmail(new \CraftCms\Cms\ProjectConfig\Events\ItemUpdated($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + public function deleteEmailById(int $id): bool + { + return app(\CraftCms\Commerce\Email\Emails::class)->deleteEmailById($id); + } + + /** + * @throws Throwable + */ + public function handleDeletedEmail(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Email\Emails::class)->handleDeletedEmail(new \CraftCms\Cms\ProjectConfig\Events\ItemRemoved($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + /** + * @throws Exception + * @throws Throwable + */ + public function sendEmail(Email $email, Order $order, ?OrderHistory $orderHistory = null, ?array $orderData = null, string &$error = ''): bool + { + return app(\CraftCms\Commerce\Email\Emails::class)->sendEmail($email, $order, $orderHistory, $orderData, $error); + } + + /** + * @return Email[] + */ + public function getAllEmailsByOrderStatusId(int $id): array + { + return app(\CraftCms\Commerce\Email\Emails::class)->getAllEmailsByOrderStatusId($id); + } + + public static function registerEvents(): void + { + Event::listen(EmailSaving::class, static function(EmailSaving $event) { + $legacy = Plugin::getInstance()->getEmails(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_SAVE_EMAIL)) { + $legacy->trigger(self::EVENT_BEFORE_SAVE_EMAIL, $event); + } + }); + + Event::listen(EmailSaved::class, static function(EmailSaved $event) { + $legacy = Plugin::getInstance()->getEmails(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_SAVE_EMAIL)) { + $legacy->trigger(self::EVENT_AFTER_SAVE_EMAIL, $event); + } + }); + + Event::listen(EmailDeleting::class, static function(EmailDeleting $event) { + $legacy = Plugin::getInstance()->getEmails(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_DELETE_EMAIL)) { + $legacy->trigger(self::EVENT_BEFORE_DELETE_EMAIL, $event); + } + }); + + Event::listen(EmailDeleted::class, static function(EmailDeleted $event) { + $legacy = Plugin::getInstance()->getEmails(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_DELETE_EMAIL)) { + $legacy->trigger(self::EVENT_AFTER_DELETE_EMAIL, $event); + } + }); + + Event::listen(MailSending::class, static function(MailSending $event) { + $legacy = Plugin::getInstance()->getEmails(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_SEND_MAIL)) { + $legacy->trigger(self::EVENT_BEFORE_SEND_MAIL, $event); + } + }); + + Event::listen(MailSent::class, static function(MailSent $event) { + $legacy = Plugin::getInstance()->getEmails(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_SEND_MAIL)) { + $legacy->trigger(self::EVENT_AFTER_SEND_MAIL, $event); + } + }); + } +} diff --git a/src-yii2/services/Formulas.php b/src-yii2/services/Formulas.php new file mode 100644 index 0000000000..f2e7800209 --- /dev/null +++ b/src-yii2/services/Formulas.php @@ -0,0 +1,41 @@ +validateConditionSyntax($condition, $params); + } + + public function validateFormulaSyntax(string $formula, array $params): bool + { + return app(\CraftCms\Commerce\Formula\Formulas::class)->validateFormulaSyntax($formula, $params); + } + + /** + * @throws SyntaxError + * @throws LoaderError + */ + public function evaluateCondition(string $formula, array $params, string $name = 'Evaluate Condition'): bool + { + return app(\CraftCms\Commerce\Formula\Formulas::class)->evaluateCondition($formula, $params, $name); + } + + /** + * @throws SyntaxError + * @throws LoaderError + */ + public function evaluateFormula(string $formula, array $params, ?string $setType = null, ?string $name = 'Inline formula'): mixed + { + return app(\CraftCms\Commerce\Formula\Formulas::class)->evaluateFormula($formula, $params, $setType, $name); + } +} diff --git a/src-yii2/services/Gateways.php b/src-yii2/services/Gateways.php new file mode 100644 index 0000000000..54038869e1 --- /dev/null +++ b/src-yii2/services/Gateways.php @@ -0,0 +1,118 @@ +register()` instead. */ + public const EVENT_REGISTER_GATEWAY_TYPES = 'registerGatewayTypes'; + + public const CONFIG_GATEWAY_KEY = \CraftCms\Commerce\Payment\Gateway\Gateways::CONFIG_GATEWAY_KEY; + + /** + * @return string[] + */ + public function getAllGatewayTypes(): array + { + return app(\CraftCms\Commerce\Payment\Gateway\Gateways::class)->getAllGatewayTypes(); + } + + /** + * @return Collection + */ + public function getAllCustomerEnabledGateways(): Collection + { + return app(\CraftCms\Commerce\Payment\Gateway\Gateways::class)->getAllCustomerEnabledGateways(); + } + + /** + * @return Collection + */ + public function getAllCustomerEnabledGatewaysAndAvailableForUseWithOrder(Order $order): Collection + { + return app(\CraftCms\Commerce\Payment\Gateway\Gateways::class)->getAllCustomerEnabledGatewaysAndAvailableForUseWithOrder($order); + } + + /** + * @return Collection + */ + public function getAllGateways(): Collection + { + return app(\CraftCms\Commerce\Payment\Gateway\Gateways::class)->getAllGateways(); + } + + /** + * @return Gateway[] + */ + public function getAllArchivedGateways(): array + { + return app(\CraftCms\Commerce\Payment\Gateway\Gateways::class)->getAllArchivedGateways(); + } + + public function archiveGatewayById(int $id): bool + { + return app(\CraftCms\Commerce\Payment\Gateway\Gateways::class)->archiveGatewayById($id); + } + + public function getGatewayById(int $id): ?Gateway + { + return app(\CraftCms\Commerce\Payment\Gateway\Gateways::class)->getGatewayById($id); + } + + public function getGatewayByHandle(string $handle): ?Gateway + { + return app(\CraftCms\Commerce\Payment\Gateway\Gateways::class)->getGatewayByHandle($handle); + } + + public function saveGateway(Gateway $gateway, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Payment\Gateway\Gateways::class)->saveGateway($gateway, $runValidation); + } + + /** + * @throws Throwable if reasons + */ + public function handleChangedGateway(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Payment\Gateway\Gateways::class)->handleChangedGateway(new \CraftCms\Cms\ProjectConfig\Events\ItemUpdated($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + /** + * @throws Throwable if reasons + */ + public function handleArchivedGateway(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Payment\Gateway\Gateways::class)->handleArchivedGateway(new \CraftCms\Cms\ProjectConfig\Events\ItemRemoved($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + /** + * @param int[] $ids + */ + public function reorderGateways(array $ids): bool + { + return app(\CraftCms\Commerce\Payment\Gateway\Gateways::class)->reorderGateways($ids); + } + + public function createGateway(string|array $config): Gateway + { + return app(\CraftCms\Commerce\Payment\Gateway\Gateways::class)->createGateway($config); + } + + /** @internal */ + public static function finalizeRegistrationEvents(): void + { + TypeRegistryCompatibility::reconcile(app(GatewayTypes::class), \craft\commerce\Plugin::getInstance()->getGateways(), self::EVENT_REGISTER_GATEWAY_TYPES); + } +} diff --git a/src-yii2/services/Inventory.php b/src-yii2/services/Inventory.php new file mode 100644 index 0000000000..da2a5bea30 --- /dev/null +++ b/src-yii2/services/Inventory.php @@ -0,0 +1,176 @@ + + */ + public function getInventoryLevelsForPurchasable(Purchasable|NewPurchasable $purchasable): Collection + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->getInventoryLevelsForPurchasable($purchasable); + } + + public function getInventoryItemByPurchasable(Purchasable|NewPurchasable $purchasable): InventoryItem + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->getInventoryItemByPurchasable($purchasable); + } + + public function ensureInventoryItemRecord(Purchasable|NewPurchasable $purchasable): ?InventoryItemRecord + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->ensureInventoryItemRecord($purchasable); + } + + public function getInventoryItemById(int $id): InventoryItem + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->getInventoryItemById($id); + } + + /** + * @param array $ids + * @return Collection + */ + public function getInventoryItemsByIds(array $ids): Collection + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->getInventoryItemsByIds($ids); + } + + public function getInventoryLevel(InventoryItem|int $inventoryItem, InventoryLocation|int $inventoryLocation, bool $withTrashed = false): ?InventoryLevel + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->getInventoryLevel($inventoryItem, $inventoryLocation, $withTrashed); + } + + public function saveInventoryItem(InventoryItem $inventoryItem, bool $validate = true): bool + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->saveInventoryItem($inventoryItem); + } + + /** + * @return Collection + */ + public function getInventoryLocationLevels(InventoryLocation $inventoryLocation, bool $withTrashed = false): Collection + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->getInventoryLocationLevels($inventoryLocation, $withTrashed); + } + + public function getInventoryLevelQuery(?int $limit = null, ?int $offset = null, bool $withTrashed = false): \Illuminate\Database\Query\Builder + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->getInventoryLevelQuery($limit, $offset, $withTrashed); + } + + public function getInventoryItemQuery(): \Illuminate\Database\Query\Builder + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->getInventoryItemQuery(); + } + + public function executeUpdateInventoryLevels(UpdateInventoryLevelCollection $updateInventoryLevels): bool + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->executeUpdateInventoryLevels($updateInventoryLevels); + } + + /** + * @param array $updateInventoryLevelAttributes + */ + public function updateInventoryLevel(int $inventoryItemId, int $quantity, array $updateInventoryLevelAttributes = []): void + { + app(\CraftCms\Commerce\Inventory\Inventory::class)->updateInventoryLevel($inventoryItemId, $quantity, $updateInventoryLevelAttributes); + } + + /** + * @param array $updateInventoryLevelAttributes + */ + public function updatePurchasableInventoryLevel(Purchasable|NewPurchasable $purchasable, int $quantity, array $updateInventoryLevelAttributes = []): void + { + app(\CraftCms\Commerce\Inventory\Inventory::class)->updatePurchasableInventoryLevel($purchasable, $quantity, $updateInventoryLevelAttributes); + } + + public function executeInventoryMovements(InventoryMovementCollection $inventoryMovements): bool + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->executeInventoryMovements($inventoryMovements); + } + + public function getMovementHash(): string + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->getMovementHash(); + } + + public function getUnfulfilledOrders(InventoryItem|int $inventoryItem, InventoryLocation|int $inventoryLocation): array + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->getUnfulfilledOrders($inventoryItem, $inventoryLocation); + } + + public function getTransactionQuery(): \Illuminate\Database\Query\Builder + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->getTransactionQuery(); + } + + /** + * @return Collection + */ + public function getInventoryTransactions(InventoryItem $inventoryItem, InventoryLocation $inventoryLocation): Collection + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->getInventoryTransactions($inventoryItem, $inventoryLocation); + } + + /** + * @return Collection + */ + public function getInventoryFulfillmentLevels(Order $order): Collection + { + return app(\CraftCms\Commerce\Inventory\Inventory::class)->getInventoryFulfillmentLevels($order); + } + + public function orderCompleteHandler(Order $order): void + { + app(\CraftCms\Commerce\Inventory\Inventory::class)->orderCompleteHandler($order); + } + + public static function registerEvents(): void + { + Event::listen(UpdateInventoryLevelEvent::class, static function(UpdateInventoryLevelEvent $event) { + $legacy = Plugin::getInstance()->getInventory(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_EXECUTE_UPDATE_INVENTORY_LEVEL)) { + $legacy->trigger(self::EVENT_AFTER_EXECUTE_UPDATE_INVENTORY_LEVEL, $event); + } + }); + + Event::listen(InventoryMovementEvent::class, static function(InventoryMovementEvent $event) { + $legacy = Plugin::getInstance()->getInventory(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_EXECUTE_INVENTORY_MOVEMENT)) { + $legacy->trigger(self::EVENT_AFTER_EXECUTE_INVENTORY_MOVEMENT, $event); + } + }); + } +} diff --git a/src-yii2/services/InventoryLocations.php b/src-yii2/services/InventoryLocations.php new file mode 100644 index 0000000000..f327b247bc --- /dev/null +++ b/src-yii2/services/InventoryLocations.php @@ -0,0 +1,61 @@ + + */ + public function getAllInventoryLocations(bool $withTrashed = false): Collection + { + return app(\CraftCms\Commerce\Inventory\InventoryLocations::class)->getAllInventoryLocations($withTrashed); + } + + public function getAllInventoryLocationsAsList(bool $withTrashed = false): array + { + return app(\CraftCms\Commerce\Inventory\InventoryLocations::class)->getAllInventoryLocationsAsList($withTrashed); + } + + public function getInventoryLocationById(int $id, bool $withTrashed = false): ?InventoryLocation + { + return app(\CraftCms\Commerce\Inventory\InventoryLocations::class)->getInventoryLocationById($id, $withTrashed); + } + + /** + * @return Collection + */ + public function getInventoryLocations(?int $storeId = null, bool $withTrashed = false): Collection + { + return app(\CraftCms\Commerce\Inventory\InventoryLocations::class)->getInventoryLocations($storeId, $withTrashed); + } + + public function saveStoreInventoryLocations(Store $store, array $inventoryLocationIds): bool + { + return app(\CraftCms\Commerce\Inventory\InventoryLocations::class)->saveStoreInventoryLocations($store, $inventoryLocationIds); + } + + public function executeDeactivateInventoryLocation(DeactivateInventoryLocation $deactivateInventoryLocation): bool + { + return app(\CraftCms\Commerce\Inventory\InventoryLocations::class)->executeDeactivateInventoryLocation($deactivateInventoryLocation); + } + + public function getInventoryLocationByHandle(string $handle): ?InventoryLocation + { + return app(\CraftCms\Commerce\Inventory\InventoryLocations::class)->getInventoryLocationByHandle($handle); + } + + public function saveInventoryLocation(InventoryLocation $inventoryLocation, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Inventory\InventoryLocations::class)->saveInventoryLocation($inventoryLocation, $runValidation); + } +} diff --git a/src-yii2/services/LineItemStatuses.php b/src-yii2/services/LineItemStatuses.php new file mode 100644 index 0000000000..55e5691809 --- /dev/null +++ b/src-yii2/services/LineItemStatuses.php @@ -0,0 +1,104 @@ +getLineItemStatusByHandle($handle, $storeId); + } + + public function getDefaultLineItemStatusId(?int $storeId = null): ?int + { + return app(\CraftCms\Commerce\Order\LineItemStatuses::class)->getDefaultLineItemStatusId($storeId); + } + + public function getDefaultLineItemStatus(?int $storeId = null): ?LineItemStatus + { + return app(\CraftCms\Commerce\Order\LineItemStatuses::class)->getDefaultLineItemStatus($storeId); + } + + public function getDefaultLineItemStatusForLineItem(LineItem $lineItem): ?LineItemStatus + { + return app(\CraftCms\Commerce\Order\LineItemStatuses::class)->getDefaultLineItemStatusForLineItem($lineItem); + } + + public function saveLineItemStatus(LineItemStatus $lineItemStatus, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Order\LineItemStatuses::class)->saveLineItemStatus($lineItemStatus, $runValidation); + } + + /** + * @throws Throwable if reasons + */ + public function handleChangedLineItemStatus(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Order\LineItemStatuses::class)->handleChangedLineItemStatus(new \CraftCms\Cms\ProjectConfig\Events\ItemUpdated($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + /** + * @throws Throwable + */ + public function archiveLineItemStatusById(int $id, ?int $storeId = null): bool + { + return app(\CraftCms\Commerce\Order\LineItemStatuses::class)->archiveLineItemStatusById($id, $storeId); + } + + /** + * @throws Throwable if reasons + */ + public function handleArchivedLineItemStatus(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Order\LineItemStatuses::class)->handleArchivedLineItemStatus(new \CraftCms\Cms\ProjectConfig\Events\ItemRemoved($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + /** + * @return Collection + */ + public function getAllLineItemStatuses(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Order\LineItemStatuses::class)->getAllLineItemStatuses($storeId); + } + + public function getLineItemStatusById(int $id, ?int $storeId = null): ?LineItemStatus + { + return app(\CraftCms\Commerce\Order\LineItemStatuses::class)->getLineItemStatusById($id, $storeId); + } + + /** + * @param int[] $ids + */ + public function reorderLineItemStatuses(array $ids): bool + { + return app(\CraftCms\Commerce\Order\LineItemStatuses::class)->reorderLineItemStatuses($ids); + } + + public static function registerEvents(): void + { + Event::listen(DefaultLineItemStatusEvent::class, static function(DefaultLineItemStatusEvent $event) { + $legacy = Plugin::getInstance()->getLineItemStatuses(); + if ($legacy->hasEventHandlers(self::EVENT_DEFAULT_LINE_ITEM_STATUS)) { + $legacy->trigger(self::EVENT_DEFAULT_LINE_ITEM_STATUS, $event); + } + }); + } +} diff --git a/src-yii2/services/LineItems.php b/src-yii2/services/LineItems.php new file mode 100644 index 0000000000..2c08f9012f --- /dev/null +++ b/src-yii2/services/LineItems.php @@ -0,0 +1,112 @@ +getAllLineItemsByOrderId($orderId); + } + + public function resolveLineItem(Order $order, int $purchasableId, array $options = [], array $params = []): LineItem + { + return app(\CraftCms\Commerce\Order\LineItem\LineItems::class)->resolveLineItem($order, $purchasableId, $options, $params); + } + + public function resolveCustomLineItem(Order $order, string $sku, array $options = []): LineItem + { + return app(\CraftCms\Commerce\Order\LineItem\LineItems::class)->resolveCustomLineItem($order, $sku, $options); + } + + public function saveLineItem(LineItem $lineItem, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Order\LineItem\LineItems::class)->saveLineItem($lineItem, $runValidation); + } + + public function getLineItemById(int $id): ?LineItem + { + return app(\CraftCms\Commerce\Order\LineItem\LineItems::class)->getLineItemById($id); + } + + public function create(Order $order, array $params = [], LineItemType $type = LineItemType::Purchasable): LineItem + { + return app(\CraftCms\Commerce\Order\LineItem\LineItems::class)->create($order, $params, $type); + } + + public function deleteAllLineItemsByOrderId(int $orderId): bool + { + return app(\CraftCms\Commerce\Order\LineItem\LineItems::class)->deleteAllLineItemsByOrderId($orderId); + } + + /** + * @param Order[] $orders + * @return Order[] + */ + public function eagerLoadLineItemsForOrders(array $orders): array + { + return app(\CraftCms\Commerce\Order\LineItem\LineItems::class)->eagerLoadLineItemsForOrders($orders); + } + + public function orderCompleteHandler(LineItem $lineItem, Order $order): void + { + app(\CraftCms\Commerce\Order\LineItem\LineItems::class)->orderCompleteHandler($lineItem, $order); + } + + public static function registerEvents(): void + { + Event::listen(LineItemSaving::class, static function(LineItemSaving $event) { + $legacy = Plugin::getInstance()->getLineItems(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_SAVE_LINE_ITEM)) { + $legacy->trigger(self::EVENT_BEFORE_SAVE_LINE_ITEM, $event); + } + }); + + Event::listen(LineItemSaved::class, static function(LineItemSaved $event) { + $legacy = Plugin::getInstance()->getLineItems(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_SAVE_LINE_ITEM)) { + $legacy->trigger(self::EVENT_AFTER_SAVE_LINE_ITEM, $event); + } + }); + + Event::listen(LineItemCreated::class, static function(LineItemCreated $event) { + $legacy = Plugin::getInstance()->getLineItems(); + if ($legacy->hasEventHandlers(self::EVENT_CREATE_LINE_ITEM)) { + $legacy->trigger(self::EVENT_CREATE_LINE_ITEM, $event); + } + }); + + Event::listen(LineItemPopulated::class, static function(LineItemPopulated $event) { + $legacy = Plugin::getInstance()->getLineItems(); + if ($legacy->hasEventHandlers(self::EVENT_POPULATE_LINE_ITEM)) { + $legacy->trigger(self::EVENT_POPULATE_LINE_ITEM, $event); + } + }); + } +} diff --git a/src-yii2/services/OrderAdjustments.php b/src-yii2/services/OrderAdjustments.php new file mode 100644 index 0000000000..3779671d8b --- /dev/null +++ b/src-yii2/services/OrderAdjustments.php @@ -0,0 +1,85 @@ +register()` instead. */ + public const EVENT_REGISTER_ORDER_ADJUSTERS = 'registerOrderAdjusters'; + + /** @deprecated in 6.0.0. Use `app(\CraftCms\Commerce\Order\Adjuster\DiscountAdjusterTypes::class)->register()` instead. */ + public const EVENT_REGISTER_DISCOUNT_ADJUSTERS = 'registerDiscountAdjusters'; + + /** + * @return class-string[] + */ + public function getAdjusters(): array + { + return app(\CraftCms\Commerce\Order\OrderAdjustments::class)->getAdjusters(); + } + + public function getOrderAdjustmentById(int $id): ?OrderAdjustment + { + return app(\CraftCms\Commerce\Order\OrderAdjustments::class)->getOrderAdjustmentById($id); + } + + /** + * @return OrderAdjustment[] + */ + public function getAllOrderAdjustmentsByOrderId(int $orderId): array + { + return app(\CraftCms\Commerce\Order\OrderAdjustments::class)->getAllOrderAdjustmentsByOrderId($orderId); + } + + public function saveOrderAdjustment(OrderAdjustment $orderAdjustment, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Order\OrderAdjustments::class)->saveOrderAdjustment($orderAdjustment, $runValidation); + } + + public function deleteAllOrderAdjustmentsByOrderId(int $orderId): bool + { + return app(\CraftCms\Commerce\Order\OrderAdjustments::class)->deleteAllOrderAdjustmentsByOrderId($orderId); + } + + public function deleteOrderAdjustmentByAdjustmentId(int $adjustmentId): bool + { + return app(\CraftCms\Commerce\Order\OrderAdjustments::class)->deleteOrderAdjustmentByAdjustmentId($adjustmentId); + } + + /** + * @param Order[] $orders + * @return Order[] + */ + public function eagerLoadOrderAdjustmentsForOrders(array $orders): array + { + return app(\CraftCms\Commerce\Order\OrderAdjustments::class)->eagerLoadOrderAdjustmentsForOrders($orders); + } + + /** + * @return class-string[] + */ + public function getDiscountAdjusters(): array + { + return app(\CraftCms\Commerce\Order\OrderAdjustments::class)->getDiscountAdjusters(); + } + + /** @internal */ + public static function finalizeRegistrationEvents(): void + { + $plugin = \craft\commerce\Plugin::getInstance()->getOrderAdjustments(); + + TypeRegistryCompatibility::reconcile(app(AdjusterTypes::class), $plugin, self::EVENT_REGISTER_ORDER_ADJUSTERS); + TypeRegistryCompatibility::reconcile(app(DiscountAdjusterTypes::class), $plugin, self::EVENT_REGISTER_DISCOUNT_ADJUSTERS); + } +} diff --git a/src-yii2/services/OrderHistories.php b/src-yii2/services/OrderHistories.php new file mode 100644 index 0000000000..ac1c7f9318 --- /dev/null +++ b/src-yii2/services/OrderHistories.php @@ -0,0 +1,57 @@ +getOrderHistoryById($id); + } + + /** + * @return OrderHistory[] + */ + public function getAllOrderHistoriesByOrderId(int $id): array + { + return app(\CraftCms\Commerce\Order\OrderHistories::class)->getAllOrderHistoriesByOrderId($id); + } + + public function createOrderHistoryFromOrder(Order $order, ?int $oldStatusId): bool + { + return app(\CraftCms\Commerce\Order\OrderHistories::class)->createOrderHistoryFromOrder($order, $oldStatusId); + } + + public function saveOrderHistory(OrderHistory $model, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Order\OrderHistories::class)->saveOrderHistory($model, $runValidation); + } + + public function deleteOrderHistoryById(int $id): bool + { + return app(\CraftCms\Commerce\Order\OrderHistories::class)->deleteOrderHistoryById($id); + } + + public static function registerEvents(): void + { + Event::listen(OrderStatusEvent::class, static function(OrderStatusEvent $event) { + $legacy = Plugin::getInstance()->getOrderHistories(); + if ($legacy->hasEventHandlers(self::EVENT_ORDER_STATUS_CHANGE)) { + $legacy->trigger(self::EVENT_ORDER_STATUS_CHANGE, $event); + } + }); + } +} diff --git a/src-yii2/services/OrderNotices.php b/src-yii2/services/OrderNotices.php new file mode 100644 index 0000000000..a7ad388e35 --- /dev/null +++ b/src-yii2/services/OrderNotices.php @@ -0,0 +1,23 @@ +eagerLoadOrderNoticesForOrders($orders); + } +} diff --git a/src-yii2/services/OrderStatuses.php b/src-yii2/services/OrderStatuses.php new file mode 100644 index 0000000000..fba6624e5b --- /dev/null +++ b/src-yii2/services/OrderStatuses.php @@ -0,0 +1,136 @@ + + */ + public function getAllOrderStatuses(?int $storeId = null, bool $withTrashed = false): Collection + { + return app(\CraftCms\Commerce\Order\OrderStatuses::class)->getAllOrderStatuses($storeId, $withTrashed); + } + + public function getOrderStatusById(int $id, ?int $storeId = null): ?OrderStatus + { + return app(\CraftCms\Commerce\Order\OrderStatuses::class)->getOrderStatusById($id, $storeId); + } + + public function getOrderStatusByUid(string $uid, ?int $storeId = null): ?OrderStatus + { + return app(\CraftCms\Commerce\Order\OrderStatuses::class)->getOrderStatusByUid($uid, $storeId); + } + + public function getOrderStatusByHandle(string $handle, ?int $storeId = null): ?OrderStatus + { + return app(\CraftCms\Commerce\Order\OrderStatuses::class)->getOrderStatusByHandle($handle, $storeId); + } + + public function getDefaultOrderStatus(?int $storeId = null): ?OrderStatus + { + return app(\CraftCms\Commerce\Order\OrderStatuses::class)->getDefaultOrderStatus($storeId); + } + + public function getDefaultOrderStatusId(?int $storeId = null): ?int + { + return app(\CraftCms\Commerce\Order\OrderStatuses::class)->getDefaultOrderStatusId($storeId); + } + + public function getDefaultOrderStatusForOrder(Order $order): ?OrderStatus + { + return app(\CraftCms\Commerce\Order\OrderStatuses::class)->getDefaultOrderStatusForOrder($order); + } + + public function getOrderCountByStatus(?int $storeId = null): array + { + return app(\CraftCms\Commerce\Order\OrderStatuses::class)->getOrderCountByStatus($storeId); + } + + public function saveOrderStatus(OrderStatus $orderStatus, array $emailIds = [], bool $runValidation = true, bool $force = false): bool + { + return app(\CraftCms\Commerce\Order\OrderStatuses::class)->saveOrderStatus($orderStatus, $emailIds, $runValidation, $force); + } + + /** + * @throws Throwable if reasons + */ + public function handleChangedOrderStatus(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Order\OrderStatuses::class)->handleChangedOrderStatus(new \CraftCms\Cms\ProjectConfig\Events\ItemUpdated($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + /** + * @throws Throwable + */ + public function deleteOrderStatusById(int $id, ?int $storeId = null): bool + { + return app(\CraftCms\Commerce\Order\OrderStatuses::class)->deleteOrderStatusById($id, $storeId); + } + + /** + * @throws Throwable if reasons + */ + public function handleDeletedOrderStatus(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Order\OrderStatuses::class)->handleDeletedOrderStatus(new \CraftCms\Cms\ProjectConfig\Events\ItemRemoved($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + public function pruneDeletedEmail(EmailEvent $event): void + { + app(\CraftCms\Commerce\Order\OrderStatuses::class)->pruneDeletedEmail($event); + } + + public function statusChangeHandler(Order $order, OrderHistory $orderHistory): void + { + app(\CraftCms\Commerce\Order\OrderStatuses::class)->statusChangeHandler($order, $orderHistory); + } + + /** + * @param int[] $ids + */ + public function reorderOrderStatuses(array $ids): bool + { + return app(\CraftCms\Commerce\Order\OrderStatuses::class)->reorderOrderStatuses($ids); + } + + public static function registerEvents(): void + { + Event::listen(DefaultOrderStatusEvent::class, static function(DefaultOrderStatusEvent $event) { + $legacy = Plugin::getInstance()->getOrderStatuses(); + if ($legacy->hasEventHandlers(self::EVENT_DEFAULT_ORDER_STATUS)) { + $legacy->trigger(self::EVENT_DEFAULT_ORDER_STATUS, $event); + } + }); + + Event::listen(OrderStatusEmailsEvent::class, static function(OrderStatusEmailsEvent $event) { + $legacy = Plugin::getInstance()->getOrderStatuses(); + if ($legacy->hasEventHandlers(self::EVENT_ORDER_STATUS_CHANGE_EMAILS)) { + $legacy->trigger(self::EVENT_ORDER_STATUS_CHANGE_EMAILS, $event); + } + }); + } +} diff --git a/src-yii2/services/Orders.php b/src-yii2/services/Orders.php new file mode 100644 index 0000000000..ef5eec165c --- /dev/null +++ b/src-yii2/services/Orders.php @@ -0,0 +1,77 @@ +handleChangedFieldLayout(new \CraftCms\Cms\ProjectConfig\Events\ItemUpdated($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + public function handleDeletedFieldLayout(): void + { + app(\CraftCms\Commerce\Order\Orders::class)->handleDeletedFieldLayout(); + } + + public function getOrderById(int $id): ?Order + { + return app(\CraftCms\Commerce\Order\Orders::class)->getOrderById($id); + } + + public function getOrderByNumber(string $number): ?Order + { + return app(\CraftCms\Commerce\Order\Orders::class)->getOrderByNumber($number); + } + + /** + * @return Order[]|null + */ + public function getOrdersByCustomer(User|int $customer): ?array + { + return app(\CraftCms\Commerce\Order\Orders::class)->getOrdersByCustomer($customer); + } + + /** + * @return Order[]|null + */ + public function getOrdersByEmail(string $email): ?array + { + return app(\CraftCms\Commerce\Order\Orders::class)->getOrdersByEmail($email); + } + + /** + * @param Order[] $orders + * @return Order[] + */ + public function eagerLoadAddressesForOrders(array $orders): array + { + return app(\CraftCms\Commerce\Order\Orders::class)->eagerLoadAddressesForOrders($orders); + } + + /** + * @param int|int[] $oldUserId + */ + public function reassignOrders(int|array $oldUserId, int $newUserId): int + { + return app(\CraftCms\Commerce\Order\Orders::class)->reassignOrders($oldUserId, $newUserId); + } + + /** + * @param int|int[] $orderIds + */ + public function removeCustomerData(int|array $orderIds, array $dataToRemove = ['customerId', 'email']): int + { + return app(\CraftCms\Commerce\Order\Orders::class)->removeCustomerData($orderIds, $dataToRemove); + } +} diff --git a/src-yii2/services/PaymentCurrencies.php b/src-yii2/services/PaymentCurrencies.php new file mode 100644 index 0000000000..93f5406531 --- /dev/null +++ b/src-yii2/services/PaymentCurrencies.php @@ -0,0 +1,104 @@ +getRateFor($currency, $transaction); + } + + public function getPaymentCurrencyById(int $id, ?int $storeId = null): ?PaymentCurrency + { + return app(\CraftCms\Commerce\Payment\PaymentCurrencies::class)->getPaymentCurrencyById($id, $storeId); + } + + /** + * @return Collection + */ + public function getAllPaymentCurrencies(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Payment\PaymentCurrencies::class)->getAllPaymentCurrencies($storeId); + } + + public function getPaymentCurrencyByIso(string $iso, ?int $storeId = null): ?PaymentCurrency + { + return app(\CraftCms\Commerce\Payment\PaymentCurrencies::class)->getPaymentCurrencyByIso($iso, $storeId); + } + + public function getPrimaryPaymentCurrencyIso(?int $storeId = null): string + { + return app(\CraftCms\Commerce\Payment\PaymentCurrencies::class)->getPrimaryPaymentCurrencyIso($storeId); + } + + public function getPrimaryPaymentCurrency(?int $storeId = null): ?PaymentCurrency + { + return app(\CraftCms\Commerce\Payment\PaymentCurrencies::class)->getPrimaryPaymentCurrency($storeId); + } + + /** + * @return Collection + */ + public function getNonPrimaryPaymentCurrencies(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Payment\PaymentCurrencies::class)->getNonPrimaryPaymentCurrencies($storeId); + } + + public function convert(float $amount, string $currency): float + { + return app(\CraftCms\Commerce\Payment\PaymentCurrencies::class)->convert($amount, $currency); + } + + /** + * @deprecated 6.0.0 use convertAmount() or convert() instead. + */ + public function convertCurrency(float $amount, string $fromCurrency, string $toCurrency, bool $round = false): float + { + return app(\CraftCms\Commerce\Payment\PaymentCurrencies::class)->convertCurrency($amount, $fromCurrency, $toCurrency, $round); + } + + public function savePaymentCurrency(PaymentCurrency $model, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Payment\PaymentCurrencies::class)->savePaymentCurrency($model, $runValidation); + } + + public function deletePaymentCurrencyById(int $id): bool + { + return app(\CraftCms\Commerce\Payment\PaymentCurrencies::class)->deletePaymentCurrencyById($id); + } + + public function convertAmount(Money $amount, Currency|string $currency, ?int $storeId = null): Money + { + return app(\CraftCms\Commerce\Payment\PaymentCurrencies::class)->convertAmount($amount, $currency, $storeId); + } + + public static function registerEvents(): void + { + Event::listen(PaymentCurrencyRateEvent::class, static function(PaymentCurrencyRateEvent $event) { + $legacy = Plugin::getInstance()->getPaymentCurrencies(); + if ($legacy->hasEventHandlers(self::EVENT_DEFINE_PAYMENT_CURRENCY_RATE)) { + $legacy->trigger(self::EVENT_DEFINE_PAYMENT_CURRENCY_RATE, $event); + } + }); + } +} diff --git a/src-yii2/services/PaymentSources.php b/src-yii2/services/PaymentSources.php new file mode 100644 index 0000000000..2f6b189f55 --- /dev/null +++ b/src-yii2/services/PaymentSources.php @@ -0,0 +1,105 @@ + + */ + public function getAllPaymentSourcesByCustomerId(?int $customerId = null, ?int $gatewayId = null): Collection + { + return app(\CraftCms\Commerce\Payment\PaymentSources::class)->getAllPaymentSourcesByCustomerId($customerId, $gatewayId); + } + + /** + * @return Collection + */ + public function getAllPaymentSourcesByGatewayId(?int $gatewayId = null): Collection + { + return app(\CraftCms\Commerce\Payment\PaymentSources::class)->getAllPaymentSourcesByGatewayId($gatewayId); + } + + /** + * @return Collection + */ + public function getAllGatewayPaymentSourcesByCustomerId(?int $gatewayId = null, ?int $customerId = null): Collection + { + return app(\CraftCms\Commerce\Payment\PaymentSources::class)->getAllGatewayPaymentSourcesByCustomerId($gatewayId, $customerId); + } + + public function getPaymentSourceByTokenAndGatewayId(string $token, int $gatewayId): ?PaymentSource + { + return app(\CraftCms\Commerce\Payment\PaymentSources::class)->getPaymentSourceByTokenAndGatewayId($token, $gatewayId); + } + + public function getPaymentSourceById(int $sourceId): ?PaymentSource + { + return app(\CraftCms\Commerce\Payment\PaymentSources::class)->getPaymentSourceById($sourceId); + } + + public function getPaymentSourceByIdAndUserId(int $sourceId, int $userId): ?PaymentSource + { + return app(\CraftCms\Commerce\Payment\PaymentSources::class)->getPaymentSourceByIdAndUserId($sourceId, $userId); + } + + public function createPaymentSource(int $customerId, GatewayInterface $gateway, BasePaymentForm $paymentForm, ?string $sourceDescription = null, bool $makePrimarySource = false): PaymentSource + { + return app(\CraftCms\Commerce\Payment\PaymentSources::class)->createPaymentSource($customerId, $gateway, $paymentForm, $sourceDescription, $makePrimarySource); + } + + public function savePaymentSource(PaymentSource $paymentSource, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Payment\PaymentSources::class)->savePaymentSource($paymentSource, $runValidation); + } + + public function deletePaymentSourceById(int $id): bool + { + return app(\CraftCms\Commerce\Payment\PaymentSources::class)->deletePaymentSourceById($id); + } + + public static function registerEvents(): void + { + Event::listen(PaymentSourceSaving::class, static function(PaymentSourceSaving $event) { + $legacy = Plugin::getInstance()->getPaymentSources(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_SAVE_PAYMENT_SOURCE)) { + $legacy->trigger(self::EVENT_BEFORE_SAVE_PAYMENT_SOURCE, $event); + } + }); + + Event::listen(PaymentSourceSaved::class, static function(PaymentSourceSaved $event) { + $legacy = Plugin::getInstance()->getPaymentSources(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_SAVE_PAYMENT_SOURCE)) { + $legacy->trigger(self::EVENT_AFTER_SAVE_PAYMENT_SOURCE, $event); + } + }); + + Event::listen(PaymentSourceDeleting::class, static function(PaymentSourceDeleting $event) { + $legacy = Plugin::getInstance()->getPaymentSources(); + if ($legacy->hasEventHandlers(self::EVENT_DELETE_PAYMENT_SOURCE)) { + $legacy->trigger(self::EVENT_DELETE_PAYMENT_SOURCE, $event); + } + }); + } +} diff --git a/src-yii2/services/Payments.php b/src-yii2/services/Payments.php new file mode 100644 index 0000000000..a2665690bd --- /dev/null +++ b/src-yii2/services/Payments.php @@ -0,0 +1,125 @@ +processPayment($order, $form, $redirect, $transaction, $redirectData); + } + + /** + * @throws TransactionException if something went wrong when saving the transaction + */ + public function captureTransaction(Transaction $transaction): Transaction + { + return app(\CraftCms\Commerce\Payment\Payments::class)->captureTransaction($transaction); + } + + /** + * @throws RefundException if something went wrong during the refund. + */ + public function refundTransaction(Transaction $transaction, ?float $amount = null, string $note = ''): Transaction + { + return app(\CraftCms\Commerce\Payment\Payments::class)->refundTransaction($transaction, $amount, $note); + } + + public function completePayment(Transaction $transaction, ?string &$customError): bool + { + return app(\CraftCms\Commerce\Payment\Payments::class)->completePayment($transaction, $customError); + } + + public static function registerEvents(): void + { + Event::listen(PaymentProcessing::class, static function(PaymentProcessing $event) { + $legacy = Plugin::getInstance()->getPayments(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_PROCESS_PAYMENT)) { + $legacy->trigger(self::EVENT_BEFORE_PROCESS_PAYMENT, $event); + } + }); + + Event::listen(PaymentProcessed::class, static function(PaymentProcessed $event) { + $legacy = Plugin::getInstance()->getPayments(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_PROCESS_PAYMENT)) { + $legacy->trigger(self::EVENT_AFTER_PROCESS_PAYMENT, $event); + } + }); + + Event::listen(TransactionCapturing::class, static function(TransactionCapturing $event) { + $legacy = Plugin::getInstance()->getPayments(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_CAPTURE_TRANSACTION)) { + $legacy->trigger(self::EVENT_BEFORE_CAPTURE_TRANSACTION, $event); + } + }); + + Event::listen(TransactionCaptured::class, static function(TransactionCaptured $event) { + $legacy = Plugin::getInstance()->getPayments(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_CAPTURE_TRANSACTION)) { + $legacy->trigger(self::EVENT_AFTER_CAPTURE_TRANSACTION, $event); + } + }); + + Event::listen(TransactionRefunding::class, static function(TransactionRefunding $event) { + $legacy = Plugin::getInstance()->getPayments(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_REFUND_TRANSACTION)) { + $legacy->trigger(self::EVENT_BEFORE_REFUND_TRANSACTION, $event); + } + }); + + Event::listen(TransactionRefunded::class, static function(TransactionRefunded $event) { + $legacy = Plugin::getInstance()->getPayments(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_REFUND_TRANSACTION)) { + $legacy->trigger(self::EVENT_AFTER_REFUND_TRANSACTION, $event); + } + }); + + Event::listen(PaymentCompleted::class, static function(PaymentCompleted $event) { + $legacy = Plugin::getInstance()->getPayments(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_COMPLETE_PAYMENT)) { + $legacy->trigger(self::EVENT_AFTER_COMPLETE_PAYMENT, $event); + } + }); + } +} diff --git a/src-yii2/services/Pdfs.php b/src-yii2/services/Pdfs.php new file mode 100644 index 0000000000..fb0c7b3c30 --- /dev/null +++ b/src-yii2/services/Pdfs.php @@ -0,0 +1,161 @@ + + */ + public function getAllPdfs(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Pdf\Pdfs::class)->getAllPdfs($storeId); + } + + public function getHasEnabledPdf(?int $storeId = null): bool + { + return app(\CraftCms\Commerce\Pdf\Pdfs::class)->getHasEnabledPdf($storeId); + } + + /** + * @return Collection + */ + public function getAllEnabledPdfs(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Pdf\Pdfs::class)->getAllEnabledPdfs($storeId); + } + + public function getDefaultPdf(?int $storeId = null): ?Pdf + { + return app(\CraftCms\Commerce\Pdf\Pdfs::class)->getDefaultPdf($storeId); + } + + public function getPdfByHandle(string $handle, ?int $storeId = null): ?Pdf + { + return app(\CraftCms\Commerce\Pdf\Pdfs::class)->getPdfByHandle($handle, $storeId); + } + + public function getPdfById(int $id, ?int $storeId = null): ?Pdf + { + return app(\CraftCms\Commerce\Pdf\Pdfs::class)->getPdfById($id, $storeId); + } + + public function savePdf(Pdf $pdf, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Pdf\Pdfs::class)->savePdf($pdf, $runValidation); + } + + public function handleChangedPdf(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Pdf\Pdfs::class)->handleChangedPdf(new \CraftCms\Cms\ProjectConfig\Events\ItemUpdated($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + public function deletePdfById(int $id): bool + { + return app(\CraftCms\Commerce\Pdf\Pdfs::class)->deletePdfById($id); + } + + /** + * @throws Throwable + */ + public function handleDeletedPdf(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Pdf\Pdfs::class)->handleDeletedPdf(new \CraftCms\Cms\ProjectConfig\Events\ItemRemoved($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + /** + * @param int[] $ids + */ + public function reorderPdfs(array $ids): bool + { + return app(\CraftCms\Commerce\Pdf\Pdfs::class)->reorderPdfs($ids); + } + + public function getPdfUrl(Order $order, ?string $option = null, ?string $pdfHandle = null, bool $inline = false): string + { + return app(\CraftCms\Commerce\Pdf\Pdfs::class)->getPdfUrl($order, $option, $pdfHandle, $inline); + } + + public function renderPdfForOrder(Order $order, string $option = '', ?string $templatePath = null, array $variables = [], ?Pdf $pdf = null): string + { + return app(\CraftCms\Commerce\Pdf\Pdfs::class)->renderPdfForOrder($order, $option, $templatePath, $variables, $pdf); + } + + public static function registerEvents(): void + { + Event::listen(PdfSaving::class, static function(PdfSaving $event) { + $legacy = Plugin::getInstance()->getPdfs(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_SAVE_PDF)) { + $legacy->trigger(self::EVENT_BEFORE_SAVE_PDF, $event); + } + }); + + Event::listen(PdfSaved::class, static function(PdfSaved $event) { + $legacy = Plugin::getInstance()->getPdfs(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_SAVE_PDF)) { + $legacy->trigger(self::EVENT_AFTER_SAVE_PDF, $event); + } + }); + + Event::listen(PdfDeleting::class, static function(PdfDeleting $event) { + $legacy = Plugin::getInstance()->getPdfs(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_DELETE_PDF)) { + $legacy->trigger(self::EVENT_BEFORE_DELETE_PDF, $event); + } + }); + + Event::listen(PdfRendering::class, static function(PdfRendering $event) { + $legacy = Plugin::getInstance()->getPdfs(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_RENDER_PDF)) { + $legacy->trigger(self::EVENT_BEFORE_RENDER_PDF, $event); + } + }); + + Event::listen(PdfRendered::class, static function(PdfRendered $event) { + $legacy = Plugin::getInstance()->getPdfs(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_RENDER_PDF)) { + $legacy->trigger(self::EVENT_AFTER_RENDER_PDF, $event); + } + }); + + Event::listen(PdfRenderOptionsEvent::class, static function(PdfRenderOptionsEvent $event) { + $legacy = Plugin::getInstance()->getPdfs(); + if ($legacy->hasEventHandlers(self::EVENT_MODIFY_RENDER_OPTIONS)) { + $legacy->trigger(self::EVENT_MODIFY_RENDER_OPTIONS, $event); + } + }); + } +} diff --git a/src-yii2/services/ProductTypes.php b/src-yii2/services/ProductTypes.php new file mode 100755 index 0000000000..93b8dab565 --- /dev/null +++ b/src-yii2/services/ProductTypes.php @@ -0,0 +1,151 @@ +getViewableProductTypes(); + } + + public function getViewableProductTypeIds(bool $anySite = false): array + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->getViewableProductTypeIds($anySite); + } + + public function getCreatableProductTypeIds(): array + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->getCreatableProductTypeIds(); + } + + /** + * @return ProductType[] + */ + public function getCreatableProductTypes(): array + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->getCreatableProductTypes(); + } + + public function getAllProductTypeIds(): array + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->getAllProductTypeIds(); + } + + /** + * @return ProductType[] + */ + public function getAllProductTypes(): array + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->getAllProductTypes(); + } + + public function getProductTypeByHandle(string $handle): ?ProductType + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->getProductTypeByHandle($handle); + } + + public function getProductTypeById(int $productTypeId): ?ProductType + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->getProductTypeById($productTypeId); + } + + public function getProductTypeByUid(string $uid): ?ProductType + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->getProductTypeByUid($uid); + } + + /** + * @return ProductType[] + */ + public function getProductTypesByTaxCategoryId(int $taxCategoryId): array + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->getProductTypesByTaxCategoryId($taxCategoryId); + } + + /** + * @return ProductType[] + */ + public function getProductTypesByShippingCategoryId(int $shippingCategoryId): array + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->getProductTypesByShippingCategoryId($shippingCategoryId); + } + + /** + * @return ProductTypeSite[] + */ + public function getProductTypeSites(int $productTypeId): array + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->getProductTypeSites($productTypeId); + } + + public function saveProductType(ProductType $productType, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->saveProductType($productType, $runValidation); + } + + public function handleChangedProductType(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->handleChangedProductType(new \CraftCms\Cms\ProjectConfig\Events\ItemUpdated($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + public function deleteProductTypeById(int $id): bool + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->deleteProductTypeById($id); + } + + public function handleDeletedProductType(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->handleDeletedProductType(new \CraftCms\Cms\ProjectConfig\Events\ItemRemoved($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + public function pruneDeletedSite(DeleteSiteEvent $event): void + { + app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->pruneDeletedSite(new \CraftCms\Cms\Site\Events\SiteDeleted(site: $event->site)); + } + + public function isProductTypeTemplateValid(ProductType $productType, int $siteId): bool + { + return app(\CraftCms\Commerce\Product\ProductType\ProductTypes::class)->isProductTypeTemplateValid($productType, $siteId); + } + + public static function registerEvents(): void + { + Event::listen(ProductTypeSaving::class, static function(ProductTypeSaving $event) { + $legacy = Plugin::getInstance()->getProductTypes(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_SAVE_PRODUCTTYPE)) { + $legacy->trigger(self::EVENT_BEFORE_SAVE_PRODUCTTYPE, $event); + } + }); + + Event::listen(ProductTypeSaved::class, static function(ProductTypeSaved $event) { + $legacy = Plugin::getInstance()->getProductTypes(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_SAVE_PRODUCTTYPE)) { + $legacy->trigger(self::EVENT_AFTER_SAVE_PRODUCTTYPE, $event); + } + }); + } +} diff --git a/src-yii2/services/Products.php b/src-yii2/services/Products.php new file mode 100644 index 0000000000..d9d5d4ceef --- /dev/null +++ b/src-yii2/services/Products.php @@ -0,0 +1,20 @@ +getProductById($id, $siteId, $criteria); + } +} diff --git a/src-yii2/services/Purchasables.php b/src-yii2/services/Purchasables.php new file mode 100644 index 0000000000..a0d9ffdd37 --- /dev/null +++ b/src-yii2/services/Purchasables.php @@ -0,0 +1,106 @@ +register()` instead. */ + public const EVENT_REGISTER_PURCHASABLE_ELEMENT_TYPES = 'registerPurchasableElementTypes'; + + /** + * @throws Throwable + */ + public function isPurchasableOutOfStockPurchasingAllowed(PurchasableInterface $purchasable, ?Order $order = null, ?User $currentUser = null): bool + { + return app(\CraftCms\Commerce\Purchasable\Purchasables::class)->isPurchasableOutOfStockPurchasingAllowed($purchasable, $order, $currentUser); + } + + public function isPurchasableAvailable(PurchasableInterface $purchasable, ?Order $order = null, ?User $currentUser = null): bool + { + return app(\CraftCms\Commerce\Purchasable\Purchasables::class)->isPurchasableAvailable($purchasable, $order, $currentUser); + } + + public function isPurchasableShippable(PurchasableInterface $purchasable, ?Order $order = null, ?User $currentUser = null): bool + { + return app(\CraftCms\Commerce\Purchasable\Purchasables::class)->isPurchasableShippable($purchasable, $order, $currentUser); + } + + public function updateStoreStockCache(PurchasableInterface $purchasable, bool $allSites = false): void + { + app(\CraftCms\Commerce\Purchasable\Purchasables::class)->updateStoreStockCache($purchasable, $allSites); + } + + /** + * @throws Throwable + */ + public function deletePurchasableById(int $purchasableId): bool + { + return app(\CraftCms\Commerce\Purchasable\Purchasables::class)->deletePurchasableById($purchasableId); + } + + public function getPurchasableById(int $purchasableId, ?int $siteId = null, int|false|null $forCustomer = null): ?PurchasableInterface + { + return app(\CraftCms\Commerce\Purchasable\Purchasables::class)->getPurchasableById($purchasableId, $siteId, $forCustomer); + } + + /** + * @return string[] + */ + public function getAllPurchasableElementTypes(): array + { + return app(\CraftCms\Commerce\Purchasable\Purchasables::class)->getAllPurchasableElementTypes(); + } + + /** @internal */ + public static function finalizeRegistrationEvents(): void + { + TypeRegistryCompatibility::reconcile(app(PurchasableTypes::class), \craft\commerce\Plugin::getInstance()->getPurchasables(), self::EVENT_REGISTER_PURCHASABLE_ELEMENT_TYPES); + } + + public static function registerEvents(): void + { + Event::listen(PurchasableOutOfStockPurchasesAllowedEvent::class, static function(PurchasableOutOfStockPurchasesAllowedEvent $event) { + $legacy = Plugin::getInstance()->getPurchasables(); + if ($legacy->hasEventHandlers(self::EVENT_PURCHASABLE_OUT_OF_STOCK_PURCHASES_ALLOWED)) { + $legacy->trigger(self::EVENT_PURCHASABLE_OUT_OF_STOCK_PURCHASES_ALLOWED, $event); + } + }); + + Event::listen(PurchasableAvailableEvent::class, static function(PurchasableAvailableEvent $event) { + $legacy = Plugin::getInstance()->getPurchasables(); + if ($legacy->hasEventHandlers(self::EVENT_PURCHASABLE_AVAILABLE)) { + $legacy->trigger(self::EVENT_PURCHASABLE_AVAILABLE, $event); + } + }); + + Event::listen(PurchasableShippableEvent::class, static function(PurchasableShippableEvent $event) { + $legacy = Plugin::getInstance()->getPurchasables(); + if ($legacy->hasEventHandlers(self::EVENT_PURCHASABLE_SHIPPABLE)) { + $legacy->trigger(self::EVENT_PURCHASABLE_SHIPPABLE, $event); + } + }); + } +} diff --git a/src-yii2/services/Sales.php b/src-yii2/services/Sales.php new file mode 100644 index 0000000000..41da0b0d6d --- /dev/null +++ b/src-yii2/services/Sales.php @@ -0,0 +1,73 @@ +canUseSales(); + } + + public function getSaleById(int $id): ?Sale + { + return app(\CraftCms\Commerce\Promotion\Sales::class)->getSaleById($id); + } + + /** + * @return Sale[] + */ + public function getAllSales(): array + { + return app(\CraftCms\Commerce\Promotion\Sales::class)->getAllSales(); + } + + /** + * @return Sale[] + */ + public function getSalesForPurchasable(PurchasableInterface $purchasable, ?Order $order = null): array + { + return app(\CraftCms\Commerce\Promotion\Sales::class)->getSalesForPurchasable($purchasable, $order); + } + + /** + * @return Sale[] + */ + public function getSalesRelatedToPurchasable(PurchasableInterface $purchasable): array + { + return app(\CraftCms\Commerce\Promotion\Sales::class)->getSalesRelatedToPurchasable($purchasable); + } + + public function getSalePriceForPurchasable(PurchasableInterface $purchasable, ?Order $order = null): float + { + return app(\CraftCms\Commerce\Promotion\Sales::class)->getSalePriceForPurchasable($purchasable, $order); + } + + public function matchPurchasableAndSale(PurchasableInterface $purchasable, Sale $sale, ?Order $order = null): bool + { + return app(\CraftCms\Commerce\Promotion\Sales::class)->matchPurchasableAndSale($purchasable, $sale, $order); + } + + public function saveSale(Sale $model, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Promotion\Sales::class)->saveSale($model, $runValidation); + } + + public function reorderSales(array $ids): bool + { + return app(\CraftCms\Commerce\Promotion\Sales::class)->reorderSales($ids); + } + + public function deleteSaleById(int $id): bool + { + return app(\CraftCms\Commerce\Promotion\Sales::class)->deleteSaleById($id); + } +} diff --git a/src-yii2/services/ShippingCategories.php b/src-yii2/services/ShippingCategories.php new file mode 100644 index 0000000000..5992bd0988 --- /dev/null +++ b/src-yii2/services/ShippingCategories.php @@ -0,0 +1,67 @@ + + */ + public function getAllShippingCategories(?int $storeId = null, bool $withTrashed = false): Collection + { + return app(\CraftCms\Commerce\Shipping\ShippingCategories::class)->getAllShippingCategories($storeId, $withTrashed); + } + + /** + * @return array + */ + public function getAllShippingCategoriesAsList(?int $storeId = null): array + { + return app(\CraftCms\Commerce\Shipping\ShippingCategories::class)->getAllShippingCategoriesAsList($storeId); + } + + public function getShippingCategoryById(int $shippingCategoryId, ?int $storeId = null): ?ShippingCategory + { + return app(\CraftCms\Commerce\Shipping\ShippingCategories::class)->getShippingCategoryById($shippingCategoryId, $storeId); + } + + public function getShippingCategoryByHandle(string $shippingCategoryHandle, ?int $storeId = null): ?ShippingCategory + { + return app(\CraftCms\Commerce\Shipping\ShippingCategories::class)->getShippingCategoryByHandle($shippingCategoryHandle, $storeId); + } + + public function getDefaultShippingCategory(int $storeId): ShippingCategory + { + return app(\CraftCms\Commerce\Shipping\ShippingCategories::class)->getDefaultShippingCategory($storeId); + } + + public function saveShippingCategory(ShippingCategory $shippingCategory, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Shipping\ShippingCategories::class)->saveShippingCategory($shippingCategory, $runValidation); + } + + public function deleteShippingCategoryById(int $id): bool + { + return app(\CraftCms\Commerce\Shipping\ShippingCategories::class)->deleteShippingCategoryById($id); + } + + /** + * @return array + */ + public function getShippingCategoriesByProductTypeId(int $productTypeId): array + { + return app(\CraftCms\Commerce\Shipping\ShippingCategories::class)->getShippingCategoriesByProductTypeId($productTypeId); + } + + public function clearCaches(): void + { + app(\CraftCms\Commerce\Shipping\ShippingCategories::class)->clearCaches(); + } +} diff --git a/src-yii2/services/ShippingMethods.php b/src-yii2/services/ShippingMethods.php new file mode 100644 index 0000000000..ea695901db --- /dev/null +++ b/src-yii2/services/ShippingMethods.php @@ -0,0 +1,83 @@ + + */ + public function getAllShippingMethods(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Shipping\ShippingMethods::class)->getAllShippingMethods($storeId); + } + + public function getShippingMethodByHandle(string $handle, ?int $storeId = null): ?ShippingMethod + { + return app(\CraftCms\Commerce\Shipping\ShippingMethods::class)->getShippingMethodByHandle($handle, $storeId); + } + + public function getShippingMethodById(int $id, ?int $storeId = null): ?ShippingMethod + { + return app(\CraftCms\Commerce\Shipping\ShippingMethods::class)->getShippingMethodById($id, $storeId); + } + + /** + * @return array + */ + public function getMatchingShippingMethods(Order $order): array + { + return app(\CraftCms\Commerce\Shipping\ShippingMethods::class)->getMatchingShippingMethods($order); + } + + public function getSerializedOrderForMatchingRules(Order $order): array + { + return app(\CraftCms\Commerce\Shipping\ShippingMethods::class)->getSerializedOrderForMatchingRules($order); + } + + public function getMatchingShippingRule(Order $order, ShippingMethodInterface $method): ?ShippingRuleInterface + { + return app(\CraftCms\Commerce\Shipping\ShippingMethods::class)->getMatchingShippingRule($order, $method); + } + + public function saveShippingMethod(ShippingMethod $model, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Shipping\ShippingMethods::class)->saveShippingMethod($model, $runValidation); + } + + public function deleteShippingMethodById(int $id): bool + { + return app(\CraftCms\Commerce\Shipping\ShippingMethods::class)->deleteShippingMethodById($id); + } + + public function clearCache(): void + { + app(\CraftCms\Commerce\Shipping\ShippingMethods::class)->clearCache(); + } + + public static function registerEvents(): void + { + Event::listen(RegisterAvailableShippingMethodsEvent::class, static function(RegisterAvailableShippingMethodsEvent $event) { + $legacy = Plugin::getInstance()->getShippingMethods(); + if ($legacy->hasEventHandlers(self::EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS)) { + $legacy->trigger(self::EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS, $event); + } + }); + } +} diff --git a/src-yii2/services/ShippingRuleCategories.php b/src-yii2/services/ShippingRuleCategories.php new file mode 100644 index 0000000000..5428a43cab --- /dev/null +++ b/src-yii2/services/ShippingRuleCategories.php @@ -0,0 +1,39 @@ + + */ + public function getShippingRuleCategoriesByRuleId(int $ruleId): array + { + return app(\CraftCms\Commerce\Shipping\ShippingRuleCategories::class)->getShippingRuleCategoriesByRuleId($ruleId); + } + + /** + * @param int[] $ruleIds + * @return array> + */ + public function getShippingRuleCategoriesByRuleIds(array $ruleIds): array + { + return app(\CraftCms\Commerce\Shipping\ShippingRuleCategories::class)->getShippingRuleCategoriesByRuleIds($ruleIds); + } + + public function createShippingRuleCategory(ShippingRuleCategory $model, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Shipping\ShippingRuleCategories::class)->createShippingRuleCategory($model, $runValidation); + } + + public function deleteShippingRuleCategoryById(int $id): bool + { + return app(\CraftCms\Commerce\Shipping\ShippingRuleCategories::class)->deleteShippingRuleCategoryById($id); + } +} diff --git a/src-yii2/services/ShippingRules.php b/src-yii2/services/ShippingRules.php new file mode 100644 index 0000000000..c0cc395e56 --- /dev/null +++ b/src-yii2/services/ShippingRules.php @@ -0,0 +1,49 @@ + + */ + public function getAllShippingRules(): Collection + { + return app(\CraftCms\Commerce\Shipping\ShippingRules::class)->getAllShippingRules(); + } + + /** + * @return Collection + */ + public function getAllShippingRulesByShippingMethodId(int $methodId): Collection + { + return app(\CraftCms\Commerce\Shipping\ShippingRules::class)->getAllShippingRulesByShippingMethodId($methodId); + } + + public function getShippingRuleById(int $id): ?ShippingRule + { + return app(\CraftCms\Commerce\Shipping\ShippingRules::class)->getShippingRuleById($id); + } + + public function saveShippingRule(ShippingRule $model, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Shipping\ShippingRules::class)->saveShippingRule($model, $runValidation); + } + + public function reorderShippingRules(array $ids): bool + { + return app(\CraftCms\Commerce\Shipping\ShippingRules::class)->reorderShippingRules($ids); + } + + public function deleteShippingRuleById(int $id): bool + { + return app(\CraftCms\Commerce\Shipping\ShippingRules::class)->deleteShippingRuleById($id); + } +} diff --git a/src-yii2/services/ShippingZones.php b/src-yii2/services/ShippingZones.php new file mode 100644 index 0000000000..13e941ab03 --- /dev/null +++ b/src-yii2/services/ShippingZones.php @@ -0,0 +1,36 @@ + + */ + public function getAllShippingZones(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Shipping\ShippingZones::class)->getAllShippingZones($storeId); + } + + public function getShippingZoneById(int $id, ?int $storeId = null): ?ShippingAddressZone + { + return app(\CraftCms\Commerce\Shipping\ShippingZones::class)->getShippingZoneById($id, $storeId); + } + + public function saveShippingZone(ShippingAddressZone $model, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Shipping\ShippingZones::class)->saveShippingZone($model, $runValidation); + } + + public function deleteShippingZoneById(int $id): bool + { + return app(\CraftCms\Commerce\Shipping\ShippingZones::class)->deleteShippingZoneById($id); + } +} diff --git a/src-yii2/services/StoreSettings.php b/src-yii2/services/StoreSettings.php new file mode 100644 index 0000000000..995dbefca1 --- /dev/null +++ b/src-yii2/services/StoreSettings.php @@ -0,0 +1,35 @@ +getStoreSettingsById($id); + } + + /** + * @return Collection + */ + public function getAllStoreSettings(): Collection + { + return app(\CraftCms\Commerce\Store\StoreSettings::class)->getAllStoreSettings(); + } + + /** + * @throws InvalidConfigException + */ + public function saveStoreSettings(StoreSettingsModel $storeSettings): bool + { + return app(\CraftCms\Commerce\Store\StoreSettings::class)->saveStoreSettings($storeSettings); + } +} diff --git a/src-yii2/services/Stores.php b/src-yii2/services/Stores.php new file mode 100644 index 0000000000..7dece3c472 --- /dev/null +++ b/src-yii2/services/Stores.php @@ -0,0 +1,219 @@ +getCurrentStore(); + } + + /** + * @return Collection + */ + public function getAllStores(): Collection + { + return app(\CraftCms\Commerce\Store\Stores::class)->getAllStores(); + } + + public function getStoreById(int $id): ?Store + { + return app(\CraftCms\Commerce\Store\Stores::class)->getStoreById($id); + } + + public function getStoreByUid(string $uid): ?Store + { + return app(\CraftCms\Commerce\Store\Stores::class)->getStoreByUid($uid); + } + + public function getStoreBySiteId(int $siteId): ?Store + { + return app(\CraftCms\Commerce\Store\Stores::class)->getStoreBySiteId($siteId); + } + + public function getStoreByHandle(string $handle): ?Store + { + return app(\CraftCms\Commerce\Store\Stores::class)->getStoreByHandle($handle); + } + + /** + * @return Collection + */ + public function getStoresByUserId(int $userId): Collection + { + return app(\CraftCms\Commerce\Store\Stores::class)->getStoresByUserId($userId); + } + + public function saveStore(Store $store, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Store\Stores::class)->saveStore($store, $runValidation); + } + + public function deleteStoreById(int $storeId): bool + { + return app(\CraftCms\Commerce\Store\Stores::class)->deleteStoreById($storeId); + } + + public function deleteStore(Store $store): bool + { + return app(\CraftCms\Commerce\Store\Stores::class)->deleteStore($store); + } + + /** + * @throws Throwable + */ + public function handleChangedStore(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Store\Stores::class)->handleChangedStore(new \CraftCms\Cms\ProjectConfig\Events\ItemUpdated($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + /** + * @throws Throwable + */ + public function handleDeletedStore(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Store\Stores::class)->handleDeletedStore(new \CraftCms\Cms\ProjectConfig\Events\ItemRemoved($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + public function refreshStores(): void + { + app(\CraftCms\Commerce\Store\Stores::class)->refreshStores(); + } + + public function getPrimaryStore(): ?Store + { + return app(\CraftCms\Commerce\Store\Stores::class)->getPrimaryStore(); + } + + /** + * @param int[] $ids + */ + public function reorderStores(array $ids): bool + { + return app(\CraftCms\Commerce\Store\Stores::class)->reorderStores($ids); + } + + /** + * @return Collection + */ + public function getAllSitesForStore(Store $store): Collection + { + return app(\CraftCms\Commerce\Store\Stores::class)->getAllSitesForStore($store); + } + + /** + * @return Collection + */ + public function getAllSiteStores(): Collection + { + return app(\CraftCms\Commerce\Store\Stores::class)->getAllSiteStores(); + } + + public function getSiteIdsAvailableForAssignmentToNewStores(): array + { + return app(\CraftCms\Commerce\Store\Stores::class)->getSiteIdsAvailableForAssignmentToNewStores(); + } + + /** + * @throws Throwable + */ + public function saveSiteStore(SiteStore $siteStore, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Store\Stores::class)->saveSiteStore($siteStore, $runValidation); + } + + /** + * @throws Throwable + */ + public function handleChangedSiteStore(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Store\Stores::class)->handleChangedSiteStore(new \CraftCms\Cms\ProjectConfig\Events\ItemUpdated($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + /** + * @throws Throwable + */ + public function handleDeletedSiteStore(ConfigEvent $event): void + { + app(\CraftCms\Commerce\Store\Stores::class)->handleDeletedSiteStore(new \CraftCms\Cms\ProjectConfig\Events\ItemRemoved($event->path, $event->oldValue, $event->newValue, $event->tokenMatches)); + } + + + public static function registerEvents(): void + { + Event::listen(StoreSaving::class, static function(StoreSaving $event) { + $legacy = Plugin::getInstance()->getStores(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_SAVE_STORE)) { + $legacy->trigger(self::EVENT_BEFORE_SAVE_STORE, $event); + } + }); + + Event::listen(StoreSaved::class, static function(StoreSaved $event) { + $legacy = Plugin::getInstance()->getStores(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_SAVE_STORE)) { + $legacy->trigger(self::EVENT_AFTER_SAVE_STORE, $event); + } + }); + + Event::listen(StoreDeleting::class, static function(StoreDeleting $event) { + $legacy = Plugin::getInstance()->getStores(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_DELETE_STORE)) { + $legacy->trigger(self::EVENT_BEFORE_DELETE_STORE, $event); + } + }); + + Event::listen(StoreDeleteApplying::class, static function(StoreDeleteApplying $event) { + $legacy = Plugin::getInstance()->getStores(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_APPLY_STORE_DELETE)) { + $legacy->trigger(self::EVENT_BEFORE_APPLY_STORE_DELETE, $event); + } + }); + + Event::listen(StoreDeleted::class, static function(StoreDeleted $event) { + $legacy = Plugin::getInstance()->getStores(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_DELETE_STORE)) { + $legacy->trigger(self::EVENT_AFTER_DELETE_STORE, $event); + } + }); + } +} diff --git a/src-yii2/services/TaxCategories.php b/src-yii2/services/TaxCategories.php new file mode 100644 index 0000000000..948278b333 --- /dev/null +++ b/src-yii2/services/TaxCategories.php @@ -0,0 +1,61 @@ +getAllTaxCategories($withTrashed); + } + + public function getTaxCategoryById(int $taxCategoryId): ?TaxCategory + { + return app(\CraftCms\Commerce\Tax\TaxCategories::class)->getTaxCategoryById($taxCategoryId); + } + + public function getTaxCategoryByHandle(string $taxCategoryHandle): ?TaxCategory + { + return app(\CraftCms\Commerce\Tax\TaxCategories::class)->getTaxCategoryByHandle($taxCategoryHandle); + } + + /** + * @return array + */ + public function getAllTaxCategoriesAsList(): array + { + return app(\CraftCms\Commerce\Tax\TaxCategories::class)->getAllTaxCategoriesAsList(); + } + + public function getDefaultTaxCategory(): TaxCategory + { + return app(\CraftCms\Commerce\Tax\TaxCategories::class)->getDefaultTaxCategory(); + } + + public function saveTaxCategory(TaxCategory $taxCategory, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Tax\TaxCategories::class)->saveTaxCategory($taxCategory, $runValidation); + } + + public function deleteTaxCategoryById(int $id): bool + { + return app(\CraftCms\Commerce\Tax\TaxCategories::class)->deleteTaxCategoryById($id); + } + + /** + * @return array + */ + public function getTaxCategoriesByProductTypeId(int $productTypeId): array + { + return app(\CraftCms\Commerce\Tax\TaxCategories::class)->getTaxCategoriesByProductTypeId($productTypeId); + } +} diff --git a/src-yii2/services/TaxRates.php b/src-yii2/services/TaxRates.php new file mode 100644 index 0000000000..4c071027d2 --- /dev/null +++ b/src-yii2/services/TaxRates.php @@ -0,0 +1,52 @@ + + */ + public function getAllTaxRates(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Tax\TaxRates::class)->getAllTaxRates($storeId); + } + + /** + * @return Collection + */ + public function getAllEnabledTaxRates(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Tax\TaxRates::class)->getAllEnabledTaxRates($storeId); + } + + /** + * @return Collection + */ + public function getTaxRatesByTaxZoneId(int $taxZoneId, ?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Tax\TaxRates::class)->getTaxRatesByTaxZoneId($taxZoneId, $storeId); + } + + public function getTaxRateById(int $id, ?int $storeId = null): ?TaxRate + { + return app(\CraftCms\Commerce\Tax\TaxRates::class)->getTaxRateById($id, $storeId); + } + + public function saveTaxRate(TaxRate $model, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Tax\TaxRates::class)->saveTaxRate($model, $runValidation); + } + + public function deleteTaxRateById(int $id): bool + { + return app(\CraftCms\Commerce\Tax\TaxRates::class)->deleteTaxRateById($id); + } +} diff --git a/src-yii2/services/TaxZones.php b/src-yii2/services/TaxZones.php new file mode 100644 index 0000000000..3d07a26157 --- /dev/null +++ b/src-yii2/services/TaxZones.php @@ -0,0 +1,36 @@ + + */ + public function getAllTaxZones(?int $storeId = null): Collection + { + return app(\CraftCms\Commerce\Tax\TaxZones::class)->getAllTaxZones($storeId); + } + + public function getTaxZoneById(int $id, ?int $storeId = null): ?TaxAddressZone + { + return app(\CraftCms\Commerce\Tax\TaxZones::class)->getTaxZoneById($id, $storeId); + } + + public function saveTaxZone(TaxAddressZone $model, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Tax\TaxZones::class)->saveTaxZone($model, $runValidation); + } + + public function deleteTaxZoneById(int $id): bool + { + return app(\CraftCms\Commerce\Tax\TaxZones::class)->deleteTaxZoneById($id); + } +} diff --git a/src-yii2/services/Taxes.php b/src-yii2/services/Taxes.php new file mode 100644 index 0000000000..c8a7640fd0 --- /dev/null +++ b/src-yii2/services/Taxes.php @@ -0,0 +1,153 @@ + + */ + public function getTaxIdValidators(): Collection + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->getTaxIdValidators(); + } + + /** + * @return Collection + */ + public function getEnabledTaxIdValidators(): Collection + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->getEnabledTaxIdValidators(); + } + + public function getEngine(): NewTaxEngineInterface + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->getEngine(); + } + + public function taxAdjusterClass(): string + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->taxAdjusterClass(); + } + + public function viewTaxCategories(): bool + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->viewTaxCategories(); + } + + public function createTaxCategories(): bool + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->createTaxCategories(); + } + + public function editTaxCategories(): bool + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->editTaxCategories(); + } + + public function deleteTaxCategories(): bool + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->deleteTaxCategories(); + } + + public function taxCategoryActionHtml(): string + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->taxCategoryActionHtml(); + } + + public function viewTaxZones(): bool + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->viewTaxZones(); + } + + public function editTaxZones(): bool + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->editTaxZones(); + } + + public function viewTaxRates(): bool + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->viewTaxRates(); + } + + public function editTaxRates(): bool + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->editTaxRates(); + } + + public function cpTaxNavSubItems(): array + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->cpTaxNavSubItems(); + } + + public function createTaxZones(): bool + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->createTaxZones(); + } + + public function deleteTaxZones(): bool + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->deleteTaxZones(); + } + + public function taxZoneActionHtml(): string + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->taxZoneActionHtml(); + } + + public function createTaxRates(): bool + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->createTaxRates(); + } + + public function deleteTaxRates(): bool + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->deleteTaxRates(); + } + + public function taxRateActionHtml(): string + { + return app(\CraftCms\Commerce\Tax\Taxes::class)->taxRateActionHtml(); + } + + public static function registerEvents(): void + { + Event::listen(TaxIdValidatorsEvent::class, static function(TaxIdValidatorsEvent $event) { + $legacy = Plugin::getInstance()->getTaxes(); + if ($legacy->hasEventHandlers(self::EVENT_REGISTER_TAX_ID_VALIDATORS)) { + $legacy->trigger(self::EVENT_REGISTER_TAX_ID_VALIDATORS, $event); + } + }); + + Event::listen(TaxEngineEvent::class, static function(TaxEngineEvent $event) { + $legacy = Plugin::getInstance()->getTaxes(); + if ($legacy->hasEventHandlers(self::EVENT_REGISTER_TAX_ENGINE)) { + $legacy->trigger(self::EVENT_REGISTER_TAX_ENGINE, $event); + } + }); + } +} diff --git a/src-yii2/services/Transactions.php b/src-yii2/services/Transactions.php new file mode 100644 index 0000000000..f8b414735b --- /dev/null +++ b/src-yii2/services/Transactions.php @@ -0,0 +1,127 @@ +canCaptureTransaction($transaction); + } + + public function canRefundTransaction(Transaction $transaction): bool + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->canRefundTransaction($transaction); + } + + public function refundableAmountForTransaction(Transaction $transaction): float + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->refundableAmountForTransaction($transaction); + } + + public function createTransaction(?Order $order = null, ?Transaction $parentTransaction = null, ?string $typeOverride = null): Transaction + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->createTransaction($order, $parentTransaction, $typeOverride); + } + + public function deleteTransactionById(int $id): bool + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->deleteTransactionById($id); + } + + /** + * @return Transaction[] + */ + public function getAllTopLevelTransactionsByOrderId(int $orderId): array + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->getAllTopLevelTransactionsByOrderId($orderId); + } + + /** + * @return Transaction[] + */ + public function getAllTransactionsByOrderId(int $orderId): array + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->getAllTransactionsByOrderId($orderId); + } + + /** + * @return Transaction[] + */ + public function getChildrenByTransactionId(int $transactionId): array + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->getChildrenByTransactionId($transactionId); + } + + public function getTransactionByHash(string $hash): ?Transaction + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->getTransactionByHash($hash); + } + + public function getTransactionByReferenceAndStatus(string $reference, string $status): ?Transaction + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->getTransactionByReferenceAndStatus($reference, $status); + } + + public function getTransactionByReference(string $reference): ?Transaction + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->getTransactionByReference($reference); + } + + public function getTransactionById(int $id): ?Transaction + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->getTransactionById($id); + } + + public function isTransactionSuccessful(Transaction $transaction): bool + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->isTransactionSuccessful($transaction); + } + + public function saveTransaction(Transaction $model, bool $runValidation = true): bool + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->saveTransaction($model, $runValidation); + } + + /** + * @param Order[] $orders + * @return Order[] + */ + public function eagerLoadTransactionsForOrders(array $orders): array + { + return app(\CraftCms\Commerce\Payment\Transactions::class)->eagerLoadTransactionsForOrders($orders); + } + + public static function registerEvents(): void + { + Event::listen(TransactionCreated::class, static function(TransactionCreated $event) { + $legacy = Plugin::getInstance()->getTransactions(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_CREATE_TRANSACTION)) { + $legacy->trigger(self::EVENT_AFTER_CREATE_TRANSACTION, $event); + } + }); + + Event::listen(TransactionSaved::class, static function(TransactionSaved $event) { + $legacy = Plugin::getInstance()->getTransactions(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_SAVE_TRANSACTION)) { + $legacy->trigger(self::EVENT_AFTER_SAVE_TRANSACTION, $event); + } + }); + } +} diff --git a/src-yii2/services/Transfers.php b/src-yii2/services/Transfers.php new file mode 100644 index 0000000000..e5340d6e23 --- /dev/null +++ b/src-yii2/services/Transfers.php @@ -0,0 +1,11 @@ +getAllVariantsByProductId($productId, $siteId, $includeDisabled); + } + + public function getVariantById(int $variantId, ?int $siteId = null): ?Variant + { + return app(\CraftCms\Commerce\Product\Variant\Variants::class)->getVariantById($variantId, $siteId); + } + + /** + * @throws InvalidConfigException + */ + public function getVariantGqlContentArguments(): array + { + return app(\CraftCms\Commerce\Product\Variant\Variants::class)->getVariantGqlContentArguments(); + } +} diff --git a/src-yii2/services/Vat.php b/src-yii2/services/Vat.php new file mode 100644 index 0000000000..48ee575f0d --- /dev/null +++ b/src-yii2/services/Vat.php @@ -0,0 +1,16 @@ +isValidVatId($vatId); + } +} diff --git a/src-yii2/services/Webhooks.php b/src-yii2/services/Webhooks.php new file mode 100644 index 0000000000..6dbb7e31cb --- /dev/null +++ b/src-yii2/services/Webhooks.php @@ -0,0 +1,47 @@ +processWebhook($gateway); + } + + public static function registerEvents(): void + { + Event::listen(WebhookProcessing::class, static function(WebhookProcessing $event) { + $legacy = Plugin::getInstance()->getWebhooks(); + if ($legacy->hasEventHandlers(self::EVENT_BEFORE_PROCESS_WEBHOOK)) { + $legacy->trigger(self::EVENT_BEFORE_PROCESS_WEBHOOK, $event); + } + }); + + Event::listen(WebhookProcessed::class, static function(WebhookProcessed $event) { + $legacy = Plugin::getInstance()->getWebhooks(); + if ($legacy->hasEventHandlers(self::EVENT_AFTER_PROCESS_WEBHOOK)) { + $legacy->trigger(self::EVENT_AFTER_PROCESS_WEBHOOK, $event); + } + }); + } +} diff --git a/src-yii2/stats/AverageOrderTotal.php b/src-yii2/stats/AverageOrderTotal.php new file mode 100644 index 0000000000..f0c9c7e546 --- /dev/null +++ b/src-yii2/stats/AverageOrderTotal.php @@ -0,0 +1,11 @@ + 0 %} + {% redirect 'commerce/products' %} +{% endif %} + +{% if currentUser.can('commerce-manageOrders') %} + {% redirect 'commerce/orders' %} +{% endif %} + +{% if craft.commerce.productTypes.viewableProductTypes|length > 0 %} + {% redirect 'commerce/products' %} +{% endif %} + +{% if currentUser.can('commerce-manageStoreSettings') %} + {% redirect "commerce/store-management" %} +{% endif %} + +{% if currentUser.can('commerce-manageInventoryStockLevels') %} + {% redirect "commerce/inventory" %} +{% endif %} + +{% if currentUser.can('commerce-managePromotions') %} + {% redirect "commerce/store-management/#{primaryStore.handle}/discounts" %} +{% endif %} + +{% if currentUser.can('commerce-manageShipping') %} + {% redirect "commerce/store-management/#{primaryStore.handle}/shippingmethods" %} +{% endif %} + +{% if currentUser.can('commerce-manageTaxes') %} + {% redirect "commerce/store-management/#{primaryStore.handle}/taxrates" %} +{% endif %} + +{% exit 403 %} diff --git a/src/templates/inventory-locations/_deleteModal.twig b/src-yii2/templates/inventory-locations/_deleteModal.twig similarity index 100% rename from src/templates/inventory-locations/_deleteModal.twig rename to src-yii2/templates/inventory-locations/_deleteModal.twig diff --git a/src-yii2/templates/inventory-locations/_edit.twig b/src-yii2/templates/inventory-locations/_edit.twig new file mode 100644 index 0000000000..1c5735c784 --- /dev/null +++ b/src-yii2/templates/inventory-locations/_edit.twig @@ -0,0 +1,11 @@ +{% namespace 'inventoryLocationAddress' %} +{{ extraFieldsHtml|raw }} +{% endnamespace %} + +{{ form|raw }} + +{% hook "cp.commerce.inventoryLocation.edit" %} + +{% if not inventoryLocation.id %} +{% js "new Craft.HandleGenerator('##{'name'|namespaceInputId}', '##{'handle'|namespaceInputId}');" %} +{% endif %} diff --git a/src/templates/inventory-locations/_index.twig b/src-yii2/templates/inventory-locations/_index.twig similarity index 100% rename from src/templates/inventory-locations/_index.twig rename to src-yii2/templates/inventory-locations/_index.twig diff --git a/src/templates/inventory-locations/_sidebar.twig b/src-yii2/templates/inventory-locations/_sidebar.twig similarity index 100% rename from src/templates/inventory-locations/_sidebar.twig rename to src-yii2/templates/inventory-locations/_sidebar.twig diff --git a/src/templates/inventory/item/_edit.twig b/src-yii2/templates/inventory/item/_edit.twig similarity index 100% rename from src/templates/inventory/item/_edit.twig rename to src-yii2/templates/inventory/item/_edit.twig diff --git a/src/templates/inventory/levels/_index.twig b/src-yii2/templates/inventory/levels/_index.twig similarity index 100% rename from src/templates/inventory/levels/_index.twig rename to src-yii2/templates/inventory/levels/_index.twig diff --git a/src/templates/inventory/levels/_inventoryMovementModal.twig b/src-yii2/templates/inventory/levels/_inventoryMovementModal.twig similarity index 100% rename from src/templates/inventory/levels/_inventoryMovementModal.twig rename to src-yii2/templates/inventory/levels/_inventoryMovementModal.twig diff --git a/src/templates/inventory/levels/_inventoryMovementPreview.twig b/src-yii2/templates/inventory/levels/_inventoryMovementPreview.twig similarity index 100% rename from src/templates/inventory/levels/_inventoryMovementPreview.twig rename to src-yii2/templates/inventory/levels/_inventoryMovementPreview.twig diff --git a/src/templates/inventory/levels/_unfulfilledOrdersModal.twig b/src-yii2/templates/inventory/levels/_unfulfilledOrdersModal.twig similarity index 100% rename from src/templates/inventory/levels/_unfulfilledOrdersModal.twig rename to src-yii2/templates/inventory/levels/_unfulfilledOrdersModal.twig diff --git a/src/templates/inventory/levels/_updateInventoryLevelModal.twig b/src-yii2/templates/inventory/levels/_updateInventoryLevelModal.twig similarity index 100% rename from src/templates/inventory/levels/_updateInventoryLevelModal.twig rename to src-yii2/templates/inventory/levels/_updateInventoryLevelModal.twig diff --git a/src/templates/inventory/levels/_updateInventoryLevelPreview.twig b/src-yii2/templates/inventory/levels/_updateInventoryLevelPreview.twig similarity index 100% rename from src/templates/inventory/levels/_updateInventoryLevelPreview.twig rename to src-yii2/templates/inventory/levels/_updateInventoryLevelPreview.twig diff --git a/src/templates/inventory/transfers/_index.twig b/src-yii2/templates/inventory/transfers/_index.twig similarity index 100% rename from src/templates/inventory/transfers/_index.twig rename to src-yii2/templates/inventory/transfers/_index.twig diff --git a/src/templates/orders/_edit.twig b/src-yii2/templates/orders/_edit.twig similarity index 100% rename from src/templates/orders/_edit.twig rename to src-yii2/templates/orders/_edit.twig diff --git a/src/templates/orders/_history.twig b/src-yii2/templates/orders/_history.twig similarity index 100% rename from src/templates/orders/_history.twig rename to src-yii2/templates/orders/_history.twig diff --git a/src/templates/orders/_index.twig b/src-yii2/templates/orders/_index.twig similarity index 100% rename from src/templates/orders/_index.twig rename to src-yii2/templates/orders/_index.twig diff --git a/src/templates/orders/_paymentForms.twig b/src-yii2/templates/orders/_paymentForms.twig similarity index 100% rename from src/templates/orders/_paymentForms.twig rename to src-yii2/templates/orders/_paymentForms.twig diff --git a/src/templates/orders/_paymentmodal.twig b/src-yii2/templates/orders/_paymentmodal.twig similarity index 100% rename from src/templates/orders/_paymentmodal.twig rename to src-yii2/templates/orders/_paymentmodal.twig diff --git a/src/templates/orders/_transactions.twig b/src-yii2/templates/orders/_transactions.twig similarity index 100% rename from src/templates/orders/_transactions.twig rename to src-yii2/templates/orders/_transactions.twig diff --git a/src/templates/orders/includes/_capture.twig b/src-yii2/templates/orders/includes/_capture.twig similarity index 100% rename from src/templates/orders/includes/_capture.twig rename to src-yii2/templates/orders/includes/_capture.twig diff --git a/src/templates/orders/includes/_refund.twig b/src-yii2/templates/orders/includes/_refund.twig similarity index 100% rename from src/templates/orders/includes/_refund.twig rename to src-yii2/templates/orders/includes/_refund.twig diff --git a/src/templates/orders/modals/_fulfillmentModal.twig b/src-yii2/templates/orders/modals/_fulfillmentModal.twig similarity index 100% rename from src/templates/orders/modals/_fulfillmentModal.twig rename to src-yii2/templates/orders/modals/_fulfillmentModal.twig diff --git a/src/templates/prices/_index.twig b/src-yii2/templates/prices/_index.twig similarity index 100% rename from src/templates/prices/_index.twig rename to src-yii2/templates/prices/_index.twig diff --git a/src/templates/prices/_polling.twig b/src-yii2/templates/prices/_polling.twig similarity index 100% rename from src/templates/prices/_polling.twig rename to src-yii2/templates/prices/_polling.twig diff --git a/src/templates/prices/_status.twig b/src-yii2/templates/prices/_status.twig similarity index 100% rename from src/templates/prices/_status.twig rename to src-yii2/templates/prices/_status.twig diff --git a/src/templates/prices/_table.twig b/src-yii2/templates/prices/_table.twig similarity index 100% rename from src/templates/prices/_table.twig rename to src-yii2/templates/prices/_table.twig diff --git a/src/templates/products/_index.twig b/src-yii2/templates/products/_index.twig similarity index 100% rename from src/templates/products/_index.twig rename to src-yii2/templates/products/_index.twig diff --git a/src/templates/promotions/index.twig b/src-yii2/templates/promotions/index.twig similarity index 100% rename from src/templates/promotions/index.twig rename to src-yii2/templates/promotions/index.twig diff --git a/src-yii2/templates/promotions/sales/_edit.twig b/src-yii2/templates/promotions/sales/_edit.twig new file mode 100644 index 0000000000..bd8eec9c68 --- /dev/null +++ b/src-yii2/templates/promotions/sales/_edit.twig @@ -0,0 +1,361 @@ +{% extends "commerce/_layouts/store-management" %} +{% set isIndex = false %} + +{% set crumbs = [ + { label: 'Commerce'|t('commerce'), url: url('commerce') }, + { label: "Store Management"|t('commerce'), url: url('commerce/store-management/#{storeHandle}') }, + { label: "Sales"|t('commerce'), url: url("commerce/store-management/#{storeHandle}/sales") }, +] %} + +{% set fullPageForm = true %} + +{% import "_includes/forms" as forms %} +{% import "commerce/_includes/forms/commerceForms" as commerceForms %} + +{% set mainFormAttributes = { + id: 'saleform', + method: 'post', + 'accept-charset': 'UTF-8' +} %} + +{% set formActions = [{ + label: 'Save and continue editing'|t('app'), + redirect: (isNewSale ? "commerce/store-management/#{storeHandle}/sales/{id}" : sale.getCpEditUrl())|hash, + retainScroll: true, + shortcut: true, +}] %} + +{% set actionClasses = "" %} +{% if (sale.getErrors('applyAmount') or sale.getErrors('apply')) %} + {% set actionClasses = "error" %} +{% endif %} + +{% set matchingItemsClasses = "" %} +{% if false %} + {% set matchingItemsClasses = "error" %} +{% endif %} + +{% set saleClasses = "" %} +{% if(sale.getErrors('name')) %} + {% set saleClasses = "error" %} +{% endif %} + +{% set tabs = { + sale: {'label':'Sale'|t('commerce'),'url':'#sale','class': saleClasses}, + matchingItems: {'label':'Matching Items'|t('commerce'),'url':'#matching-items'}, + conditions: {'label':'Conditions'|t('commerce'),'url':'#conditions'}, + actions: {'label':'Actions'|t('commerce'),'url':'#actions','class': actionClasses} +} %} + +{% hook "cp.commerce.sales.edit" %} + +{% block details %} + +
+ {{ forms.lightSwitchField({ + label: "Enable this sale"|t('commerce'), + id: 'enabled', + name: 'enabled', + value: 1, + on: sale.enabled, + checked: sale.enabled, + errors: sale.getErrors('enabled'), + instructions: 'Whether this sale should be available for use, regardless of other conditions.'|t('commerce') + }) }} +
+ + {% if sale and sale.id %} +
+
+
{{ "Created at"|t('app') }}
+
{{ sale.dateCreated|datetime('short') }}
+
+
+
{{ "Updated at"|t('app') }}
+
{{ sale.dateUpdated|datetime('short') }}
+
+
+ {% endif %} + + {% hook "cp.commerce.sales.edit.details" %} +{% endblock %} + +{% block content %} + + {{ redirectInput("commerce/store-management/#{storeHandle}/sales") }} + {% if sale.id %} + + + {% endif %} + +
+ {{ forms.textField({ + first: true, + label: "Name"|t('commerce'), + instructions: "What this sale will be called in the control panel."|t('commerce'), + id: 'name', + name: 'name', + value: sale.name, + errors: sale.getErrors('name'), + autofocus: true, + required: true, + }) }} + + {{ forms.textField({ + label: "Description"|t('commerce'), + instructions: "Sale description."|t('commerce'), + id: 'description', + name: 'description', + value: sale.description, + errors: sale.getErrors('description'), + }) }} + +
+ + + + + + + + {% hook "cp.commerce.sales.edit.content" %} +{% endblock %} + +{% js %} +$(function() { + $('#groups, #productTypes').selectize({ + plugins: ['remove_button'], + dropdownParent: 'body' + }); + + $("form").submit(function() { + $("input[name=ignorePrevious]").prop('disabled', false); + if ($("input[name=ignorePrevious]").prop('checked') == true) { + $("#ignorePrevious-field").css('opacity', 0.25); + } + }); + + $('select[name=apply]').change(function() { + + if (this.value == 'byPercent' || this.value == 'toPercent') { + $('#applyAmount-percent-symbol').removeClass('hidden'); + $('#applyAmount-currency-symbol').addClass('hidden'); + }else{ + $('#applyAmount-percent-symbol').addClass('hidden'); + $('#applyAmount-currency-symbol').removeClass('hidden'); + } + + if (this.value == 'toFlat' || this.value == 'toPercent') { + $('input[name=ignorePrevious]').prop('disabled', true); + $('#ignorePrevious').prop('disabled', true); + $('#ignorePrevious').addClass('disabled', true); + } + if (this.value != 'toFlat' && this.value != 'toPercent') { + $('input[name=ignorePrevious]').prop('disabled', false); + $('#ignorePrevious').prop('disabled', false); + $('#ignorePrevious').removeClass('disabled', true); + } + }); +}); +{% endjs %} diff --git a/src/templates/promotions/sales/index.twig b/src-yii2/templates/promotions/sales/index.twig similarity index 100% rename from src/templates/promotions/sales/index.twig rename to src-yii2/templates/promotions/sales/index.twig diff --git a/src/templates/settings/emails/_edit.twig b/src-yii2/templates/settings/emails/_edit.twig similarity index 100% rename from src/templates/settings/emails/_edit.twig rename to src-yii2/templates/settings/emails/_edit.twig diff --git a/src/templates/settings/emails/_previewError.twig b/src-yii2/templates/settings/emails/_previewError.twig similarity index 100% rename from src/templates/settings/emails/_previewError.twig rename to src-yii2/templates/settings/emails/_previewError.twig diff --git a/src/templates/settings/emails/index.twig b/src-yii2/templates/settings/emails/index.twig similarity index 100% rename from src/templates/settings/emails/index.twig rename to src-yii2/templates/settings/emails/index.twig diff --git a/src-yii2/templates/settings/gateways/_edit.twig b/src-yii2/templates/settings/gateways/_edit.twig new file mode 100644 index 0000000000..7f98b3aa44 --- /dev/null +++ b/src-yii2/templates/settings/gateways/_edit.twig @@ -0,0 +1,147 @@ +{% extends "commerce/_layouts/cp" %} + +{% set crumbs = [ + { label: 'Commerce'|t('commerce'), url: url('commerce') }, + { label: 'Settings'|t('app'), url: url('commerce/settings'), ariaLabel: 'Commerce Settings'|t('commerce') }, + { label: "Gateways"|t('commerce'), url: url('commerce/settings/gateways') }, +] %} + +{% set selectedSubnavItem = 'settings' %} + +{% set fullPageForm = not readOnly %} + +{% if readOnly %} + {% set contentNotice = readOnlyNotice() %} +{% endif %} + +{% import "_includes/forms" as forms %} + +{% block content %} + {{ hiddenInput('id', gateway.id) }} + {{ actionInput('commerce/gateways/save') }} + {{ redirectInput("commerce/settings/gateways") }} + + {{ forms.textField({ + label: 'Name'|t('commerce'), + name: 'name', + id: 'name', + value : gateway.name, + required: true, + errors: gateway.getErrors('name'), + disabled: readOnly, + }) }} + + {{ forms.textField({ + label: 'Handle'|t('commerce'), + name: 'handle', + id: 'handle', + class: 'code', + value : gateway.handle, + required: true, + errors: gateway.getErrors('handle'), + disabled: readOnly, + }) }} + + {% if gateway.supportsWebhooks() %} + {{ forms.textField({ + label: "Webhook URL"|t('commerce'), + instructions: "The webhook URL for this gateway."|t('commerce'), + disabled: true, + value: gateway.webhookUrl, + disabled: readOnly, + }) }} + {% endif %} +
+ + {{ forms.selectField({ + first: true, + label: 'Gateway'|t('commerce'), + warning: (gateway.id ? "Changing this value may affect your ability to refund existing transactions."|t('commerce')), + id: 'type', + name: 'type', + options : gatewayOptions, + value : className(gateway), + required: true, + errors: gateway.getErrors('type') ?? null, + toggle: true, + disabled: readOnly, + }) }} + + + + {% for gatewayType in gatewayTypes %} + {% set isCurrent = (gatewayType == className(gateway)) %} + + + {% endfor %} + + {{ forms.booleanMenuField({ + label: "Enabled for customers to select during checkout?"|t('commerce'), + id: 'isFrontendEnabled', + name: 'isFrontendEnabled', + includeEnvVars: true, + value: gateway.isFrontendEnabled(false), + errors: gateway.getErrors('isFrontendEnabled'), + disabled: readOnly, + }) }} + +
+ {{ forms.field({ + label: 'Match Order'|t('commerce'), + instructions: 'Create rules that allow this gateway to match the order.'|t('commerce'), + errors: gateway.getErrors('orderCondition'), + }, orderConditionHtml|raw) }} + + {{ forms.field({ + label: 'Match Billing Address'|t('commerce'), + instructions: 'Create rules that allow this gateway to match the billing address.'|t('commerce'), + errors: gateway.getErrors('billingAddressCondition'), + }, billingAddressConditionHtml|raw) }} + + {{ forms.field({ + label: 'Match Shipping Address'|t('commerce'), + instructions: 'Create rules that allow this gateway to match the shipping address.'|t('commerce'), + errors: gateway.getErrors('shippingAddressCondition'), + }, shippingAddressConditionHtml|raw) }} + +{% endblock %} + +{% js %} + $(function() { + $('#type').change(function() { + $('.gateway-settings').hide().find('select, input, textarea').prop('disabled', true); + if($(this).val()) { + $('#gateway-' + $(this).val()).show().find('select, input, textarea').prop('disabled', false); + } + }).change(); + }); +{% endjs %} + +{% if gateway is not defined or not gateway.handle %} + {% js %} + new Craft.HandleGenerator('#name', '#handle'); + {% endjs %} +{% endif %} diff --git a/src/templates/settings/gateways/index.twig b/src-yii2/templates/settings/gateways/index.twig similarity index 100% rename from src/templates/settings/gateways/index.twig rename to src-yii2/templates/settings/gateways/index.twig diff --git a/src-yii2/templates/settings/general/index.twig b/src-yii2/templates/settings/general/index.twig new file mode 100644 index 0000000000..feeda0e7d6 --- /dev/null +++ b/src-yii2/templates/settings/general/index.twig @@ -0,0 +1,65 @@ +{# @var settings \craft\commerce\models\Settings #} +{% extends "commerce/_layouts/settings" %} + +{% set selectedTab = 'settings' %} +{% set fullPageForm = not readOnly %} + +{% set crumbs = [ + { label: 'Commerce'|t('commerce'), url: url('commerce') }, +] %} + +{% import "_includes/forms" as forms %} + +{% from _self import configWarning %} + +{% block content %} +

{{ "General Settings"|t('commerce') }}

+ +
+ + {% if not readOnly %} + {{ actionInput('commerce/settings/save-settings') }} + {{ redirectInput('commerce/settings/general') }} + {% endif %} + +

{{ 'Units'|t('commerce') }}

+ {{ forms.selectField({ + label: "Weight Unit"|t('commerce'), + instructions: "The unit of measurement that should be used when specifying product weights."|t('commerce'), + name: 'settings[weightUnits]', + value: settings.weightUnits, + options: settings.getWeightUnitsOptions(), + errors: settings.getErrors('weightUnits'), + required: true, + disabled: readOnly, + warning: configWarning('weightUnits', 'commerce'), + }) }} + + {{ forms.selectField({ + label: "Dimension Unit"|t('commerce'), + instructions: "The unit of measurement that should be used when specifying product dimensions."|t('commerce'), + name: 'settings[dimensionUnits]', + value: settings.dimensionUnits, + options: settings.getDimensionUnits(), + errors: settings.getErrors('dimensionUnits'), + required: true, + disabled: readOnly, + warning: configWarning('dimensionUnits', 'commerce'), + }) }} + +
+

{{ 'Control Panel Settings'|t('commerce') }}

+ {{ forms.selectField({ + label: "Default View"|t('commerce'), + instructions: "Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access."|t('commerce'), + name: 'settings[defaultView]', + value: settings.defaultView, + options: settings.getDefaultViewOptions(), + errors: settings.getErrors('defaultView'), + disabled: readOnly, + required: true, + warning: configWarning('defaultView', 'commerce'), + }) }} +
+ +{% endblock %} diff --git a/src/templates/settings/index.twig b/src-yii2/templates/settings/index.twig similarity index 100% rename from src/templates/settings/index.twig rename to src-yii2/templates/settings/index.twig diff --git a/src/templates/settings/lineitemstatuses/_edit.twig b/src-yii2/templates/settings/lineitemstatuses/_edit.twig similarity index 100% rename from src/templates/settings/lineitemstatuses/_edit.twig rename to src-yii2/templates/settings/lineitemstatuses/_edit.twig diff --git a/src/templates/settings/lineitemstatuses/index.twig b/src-yii2/templates/settings/lineitemstatuses/index.twig similarity index 100% rename from src/templates/settings/lineitemstatuses/index.twig rename to src-yii2/templates/settings/lineitemstatuses/index.twig diff --git a/src/templates/settings/ordersettings/_edit.twig b/src-yii2/templates/settings/ordersettings/_edit.twig similarity index 100% rename from src/templates/settings/ordersettings/_edit.twig rename to src-yii2/templates/settings/ordersettings/_edit.twig diff --git a/src/templates/settings/orderstatuses/_edit.twig b/src-yii2/templates/settings/orderstatuses/_edit.twig similarity index 100% rename from src/templates/settings/orderstatuses/_edit.twig rename to src-yii2/templates/settings/orderstatuses/_edit.twig diff --git a/src/templates/settings/orderstatuses/index.twig b/src-yii2/templates/settings/orderstatuses/index.twig similarity index 100% rename from src/templates/settings/orderstatuses/index.twig rename to src-yii2/templates/settings/orderstatuses/index.twig diff --git a/src/templates/settings/pdfs/_edit.twig b/src-yii2/templates/settings/pdfs/_edit.twig similarity index 100% rename from src/templates/settings/pdfs/_edit.twig rename to src-yii2/templates/settings/pdfs/_edit.twig diff --git a/src/templates/settings/pdfs/index.twig b/src-yii2/templates/settings/pdfs/index.twig similarity index 100% rename from src/templates/settings/pdfs/index.twig rename to src-yii2/templates/settings/pdfs/index.twig diff --git a/src/templates/settings/producttypes/_edit.twig b/src-yii2/templates/settings/producttypes/_edit.twig similarity index 100% rename from src/templates/settings/producttypes/_edit.twig rename to src-yii2/templates/settings/producttypes/_edit.twig diff --git a/src/templates/settings/producttypes/index.twig b/src-yii2/templates/settings/producttypes/index.twig similarity index 100% rename from src/templates/settings/producttypes/index.twig rename to src-yii2/templates/settings/producttypes/index.twig diff --git a/src/templates/settings/stores/_edit.twig b/src-yii2/templates/settings/stores/_edit.twig similarity index 100% rename from src/templates/settings/stores/_edit.twig rename to src-yii2/templates/settings/stores/_edit.twig diff --git a/src/templates/settings/stores/_siteStore.twig b/src-yii2/templates/settings/stores/_siteStore.twig similarity index 100% rename from src/templates/settings/stores/_siteStore.twig rename to src-yii2/templates/settings/stores/_siteStore.twig diff --git a/src/templates/settings/stores/index.twig b/src-yii2/templates/settings/stores/index.twig similarity index 100% rename from src/templates/settings/stores/index.twig rename to src-yii2/templates/settings/stores/index.twig diff --git a/src/templates/settings/transfers/_edit.twig b/src-yii2/templates/settings/transfers/_edit.twig similarity index 100% rename from src/templates/settings/transfers/_edit.twig rename to src-yii2/templates/settings/transfers/_edit.twig diff --git a/src-yii2/templates/store-management/discounts/_edit.twig b/src-yii2/templates/store-management/discounts/_edit.twig new file mode 100644 index 0000000000..47a2de29b3 --- /dev/null +++ b/src-yii2/templates/store-management/discounts/_edit.twig @@ -0,0 +1,639 @@ + +{% set fullPageForm = true %} + +{% import "_includes/forms" as forms %} +{% import "commerce/_includes/forms/commerceForms" as commerceForms %} + +{% set mainFormAttributes = { + id: 'discountform', + method: 'post', + 'accept-charset': 'UTF-8' +} %} + +{% set formActions = [ + { + label: 'Save and continue editing'|t('app'), + redirect: (isNewDiscount ? 'commerce/store-management/#{storeHandle}/discounts/{id}' : discount.getCpEditUrl())|hash, + retainScroll: true, + shortcut: true, + }] +%} + +{% set couponsTable = { + name: 'coupons', + id: 'coupons-table', + cols: { + id: { + type: 'singleline', + heading: 'id'|t('app'), + class: 'hidden', + }, + code: { + type: 'singleline', + heading: 'Code'|t('commerce'), + }, + uses: { + type: 'singleline', + heading: 'Uses'|t('commerce'), + }, + maxUses: { + type: 'singleline', + heading: 'Max Uses'|t('commerce'), + info: 'Leave blank for unlimited uses.'|t('commerce'), + }, + }, + defaultValues: { uses: 0 } +} %} + +{% hook "cp.commerce.discounts.edit" %} + +{% block content %} + {% set formAttributes = { + id: 'discountform', + method: 'post', + 'accept-charset': 'UTF-8', + data: { + saveshortcut: true, + 'saveshortcut-redirect': "commerce/store-management/#{storeHandle}/discounts"|hash, + 'confirm-unload': true + }, + } %} + + {{ hiddenInput('storeId', discount.storeId) }} + {% if discount.id %} + + + {% endif %} + +
+ {{ forms.textField({ + first: true, + label: "Name"|t('commerce'), + instructions: "What this discount will be called in the control panel."|t('commerce'), + id: 'name', + name: 'name', + value: discount.name, + errors: discount.getErrors('name'), + autofocus: true, + required: true, + }) }} + + {{ forms.textField({ + label: "Description"|t('commerce'), + instructions: "Discount description."|t('commerce'), + id: 'description', + name: 'description', + value: discount.description, + errors: discount.getErrors('description'), + }) }} + + {% hook "cp.commerce.discount.edit" %} +
+ + + + + + + + + + {% hook "cp.commerce.discounts.edit.content" %} +{% endblock %} + +{% js %} +$(function() { + + $('#code').on('keyup blur', function(event) { + if (this.value.length === 0) { + $('#coupon-fields').addClass('hidden'); + } else { + $('#coupon-fields').removeClass('hidden'); + } + }); + + function disableShippingSwitch() { + $('#hasFreeShippingForMatchingItems').data('lightswitch').turnOff(); + $('input[name="hasFreeShippingForMatchingItems"]').prop("disabled", true); + $('#hasFreeShippingForMatchingItems').prop("disabled", true); + $("#hasFreeShippingForMatchingItems").addClass("disabled"); + } + + function enableShippingSwitch() { + $('input[name="hasFreeShippingForMatchingItems"]').prop("disabled", false); + $('#hasFreeShippingForMatchingItems').prop("disabled", false); + $("#hasFreeShippingForMatchingItems").removeClass("disabled"); + } + + if ($('input[name="hasFreeShippingForOrder"]').val() == 1) { + disableShippingSwitch(); + } + + $('#hasFreeShippingForOrder').click(function() { + if ($('input[name="hasFreeShippingForOrder"]').val() == 1) { + disableShippingSwitch(); + } else { + enableShippingSwitch(); + } + }); + + $('.clear-btn.discount-clear-use').click(function(event) { + var $this = $(this); + var $spinner = $($this.data('spinner')); + var $field = $($this.data('field')); + var type = $this.data('type'); + var r = confirm(Craft.t('commerce', 'Are you sure you want to clear this discount usage counter?')); + + if (r == true) { + $spinner.toggleClass('hidden'); + $.ajax({ + type: "POST", + dataType: 'json', + headers: { + "X-CSRF-Token": '{{ craft.app.request.csrfToken }}', + }, + url: '', + data: { + 'action' : 'commerce/discounts/clear-discount-uses', + 'id': '{{ discount.id ?? '' }}', + 'type': type + }, + success: function(data){ + $spinner.toggleClass('hidden'); + $field.val(''); + Craft.cp.displayNotice(Craft.t('commerce', 'Counter has been cleared.')); + $this.attr('disabled', 'disabled').prop('disabled', 'disabled'); + } + }); + } + }); + + new Craft.Commerce.Coupons('#commerce-coupons', { + couponFormat: "{{ discount.couponFormat|e('js') }}", + table: { + name: "{{ couponsTable.name|namespaceInputName|e('js') }}", + cols: {{ couponsTable.cols|json_encode|raw }}, + defaultValues: {{ couponsTable.defaultValues|json_encode|raw }} + }, + }); +}); +{% endjs %} diff --git a/src/templates/store-management/discounts/_sidebar.twig b/src-yii2/templates/store-management/discounts/_sidebar.twig similarity index 100% rename from src/templates/store-management/discounts/_sidebar.twig rename to src-yii2/templates/store-management/discounts/_sidebar.twig diff --git a/src/templates/store-management/discounts/index.twig b/src-yii2/templates/store-management/discounts/index.twig similarity index 100% rename from src/templates/store-management/discounts/index.twig rename to src-yii2/templates/store-management/discounts/index.twig diff --git a/src/templates/store-management/general/_edit.twig b/src-yii2/templates/store-management/general/_edit.twig similarity index 100% rename from src/templates/store-management/general/_edit.twig rename to src-yii2/templates/store-management/general/_edit.twig diff --git a/src/templates/store-management/paymentcurrencies/_edit.twig b/src-yii2/templates/store-management/paymentcurrencies/_edit.twig similarity index 100% rename from src/templates/store-management/paymentcurrencies/_edit.twig rename to src-yii2/templates/store-management/paymentcurrencies/_edit.twig diff --git a/src/templates/store-management/paymentcurrencies/index.twig b/src-yii2/templates/store-management/paymentcurrencies/index.twig similarity index 100% rename from src/templates/store-management/paymentcurrencies/index.twig rename to src-yii2/templates/store-management/paymentcurrencies/index.twig diff --git a/src/templates/store-management/pricing-rules/_actions-fields.twig b/src-yii2/templates/store-management/pricing-rules/_actions-fields.twig similarity index 100% rename from src/templates/store-management/pricing-rules/_actions-fields.twig rename to src-yii2/templates/store-management/pricing-rules/_actions-fields.twig diff --git a/src-yii2/templates/store-management/pricing-rules/_edit.twig b/src-yii2/templates/store-management/pricing-rules/_edit.twig new file mode 100644 index 0000000000..2cf7097ae3 --- /dev/null +++ b/src-yii2/templates/store-management/pricing-rules/_edit.twig @@ -0,0 +1,89 @@ +{% import "_includes/forms" as forms %} +{% import "commerce/_includes/forms/commerceForms" as commerceForms %} + +{% if catalogPricingRule.id %} + {{ hiddenInput('id', catalogPricingRule.id) }} +{% endif %} +{{ hiddenInput('storeId', catalogPricingRule.storeId) }} + +
+ {{ forms.textField({ + first: true, + label: "Name"|t('commerce'), + instructions: "What this catalog pricing rule will be called in the control panel."|t('commerce'), + id: 'name', + name: 'name', + value: catalogPricingRule.name, + errors: catalogPricingRule.getErrors('name'), + autofocus: true, + required: true, + }) }} + + {{ forms.textField({ + label: "Description"|t('commerce'), + instructions: "Catalog pricing rule description."|t('commerce'), + id: 'description', + name: 'description', + value: catalogPricingRule.description, + errors: catalogPricingRule.getErrors('description'), + }) }} +
+ + + + + + {% hook "cp.commerce.catalogPricingRules.edit.content" %} \ No newline at end of file diff --git a/src/templates/store-management/pricing-rules/_sidebar.twig b/src-yii2/templates/store-management/pricing-rules/_sidebar.twig similarity index 100% rename from src/templates/store-management/pricing-rules/_sidebar.twig rename to src-yii2/templates/store-management/pricing-rules/_sidebar.twig diff --git a/src/templates/store-management/pricing-rules/_slideout.twig b/src-yii2/templates/store-management/pricing-rules/_slideout.twig similarity index 91% rename from src/templates/store-management/pricing-rules/_slideout.twig rename to src-yii2/templates/store-management/pricing-rules/_slideout.twig index 82c6b33a7e..1f278eee1b 100644 --- a/src/templates/store-management/pricing-rules/_slideout.twig +++ b/src-yii2/templates/store-management/pricing-rules/_slideout.twig @@ -36,15 +36,11 @@ errors: catalogPricingRule.getErrors('dateTo'), }) }} - {% set customerConditionInput %} - {{ catalogPricingRule.customerCondition.getBuilderHtml()|raw }} - {% endset %} - {{ forms.field({ id: 'customerCondition', label: 'Match Customer'|t('commerce'), errors: catalogPricingRule.getErrors('customerCondition') - }, customerConditionInput) }} + }, customerConditionHtml|raw) }} '; - } - } - }, - ]; - - new Craft.VueAdminTable({ - actions: [ - { - label: '', - icon: 'settings', - actions: [ - { - label: Craft.t('commerce', 'Set Default Category'), - action: 'commerce/shipping-categories/set-default-category', - param: 'storeHandle', - value: '{$storeHandle}', - allowMultiple: false - } - ] - } - ], - checkboxes: true, - columns: columns, - container: '#shipping-vue-admin-table', - deleteAction: 'commerce/shipping-categories/delete', - padded: true, - tableData: {$tableData}, - }); - -JS; - - - $this->getView()->registerJs($js, View::POS_END); - - return $this->asStoreManagementCpScreen($storeHandle) - ->additionalButtonsHtml(Html::a( - Craft::t('commerce', 'New shipping category'), - $store->getStoreSettingsUrl('shippingcategories/new'), - ['class' => 'btn submit add icon'] - )) - ->contentHtml(Html::tag('div', '', ['id' => 'shipping-vue-admin-table'])); - } - - /** - * @param int|null $id - * @param ShippingCategory|null $shippingCategory - * @throws HttpException - */ - public function actionEdit(?string $storeHandle = null, int $id = null, ShippingCategory $shippingCategory = null): Response - { - $variables = [ - 'id' => $id, - 'shippingCategory' => $shippingCategory, - 'productTypes' => Plugin::getInstance()->getProductTypes()->getAllProductTypes(), - 'storeHandle' => $storeHandle, - ]; - - $store = null; - if ($storeHandle !== null) { - $store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle); - } - - $store ??= Plugin::getInstance()->getStores()->getPrimaryStore(); - - if (!$variables['shippingCategory']) { - if ($variables['id']) { - $variables['shippingCategory'] = Plugin::getInstance() - ->getShippingCategories() - ->getShippingCategoryById($variables['id'], $store->id); - - if (!$variables['shippingCategory']) { - throw new HttpException(404); - } - } else { - $variables['shippingCategory'] = Craft::createObject([ - 'class' => ShippingCategory::class, - 'attributes' => ['storeId' => $store->id], - ]); - } - } - - if ($variables['shippingCategory']->id) { - $variables['title'] = $variables['shippingCategory']->name; - } else { - $variables['title'] = Craft::t('commerce', 'Create a new shipping category'); - } - - DebugPanel::prependOrAppendModelTab(model: $variables['shippingCategory'], prepend: true); - - $variables['productTypesOptions'] = []; - if (!empty($variables['productTypes'])) { - $variables['productTypesOptions'] = ArrayHelper::map($variables['productTypes'], 'id', fn($row) => ['label' => $row->name, 'value' => $row->id]); - } - - $allShippingCategories = Plugin::getInstance()->getShippingCategories()->getAllShippingCategories($store->id); - $variables['isDefaultAndOnlyCategory'] = $variables['id'] && $allShippingCategories->count() === 1 && $allShippingCategories->firstWhere('id', $variables['id']); - - $metaSidebar = ''; - if ($variables['shippingCategory']->id) { - $metaSidebar = Cp::metadataHtml([ - Craft::t('app', 'Created at') => Craft::$app->getFormatter()->asDatetime($variables['shippingCategory']->dateCreated, 'short'), - Craft::t('app', 'Updated at') => Craft::$app->getFormatter()->asDatetime($variables['shippingCategory']->dateUpdated, 'short'), - ]); - } - - return $this->asStoreManagementCpScreen($storeHandle, false) - ->title($variables['title']) - ->addCrumb(Craft::t('commerce', 'Shipping Categories'),$store->getStoreSettingsUrl('shippingcategories')) - ->action('commerce/shipping-categories/save') - ->redirectUrl($store->getStoreSettingsUrl('shippingcategories')) - ->metaSidebarHtml($metaSidebar) - ->contentTemplate('commerce/store-management/shipping/shippingcategories/_edit', $variables); - } - - /** - * @throws BadRequestHttpException - * @throws Exception - * @noinspection Duplicates - */ - public function actionSave(): ?Response - { - $this->requirePostRequest(); - - $shippingCategory = new ShippingCategory(); - - // Shared attributes - $shippingCategory->id = $this->request->getBodyParam('shippingCategoryId'); - $shippingCategory->storeId = $this->request->getBodyParam('storeId'); - $this->requireStoreAccess($shippingCategory->storeId); - $shippingCategory->name = $this->request->getBodyParam('name'); - $shippingCategory->handle = $this->request->getBodyParam('handle'); - $shippingCategory->icon = $this->request->getBodyParam('icon'); - $shippingCategory->color = $this->request->getBodyParam('color'); - $shippingCategory->description = $this->request->getBodyParam('description'); - $shippingCategory->default = (bool)$this->request->getBodyParam('default'); - - // Set the new product types - // If this is the default category, it should be available to all product types - if ($shippingCategory->default) { - $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); - } else { - $postedProductTypes = $this->request->getBodyParam('productTypes', []) ?: []; - $productTypes = []; - foreach ($postedProductTypes as $productTypeId) { - if ($productTypeId && $productType = Plugin::getInstance()->getProductTypes()->getProductTypeById($productTypeId)) { - $productTypes[] = $productType; - } - } - } - $shippingCategory->setProductTypes($productTypes); - - - // Save it - if (!Plugin::getInstance()->getShippingCategories()->saveShippingCategory($shippingCategory)) { - return $this->asModelFailure( - $shippingCategory, - Craft::t('commerce', 'Couldn’t save shipping category.'), - 'shippingCategory' - ); - } - - return $this->asModelSuccess( - $shippingCategory, - Craft::t('commerce', 'Shipping category saved.'), - 'shippingCategory', - data: [ - 'id' => $shippingCategory->id, - 'name' => $shippingCategory->name, - ] - ); - } - - /** - * @throws HttpException - */ - public function actionDelete(): ?Response - { - $this->requirePostRequest(); - - $id = $this->request->getBodyParam('id'); - $ids = $this->request->getBodyParam('ids'); - - if ((!$id && empty($ids)) || ($id && !empty($ids))) { - throw new BadRequestHttpException('id or ids must be specified.'); - } - - if ($id) { - // If it is just the one id we know it has come from an ajax request on the table - $this->requireAcceptsJson(); - $ids = [$id]; - } - - $failedIds = []; - foreach ($ids as $id) { - $shippingCategory = Plugin::getInstance()->getShippingCategories()->getShippingCategoryById($id); - if ($shippingCategory) { - $this->requireStoreAccess($shippingCategory->storeId); - } - - if (!$shippingCategory || !Plugin::getInstance()->getShippingCategories()->deleteShippingCategoryById($id)) { - $failedIds[] = $id; - } - } - - if (!empty($failedIds)) { - return $this->asFailure(Craft::t('commerce', 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.', [ - 'count' => count($failedIds), - ])); - } - - return $this->asSuccess(Craft::t('commerce', 'Shipping categories deleted.')); - } - - /** - * @throws BadRequestHttpException - * @throws Exception - * @since 3.2.9 - */ - public function actionSetDefaultCategory(): ?Response - { - $this->requirePostRequest(); - - $ids = $this->request->getRequiredBodyParam('ids'); - $storeHandle = $this->request->getRequiredBodyParam('storeHandle'); - if (!$storeHandle || !$store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle)) { - throw new InvalidConfigException('Invalid store.'); - } - - $this->requireStoreAccess($store->id); - - if (!empty($ids)) { - $id = ArrayHelper::firstValue($ids); - - $shippingCategory = Plugin::getInstance()->getShippingCategories()->getShippingCategoryById($id, $store->id); - if ($shippingCategory) { - $shippingCategory->default = true; - if (Plugin::getInstance()->getShippingCategories()->saveShippingCategory($shippingCategory)) { - $this->setSuccessFlash(Craft::t('commerce', 'Shipping category updated.')); - return null; - } - } - } - - $this->setFailFlash(Craft::t('commerce', 'Unable to set default shipping category.')); - return null; - } -} diff --git a/src/controllers/ShippingMethodsController.php b/src/controllers/ShippingMethodsController.php deleted file mode 100644 index 819b6bd94d..0000000000 --- a/src/controllers/ShippingMethodsController.php +++ /dev/null @@ -1,304 +0,0 @@ - - * @since 2.0 - */ -class ShippingMethodsController extends BaseShippingSettingsController -{ - /** - * @throws InvalidConfigException - */ - public function actionIndex(?string $storeHandle = null): Response - { - if ($storeHandle === null || !$store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle)) { - $store = Plugin::getInstance()->getStores()->getPrimaryStore(); - } - - $shippingMethods = Plugin::getInstance()->getShippingMethods()->getAllShippingMethods($store->id); - - // Generate table data with chips - $tableData = []; - foreach ($shippingMethods as $shippingMethod) { - $label = Html::encode(Craft::t('site', $shippingMethod->name)); - $tableData[] = [ - 'id' => $shippingMethod->id, - 'title' => $label, - 'chip' => Cp::chipHtml($shippingMethod, [ - 'showStatus' => true, - 'showThumb' => true, - 'labelHtml' => Html::a($label, $shippingMethod->getCpEditUrl(), [ - 'class' => ['chip-label', 'cell-bold'], - ]), - ]), - 'url' => $shippingMethod->getCpEditUrl(), - 'handle' => $shippingMethod->handle, - 'type' => $shippingMethod->getType(), - 'status' => $shippingMethod->enabled, - ]; - } - - $this->getView()->registerTranslations('commerce', [ - 'Disabled', - 'Enabled', - 'Handle', - 'Name', - 'Set status', - 'Type', - ]); - - $tableData = Json::encode($tableData); - - $js = <<getView()->registerJs($js, View::POS_END); - - return $this->asStoreManagementCpScreen($storeHandle) - ->additionalButtonsHtml(Html::a(Craft::t('commerce', 'New shipping method'), $store->getStoreSettingsUrl('shippingmethods/new'), ['class' => 'btn submit add icon'])) - ->contentHtml(Html::tag('div', '', ['id' => 'shipping-vue-admin-table'])); - } - - /** - * @param int|null $id - * @param ShippingMethod|null $shippingMethod - * @throws HttpException - * @throws InvalidConfigException - */ - public function actionEdit(?string $storeHandle = null, int $id = null, ShippingMethod $shippingMethod = null): Response - { - if ($storeHandle === null || !$store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle)) { - $store = Plugin::getInstance()->getStores()->getPrimaryStore(); - } - - if (!$shippingMethod) { - if ($id) { - $shippingMethod = Plugin::getInstance()->getShippingMethods()->getShippingMethodById($id, $store->id); - - if (!$shippingMethod) { - throw new HttpException(404); - } - } else { - $shippingMethod = Craft::createObject([ - 'class' => ShippingMethod::class, - 'attributes' => ['storeId' => $store->id], - ]); - } - } - - if ($shippingMethod->id) { - $title = $shippingMethod->name; - } else { - $title = Craft::t('commerce', 'Create a new shipping method'); - } - - $storeHandle = $store->handle; - - DebugPanel::prependOrAppendModelTab(model: $shippingMethod, prepend: true); - - $shippingRules = $shippingMethod->id !== null - ? Plugin::getInstance()->getShippingRules()->getAllShippingRulesByShippingMethodId($shippingMethod->id) - : []; - - $this->getView()->registerTranslations('commerce', [ - 'Couldn’t reorder rules.', - 'Description', - 'No shipping rules exist yet.', - 'Rules reordered.', - 'Shipping Rule', - ]); - - $metaDataHtml = Html::beginTag('div', ['class' => 'meta']) . - Cp::lightswitchFieldHtml([ - 'label' => Craft::t('commerce', 'Enable this shipping method on the front end'), - 'id' => 'enabled', - 'name' => 'enabled', - 'on' => $shippingMethod->enabled, - 'errors' => $shippingMethod->getErrors('enabled'), - ]) . - Html::endTag('div'); - - if ($shippingMethod->id) { - $metaDataHtml .= Cp::metadataHtml([ - Craft::t('app', 'Created at') => Craft::$app->getFormatter()->asDatetime($shippingMethod->dateCreated, 'short'), - Craft::t('app', 'Updated at') => Craft::$app->getFormatter()->asDatetime($shippingMethod->dateUpdated, 'short'), - ]); - } - - return $this->asStoreManagementCpScreen($storeHandle, false) - ->title($title) - ->action('commerce/shipping-methods/save') - ->redirectUrl($store->getStoreSettingsUrl('shippingmethods/{id}#rules')) - ->addCrumb(Craft::t('commerce', 'Shipping Methods'), $store->getStoreSettingsUrl('shippingmethods')) - ->metaSidebarHtml($metaDataHtml) - ->submitButtonLabel($shippingMethod->id ? Craft::t('commerce', 'Save and set rules') : Craft::t('app', 'Save')) - ->contentTemplate('commerce/store-management/shipping/shippingmethods/_edit', [ - 'shippingMethod' => $shippingMethod, - 'shippingRules' => $shippingRules, - 'store' => $store, - 'storeHandle' => $storeHandle, - ]); - } - - /** - * @throws BadRequestHttpException - * @throws \yii\base\Exception - */ - public function actionSave(): ?Response - { - $this->requirePostRequest(); - $shippingMethod = new ShippingMethod(); - - // Shared attributes - $shippingMethod->id = $this->request->getBodyParam('shippingMethodId'); - $shippingMethod->name = $this->request->getBodyParam('name'); - $shippingMethod->handle = $this->request->getBodyParam('handle'); - $shippingMethod->icon = $this->request->getBodyParam('icon'); - $shippingMethod->color = $this->request->getBodyParam('color'); - $shippingMethod->storeId = $this->request->getBodyParam('storeId'); - $this->requireStoreAccess($shippingMethod->storeId); - $shippingMethod->setOrderCondition($this->request->getBodyParam('orderCondition')); - $shippingMethod->setCustomerCondition($this->request->getBodyParam('customerCondition')); - $shippingMethod->enabled = (bool)$this->request->getBodyParam('enabled'); - - // Save it - if (!Plugin::getInstance()->getShippingMethods()->saveShippingMethod($shippingMethod)) { - return $this->asModelFailure($shippingMethod, Craft::t('commerce', 'Couldn’t save shipping method.'), 'shippingMethod'); - } - - return $this->asModelSuccess($shippingMethod, Craft::t('commerce', 'Shipping method saved.'), 'shippingMethod'); - } - - /** - * @throws HttpException - */ - public function actionDelete(): ?Response - { - $this->requirePostRequest(); - - $id = $this->request->getBodyParam('id'); - $ids = $this->request->getBodyParam('ids'); - - if ((!$id && empty($ids)) || ($id && !empty($ids))) { - throw new BadRequestHttpException('id or ids must be specified.'); - } - - if ($id) { - // If it is just the one id we know it has come from an ajax request on the table - $this->requireAcceptsJson(); - $ids = [$id]; - } - - $failedIds = []; - foreach ($ids as $id) { - $shippingMethod = Plugin::getInstance()->getShippingMethods()->getShippingMethodById($id); - if ($shippingMethod) { - $this->requireStoreAccess($shippingMethod->storeId); - } - - if (!$shippingMethod || !Plugin::getInstance()->getShippingMethods()->deleteShippingMethodById($id)) { - $failedIds[] = $id; - } - } - - if (!empty($failedIds)) { - return $this->asFailure(Craft::t('commerce', 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.', [ - 'count' => count($failedIds), - ])); - } - - return $this->asSuccess(Craft::t('commerce', 'Shipping methods and rules deleted.')); - } - - /** - * @throws BadRequestHttpException - * @throws Exception - * @since 3.2.9 - */ - public function actionUpdateStatus(): void - { - $this->requirePostRequest(); - $ids = $this->request->getRequiredBodyParam('ids'); - $status = $this->request->getRequiredBodyParam('status'); - - if (empty($ids)) { - $this->setFailFlash(Craft::t('commerce', 'Couldn’t update status.')); - } - - $transaction = Craft::$app->getDb()->beginTransaction(); - $shippingMethods = ShippingMethodRecord::find() - ->where(['id' => $ids]) - ->all(); - - /** @var ShippingMethodRecord $shippingMethod */ - foreach ($shippingMethods as $shippingMethod) { - $this->requireStoreAccess($shippingMethod->storeId); - $shippingMethod->enabled = ($status == 'enabled'); - $shippingMethod->save(); - } - $transaction->commit(); - - $this->setSuccessFlash(Craft::t('commerce', 'Shipping methods updated.')); - } -} diff --git a/src/controllers/ShippingRulesController.php b/src/controllers/ShippingRulesController.php deleted file mode 100644 index 942e82f3a2..0000000000 --- a/src/controllers/ShippingRulesController.php +++ /dev/null @@ -1,261 +0,0 @@ - - * @since 2.0 - */ -class ShippingRulesController extends BaseShippingSettingsController -{ - /** - * @param int|null $methodId - * @param int|null $ruleId - * @param ShippingRule|null $shippingRule - * @throws HttpException - * @throws LoaderError - * @throws RuntimeError - * @throws SyntaxError - * @throws Exception - */ - public function actionEdit(?string $storeHandle = null, int $methodId = null, int $ruleId = null, ShippingRule $shippingRule = null): Response - { - if ($storeHandle === null || !$store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle)) { - $store = Plugin::getInstance()->getStores()->getPrimaryStore(); - } - - $variables = compact('methodId', 'ruleId', 'shippingRule'); - - $plugin = Plugin::getInstance(); - $variables['shippingMethod'] = $plugin->getShippingMethods()->getShippingMethodById($variables['methodId'], $store->id); - - if (!$variables['shippingMethod']) { - throw new HttpException(404); - } - - if (!$variables['shippingRule']) { - if ($variables['ruleId']) { - $variables['shippingRule'] = $plugin->getShippingRules()->getShippingRuleById($variables['ruleId']); - - if (!$variables['shippingRule']) { - throw new HttpException(404); - } - } else { - $variables['shippingRule'] = new ShippingRule(); - $variables['shippingRule']->methodId = $variables['shippingMethod']->id; - $variables['shippingRule']->storeId = $variables['shippingMethod']->storeId; - } - } - - $this->getView()->setNamespace('new'); - - $this->getView()->startJsBuffer(); - - $newZone = new ShippingAddressZone(); - $condition = $newZone->getCondition(); - $condition->mainTag = 'div'; - $condition->name = 'condition'; - $condition->id = 'condition'; - - $variables['newShippingZoneFields'] = $this->getView()->namespaceInputs( - $this->getView()->renderTemplate('commerce/store-management/shipping/shippingzones/_fields', ['condition' => $condition]) - ); - $variables['newShippingZoneJs'] = $this->getView()->clearJsBuffer(false); - $this->getView()->setNamespace(null); - - if (!empty($variables['ruleId'])) { - $variables['title'] = $variables['shippingRule']->name; - } else { - $variables['title'] = Craft::t('commerce', 'Create a new shipping rule'); - } - - DebugPanel::prependOrAppendModelTab(model: $variables['shippingMethod'], prepend: true); - DebugPanel::prependOrAppendModelTab(model: $variables['shippingRule'], prepend: true); - - $shippingZones = $plugin->getShippingZones()->getAllShippingZones($store->id)->all(); - $variables['shippingZones'] = []; - $variables['shippingZones'][] = Craft::t('commerce', 'Anywhere'); - foreach ($shippingZones as $model) { - $variables['shippingZones'][$model->id] = $model->name; - } - - $variables['categoryShippingOptions'] = []; - $variables['categoryShippingOptions'][] = ['label' => Craft::t('commerce', 'Allow'), 'value' => ShippingRuleCategoryRecord::CONDITION_ALLOW]; - $variables['categoryShippingOptions'][] = ['label' => Craft::t('commerce', 'Disallow'), 'value' => ShippingRuleCategoryRecord::CONDITION_DISALLOW]; - $variables['categoryShippingOptions'][] = ['label' => Craft::t('commerce', 'Require'), 'value' => ShippingRuleCategoryRecord::CONDITION_REQUIRE]; - - $variables['storeId'] = $store->id; - $variables['storeHandle'] = $store->handle; - - return $this->renderTemplate('commerce/store-management/shipping/shippingrules/_edit', $variables); - } - - /** - * Duplicates a shipping rule. - * - * @throws InvalidRouteException - * @since 3.2 - */ - public function actionDuplicate(): ?Response - { - return $this->runAction('save', ['duplicate' => true]); - } - - /** - * @throws BadRequestHttpException - * @throws Exception - */ - public function actionSave(bool $duplicate = false): void - { - $this->requirePostRequest(); - - $shippingRule = new ShippingRule(); - - if (!$duplicate) { - $shippingRule->id = $this->request->getBodyParam('id'); - } - $shippingRule->storeId = $this->request->getBodyParam('storeId'); - - $moneyInputs = [ - 'baseRate', - 'maxRate', - 'minRate', - 'perItemRate', - 'weightRate', - ]; - - foreach ($moneyInputs as $moneyInput) { - $input = $this->request->getBodyParam($moneyInput); - $input += [ - 'currency' => $shippingRule->getStore()->getCurrency(), - ]; - $shippingRule->$moneyInput = (float)MoneyHelper::toDecimal(MoneyHelper::toMoney($input)); - } - - $shippingRule->name = $this->request->getBodyParam('name'); - $shippingRule->description = $this->request->getBodyParam('description'); - $shippingRule->methodId = $this->request->getBodyParam('methodId'); - $shippingRule->enabled = (bool)$this->request->getBodyParam('enabled'); - $shippingRule->orderConditionFormula = trim($this->request->getBodyParam('orderConditionFormula', '')); - $shippingRule->percentageRate = Localization::normalizeNumber($this->request->getBodyParam('percentageRate')); - $shippingRule->setOrderCondition($this->request->getBodyParam('orderCondition')); - $shippingRule->setCustomerCondition($this->request->getBodyParam('customerCondition')); - - $ruleCategories = []; - $allRulesCategories = $this->request->getBodyParam('ruleCategories'); - foreach ($allRulesCategories as $key => $ruleCategory) { - $perItemRate = $ruleCategory['perItemRate']; - $weightRate = $ruleCategory['weightRate']; - $percentageRate = $ruleCategory['percentageRate']; - $ruleCategory['perItemRate'] = (!isset($perItemRate) || trim($perItemRate['value']) === '') - ? null - : MoneyHelper::toDecimal(MoneyHelper::toMoney(array_merge([ - 'currency' => $shippingRule->getStore()->getCurrency(), - ], $perItemRate))); - $ruleCategory['weightRate'] = (!isset($weightRate) || trim($weightRate['value']) === '') - ? null - : MoneyHelper::toDecimal(MoneyHelper::toMoney(array_merge([ - 'currency' => $shippingRule->getStore()->getCurrency(), - ], $weightRate))); - $ruleCategory['percentageRate'] = (!isset($percentageRate) || trim($percentageRate) === '') ? null : Localization::normalizeNumber($percentageRate); - - $ruleCategories[$key] = new ShippingRuleCategory($ruleCategory); - $ruleCategories[$key]->shippingCategoryId = $key; - } - - $shippingRule->setShippingRuleCategories($ruleCategories); - - // Save it - if (Plugin::getInstance()->getShippingRules()->saveShippingRule($shippingRule)) { - $this->setSuccessFlash(Craft::t('commerce', 'Shipping rule saved.')); - $this->redirectToPostedUrl($shippingRule); - } else { - $this->setFailFlash(Craft::t('commerce', 'Couldn’t save shipping rule.')); - } - - // Send the model back to the template - Craft::$app->getUrlManager()->setRouteParams(['shippingRule' => $shippingRule]); - } - - /** - * @return Response - * @throws BadRequestHttpException - * @throws InvalidConfigException - * @throws \yii\db\Exception - */ - public function actionReorder(): Response - { - $this->requirePostRequest(); - $this->requireAcceptsJson(); - - $ids = Json::decode($this->request->getRequiredBodyParam('ids')); - Plugin::getInstance()->getShippingRules()->reorderShippingRules($ids); - - return $this->asSuccess(); - } - - /** - * @return Response - * @throws BadRequestHttpException - * @throws Exception - * @throws InvalidConfigException - * @throws Throwable - * @throws StaleObjectException - */ - public function actionDelete(): Response - { - $this->requirePostRequest(); - - if (Craft::$app->getRequest()->getIsAjax()) { - $this->requireAcceptsJson(); - } - - if (!$id = $this->request->getRequiredBodyParam('id')) { - throw new BadRequestHttpException('Shipping rule ID not submitted'); - } - - $rule = Plugin::getInstance()->getShippingRules()->getShippingRuleById($id); - if (!$rule) { - throw new Exception('Cannot find shipping rule to delete'); - } - - if (!Plugin::getInstance()->getShippingRules()->deleteShippingRuleById($id)) { - return $this->asFailure(Craft::t('commerce', 'Could not delete shipping rule')); - } - - if (Craft::$app->getRequest()->getIsAjax()) { - return $this->asSuccess(); - } - - return $this->redirectToPostedUrl($rule); - } -} diff --git a/src/controllers/ShippingZonesController.php b/src/controllers/ShippingZonesController.php deleted file mode 100644 index dabc609918..0000000000 --- a/src/controllers/ShippingZonesController.php +++ /dev/null @@ -1,224 +0,0 @@ - - * @since 2.0 - */ -class ShippingZonesController extends BaseShippingSettingsController -{ - public function actionIndex(?string $storeHandle = null): Response - { - if ($storeHandle === null || !$store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle)) { - $store = Plugin::getInstance()->getStores()->getPrimaryStore(); - } - - $shippingZones = Plugin::getInstance()->getShippingZones()->getAllShippingZones($store->id); - - // Generate table data - $tableData = []; - foreach ($shippingZones as $shippingZone) { - $label = Html::encode(Craft::t('site', $shippingZone->name)); - $tableData[] = [ - 'id' => $shippingZone->id, - 'title' => Html::a($label, $shippingZone->getCpEditUrl()), - 'url' => $shippingZone->getCpEditUrl(), - 'description' => Html::encode(Craft::t('site', $shippingZone->description)), - ]; - } - - $tableData = Json::encode($tableData); - - $js = <<getView()->registerJs($js, View::POS_END); - - $this->getView()->registerTranslations('commerce', [ - 'Name', - 'Description', - ]); - - return $this->asStoreManagementCpScreen($storeHandle) - ->additionalButtonsHtml(Html::a(Craft::t('commerce', 'New shipping zone'), $store->getStoreSettingsUrl('shippingzones/new'), ['class' => 'btn submit add icon'])) - ->contentHtml(Html::tag('div', '', ['id' => 'shipping-vue-admin-table'])); - } - - /** - * @param int|null $id - * @param ShippingAddressZone|null $shippingZone - * @throws HttpException - */ - public function actionEdit(?string $storeHandle = null, int $id = null, ShippingAddressZone $shippingZone = null): Response - { - if ($storeHandle === null || !$store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle)) { - $store = Plugin::getInstance()->getStores()->getPrimaryStore(); - } - - if (!$shippingZone) { - if ($id) { - $shippingZone = Plugin::getInstance()->getShippingZones()->getShippingZoneById($id, $store->id); - - if (!$shippingZone) { - throw new HttpException(404); - } - } else { - $shippingZone = Craft::createObject([ - 'class' => ShippingAddressZone::class, - 'attributes' => ['storeId' => $store->id], - ]); - } - } - - if ($shippingZone->id) { - $title = $shippingZone->name; - } else { - $title = Craft::t('commerce', 'Create a shipping zone'); - } - - $storeHandle = $store->handle; - - $condition = $shippingZone->getCondition(); - $condition->mainTag = 'div'; - $condition->name = 'condition'; - $condition->id = 'condition'; - - DebugPanel::prependOrAppendModelTab(model: $shippingZone, prepend: true); - - $metadata = []; - if ($shippingZone->id) { - $metadata = [ - Craft::t('app', 'Created at') => Craft::$app->getFormatter()->asDatetime($shippingZone->dateCreated, 'short'), - Craft::t('app', 'Updated at') => Craft::$app->getFormatter()->asDatetime($shippingZone->dateUpdated, 'short'), - ]; - } - - return $this->asStoreManagementCpScreen($storeHandle, false) - ->title($title) - ->addCrumb(Craft::t('commerce', 'Shipping Zones'), $store->getStoreSettingsUrl('shippingzones')) - ->action('commerce/shipping-zones/save') - ->redirectUrl($store->getStoreSettingsUrl('shippingzones')) - ->metaSidebarHtml(Cp::metadataHtml($metadata)) - ->contentTemplate('commerce/store-management/shipping/shippingzones/_edit', [ - 'shippingZone' => $shippingZone, - 'condition' => $condition, - 'store' => $store, - ]); - } - - /** - * @throws Exception - * @throws BadRequestHttpException - */ - public function actionSave(): ?Response - { - $this->requirePostRequest(); - - $shippingZone = new ShippingAddressZone(); - - // Shared attributes - $shippingZone->id = $this->request->getBodyParam('shippingZoneId'); - $shippingZone->storeId = $this->request->getBodyParam('storeId'); - $this->requireStoreAccess($shippingZone->storeId); - $shippingZone->name = $this->request->getBodyParam('name'); - $shippingZone->description = $this->request->getBodyParam('description'); - $shippingZone->setCondition($this->request->getBodyParam('condition')); - - if ($shippingZone->validate() && Plugin::getInstance()->getShippingZones()->saveShippingZone($shippingZone)) { - return $this->asModelSuccess( - $shippingZone, - Craft::t('commerce', 'Shipping zone saved.'), - 'shippingZone', - data: [ - 'id' => $shippingZone->id, - 'name' => $shippingZone->name, - ] - ); - } - - return $this->asModelFailure( - $shippingZone, - Craft::t('commerce', 'Couldn’t save shipping zone.'), - 'shippingZone' - ); - } - - /** - * @throws HttpException - */ - public function actionDelete(): Response - { - $this->requirePostRequest(); - $this->requireAcceptsJson(); - - $id = $this->request->getRequiredBodyParam('id'); - - $shippingZone = Plugin::getInstance()->getShippingZones()->getShippingZoneById($id); - if ($shippingZone) { - $this->requireStoreAccess($shippingZone->storeId); - } - - if (!$shippingZone || !Plugin::getInstance()->getShippingZones()->deleteShippingZoneById($id)) { - return $this->asFailure(Craft::t('commerce', 'Could not delete shipping zone')); - } - - return $this->asSuccess(); - } - - /** - * @throws BadRequestHttpException - * @throws LoaderError - * @throws SyntaxError - * @since 2.2 - */ - public function actionTestZip(): Response - { - $this->requirePostRequest(); - $this->requireAcceptsJson(); - - $zipCodeFormula = (string)$this->request->getRequiredBodyParam('zipCodeConditionFormula'); - $testZipCode = (string)$this->request->getRequiredBodyParam('testZipCode'); - - $params = ['zipCode' => $testZipCode]; - - if (!Plugin::getInstance()->getFormulas()->evaluateCondition($zipCodeFormula, $params)) { - return $this->asFailure('failed'); - } - - return $this->asSuccess(); - } -} diff --git a/src/controllers/StoreManagementController.php b/src/controllers/StoreManagementController.php deleted file mode 100644 index 32cf65a322..0000000000 --- a/src/controllers/StoreManagementController.php +++ /dev/null @@ -1,244 +0,0 @@ - - * @since 4.0 - */ -class StoreManagementController extends BaseStoreManagementController -{ - public function actionIndex(): Response - { - $user = Craft::$app->getUser(); - /** @var Site|HasStoreInterface $site */ - $site = Cp::requestedSite(); - - if ($user->checkPermission('commerce-manageGeneralStoreSettings')) { - return $this->redirect($site->getStore()->getStoreSettingsUrl()); - } - - if ($user->checkPermission('commerce-managePaymentCurrencies')) { - return $this->redirect($site->getStore()->getStoreSettingsUrl('payment-currencies')); - } - - if ($user->checkPermission('commerce-managePromotions')) { - return $this->redirect($site->getStore()->getStoreSettingsUrl('discounts')); - } - - if ($user->checkPermission('commerce-manageShipping')) { - return $this->redirect($site->getStore()->getStoreSettingsUrl('shipping')); - } - - if ($user->checkPermission('commerce-manageTaxes')) { - return $this->redirect($site->getStore()->getStoreSettingsUrl('taxrates')); - } - - return $this->asStoreManagementCpScreen($site->getStore()->handle) - ->contentHtml(Html::tag( - 'p', - Craft::t('commerce', 'No access given to any specific store management features.') - )); - } - - /** - * @return YiiResponse - * @throws TemplateLoaderException - * @throws InvalidConfigException - */ - public function actionEdit(StoreSettings $storeSettings = null, ?string $storeHandle = null): Response - { - $this->requirePermission('commerce-manageGeneralStoreSettings'); - - if (!$storeSettings) { - if ($storeHandle) { - // Store has the same ID as Store Settings - $store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle); - - if (!$store) { - throw new HttpException(404); - } - - $storeSettings = $store->getSettings(); - } else { - // Attempt to redirect the user to the correct store settings for the site they were working on - /** @var Site|StoreBehavior $site */ - $site = Cp::requestedSite(); - return $this->redirect($site->getStore()->getStoreSettingsUrl()); - } - } else { - $store = Plugin::getInstance()->getStores()->getStoreById($storeSettings->id); - } - - $addressesService = Craft::$app->getAddresses(); - $allCountries = $addressesService->getCountryRepository()->getList(Craft::$app->language); - - $locationFieldHtml = Cp::elementCardHtml($storeSettings->getLocationAddress(), [ - 'context' => 'field', - 'inputName' => 'locationAddressId', - 'showActionMenu' => true, - ]); - - // Countries market condition field HTML - $condition = $storeSettings->getMarketAddressCondition(); - $condition->mainTag = 'div'; - $condition->name = 'marketAddressCondition'; - $condition->id = 'marketAddressCondition'; - $marketAddressConditionFieldHtml = Cp::fieldHtml($condition->getBuilderHtml(), [ - 'label' => Craft::t('app', 'Order Address Condition'), - 'instructions' => Craft::t('app', 'Only allow orders with addresses that match the following rules:'), - ]); - - // Countries allowed field HTML - $countriesField = Cp::selectizeFieldHtml([ - 'label' => Craft::t('commerce', 'Country List'), - 'instructions' => Craft::t('commerce', 'The countries that orders are allowed to be placed from.'), - 'id' => 'countries', - 'name' => 'countries', - 'multi' => true, - 'values' => $storeSettings->getCountries(), - 'options' => $allCountries, - 'errors' => $storeSettings->getErrors('countries'), - 'allowEmptyOption' => true, - ]); - - // Inventory locations field HTML - $inventoryLocations = Plugin::getInstance()->getInventoryLocations()->getInventoryLocations($store->id); - $allInventoryLocations = Plugin::getInstance()->getInventoryLocations()->getAllInventoryLocations(); - $currentUser = Craft::$app->getUser()->getIdentity(); - - $locationsCount = count($allInventoryLocations); - $userCanCreate = $currentUser->can('commerce-manageInventoryLocations'); - $inventoryLocationsField = ''; - - if ($userCanCreate) { - $canCreate = false; - - $limit = Plugin::EDITION_PRO_STORE_LIMIT; - if ($locationsCount < $limit) { - $canCreate = true; - } - - if (Plugin::getInstance()->is(Plugin::EDITION_ENTERPRISE, '=')) { - $limit = null; - $canCreate = true; - } - - $config = [ - 'label' => Craft::t('commerce', 'Inventory Locations'), - 'instructions' => Craft::t('commerce', 'The inventory locations this store uses.'), - 'id' => 'inventoryLocations', - 'name' => 'inventoryLocations[]', - 'values' => $inventoryLocations, - 'create' => $canCreate, - ]; - - if ($limit !== null) { - $config['limit'] = $limit; - } - - $inventoryLocationsField = CommerceCp::inventoryLocationFieldHtml($config); - } - - return $this->asStoreManagementCpScreen($storeHandle) - ->action('commerce/store-management/save') - ->redirectUrl($store->getStoreSettingsUrl()) - ->submitButtonLabel(Craft::t('app', 'Save')) - ->contentTemplate('commerce/store-management/general/_edit', [ - 'store' => $store, - 'storeHandle' => $storeHandle, - 'storeSettings' => $storeSettings, - 'marketAddressConditionField' => $marketAddressConditionFieldHtml, - 'countriesField' => $countriesField, - 'locationField' => $locationFieldHtml, - 'inventoryLocationsField' => $inventoryLocationsField, - ]); - } - - /** - * @return YiiResponse|null - * @throws InvalidConfigException - * @throws Throwable - * @throws Exception - */ - public function actionSave(): ?YiiResponse - { - $this->requirePermission('commerce-manageGeneralStoreSettings'); - - $storeId = Craft::$app->getRequest()->getBodyParam('id'); - $this->requireStoreAccess($storeId); - $store = Plugin::getInstance()->getStores()->getStoreById($storeId); - $storeSettings = Plugin::getInstance()->getStoreSettings()->getStoreSettingsById($storeId); - $currentUser = Craft::$app->getUser()->getIdentity(); - - if ($locationAddressId = $this->request->getBodyParam('locationAddressId')) { - /** @var Address|null $locationAddress */ - $locationAddress = Address::find()->id($locationAddressId)->one(); - if ($locationAddress) { - $storeSettings->setLocationAddress($locationAddress); - } - } - $marketAddressCondition = $this->request->getBodyParam('marketAddressCondition') ?? new ZoneAddressCondition(); - $storeSettings->setMarketAddressCondition($marketAddressCondition); - $countries = $this->request->getBodyParam('countries') ?: []; - $storeSettings->setCountries($countries); - - // Save inventory locations - if ($currentUser->can('commerce-manageInventoryLocations')) { - $inventoryLocations = Craft::$app->getRequest()->getParam('inventoryLocations'); - - if (!$inventoryLocations) { - return $this->asFailure( - Craft::t('commerce', 'Missing a default inventory location.'), - - ); - } - - if (!Plugin::getInstance()->getInventoryLocations()->saveStoreInventoryLocations($store, $inventoryLocations)) { - return $this->asFailure( - Craft::t('commerce', 'Inventory locations not saved.') - ); - } - } - - if (!$storeSettings->validate() || !Plugin::getInstance()->getStoreSettings()->saveStoreSettings($storeSettings)) { - return $this->asModelFailure( - model: $storeSettings, - message: Craft::t('commerce', 'Couldn’t save store.'), - modelName: 'storeSettings', - ); - } - - return $this->asModelSuccess( - model: $storeSettings, - message: Craft::t('commerce', 'Store saved.'), - modelName: 'storeSettings', - ); - } -} diff --git a/src/controllers/StoresController.php b/src/controllers/StoresController.php deleted file mode 100644 index 6c48478a01..0000000000 --- a/src/controllers/StoresController.php +++ /dev/null @@ -1,400 +0,0 @@ - - * @since 5.0 - */ -class StoresController extends BaseAdminController -{ - /** - * Edit a store. - * - * @param int|null $storeId The Store’s ID, if editing an existing Store - * @param Store|null $storeModel The Store being edited, if there were any validation errors - */ - public function actionEditStore(?int $storeId = null, ?Store $storeModel = null): Response - { - $storesService = Plugin::getInstance()->getStores(); - - $brandNewStore = false; - $allowCurrencyChange = false; - - if ($storeId !== null) { - if ($storeModel === null) { - $storeModel = $storesService->getStoreById($storeId); - - if (!$storeModel) { - throw new NotFoundHttpException('Store not found'); - } - } - - $title = trim($storeModel->getName()) ?: Craft::t('app', 'Edit Store'); - } else { - if ($storeModel === null) { - $storeModel = new Store(); - $brandNewStore = true; - $allowCurrencyChange = true; - } - - $title = Craft::t('app', 'Create a new Store'); - } - - // Breadcrumbs - $crumbs = [ - [ - 'label' => Craft::t('commerce', 'Commerce'), - 'url' => UrlHelper::url('commerce'), - ], - [ - 'label' => Craft::t('commerce', 'Settings'), - 'url' => UrlHelper::url('commerce/settings'), - ], - [ - 'label' => Craft::t('app', 'Stores'), - 'url' => UrlHelper::url('commerce/settings/stores'), - ], - ]; - - $hasOrders = $storeModel->id && Order::find() - ->trashed(null) - ->storeId($storeModel->id) - ->exists(); - - if (!$hasOrders) { - $allowCurrencyChange = true; - } - - // map sites into select box options array - $availableSiteOptions = collect(Craft::$app->getSites()->getAllSites())->map(function($site) { - $availableForAssignmentToNewStores = Plugin::getInstance()->getStores()->getSiteIdsAvailableForAssignmentToNewStores(); - return [ - 'label' => $site->name, - 'value' => $site->id, - 'disabled' => collect($availableForAssignmentToNewStores)->contains($site->id) === false, - ]; - })->all(); - - $currencyOptions = Plugin::getInstance()->getCurrencies()->getAllCurrenciesList(); - - return $this->renderTemplate('commerce/settings/stores/_edit', [ - 'brandNewStore' => $brandNewStore, - 'allowCurrencyChange' => $allowCurrencyChange, - 'title' => $title, - 'crumbs' => $crumbs, - 'store' => $storeModel, - 'currencyOptions' => $currencyOptions, - 'availableSiteOptions' => $availableSiteOptions, - 'freeOrderPaymentStrategyOptions' => $storeModel->getFreeOrderPaymentStrategyOptions(), - 'minimumTotalPriceStrategyOptions' => $storeModel->getMinimumTotalPriceStrategyOptions(), - 'readOnly' => $this->isReadOnlyScreen(), - ]); - } - - /** - * Saves a store. - * - * @return Response|null - * @throws BadRequestHttpException - * @throws BusyResourceException - * @throws StaleResourceException - * @throws ErrorException - * @throws Exception - * @throws InvalidConfigException - * @throws NotSupportedException - * @throws ServerErrorHttpException - */ - public function actionSaveStore(): ?Response - { - $this->requirePostRequest(); - - $storesService = Plugin::getInstance()->getStores(); - $storeId = $this->request->getBodyParam('storeId'); - - if ($storeId) { - $store = $storesService->getStoreById($storeId); - if (!$store) { - throw new BadRequestHttpException("Invalid store ID: $storeId"); - } - } else { - $store = new Store(); - } - - $store->setName($this->request->getBodyParam('name')); - $store->handle = $this->request->getBodyParam('handle'); - $store->setAutoSetNewCartAddresses($this->request->getBodyParam('autoSetNewCartAddresses')); - $store->setAutoSetCartShippingMethodOption($this->request->getBodyParam('autoSetCartShippingMethodOption')); - $store->setAutoSetPaymentSource($this->request->getBodyParam('autoSetPaymentSource')); - $store->setAllowEmptyCartOnCheckout($this->request->getBodyParam('allowEmptyCartOnCheckout')); - $store->setAllowCheckoutWithoutPayment($this->request->getBodyParam('allowCheckoutWithoutPayment')); - $store->setAllowPartialPaymentOnCheckout($this->request->getBodyParam('allowPartialPaymentOnCheckout')); - $store->setRequireShippingAddressAtCheckout($this->request->getBodyParam('requireShippingAddressAtCheckout')); - $store->setRequireBillingAddressAtCheckout($this->request->getBodyParam('requireBillingAddressAtCheckout')); - $store->setRequireShippingMethodSelectionAtCheckout($this->request->getBodyParam('requireShippingMethodSelectionAtCheckout')); - $store->setUseBillingAddressForTax($this->request->getBodyParam('useBillingAddressForTax')); - $store->setValidateOrganizationTaxIdAsVatId($this->request->getBodyParam('validateOrganizationTaxIdAsVatId')); - $store->setOrderReferenceFormat($this->request->getBodyParam('orderReferenceFormat')); - $store->setFreeOrderPaymentStrategy($this->request->getBodyParam('freeOrderPaymentStrategy')); - $store->setMinimumTotalPriceStrategy($this->request->getBodyParam('minimumTotalPriceStrategy')); - $store->primary = (bool)$this->request->getBodyParam('primary', $store->primary); - - if ($currency = $this->request->getBodyParam('currency')) { - $store->setCurrency($currency); - } - - if ($storeId && $savedStore = $storesService->getStoreById($storeId)) { - $store->uid = $savedStore->uid; - $store->sortOrder = $savedStore->sortOrder; - } elseif (!$storeId) { - $store->sortOrder = (new Query())->from(Table::STORES)->max('[[sortOrder]]') + 1; - } - - // Save it - if (!$store->validate() || !$storesService->saveStore($store)) { - $this->setFailFlash(Craft::t('app', 'Couldn’t save the store.')); - - // Send the store back to the template - Craft::$app->getUrlManager()->setRouteParams([ - 'storeModel' => $store, - ]); - - return null; - } - - // Create the site store relationship for this new order - if ($siteId = $this->request->getBodyParam('siteId')) { - $siteStore = collect($storesService->getAllSiteStores())->where('siteId', $siteId)->first(); - $siteStore->storeId = $store->id; - $storesService->saveSiteStore($siteStore); - } - - - $this->setSuccessFlash(Craft::t('app', 'Store saved.')); - return $this->redirectToPostedUrl($store); - } - - - /** - * @return Response - * @throws \yii\base\InvalidConfigException - */ - public function actionStoresIndex(): Response - { - $stores = Plugin::getInstance()->getStores()->getAllStores(); - - // Breadcrumbs - $crumbs = [ - [ - 'label' => Craft::t('commerce', 'Commerce'), - 'url' => UrlHelper::url('commerce'), - ], - ]; - - $menuItems = []; - $stores->each(function(Store $s) use (&$menuItems) { - $m = []; - $m[] = [ - 'label' => Craft::t('commerce', 'Payment Currencies'), - 'url' => UrlHelper::cpUrl('commerce/store-management/' . $s->handle . '/payment-currencies'), - ]; - - $m[] = [ - 'label' => Craft::t('commerce', 'Discounts'), - 'url' => UrlHelper::cpUrl('commerce/store-management/' . $s->handle . '/discounts'), - ]; - - if (Plugin::getInstance()->getCatalogPricingRules()->canUseCatalogPricingRules()) { - $m[] = [ - 'label' => Craft::t('commerce', 'Pricing Rules'), - 'url' => UrlHelper::cpUrl('commerce/store-management/' . $s->handle . '/pricing-rules'), - ]; - } else { - $m[] = [ - 'label' => Craft::t('commerce', 'Sales'), - 'url' => UrlHelper::cpUrl('commerce/store-management/' . $s->handle . '/sales'), - ]; - } - - $m[] = [ - 'label' => Craft::t('commerce', 'Shipping Methods'), - 'url' => UrlHelper::cpUrl('commerce/store-management/' . $s->handle . '/shippingmethods'), - ]; - - $m[] = [ - 'label' => Craft::t('commerce', 'Shipping Zones'), - 'url' => UrlHelper::cpUrl('commerce/store-management/' . $s->handle . '/shippingzones'), - ]; - - $m[] = [ - 'label' => Craft::t('commerce', 'Shipping Categories'), - 'url' => UrlHelper::cpUrl('commerce/store-management/' . $s->handle . '/shippingcategories'), - ]; - - $m[] = [ - 'label' => Craft::t('commerce', 'Tax Rates'), - 'url' => UrlHelper::cpUrl('commerce/store-management/' . $s->handle . '/taxrates'), - ]; - - $m[] = [ - 'label' => Craft::t('commerce', 'Tax Zones'), - 'url' => UrlHelper::cpUrl('commerce/store-management/' . $s->handle . '/taxzones'), - ]; - - $m[] = [ - 'label' => Craft::t('commerce', 'Tax Categories'), - 'url' => UrlHelper::cpUrl('commerce/store-management/' . $s->handle . '/taxcategories'), - ]; - - $menuItems[$s->handle] = $m; - }); - - - return $this->renderTemplate('commerce/settings/stores/index', [ - 'stores' => $stores, - 'crumbs' => $crumbs, - 'sitesStores' => Plugin::getInstance()->getStores()->getAllSiteStores(), - 'primaryStoreId' => Plugin::getInstance()->getStores()->getPrimaryStore()->id, - 'menuItems' => $menuItems, - 'readOnly' => $this->isReadOnlyScreen(), - ]); - } - - /** - * Deletes a store. - * - * @return Response - */ - public function actionDeleteStore(): Response - { - $this->requirePostRequest(); - $this->requireAcceptsJson(); - - $siteId = $this->request->getRequiredBodyParam('id'); - - Plugin::getInstance()->getStores()->deleteStoreById($siteId); - - return $this->asSuccess(); - } - - /** - * @return Response - * @throws BadRequestHttpException - * @throws ErrorException - * @throws Exception - * @throws InvalidConfigException - * @throws NotSupportedException - * @throws ServerErrorHttpException - */ - public function actionReorderStores(): Response - { - $this->requirePostRequest(); - $this->requireAcceptsJson(); - - $ids = Json::decode($this->request->getRequiredBodyParam('ids')); - - if (!Plugin::getInstance()->getStores()->reorderStores($ids)) { - return $this->asFailure(Craft::t('commerce', 'Couldn’t reorder stores.')); - } - - return $this->asSuccess(); - } - - /** - * @param Collection|null $sitesStores - * @return Response - * @throws InvalidConfigException - */ - public function actionEditSiteStores(Collection $sitesStores = null): Response - { - // Breadcrumbs - $crumbs = [ - [ - 'label' => Craft::t('commerce', 'Commerce'), - 'url' => UrlHelper::url('commerce'), - ], - ]; - - return $this->renderTemplate('commerce/settings/stores/_siteStore', [ - 'crumbs' => $crumbs, - 'stores' => Plugin::getInstance()->getStores()->getAllStores(), - 'sites' => Craft::$app->getSites()->getAllSites(), - 'sitesStores' => $sitesStores ?? Plugin::getInstance()->getStores()->getAllSiteStores(), - 'primaryStoreId' => Plugin::getInstance()->getStores()->getPrimaryStore()->id, - 'readOnly' => $this->isReadOnlyScreen(), - ]); - } - - /** - * Saves the site settings records - * - * @return ?Response - */ - public function actionSaveSiteStores(): ?Response - { - $siteStoresData = $this->request->getBodyParam('siteStores', []); - $sitesStores = Plugin::getInstance()->getStores()->getAllSiteStores(); - $stores = Plugin::getInstance()->getStores()->getAllStores(); - - foreach ($sitesStores as $siteStore) { - if (isset($siteStoresData[$siteStore->siteId])) { - $siteStore->storeId = $siteStoresData[$siteStore->siteId]['storeId']; - } - } - - $unassignedStores = []; - foreach ($stores as $store) { - $storeAssigned = false; - foreach ($sitesStores as $siteStore) { - if ($siteStore->storeId == $store->id) { - $storeAssigned = true; - } - } - if (!$storeAssigned) { - $unassignedStores[] = $store->getName(); - } - } - if ($unassignedStores) { - return $this->asFailure( - Craft::t('commerce', '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.', [ - 'storeNames' => implode(', ', $unassignedStores), - 'num' => count($unassignedStores), - ]), - routeParams: ['sitesStores' => collect($sitesStores)] - ); - } - - foreach ($sitesStores as $siteStore) { - Plugin::getInstance()->getStores()->saveSiteStore($siteStore); - } - - return $this->asSuccess(Craft::t('commerce', 'Site store mapping saved.')); - } -} diff --git a/src/controllers/SubscriptionsController.php b/src/controllers/SubscriptionsController.php deleted file mode 100644 index d7c7c785d7..0000000000 --- a/src/controllers/SubscriptionsController.php +++ /dev/null @@ -1,652 +0,0 @@ - - * @since 2.0 - */ -class SubscriptionsController extends BaseController -{ - /** - * @throws ForbiddenHttpException - */ - public function actionIndex(): Response - { - $this->requirePermission('commerce-manageSubscriptions'); - return $this->renderTemplate('commerce/subscriptions/_index'); - } - - /** - * @param int|null $subscriptionId - * @param Subscription|null $subscription - * @throws HttpException - * @throws InvalidConfigException - */ - public function actionEdit(int $subscriptionId = null, Subscription $subscription = null): Response - { - $variables = []; - - $this->getView()->registerAssetBundle(CommerceCpAsset::class); - - if ($subscription === null && $subscriptionId) { - /** @var Subscription|null $subscription */ - $subscription = Subscription::find()->status(null)->id($subscriptionId)->one(); - } - - if (!$subscription) { - throw new NotFoundHttpException('Subscription not found'); - } - - $this->enforceManageSubscriptionPermissions($subscription); - - $fieldLayout = Craft::$app->getFields()->getLayoutByType(Subscription::class); - - $form = $fieldLayout->createForm($subscription); - $tabMenu = $form->getTabMenu(); - $tabMenu['tab--subscriptionManageTab'] = [ - 'label' => Craft::t('commerce', 'Manage'), - 'url' => '#tab--subscriptionManageTab', - 'class' => null, - ]; - $variables['tabs'] = $tabMenu; - $variables['fieldsHtml'] = $form->render(); - - $variables['continueEditingUrl'] = $subscription->getCpEditUrl(); - $variables['subscriptionId'] = $subscriptionId; - $variables['subscription'] = $subscription; - $variables['fieldLayout'] = $fieldLayout; - - return $this->renderTemplate('commerce/subscriptions/_edit', $variables); - } - - /** - * Save a subscription's custom fields. - * - * @throws NotFoundHttpException if subscription not found - * @throws ForbiddenHttpException if permissions are lacking - * @throws HttpException if invalid data posted - * @throws Throwable if reasons - */ - public function actionSave(): ?Response - { - $this->requirePostRequest(); - - $subscriptionId = $this->request->getRequiredBodyParam('subscriptionId'); - /** @var Subscription|null $subscription */ - $subscription = Subscription::find()->status(null)->id($subscriptionId)->one(); - - if (!$subscription) { - throw new NotFoundHttpException('Subscription not found'); - } - - if (!$this->_canUpdateSubscription($subscription) === true) { - $this->enforceManageSubscriptionPermissions($subscription); - } - - $subscription->setFieldValuesFromRequest('fields'); - - $subscription->setScenario(Element::SCENARIO_LIVE); - - if (!Craft::$app->getElements()->saveElement($subscription)) { - $this->setFailFlash(Craft::t('commerce', 'Couldn’t save subscription.')); - Craft::$app->getUrlManager()->setRouteParams([ - 'subscription' => $subscription, - ]); - return null; - } - - return $this->redirectToPostedUrl($subscription); - } - - /** - * Refreshes all subscription payments - * - * @throws BadRequestHttpException If not POST request - * @throws ForbiddenHttpException If permissions are lacking - * @throws NotFoundHttpException If subscription not found - * @throws InvalidConfigException - */ - public function actionRefreshPayments(): Response - { - $this->requirePostRequest(); - $this->requirePermission('commerce-manageSubscriptions'); - - $subscriptionId = $this->request->getRequiredBodyParam('subscriptionId'); - - if (!$subscription = Subscription::find()->status(null)->id($subscriptionId)->one()) { - throw new NotFoundHttpException('Subscription not found'); - } - - /** @var Subscription $subscription */ - $gateway = $subscription->getGateway(); - $gateway->refreshPaymentHistory($subscription); - - // Save - return $this->redirectToPostedUrl($subscription); - } - - /** - * @throws Exception - * @throws HttpException if request does not match requirements - * @throws InvalidConfigException if gateway does not support subscriptions - * @throws BadRequestHttpException - */ - public function actionSubscribe(): ?Response - { - $this->requireLogin(); - $this->requirePostRequest(); - $user = Craft::$app->getUser()->getIdentity(); - - $returnUrl = $this->request->getValidatedBodyParam('redirect'); - - $plugin = Commerce::getInstance(); - - $planUid = $this->request->getValidatedBodyParam('planUid'); - - if (!$planUid || !$plan = $plugin->getPlans()->getPlanByUid($planUid)) { - throw new InvalidConfigException('Subscription plan not found with that id.'); - } - - $error = null; - $subscription = null; - - try { - /** @var SubscriptionGateway $gateway */ - $gateway = $plan->getGateway(); - $parameters = $gateway->getSubscriptionFormModel(); - - foreach ($parameters->attributes() as $attributeName) { - $value = $this->request->getValidatedBodyParam($attributeName); - - if (is_string($value) && StringHelper::countSubstrings($value, ':') > 0) { - [$hashedPlanUid, $parameterValue] = explode(':', $value); - - if ($plan->uid == $hashedPlanUid) { - $parameters->{$attributeName} = $parameterValue; - } - } - } - - try { - $paymentFormData = $this->request->getBodyParam(PaymentForm::getPaymentFormParamName($gateway->handle)) ?? []; - - if (!empty($paymentFormData)) { - Craft::$app->getDeprecator()->log('SubscriptionController::create-newPaymentMethod', 'The subscription create action now requires that a customer’s default payment source is set up before subscribing, or pass the payment source information to the subscribe form.'); - - $createPaymentSource = function($gateway, $paymentFormData) use ($plugin) { - $paymentForm = $gateway->getPaymentFormModel(); - $paymentForm->setAttributes($paymentFormData, false); - - if ($paymentForm->validate()) { - $plugin->getPaymentSources()->createPaymentSource(Craft::$app->getUser()->getId(), $gateway, $paymentForm); - } - }; - - $exists = class_exists(PaymentIntents::class); - /** @phpstan-ignore-next-line */ - if ($exists && $plan->getGateway() instanceof PaymentIntents) { - if (isset($paymentFormData['paymentMethodId'])) { - $createPaymentSource($gateway, $paymentFormData); - } - } else { - $createPaymentSource($gateway, $paymentFormData); - } - } - - $fieldsLocation = $this->request->getParam('fieldsLocation', 'fields'); - $fieldValues = $this->request->getBodyParam($fieldsLocation, []); - - $subscription = $plugin->getSubscriptions()->createSubscription($user, $plan, $parameters, $fieldValues); - } catch (\Exception $exception) { - Craft::$app->getErrorHandler()->logException($exception); - - throw new SubscriptionException(Craft::t('commerce', 'Unable to start the subscription. ' . $exception->getMessage())); - } - } catch (SubscriptionException $exception) { - $error = $exception->getMessage(); - } - - if ($subscription && $returnUrl) { - $returnUrl = $this->getView()->renderSandboxedObjectTemplate($returnUrl, $subscription); - $subscriptionRecord = SubscriptionRecord::findOne($subscription->id); - $subscriptionRecord->returnUrl = $returnUrl; - $subscriptionRecord->save(); - $subscription->returnUrl = $returnUrl; - } - - if (!$error && $subscription && $subscription->isSuspended && !$subscription->hasStarted) { - $url = Plugin::getInstance()->getSettings()->updateBillingDetailsUrl; - - if (empty($url)) { - $error = Craft::t('commerce', 'Unable to start the subscription. Please check your payment details.'); - } else { - return $this->redirect(UrlHelper::url(App::parseEnv($url), ['subscription' => $subscription->uid])); - } - } - - if ($error) { - return $this->asFailure($error); - } - - return $this->asSuccess( - Craft::t('commerce', 'Subscription started.'), - data: [ - 'subscription' => $subscription ?? null, - ], - redirect: $returnUrl - ); - } - - /** - * @throws BadRequestHttpException - * @throws Throwable - */ - public function actionReactivate(): ?Response - { - $this->requireLogin(); - $this->requirePostRequest(); - - $plugin = Commerce::getInstance(); - - $error = false; - $subscription = null; - - try { - $subscriptionUid = $this->request->getValidatedBodyParam('subscriptionUid'); - /** @var Subscription|null $subscription */ - $subscription = Subscription::find()->status(null)->uid($subscriptionUid)->one(); - - $validData = $subscriptionUid && $subscription; - $validAction = $subscription->canReactivate(); - $canModifySubscription = Craft::$app->getElements()->canSave($subscription); - - if (($validData && $validAction && $canModifySubscription) || $this->_canUpdateSubscription($subscription)) { - if (!$plugin->getSubscriptions()->reactivateSubscription($subscription)) { - $error = Craft::t('commerce', 'Unable to reactivate subscription at this time.'); - } - } else { - $error = Craft::t('commerce', 'Unable to reactivate subscription at this time.'); - } - } catch (Exception $exception) { - $error = $exception->getMessage(); - } - - if ($error) { - return $this->asFailure($error); - } - - return $this->asSuccess( - Craft::t('commerce', 'Subscription reactivated.'), - data: [ - 'subscription' => $subscription, - ] - ); - } - - /** - * @throws InvalidConfigException - * @throws BadRequestHttpException - */ - public function actionSwitch(): ?Response - { - $this->requireLogin(); - $this->requirePostRequest(); - - $plugin = Commerce::getInstance(); - - $subscriptionUid = $this->request->getValidatedBodyParam('subscriptionUid'); - $planUid = $this->request->getValidatedBodyParam('planUid'); - - $error = false; - - try { - /** @var Subscription|null $subscription */ - $subscription = Subscription::find()->status(null)->uid($subscriptionUid)->one(); - $plan = Commerce::getInstance()->getPlans()->getPlanByUid($planUid); - - $validData = $planUid && $plan && $subscriptionUid && $subscription; - $validAction = $plan->canSwitchFrom($subscription->getPlan()); - $canModifySubscription = Craft::$app->getElements()->canSave($subscription); - - if (($validData && $validAction && $canModifySubscription) || $this->_canUpdateSubscription($subscription)) { - /** @var SubscriptionGateway $gateway */ - $gateway = $subscription->getGateway(); - $parameters = $gateway->getSwitchPlansFormModel(); - - foreach ($parameters->attributes() as $attributeName) { - $value = $this->request->getValidatedBodyParam($attributeName); - - if (is_string($value) && StringHelper::countSubstrings($value, ':') > 0) { - [$hashedPlanUid, $parameterValue] = explode(':', $value); - - if ($hashedPlanUid == $planUid) { - $parameters->{$attributeName} = $parameterValue; - } - } - } - - if (!$plugin->getSubscriptions()->switchSubscriptionPlan($subscription, $plan, $parameters)) { - $error = Craft::t('commerce', 'Unable to modify subscription at this time.'); - } - } else { - $error = Craft::t('commerce', 'Unable to modify subscription at this time.'); - } - } catch (SubscriptionException $exception) { - return $this->asFailure($exception->getMessage()); - } - - if ($error) { - return $this->asFailure($error); - } - - return $this->asSuccess( - Craft::t('commerce', 'Subscription switched.'), - data: [ - 'subscription' => $subscription, - ] - ); - } - - /** - * @throws InvalidConfigException - * @throws BadRequestHttpException - */ - public function actionCancel(): ?Response - { - $this->requireLogin(); - $this->requirePostRequest(); - - $plugin = Commerce::getInstance(); - - $error = false; - $subscription = null; - - try { - $subscriptionUid = $this->request->getValidatedBodyParam('subscriptionUid'); - /** @var Subscription|null $subscription */ - $subscription = Subscription::find()->status(null)->uid($subscriptionUid)->one(); - $validData = $subscriptionUid && $subscription; - - $canModifySubscription = Craft::$app->getElements()->canSave($subscription); - - if (($validData === true && $canModifySubscription === true) || $this->_canUpdateSubscription($subscription)) { - /** @var SubscriptionGateway $gateway */ - $gateway = $subscription->getGateway(); - $parameters = $gateway->getCancelSubscriptionFormModel(); - - foreach ($parameters->attributes() as $attributeName) { - $value = $this->request->getValidatedBodyParam($attributeName); - - if (is_string($value) && StringHelper::countSubstrings($value, ':') > 0) { - [$hashedSubscriptionUid, $parameterValue] = explode(':', $value); - - if ($hashedSubscriptionUid == $subscriptionUid) { - $parameters->{$attributeName} = $parameterValue; - } - } - } - - if (!$plugin->getSubscriptions()->cancelSubscription($subscription, $parameters)) { - $error = Craft::t('commerce', 'Unable to cancel subscription at this time.'); - } - } else { - $error = Craft::t('commerce', 'Unable to cancel subscription at this time.'); - } - } catch (SubscriptionException $exception) { - $error = $exception->getMessage(); - } - - if ($error) { - return $this->asFailure($error); - } - - return $this->asSuccess( - Craft::t('commerce', 'Subscription cancelled.'), - data: [ - 'subscription' => $subscription, - ] - ); - } - - public function actionCompleteSubscription(): ?Response - { - $subscriptionUid = $this->request->getRequiredQueryParam('subscription'); - $subscription = Subscription::find()->status(null)->uid($subscriptionUid)->one(); - - if (!$subscription) { - throw new NotFoundHttpException('Subscription not found'); - } - - $gateway = $subscription->getGateway(); - $transactionHash = $gateway->getTransactionHashFromWebhook(); - $useMutex = (bool)$transactionHash; - $transactionLockName = 'commerceTransaction:' . $transactionHash; - $mutex = Craft::$app->getMutex(); - - if ($useMutex && !$mutex->acquire($transactionLockName, 15)) { - throw new Exception('Unable to acquire a lock for transaction: ' . $transactionHash); - } - - $gateway->refreshPaymentHistory($subscription); - - if ($useMutex) { - $mutex->release($transactionLockName); - } - - return $this->asSuccess(redirect: $subscription->returnUrl); - } - - - /** - * @since 5.7.0 - */ - public function actionDeleteSubscriptionsModal(): Response - { - $this->requireCpRequest(); - $this->requireAcceptsJson(); - $this->requirePermission('deleteUsers'); - - $numSubscriptions = count($this->request->getRequiredParam('subscriptionIds')); - - return $this->_renderGatewayCancelModal('commerce/subscriptions/delete-subscriptions') - ->submitButtonLabel(Craft::t('app', 'Delete {type}', [ - 'type' => $numSubscriptions === 1 ? Subscription::lowerDisplayName() : Subscription::pluralLowerDisplayName(), - ])); - } - - /** - * @since 5.7.0 - */ - public function actionDeleteSubscriptions(): Response - { - $this->requireCpRequest(); - $this->requireAcceptsJson(); - $this->requirePermission('deleteUsers'); - - $subscriptions = $this->_subscriptionsFromRequest(); - $this->_cancelSubscriptionsAtGateway($subscriptions); - - foreach ($subscriptions as $subscription) { - if (!Craft::$app->getElements()->deleteElement($subscription)) { - Craft::warning('Failed to delete subscription ' . $subscription->id . ' (' . $subscription->reference . ')', __METHOD__); - } - } - - $numSubscriptions = count($subscriptions); - - return $this->asSuccess(Craft::t('app', '{type} deleted.', [ - 'type' => $numSubscriptions === 1 ? Subscription::displayName() : Subscription::pluralDisplayName(), - ])); - } - - /** - * Returns the gateway cancel modal response, with an action URL for the submit endpoint. - */ - private function _renderGatewayCancelModal(string $actionUrl): \craft\web\Response - { - $subscriptionIds = collect($this->request->getRequiredParam('subscriptionIds'))->filter()->map(fn($id) => (int)$id)->all(); - $gatewayId = (int)$this->request->getRequiredParam('gatewayId'); - - $gateway = Plugin::getInstance()->getGateways()->getGatewayById($gatewayId); - $subscription = Subscription::find()->id($subscriptionIds)->status(null)->one(); - - $cancelFormHtml = ''; - if ($gateway instanceof SubscriptionGateway && $subscription) { - $cancelFormHtml = $gateway->getCancelSubscriptionFormHtml($subscription); - } - - return $this->asCpModal() - ->action($actionUrl) - ->contentHtml(function() use ($cancelFormHtml, $subscriptionIds, $gatewayId) { - $view = Craft::$app->getView(); - - if ($cancelFormHtml) { - $view->registerJsWithVars( - fn($formId, $inputName) => <<namespaceInputId('cancel-form'), - $view->namespaceInputName('cancelWithGateway'), - ] - ); - } - - return Cp::fieldHtml('template:_includes/forms/radioGroup.twig', [ - 'label' => Craft::t('commerce', 'Gateway'), - 'name' => 'cancelWithGateway', - 'value' => '1', - 'options' => [ - ['label' => Craft::t('commerce', 'Cancel with gateway now'), 'value' => '1'], - ['label' => Craft::t('commerce', 'Leave gateway subscription as-is'), 'value' => '0'], - ], - ]) . - ($cancelFormHtml ? Html::tag('div', $cancelFormHtml, ['id' => 'cancel-form']) : '') . - implode('', array_map(fn($id) => Html::hiddenInput('subscriptionIds[]', (string)$id), $subscriptionIds)) . - Html::hiddenInput('gatewayId', (string)$gatewayId); - }); - } - - /** - * @return Subscription[] - */ - private function _subscriptionsFromRequest(): array - { - $subscriptionIds = collect($this->request->getRequiredParam('subscriptionIds'))->filter()->map(fn($id) => (int)$id)->all(); - - return Subscription::find() - ->id($subscriptionIds) - ->status(null) - ->all(); - } - - /** - * Cancels the given subscriptions at the gateway if the request opted in. Returns whether anything was cancelled. - * - * @param Subscription[] $subscriptions - */ - private function _cancelSubscriptionsAtGateway(array $subscriptions): bool - { - $cancelWithGateway = (bool)$this->request->getBodyParam('cancelWithGateway', false); - if (!$cancelWithGateway) { - return false; - } - - $gatewayId = (int)$this->request->getRequiredParam('gatewayId'); - $gateway = Plugin::getInstance()->getGateways()->getGatewayById($gatewayId); - if (!$gateway instanceof SubscriptionGateway) { - return false; - } - - $parameters = $gateway->getCancelSubscriptionFormModel(); - foreach ($parameters->attributes() as $attribute) { - $value = $this->request->getBodyParam($attribute); - if ($value !== null) { - $parameters->$attribute = $value; - } - } - - $subscriptionsService = Plugin::getInstance()->getSubscriptions(); - $cancelled = false; - - foreach ($subscriptions as $subscription) { - if (!$subscription->isExpired) { - try { - $subscriptionsService->cancelSubscription($subscription, $parameters); - $cancelled = true; - } catch (Throwable $e) { - Craft::warning('Failed to cancel subscription ' . $subscription->reference . ' with gateway: ' . $e->getMessage(), __METHOD__); - } - } - } - - return $cancelled; - } - - /** - * @param Subscription $subscription - * @throws ForbiddenHttpException - */ - protected function enforceManageSubscriptionPermissions(Subscription $subscription) - { - if (!Craft::$app->getElements()->canView($subscription)) { - throw new ForbiddenHttpException('User not authorized to view this subscription.'); - } - } - - /** - * @param Subscription $subscription - * @return bool - * @throws Throwable - */ - private function _canUpdateSubscription(Subscription $subscription): bool - { - $currentUser = Craft::$app->getUser()->getIdentity(); - - $isOwner = $subscription->userId === $currentUser->id; - $isFrontEnd = !Craft::$app->getRequest()->getIsCpRequest(); - - return ($isOwner === true && $isFrontEnd === true); - } -} diff --git a/src/controllers/TaxCategoriesController.php b/src/controllers/TaxCategoriesController.php deleted file mode 100644 index a2553e37a3..0000000000 --- a/src/controllers/TaxCategoriesController.php +++ /dev/null @@ -1,316 +0,0 @@ - - * @since 2.0 - */ -class TaxCategoriesController extends BaseTaxSettingsController -{ - /** - * @param string|null $storeHandle - * @return Response - * @throws InvalidConfigException - */ - public function actionIndex(?string $storeHandle = null): Response - { - if ($storeHandle === null || !$store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle)) { - $store = Plugin::getInstance()->getStores()->getPrimaryStore(); - } - - $taxCategories = Plugin::getInstance()->getTaxCategories()->getAllTaxCategories(); - - // Generate table data with chips - $tableData = []; - foreach ($taxCategories as $taxCategory) { - $label = Html::encode(Craft::t('site', $taxCategory->name)); - $taxRates = $taxCategory->getTaxRates($store->id); - $tableData[] = [ - 'id' => $taxCategory->id, - 'title' => $label, - 'chip' => Cp::chipHtml($taxCategory, [ - 'labelHtml' => Html::a($label, $taxCategory->getCpEditUrl($store->id), [ - 'class' => ['chip-label', 'cell-bold'], - ]), - ]), - 'url' => $taxCategory->getCpEditUrl($store->id), - 'handle' => $taxCategory->handle, - 'description' => Html::encode(Craft::t('site', $taxCategory->description)), - 'default' => $taxCategory->default, - '_showDelete' => $taxRates->isEmpty() && (count($taxCategories) > 1 && !$taxCategory->default), - ]; - } - - $this->getView()->registerTranslations('commerce', [ - 'Default?', - 'Description', - 'Handle', - 'Name', - 'Set default category', - 'Used By Tax Rates', - 'Used by Tax Rates', - ]); - - $buttons = Plugin::getInstance()->getTaxes()->taxCategoryActionHtml(); - if (Plugin::getInstance()->getTaxes()->createTaxCategories()) { - $buttons .= Html::a(Craft::t('commerce', 'New tax category'), $store->getStoreSettingsUrl('taxcategories/new'), [ - 'class' => ['btn', 'submit', 'add', 'icon'], - ]); - } - - $tableData = Json::encode($tableData); - $deleteAction = Plugin::getInstance()->getTaxes()->deleteTaxCategories() ? "'commerce/tax-categories/delete'" : 'null'; - - $js = <<'; - } - } - }, - ]; - - var actions = [ - { - label: '', - icon: 'settings', - actions: [ - { - label: Craft.t('commerce', 'Set default category'), - action: 'commerce/tax-categories/set-default-category', - param: 'default', - value: 1, - allowMultiple: false - } - ] - } - ]; - - new Craft.VueAdminTable({ - columns: columns, - checkboxes: true, - actions: actions, - padded: true, - container: '#tax-vue-admin-table', - deleteAction: {$deleteAction}, - tableData: {$tableData}, - }); -JS; - - $this->getView()->registerJs($js, View::POS_END); - - return $this->asStoreManagementCpScreen($storeHandle, hasStoreSwitcher: false) - ->additionalButtonsHtml($buttons) - ->contentHtml(Html::tag('div', '', ['id' => 'tax-vue-admin-table'])); - } - - /** - * @param int|null $id - * @param TaxCategory|null $taxCategory - * @throws HttpException - */ - public function actionEdit(?string $storeHandle = null, int $id = null, TaxCategory $taxCategory = null): Response - { - if ($storeHandle === null || !$store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle)) { - $store = Plugin::getInstance()->getStores()->getPrimaryStore(); - } - - $storeHandle = $store->handle; - - $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); - - if (!$taxCategory) { - if ($id) { - $taxCategory = Plugin::getInstance()->getTaxCategories()->getTaxCategoryById($id); - - if (!$taxCategory) { - throw new HttpException(404); - } - } else { - $taxCategory = new TaxCategory(); - } - } - - $title = $taxCategory->id ? $taxCategory->name : Craft::t('commerce', 'Create a new tax category'); - - DebugPanel::prependOrAppendModelTab(model: $taxCategory, prepend: true); - - $productTypesOptions = []; - if (!empty($productTypes)) { - $productTypesOptions = ArrayHelper::map($productTypes, 'id', fn($row) => ['label' => $row->name, 'value' => $row->id]); - } - - $allTaxCategoryIds = array_keys(Plugin::getInstance()->getTaxCategories()->getAllTaxCategories()); - $isDefaultAndOnlyCategory = $id && count($allTaxCategoryIds) === 1 && in_array($id, $allTaxCategoryIds); - - // Get all tax rates for all stores - $taxRates = collect(); - Plugin::getInstance()->getStores()->getAllStores()->each(fn(Store $s) => $taxRates->push(...Plugin::getInstance()->getTaxRates()->getAllTaxRates($s->id)->all())); - - $metaSidebar = ''; - if ($taxCategory->id) { - $metaSidebar = Cp::metadataHtml([ - Craft::t('app', 'Created at') => Craft::$app->getFormatter()->asDatetime($taxCategory->dateCreated, 'short'), - Craft::t('app', 'Updated at') => Craft::$app->getFormatter()->asDatetime($taxCategory->dateUpdated, 'short'), - ]); - } - - return $this->asStoreManagementCpScreen($storeHandle, false, false) - ->title($title) - ->addCrumb(Craft::t('commerce', 'Tax Categories'), $store->getStoreSettingsUrl('taxcategories')) - ->action('commerce/tax-categories/save') - ->redirectUrl($store->getStoreSettingsUrl('taxcategories')) - ->metaSidebarHtml($metaSidebar) - ->contentTemplate('commerce/store-management/tax/taxcategories/_edit', [ - 'taxCategory' => $taxCategory, - 'productTypes' => $productTypes, - 'productTypesOptions' => $productTypesOptions, - 'isDefaultAndOnlyCategory' => $isDefaultAndOnlyCategory, - 'taxRates' => $taxRates, - 'store' => $store, - ]); - } - - /** - * @throws BadRequestHttpException - * @throws Exception - * @noinspection Duplicates - */ - public function actionSave(): ?Response - { - $this->requirePostRequest(); - - $taxCategory = new TaxCategory(); - - // Shared attributes - $taxCategory->id = $this->request->getBodyParam('taxCategoryId'); - $taxCategory->name = $this->request->getBodyParam('name'); - $taxCategory->handle = $this->request->getBodyParam('handle'); - $taxCategory->icon = $this->request->getBodyParam('icon'); - $taxCategory->color = $this->request->getBodyParam('color'); - $taxCategory->description = $this->request->getBodyParam('description'); - $taxCategory->default = (bool)$this->request->getBodyParam('default'); - - // Set the new product types - $postedProductTypes = $this->request->getBodyParam('productTypes', []) ?: []; - $productTypes = []; - foreach ($postedProductTypes as $productTypeId) { - if ($productTypeId && $productType = Plugin::getInstance()->getProductTypes()->getProductTypeById($productTypeId)) { - $productTypes[] = $productType; - } - } - $taxCategory->setProductTypes($productTypes); - - // Save it - if (!Plugin::getInstance()->getTaxCategories()->saveTaxCategory($taxCategory)) { - return $this->asModelFailure( - $taxCategory, - Craft::t('commerce', 'Couldn’t save tax category.'), - 'taxCategory' - ); - } - - return $this->asModelSuccess( - $taxCategory, - Craft::t('commerce', 'Tax category saved.'), - 'taxCategory' - ); - } - - /** - * @throws HttpException - */ - public function actionDelete(): ?Response - { - $this->requirePostRequest(); - - $id = $this->request->getBodyParam('id'); - $ids = $this->request->getBodyParam('ids'); - - if ((!$id && empty($ids)) || ($id && !empty($ids))) { - throw new BadRequestHttpException('id or ids must be specified.'); - } - - if ($id) { - // If it is just the one id we know it has come from an ajax request on the table - $this->requireAcceptsJson(); - $ids = [$id]; - } - - $failedIds = []; - foreach ($ids as $id) { - if (!Plugin::getInstance()->getTaxCategories()->deleteTaxCategoryById($id)) { - $failedIds[] = $id; - } - } - - if (!empty($failedIds)) { - return $this->asFailure(Craft::t('commerce', 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.', [ - 'count' => count($failedIds), - ])); - } - - return $this->asSuccess(Craft::t('commerce', 'Tax categories deleted.')); - } - - /** - * @throws MissingComponentException - * @throws Exception - * @throws BadRequestHttpException - * @since 3.2.9 - */ - public function actionSetDefaultCategory(): ?Response - { - $this->requirePostRequest(); - - $ids = $this->request->getRequiredBodyParam('ids'); - - if (!empty($ids)) { - $id = ArrayHelper::firstValue($ids); - - $taxCategory = Plugin::getInstance()->getTaxCategories()->getTaxCategoryById($id); - if ($taxCategory) { - $taxCategory->default = true; - if (Plugin::getInstance()->getTaxCategories()->saveTaxCategory($taxCategory)) { - $this->setSuccessFlash(Craft::t('commerce', 'Tax category updated.')); - return null; - } - } - } - - $this->setFailFlash(Craft::t('commerce', 'Unable to set default tax category.')); - return null; - } -} diff --git a/src/controllers/TaxRatesController.php b/src/controllers/TaxRatesController.php deleted file mode 100644 index 3623b8e7fd..0000000000 --- a/src/controllers/TaxRatesController.php +++ /dev/null @@ -1,370 +0,0 @@ - - * @since 2.0 - */ -class TaxRatesController extends BaseTaxSettingsController -{ - /** - * @param string|null $storeHandle - * @return Response - * @throws StoreNotFoundException - * @throws InvalidConfigException - */ - public function actionIndex(?string $storeHandle = null): Response - { - if ($storeHandle === null || !$store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle)) { - $store = Plugin::getInstance()->getStores()->getPrimaryStore(); - } - - $plugin = Plugin::getInstance(); - $taxRates = $plugin->getTaxRates()->getAllTaxRates($store->id); - - // Preload all zone and category data for listing. - $plugin->getTaxZones()->getAllTaxZones($store->id); - $plugin->getTaxCategories()->getAllTaxCategories(); - - // Generate table data - $tableData = []; - foreach ($taxRates as $taxRate) { - $label = Html::encode(Craft::t('site', $taxRate->name)); - $tableData[] = [ - 'id' => $taxRate->id, - 'status' => $taxRate->enabled, - 'title' => Html::a($label, $taxRate->getCpEditUrl()), - 'url' => $taxRate->getCpEditUrl(), - 'rate' => $taxRate->getRateAsPercent(), - 'included' => $taxRate->include, - 'removeIncluded' => $taxRate->removeIncluded, - 'vat' => $taxRate->isVat, - 'zone' => $taxRate->isEverywhere ? Craft::t('commerce', 'Everywhere') : ($taxRate->taxZone ? Html::encode($taxRate->taxZone->name) : ''), - 'category' => $taxRate->taxCategory ? Cp::chipHtml($taxRate->taxCategory) : '', - ]; - } - - $this->getView()->registerTranslations('commerce', [ - 'Include in price?', - 'Remove from price?', - 'Name', - 'Rate', - 'Tax Category', - 'Tax Zone', - 'Yes', - ]); - - $buttonsHtml = Plugin::getInstance()->getTaxes()->taxRateActionHtml(); - - if (Plugin::getInstance()->getTaxes()->createTaxRates()) { - $buttonsHtml .= Html::a(Craft::t('commerce', 'New tax rate'), "commerce/store-management/$storeHandle/taxrates/new", [ - 'class' => 'btn submit add icon', - ]); - } - - $tableData = Json::encode($tableData, JSON_UNESCAPED_UNICODE); - $deleteAction = Plugin::getInstance()->getTaxes()->deleteTaxRates() ? 'commerce/tax-rates/delete' : null; - - $js = <<'; - } - } }, - { name: 'removeIncluded', title: Craft.t('commerce', 'Remove from price?'), callback: function(value) { - if (value) { - return ''; - } - } }, - { name: 'zone', title: Craft.t('commerce', 'Tax Zone') }, - { name: 'category', title: Craft.t('commerce', 'Tax Category') } -]; - -var actions = [ - { - label: Craft.t('commerce', 'Set status'), - actions: [ - { - label: Craft.t('commerce', 'Enabled'), - action: 'commerce/tax-rates/update-status', - param: 'status', - value: 'enabled', - status: 'enabled' - }, - { - label: Craft.t('commerce', 'Disabled'), - action: 'commerce/tax-rates/update-status', - param: 'status', - value: 'disabled', - status: 'disabled' - } - ] - } -]; - -new Craft.VueAdminTable({ - columns: columns, - actions: actions, - checkboxes: true, - container: '#taxrate-vue-admin-table', - deleteAction: '{$deleteAction}', - tableData: {$tableData}, -}); -JS; - - $this->getView()->registerJs($js, View::POS_END); - - return $this->asStoreManagementCpScreen($storeHandle) - ->additionalButtonsHtml($buttonsHtml) - ->contentHtml(Html::tag('div', '', ['id' => 'taxrate-vue-admin-table'])); - } - - /** - * @param int|null $id - * @param TaxRate|null $taxRate - * @throws ForbiddenHttpException - * @throws HttpException - * @throws \Twig\Error\LoaderError - * @throws \Twig\Error\RuntimeError - * @throws \Twig\Error\SyntaxError - * @throws Exception - */ - public function actionEdit(?string $storeHandle = null, int $id = null, TaxRate $taxRate = null): Response - { - if (!Plugin::getInstance()->getTaxes()->viewTaxRates()) { - throw new ForbiddenHttpException('Tax engine does not permit you to perform this action'); - } - - if ($storeHandle === null || !$store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle)) { - $store = Plugin::getInstance()->getStores()->getPrimaryStore(); - } - - $storeHandle = $store->handle; - $percentSymbol = Craft::$app->getFormattingLocale()->getNumberSymbol(Locale::SYMBOL_PERCENT); - - $plugin = Plugin::getInstance(); - - if (!$taxRate) { - if ($id) { - $taxRate = $plugin->getTaxRates()->getTaxRateById($id, $store->id); - - if (!$taxRate) { - throw new HttpException(404); - } - } else { - $taxRate = Craft::createObject([ - 'class' => TaxRate::class, - 'storeId' => $store->id, - ]); - } - } - - $title = $taxRate->id ? $taxRate->name : Craft::t('commerce', 'Create a new tax rate'); - - DebugPanel::prependOrAppendModelTab(model: $taxRate, prepend: true); - - $variables = compact('taxRate', 'store', 'storeHandle', 'percentSymbol'); - - // Get the actual tax zone object if there's an ID - $taxZone = null; - if ($taxRate->taxZoneId) { - $taxZone = $plugin->getTaxZones()->getTaxZoneById($taxRate->taxZoneId, $store->id); - } - - // Get the actual tax category object if there's an ID - $taxCategory = null; - if ($taxRate->taxCategoryId) { - $taxCategory = $plugin->getTaxCategories()->getTaxCategoryById($taxRate->taxCategoryId); - } - - // Tax zone field with slideout - $variables['taxZoneField'] = CommerceCp::taxZoneFieldHtml([ - 'label' => Craft::t('commerce', 'Tax Zone'), - 'instructions' => Craft::t('commerce', 'Select a tax zone. If empty, this rate will match anywhere.'), - 'id' => 'taxZoneId', - 'name' => 'taxZoneId', - 'value' => $taxZone, - 'errors' => $taxRate->getErrors('taxZoneId'), - 'required' => false, - 'limit' => 1, - 'storeId' => $store->id, - 'storeHandle' => $storeHandle, - ]); - - // Tax category field with slideout - $variables['taxCategoryField'] = CommerceCp::taxCategoryFieldHtml([ - 'label' => Craft::t('commerce', 'Tax Category'), - 'instructions' => Craft::t('commerce', 'Select a tax category.'), - 'id' => 'taxCategoryId', - 'name' => 'taxCategoryId', - 'value' => $taxCategory, - 'errors' => $taxRate->getErrors('taxCategoryId'), - 'required' => true, - 'limit' => 1, - 'storeHandle' => $storeHandle, - ]); - - $taxable = []; - $taxable[TaxRateRecord::TAXABLE_PURCHASABLE] = Craft::t('commerce', 'Unit price (minus discounts)'); - $taxable[TaxRateRecord::TAXABLE_PRICE] = Craft::t('commerce', 'Line item price (minus discounts)'); - $taxable[TaxRateRecord::TAXABLE_SHIPPING] = Craft::t('commerce', 'Line item shipping cost'); - $taxable[TaxRateRecord::TAXABLE_PRICE_SHIPPING] = Craft::t('commerce', 'Both (Line item price + Line item shipping costs)'); - $taxable[TaxRateRecord::TAXABLE_ORDER_TOTAL_SHIPPING] = Craft::t('commerce', 'Order total shipping cost'); - $taxable[TaxRateRecord::TAXABLE_ORDER_TOTAL_PRICE] = Craft::t('commerce', 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)'); - $variables['taxables'] = $taxable; - $variables['taxablesNoTaxCategory'] = TaxRateRecord::ORDER_TAXABALES; - - $variables['hideTaxCategory'] = false; - if ($variables['taxRate']->id && in_array($variables['taxRate']->taxable, $variables['taxablesNoTaxCategory'], false)) { - $variables['hideTaxCategory'] = true; - } - - $taxIdValidators = Plugin::getInstance()->getTaxes()->getEnabledTaxIdValidators(); - foreach ($taxIdValidators as $validator) { - $variables['taxIdValidators'][] = $validator; - } - - return $this->asStoreManagementCpScreen($storeHandle, false) - ->title($title) - ->addCrumb(Craft::t('commerce', 'Tax Rates'), $store->getStoreSettingsUrl('taxrates')) - ->selectedSubnavItem('store-management') - ->action('commerce/tax-rates/save') - ->redirectUrl($store->getStoreSettingsUrl('taxrates')) - ->metaSidebarTemplate('commerce/store-management/tax/taxrates/_sidebar', $variables) - ->contentTemplate('commerce/store-management/tax/taxrates/_edit', $variables); - } - - /** - * @throws Exception - * @throws ForbiddenHttpException - * @throws BadRequestHttpException - */ - public function actionSave(): void - { - if (!Plugin::getInstance()->getTaxes()->editTaxRates()) { - throw new ForbiddenHttpException('Tax engine does not permit you to perform this action'); - } - - $this->requirePostRequest(); - - $taxRate = new TaxRate(); - - // Shared attributes - $taxRate->id = $this->request->getBodyParam('taxRateId'); - $taxRate->storeId = $this->request->getBodyParam('storeId'); - $this->requireStoreAccess($taxRate->storeId); - $taxRate->name = $this->request->getBodyParam('name'); - $taxRate->code = $this->request->getBodyParam('code'); - $taxRate->include = (bool)$this->request->getBodyParam('include'); - $taxRate->removeIncluded = (bool)$this->request->getBodyParam('removeIncluded'); - $taxRate->removeVatIncluded = (bool)$this->request->getBodyParam('removeVatIncluded'); - $taxRate->taxable = $this->request->getBodyParam('taxable'); - $taxRate->taxCategoryId = (int)$this->request->getBodyParam('taxCategoryId') ?: null; - $taxRate->taxZoneId = (int)$this->request->getBodyParam('taxZoneId') ?: null; - $taxRate->rate = Localization::normalizePercentage($this->request->getBodyParam('rate')); - $taxRate->enabled = (bool)($this->request->getBodyParam('enabled')); - - // data comes in as className => bool, we want just the class names that are true - $validators = collect($this->request->getBodyParam('taxIdValidators'))->filter(fn($enabled) => (bool)$enabled)->keys(); - $taxRate->taxIdValidators = $validators->toArray(); - - // Save it - if (Plugin::getInstance()->getTaxRates()->saveTaxRate($taxRate)) { - $this->setSuccessFlash(Craft::t('commerce', 'Tax rate saved.')); - $this->redirectToPostedUrl($taxRate); - } else { - $this->setFailFlash(Craft::t('commerce', 'Couldn’t save tax rate.')); - } - - // Send the model back to the template - Craft::$app->getUrlManager()->setRouteParams([ - 'taxRate' => $taxRate, - ]); - } - - /** - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - */ - public function actionDelete(): Response - { - if (!Plugin::getInstance()->getTaxes()->deleteTaxRates()) { - throw new ForbiddenHttpException('Tax engine does not permit you to perform this action'); - } - - $this->requirePostRequest(); - $this->requireAcceptsJson(); - - $id = $this->request->getRequiredBodyParam('id'); - - $taxRate = Plugin::getInstance()->getTaxRates()->getTaxRateById($id); - if ($taxRate) { - $this->requireStoreAccess($taxRate->storeId); - } - - Plugin::getInstance()->getTaxRates()->deleteTaxRateById($id); - return $this->asSuccess(); - } - - /** - * @throws BadRequestHttpException - * @throws Exception - * @since 5.x - */ - public function actionUpdateStatus(): void - { - $this->requirePostRequest(); - $ids = $this->request->getRequiredBodyParam('ids'); - $status = $this->request->getRequiredBodyParam('status'); - - if (empty($ids)) { - $this->setFailFlash(Craft::t('commerce', 'Couldn’t update status.')); - } - - $transaction = Craft::$app->getDb()->beginTransaction(); - $taxRates = TaxRateRecord::find() - ->where(['id' => $ids]) - ->all(); - - /** @var TaxRateRecord $taxRate */ - foreach ($taxRates as $taxRate) { - $this->requireStoreAccess($taxRate->storeId); - $taxRate->enabled = ($status == 'enabled'); - $taxRate->save(); - } - $transaction->commit(); - - $this->setSuccessFlash(Craft::t('commerce', 'Tax rates updated.')); - } -} diff --git a/src/controllers/TaxZonesController.php b/src/controllers/TaxZonesController.php deleted file mode 100644 index 3be4a76cde..0000000000 --- a/src/controllers/TaxZonesController.php +++ /dev/null @@ -1,239 +0,0 @@ - - * @since 2.0 - */ -class TaxZonesController extends BaseTaxSettingsController -{ - /** - * @param string|null $storeHandle - * @return Response - * @throws StoreNotFoundException - * @throws InvalidConfigException - */ - public function actionIndex(?string $storeHandle = null): Response - { - if ($storeHandle === null || !$store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle)) { - $store = Plugin::getInstance()->getStores()->getPrimaryStore(); - } - - $taxZones = Plugin::getInstance()->getTaxZones()->getAllTaxZones($store->id); - - // Generate table data - $tableData = []; - foreach ($taxZones as $taxZone) { - $label = Html::encode(Craft::t('site', $taxZone->name)); - $tableData[] = [ - 'id' => $taxZone->id, - 'title' => Html::a($label, $taxZone->getCpEditUrl()), - 'url' => $taxZone->getCpEditUrl(), - 'description' => Html::encode(Craft::t('site', $taxZone->description)), - 'default' => $taxZone->default, - ]; - } - - $this->getView()->registerTranslations('commerce', [ - 'Name', - 'Description', - 'Default Zone', - ]); - - $tableData = Json::encode($tableData); - - $js = <<'; - } - } - }, -]; - -new Craft.VueAdminTable({ - columns: columns, - container: '#tax-vue-admin-table', - deleteAction: 'commerce/tax-zones/delete', - tableData: {$tableData}, - }); -JS; - $this->getView()->registerJs($js, View::POS_END); - - return $this->asStoreManagementCpScreen($storeHandle) - ->additionalButtonsHtml(Html::a(Craft::t('commerce', 'New tax zone'), $store->getStoreSettingsUrl('taxzones/new'), ['class' => 'btn submit add icon'])) - ->contentHtml(Html::tag( - 'div', - '', - ['id' => 'tax-vue-admin-table'] - )); - } - - /** - * @param int|null $id - * @param TaxAddressZone|null $taxZone - * @throws HttpException - */ - public function actionEdit(?string $storeHandle = null, int $id = null, TaxAddressZone $taxZone = null): Response - { - if ($storeHandle === null || !$store = Plugin::getInstance()->getStores()->getStoreByHandle($storeHandle)) { - $store = Plugin::getInstance()->getStores()->getPrimaryStore(); - } - - $storeHandle = $store->handle; - - if (!$taxZone) { - if ($id) { - $taxZone = Plugin::getInstance()->getTaxZones()->getTaxZoneById($id, $store->id); - - if (!$taxZone) { - throw new HttpException(404); - } - } else { - $taxZone = Craft::createObject([ - 'class' => TaxAddressZone::class, - 'storeId' => $store->id, - ]); - } - } - - $title = $taxZone->id ? $taxZone->name : Craft::t('commerce', 'Create a tax zone'); - - $condition = $taxZone->getCondition(); - $condition->mainTag = 'div'; - $condition->name = 'condition'; - $condition->id = 'condition'; - - DebugPanel::prependOrAppendModelTab(model: $taxZone, prepend: true); - - $metaSidebar = ''; - if ($taxZone->id) { - $metaSidebar = Cp::metadataHtml([ - Craft::t('app', 'Created at') => Craft::$app->getFormatter()->asDatetime($taxZone->dateCreated, 'short'), - Craft::t('app', 'Updated at') => Craft::$app->getFormatter()->asDatetime($taxZone->dateUpdated, 'short'), - ]); - } - - return $this->asStoreManagementCpScreen($storeHandle, false) - ->title($title) - ->addCrumb(Craft::t('commerce', 'Tax Zones'), $store->getStoreSettingsUrl('taxzones')) - ->selectedSubnavItem('store-management') - ->action('commerce/tax-zones/save') - ->redirectUrl($store->getStoreSettingsUrl('taxzones')) - ->metaSidebarHtml($metaSidebar) - ->contentTemplate('commerce/store-management/tax/taxzones/_edit', [ - 'taxZone' => $taxZone, - 'store' => $store, - 'condition' => $condition, - ]); - } - - /** - * @throws Exception - * @throws BadRequestHttpException - */ - public function actionSave(): ?Response - { - $this->requirePostRequest(); - - $taxZone = new TaxAddressZone(); - - $taxZone->id = $this->request->getBodyParam('taxZoneId'); - $taxZone->storeId = $this->request->getBodyParam('storeId'); - $this->requireStoreAccess($taxZone->storeId); - $taxZone->name = $this->request->getBodyParam('name'); - $taxZone->description = $this->request->getBodyParam('description'); - $taxZone->default = (bool)$this->request->getBodyParam('default'); - $taxZone->setCondition($this->request->getBodyParam('condition')); - - if ($taxZone->validate() && Plugin::getInstance()->getTaxZones()->saveTaxZone($taxZone)) { - return $this->asModelSuccess( - $taxZone, - Craft::t('commerce', 'Tax zone saved.'), - 'taxZone', - data: [ - 'id' => $taxZone->id, - 'name' => $taxZone->name, - ] - ); - } - - return $this->asModelFailure( - $taxZone, - Craft::t('commerce', 'Couldn’t save tax zone.'), - 'taxZone' - ); - } - - /** - * @throws HttpException - */ - public function actionDelete(): Response - { - $this->requirePostRequest(); - $this->requireAcceptsJson(); - - $id = $this->request->getRequiredBodyParam('id'); - - $taxZone = Plugin::getInstance()->getTaxZones()->getTaxZoneById($id); - if ($taxZone) { - $this->requireStoreAccess($taxZone->storeId); - } - - Plugin::getInstance()->getTaxZones()->deleteTaxZoneById($id); - return $this->asSuccess(); - } - - /** - * @throws BadRequestHttpException - * @throws LoaderError - * @throws SyntaxError - * @since 2.2 - */ - public function actionTestZip(): Response - { - $this->requirePostRequest(); - $this->requireAcceptsJson(); - - $zipCodeFormula = (string)$this->request->getRequiredBodyParam('zipCodeConditionFormula'); - $testZipCode = (string)$this->request->getRequiredBodyParam('testZipCode'); - - $params = ['zipCode' => $testZipCode]; - if (!Plugin::getInstance()->getFormulas()->evaluateCondition($zipCodeFormula, $params)) { - return $this->asFailure('failed'); - } - - return $this->asSuccess(); - } -} diff --git a/src/controllers/TransfersController.php b/src/controllers/TransfersController.php deleted file mode 100644 index 7be7113cf2..0000000000 --- a/src/controllers/TransfersController.php +++ /dev/null @@ -1,346 +0,0 @@ - - * @since 5.1.0 - */ -class TransfersController extends BaseCpController -{ - /** - * @return void - * @throws \yii\base\InvalidConfigException - * @throws \yii\web\ForbiddenHttpException - */ - public function init(): void - { - parent::init(); - - $this->requirePermission('commerce-manageInventoryTransfers'); - } - - /** - * @return Response - */ - public function actionCreate(): Response - { - $user = static::currentUser(); - $transfer = Craft::createObject(Transfer::class); - - if (!Craft::$app->getElements()->canSave($transfer, $user)) { - throw new ForbiddenHttpException('User not authorized to save this transfer.'); - } - - $transfer->setScenario(Element::SCENARIO_ESSENTIALS); - $success = Craft::$app->getDrafts()->saveElementAsDraft($transfer, Craft::$app->getUser()->getId(), null, null, false); - - if (!$success) { - return $this->asModelFailure($transfer, Craft::t('app', 'Couldn’t create {type}.', [ - 'type' => Transfer::lowerDisplayName(), - ]), 'transfer'); - } - - $editUrl = $transfer->getCpEditUrl(); - - $response = $this->asModelSuccess($transfer, Craft::t('app', '{type} created.', [ - 'type' => Transfer::displayName(), - ]), 'transfer', array_filter([ - 'cpEditUrl' => $this->request->isCpRequest ? $editUrl : null, - ])); - - if (!$this->request->getAcceptsJson()) { - $response->redirect(UrlHelper::urlWithParams($editUrl, [ - 'fresh' => 1, - ])); - } - - return $response; - } - - /** - * @return Response - */ - public function actionIndex(): Response - { - return $this->renderTemplate('commerce/inventory/transfers/_index'); - } - - /** - * @return Response - * @throws \yii\base\InvalidConfigException - * @throws \yii\web\BadRequestHttpException - * @throws \yii\web\MethodNotAllowedHttpException - */ - public function actionMarkAsPending(): Response - { - $this->requirePostRequest(); - - $transferId = $this->request->getRequiredBodyParam('transferId'); - $transfer = Transfer::findOne($transferId); - $transfer->transferStatus = TransferStatusType::PENDING; - - if (!Craft::$app->getElements()->saveElement($transfer)) { - return $this->asFailure(Craft::t('app', 'Couldn’t mark transfer as pending.')); - } - - return $this->asSuccess(Craft::t('app', 'Transfer marked as pending.')); - } - - /** - * @return Response - */ - public function actionSaveSettings(): Response - { - $this->requirePostRequest(); - - $fieldLayout = Craft::$app->getFields()->assembleLayoutFromPost(); - - $fieldLayout->reservedFieldHandles = [ - 'originLocationId', - 'originLocation', - 'destinationLocationId', - 'destinationLocation', - ]; - - $fieldLayout->type = Transfer::class; - - if (!$fieldLayout->validate()) { - Craft::info('Field layout not saved due to validation error.', __METHOD__); - - Craft::$app->getUrlManager()->setRouteParams([ - 'variables' => [ - 'fieldLayout' => $fieldLayout, - ], - ]); - - return $this->asFailure(Craft::t('commerce', 'Couldn’t save transfer fields.')); - } - - if ($currentTransfersFieldLayout = Craft::$app->getProjectConfig()->get(Transfers::CONFIG_FIELDLAYOUT_KEY)) { - $uid = array_key_first($currentTransfersFieldLayout); - } else { - $uid = StringHelper::UUID(); - } - - $configData = [$uid => $fieldLayout->getConfig()]; - $result = Craft::$app->getProjectConfig()->set(Transfers::CONFIG_FIELDLAYOUT_KEY, $configData, force: true); - - if (!$result) { - return $this->asFailure(Craft::t('app', 'Couldn’t save transfer fields.')); - } - - return $this->asSuccess(Craft::t('commerce', 'Transfer fields saved.')); - } - - /** - * @return Response - */ - public function actionReceiveTransfer(): Response - { - $details = $this->request->getParam('details', []); - $transferId = $this->request->getRequiredParam('transferId'); - /** @var Transfer $transfer */ - $transfer = Transfer::find()->id($transferId)->one(); - - $inventoryMovementCollection = new InventoryMovementCollection(); - $inventoryUpdateCollection = new UpdateInventoryLevelCollection(); - - $transferDetails = $transfer->getDetails(); - - foreach ($transferDetails as $detail) { - if ($acceptedAmount = $details[$detail->uid]['accept'] ?? null) { - // Update the total accepted - $detail->quantityAccepted += $acceptedAmount; - - $inventoryAcceptedMovement = new InventoryTransferMovement(); - $inventoryAcceptedMovement->quantity = $acceptedAmount; - $inventoryAcceptedMovement->transferId = $transfer->id; - $inventoryAcceptedMovement->setInventoryItem($detail->getInventoryItem()); - $inventoryAcceptedMovement->toInventoryLocation = $transfer->getDestinationLocation(); - $inventoryAcceptedMovement->fromInventoryLocation = $transfer->getDestinationLocation(); // we are moving from incoming to available - $inventoryAcceptedMovement->toInventoryTransactionType = InventoryTransactionType::AVAILABLE; - $inventoryAcceptedMovement->fromInventoryTransactionType = InventoryTransactionType::INCOMING; - - $inventoryMovementCollection->push($inventoryAcceptedMovement); - } - - if ($rejectedAmount = $details[$detail->uid]['reject'] ?? null) { - // Update the total rejected - $detail->quantityRejected += $rejectedAmount; - - $inventoryRejectedMovement = new UpdateInventoryLevel(); - $inventoryRejectedMovement->quantity = $rejectedAmount * -1; - $inventoryRejectedMovement->updateAction = InventoryUpdateQuantityType::ADJUST; - $inventoryRejectedMovement->inventoryItemId = $detail->inventoryItemId; - $inventoryRejectedMovement->transferId = $transfer->id; - $inventoryRejectedMovement->setInventoryLocation($transfer->getDestinationLocation()); - $inventoryRejectedMovement->type = InventoryTransactionType::INCOMING->value; - - $inventoryUpdateCollection->push($inventoryRejectedMovement); - } - } - - $transfer->setDetails($transferDetails); - - try { - // Accepted movement - Plugin::getInstance()->getInventory()->executeInventoryMovements($inventoryMovementCollection); - // Rejected updates - Plugin::getInstance()->getInventory()->executeUpdateInventoryLevels($inventoryUpdateCollection); - Craft::$app->getElements()->saveElement($transfer, false); - } catch (\Throwable $e) { - Craft::error('Failed to save transfer details: ' . $e->getMessage(), __METHOD__); - return $this->asFailure(Craft::t('commerce', 'Failed to receive transfer: {error}', ['error' => $e->getMessage()])); - } - - return $this->asSuccess(Craft::t('commerce', 'Updated')); - } - - /** - * @return Response - */ - public function actionReceiveTransferScreen(): Response - { - $transferId = $this->request->getRequiredParam('transferId'); - /** @var ?Transfer $transfer */ - $transfer = Transfer::find()->id($transferId)->one(); - - if (!$transfer) { - return $this->asCpScreen() - ->contentHtml('Cant find transfer'); - } - - $html = Html::beginTag('div', [ - 'hx' => [ - 'action' => 'commerce/transfers/receive-transfer-modal-content', - ], - ]); - - $html .= Html::tag('h2', Craft::t('commerce', 'Receive Transfer')); - - $html .= Html::hiddenInput('transferId', $transferId); - - // @TODO Add shortcut links to accept-all and reject-all unreceived items in the receive-transfer modal - // $html .= Html::a(Craft::t('commerce', 'Accept All Unreceived'), '#'); - // $html .= Html::a(Craft::t('commerce', 'Reject All Unreceived'), '#'); - - $tableRows = ''; - foreach ($transfer->getDetails() as $detail) { - $deleted = $detail->inventoryItemId == null; - $key = $detail->uid; - $purchasable = $detail->getInventoryItem()?->getPurchasable(CraftCp::requestedSite()->id); - $label = $purchasable ? CraftCp::elementChipHtml($purchasable) : $detail->inventoryItemDescription; - $tableRows .= Html::beginTag('tr'); - $tableRows .= Html::tag('td', $label); - $tableRows .= Html::tag('td', (string)$detail->quantityAccepted, ['class' => 'rightalign']); - $tableRows .= Html::tag('td', - Html::input('number', 'details[' . $key . '][accept]', '', [ - 'class' => 'text fullwidth', - 'disabled' => $deleted, - 'placeholder' => $deleted ? Craft::t('app', '“{name}” deleted.', ['name' => $detail->inventoryItemDescription]) : '', - ]) - ); - $tableRows .= Html::tag('td', (string)$detail->quantityRejected, ['class' => 'rightalign']); - $tableRows .= Html::tag('td', - Html::input('number', 'details[' . $key . '][reject]', '', [ - 'class' => 'text fullwidth', - 'disabled' => $deleted, - 'placeholder' => $deleted ? Craft::t('app', '“{name}” deleted.', ['name' => $detail->inventoryItemDescription]) : '', - ]) - ); - } - - $html .= Html::tag('table', - Html::tag('thead', - Html::tag('tr', - Html::tag('th', Craft::t('commerce', 'Item')) . - Html::tag('th', Craft::t('commerce', 'Accepted'), ['class' => 'rightalign']) . - Html::tag('th', Craft::t('commerce', 'Accept')) . - Html::tag('th', Craft::t('commerce', 'Rejected'), ['class' => 'rightalign']) . - Html::tag('th', Craft::t('commerce', 'Reject')) - ) - ) . - $tableRows, - ['class' => 'data fullwidth']); - - $html .= Html::endTag('div'); - - return $this->asCpScreen() - ->action('commerce/transfers/receive-transfer') - ->submitButtonLabel(Craft::t('commerce', 'Receive')) -// ->additionalButtonsHtml($acceptAllUnreceivedButton) - ->contentHtml($html); - } - - public function actionRenderManagement(): string - { - $transferId = $this->request->getRequiredParam('transferId'); - - /** @var ?Transfer $transfer */ - $transfer = Transfer::find()->id($transferId)->drafts(null)->one(); - - // We will only change the transfer if it is a draft. - if ($transfer && $transfer->isTransferDraft()) { - $allLocations = Plugin::getInstance()->getInventoryLocations()->getAllInventoryLocations(); - $defaultFirstLocationId = $allLocations->first()->id; - $defaultSecondLocationId = $allLocations->skip(1)->first()->id; - - $originLocationId = (int)$this->request->getParam('originLocationId', $defaultFirstLocationId); - $destinationLocationId = (int)$this->request->getParam('destinationLocationId', $defaultSecondLocationId); - - $transfer->originLocationId = $originLocationId; - $transfer->destinationLocationId = $destinationLocationId; - - $details = $this->request->getParam('details', []); - $transfer->setDetails($details); - - $details = $this->request->getParam('details', []); - - if ($this->request->getParam('removeInventoryItemUid')) { - $details = array_filter($details, fn($detail) => $detail['uid'] !== $this->request->getParam('removeInventoryItemUid')); - } - $transfer->setDetails($details); - - $addItem = $this->request->getParam('addItem', false); - $addInventoryItemId = $this->request->getParam('newInventoryItemId', null); - if ($addItem && $addInventoryItemId) { - $transfer->addDetail(new TransferDetail([ - 'uid' => StringHelper::UUID(), - 'inventoryItemId' => $addInventoryItemId, - 'quantity' => 1, - ])); - } - } - - return TransferManagementField::renderFieldHtml($transfer); - } -} diff --git a/src/controllers/UserOrdersController.php b/src/controllers/UserOrdersController.php deleted file mode 100644 index 2a6680c6a1..0000000000 --- a/src/controllers/UserOrdersController.php +++ /dev/null @@ -1,46 +0,0 @@ - - * @since 4.0 - */ -class UserOrdersController extends BaseFrontEndController -{ - /** - * Get customer's orders - * - * @throws BadRequestHttpException - */ - public function actionGetOrders(): Response - { - $this->requireAcceptsJson(); - - /** @var User|CustomerBehavior|null $user */ - $user = Craft::$app->getUser()->getIdentity(); - - if (!$user) { - return $this->asFailure(Craft::t('commerce', 'No user authenticated.')); - } - - $orders = $user->getOrders(); - - return $this->asSuccess(data: [ - 'orders' => $orders, - ]); - } -} diff --git a/src/controllers/UsersController.php b/src/controllers/UsersController.php deleted file mode 100644 index a000d548cb..0000000000 --- a/src/controllers/UsersController.php +++ /dev/null @@ -1,141 +0,0 @@ - - * @since 5.0.0 - */ -class UsersController extends BaseFrontEndController -{ - use EditUserTrait; - - public const SCREEN_COMMERCE = 'commerce'; - - /** - * @param int|null $userId - * @return Response - * @throws BadRequestHttpException - * @throws ForbiddenHttpException - * @throws \Throwable - * @throws InvalidConfigException - */ - public function actionIndex(?int $userId = null): Response - { - $user = $this->editedUser($userId); - - /** @var Response|CpScreenResponseBehavior $response */ - $response = $this->asEditUserScreen($user, 'commerce'); - - $view = Craft::$app->getView(); - $view->registerAssetBundle(CommerceCpAsset::class); - - $config = [ - 'context' => 'embedded-index', - 'sources' => false, - 'showSiteMenu' => true, - 'jsSettings' => [ - 'criteria' => ['customerId' => $user->id], - ], - ]; - - $edge = Plugin::getInstance()->getCarts()->getActiveCartEdgeDuration(); - - $content = ''; - $key = 'Commerce-Users-element-indexes-%s'; - - if (Craft::$app->getUser()->getIdentity()->can('commerce-manageOrders')) { - $completedOrdersKey = sprintf($key, 'completed-orders'); - $activeCartsKey = sprintf($key, 'active-carts'); - $inactiveCartsKey = sprintf($key, 'inactive-carts'); - - $content .= Html::tag('h2', Craft::t('commerce', 'Orders')) . - Html::beginTag('div', ['class' => 'commerce-user-orders']) . - Cp::elementIndexHtml(Order::class, ArrayHelper::merge($config, [ - 'id' => $completedOrdersKey, - 'jsSettings' => [ - 'criteria' => ['isCompleted' => true], - 'storageKey' => $completedOrdersKey, - ], - ])) . - Html::endTag('div') . - - Html::tag('hr') . - - Html::tag('h2', Craft::t('commerce', 'Active Carts')) . - Html::beginTag('div', ['class' => 'commerce-user-active-carts']) . - Cp::elementIndexHtml(Order::class, ArrayHelper::merge($config, [ - 'id' => $activeCartsKey, - 'jsSettings' => [ - 'criteria' => [ - 'isCompleted' => false, - 'dateUpdated' => '>= ' . $edge, - ], - 'storageKey' => $activeCartsKey, - ], - ])) . - Html::endTag('div') . - - Html::tag('hr') . - - Html::tag('h2', Craft::t('commerce', 'Inactive Carts')) . - Html::beginTag('div', ['class' => 'commerce-user-active-carts']) . - Cp::elementIndexHtml(Order::class, ArrayHelper::merge($config, [ - 'id' => $inactiveCartsKey, - 'jsSettings' => [ - 'criteria' => [ - 'isCompleted' => false, - 'dateUpdated' => '< ' . $edge, - ], - 'storageKey' => $inactiveCartsKey, - ], - ])) . - Html::endTag('div'); - } - - - if (Craft::$app->getUser()->getIdentity()->can('commerce-manageSubscriptions') and !empty(Plugin::getInstance()->getPlans()->getAllPlans())) { - $subscriptionsKey = sprintf($key, 'subscriptions'); - $content .= Html::tag('hr') . - Html::tag('h2', Craft::t('commerce', 'Subscriptions')) . - Html::beginTag('div', ['class' => 'commerce-user-subscriptions']) . - Cp::elementIndexHtml(Subscription::class, [ - 'id' => $subscriptionsKey, - 'context' => 'embedded-index', - 'sources' => false, - 'jsSettings' => [ - 'criteria' => [ - 'userId' => $user->id, - 'status' => null, - ], - 'storageKey' => $subscriptionsKey, - ], - ]) . - Html::endTag('div'); - } - - return $response->contentHtml($content); - } -} diff --git a/src/controllers/VariantsController.php b/src/controllers/VariantsController.php deleted file mode 100755 index b5945919ff..0000000000 --- a/src/controllers/VariantsController.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @since 5.0.0 - */ -class VariantsController extends BaseCpController -{ - /** - * @inheritdoc - * @throws ForbiddenHttpException - */ - public function init(): void - { - parent::init(); - - if (empty(Plugin::getInstance()->getProductTypes()->getViewableProductTypeIds(true))) { - throw new ForbiddenHttpException('User is not permitted to view any product types.'); - } - } - - /** - * @return Response - */ - public function actionIndex(): Response - { - return $this->renderTemplate('commerce/variants/_index'); - } -} diff --git a/src/controllers/WebhooksController.php b/src/controllers/WebhooksController.php deleted file mode 100644 index e7aff419c6..0000000000 --- a/src/controllers/WebhooksController.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @since 2.0 - */ -class WebhooksController extends BaseController -{ - /** - * @inheritdoc - */ - protected array|bool|int $allowAnonymous = ['process-webhook']; - - /** - * @inheritdoc - */ - public $enableCsrfValidation = false; - - /** - * @param int|null $gatewayId - * @return Response - * @throws BadRequestHttpException - * @throws NotFoundHttpException - * @throws InvalidConfigException - */ - public function actionProcessWebhook(?int $gatewayId = null): Response - { - if ($gatewayId === null) { - $gatewayId = $this->request->getRequiredParam('gateway'); - } - - if (!$gatewayId) { - throw new BadRequestHttpException('Invalid gateway ID: ' . $gatewayId); - } - - if (!$gateway = Plugin::getInstance()->getGateways()->getGatewayById($gatewayId)) { - throw new NotFoundHttpException('Gateway not found'); - } - - return Plugin::getInstance()->getWebhooks()->processWebhook($gateway); - } -} diff --git a/src/db/Table.php b/src/db/Table.php deleted file mode 100644 index 64434cd844..0000000000 --- a/src/db/Table.php +++ /dev/null @@ -1,80 +0,0 @@ - - * @since 2.2 - */ -abstract class Table -{ - public const COUPONS = '{{%commerce_coupons}}'; - public const CHARGES = '{{%commerce_charges}}'; - public const CUSTOMER_DISCOUNTUSES = '{{%commerce_customer_discountuses}}'; - public const CUSTOMERS = '{{%commerce_customers}}'; - public const DISCOUNT_CATEGORIES = '{{%commerce_discount_categories}}'; - public const DISCOUNT_PURCHASABLES = '{{%commerce_discount_purchasables}}'; - public const DISCOUNTS = '{{%commerce_discounts}}'; - public const DONATIONS = '{{%commerce_donations}}'; - public const EMAIL_DISCOUNTUSES = '{{%commerce_email_discountuses}}'; - public const EMAILS = '{{%commerce_emails}}'; - public const GATEWAYS = '{{%commerce_gateways}}'; - public const LINEITEMS = '{{%commerce_lineitems}}'; - public const LINEITEMSTATUSES = '{{%commerce_lineitemstatuses}}'; - public const ORDERADJUSTMENTS = '{{%commerce_orderadjustments}}'; - public const ORDERHISTORIES = '{{%commerce_orderhistories}}'; - public const ORDERS = '{{%commerce_orders}}'; - public const ORDERNOTICES = '{{%commerce_ordernotices}}'; - public const ORDERSTATUS_EMAILS = '{{%commerce_orderstatus_emails}}'; - public const ORDERSTATUSES = '{{%commerce_orderstatuses}}'; - public const PAYMENTCURRENCIES = '{{%commerce_paymentcurrencies}}'; - public const PAYMENTSOURCES = '{{%commerce_paymentsources}}'; - public const PDFS = '{{%commerce_pdfs}}'; - public const PLANS = '{{%commerce_plans}}'; - public const PRODUCTS = '{{%commerce_products}}'; - public const PRODUCTTYPES = '{{%commerce_producttypes}}'; - public const PRODUCTTYPES_SHIPPINGCATEGORIES = '{{%commerce_producttypes_shippingcategories}}'; - public const PRODUCTTYPES_SITES = '{{%commerce_producttypes_sites}}'; - public const PRODUCTTYPES_TAXCATEGORIES = '{{%commerce_producttypes_taxcategories}}'; - public const PURCHASABLES = '{{%commerce_purchasables}}'; - public const SALE_CATEGORIES = '{{%commerce_sale_categories}}'; - public const SALE_PURCHASABLES = '{{%commerce_sale_purchasables}}'; - public const SALE_USERGROUPS = '{{%commerce_sale_usergroups}}'; - public const SALES = '{{%commerce_sales}}'; - public const SHIPPINGCATEGORIES = '{{%commerce_shippingcategories}}'; - public const SHIPPINGMETHODS = '{{%commerce_shippingmethods}}'; - public const SHIPPINGRULE_CATEGORIES = '{{%commerce_shippingrule_categories}}'; - public const SHIPPINGRULES = '{{%commerce_shippingrules}}'; - public const SHIPPINGZONES = '{{%commerce_shippingzones}}'; - public const SUBSCRIPTIONS = '{{%commerce_subscriptions}}'; - public const TAXCATEGORIES = '{{%commerce_taxcategories}}'; - public const TAXRATES = '{{%commerce_taxrates}}'; - public const TAXZONES = '{{%commerce_taxzones}}'; - public const TRANSACTIONS = '{{%commerce_transactions}}'; - public const VARIANTS = '{{%commerce_variants}}'; - - /** @since 5.0.0 */ - public const CATALOG_PRICING = '{{%commerce_catalogpricing}}'; - public const CATALOG_PRICING_RULES = '{{%commerce_catalogpricingrules}}'; - public const CATALOG_PRICING_RULES_USERS = '{{%commerce_catalogpricingrules_users}}'; - public const PURCHASABLES_STORES = '{{%commerce_purchasables_stores}}'; - public const SITESTORES = '{{%commerce_site_stores}}'; - public const STORES = '{{%commerce_stores}}'; - public const STORESETTINGS = '{{%commerce_storesettings}}'; // Previously stores table - public const TRANSFERS = '{{%commerce_transfers}}'; - public const TRANSFERDETAILS = '{{%commerce_transferdetails}}'; - public const INVENTORYITEMS = '{{%commerce_inventoryitems}}'; - public const INVENTORYLOCATIONS = '{{%commerce_inventorylocations}}'; - public const INVENTORYLOCATIONS_STORES = '{{%commerce_inventorylocations_stores}}'; - public const INVENTORYTRANSACTIONS = '{{%commerce_inventorytransactions}}'; - - /** @since 5.7.0 */ - public const CATALOG_PRICING_QUEUE = '{{%commerce_catalogpricing_queue}}'; -} diff --git a/src/debug/CommercePanel.php b/src/debug/CommercePanel.php deleted file mode 100644 index b5ba2f41f4..0000000000 --- a/src/debug/CommercePanel.php +++ /dev/null @@ -1,103 +0,0 @@ - - * @since 3.0.0 - */ -class CommercePanel extends Panel -{ - /** - * @event \yii\base\Event The event that is triggered after the data for the panel is prepared. - * - * ```php - * use craft\commerce\debug\CommercePanel; - * use craft\commerce\events\CommerceDebugPanelDataEvent; - * use yii\base\Event; - * - * Event::on( - * CommercePanel::class, - * CommercePanel::EVENT_AFTER_DATA_PREPARE, - * function(CommerceDebugPanelDataEvent $event) { - * $event->nav[] = 'Foo'; - * $event->content[] = 'Bar'; - * } - * ); - * ``` - */ - public const EVENT_AFTER_DATA_PREPARE = 'afterDataPrepare'; - - /** - * @var Order|null - */ - public ?Order $cart = null; - - /** - * @inheritdoc - */ - public function getName(): string - { - return 'Commerce'; - } - - /** - * @inheritdoc - */ - public function getSummary(): string - { - return Craft::$app->getView()->render('@craft/commerce/views/debug/commerce/summary', [ - 'panel' => $this, - ]); - } - - /** - * @inheritdoc - */ - public function getDetail(): string - { - return Craft::$app->getView()->render('@craft/commerce/views/debug/commerce/detail', [ - 'panel' => $this, - ]); - } - - /** - * @inheritdoc - */ - public function save() - { - $nav = []; - $content = []; - - if (!Craft::$app->getRequest()->getIsCpRequest()) { - $this->cart = Plugin::getInstance()->getCarts()->getCart(); - } - - if ($this->cart) { - $nav[] = 'Cart'; - - $content[] = Craft::$app->getView()->render('@craft/commerce/views/debug/commerce/model', [ - 'model' => $this->cart, - ]); - } - - // Trigger event allowing extra tabs to be added. - $event = new CommerceDebugPanelDataEvent(['nav' => $nav, 'content' => $content]); - $this->trigger(self::EVENT_AFTER_DATA_PREPARE, $event); - - return ['nav' => $event->nav, 'content' => $event->content]; - } -} diff --git a/src/elements/Donation.php b/src/elements/Donation.php deleted file mode 100644 index e7f8568a5a..0000000000 --- a/src/elements/Donation.php +++ /dev/null @@ -1,300 +0,0 @@ - - * @since 2.0 - */ -class Donation extends Purchasable -{ - /** - * By default the donation is not available for purchase. - * - * @inerhitdoc - */ - public bool $availableForPurchase = false; - - - /** - * @inheritdoc - */ - public static function hasInventory(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function behaviors(): array - { - $behaviors = parent::behaviors(); - - $behaviors['currencyAttributes'] = [ - 'class' => CurrencyAttributeBehavior::class, - 'currencyAttributes' => $this->currencyAttributes(), - ]; - - return $behaviors; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - - $rules[] = [['sku'], 'trim']; - $rules[] = [ - ['sku'], 'required', 'when' => fn($model) => - /** @var self $model */ - $model->availableForPurchase && $model->enabled, - ]; - - return $rules; - } - - /** - * @inerhitdoc - */ - public static function hasStatuses(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function getPrice(?Store $store = null): ?float - { - return 0; - } - - /** - * @inheritdoc - */ - public function __toString(): string - { - return Craft::t('commerce', 'Donation'); - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Donation'); - } - - /** - * @inheritdoc - */ - public static function lowerDisplayName(): string - { - return Craft::t('commerce', 'donation'); - } - - /** - * @inheritdoc - */ - public static function pluralDisplayName(): string - { - return Craft::t('commerce', 'Donations'); - } - - /** - * @inheritdoc - */ - public static function pluralLowerDisplayName(): string - { - return Craft::t('commerce', 'donations'); - } - - /** - * @inheritdoc - */ - public static function refHandle(): ?string - { - return 'donation'; - } - - /** - * @inheritdoc - * @return DonationQuery The newly created [[DonationQuery]] instance. - */ - public static function find(): ElementQueryInterface - { - return new DonationQuery(static::class); - } - - /** - * Returns the product title and variants title together for variable products. - */ - public function getDescription(): string - { - return Craft::t('commerce', 'Donation'); - } - - /** - * @inheritdoc - */ - public function getCpEditUrl(): ?string - { - return UrlHelper::cpUrl(sprintf('commerce/store-management/%s/donation', $this->getStore()->handle)); - } - - /** - * @inheritdoc - */ - public function getUrl(): ?string - { - return ''; - } - - /** - * @inheritdoc - */ - public function hasFreeShipping(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function getIsShippable(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function getIsTaxable(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function populateLineItem(LineItem $lineItem): void - { - $options = $lineItem->getOptions(); - if (isset($options['donationAmount'])) { - $lineItem->price = $options['donationAmount']; - } - } - - /** - * @inheritdoc - */ - public function getLineItemRules(LineItem $lineItem): array - { - return [ - [ - 'purchasableId', - function($attribute, $params, Validator $validator) use ($lineItem) { - $options = $lineItem->getOptions(); - if (!isset($options['donationAmount'])) { - $validator->addError($lineItem, $attribute, Craft::t('commerce', 'No donation amount supplied.')); - } - if (isset($options['donationAmount']) && !is_numeric($options['donationAmount'])) { - $validator->addError($lineItem, $attribute, Craft::t('commerce', 'Donation needs to be an amount.')); - } - if (isset($options['donationAmount']) && $options['donationAmount'] == 0) { - $validator->addError($lineItem, $attribute, Craft::t('commerce', 'Donation can not be zero.')); - } - }, - ], - ]; - } - - /** - * @inheritdoc - */ - public function getIsPromotable(?Store $store = null): bool - { - return false; - } - - /** - * @throws Exception - */ - public function afterSave(bool $isNew): void - { - if (!$isNew) { - $record = DonationRecord::findOne($this->id); - - if (!$record) { - throw new Exception('Invalid donation ID: ' . $this->id); - } - } else { - $record = new DonationRecord(); - $record->id = $this->id; - } - - $record->sku = $this->sku; - - // We want to always have the same date as the element table, based on the logic for updating these in the element service i.e resaving - $record->dateUpdated = $this->dateUpdated; - $record->dateCreated = $this->dateCreated; - - $record->save(false); - - parent::afterSave($isNew); - - // Loop through other stores to save the donation to all stores - $stores = Plugin::getInstance()->getStores()->getAllStores(); - $stores - ->filter(fn(Store $s) => $s->id !== $this->getStore()->id) - ->each(function(Store $store) use ($isNew) { - $purchasableStoreRecord = PurchasableStore::findOne(['purchasableId' => $this->id, 'storeId' => $store->id]); - if ($isNew || !$purchasableStoreRecord) { - $purchasableStoreRecord = new PurchasableStore(); - $purchasableStoreRecord->purchasableId = $this->id; - $purchasableStoreRecord->storeId = $store->id; - }; - - $purchasableStoreRecord->basePrice = 0; - $purchasableStoreRecord->basePromotionalPrice = null; - $purchasableStoreRecord->stock = null; - $purchasableStoreRecord->inventoryTracked = false; - $purchasableStoreRecord->allowOutOfStockPurchases = false; - $purchasableStoreRecord->minQty = null; - $purchasableStoreRecord->maxQty = null; - $purchasableStoreRecord->promotable = false; - $purchasableStoreRecord->availableForPurchase = $this->availableForPurchase; - $purchasableStoreRecord->freeShipping = true; - $purchasableStoreRecord->shippingCategoryId = Plugin::getInstance()->getShippingCategories()->getDefaultShippingCategory($store->id)->id; - - $purchasableStoreRecord->save(false); - }); - } -} diff --git a/src/elements/Order.php b/src/elements/Order.php deleted file mode 100644 index 60ff0ce86b..0000000000 --- a/src/elements/Order.php +++ /dev/null @@ -1,4077 +0,0 @@ - - * @since 2.0 - */ -class Order extends Element implements HasStoreInterface -{ - use OrderValidatorsTrait; - use OrderElementTrait; - use OrderNoticesTrait; - use StoreTrait; - - /** - * Payments exceed order total. - */ - public const PAID_STATUS_OVERPAID = 'overPaid'; - - /** - * Payments equal order total. - */ - public const PAID_STATUS_PAID = 'paid'; - - /** - * Payments less than order total. - */ - public const PAID_STATUS_PARTIAL = 'partial'; - - /** - * Payments total zero on non-free order. - */ - public const PAID_STATUS_UNPAID = 'unpaid'; - - /** - * Recalculates line items, populates from purchasables, and regenerates adjustments. - */ - public const RECALCULATION_MODE_ALL = 'all'; - - /** - * Recalculates adjustments only; does not recalculate line items or populate from purchasables. - */ - public const RECALCULATION_MODE_ADJUSTMENTS_ONLY = 'adjustmentsOnly'; - - /** - * Does not recalculate anything on the order. - */ - public const RECALCULATION_MODE_NONE = 'none'; - - /** - * Order created from the front end. - */ - public const ORIGIN_WEB = 'web'; - - /** - * Order created from the control panel. - */ - public const ORIGIN_CP = 'cp'; - - /** - * Order created by a remote source. - */ - public const ORIGIN_REMOTE = 'remote'; - - /** - * @event \yii\base\Event The event that is triggered before a new line item has been added to the order. - * - * ```php - * use craft\commerce\elements\Order; - * use craft\commerce\models\LineItem; - * use craft\commerce\events\AddLineItemEvent; - * use yii\base\Event; - * - * Event::on( - * Order::class, - * Order::EVENT_BEFORE_ADD_LINE_ITEM, - * function(AddLineItemEvent $event) { - * // @var LineItem $lineItem - * $lineItem = $event->lineItem; - * // @var bool $isNew - * $isNew = $event->isNew; - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_ADD_LINE_ITEM = 'beforeAddLineItemToOrder'; - - /** - * @event \yii\base\Event The event that is triggered after a line item has been added to an order. - * - * ```php - * use craft\commerce\elements\Order; - * use craft\commerce\events\LineItemEvent; - * use craft\commerce\models\LineItem; - * use yii\base\Event; - * - * Event::on( - * Order::class, - * Order::EVENT_AFTER_APPLY_ADD_LINE_ITEM, - * function(LineItemEvent $event) { - * // @var LineItem $lineItem - * $lineItem = $event->lineItem; - * // @var bool $isNew - * $isNew = $event->isNew; - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_APPLY_ADD_LINE_ITEM = 'afterApplyAddLineItemToOrder'; - - /** - * @event \yii\base\Event The event that is triggered after a line item has been added to an order. - * - * ```php - * use craft\commerce\elements\Order; - * use craft\commerce\events\LineItemEvent; - * use craft\commerce\models\LineItem; - * use yii\base\Event; - * - * Event::on( - * Order::class, - * Order::EVENT_AFTER_ADD_LINE_ITEM, - * function(LineItemEvent $event) { - * // @var LineItem $lineItem - * $lineItem = $event->lineItem; - * // @var bool $isNew - * $isNew = $event->isNew; - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_ADD_LINE_ITEM = 'afterAddLineItemToOrder'; - - /** - * @event \yii\base\Event The event that is triggered after a line item has been removed from an order. - * - * ```php - * use craft\commerce\elements\Order; - * use craft\commerce\events\LineItemEvent; - * use craft\commerce\models\LineItem; - * use yii\base\Event; - * - * Event::on( - * Order::class, - * Order::EVENT_AFTER_REMOVE_LINE_ITEM, - * function(LineItemEvent $event) { - * // @var LineItem $lineItem - * $lineItem = $event->lineItem; - * // @var bool $isNew - * $isNew = $event->isNew; - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_REMOVE_LINE_ITEM = 'afterRemoveLineItemFromOrder'; - - /** - * @event \yii\base\Event The event that is triggered after a line item has been removed from an order. - * - * ```php - * use craft\commerce\elements\Order; - * use craft\commerce\events\LineItemEvent; - * use craft\commerce\models\LineItem; - * use yii\base\Event; - * - * Event::on( - * Order::class, - * Order::EVENT_AFTER_APPLY_REMOVE_LINE_ITEM, - * function(LineItemEvent $event) { - * // @var LineItem $lineItem - * $lineItem = $event->lineItem; - * // @var bool $isNew - * $isNew = $event->isNew; - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_APPLY_REMOVE_LINE_ITEM = 'afterApplyRemoveLineItemFromOrder'; - - /** - * @event \yii\base\Event The event that is triggered before an order is completed. - * - * ```php - * use craft\commerce\elements\Order; - * use yii\base\Event; - * - * Event::on( - * Order::class, - * Order::EVENT_BEFORE_COMPLETE_ORDER, - * function(Event $event) { - * // @var Order $order - * $order = $event->sender; - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_COMPLETE_ORDER = 'beforeCompleteOrder'; - - /** - * @event \yii\base\Event The event that is triggered after an order is completed. - * - * ```php - * use craft\commerce\elements\Order; - * use yii\base\Event; - * - * Event::on( - * Order::class, - * Order::EVENT_AFTER_COMPLETE_ORDER, - * function(Event $event) { - * // @var Order $order - * $order = $event->sender; - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_COMPLETE_ORDER = 'afterCompleteOrder'; - - /** - * @event \yii\base\Event The event that is triggered after an order is paid and completed. - * - * ```php - * use craft\commerce\elements\Order; - * use yii\base\Event; - * - * Event::on( - * Order::class, - * Order::EVENT_AFTER_ORDER_PAID, - * function(Event $event) { - * // @var Order $order - * $order = $event->sender; - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_ORDER_PAID = 'afterOrderPaid'; - - /** - * @event \yii\base\Event This event is raised after an order is authorized in full and completed - * - * Plugins can get notified after an order is authorized in full and completed - * - * ```php - * use craft\commerce\elements\Order; - * use yii\base\Event; - * - * Event::on(Order::class, Order::EVENT_AFTER_ORDER_AUTHORIZED, function(Event $e) { - * // @var Order $order - * $order = $e->sender; - * // ... - * }); - * ``` - */ - public const EVENT_AFTER_ORDER_AUTHORIZED = 'afterOrderAuthorized'; - - /** - * @event \yii\base\Event The event that is triggered before a notice has been added to the order. - * - * ```php - * use craft\commerce\elements\Order; - * use craft\commerce\models\OrderNotice; - * use craft\commerce\events\OrderNoticeEvent; - * use yii\base\Event; - * - * Event::on( - * Order::class, - * Order::EVENT_BEFORE_APPLY_ADD_NOTICE, - * function(OrderNoticeEvent $event) { - * // @var OrderNotice $orderNotice - * $orderNotice = $event->orderNotice; - * // ... - * } - * ); - * ``` - * - * @since 4.1.0 - */ - public const EVENT_BEFORE_APPLY_ADD_NOTICE = 'beforeApplyAddNoticeToOrder'; - - /** - * @event \yii\base\Event The event that is triggered before line items are refreshed during recalculation of an order. - * - * ```php - * use craft\commerce\elements\Order; - * use craft\commerce\events\OrderLineItemsRefreshEvent; - * use yii\base\Event; - * - * Event::on( - * Order::class, - * Order::EVENT_BEFORE_LINE_ITEMS_REFRESHED, - * function(OrderLineItemsRefreshEvent $event) { - * $event->lineItems = []; - * $event->recalculate = true; - * // ... - * } - * ); - * ``` - * - * @since 5.1.0 - */ - public const EVENT_BEFORE_LINE_ITEMS_REFRESHED = 'beforeLineItemsRefreshed'; - - /** - * @event \yii\base\Event The event that is triggered after line items are refreshed during recalculation of an order. - * - * ```php - * use craft\commerce\elements\Order; - * use craft\commerce\events\OrderLineItemsRefreshEvent; - * use yii\base\Event; - * - * Event::on( - * Order::class, - * Order::EVENT_AFTER_LINE_ITEMS_REFRESHED, - * function(OrderLineItemsRefreshEvent $event) { - * $event->lineItems = []; - * $event->recalculate = true; - * // ... - * } - * ); - * ``` - * - * @since 5.1.0 - */ - public const EVENT_AFTER_LINE_ITEMS_REFRESHED = 'afterLineItemsRefreshed'; - - /** - * This is the unique number (hash) generated for the order when it was first created. - * - * @var string|null Number - * --- - * ```php - * echo $order->number; - * ``` - * ```twig - * {{ order.number }} - * ``` - */ - public ?string $number = null; - - /** - * This is the reference number generated once the order was completed. - * While the order is a cart, this is null. - * - * @var string|null Reference - * --- - * ```php - * echo $order->reference; - * ``` - * ```twig - * {{ order.reference }} - * ``` - */ - public ?string $reference = null; - - /** - * This is the currently applied coupon code. - * - * @var string|null Coupon Code - * --- - * ```php - * echo $order->couponCode; - * ``` - * ```twig - * {{ order.couponCode }} - * ``` - */ - public ?string $couponCode = null; - - /** - * Is this order completed (no longer a cart). - * - * @var bool Is completed - * --- - * ```php - * echo $order->isCompleted; - * ``` - * ```twig - * {{ order.isCompleted }} - * ``` - */ - public bool $isCompleted = false; - - /** - * The date and time this order was completed - * - * @var DateTime|null Date ordered - * --- - * ```php - * echo $order->dateOrdered; - * ``` - * ```twig - * {{ order.dateOrdered }} - * ``` - */ - public ?DateTime $dateOrdered = null; - - /** - * The date and time this order was paid in full. - * - * @var DateTime|null Date paid - * --- - * ```php - * echo $order->datePaid; - * ``` - * ```twig - * {{ order.datePaid }} - * ``` - */ - public ?DateTime $datePaid = null; - - /** - * The date and time this order was first paid in full. - * - * @var DateTime|null Date first paid - * --- - * ```php - * echo $order->dateFirstPaid; - * ``` - * ```twig - * {{ order.dateFirstPaid }} - * ``` - */ - public ?DateTime $dateFirstPaid = null; - - /** - * The date and time this order was authorized in full. - * This may the same date as datePaid if the order was paid immediately. - * - * @var DateTime|null Date authorized - * --- - * ```php - * echo $order->dateAuthorized; - * ``` - * ```twig - * {{ order.dateAuthorized }} - * ``` - */ - public ?DateTime $dateAuthorized = null; - - /** - * The currency of the order (ISO code) - * - * @var string|null Currency - * --- - * ```php - * echo $order->currency; - * ``` - * ```twig - * {{ order.currency }} - * ``` - */ - public ?string $currency = null; - - /** - * The current gateway ID to identify the gateway the order should use when accepting payments. - * If the `paymentSourceId` is set on this order, this `gatewayId` will be that belonging to the - * payment source. - * - * @var int|null Gateway ID - * --- - * ```php - * echo $order->gatewayId; - * ``` - * ```twig - * {{ order.gatewayId }} - * ``` - */ - public ?int $gatewayId = null; - - /** - * The last IP address of the user building the order before it was marked as complete. - * - * @var string|null Last IP address - * --- - * ```php - * echo $order->lastIp; - * ``` - * ```twig - * {{ order.lastIp }} - * ``` - */ - public ?string $lastIp = null; - - /** - * The current message set on the order when having it’s order status being changed. - * - * @var string|null message - * --- - * ```php - * echo $order->message; - * ``` - * ```twig - * {{ order.message }} - * ``` - */ - public ?string $message = null; - - /** - * The current URL the order should return to after successful payment. - * This is stored on the order as we may be redirected off-site for payments. - * - * @var string|null Return URL - * --- - * ```php - * echo $order->returnUrl; - * ``` - * ```twig - * {{ order.returnUrl }} - * ``` - */ - public ?string $returnUrl = null; - - /** - * The current URL the order should return to if the customer cancels payment off-site. - * This is stored on the order as we may be redirected off-site for payments. - * - * @var string|null Cancel URL - * --- - * ```php - * echo $order->cancelUrl; - * ``` - * ```twig - * {{ order.cancelUrl }} - * ``` - */ - public ?string $cancelUrl = null; - - /** - * The current order status ID. This will be null if the order is not complete - * and is still a cart. - * - * @var int|null Order status ID - * --- - * ```php - * echo $order->orderStatusId; - * ``` - * ```twig - * {{ order.orderStatusId }} - * ``` - */ - public ?int $orderStatusId = null; - - /** - * The language the cart was created in. - * - * @var string|null The language the order was made in. - * --- - * ```php - * echo $order->orderLanguage; - * ``` - * ```twig - * {{ order.orderLanguage }} - * ``` - */ - public ?string $orderLanguage = null; - - /** - * The store the order was created in. - * - * @var int|null Order store ID - * --- - * ```php - * echo $order->storeId; - * ``` - * ```twig - * {{ order.storeId }} - * ``` - */ - public ?int $storeId = null; - - /** - * The site the order was created in. - * - * @var int|null Order site ID - * --- - * ```php - * echo $order->orderSiteId; - * ``` - * ```twig - * {{ order.orderSiteId }} - * ``` - */ - public ?int $orderSiteId = null; - - - /** - * The origin of the order when it was first created. - * Values can be 'web', 'cp', or 'api' - * - * @var string|null Order origin - * --- - * ```php - * echo $order->origin; - * ``` - * ```twig - * {{ order.origin }} - * ``` - */ - public ?string $origin = null; - - /** - * The email address that was on the cart when the order was completed. - * This is only stored for historic data. - * - * @var string|null The email address when the order was completed - * @since 4.2.12 - * --- - * ```php - * echo $order->orderCompletedEmail; - * ``` - * ```twig - * {{ order.orderCompletedEmail }} - * ``` - */ - public ?string $orderCompletedEmail = null; - - /** - * The current billing address ID - * - * @var int|null Billing address ID - * --- - * ```php - * echo $order->billingAddressId; - * ``` - * ```twig - * {{ order.billingAddressId }} - * ``` - */ - public ?int $billingAddressId = null; - - /** - * The current shipping address ID - * - * @var int|null Shipping address ID - * --- - * ```php - * echo $order->shippingAddressId; - * ``` - * ```twig - * {{ order.shippingAddressId }} - * ``` - */ - public ?int $shippingAddressId = null; - - - /** - * Whether the shipping address should be made the primary address of the - * order‘s customer. This is persisted while the order is a cart, and is only used during the - * update cart request or on order completion and new addresses are being saved. - * - * @var bool Make this the customer’s primary shipping address - * @see \craft\commerce\services\Customers::_saveAddressesFromOrder() - * --- - * ```php - * echo $order->makePrimaryShippingAddress; - * ``` - * ```twig - * {{ order.makePrimaryShippingAddress }} - * ``` - */ - public bool $makePrimaryShippingAddress = false; - - /** - * Whether the billing address should be made the primary address of the - * order‘s customer. This is persisted while the order is a cart, and is only used during the - * update cart request or on order completion and new addresses are being saved. - * - * @var bool Make this the customer‘s primary billing address - * @see \craft\commerce\services\Customers::_saveAddressesFromOrder() - * --- - * ```php - * echo $order->makePrimaryBillingAddress; - * ``` - * ```twig - * {{ order.makePrimaryBillingAddress }} - * ``` - */ - public bool $makePrimaryBillingAddress = false; - - /** - * Whether the shipping address should be the same address as the order’s - * billing address. This is not persisted on the order, and is only used during the - * update order request. Can not be set to `true` at the same time as setting - * `billingSameAsShipping` to true, or an error will be raised. - * - * @var bool Make this the shipping address the same as the billing address - * --- - * ```php - * echo $order->shippingSameAsBilling; - * ``` - * ```twig - * {{ order.shippingSameAsBilling }} - * ``` - */ - public bool $shippingSameAsBilling = false; - - /** - * Whether the billing address should be the same address as the order’s - * shipping address. This is not persisted on the order, and is only used during the - * update order request. Can not be set to `true` at the same time as setting - * `shippingSameAsBilling` to true, or an error will be raised. - * - * @var bool Make this the shipping address the same as the billing address - * --- - * ```php - * echo $order->billingSameAsShipping; - * ``` - * ```twig - * {{ order.billingSameAsShipping }} - * ``` - */ - public bool $billingSameAsShipping = false; - - /** - * @var int|null Estimated Billing address ID - * @since 2.2 - */ - public ?int $estimatedBillingAddressId = null; - - /** - * @var int|null Estimated Shipping address ID - * @since 2.2 - */ - public ?int $estimatedShippingAddressId = null; - - /** - * @var int|null The billing address ID that was selected from the customer’s address book, - * which populated the billing address on the order. - * @since 4.0 - */ - public ?int $sourceBillingAddressId = null; - - /** - * @var int|null The shipping address ID that was selected from the customer’s address book, - * which populated the shipping address on the order. - * @since 4.0 - */ - public ?int $sourceShippingAddressId = null; - - /** - * @var bool Whether estimated billing address should be set to the same address as estimated shipping - * @since 2.2 - */ - public bool $estimatedBillingSameAsShipping = false; - - /** - * @var string|null Shipping Method Handle - * @todo Change type to just `string` in Commerce 6.0 - */ - public ?string $shippingMethodHandle = ''; - - /** - * @var string|null Shipping Method Name - * @since 3.2.0 - */ - public ?string $shippingMethodName = null; - - /** - * @var int|null Customer’s ID - */ - private ?int $_customerId = null; - - /** - * @var bool Whether the customer has been deleted - */ - private bool $_customerDeleted = false; - - /** - * Whether the email address on the order should be used to register - * as a user account when the order is complete. - * - * @var bool Register user on order complete - * --- - * ```php - * echo $order->registerUserOnOrderComplete; - * ``` - * ```twig - * {{ order.registerUserOnOrderComplete }} - * ``` - */ - public bool $registerUserOnOrderComplete = false; - - /** - * Whether the billing address on the order should be saved to the customer's - * address book when the order is complete. - * - * @var bool Save the order's billing address to the customer's address book - * --- - * ```php - * echo $order->saveBillingAddressOnOrderComplete; - * ``` - * ```twig - * {{ order.saveBillingAddressOnOrderComplete }} - * ``` - */ - public bool $saveBillingAddressOnOrderComplete = false; - - /** - * Whether the shipping address on the order should be saved to the customer's - * address book when the order is complete. - * - * @var bool Save the order's shipping address to the customer's address book - * --- - * ```php - * echo $order->saveShippingAddressOnOrderComplete; - * ``` - * ```twig - * {{ order.saveShippingAddressOnOrderComplete }} - * ``` - */ - public bool $saveShippingAddressOnOrderComplete = false; - - /** - * The current payment source that should be used to make payments on the - * order. If this is set, the `gatewayId` will also be set to the related - * gateway. - * - * @var int|null Payment source ID - * --- - * ```php - * echo $order->paymentSourceId; - * ``` - * ```twig - * {{ order.paymentSourceId }} - * ``` - */ - public ?int $paymentSourceId = null; - - - /** - * @var float|null The total price as stored in the database from last retrieval - * --- - * ```php - * echo $order->storedTotalPrice; - * ``` - * ```twig - * {{ order.storedTotalPrice }} - * ``` - */ - public ?float $storedTotalPrice = null; - - /** - * @var float|null The total as stored in the database from last retrieval - * --- - * ```php - * echo $order->storedTotal; - * ``` - * ```twig - * {{ order.storedTotal }} - * ``` - */ - public ?float $storedTotal = null; - - /** - * @var float|null The total paid as stored in the database from last retrieval - * --- - * ```php - * echo $order->storedTotalPaid; - * ``` - * ```twig - * {{ order.storedTotalPaid }} - * ``` - */ - public ?float $storedTotalPaid = null; - - /** - * @var float|null The item total as stored in the database from last retrieval - * --- - * ```php - * echo $order->storedItemTotal; - * ``` - * ```twig - * {{ order.storedItemTotal }} - * ``` - */ - public ?float $storedItemTotal = null; - - /** - * @var float|null The item subtotal as stored in the database from last retrieval - * @since 3.2.4 - * --- - * ```php - * echo $order->storedItemSubtotal; - * ``` - * ```twig - * {{ order.storedItemSubtotal }} - * ``` - */ - public ?float $storedItemSubtotal = null; - - /** - * @var float|null The total shipping cost adjustments as stored in the database from last retrieval - * --- - * ```php - * echo $order->storedTotalShippingCost; - * ``` - * ```twig - * {{ order.storedTotalShippingCost }} - * ``` - */ - public ?float $storedTotalShippingCost = null; - - /** - * @var float|null The total of discount adjustments as stored in the database from last retrieval - * --- - * ```php - * echo $order->storedTotalDiscount; - * ``` - * ```twig - * {{ order.storedTotalDiscount }} - * ``` - */ - public ?float $storedTotalDiscount = null; - - /** - * @var float|null The total tax adjustments as stored in the database from last retrieval - * --- - * ```php - * echo $order->storedTotalTax; - * ``` - * ```twig - * {{ order.storedTotalTax }} - * ``` - */ - public ?float $storedTotalTax = null; - - /** - * @var float|null The total tax included adjustments as stored in the database from last retrieval - * --- - * ```php - * echo $order->storedTotalTaxIncluded; - * ``` - * ```twig - * {{ order.storedTotalTaxIncluded }} - * ``` - */ - public ?float $storedTotalTaxIncluded = null; - - /** - * @var int|null The total quantity as stored in the database from last retrieval - * --- - * ```php - * echo $order->storedTotalQty; - * ``` - * ```twig - * {{ order.storedTotalQty }} - * ``` - */ - public ?int $storedTotalQty = null; - - /** - * @var string|null - * @see Order::setRecalculationMode() To set the current recalculation mode - * @see Order::getRecalculationMode() To get the current recalculation mode - * --- - * ```php - * echo $order->recalculationMode; - * ``` - * ```twig - * {{ order.recalculationMode }} - * ``` - */ - private ?string $_recalculationMode = null; - - /** - * @var AddressElement|null - * @see Order::setShippingAddress() To set the current shipping address - * @see Order::getShippingAddress() To get the current shipping address - * --- - * ```php - * if ($order->shippingAddress) { - * echo $order->shippingAddress->firstName; - * } - * ``` - * ```twig - * {% if order.shippingAddress %} - * {{ order.shippingAddress.firstName }} - * {% endif %} - * ``` - */ - private ?AddressElement $_shippingAddress = null; - - /** - * @var AddressElement|null - * @see Order::setBillingAddress() To set the current billing address - * @see Order::getBillingAddress() To get the current billing address - * --- - * ```php - * if ($order->billingAddress) { - * echo $order->billingAddress->firstName; - * } - * ``` - * ```twig - * {% if order.billingAddress %} - * {{ order.billingAddress.firstName }} - * {% endif %} - * ``` - */ - private ?AddressElement $_billingAddress = null; - - /** - * @var AddressElement|null - * @since 2.2 - */ - private ?AddressElement $_estimatedShippingAddress = null; - - /** - * @var AddressElement|null - * @since 2.2 - */ - private ?AddressElement $_estimatedBillingAddress = null; - - /** - * @var LineItem[] - * @see Order::setLineItems() To set the order line items - * @see Order::getLineItems() To get the order line items - * --- - * ```php - * foreach ($order->getLineItems() as $lineItem) { - * echo $lineItem->description'; - * } - * ``` - * ```twig - * {% for lineItem in order.lineItems %} - * {{ lineItem.description }} - * {% endfor %} - * ``` - */ - private array $_lineItems; - private array $_deletingLineItems = []; - - /** - * @var OrderAdjustment[]|null - * @see Order::setAdjustments() To set the order adjustments - * @see Order::setAdjustments() To get the order adjustments - * --- - * ```php - * foreach ($order->getAdjustments() as $adjustment) { - * echo $adjustment->amount'; - * } - * ``` - * ```twig - * {% for adjustment in order.adjustments %} - * {{ adjustment.amount }} - * {% endfor %} - * ``` - */ - private ?array $_orderAdjustments = null; - - /** - * @var string|null - * @see Order::setPaymentCurrency() To set the payment currency - * @see Order::getPaymentCurrency() To get the payment currency - * --- - * ```php - * echo $order->paymentCurrency; - * ``` - * ```twig - * {{ order.paymentCurrency }} - * ``` - */ - private ?string $_paymentCurrency = null; - - /** - * @var Transaction[]|null - * @see Order::getTransactions() - * --- - * ```php - * echo $order->transactions; - * ``` - * ```twig - * {{ order.transactions }} - * ``` - */ - private ?array $_transactions = null; - - /** - * @var User|null|false - * @see Order::getCustomer() - * @see Order::setCustomer() - * --- - * ```php - * echo $order->customer; - * ``` - * ```twig - * {{ order.customer }} - * ``` - */ - private User|null|false $_customer = null; - - /** - * @var float|null - * @see Order::setPaymentAmount() To set the order payment amount - * @see Order::getPaymentAmount() To get the order payment amount - * --- - * ```php - * echo $order->paymentAmount; - * ``` - * ```twig - * {{ order.paymentAmount }} - * ``` - */ - private ?float $_paymentAmount = null; - - /** - * Ability to cancel email sending to avoid email even being queued. - * - * @var bool - */ - public bool $suppressEmails = false; - - /** - * @inheritdoc - */ - public function init(): void - { - if ($this->orderLanguage === null) { - $this->orderLanguage = Craft::$app->language; - } - - if ($this->storeId === null) { - $this->storeId = Plugin::getInstance()->getStores()->getCurrentStore()->id; - } - - if ($this->orderSiteId === null) { - $storeSites = $this->getStore()->getSites(); - $primarySite = Craft::$app->getSites()->getPrimarySite(); - // Prefer the Craft primary site if it belongs to this store, otherwise use the first available site - $this->orderSiteId = $storeSites->firstWhere('id', $primarySite->id)?->id ?? $storeSites->first()->id; - } - - if ($this->currency === null) { - $this->currency = $this->getStore()->getCurrency(); - } - - // Better default for carts if the base currency changes (usually only happens in development) - if (!$this->isCompleted && $this->paymentCurrency && !Plugin::getInstance()->getPaymentCurrencies()->getPaymentCurrencyByIso($this->paymentCurrency, $this->getStore()->id)) { - $this->paymentCurrency = Plugin::getInstance()->getPaymentCurrencies()->getPrimaryPaymentCurrencyIso($this->getStore()->id); - } - - if ($this->origin === null) { - $this->origin = static::ORIGIN_WEB; - } - - if ($this->_recalculationMode === null) { - if ($this->isCompleted) { - $this->setRecalculationMode(self::RECALCULATION_MODE_NONE); - } else { - $this->setRecalculationMode(self::RECALCULATION_MODE_ALL); - } - } - - parent::init(); - } - - public function behaviors(): array - { - $behaviors = parent::behaviors(); - - $behaviors['currencyAttributes'] = [ - 'class' => CurrencyAttributeBehavior::class, - 'currencyAttributes' => $this->currencyAttributes(), - ]; - - return $behaviors; - } - - /** - * @return string - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Order'); - } - - /** - * @inheritdoc - */ - public static function lowerDisplayName(): string - { - return Craft::t('commerce', 'order'); - } - - /** - * @inheritdoc - */ - public static function pluralDisplayName(): string - { - return Craft::t('commerce', 'Orders'); - } - - /** - * @inheritdoc - */ - public static function pluralLowerDisplayName(): string - { - return Craft::t('commerce', 'orders'); - } - - /** - * @inheritdoc - */ - public function __toString(): string - { - return $this->reference ?: $this->getShortNumber(); - } - - /** - * @inheritdoc - */ - public function canSave(User $user): bool - { - return parent::canSave($user) || $user->can('commerce-editOrders'); - } - - /** - * @inheritdoc - */ - public function canView(User $user): bool - { - return parent::canView($user) || $user->can('commerce-manageOrders'); - } - - /** - * @inheritdoc - */ - public function canDuplicate(User $user): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function canDelete(User $user): bool - { - return parent::canDelete($user) || $user->can('commerce-deleteOrders'); - } - - /** - * @inheritdoc - */ - public function beforeValidate(): bool - { - // Set default gateway if none present and no payment source selected - if (!$this->gatewayId && !$this->paymentSourceId) { - $gateways = Plugin::getInstance()->getGateways()->getAllCustomerEnabledGateways(); - if ($gateways->isNotEmpty()) { - $gateway = $gateways->filter(fn(GatewayInterface $g) => $g->availableForUseWithOrder($this))->first(); - - if ($gateway) { - $this->gatewayId = $gateway->id; - } - } - } - - // If the gateway ID doesn't exist, just drop it. - if ($this->gatewayId && !$this->getGateway()) { - $this->gatewayId = null; - } - - return parent::beforeValidate(); - } - - /** - * @inheritdoc - */ - public function attributes(): array - { - $names = parent::attributes(); - $names[] = 'adjustmentSubtotal'; - $names[] = 'adjustmentsTotal'; - $names[] = 'customer'; - $names[] = 'customerId'; - $names[] = 'customerDeleted'; - $names[] = 'paymentCurrency'; - $names[] = 'paymentAmount'; - $names[] = 'isPaid'; - $names[] = 'itemSubtotal'; - $names[] = 'itemTotal'; - $names[] = 'lineItems'; - $names[] = 'orderAdjustments'; - $names[] = 'outstandingBalance'; - $names[] = 'paidStatus'; - $names[] = 'recalculationMode'; - $names[] = 'shortNumber'; - $names[] = 'totalPaid'; - $names[] = 'total'; - $names[] = 'totalPrice'; - $names[] = 'totalQty'; - $names[] = 'totalPromotionalAmount'; - $names[] = 'totalWeight'; - return $names; - } - - /** - * The attributes on the order that should be made available as formatted currency. - */ - public function currencyAttributes(): array - { - $attributes = []; - $attributes[] = 'adjustmentSubtotal'; - $attributes[] = 'adjustmentsTotal'; - $attributes[] = 'itemSubtotal'; - $attributes[] = 'itemTotal'; - $attributes[] = 'outstandingBalance'; - $attributes[] = 'paymentAmount'; - $attributes[] = 'totalPaid'; - $attributes[] = 'total'; - $attributes[] = 'totalPrice'; - $attributes[] = 'totalPromotionalAmount'; - $attributes[] = 'totalTax'; - $attributes[] = 'totalTaxIncluded'; - $attributes[] = 'totalShippingCost'; - $attributes[] = 'totalDiscount'; - $attributes[] = 'storedTotal'; - $attributes[] = 'storedTotalPrice'; - $attributes[] = 'storedTotalPaid'; - $attributes[] = 'storedItemTotal'; - $attributes[] = 'storedItemSubtotal'; - $attributes[] = 'storedTotalShippingCost'; - $attributes[] = 'storedTotalDiscount'; - $attributes[] = 'storedTotalTax'; - $attributes[] = 'storedTotalTaxIncluded'; - - return $attributes; - } - - public function fields(): array - { - $fields = parent::fields(); - - $datetimeAttributes = Component::datetimeAttributes($this); - - // @todo Commerce 6 - remove this and let the parent handle ISO-8601 serialization; update Vue components - // (OrderMeta.vue, DateOrderedInput.vue) to parse/format dates from ISO-8601 using the JS Intl API instead. - foreach ($datetimeAttributes as $attribute) { - $fields[$attribute] = static function($model, $attribute) { - if (!empty($model->$attribute)) { - $formatter = Craft::$app->getFormatter(); - - return [ - 'date' => $formatter->asDate($model->$attribute, Locale::LENGTH_SHORT), - 'time' => $formatter->asTime($model->$attribute, Locale::LENGTH_SHORT), - ]; - } - - return $model->$attribute; - }; - } - - $fields['email'] = 'email'; - $fields['paidStatusHtml'] = 'paidStatusHtml'; - $fields['customerLinkHtml'] = 'customerLinkHtml'; - $fields['orderStatusHtml'] = 'orderStatusHtml'; - $fields['totalTax'] = 'totalTax'; - $fields['totalTaxIncluded'] = 'totalTaxIncluded'; - $fields['totalShippingCost'] = 'totalShippingCost'; - $fields['totalDiscount'] = 'totalDiscount'; - - // @TODO Remove these deprecated `totalSaleAmount` aliases in Commerce 6.0 - $fields['totalSaleAmount'] = 'totalPromotionalAmount'; - $fields['totalSaleAmountAsCurrency'] = 'totalPromotionalAmountAsCurrency'; - - return $fields; - } - - /** - * Returns the order's raw datetime attribute values, for use as extra `$variables` when rendering an object - * template against this order (e.g. the order reference format or a PDF file name format). - * - * `fields()` re-serializes datetime attributes into `['date' => ..., 'time' => ...]` arrays for the control - * panel's Vue components, but `craft\web\View::renderObjectTemplate()` populates its template variables from - * `fields()` before falling back to raw attributes, so a template like `{{ dateOrdered|date('Y-m-d') }}` would - * otherwise receive that array instead of a `DateTime` object. `renderObjectTemplate()` won't overwrite - * variables that are already set, so passing this array in preserves the raw values. - * - * @see fields() - * @see https://github.com/craftcms/commerce/issues/4255 - */ - public function getObjectTemplateVariables(): array - { - $variables = []; - - foreach (Component::datetimeAttributes($this) as $attribute) { - $variables[$attribute] = $this->$attribute; - } - - return $variables; - } - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $names = parent::extraFields(); - $names[] = 'adjustments'; - $names[] = 'availableShippingMethodOptions'; - $names[] = 'billingAddress'; - $names[] = 'customer'; - $names[] = 'estimatedBillingAddress'; - $names[] = 'estimatedShippingAddress'; - $names[] = 'gateway'; - $names[] = 'histories'; - $names[] = 'loadCartUrl'; - $names[] = 'nestedTransactions'; - $names[] = 'adminNotices'; - $names[] = 'notices'; - $names[] = 'orderSite'; - $names[] = 'orderStatus'; - $names[] = 'pdfUrl'; - $names[] = 'shippingAddress'; - $names[] = 'shippingMethod'; - $names[] = 'store'; - $names[] = 'totalCommittedStock'; - $names[] = 'transactions'; - return $names; - } - - /** - * @return Teller - * @throws InvalidConfigException - * @since 5.3.0 - */ - public function getTeller(): Teller - { - return Plugin::getInstance()->getCurrencies()->getTeller($this->currency); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return array_merge(parent::defineRules(), [ - // Address models are valid - [['billingAddress', 'shippingAddress'], 'validateAddress'], - [['billingAddress', 'shippingAddress'], 'validateAddressCountry'], - - // Are the addresses both being set to each other. - [ - ['billingAddress', 'shippingAddress'], 'validateAddressReuse', - 'when' => fn($model) => /** @var Order $model */ - !$model->isCompleted, - ], - - [['shippingAddress'], 'validateOrganizationTaxIdAsVatId', 'when' => fn(Order $order) => $order->getStore()->getValidateOrganizationTaxIdAsVatId() && !$order->getStore()->getUseBillingAddressForTax()], - [['billingAddress'], 'validateOrganizationTaxIdAsVatId', 'when' => fn(Order $order) => $order->getStore()->getValidateOrganizationTaxIdAsVatId() && $order->getStore()->getUseBillingAddressForTax()], - - // Line items are valid? - [['lineItems'], 'validateLineItems'], - - // Coupon Code valid? - [['couponCode'], 'validateCouponCode'], - - [['gatewayId'], 'number', 'integerOnly' => true], - [['gatewayId'], 'validateGatewayId'], - [['shippingAddressId'], 'number', 'integerOnly' => true], - [['billingAddressId'], 'number', 'integerOnly' => true], - - [['paymentCurrency'], 'validatePaymentCurrency'], - - [['paymentSourceId'], 'number', 'integerOnly' => true], - [['paymentSourceId'], 'validatePaymentSourceId'], - [['number', 'user', 'customer', 'storeId', 'orderSiteId', 'orderCompletedEmail', 'saveBillingAddressOnOrderComplete', 'saveShippingAddressOnOrderComplete', 'origin'], 'safe'], - ]); - } - - /** - * Automatically set addresses on the order if it's a cart and `autoSetNewCartAddresses` is `true`. - * - * - * @return bool returns true if order is mutated - * @throws Throwable - * @throws InvalidElementException - * @throws UnsupportedSiteException - * @since 3.4.14 - */ - public function autoSetAddresses(): bool - { - if ($this->isCompleted || !$this->getStore()->getAutoSetNewCartAddresses()) { - return false; - } - - /** @var User|CustomerBehavior|null $user */ - $user = $this->getCustomer(); - if (!$user) { - return false; - } - - $autoSetOccurred = false; - - if (!$this->_shippingAddress && !$this->shippingAddressId && $primaryShippingAddress = $user->getPrimaryShippingAddress()) { - $this->sourceShippingAddressId = $primaryShippingAddress->id; - $shippingAddress = Craft::$app->getElements()->duplicateElement($primaryShippingAddress, [ - 'owner' => $this, - 'primaryOwner' => $this, - ]); - $this->setShippingAddress($shippingAddress); - $autoSetOccurred = true; - } - - if (!$this->_billingAddress && !$this->billingAddressId && $primaryBillingAddress = $user->getPrimaryBillingAddress()) { - $this->sourceBillingAddressId = $primaryBillingAddress->id; - $billingAddress = Craft::$app->getElements()->duplicateElement($primaryBillingAddress, [ - 'owner' => $this, - 'primaryOwner' => $this, - ]); - $this->setBillingAddress($billingAddress); - $autoSetOccurred = true; - } - - return $autoSetOccurred; - } - - /** - * @return bool - * @throws InvalidConfigException - * @since 4.2 - */ - public function autoSetPaymentSource(): bool - { - if ($this->isCompleted || !$this->getStore()->getAutoSetPaymentSource() || $this->paymentSourceId || $this->gatewayId) { - return false; - } - - /** @var User|CustomerBehavior|null $customer */ - $customer = $this->getCustomer(); - - // Only set the payment source if there is a customer set and that is it the current user - if (!$customer || $customer->id !== Craft::$app->getUser()->getIdentity()?->id) { - return false; - } - - $paymentSource = $customer->getPrimaryPaymentSource(); - if (!$paymentSource) { - return false; - } - - $this->setPaymentSource($paymentSource); - return true; - } - - /** - * Auto set shipping method based on config settings and available options - * - * @return bool returns true if order is mutated - * @since 4.1 - */ - public function autoSetShippingMethod(): bool - { - if ($this->shippingMethodHandle || $this->isCompleted || !$this->getStore()->getAutoSetCartShippingMethodOption()) { - return false; - } - - $availableMethodOptions = $this->getAvailableShippingMethodOptions(); - if (empty($availableMethodOptions)) { - return false; - } - - $this->shippingMethodHandle = ArrayHelper::firstKey($availableMethodOptions); - - return true; - } - - /** - * Updates the paid status and paid date of the order, and marks as complete if the order is paid or authorized. - */ - public function updateOrderPaidInformation(): void - { - $this->_transactions = null; // clear order's transaction cache - - $paidInFull = !$this->hasOutstandingBalance(); - $authorizedInFull = $this->getTotalAuthorized() >= $this->getTotalPrice(); - - $justPaid = $paidInFull && $this->datePaid == null; - $justAuthorized = $authorizedInFull && $this->dateAuthorized == null; - - $completeTotal = $this->getTeller()->add($this->getTotalAuthorized(), $this->getTotalPaid()); - $canComplete = $this->getTeller()->greaterThan($completeTotal, 0); - - // If it is no longer paid in full, set datePaid to null - if (!$paidInFull) { - $this->datePaid = null; - } - - // If it is no longer authorized in full, set dateAuthorized to null - if (!$authorizedInFull) { - $this->dateAuthorized = null; - } - - // If it was just paid set the date paid to now. - if ($justPaid) { - $this->datePaid = new DateTime(); - } - - // If it was just paid and this is the first time, set the date first paid to now. - if ($justPaid && $this->dateFirstPaid === null) { - $this->dateFirstPaid = new DateTime(); - } - - // If it was just authorized set the date authorized to now. - if ($justAuthorized) { - $this->dateAuthorized = new DateTime(); - } - - // Lock for recalculation - $originalRecalculationMode = $this->getRecalculationMode(); - $this->setRecalculationMode(self::RECALCULATION_MODE_NONE); - - // Saving the order will update the datePaid as set above and also update the paidStatus. - Craft::$app->getElements()->saveElement($this, false); - - // If the order is now paid or authorized in full, lets mark it as complete if it has not already been. - if (!$this->isCompleted) { - $totalAuthorized = $this->getTotalAuthorized(); - if ($totalAuthorized >= $this->getTotalPrice() || $paidInFull || $canComplete) { - // We need to remove the payment source from the order now that it's paid - // This means the order needs new payment details for future payments: https://github.com/craftcms/commerce/issues/891 - // Payment information is still stored in the transactions. - $this->paymentSourceId = null; - - $this->markAsComplete(); - } - } - - if ($justPaid && $this->hasEventHandlers(self::EVENT_AFTER_ORDER_PAID)) { - $this->trigger(self::EVENT_AFTER_ORDER_PAID); - } - - if ($justAuthorized && $this->hasEventHandlers(self::EVENT_AFTER_ORDER_AUTHORIZED)) { - $this->trigger(self::EVENT_AFTER_ORDER_AUTHORIZED); - } - - // Restore the original recalculation mode, unless this call completed the order - // a completed order must stay locked at `RECALCULATION_MODE_NONE` rather than reverting to its cart mode. - if (!$this->isCompleted) { - $this->setRecalculationMode($originalRecalculationMode); - } - } - - /** - * Marks the order as complete and sets the default order status, then saves the order. - * - * @throws OrderStatusException - * @throws Exception - * @throws Throwable - * @throws ElementNotFoundException - */ - public function markAsComplete(): bool - { - // Use a mutex to make sure we check the order is not already complete due to a race condition. - $lockName = 'orderComplete:' . $this->id; - $mutex = Craft::$app->getMutex(); - if (!$mutex->acquire($lockName, 5)) { - throw new Exception('Unable to acquire a lock for completion of Order: ' . $this->id); - } - - // Now that we have a lock, make sure this order is not already completed. - if ($this->isCompleted) { - $mutex->release($lockName); - return true; - } - - // Try to catch where the order could be marked as completed twice at the same time, and thus cause a race condition. - $completedInDb = (new Query()) - ->select('id') - ->from([Table::ORDERS]) - ->where(['isCompleted' => true]) - ->andWhere(['id' => $this->id]) - ->exists(); - - if ($completedInDb) { - $mutex->release($lockName); - return true; - } - - $this->isCompleted = true; - $this->dateOrdered = new DateTime(); - - // Reset estimated address relations - $this->estimatedShippingAddressId = null; - $this->estimatedBillingAddressId = null; - $this->orderCompletedEmail = $this->getEmail(); - - $orderStatus = Plugin::getInstance()->getOrderStatuses()->getDefaultOrderStatusForOrder($this); - - // If the order status returned was overridden by a plugin, use the configured default order status if they give us a bogus one with no ID. - if ($orderStatus && $orderStatus->id) { - $this->orderStatusId = $orderStatus->id; - } else { - $mutex->release($lockName); - throw new OrderStatusException('Could not find a valid default order status.'); - } - - if ($this->reference == null) { - $referenceTemplate = $this->getStore()->getOrderReferenceFormat(); - - try { - $baseReference = Craft::$app->getView()->renderSandboxedObjectTemplate($referenceTemplate, $this, $this->getObjectTemplateVariables()); - - // Check if this reference already exists and append suffix if needed - $suffix = 0; - $testReference = $baseReference; - - while (true) { - $existingReference = (new Query()) - ->select('id') - ->from([Table::ORDERS]) - ->where(['reference' => $testReference]) - ->exists(); - - if (!$existingReference) { - // Reference is unique, use it - $this->reference = $testReference; - break; - } - - // Reference exists, increment suffix and try again - $suffix++; - $testReference = $baseReference . '-' . $suffix; - } - } catch (Throwable $exception) { - $mutex->release($lockName); - Craft::error('Unable to generate order completion reference for order ID: ' . $this->id . ', with format: ' . $referenceTemplate . ', error: ' . $exception->getMessage()); - throw $exception; - } - } - - // Raising the 'beforeCompleteOrder' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_COMPLETE_ORDER)) { - $this->trigger(self::EVENT_BEFORE_COMPLETE_ORDER); - } - - // Completed orders should no longer recalculate anything by default - $this->setRecalculationMode(static::RECALCULATION_MODE_NONE); - - $this->clearNotices(); // Customer notices are assessed as being delivered once the customer decides to complete the order. - $success = Craft::$app->getElements()->saveElement($this, false); - - if (!$success) { - Craft::error(Craft::t('commerce', 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}', - ['number' => $this->number, 'order' => json_encode($this->errors)]), __METHOD__); - - $mutex->release($lockName); - return false; - } - - $mutex->release($lockName); - - $this->afterOrderComplete(); - - return true; - } - - /** - * Called after the order successfully completes - */ - public function afterOrderComplete(): void - { - // Run order complete handlers directly. - Plugin::getInstance()->getDiscounts()->orderCompleteHandler($this); - Plugin::getInstance()->getCustomers()->orderCompleteHandler($this); - Plugin::getInstance()->getInventory()->orderCompleteHandler($this); - - foreach ($this->getLineItems() as $lineItem) { - Plugin::getInstance()->getLineItems()->orderCompleteHandler($lineItem, $this); - } - - // Persist any admin notices added by the handlers above. - $this->_saveNotices(); - - // Raising the 'afterCompleteOrder' event - if ($this->hasEventHandlers(self::EVENT_AFTER_COMPLETE_ORDER)) { - $this->trigger(self::EVENT_AFTER_COMPLETE_ORDER); - } - } - - /** - * Removes a specific line item from the order. - */ - public function removeLineItem(LineItem $lineItem): void - { - $lineItems = $this->getLineItems(); - foreach ($lineItems as $key => $item) { - if (($item->id !== null && $lineItem->id == $item->id) || $lineItem === $item) { - unset($lineItems[$key]); - $this->setLineItems($lineItems); - } - } - - if ($this->hasEventHandlers(self::EVENT_AFTER_REMOVE_LINE_ITEM)) { - $this->trigger(self::EVENT_AFTER_REMOVE_LINE_ITEM, new LineItemEvent([ - 'lineItem' => $lineItem, - ])); - } - } - - /** - * Adds a line item to the order. Updates the line item if the ID of that line item is already in the cart. - */ - public function addLineItem(LineItem $lineItem): void - { - $lineItems = $this->getLineItems(); - $isNew = ($lineItem->id === null); - - if ($isNew && $this->hasEventHandlers(self::EVENT_BEFORE_ADD_LINE_ITEM)) { - $lineItemEvent = new AddLineItemEvent(compact('lineItem', 'isNew')); - $this->trigger(self::EVENT_BEFORE_ADD_LINE_ITEM, $lineItemEvent); - - if (!$lineItemEvent->isValid) { - return; - } - } - - $replaced = false; - foreach ($lineItems as $key => $item) { - if ($lineItem->id && $item->id == $lineItem->id) { - $lineItems[$key] = $lineItem; - $replaced = true; - } - } - - if (!$replaced) { - array_unshift($lineItems, $lineItem); - } - - $this->setLineItems($lineItems); - - // Raising the 'afterAddLineItemToOrder' event - if ($this->hasEventHandlers(self::EVENT_AFTER_ADD_LINE_ITEM)) { - $this->trigger(self::EVENT_AFTER_ADD_LINE_ITEM, new LineItemEvent([ - 'lineItem' => $lineItem, - 'isNew' => !$replaced, - ])); - } - } - - /** - * Returns any line item with that purchasable - * - * @param Purchasable $purchasable - * @return Collection - */ - public function lineItemsByPurchasable(Purchasable $purchasable): Collection - { - return collect($this->getLineItems()) - ->filter(fn(LineItem $lineItem) => $lineItem->purchasableId == $purchasable->getId()); - } - - /** - * Gets the recalculation mode of the order - */ - public function getRecalculationMode(): string - { - return $this->_recalculationMode; - } - - /** - * Sets the recalculation mode of the order - */ - public function setRecalculationMode(string $value): void - { - $this->_recalculationMode = $value; - } - - /** - * Regenerates all adjusters and updates line items, depending on the current recalculationMode - * - * @throws Exception - */ - public function recalculate(): void - { - if (!$this->id) { - throw new InvalidCallException('Do not recalculate an order that has not been saved'); - } - - // create a new before relcalculate event - - - if ($this->hasErrors()) { - Craft::getLogger()->log(Craft::t('commerce', 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.', ['orderNumber' => $this->number]), Logger::LEVEL_INFO); - return; - } - - if ($this->getRecalculationMode() == self::RECALCULATION_MODE_NONE) { - return; - } - - if ($this->getRecalculationMode() == self::RECALCULATION_MODE_ALL) { - - // Make sure we set a default shipping method option - if (!$this->isCompleted && $this->getStore()->getAutoSetCartShippingMethodOption()) { - $availableMethodOptions = $this->getAvailableShippingMethodOptions(); - if (!$this->shippingMethodHandle || !isset($availableMethodOptions[$this->shippingMethodHandle])) { - $this->shippingMethodHandle = ArrayHelper::firstKey($availableMethodOptions); - } - } - - if (!$this->shippingMethodHandle) { - $this->shippingMethodName = null; - } else { - $shippingMethod = ArrayHelper::firstWhere($this->getAvailableShippingMethodOptions(), 'handle', $this->shippingMethodHandle); - if ($shippingMethod) { - $this->shippingMethodName = $shippingMethod->getName(); - } - } - - $recalculateOrder = false; - if ($this->hasEventHandlers(self::EVENT_BEFORE_LINE_ITEMS_REFRESHED)) { - $event = new OrderLineItemsRefreshEvent([ - 'lineItems' => $this->getLineItems(), - 'recalculate' => $recalculateOrder, - ]); - $this->trigger(self::EVENT_BEFORE_LINE_ITEMS_REFRESHED, $event); - - $this->setLineItems($event->lineItems); - $recalculateOrder = $event->recalculate; - } - - foreach ($this->getLineItems() as $item) { - $originalSalePrice = $item->getSalePrice(); - $originalSalePriceAsCurrency = $item->salePriceAsCurrency; - - if ($item->refresh()) { - if ($originalSalePrice > $item->salePrice) { - $message = Craft::t('commerce', 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}', ['originalSalePriceAsCurrency' => $originalSalePriceAsCurrency, 'newSalePriceAsCurrency' => $item->salePriceAsCurrency, 'description' => $item->getDescription()]); - /** @var OrderNotice $notice */ - $notice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'lineItemSalePriceChanged', - 'attribute' => "lineItems.$item->id.salePrice", - 'message' => $message, - ], - ]); - $this->addNotice($notice); - } - - if ($originalSalePrice < $item->salePrice) { - $message = Craft::t('commerce', 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}', ['originalSalePriceAsCurrency' => $originalSalePriceAsCurrency, 'newSalePriceAsCurrency' => $item->salePriceAsCurrency, 'description' => $item->getDescription()]); - /** @var OrderNotice $notice */ - $notice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'lineItemSalePriceChanged', - 'attribute' => "lineItems.$item->id.salePrice", - 'message' => $message, - ], - ]); - $this->addNotice($notice); - } - } else { - $message = Craft::t('commerce', '{description} is no longer available.', ['description' => $item->getDescription()]); - /** @var OrderNotice $notice */ - $notice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'message' => $message, - 'type' => 'lineItemRemoved', - 'attribute' => 'lineItems', - ], - ]); - $this->addNotice($notice); - $this->removeLineItem($item); - $recalculateOrder = true; - } - } - - // This is run in a validation, but need to run again incase the options - // data was changed on population of the line item by a plugin. - if (OrderHelper::mergeDuplicateLineItems($this)) { - $recalculateOrder = true; - } - - if ($this->hasEventHandlers(self::EVENT_AFTER_LINE_ITEMS_REFRESHED)) { - $event = new OrderLineItemsRefreshEvent([ - 'lineItems' => $this->getLineItems(), - 'recalculate' => $recalculateOrder, - ]); - $this->trigger(self::EVENT_AFTER_LINE_ITEMS_REFRESHED, $event); - - $this->setLineItems($event->lineItems); - $recalculateOrder = $event->recalculate; - } - - if ($recalculateOrder) { - $this->recalculate(); - return; - } - } - - if ($this->getRecalculationMode() == self::RECALCULATION_MODE_ALL || $this->getRecalculationMode() == self::RECALCULATION_MODE_ADJUSTMENTS_ONLY) { - //clear adjustments - $this->setAdjustments([]); - - foreach (Plugin::getInstance()->getOrderAdjustments()->getAdjusters() as $adjuster) { - /** @var string|AdjusterInterface $adjuster */ - $adjuster = Craft::createObject($adjuster); - $adjustments = $adjuster->adjust($this); - $this->setAdjustments(array_merge($this->getAdjustments(), $adjustments)); - } - } - - if ($this->getRecalculationMode() == self::RECALCULATION_MODE_ALL) { - // Since shipping adjusters run on the original price, pre discount, let's recalculate - // if the currently selected shipping method is now not available after adjustments have run. - $availableMethodOptions = $this->getAvailableShippingMethodOptions(); - if ($this->shippingMethodHandle && !isset($availableMethodOptions[$this->shippingMethodHandle])) { - $this->shippingMethodHandle = ArrayHelper::firstKey($availableMethodOptions); - $message = Craft::t('commerce', 'The previously-selected shipping method is no longer available.'); - $orderNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'shippingMethodChanged', - 'attribute' => 'shippingMethodHandle', - 'message' => $message, - ], - ]); - - $this->addNotice($orderNotice); - $this->recalculate(); - } - } - } - - /** - * @return ShippingMethodOption[] - * - * @since 3.1 - */ - public function getAvailableShippingMethodOptions(): array - { - // Matching will contain the core shipping methods and any plugin dynamically returned shipping methods. - $methods = Plugin::getInstance()->getShippingMethods()->getMatchingShippingMethods($this); - $matchingMethodHandles = ArrayHelper::getColumn($methods, fn(ShippingMethodInterface $sm) => $sm->getHandle()); - - // Get all regular methods and add them to the list, for use only when the order is complete. - if ($this->isCompleted) { - $allShippingMethods = Plugin::getInstance()->getShippingMethods()->getAllShippingMethods() - ->keyBy(fn(ShippingMethodInterface $sm) => $sm->getHandle()) - ->filter(fn(ShippingMethodInterface $sm) => $sm->getIsEnabled()) - ->all(); - - $methods = ArrayHelper::merge($allShippingMethods, $methods); - } - - $availableShippingMethodOptions = []; - - foreach ($methods as $method) { - $option = new ShippingMethodOption(); - - $storeId = $this->storeId; - - if ($method instanceof ShippingMethod) { - // @TODO Remove this dateCreated/dateUpdated copy in Commerce 6.0 once ShippingMethodOption no longer exposes those attributes - foreach (['dateCreated', 'dateUpdated'] as $attribute) { - $option->$attribute = $method->$attribute; - } - - if ($method->storeId !== $storeId) { - continue; - } - } - - $matchesOrder = ArrayHelper::isIn($method->getHandle(), $matchingMethodHandles); - $option->setOrder($this); - $option->enabled = $method->getIsEnabled(); - $option->id = $method->getId(); - $option->name = $method->getName(); - $option->handle = $method->getHandle(); - $option->matchesOrder = $matchesOrder; - $option->price = $matchesOrder ? $method->getPriceForOrder($this) : 0; - $option->shippingMethod = $method; - $option->storeId = $storeId; - - // Add all methods if completed, and only the matching methods when it is not completed. - if ($this->isCompleted || $option->matchesOrder) { - $availableShippingMethodOptions[$option->handle] = $option; - } - } - - return $availableShippingMethodOptions; - } - - /** - * @return Collection - * @throws DeprecationException - * @throws InvalidConfigException - * @since 5.5 - */ - public function getAvailableGateways(): Collection - { - return Plugin::getInstance()->getGateways()->getAllCustomerEnabledGatewaysAndAvailableForUseWithOrder($this); - } - - /** - * @inheritdoc - */ - public function afterSave(bool $isNew): void - { - $lockKey = "order-after-save:$this->number"; - $mutex = Craft::$app->getMutex(); - if (!$mutex->acquire($lockKey, 15)) { - throw new MutexException($lockKey, 'Could not acquire a lock to save the order.'); - } - - try { - - // Make sure addresses are set before recalculation so that on the next page load - // the correct adjustments and totals are shown - if ($this->shippingSameAsBilling) { - $this->setShippingAddress($this->getBillingAddress()); - } - - if ($this->billingSameAsShipping) { - $this->setBillingAddress($this->getShippingAddress()); - } - - // @TODO Move recalculate() out of afterSave(); saving should not implicitly recalculate, and the always-recalc-on-save-when-incomplete behavior should be opt-in #COM-40 - $this->recalculate(); - - if (!$isNew) { - $orderRecord = OrderRecord::findOne($this->id); - - if (!$orderRecord) { - throw new Exception('Invalid order ID: ' . $this->id); - } - } else { - $orderRecord = new OrderRecord(); - $orderRecord->id = $this->id; - } - - $oldStatusId = $orderRecord->orderStatusId; - - $orderRecord->storeId = $this->storeId ?? Plugin::getInstance()->getStores()->getCurrentStore()->id; - $orderRecord->number = $this->number; - $orderRecord->reference = $this->reference; - $orderRecord->itemTotal = $this->getItemTotal(); - $orderRecord->itemSubtotal = $this->getItemSubtotal(); - $orderRecord->email = $this->getEmail() ?: ''; - $orderRecord->orderCompletedEmail = $this->orderCompletedEmail; - $orderRecord->isCompleted = $this->isCompleted; - - $dateOrdered = $this->dateOrdered; - if (!$dateOrdered && $orderRecord->isCompleted) { - $dateOrdered = Db::prepareDateForDb(new DateTime()); - } - $orderRecord->dateOrdered = $dateOrdered; - - $orderRecord->datePaid = $this->datePaid ?: null; - $orderRecord->dateFirstPaid = $this->dateFirstPaid ?: null; - $orderRecord->dateAuthorized = $this->dateAuthorized ?: null; - $orderRecord->shippingMethodHandle = $this->shippingMethodHandle ?? ''; - $orderRecord->shippingMethodName = $this->shippingMethodName ?? ''; - $orderRecord->paymentSourceId = $this->getPaymentSource() ? $this->getPaymentSource()->id : null; - $orderRecord->gatewayId = $this->gatewayId; - $orderRecord->orderStatusId = $this->orderStatusId; - $orderRecord->couponCode = $this->couponCode; - $orderRecord->total = $this->getTotal(); - $orderRecord->totalPrice = $this->getTotalPrice(); - $orderRecord->totalPaid = $this->getTotalPaid(); - $orderRecord->totalDiscount = $this->getTotalDiscount(); - $orderRecord->totalShippingCost = $this->getTotalShippingCost(); - $orderRecord->totalTax = $this->getTotalTax(); - $orderRecord->totalTaxIncluded = $this->getTotalTaxIncluded(); - $orderRecord->totalQty = $this->getTotalQty(); - $orderRecord->totalWeight = $this->getTotalWeight(); - $orderRecord->currency = $this->currency; - $orderRecord->lastIp = $this->lastIp; - $orderRecord->orderLanguage = $this->orderLanguage; - $orderRecord->orderSiteId = $this->orderSiteId; - $orderRecord->origin = $this->origin; - $orderRecord->paymentCurrency = $this->paymentCurrency; - $orderRecord->customerId = $this->getCustomerId(); - $orderRecord->customerDeleted = $this->getCustomerDeleted(); - $orderRecord->registerUserOnOrderComplete = $this->registerUserOnOrderComplete; - $orderRecord->saveBillingAddressOnOrderComplete = $this->saveBillingAddressOnOrderComplete; - $orderRecord->saveShippingAddressOnOrderComplete = $this->saveShippingAddressOnOrderComplete; - $orderRecord->returnUrl = $this->returnUrl; - $orderRecord->cancelUrl = $this->cancelUrl; - $orderRecord->message = $this->message; - $orderRecord->paidStatus = $this->getPaidStatus(); - $orderRecord->recalculationMode = $this->getRecalculationMode(); - $orderRecord->sourceShippingAddressId = $this->sourceShippingAddressId; - $orderRecord->sourceBillingAddressId = $this->sourceBillingAddressId; - $orderRecord->makePrimaryShippingAddress = $this->makePrimaryShippingAddress; - $orderRecord->makePrimaryBillingAddress = $this->makePrimaryBillingAddress; - - // We want to always have the same date as the element table, based on the logic for updating these in the element service i.e resaving - $orderRecord->dateUpdated = $this->dateUpdated; - $orderRecord->dateCreated = $this->dateCreated; - - $currentUser = Craft::$app->getUser()->getIdentity(); - $currentUserIsCustomer = ($currentUser && $this->getCustomer() && $currentUser->id == $this->getCustomer()->id); - - if ($shippingAddress = $this->getShippingAddress()) { - // If we only set the owner ID an element query will be triggered. If this is a brand-new order we will encounter an error - // This is because the order record has not been saved. - // We can avoid this by simply fully setting the owner on the address element. This is also a performance optimisation to avoid an extra query. - $shippingAddress->setPrimaryOwner($this); // Always ensure the address is owned by the order - $shippingAddress->title = Craft::t('commerce', 'Shipping Address'); // Ensure the address is labelled correctly - Craft::$app->getElements()->saveElement($shippingAddress, false); - $orderRecord->shippingAddressId = $shippingAddress->id; - $this->setShippingAddress($shippingAddress); - // Set primary shipping if asked - if ($this->makePrimaryShippingAddress && $currentUserIsCustomer && $this->sourceShippingAddressId) { - Plugin::getInstance()->getCustomers()->savePrimaryShippingAddressId($this->getCustomer(), $this->sourceShippingAddressId); - } - } else { - $orderRecord->shippingAddressId = null; - $this->setShippingAddress(null); - } - - if ($billingAddress = $this->getBillingAddress()) { - // If these were set to the same address element, we don't want the same address IDs - if ($shippingAddress && $billingAddress->id == $shippingAddress->id) { - $billingAddress = Craft::$app->getElements()->duplicateElement($billingAddress, - ['owner' => $this, 'title' => Craft::t('commerce', 'Billing Address')]); - } else { - // If we only set the owner ID an element query will be triggered. If this is a brand-new order we will encounter an error - // This is because the order record has not been saved. - // We can avoid this by simply fully setting the owner on the address element. This is also a performance optimisation to avoid an extra query. - $billingAddress->setOwner($this); // Always ensure the address is owned by the order - $billingAddress->title = Craft::t('commerce', 'Billing Address'); // Ensure the address is labelled correctly - Craft::$app->getElements()->saveElement($billingAddress, false); - } - - $orderRecord->billingAddressId = $billingAddress->id; - $this->setBillingAddress($billingAddress); - // Set primary billing if asked - if ($this->makePrimaryBillingAddress && $currentUserIsCustomer && $this->sourceBillingAddressId) { - Plugin::getInstance()->getCustomers()->savePrimaryBillingAddressId($this->getCustomer(), $this->sourceBillingAddressId); - } - } else { - $orderRecord->billingAddressId = null; - $this->setBillingAddress(null); - } - - if ($estimatedShippingAddress = $this->getEstimatedShippingAddress()) { - // If we only set the owner ID an element query will be triggered. If this is a brand-new order we will encounter an error - // This is because the order record has not been saved. - // We can avoid this by simply fully setting the owner on the address element. This is also a performance optimisation to avoid an extra query. - $estimatedShippingAddress->setPrimaryOwner($this); // Always ensure the address is owned by the order - Craft::$app->getElements()->saveElement($estimatedShippingAddress, false); - $orderRecord->estimatedShippingAddressId = $estimatedShippingAddress->id; - $this->setEstimatedShippingAddress($estimatedShippingAddress); - - // If estimate billing same as shipping set it here - if ($this->estimatedBillingSameAsShipping) { - $orderRecord->estimatedBillingAddressId = $estimatedShippingAddress->id; - $this->setEstimatedBillingAddress($estimatedShippingAddress); - } - } - - if (!$this->estimatedBillingSameAsShipping && $estimatedBillingAddress = $this->getEstimatedBillingAddress()) { - // If we only set the owner ID an element query will be triggered. If this is a brand-new order we will encounter an error - // This is because the order record has not been saved. - // We can avoid this by simply fully setting the owner on the address element. This is also a performance optimisation to avoid an extra query. - $estimatedBillingAddress->setOwner($this); // Always ensure the address is owned by the order - Craft::$app->getElements()->saveElement($estimatedBillingAddress, false); - $orderRecord->estimatedBillingAddressId = $estimatedBillingAddress->id; - $this->setEstimatedBillingAddress($estimatedBillingAddress); - } - - $orderRecord->save(false); - - $this->_saveAdjustments(); - $this->_saveLineItems(); - $this->_saveNotices(); - $this->_deleteOrphanedOrderAddresses(); - } catch (Exception $exception) { - $mutex->release($lockKey); - throw $exception; - } - - $mutex->release($lockKey); - - // We can do this after the lock - $this->_saveOrderHistory($oldStatusId, $orderRecord->orderStatusId); - - parent::afterSave($isNew); - } - - public function getShortNumber(): string - { - return substr($this->number, 0, 7); - } - - /** - * @inheritdoc - */ - public function getLink(?string $title = null, array $options = []): ?Markup - { - if ($title) { - $options['title'] = $title; - } - - $title = $title ?: ($this->reference ?: $this->getShortNumber()); - $link = Html::a($title, $this->getCpEditUrl(), $options); - - return Template::raw($link); - } - - /** - * @inheritdoc - */ - public function getCpEditUrl(): ?string - { - return UrlHelper::cpUrl('commerce/orders/' . $this->id); - } - - /** - * Returns the URL to the order's PDF invoice. - * - * @param string|null $option The option that should be available to the PDF template (e.g. "receipt") - * @param string|null $pdfHandle The handle of the PDF to use. If none is passed the default PDF is used. - * @param bool $inline Whether the PDF should be displayed inline in the browser (default: false) - * @return string The URL to the order's PDF invoice with a secure token - */ - public function getPdfUrl(string $option = null, string $pdfHandle = null, bool $inline = false): string - { - return Plugin::getInstance()->getPdfs()->getPdfUrl($this, $option, $pdfHandle, $inline); - } - - /** - * Returns the URL to the cart's load action url with a secure token. - * - * @return string|null The URL to the order's load cart URL, or null if the cart is an order - * @noinspection PhpUnused - */ - public function getLoadCartUrl(): ?string - { - if ($this->isCompleted) { - return null; - } - - return Plugin::getInstance()->getCarts()->getLoadCartUrl($this); - } - - /** - * Returns the order customer ID. - * - * @return int|null - * @since 4.0.0 - */ - public function getCustomerId(): ?int - { - return $this->_customerId; - } - - /** - * Sets the order customer ID. - * - * @param int|int[]|null $customerId - * @since 4.0.0 - */ - public function setCustomerId(mixed $customerId): void - { - if (is_array($customerId)) { - $this->_customerId = reset($customerId) ?: null; - } else { - $this->_customerId = $customerId; - } - - $this->_customer = null; - } - - /** - * @return bool - * @since 5.7.0 - */ - public function getCustomerDeleted(): bool - { - return $this->_customerDeleted && !$this->getCustomerId(); - } - - /** - * @param bool $customerDeleted - * @return void - * @since 5.7.0 - */ - public function setCustomerDeleted(bool $customerDeleted): void - { - $this->_customerDeleted = $customerDeleted; - } - - /** - * Returns the order's customer. - * - * --- - * ```php - * $customer = $order->customer; - * ``` - * ```twig - *

By {{ order.customer.name }}

- * ``` - * - * @return User|null - */ - public function getCustomer(): ?User - { - if (!isset($this->_customer)) { - if (!$this->getCustomerId()) { - return null; - } - - if (($this->_customer = Craft::$app->getUsers()->getUserById($this->getCustomerId())) === null) { - $this->_customer = false; - } - } - - return $this->_customer ?: null; - } - - /** - * Sets the order's customer. - * - * @param User|null $customer - */ - public function setCustomer(?User $customer = null): void - { - $this->_customer = $customer; - if ($this->_customer) { - $this->_customerId = $this->_customer->id; - } else { - $this->_customerId = null; - } - } - - /** - * @deprecated in 4.0.0. Use [[getCustomer()]] instead. - */ - public function getUser(): ?User - { - Craft::$app->getDeprecator()->log('Order::getUser()', 'The `Order::getUser()` is deprecated, use `Order::getCustomer()` instead.'); - return $this->getCustomer(); - } - - /** - * Sets the orders user based on the email address provided. - * - * @param string|null $email - * @throws Exception - * @deprecated in 4.3.0. Use [[setCustomer()]] instead. - */ - public function setEmail(?string $email): void - { - Craft::$app->getDeprecator()->log(__METHOD__, '`Order::setEmail()` has been deprecated use `Order::setCustomer()` instead.'); - if (!$email) { - $this->_customer = null; - $this->_customerId = null; - return; - } - - if ($this->_customer && $this->_customer->email === $email) { - return; - } - - $user = Craft::$app->getUsers()->ensureUserByEmail($email); - $this->setCustomer($user); - } - - /** - * Returns the email for this order. Will always be the customer's email if they exist. - * @return string|null - */ - public function getEmail(): ?string - { - return $this->getCustomer()?->email ?? $this->email ?? null; - } - - /** - * Returns a masked version of the email for this order. - * - * @param $length - * @return string - */ - public function getMaskedEmail(): string - { - if ($email = $this->getEmail()) { - return $this->_maskEmail($email); - } - - return ''; - } - - private function _maskEmail($email, $minLength = 3, $maxLength = 10, $mask = "***") - { - $atPos = strrpos($email, "@"); - $name = substr($email, 0, $atPos); - $len = strlen($name); - $domain = substr($email, $atPos); - - if (($len / 2) < $maxLength) { - $maxLength = ($len / 2); - } - - $shortenedEmail = (($len > $minLength) ? substr($name, 0, $maxLength) : ""); - return "{$shortenedEmail}{$mask}{$domain}"; - } - - /** - * @return bool - */ - public function getIsPaid(): bool - { - return !$this->hasOutstandingBalance() && $this->isCompleted; - } - - /** - * @noinspection PhpUnused - */ - public function getIsUnpaid(): bool - { - return $this->hasOutstandingBalance(); - } - - /** - * Returns the paymentAmount for this order. - * - * @throws CurrencyException - */ - public function getPaymentAmount(): float - { - $paymentAmount = $this->getOutstandingBalance(); - - // Only convert if we have differing currencies - if ($this->currency !== $this->getPaymentCurrency()) { - $teller = $this->getTeller(); - $tellerTo = Plugin::getInstance()->getCurrencies()->getTeller($this->getPaymentCurrency()); - $outstandingBalanceAmount = $teller->convertToMoney($this->getOutstandingBalance()); - $outstandingBalanceInPaymentCurrency = Plugin::getInstance()->getPaymentCurrencies()->convertAmount($outstandingBalanceAmount, $this->getPaymentCurrency(), $this->getStore()->id); - - $paymentAmount = (float)$tellerTo->convertToString($outstandingBalanceInPaymentCurrency); - } - - if (isset($this->_paymentAmount) && $this->_paymentAmount >= 0 && $this->_paymentAmount <= $paymentAmount) { - return $this->_paymentAmount; - } - - return $paymentAmount; - } - - /** - * Sets the order's payment amount in the order's currency. This amount is not persisted. - * This will remain null if set to zero or a negative number. - * - * @throws CurrencyException - * @throws InvalidConfigException - */ - public function setPaymentAmount(float $amount): void - { - $paymentCurrency = Plugin::getInstance()->getPaymentCurrencies()->getPaymentCurrencyByIso($this->getPaymentCurrency()); - $amount = Currency::round($amount, $paymentCurrency); - - if ($amount > 0) { - $this->_paymentAmount = $amount; - } - } - - /** - * Returns whether the payment amount currently set is a partial amount of the order's outstanding balance. - * - * @throws CurrencyException - * @throws InvalidConfigException - * @since 3.4.10 - */ - public function isPaymentAmountPartial(): bool - { - $paymentAmountInPrimaryCurrency = Plugin::getInstance()->getPaymentCurrencies()->convertCurrency($this->getPaymentAmount(), $this->getPaymentCurrency(), $this->currency, true); - - return $paymentAmountInPrimaryCurrency < $this->getOutstandingBalance(); - } - - /** - * What is the status of the orders payment - */ - public function getPaidStatus(): string - { - if ($this->getIsPaid() && - $this->getTeller()->greaterThan($this->getTotalPrice(), 0) && - $this->getTeller()->greaterThan($this->getTotalPaid(), $this->getTotalPrice()) - ) { - return self::PAID_STATUS_OVERPAID; - } - - if ($this->getIsPaid()) { - return self::PAID_STATUS_PAID; - } - - if ($this->getTeller()->greaterThan($this->getTotalPaid(), 0)) { - return self::PAID_STATUS_PARTIAL; - } - - return self::PAID_STATUS_UNPAID; - } - - /** - * Customer represented as HTML - * Customer User link represented as HTML - * - * @return string - * @since 3.0 - */ - public function getCustomerLinkHtml(): string - { - $html = ''; - if ($user = $this->getCustomer()) { - $email = Html::encode($user->email); - $html = Html::tag('a', $email, ['href' => $user->getCpEditUrl()]); - } - - return $html; - } - - /** - * @return string - * @throws InvalidConfigException - */ - public function getOrderStatusHtml(): string - { - if ($status = $this->getOrderStatus()) { - return $status->getLabelHtml(); - } - - return ''; - } - - /** - * Paid status represented as HTML - */ - public function getPaidStatusHtml(): string - { - return match ($this->getPaidStatus()) { - self::PAID_STATUS_OVERPAID => Cp::statusLabelHtml(['color' => 'blue', 'label' => Craft::t('commerce', 'Overpaid')]), - self::PAID_STATUS_PAID => Cp::statusLabelHtml(['color' => 'green', 'label' => Craft::t('commerce', 'Paid')]), - self::PAID_STATUS_PARTIAL => Cp::statusLabelHtml(['color' => 'orange', 'label' => Craft::t('commerce', 'Partial')]), - self::PAID_STATUS_UNPAID => Cp::statusLabelHtml(['color' => 'red', 'label' => Craft::t('commerce', 'Unpaid')]), - default => '', - }; - } - - /** - * Returns the raw total of the order, which is the total of all line items and adjustments. This number can be negative, so it is not the price of the order. - * - * @see Order::getTotalPrice() The actual total price of the order. - * - */ - public function getTotal(): float - { - $itemSubtotal = $this->getItemSubtotal(); - $adjustmentsTotal = $this->getAdjustmentsTotal(); - return (float)$this->getTeller()->add($itemSubtotal, $adjustmentsTotal); - } - - /** - * Get the total price of the order, whose minimum value is enforced by the configured {@link Store::getMinimumTotalPriceStrategy() strategy set for minimum total price}. - */ - public function getTotalPrice(): float - { - $total = (float)$this->getTeller()->add($this->getItemSubtotal(), $this->getAdjustmentsTotal()); - // Don't get the pre-rounded total. - $strategy = $this->getStore()->getMinimumTotalPriceStrategy(); - - if ($strategy === Store::MINIMUM_TOTAL_PRICE_STRATEGY_ZERO) { - return (float)$this->getTeller()->max(0, $total); - } - - if ($strategy === Store::MINIMUM_TOTAL_PRICE_STRATEGY_SHIPPING) { - return (float)$this->getTeller()->max($this->getTotalShippingCost(), $total); - } - - return $total; - } - - public function getItemTotal(): float - { - $total = 0; - $teller = $this->getTeller(); - foreach ($this->getLineItems() as $lineItem) { - $total = (float)$teller->add($total, $lineItem->getTotal()); - } - - return $total; - } - - /** - * @since 3.4 - */ - public function hasShippableItems(): bool - { - foreach ($this->getLineItems() as $item) { - if ($item->getIsShippable()) { - return true; - } - } - - return false; - } - - /** - * Returns the difference between the order amount and amount paid. - * - * @return float The outstanding balance. - */ - public function getOutstandingBalance(): float - { - return (float)$this->getTeller()->subtract($this->getTotalPrice(), $this->getTotalPaid()); - } - - /** - * @return bool Whether the order has an outstanding balance. - */ - public function hasOutstandingBalance(): bool - { - return $this->getTeller()->greaterThan($this->getOutstandingBalance(), 0); - } - - /** - * Returns the total `purchase` and `captured` transactions belonging to this order. - * - * @return float The total amount paid. - */ - public function getTotalPaid(): float - { - if ($this->id === null) { - return 0; - } - - if ($this->_transactions === null) { - $this->_transactions = Plugin::getInstance()->getTransactions()->getAllTransactionsByOrderId($this->id); - } - - $transactions = collect($this->_transactions); - - $paid = $transactions->filter(fn($transaction) => $transaction->status == TransactionRecord::STATUS_SUCCESS - && in_array($transaction->type, [TransactionRecord::TYPE_PURCHASE, TransactionRecord::TYPE_CAPTURE]))->sum('amount'); - - $refunded = $transactions->filter(fn($transaction) => $transaction->status == TransactionRecord::STATUS_SUCCESS - && $transaction->type == TransactionRecord::TYPE_REFUND)->sum('amount'); - - return (float)$this->getTeller()->subtract($paid, $refunded); - } - - /** - * @return float - */ - public function getTotalAuthorized(): float - { - if (!$this->id) { - return 0; - } - - $authorized = 0; - $captured = 0; - - if ($this->_transactions === null) { - $this->_transactions = Plugin::getInstance()->getTransactions()->getAllTransactionsByOrderId($this->id); - } - - foreach ($this->_transactions as $transaction) { - $isSuccess = ($transaction->status == TransactionRecord::STATUS_SUCCESS); - $isAuth = ($transaction->type == TransactionRecord::TYPE_AUTHORIZE); - $isCapture = ($transaction->type == TransactionRecord::TYPE_CAPTURE); - - if (!$isSuccess) { - continue; - } - - if ($isAuth) { - $authorized += $transaction->amount; - continue; - } - - if ($isCapture) { - $captured += $transaction->amount; - } - } - - return (float)$this->getTeller()->subtract($authorized, $captured); - } - - /** - * Returns whether this order is the user's current active cart. - * - * @throws ElementNotFoundException - * @throws Exception - * @throws Throwable - */ - public function getIsActiveCart(): bool - { - $cart = Plugin::getInstance()->getCarts()->getCart(); - - return $cart->id == $this->id; - } - - /** - * Returns whether the order has any items in it. - */ - public function getIsEmpty(): bool - { - return $this->getTotalQty() == 0; - } - - /** - * @noinspection PhpUnused - */ - public function hasLineItems(): bool - { - return (bool)$this->getLineItems(); - } - - /** - * Returns whether the order contains the given purchasable IDs. - * - * @param mixed $purchasableIds One or more purchasable IDs or purchasable models to check for. - * @param ContainsPurchasablesMatch $match The match mode. - * @return bool - */ - public function hasPurchasables(mixed $purchasableIds, ContainsPurchasablesMatch $match = ContainsPurchasablesMatch::Any): bool - { - if (!is_array($purchasableIds)) { - $purchasableIds = [$purchasableIds]; - } - - $orderPurchasableIds = collect($this->getLineItems()) - ->pluck('purchasableId') - ->filter(fn($id) => $id !== null); - - $requestedIds = collect($purchasableIds) - ->map(fn($id) => $id instanceof PurchasableInterface ? $id->getId() : $id) - ->filter(fn($id) => $id !== null); - - if ($match === ContainsPurchasablesMatch::Any) { - return $orderPurchasableIds->intersect($requestedIds)->isNotEmpty(); - } - - if ($match === ContainsPurchasablesMatch::Only) { - // If there are custom line items (null purchasableId), the order - // has purchasables beyond what was specified, so it can't be only. - $hasCustomLineItems = collect($this->getLineItems()) - ->pluck('purchasableId') - ->contains(null); - - if ($hasCustomLineItems) { - return false; - } - - return $orderPurchasableIds->diff($requestedIds)->isEmpty() - && $requestedIds->diff($orderPurchasableIds)->isEmpty(); - } - - // ContainsPurchasablesMatch::All — every requested purchasable must exist in the order - return $requestedIds->every(fn($id) => $orderPurchasableIds->contains($id)); - } - - - /** - * @return int - * @throws InvalidConfigException - * @throws DeprecationException - * @since 5.0.0 - */ - public function getTotalCommittedStock(): int - { - return Plugin::getInstance()->getInventory()->getInventoryFulfillmentLevels($this)->sum('committedQuantity') ?? 0; - } - - /** - * Returns total number of items. - */ - public function getTotalQty(): int - { - $qty = 0; - foreach ($this->getLineItems() as $item) { - $qty += $item->qty; - } - - return $qty; - } - - /** - * @return LineItem[] - */ - public function getLineItems(): array - { - if (!isset($this->_lineItems)) { - $lineItems = $this->id ? Plugin::getInstance()->getLineItems()->getAllLineItemsByOrderId($this->id) : []; - foreach ($lineItems as $lineItem) { - $lineItem->setOrder($this); - } - $this->_lineItems = $lineItems; - } - - return array_filter($this->_lineItems); - } - - /** - * @param LineItem[] $lineItems - */ - public function setLineItems(array $lineItems): void - { - $this->_lineItems = []; - - foreach ($lineItems as $lineItem) { - $lineItem->setOrder($this); - } - - $this->_lineItems = $lineItems; - } - - public function _getAdjustmentsTotalByType(array|string $types, bool $included = false): float|int - { - $amount = 0; - $teller = $this->getTeller(); - - if (is_string($types)) { - $types = StringHelper::split($types); - } - - foreach ($this->getAdjustments() as $adjustment) { - if ($adjustment->included == $included && in_array($adjustment->type, $types, false)) { - $amount = (float)$teller->add($amount, $adjustment->amount); - } - } - - return $amount; - } - - /** - * The total amount of tax adjustments that are additive taxes that affect total price. - * - * @return float - */ - public function getTotalTax(): float - { - return $this->_getAdjustmentsTotalByType('tax'); - } - - /** - * The total amount of tax adjustments on the order that are included in the price, and do not affect total price. - * - * @return float - */ - public function getTotalTaxIncluded(): float - { - return $this->_getAdjustmentsTotalByType('tax', true); - } - - /** - * The total amount of discount adjustments. - * - * @return float - */ - public function getTotalDiscount(): float - { - return $this->_getAdjustmentsTotalByType('discount'); - } - - /** - * The total amount of shipping adjustments. - * - * @return float - */ - public function getTotalShippingCost(): float - { - return $this->_getAdjustmentsTotalByType('shipping'); - } - - /** - * @noinspection PhpUnused - */ - public function getTotalWeight(): float - { - $weight = 0; - foreach ($this->getLineItems() as $item) { - $weight += ($item->qty * $item->weight); - } - - return $weight; - } - - /** - * Returns the total promotional amount. - * @since 5.0.0 - */ - public function getTotalPromotionalAmount(): float - { - $value = 0; - $teller = $this->getTeller(); - foreach ($this->getLineItems() as $item) { - $value = (float)$teller->add( - $value, - $teller->multiply($item->qty, $item->getPromotionalAmount()), - ); - } - - return $value; - } - - /** - * Returns the total sale amount. - * @deprecated in 5.0.0. Use [[getTotalPromotionalAmount()]] instead. - */ - public function getTotalSaleAmount(): float - { - Craft::$app->getDeprecator()->log(__METHOD__, '`getTotalSaleAmount()` method has been deprecated. Use `getTotalPromotionalAmount()` instead.'); - return $this->getTotalPromotionalAmount(); - } - - /** - * Returns the total of all line item's subtotals. - */ - public function getItemSubtotal(): float - { - $value = 0; - $teller = $this->getTeller(); - foreach ($this->getLineItems() as $item) { - $value = (float)$teller->add($value, $item->getSubtotal()); - } - - return $value; - } - - /** - * Returns the total of adjustments made to order. - * - * @return float - * @throws InvalidConfigException - * @noinspection PhpUnused - */ - public function getAdjustmentSubtotal(): float - { - $value = 0; - $teller = $this->getTeller(); - foreach ($this->getAdjustments() as $adjustment) { - if (!$adjustment->included) { - $value = (float)$teller->add($value, $adjustment->amount); - } - } - - return (float)$value; - } - - /** - * @return OrderAdjustment[]|null - * @throws InvalidConfigException - */ - public function getAdjustments(): ?array - { - if (isset($this->_orderAdjustments)) { - return $this->_orderAdjustments; - } - - if ($this->id) { - $this->setAdjustments(Plugin::getInstance()->getOrderAdjustments()->getAllOrderAdjustmentsByOrderId($this->id)); - } - - return $this->_orderAdjustments ?? []; - } - - /** - * @since 3.0 - */ - public function getAdjustmentsByType(string $type): array - { - $adjustments = []; - - foreach ($this->getAdjustments() as $adjustment) { - if ($adjustment->type === $type) { - $adjustments[] = $adjustment; - } - } - - return $adjustments; - } - - public function getOrderAdjustments(): array - { - $adjustments = $this->getAdjustments(); - $orderAdjustments = []; - - foreach ($adjustments as $adjustment) { - if (!$adjustment->getLineItem() && $adjustment->orderId == $this->id) { - $orderAdjustments[] = $adjustment; - } - } - - return $orderAdjustments; - } - - /** - * @param OrderAdjustment[] $adjustments - */ - public function setAdjustments(array $adjustments): void - { - $this->_orderAdjustments = []; - - foreach ($adjustments as $adjustment) { - $adjustment->setOrder($this); - } - - $this->_orderAdjustments = $adjustments; - } - - public function getAdjustmentsTotal(): float - { - $amount = 0; - $teller = $this->getTeller(); - foreach ($this->getAdjustments() as $adjustment) { - if (!$adjustment->included) { - $amount = (float)$teller->add($amount, $adjustment->amount); - } - } - - return $amount; - } - - /** - * * Get the shipping address on the order. - */ - public function getShippingAddress(): ?AddressElement - { - if (!isset($this->_shippingAddress) && $this->shippingAddressId) { - /** @var AddressElement|null $address */ - $address = AddressElement::find() - ->owner($this) - ->id($this->shippingAddressId) - ->one(); - - $this->_shippingAddress = $address; - } - - return $this->_shippingAddress; - } - - /** - * Set the shipping address on the order. - * - * @param AddressElement|array|null $address - */ - public function setShippingAddress(AddressElement|array|null $address): void - { - if ($address === null) { - $this->shippingAddressId = null; - $this->_shippingAddress = null; - return; - } - - if (is_array($address)) { - unset($address['id']); - $addressElement = $this->_shippingAddress ?: new AddressElement(); - $addressElement->setAttributes($address); - if (!empty($address['fields']) && is_array($address['fields'])) { - $addressElement->setFieldValues($address['fields']); - } - $this->_populateAddressNameAttributes($addressElement, $address); - $addressElement->setPrimaryOwner($this); - $address = $addressElement; - } - - if (!$address instanceof AddressElement) { - throw new InvalidArgumentException('Shipping address supplied is not an Address Element'); - } - - // Ensure that address can only belong to this order - if ($address->getPrimaryOwnerId() != $this->id) { - throw new InvalidArgumentException('Can not set a shipping address on the order that is not owned by the order.'); - } - - $this->shippingAddressId = $address->id; - $address->title = Craft::t('commerce', 'Shipping Address'); - $this->_shippingAddress = $address; - } - - /** - * @since 3.1 - */ - public function removeShippingAddress(): void - { - $this->shippingAddressId = null; - $this->_shippingAddress = null; - } - - /** - * @since 2.2 - */ - public function getEstimatedShippingAddress(): ?AddressElement - { - if (!isset($this->_estimatedShippingAddress) && $this->estimatedShippingAddressId) { - /** @var AddressElement|null $address */ - $address = AddressElement::find()->owner($this)->id($this->estimatedShippingAddressId)->one(); - $this->_estimatedShippingAddress = $address; - } - - return $this->_estimatedShippingAddress; - } - - /** - * @since 2.2 - */ - public function setEstimatedShippingAddress(AddressElement|array|null $address): void - { - if ($address === null) { - $this->estimatedShippingAddressId = null; - $this->_estimatedShippingAddress = null; - return; - } - - if (!$address instanceof AddressElement) { - $addressElement = new AddressElement(); - $addressElement->setAttributes($address); - if (!empty($address['fields']) && is_array($address['fields'])) { - $addressElement->setFieldValues($address['fields']); - } - $address = $addressElement; - } - - $this->estimatedShippingAddressId = $address->id; - $this->_estimatedShippingAddress = $address; - } - - /** - * Get the billing address on the order. - */ - public function getBillingAddress(): ?AddressElement - { - if (!isset($this->_billingAddress) && $this->billingAddressId) { - /** @var AddressElement|null $address */ - $address = AddressElement::find() - ->owner($this) - ->id($this->billingAddressId) - ->one(); - - $this->_billingAddress = $address; - } - - return $this->_billingAddress; - } - - /** - * Set the billing address on the order. - * - * @param AddressElement|array|null $address - */ - public function setBillingAddress(AddressElement|array|null $address): void - { - if ($address === null) { - $this->billingAddressId = null; - $this->_billingAddress = null; - return; - } - - if (is_array($address)) { - unset($address['id']); // only ever allow setting of the address data - $addressElement = $this->_billingAddress ?: new AddressElement(); - $addressElement->setAttributes($address); - if (!empty($address['fields']) && is_array($address['fields'])) { - $addressElement->setFieldValues($address['fields']); - } - $this->_populateAddressNameAttributes($addressElement, $address); - $addressElement->setPrimaryOwner($this); - $address = $addressElement; - } - - if (!$address instanceof AddressElement) { - throw new InvalidArgumentException('Billing address supplied is not an Address Element'); - } - - // Ensure that address can only belong to this order - if ($address->getPrimaryOwnerId() !== $this->id) { - throw new InvalidArgumentException('Can not set a billing address on the order that is not owned by the order.'); - } - - $address->ownerId = $this->id; - $this->billingAddressId = $address->id; - $address->title = Craft::t('commerce', 'Billing Address'); - $this->_billingAddress = $address; - } - - /** - * @since 3.1 - */ - public function removeBillingAddress(): void - { - $this->billingAddressId = null; - $this->_billingAddress = null; - } - - /** - * Returns whether the billing and shipping addresses' data matches - * - * @param string[]|null $attributes array of attributes names on which to match the addresses - * @return bool - * @since 4.1.0 - */ - public function hasMatchingAddresses(?array $attributes = null): bool - { - $addressAttributes = (new ReflectionClass(AddressInterface::class))->getMethods(); - $addressAttributes = array_map(static fn(ReflectionMethod $method) => // Remove `get` and lower case first character - lcfirst(substr($method->name, 3)), $addressAttributes); - - $relationCustomFieldHandles = []; - $customFieldHandles = array_map(static function(FieldInterface $field) use (&$relationCustomFieldHandles) { - if ($field instanceof BaseRelationField) { - $relationCustomFieldHandles[] = $field->handle; - } - - return $field->handle; - }, (new AddressElement())->getFieldLayout()->getCustomFields()); - - $nameTraitProperties = array_map(static fn(ReflectionProperty $property) => $property->name, (new ReflectionClass(NameTrait::class))->getProperties()); - - $toArrayHandles = [...$nameTraitProperties, ...$addressAttributes, ...$customFieldHandles]; - - if (!empty($attributes)) { - $toArrayHandles = array_intersect($toArrayHandles, $attributes); - } - - // Figure out if we need to do any extra work for custom fields - $toArrayRelationFields = !empty($relationCustomFieldHandles) ? array_intersect($toArrayHandles, $relationCustomFieldHandles) : []; - - $matchingShippingAddress = []; - if ($this->getShippingAddress() instanceof AddressElement) { - $matchingShippingAddress = $this->getShippingAddress()->toArray(array_diff($toArrayHandles, $toArrayRelationFields)); - } - - $matchingBillingAddress = []; - if ($this->getBillingAddress() instanceof AddressElement) { - $matchingBillingAddress = $this->getBillingAddress()->toArray(array_diff($toArrayHandles, $toArrayRelationFields)); - } - - // Add any relational custom fields to the matching arrays - if (!empty($toArrayRelationFields)) { - foreach ($toArrayRelationFields as $handle) { - if ($this->getShippingAddress() instanceof AddressElement) { - $matchingShippingAddress[$handle] = $this->getShippingAddress()->getFieldValue($handle)?->ids(); - } - - if ($this->getBillingAddress() instanceof AddressElement) { - $matchingBillingAddress[$handle] = $this->getBillingAddress()->getFieldValue($handle)?->ids(); - } - } - } - - return $matchingBillingAddress == $matchingShippingAddress; - } - - /** - * @since 2.2 - */ - public function getEstimatedBillingAddress(): ?AddressElement - { - if (!isset($this->_estimatedBillingAddress) && $this->estimatedBillingAddressId) { - /** @var AddressElement|null $address */ - $address = AddressElement::find()->owner($this)->id($this->estimatedBillingAddressId)->one(); - $this->_estimatedBillingAddress = $address; - } - - return $this->_estimatedBillingAddress; - } - - /** - * @since 2.2 - */ - public function setEstimatedBillingAddress(AddressElement|array|null $address): void - { - if ($address === null) { - $this->estimatedBillingAddressId = null; - $this->_estimatedBillingAddress = null; - return; - } - - if (!$address instanceof AddressElement) { - $addressElement = new AddressElement(); - $addressElement->setAttributes($address); - if (!empty($address['fields']) && is_array($address['fields'])) { - $addressElement->setFieldValues($address['fields']); - } - $address = $addressElement; - } - - $this->estimatedBillingAddressId = $address->id; - $this->_estimatedBillingAddress = $address; - } - - /** - * @return ShippingMethod|null - * @throws InvalidConfigException - * @deprecated in 3.4.18. Use `$shippingMethodHandle` or `$shippingMethodName` instead. - */ - public function getShippingMethod(): ?ShippingMethod - { - return Plugin::getInstance()->getShippingMethods()->getShippingMethodByHandle((string)$this->shippingMethodHandle); - } - - /** - * @return GatewayInterface|null - * @throws InvalidArgumentException - */ - public function getGateway(): ?GatewayInterface - { - if ($this->gatewayId === null && $this->paymentSourceId === null) { - return null; - } - - $gateway = null; - - // sources before gateways - if ($this->paymentSourceId) { - if ($paymentSource = Plugin::getInstance()->getPaymentSources()->getPaymentSourceById($this->paymentSourceId)) { - $gateway = Plugin::getInstance()->getGateways()->getGatewayById($paymentSource->gatewayId); - } - } else { - if ($this->gatewayId) { - $gateway = Plugin::getInstance()->getGateways()->getGatewayById((int)$this->gatewayId); - } - } - - return $gateway; - } - - /** - * Returns the current payment currency, and defaults to the primary currency if not set. - */ - public function getPaymentCurrency(): string - { - if ($this->_paymentCurrency === null) { - $this->_paymentCurrency = $this->getStore()->getCurrency(); - } - - return $this->_paymentCurrency; - } - - /** - * @param string $value the payment currency code - */ - public function setPaymentCurrency(string $value): void - { - $this->_paymentCurrency = $value; - } - - /** - * Returns the order's selected payment source if any. - * - * @throws InvalidConfigException if the payment source is being set by a guest customer. - * @throws InvalidArgumentException if the order is set to an invalid payment source. - */ - public function getPaymentSource(): ?PaymentSource - { - if ($this->paymentSourceId === null) { - return null; - } - - if (($user = $this->getCustomer()) === null) { - throw new InvalidConfigException('Guest customers can not set a payment source.'); - } - - if (($paymentSource = Plugin::getInstance()->getPaymentSources()->getPaymentSourceByIdAndUserId($this->paymentSourceId, $user->id)) === null) { - throw new InvalidArgumentException("Invalid payment source ID: $this->paymentSourceId"); - } - - return $paymentSource; - } - - /** - * Sets the order's selected payment source - */ - public function setPaymentSource(?PaymentSource $paymentSource): void - { - // Setting the payment source to null clears it - if ($paymentSource === null) { - $this->paymentSourceId = null; - return; - } - - // We are now dealing with a PaymentSource - $customer = $this->getCustomer(); - if ($customer?->id && $paymentSource->getCustomer()?->id !== $customer->id) { - throw new InvalidArgumentException('PaymentSource is not owned by the user of the order.'); - } - - $this->paymentSourceId = $paymentSource->id; - $this->gatewayId = null; - } - - /** - * Sets the order's selected gateway id. - */ - public function setGatewayId(int $gatewayId): void - { - $this->gatewayId = $gatewayId; - $this->paymentSourceId = null; - } - - /** - * @return OrderHistory[] - */ - public function getHistories(): array - { - if ($this->id === null) { - return []; - } - - $histories = Plugin::getInstance()->getOrderHistories()->getAllOrderHistoriesByOrderId($this->id); - - foreach ($histories as $history) { - $history->setOrder($this); - } - - return $histories; - } - - /** - * Set transactions on the order. Set to null to clear cache and force next getTransactions() call to get the latest transactions. - * - * @param Transaction[]|null $transactions - * @since 3.2.0 - */ - public function setTransactions(?array $transactions): void - { - $this->_transactions = $transactions; - } - - /** - * @return Transaction[] - */ - public function getTransactions(): array - { - if ($this->id === null) { - $this->_transactions = []; - } - - if ($this->_transactions === null) { - $transactions = Plugin::getInstance()->getTransactions()->getAllTransactionsByOrderId($this->id); - - foreach ($transactions as $transaction) { - $transaction->setOrder($this); - } - - $this->_transactions = $transactions; - } - - return $this->_transactions; - } - - /** - * @noinspection PhpUnused - */ - public function getLastTransaction(): ?Transaction - { - $transactions = $this->getTransactions(); - return count($transactions) ? array_pop($transactions) : null; - } - - /** - * Returns an array of transactions for the order that have child transactions set on them. - * - * @return Transaction[] - */ - public function getNestedTransactions(): array - { - // Transactions come in sorted by `id ASC`. - // Given that transactions cannot be modified, it means that parents will always come first. - // So we can just store a reference to them and build our tree in one pass. - $transactions = $this->getTransactions(); - - /** @var Transaction[] $referenceStore */ - $referenceStore = []; - $nestedTransactions = []; - - foreach ($transactions as $transaction) { - // We'll be adding all of the children in this loop, anyway, so we set the children list to an empty array. - // This way no db queries are triggered when transactions are queried for children. - $transaction->setChildTransactions([]); - if ($transaction->parentId && isset($referenceStore[$transaction->parentId])) { - $referenceStore[$transaction->parentId]->addChildTransaction($transaction); - } else { - $nestedTransactions[] = $transaction; - } - - $referenceStore[$transaction->id] = $transaction; - } - - return $nestedTransactions; - } - - /** - * @throws InvalidConfigException - */ - public function getOrderStatus(): ?OrderStatus - { - return $this->orderStatusId !== null ? Plugin::getInstance()->getOrderStatuses()->getOrderStatusById($this->orderStatusId, $this->storeId) : null; - } - - /** - * Get the site for the order. - * - * @since 3.2.9 - */ - public function getOrderSite(): ?Site - { - if (!$this->orderSiteId) { - return null; - } - - return Craft::$app->getSites()->getSiteById($this->orderSiteId); - } - - /** - * @inheritdoc - */ - public function getMetadata(): array - { - $metadata = []; - - if ($this->isCompleted) { - $metadata[Craft::t('commerce', 'Reference')] = Html::encode($this->reference); - $metadata[Craft::t('commerce', 'Date Ordered')] = Craft::$app->getFormatter()->asDatetime($this->dateOrdered, 'short'); - } - - $metadata[Craft::t('commerce', 'Coupon Code')] = Html::encode($this->couponCode); - - $orderSite = $this->getOrderSite(); - $metadata[Craft::t('commerce', 'Order Site')] = Html::encode($orderSite?->getName() ?? ''); - - $metadata[Craft::t('commerce', 'Shipping Method')] = Html::encode($this->shippingMethodName ?? ''); - - $metadata[Craft::t('app', 'ID')] = $this->id; - $metadata[Craft::t('commerce', 'Short Number')] = $this->getShortNumber(); - $metadata[Craft::t('commerce', 'Paid Status')] = $this->getPaidStatusHtml(); - $metadata[Craft::t('commerce', 'Total Price')] = $this->totalPriceAsCurrency; - $metadata[Craft::t('commerce', 'Paid Amount')] = $this->totalPaidAsCurrency; - $metadata[Craft::t('commerce', 'Origin')] = Html::encode($this->origin); - - return array_merge($metadata, parent::getMetadata()); - } - - /** - * @inheritdoc - */ - public function beforeDelete(): bool - { - if (!parent::beforeDelete()) { - return false; - } - - // Capture line items before the cascade delete fires so afterDelete() can refresh stock caches - if ($this->isCompleted) { - $this->_deletingLineItems = $this->getLineItems(); - } - - return true; - } - - /** - * @inheritdoc - */ - public function afterDelete(): void - { - parent::afterDelete(); - - if ($this->isCompleted) { - foreach ($this->_deletingLineItems as $lineItem) { - $purchasable = $lineItem->getPurchasable(); - if ($purchasable instanceof Purchasable && $purchasable::hasInventory() && $purchasable->inventoryTracked) { - Plugin::getInstance()->getPurchasables()->updateStoreStockCache($purchasable, true); - } - } - } - } - - /** - * Updates the adjustments, including deleting the old ones. - * - * @throws Exception - * @throws Throwable - * @throws StaleObjectException - */ - private function _saveAdjustments(): void - { - $newAdjustmentIds = []; - - foreach ($this->getAdjustments() as $adjustment) { - try { - // Don't run validation as validation of the adjustment should happen before saving the order - Plugin::getInstance()->getOrderAdjustments()->saveOrderAdjustment($adjustment, false); - } catch (OrderAdjustmentNotFoundException) { - // If the adjustment was not found, it means it may have previously existed but was already deleted (race condition). - // See: https://github.com/craftcms/commerce/issues/3283 - continue; - } - - $newAdjustmentIds[] = $adjustment->id; - $adjustment->orderId = $this->id; - } - - // Make sure all other adjustments have been cleaned up. - Db::delete( - Table::ORDERADJUSTMENTS, - ['and', ['orderId' => $this->id], ['not', ['id' => $newAdjustmentIds]]] - ); - } - - - /** - * @throws StaleObjectException - * @throws Throwable - */ - private function _saveNotices(): void - { - $previousNoticeIds = (new Query()) - ->select(['id']) - ->from([Table::ORDERNOTICES]) - ->where(['orderId' => $this->id]) - ->column(); - - $currentNoticeIds = []; - - // We are never updating a notice, just adding it or keeping it. - foreach (array_merge($this->getNotices(), $this->getAdminNotices()) as $notice) { - if ($notice->id === null) { - $orderNoticeEvent = new OrderNoticeEvent([ - 'orderNotice' => $notice, - ]); - - // Raising the 'beforeAddNoticeToOrder' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_APPLY_ADD_NOTICE)) { - $this->trigger(self::EVENT_BEFORE_APPLY_ADD_NOTICE, $orderNoticeEvent); - - if ($orderNoticeEvent->isValid === false) { - continue; - } - } - $noticeRecord = new OrderNoticeRecord(); - $noticeRecord->orderId = $notice->orderId; - $noticeRecord->type = $notice->type; - $noticeRecord->attribute = $notice->attribute; - $noticeRecord->message = $notice->message; - $noticeRecord->noticeType = $notice->noticeType->value; - if ($noticeRecord->save(false)) { - $notice->id = $noticeRecord->id; - } - } - - $currentNoticeIds[] = $notice->id; - } - - // Delete any notices that are no longer on the order - if ($deletableNoticeIds = array_diff($previousNoticeIds, $currentNoticeIds)) { - OrderNoticeRecord::deleteAll(['id' => $deletableNoticeIds]); - } - } - - /** - * Updates the line items, including deleting the old ones. - * - * @throws Throwable - */ - private function _saveLineItems(): void - { - // Line items that are currently in the DB - /** @var null|array|LineItemRecord[] $previousLineItems */ - $previousLineItems = LineItemRecord::find() - ->where(['orderId' => $this->id]) - ->all(); - - $currentLineItemIds = []; - - // Determine the line items that will be saved - foreach ($this->getLineItems() as $lineItem) { - // If the ID is null that's ok, it's a new line item and will be saved anyway - $currentLineItemIds[] = $lineItem->id; - } - - // Delete any line items that no longer will be saved on this order. - foreach ($previousLineItems as $previousLineItem) { - if (!in_array($previousLineItem->id, $currentLineItemIds, false)) { - $lineItem = Plugin::getInstance()->getLineItems()->getLineItemById($previousLineItem->id); - - $previousLineItem->delete(); - - if ($this->hasEventHandlers(self::EVENT_AFTER_APPLY_REMOVE_LINE_ITEM)) { - $this->trigger(self::EVENT_AFTER_APPLY_REMOVE_LINE_ITEM, new LineItemEvent([ - 'lineItem' => $lineItem, - ])); - } - } - } - - // Save the line items last, as we know that any possible duplicates are already removed. - // We also need to re-save any adjustments that didn't have a line item ID for a line item if it's new. - foreach ($this->getLineItems() as $lineItem) { - $originalId = $lineItem->id; - $lineItem->setOrder($this); // just in case. - - try { - // Don't run validation as validation of the line item should happen before saving the order - Plugin::getInstance()->getLineItems()->saveLineItem($lineItem, false); - } catch (LineItemNotFoundException) { - // If the line item was not found, it means it may have previously existed but was already deleted (race condition). - // See: https://github.com/craftcms/commerce/issues/3283 - continue; - } - - // Is this a new line item? - if ($originalId === null) { - // Raising the 'afterAddLineItemToOrder' event - if ($this->hasEventHandlers(self::EVENT_AFTER_APPLY_ADD_LINE_ITEM)) { - $this->trigger(self::EVENT_AFTER_APPLY_ADD_LINE_ITEM, new LineItemEvent([ - 'lineItem' => $lineItem, - 'isNew' => true, - ])); - } - } - - // Update any adjustments to this line item with the new line item ID. - foreach ($this->getAdjustments() as $adjustment) { - // Was the adjustment for this line item, but the line item ID didn't exist when the adjustment was made? - if ($adjustment->getLineItem() === $lineItem && !$adjustment->lineItemId) { - // Re-save the adjustment with the new line item ID, since it exists now. - $adjustment->lineItemId = $lineItem->id; - // Validation not needed as the adjustments are validated before the order is saved - try { - Plugin::getInstance()->getOrderAdjustments()->saveOrderAdjustment($adjustment, false); - } catch (OrderAdjustmentNotFoundException) { - // This can happen if the adjustment was removed during a race condition recalculation. - continue; - } - } - } - } - } - - /** - * Delete all addresses that are owned by the order but are not in use. - * - * @return void - * @throws Throwable - */ - private function _deleteOrphanedOrderAddresses(): void - { - if (!$this->id) { - return; - } - - $safeIds = array_filter([ - $this->getBillingAddress()?->id, - $this->getShippingAddress()?->id, - $this->getEstimatedBillingAddress()?->id, - $this->getEstimatedShippingAddress()?->id, - ]); - - $orphanedAddresses = AddressElement::find() - ->ownerId($this->id); - - if (!empty($safeIds)) { - ArrayHelper::prependOrAppend($safeIds, 'not', true); - $orphanedAddresses->id($safeIds); - } - - ($orphanedAddresses->collect())->each(function(AddressElement $address) { - Craft::$app->getElements()->deleteElement($address, true); - }); - } - - /** - * @param ?int $oldStatusId - * @param ?int $currentOrderStatId - * @return void - */ - private function _saveOrderHistory(?int $oldStatusId, ?int $currentOrderStatId): void - { - $hasNewStatus = ($oldStatusId !== $currentOrderStatId); - if ($this->isCompleted && $hasNewStatus) { - if (!Plugin::getInstance()->getOrderHistories()->createOrderHistoryFromOrder($this, $oldStatusId)) { - Craft::error('Error saving order history after order save.', __METHOD__); - } - } - } - - /** - * Sets the first and last name attributes on the address model if no full name is set. - * - * @param AddressElement $addressElement - * @param array $address - * @return void - */ - private function _populateAddressNameAttributes(AddressElement $addressElement, array $address): void - { - if (!isset($address['fullName']) || !$address['fullName']) { - $firstName = $address['firstName'] ?? null; - $lastName = $address['lastName'] ?? null; - - if ($firstName !== null || $lastName !== null) { - $addressElement->fullName = null; - $addressElement->firstName = $firstName ?? $addressElement->firstName; - $addressElement->lastName = $lastName ?? $addressElement->lastName; - } - } - } -} diff --git a/src/elements/Product.php b/src/elements/Product.php deleted file mode 100644 index acf2965d21..0000000000 --- a/src/elements/Product.php +++ /dev/null @@ -1,2220 +0,0 @@ - - * @since 2.0 - */ -class Product extends Element implements HasStoreInterface -{ - use StoreTrait; - - public const STATUS_LIVE = 'live'; - public const STATUS_PENDING = 'pending'; - public const STATUS_EXPIRED = 'expired'; - - /** - * @event ElementCriteriaEvent The event that is triggered when defining the parent selection criteria. - * @see _parentOptionCriteria() - * @since 5.2.0 - */ - public const EVENT_DEFINE_PARENT_SELECTION_CRITERIA = 'defineParentSelectionCriteria'; - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Product'); - } - - /** - * @inheritdoc - */ - public static function lowerDisplayName(): string - { - return Craft::t('commerce', 'product'); - } - - /** - * @inheritdoc - */ - public static function pluralDisplayName(): string - { - return Craft::t('commerce', 'Products'); - } - - /** - * @inheritdoc - */ - public static function pluralLowerDisplayName(): string - { - return Craft::t('commerce', 'products'); - } - - /** - * @inheritdoc - */ - public static function refHandle(): ?string - { - return 'product'; - } - - /** - * @inheritdoc - */ - public static function hasDrafts(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public static function trackChanges(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public static function hasTitles(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public static function hasUris(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public static function isLocalized(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public static function hasStatuses(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public static function statuses(): array - { - return [ - self::STATUS_LIVE => Craft::t('commerce', 'Live'), - self::STATUS_PENDING => Craft::t('commerce', 'Pending'), - self::STATUS_EXPIRED => Craft::t('commerce', 'Expired'), - self::STATUS_DISABLED => Craft::t('commerce', 'Disabled'), - ]; - } - - /** - * @inheritdoc - * @return ProductQuery The newly created [[ProductQuery]] instance. - */ - public static function find(): ElementQueryInterface - { - return new ProductQuery(static::class); - } - - /** - * @inheritdoc - * @return ProductCondition - */ - public static function createCondition(): ElementConditionInterface - { - return Craft::createObject(ProductCondition::class, [static::class]); - } - - /** - * @inheritdoc - */ - protected static function defineSources(string $context = null): array - { - if ($context == 'index') { - $productTypes = Plugin::getInstance()->getProductTypes()->getViewableProductTypes(); - $editable = true; - } else { - $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); - $editable = null; - } - - $productTypeIds = []; - - foreach ($productTypes as $productType) { - $productTypeIds[] = $productType->id; - } - - $sources = [ - [ - 'key' => '*', - 'label' => Craft::t('commerce', 'All products'), - 'criteria' => [ - 'typeId' => $productTypeIds, - 'editable' => $editable, - ], - 'defaultSort' => ['postDate', 'desc'], - ], - ]; - - $sources[] = ['heading' => Craft::t('commerce', 'Product Types')]; - - $user = Craft::$app->getUser()->getIdentity(); - - foreach ($productTypes as $productType) { - $key = 'productType:' . $productType->uid; - $canSaveProducts = $user && $user->can('commerce-saveProductType:' . $productType->uid); - - $sources[$key] = [ - 'key' => $key, - 'label' => Craft::t('site', $productType->name), - 'data' => [ - 'handle' => $productType->handle, - 'editable' => $canSaveProducts, - ], - 'criteria' => [ - 'typeId' => $productType->id, - 'editable' => $editable, - ], - // Get site ids enabled for this product type - 'sites' => $productType->getSiteIds(), - ]; - - if ($productType->isStructure) { - $sources[$key]['defaultSort'] = ['structure', 'asc']; - $sources[$key]['structureId'] = $productType->structureId; - $sources[$key]['structureEditable'] = $canSaveProducts; - } else { - $sources[$key]['defaultSort'] = ['postDate', 'desc']; - } - } - - return $sources; - } - - /** - * @inheritdoc - */ - public static function modifyCustomSource(array $config): array - { - try { - /** @var ProductCondition $condition */ - $condition = Craft::$app->getConditions()->createCondition($config['condition']); - } catch (InvalidConfigException) { - return $config; - } - - $rules = $condition->getConditionRules(); - - // see if it's limited to one product type - /** @var ProductTypeConditionRule|null $productTypeRule */ - $productTypeRule = ArrayHelper::firstWhere($rules, fn($rule) => $rule instanceof ProductTypeConditionRule); - $productTypeOptions = $productTypeRule?->getValues(); - - if ($productTypeOptions && count($productTypeOptions) === 1) { - $productType = Plugin::getInstance()->getProductTypes()->getProductTypeByUid(reset($productTypeOptions)); - if ($productType) { - $config['data']['handle'] = $productType->handle; - } - } - - return $config; - } - - /** - * @inheritdoc - */ - protected static function defineFieldLayouts(?string $source): array - { - if ($source === null || $source === '*') { - $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); - } else { - $productTypes = []; - if (preg_match('/^productType:(.+)$/', $source, $matches)) { - $productType = Plugin::getInstance()->getProductTypes()->getProductTypeByUid($matches[1]); - if ($productType) { - $productTypes[] = $productType; - } - } - } - - return array_map(fn(ProductType $productType) => $productType->getFieldLayout(), $productTypes); - } - - /** - * @inheritdoc - */ - protected static function defineActions(string $source = null): array - { - $elementsService = Craft::$app->getElements(); - // Get the selected site - $controller = Craft::$app->controller; - if ($controller instanceof ElementIndexesController) { - /** @var ElementQuery $elementQuery */ - $elementQuery = $controller->getElementQuery(); - } else { - $elementQuery = null; - } - $site = $elementQuery && $elementQuery->siteId - ? Craft::$app->getSites()->getSiteById($elementQuery->siteId) - : Craft::$app->getSites()->getCurrentSite(); - - // Get the section(s) we need to check permissions on - switch ($source) { - case '*': - { - $productTypes = Plugin::getInstance()->getProductTypes()->getViewableProductTypes(); - break; - } - default: - { - if (preg_match('/^productType:(\d+)$/', $source, $matches)) { - $productType = Plugin::getInstance()->getProductTypes()->getProductTypeById((int)$matches[1]); - - if ($productType) { - $productTypes = [$productType]; - } - } elseif (preg_match('/^productType:(.+)$/', $source, $matches)) { - $productType = Plugin::getInstance()->getProductTypes()->getProductTypeByUid($matches[1]); - - if ($productType) { - $productTypes = [$productType]; - } - } - } - } - - $actions = []; - - // Copy Reference Tag - $actions[] = Craft::$app->getElements()->createAction([ - 'type' => CopyReferenceTag::class, - ]); - - // Restore - $actions[] = Craft::$app->getElements()->createAction([ - 'type' => Restore::class, - 'successMessage' => Craft::t('commerce', 'Products restored.'), - 'partialSuccessMessage' => Craft::t('commerce', 'Some products restored.'), - 'failMessage' => Craft::t('commerce', 'Products not restored.'), - ]); - - if ($source === '*') { - // Delete - $actions[] = Delete::class; - } elseif (!empty($productTypes)) { - $userSession = Craft::$app->getUser(); - $currentUser = $userSession->getIdentity(); - - foreach ($productTypes as $productType) { - $canDelete = $currentUser->can('commerce-deleteProductType:' . $productType->uid); - $canCreate = $currentUser->can('commerce-createProductType:' . $productType->uid); - $canSave = $currentUser->can('commerce-saveProductType:' . $productType->uid); - - if ($canCreate && $canSave) { - // Duplicate - $actions[] = [ - 'type' => Duplicate::class, - 'asDrafts' => true, - ]; - } - - if ($canDelete) { - // Allow deletion - $deleteAction = Craft::$app->getElements()->createAction([ - 'type' => Delete::class, - 'confirmationMessage' => Craft::t('commerce', 'Are you sure you want to delete the selected product and its variants?'), - 'successMessage' => Craft::t('commerce', 'Products and Variants deleted.'), - ]); - $actions[] = $deleteAction; - } - - if ($canSave) { - $actions[] = SetStatus::class; - } - - if ( - $productType->isStructure && - $canCreate - ) { - if ($productType->maxLevels != 1) { - $actions[] = [ - 'type' => Duplicate::class, - 'asDrafts' => true, - 'deep' => true, - ]; - } - - $newProductUrl = 'commerce/products/' . $productType->handle . '/new'; - - if (Craft::$app->getIsMultiSite()) { - $newProductUrl .= '?site=' . $site->handle; - } - - $actions[] = $elementsService->createAction([ - 'type' => NewSiblingBefore::class, - 'newSiblingUrl' => $newProductUrl, - ]); - - $actions[] = $elementsService->createAction([ - 'type' => NewSiblingAfter::class, - 'newSiblingUrl' => $newProductUrl, - ]); - - if ($productType->maxLevels != 1) { - $actions[] = $elementsService->createAction([ - 'type' => NewChild::class, - 'maxLevels' => $productType->maxLevels, - 'newChildUrl' => $newProductUrl, - ]); - } - } - } - - if ($userSession->checkPermission('commerce-managePromotions')) { - if (Plugin::getInstance()->getSales()->canUseSales()) { - $actions[] = CreateSale::class; - } - - $actions[] = CreateDiscount::class; - } - } - - return $actions; - } - - /** - * @inheritdoc - */ - protected function safeActionMenuItems(): array - { - $actions = parent::safeActionMenuItems(); - - if ( - Craft::$app->getUser()->getIsAdmin() && - Craft::$app->getConfig()->getGeneral()->allowAdminChanges - ) { - // Product type settings - $productTypeEditId = sprintf('edit-product-type-%s', mt_rand()); - $actions[] = [ - 'id' => $productTypeEditId, - 'icon' => 'gear', - 'label' => Craft::t('commerce', 'Product type settings'), - ]; - - $view = Craft::$app->getView(); - $view->registerJsWithVars(fn($id, $params) => << { - $('#' + $id).on('activate', function() { - const params = $params; - new Craft.CpScreenSlideout('commerce/product-types/edit-product-type', {params}); - }); -})(); -JS, [ - $view->namespaceInputId($productTypeEditId), - ['productTypeId' => $this->typeId], - ]); - } - - return $actions; - } - - /** - * @inheritdoc - */ - protected static function includeSetStatusAction(): bool - { - return true; - } - - /** - * @inheritdoc - */ - protected static function defineSortOptions(): array - { - return [ - 'title' => Craft::t('commerce', 'Title'), - [ - 'label' => Craft::t('commerce', 'Post Date'), - 'orderBy' => 'postDate', - 'defaultDir' => 'desc', - ], - [ - 'label' => Craft::t('commerce', 'Expiry Date'), - 'orderBy' => 'expiryDate', - 'defaultDir' => 'desc', - ], - 'promotable' => Craft::t('commerce', 'Promotable?'), - 'defaultPrice' => Craft::t('commerce', 'Price'), - 'defaultSku' => Craft::t('commerce', 'SKU'), - [ - 'label' => Craft::t('app', 'Date Created'), - 'orderBy' => 'elements.dateCreated', - 'attribute' => 'dateCreated', - 'defaultDir' => 'desc', - ], - [ - 'label' => Craft::t('app', 'Date Updated'), - 'orderBy' => 'elements.dateUpdated', - 'attribute' => 'dateUpdated', - 'defaultDir' => 'desc', - ], - [ - 'label' => Craft::t('app', 'ID'), - 'orderBy' => 'elements.id', - 'attribute' => 'id', - ], - ]; - } - - /** - * @inheritdoc - */ - protected static function defineTableAttributes(): array - { - return [ - 'title' => ['label' => Craft::t('commerce', 'Product')], - 'status' => ['label' => Craft::t('commerce', 'Status')], - 'id' => ['label' => Craft::t('commerce', 'ID')], - 'type' => ['label' => Craft::t('commerce', 'Type')], - 'slug' => ['label' => Craft::t('commerce', 'Slug')], - 'uri' => ['label' => Craft::t('commerce', 'URI')], - 'postDate' => ['label' => Craft::t('commerce', 'Post Date')], - 'expiryDate' => ['label' => Craft::t('commerce', 'Expiry Date')], - 'stock' => ['label' => Craft::t('commerce', 'Stock')], - 'link' => ['label' => Craft::t('commerce', 'Link'), 'icon' => 'world'], - 'dateCreated' => ['label' => Craft::t('commerce', 'Date Created')], - 'dateUpdated' => ['label' => Craft::t('commerce', 'Date Updated')], - 'defaultPrice' => ['label' => Craft::t('commerce', 'Price')], - 'defaultPromotionalPrice' => ['label' => Craft::t('commerce', 'Promotional Price')], - 'defaultSku' => ['label' => Craft::t('commerce', 'SKU')], - 'defaultWeight' => ['label' => Craft::t('commerce', 'Weight')], - 'defaultLength' => ['label' => Craft::t('commerce', 'Length')], - 'defaultWidth' => ['label' => Craft::t('commerce', 'Width')], - 'defaultHeight' => ['label' => Craft::t('commerce', 'Height')], - 'variants' => ['label' => Craft::t('commerce', 'Variants')], - ]; - } - - /** - * @inheritdoc - */ - protected static function defineDefaultTableAttributes(string $source): array - { - $attributes = []; - - if ($source == '*') { - $attributes[] = 'type'; - } - - $attributes[] = 'status'; - $attributes[] = 'postDate'; - $attributes[] = 'expiryDate'; - $attributes[] = 'defaultPrice'; - $attributes[] = 'defaultSku'; - $attributes[] = 'link'; - - return $attributes; - } - - /** - * @inheritdoc - */ - public static function attributePreviewHtml(array $attribute): mixed - { - return match ($attribute['value']) { - 'defaultSku' => $attribute['placeholder'], - default => parent::attributePreviewHtml($attribute) - }; - } - - /** - * @inheritdoc - */ - protected static function defineCardAttributes(): array - { - return array_merge(parent::defineCardAttributes(), [ - 'defaultPrice' => [ - 'label' => Craft::t('commerce', 'Price'), - 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), - ], - 'defaultPromotionalPrice' => [ - 'label' => Craft::t('commerce', 'Promotional Price'), - 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), - ], - 'defaultSku' => [ - 'label' => Craft::t('commerce', 'SKU'), - 'placeholder' => Html::tag('code', 'SKU123'), - ], - ]); - } - - /** - * @inheritdoc - */ - protected static function defineDefaultCardAttributes(): array - { - return array_merge(parent::defineDefaultCardAttributes(), [ - 'defaultSku', - 'defaultPrice', - ]); - } - - /** - * @inheritdoc - */ - public static function eagerLoadingMap(array $sourceElements, string $handle): array|null|false - { - if ($handle == 'variants') { - $sourceElementIds = ArrayHelper::getColumn($sourceElements, 'id'); - $map = (new Query()) - ->select('ownerId as source, elementId as target') - ->from(\craft\db\Table::ELEMENTS_OWNERS) - ->where(['ownerId' => $sourceElementIds]) - ->orderBy('sortOrder asc') - ->all(); - - return [ - 'elementType' => Variant::class, - 'map' => $map, - ]; - } - - return parent::eagerLoadingMap($sourceElements, $handle); - } - - /** - * @inheritdoc - * @since 3.0 - */ - public static function gqlTypeNameByContext(mixed $context): string - { - /** @var ProductType $context */ - return $context->handle . '_Product'; - } - - /** - * @inheritdoc - * @since 3.0 - */ - public static function gqlScopesByContext(mixed $context): array - { - /** @var ProductType $context */ - return ['productTypes.' . $context->uid]; - } - - /** - * @inheritdoc - */ - public static function prepElementQueryForTableAttribute(ElementQueryInterface $elementQuery, string $attribute): void - { - // Only eager load variants for attributes that actually need them. - // Other variant-related attributes (defaultPrice, defaultSku, etc.) are already - // fetched via SQL JOINs in ProductQuery::beforePrepare() - if (in_array($attribute, ['variants', 'stock'], true)) { - $elementQuery->andWith('variants'); - } else { - parent::prepElementQueryForTableAttribute($elementQuery, $attribute); - } - } - - /** - * @var DateTime|null Post date - */ - public ?DateTime $postDate = null; - - /** - * @var DateTime|null Expiry date - */ - public ?DateTime $expiryDate = null; - - /** - * @var int|null Product type ID - */ - public ?int $typeId = null; - - /** - * @var int|null defaultVariantId - */ - public ?int $defaultVariantId = null; - - /** - * @var string|null Default SKU - */ - public ?string $defaultSku = null; - - /** - * @var float|null Default price - * @see getDefaultPrice() - * @see setDefaultPrice() - */ - private ?float $_defaultPrice = null; - - /** - * @var float|null - * @since 5.1.0 - */ - public ?float $defaultBasePrice = null; - - /** - * @var float|null - * @since 5.4.0 - */ - public ?float $defaultBasePromotionalPrice = null; - - /** - * @var float|null Default height - */ - public ?float $defaultHeight = null; - - /** - * @var float|null Default length - */ - public ?float $defaultLength = null; - - /** - * @var float|null Default width - */ - public ?float $defaultWidth = null; - - /** - * @var float|null Default weight - */ - public ?float $defaultWeight = null; - - /** - * @var TaxCategory|null Tax category - */ - public ?TaxCategory $taxCategory = null; - - /** - * @var string|null Name - */ - public ?string $name = null; - - /** - * @var VariantCollection|null This product’s variants - */ - private ?VariantCollection $_variants = null; - - /** - * @var NestedElementManager|null - * @see getVariantManager() - * @since 5.0.0 - */ - private ?NestedElementManager $_variantManager = null; - - /** - * @inheritdoc - * @since 5.1.0 - */ - public function currencyAttributes(): array - { - return ['defaultPrice', 'defaultBasePrice', 'defaultBasePromotionalPrice']; - } - - /** - * @throws InvalidConfigException - */ - public function behaviors(): array - { - $behaviors = parent::behaviors(); - - $behaviors['typecast'] = [ - 'class' => AttributeTypecastBehavior::class, - 'attributeTypes' => [ - 'id' => AttributeTypecastBehavior::TYPE_INTEGER, - ], - ]; - - $behaviors['currencyAttributes'] = [ - 'class' => CurrencyAttributeBehavior::class, - 'currencyAttributes' => $this->currencyAttributes(), - ]; - - return $behaviors; - } - - /** - * @param float|null $defaultPrice - * @return void - * @since 5.0.11 - */ - public function setDefaultPrice(?float $defaultPrice): void - { - $this->_defaultPrice = $defaultPrice; - } - - /** - * @return float|null - * @throws InvalidConfigException - * @since 5.0.11 - */ - public function getDefaultPrice(): ?float - { - return $this->_defaultPrice ?? $this->getDefaultVariant()?->price; - } - - public function canCreateDrafts(User $user): bool - { - // Everyone with view permissions can create drafts - return true; - } - - /** - * @inheritdoc - */ - public function hasRevisions(): bool - { - return $this->getType()->enableVersioning; - } - - /** - * @inheritdoc - */ - public function getPostEditUrl(): ?string - { - return UrlHelper::cpUrl('commerce/products'); - } - - /** - * @inheritdoc - */ - protected function cpRevisionsUrl(): ?string - { - return sprintf('%s/revisions', $this->cpEditUrl()); - } - - /** - * @inheritdoc - */ - public function getIsTitleTranslatable(): bool - { - return ($this->getType()->productTitleTranslationMethod !== Field::TRANSLATION_METHOD_NONE); - } - - /** - * @inheritdoc - */ - public function getTitleTranslationDescription(): ?string - { - return ElementHelper::translationDescription($this->getType()->productTitleTranslationMethod); - } - - /** - * @inheritdoc - */ - public function getTitleTranslationKey(): string - { - $type = $this->getType(); - return ElementHelper::translationKey($this, $type->productTitleTranslationMethod, $type->productTitleTranslationKeyFormat); - } - - /** - * @inheritdoc - */ - public function getIsSlugTranslatable(): bool - { - return ($this->getType()->slugTranslationMethod !== Field::TRANSLATION_METHOD_NONE); - } - - /** - * @inheritdoc - */ - public function getSlugTranslationDescription(): ?string - { - return ElementHelper::translationDescription($this->getType()->slugTranslationMethod); - } - - /** - * @inheritdoc - */ - public function getSlugTranslationKey(): string - { - $type = $this->getType(); - return ElementHelper::translationKey($this, $type->slugTranslationMethod, $type->slugTranslationKeyFormat); - } - - /** - * @inheritdoc - */ - public function __toString(): string - { - return (string)$this->title; - } - - /** - * @inheritdoc - */ - public function canView(User $user): bool - { - if (parent::canView($user)) { - return true; - } - - try { - $productType = $this->getType(); - } catch (\Exception) { - return false; - } - - return $user->can('commerce-viewProductType:' . $productType->uid); - } - - /** - * @inheritdoc - */ - public function canSave(User $user): bool - { - if (parent::canSave($user)) { - return true; - } - - try { - $productType = $this->getType(); - } catch (\Exception) { - return false; - } - - if ($this->getIsDraft()) { - /** @var static|DraftBehavior $this */ - return $this->canCreateDrafts($user); - } - - // New products require create permission - if (!$this->id) { - return $user->can('commerce-createProductType:' . $productType->uid); - } - - return $user->can('commerce-saveProductType:' . $productType->uid); - } - - /** - * @inheritdoc - */ - public function canDuplicate(User $user): bool - { - if (parent::canDuplicate($user)) { - return true; - } - - try { - $productType = $this->getType(); - } catch (\Exception) { - return false; - } - - return $user->can('commerce-createProductType:' . $productType->uid) - && $user->can('commerce-saveProductType:' . $productType->uid); - } - - /** - * @inheritdoc - */ - public function canDelete(User $user): bool - { - if (parent::canDelete($user)) { - return true; - } - - try { - $productType = $this->getType(); - } catch (\Exception) { - return false; - } - - return $user->can('commerce-deleteProductType:' . $productType->uid); - } - - /** - * @inheritdoc - */ - public function canDeleteForSite(User $user): bool - { - return Craft::$app->getElements()->canDelete($this, $user); - } - - /** - * @inheritdoc - */ - public function createAnother(): ?ElementInterface - { - return null; - } - - /** - * @inheritdoc - */ - protected function crumbs(): array - { - $productType = $this->getType(); - - $productTypes = Collection::make(Plugin::getInstance()->getProductTypes()->getViewableProductTypes()); - /** @var Collection $productTypeOptions */ - $productTypeOptions = $productTypes - ->map(fn(ProductType $t) => [ - 'label' => Craft::t('site', $t->name), - 'url' => "commerce/products/$t->handle", - 'selected' => $t->id === $productType->id, - ]); - - return [ - [ - 'label' => Craft::t('commerce', 'Products'), - 'url' => 'commerce/products', - ], - [ - 'menu' => [ - 'label' => Craft::t('commerce', 'Select product type'), - 'items' => $productTypeOptions->all(), - ], - ], - ]; - } - - /** - * @inheritdoc - */ - protected function uiLabel(): ?string - { - // This method is called in a few places before the product type is set - // If there isn't a type then fall back to the title - if ($this->typeId) { - $uiLabelFormat = $this->getType()->productUiLabelFormat; - if ($uiLabelFormat !== '{title}') { - $uiLabel = Craft::$app->getView()->renderSandboxedObjectTemplate($uiLabelFormat, $this); - if ($uiLabel !== '') { - return $uiLabel; - } - } - } - - if (!isset($this->title) || trim($this->title) === '') { - return Craft::t('app', 'Untitled {type}', [ - 'type' => self::lowerDisplayName(), - ]); - } - - return null; - } - - /** - * Returns the product's product type. - * - * @throws InvalidConfigException - */ - public function getType(): ProductType - { - if ($this->typeId === null) { - throw new InvalidConfigException('Product is missing its product type ID'); - } - - $productType = Plugin::getInstance()->getProductTypes()->getProductTypeById($this->typeId); - - if ($productType === null) { - throw new InvalidConfigException('Invalid product type ID: ' . $this->typeId); - } - - return $productType; - } - - public function getName(): ?string - { - return $this->title; - } - - /** - * @inheritdoc - */ - protected function cacheTags(): array - { - return [ - "productType:$this->typeId", - ]; - } - - /** - * @inheritdoc - */ - public function getUriFormat(): ?string - { - $productTypeSiteSettings = $this->getType()->getSiteSettings(); - - if (!isset($productTypeSiteSettings[$this->siteId])) { - throw new InvalidConfigException('The "' . $this->getType()->name . '" product type is not enabled for the "' . $this->getSite()->name . '" site.'); - } - - return $productTypeSiteSettings[$this->siteId]->uriFormat; - } - - /** - * @inheritdoc - */ - protected function cpEditUrl(): ?string - { - $productType = $this->getType(); - - $path = sprintf('commerce/products/%s/%s', $productType->handle, $this->getCanonicalId()); - - // Ignore homepage/temp slugs - if ($this->slug && !str_starts_with($this->slug, '__')) { - $path .= sprintf('-%s', str_replace('/', '-', $this->slug)); - } - - return $path; - } - - /** - * Returns the default variant. - * - * @param bool $includeDisabled - * @return Variant|null - * @throws InvalidConfigException - */ - public function getDefaultVariant(bool $includeDisabled = false): ?Variant - { - $defaultVariant = $this->getVariants($includeDisabled)->firstWhere('id', $this->defaultVariantId); - - return $defaultVariant ?: $this->getVariants($includeDisabled)->first(); - } - - /** - * Return the cheapest variant. - * - * @throws InvalidConfigException - * @noinspection PhpUnused - */ - public function getCheapestVariant(bool $includeDisabled = false): ?Variant - { - return $this->getVariants($includeDisabled)->cheapest(); - } - - /** - * Returns a collection of the product's variants. - * - * @param bool|null $includeDisabled - * @return VariantCollection - * @throws InvalidConfigException - */ - public function getVariants(?bool $includeDisabled = null): VariantCollection - { - if ($this->_variants === null) { - if (!$this->id) { - return VariantCollection::make(); - } - - /** @var self|null $duplicatingProduct */ - $duplicatingProduct = $this->duplicateOf; - if ($duplicatingProduct) { - $query = self::createVariantQuery($duplicatingProduct)->status(null); - } else { - $query = self::createVariantQuery($this)->status(null); - } - - $variants = $query->collect(); - - // Don't memoize empty collections in favour of a new query next time - if ($variants->isEmpty()) { - return $variants; - } - - $this->_variants = $variants; - $this->_variants->map(function(Variant $v) { - if (!$this->id) { - return $v; - } - - if ($v->primaryOwnerId === $this->id) { - $v->setPrimaryOwner($this); - } - - if ($v->ownerId === $this->id) { - $v->setOwner($this); - } - - return $v; - }); - } - - // When reordering variants we need to make sure disabled variants are included when calculating sort order - // @TODO Remove this controller-based default in Commerce 6.0 when `getVariants()` is updated to return an element query instance - $includeDisabled ??= Craft::$app->controller instanceof NestedElementsController; - - return $this->_variants->filter(fn(Variant $variant) => $includeDisabled || ($variant->getStatus() === self::STATUS_ENABLED)); - } - - /** - * @inheritdoc - */ - public function getSupportedSites(): array - { - if (!isset($this->typeId)) { - throw new InvalidConfigException('Require `typeId` must be set on the product.'); - } - - $productType = $this->getType(); - /** @var Site[] $allSites */ - $allSites = ArrayHelper::index(Craft::$app->getSites()->getAllSites(true), 'id'); - $sites = []; - - // If the product type is leaving it up to products to decide which sites to be propagated to, - // figure out which sites the product is currently saved in - if ( - ($this->duplicateOf->id ?? $this->id) && - $productType->propagationMethod === PropagationMethod::Custom - ) { - if ($this->id) { - $currentSites = self::find() - ->status(null) - ->id($this->id) - ->site('*') - ->select('elements_sites.siteId') - ->drafts(null) - ->provisionalDrafts(null) - ->revisions($this->getIsRevision()) - ->column(); - } else { - $currentSites = []; - } - - // If this is being duplicated from another element (e.g. a draft), include any sites the source element is saved to as well - if (!empty($this->duplicateOf->id)) { - array_push($currentSites, ...self::find() - ->status(null) - ->id($this->duplicateOf->id) - ->site('*') - ->select('elements_sites.siteId') - ->drafts(null) - ->provisionalDrafts(null) - ->revisions($this->duplicateOf->getIsRevision()) - ->column() - ); - } - - $currentSites = array_flip($currentSites); - } - - foreach ($productType->getSiteSettings() as $siteSettings) { - switch ($productType->propagationMethod) { - case PropagationMethod::None: - $include = $siteSettings->siteId == $this->siteId; - $propagate = true; - break; - case PropagationMethod::SiteGroup: - $include = $allSites[$siteSettings->siteId]->groupId == $allSites[$this->siteId]->groupId; - $propagate = true; - break; - case PropagationMethod::Language: - $include = $allSites[$siteSettings->siteId]->language == $allSites[$this->siteId]->language; - $propagate = true; - break; - case PropagationMethod::Custom: - $include = true; - // Only actually propagate to this site if it's the current site, or the product has been assigned - // a status for this site, or the product already exists for this site - $propagate = ( - $siteSettings->siteId == $this->siteId || - $this->getEnabledForSite($siteSettings->siteId) !== null || - isset($currentSites[$siteSettings->siteId]) - ); - break; - default: - $include = $propagate = true; - break; - } - - if ($include) { - $sites[] = [ - 'siteId' => $siteSettings->siteId, - 'propagate' => $propagate, - 'enabledByDefault' => $siteSettings->enabledByDefault, - ]; - } - } - - return $sites; - } - - /** - * Sets the variants on the product. Accepts an array of variant data keyed by variant ID or the string 'new'. - * - * @param VariantCollection|VariantQuery|array $variants - */ - public function setVariants(VariantCollection|VariantQuery|array $variants): void - { - if ($variants instanceof VariantQuery) { - // just unset our existing records - $this->_variants = null; - return; - } - - // Make sure each variant has an owner set in case of mass assignment of product and variants - if (is_array($variants)) { - foreach ($variants as &$variant) { - if ($variant instanceof Variant) { - continue; - } - - if (is_array($variant) && !isset($variant['owner'])) { - $variant = ['owner' => $this] + $variant; - } - } - } - - $this->_variants = $variants instanceof VariantCollection ? $variants : VariantCollection::make($variants); - } - - /** - * Returns a nested element manager for the product’s variants. - * - * @return NestedElementManager - * @since 5.0.0 - */ - public function getVariantManager(): NestedElementManager - { - if (!isset($this->_variantManager)) { - $this->_variantManager = new NestedElementManager( - Variant::class, - // @phpstan-ignore argument.type (will always be a Product) - fn(ElementInterface $product): VariantQuery => self::createVariantQuery($product), - [ - 'attribute' => 'variants', // dont change this: https://github.com/craftcms/commerce/issues/4314#issuecomment-4715539955 - 'propagationMethod' => $this->getType()->propagationMethod, - 'valueGetter' => fn() => $this->getVariants(true), - 'valueSetter' => fn($variants) => $this->setVariants($variants), - ], - ); - } - - return $this->_variantManager; - } - - /** - * @inheritdoc - */ - public function getStatus(): ?string - { - $status = parent::getStatus(); - - if ($status == self::STATUS_ENABLED && $this->postDate) { - $currentTime = DateTimeHelper::currentTimeStamp(); - $postDate = $this->postDate->getTimestamp(); - $expiryDate = $this->expiryDate?->getTimestamp(); - - if ($postDate <= $currentTime && ($expiryDate === null || $expiryDate > $currentTime)) { - return self::STATUS_LIVE; - } - - if ($postDate > $currentTime) { - return self::STATUS_PENDING; - } - - return self::STATUS_EXPIRED; - } - - return $status; - } - - /** - * @throws InvalidConfigException - * @noinspection PhpUnused - */ - public function getTotalStock(bool $includeDisabled = false): int - { - $stock = 0; - foreach ($this->getVariants($includeDisabled) as $variant) { - $stock += $variant->getStock(); - } - - return $stock; - } - - /** - * Returns whether at least one variant has unlimited stock. - * - * @throws InvalidConfigException - * @deprecated in 5.0.0 and will be removed in 6.0.0. Check each variant instead. - */ - public function getHasUnlimitedStock(bool $includeDisabled = false): bool - { - foreach ($this->getVariants($includeDisabled) as $variant) { - if (!$variant->inventoryTracked) { - return true; - } - } - - return false; - } - - /** - * @inheritdoc - * @since 3.0 - */ - public function getGqlTypeName(): string - { - return static::gqlTypeNameByContext($this->getType()); - } - - /** - * @inheritdoc - */ - public function setEagerLoadedElements(string $handle, array $elements, EagerLoadPlan $plan): void - { - if ($handle == 'variants') { - /** @var Variant[] $elements */ - $this->setVariants($elements); - } else { - parent::setEagerLoadedElements($handle, $elements, $plan); - } - } - - /** - * @inheritdoc - */ - protected function metaFieldsHtml(bool $static): string - { - $fields = []; - $view = Craft::$app->getView(); - $productType = $this->getType(); - // Slug - if ($productType->showSlugField) { - $fields[] = $this->slugFieldHtml($static); - } - - if ($productType->isStructure && $productType->maxLevels !== 1) { - $fields[] = (function() use ($static, $productType) { - if ($parentId = $this->getParentId()) { - $parent = Plugin::getInstance()->getProducts()->getProductById($parentId, $this->siteId, [ - 'drafts' => null, - 'draftOf' => false, - ]); - } else { - // If the entry already has structure data, use it. Otherwise, use its canonical entry - /** @var self|null $parent */ - $parent = self::find() - ->siteId($this->siteId) - ->ancestorOf($this->lft ? $this : ($this->getIsCanonical() ? $this->id : $this->getCanonical(true))) - ->ancestorDist(1) - ->drafts(null) - ->draftOf(false) - ->status(null) - ->one(); - } - - return Cp::elementSelectFieldHtml([ - 'label' => Craft::t('app', 'Parent'), - 'id' => 'parentId', - 'name' => 'parentId', - 'elementType' => self::class, - 'selectionLabel' => Craft::t('app', 'Choose'), - 'sources' => ["productType:$productType->uid"], - 'criteria' => $this->_parentOptionCriteria($productType), - 'limit' => 1, - 'elements' => $parent ? [$parent] : [], - 'disabled' => $static, - 'describedBy' => 'parentId-label', - 'errors' => $this->getErrors('parentId'), - ]); - })(); - } - - $isDeltaRegistrationActive = $view->getIsDeltaRegistrationActive(); - $view->setIsDeltaRegistrationActive(true); - $view->registerDeltaName('postDate'); - $view->registerDeltaName('expiryDate'); - $view->setIsDeltaRegistrationActive($isDeltaRegistrationActive); - - // Post Date - $fields[] = Cp::dateTimeFieldHtml([ - 'status' => $this->getAttributeStatus('postDate'), - 'label' => Craft::t('app', 'Post Date'), - 'id' => 'postDate', - 'name' => 'postDate', - 'value' => $this->_userPostDate(), - 'errors' => $this->getErrors('postDate'), - 'disabled' => $static, - ]); - - // Expiry Date - $fields[] = Cp::dateTimeFieldHtml([ - 'status' => $this->getAttributeStatus('expiryDate'), - 'label' => Craft::t('app', 'Expiry Date'), - 'id' => 'expiryDate', - 'name' => 'expiryDate', - 'value' => $this->expiryDate, - 'errors' => $this->getErrors('expiryDate'), - 'disabled' => $static, - ]); - - $fields[] = parent::metaFieldsHtml($static); - - return implode("\n", $fields); - } - - private function _parentOptionCriteria(ProductType $productType): array - { - $parentOptionCriteria = [ - 'siteId' => $this->siteId, - 'typeId' => $productType->id, - 'status' => null, - 'drafts' => null, - 'draftOf' => false, - ]; - - // Prevent the current entry, or any of its descendants, from being selected as a parent - if ($this->id) { - $excludeIds = self::find() - ->descendantOf($this) - ->drafts(null) - ->draftOf(false) - ->status(null) - ->ids(); - $excludeIds[] = $this->getCanonicalId(); - $parentOptionCriteria['id'] = array_merge(['not'], $excludeIds); - } - - if ($productType->maxLevels) { - if ($this->id) { - // Figure out how deep the ancestors go - $maxDepth = self::find() - ->select('level') - ->descendantOf($this) - ->status(null) - ->leaves() - ->scalar(); - $depth = 1 + ($maxDepth ?: $this->level) - $this->level; - } else { - $depth = 1; - } - - $parentOptionCriteria['level'] = sprintf('<=%s', $productType->maxLevels - $depth); - } - - // Fire a 'defineParentSelectionCriteria' event - if ($this->hasEventHandlers(self::EVENT_DEFINE_PARENT_SELECTION_CRITERIA)) { - $event = new ElementCriteriaEvent(['criteria' => $parentOptionCriteria]); - $this->trigger(self::EVENT_DEFINE_PARENT_SELECTION_CRITERIA, $event); - return $event->criteria; - } - - return $parentOptionCriteria; - } - - /** - * Returns the Post Date value that should be shown on the edit form. - * - * @return DateTime|null - */ - private function _userPostDate(): ?DateTime - { - if (!$this->postDate || ($this->getIsUnpublishedDraft() && $this->postDate == $this->dateCreated)) { - // Pretend the post date hasn't been set yet, even if it has - return null; - } - - return $this->postDate; - } - - /** - * @inheritdoc - */ - public function getMetadata(): array - { - $metadata = parent::getMetadata(); - - if (array_key_exists(Craft::t('app', 'Status'), $metadata)) { - unset($metadata[Craft::t('app', 'Status')]); - } - - return $metadata; - } - - /** - * @inheritDoc - */ - protected function searchKeywords(string $attribute): string - { - if ($attribute === 'sku') { - return $this->getVariants() - ->pluck('sku') - ->filter(fn(?string $sku) => $sku && !PurchasableHelper::isTempSku($sku)) - ->implode(' '); - } - - return parent::searchKeywords($attribute); - } - - /** - * @inheritdoc - */ - public function afterSave(bool $isNew): void - { - if (!$this->propagating) { - $productType = $this->getType(); - - if (!$isNew) { - $record = ProductRecord::findOne($this->id); - - if (!$record) { - throw new Exception('Invalid product ID: ' . $this->id); - } - } else { - $record = new ProductRecord(); - $record->id = $this->id; - } - - $record->postDate = $this->postDate; - $record->expiryDate = $this->expiryDate; - $record->typeId = $this->typeId; - - $defaultVariant = $this->getDefaultVariant(); - $record->defaultVariantId = $defaultVariant->id ?? null; - $record->defaultSku = $defaultVariant?->getSkuAsText() ?? ''; - $record->defaultPrice = $defaultVariant?->getBasePrice() ?? 0.0; - $record->defaultHeight = $defaultVariant->height ?? 0.0; - $record->defaultLength = $defaultVariant->length ?? 0.0; - $record->defaultWidth = $defaultVariant->width ?? 0.0; - $record->defaultWeight = $defaultVariant->weight ?? 0.0; - - // Make sure to update the object - $this->defaultVariantId = $defaultVariant->id ?? null; - $this->defaultSku = $defaultVariant?->getSkuAsText(); - $this->defaultPrice = $defaultVariant?->getBasePrice() ?? 0.0; - $this->defaultHeight = $defaultVariant->height ?? 0; - $this->defaultLength = $defaultVariant->length ?? 0; - $this->defaultWidth = $defaultVariant->width ?? 0; - $this->defaultWeight = $defaultVariant->weight ?? 0; - - // We want to always have the same date as the element table, based on the logic for updating these in the element service i.e resaving - $record->dateUpdated = $this->dateUpdated; - $record->dateCreated = $this->dateCreated; - - // Capture the dirty attributes from the record - $dirtyAttributes = array_keys($record->getDirtyAttributes()); - $record->save(false); - - $this->id = $record->id; - - $this->setDirtyAttributes($dirtyAttributes); - - if ($this->getIsCanonical() && - isset($this->typeId) && - $productType->isStructure - ) { - // Has the parent changed? - if ($this->hasNewParent()) { - $this->_placeInStructure($isNew, $productType); - } - - // Update the product's descendants, who may be using this product's URI in their own URIs - if (!$isNew) { - Craft::$app->getElements()->updateDescendantSlugsAndUris($this, true, true); - } - } - - // Queue job to resave variants if the variant title format references the product - if ($this->getIsCanonical() && - isset($this->typeId) && - !$productType->hasVariantTitleField && - $productType->variantTitleFormat && - StringHelper::containsAny($productType->variantTitleFormat, ['product.', 'owner.', 'primaryOwner.']) - ) { - Craft::$app->getQueue()->push(new \craft\commerce\queue\jobs\ResaveProductVariants([ - 'productId' => $this->id, - ])); - } - } - - parent::afterSave($isNew); - } - - private function _placeInStructure(bool $isNew, ProductType $productType): void - { - $parentId = $this->getParentId(); - $structuresService = Craft::$app->getStructures(); - - // If this is a provisional draft and its new parent matches the canonical product’s, just drop it from the structure - if ($this->isProvisionalDraft) { - $canonicalParentId = self::find() - ->select(['elements.id']) - ->ancestorOf($this->getCanonicalId()) - ->ancestorDist(1) - ->status(null) - ->scalar(); - - if ($parentId == $canonicalParentId) { - $structuresService->remove($this->structureId, $this); - return; - } - } - - $mode = $isNew ? Structures::MODE_INSERT : Structures::MODE_AUTO; - - if (!$parentId) { - if ($productType->defaultPlacement === ProductType::DEFAULT_PLACEMENT_BEGINNING) { - $structuresService->prependToRoot($this->structureId, $this, $mode); - } else { - $structuresService->appendToRoot($this->structureId, $this, $mode); - } - } else { - if ($productType->defaultPlacement === ProductType::DEFAULT_PLACEMENT_BEGINNING) { - $structuresService->prepend($this->structureId, $this, $this->getParent(), $mode); - } else { - $structuresService->append($this->structureId, $this, $this->getParent(), $mode); - } - } - } - - /** - * Updates the entry's title, if its entry type has a dynamic title format. - * - * @since 3.0.3 - * @see \craft\elements\Entry::updateTitle - */ - public function updateTitle(): void - { - $productType = $this->getType(); - - // check for null just incase the value comes back as 1, 0, true or false - if (!$productType->hasProductTitleField && $productType->hasProductTitleField !== null) { - // Make sure that the locale has been loaded in case the title format has any Date/Time fields - Craft::$app->getLocale(); - // Set Craft to the entry's site's language, in case the title format has any static translations - $language = Craft::$app->language; - Craft::$app->language = $this->getSite()->language; - $title = Craft::$app->getView()->renderSandboxedObjectTemplate($productType->productTitleFormat, $this); - if ($title !== '') { - $this->title = $title; - } - Craft::$app->language = $language; - } - } - - /** - * @inheritdoc - */ - public function beforeValidate(): bool - { - // We need to generate all variant sku formats before validating the product, - // since the product validates the uniqueness of all variants in memory. - $type = $this->getType(); - foreach ($this->getVariants(true) as $variant) { - if (!$variant->sku && $type->skuFormat) { - try { - $variant->sku = Craft::$app->getView()->renderSandboxedObjectTemplate($type->skuFormat, $variant); - } catch (\Exception $e) { - Craft::error('Craft Commerce could not generate the supplied SKU format: ' . $e->getMessage(), __METHOD__); - $variant->sku = ''; - } - - if ($variant->sku) { - $skuExistsQuery = function(string $sku, ?int $id) { - $query = (new Query()) - ->select(['sku']) - ->from(Table::PURCHASABLES) - ->where(['sku' => $sku]); - - // Make sure it isn't for the purchasable we are currently saving - if ($id) { - $query->andWhere(['not', ['id' => $id]]); - } - - return $query; - }; - - // Ensure there isn't a clash with an existing SKU when using auto formats - if ($skuExistsQuery($variant->sku, $variant->id)->exists()) { - // If there is a clash, we need to append a number to the end. - $baseSku = $variant->sku; - do { - $seq = Sequence::next('sku::' . $baseSku); - $newSku = $baseSku . '-' . $seq; - } while ($skuExistsQuery($newSku, $variant->id)->exists()); - - $variant->sku = $newSku; - } - } - } - } - - return parent::beforeValidate(); - } - - /** - * @inheritdoc - */ - public function beforeDelete(): bool - { - if (!parent::beforeDelete()) { - return false; - } - - $this->getVariantManager()->deleteNestedElements($this, $this->hardDelete); - - return true; - } - - /** - * @inheritDoc - */ - public function afterRestore(): void - { - $this->getVariantManager()->restoreNestedElements($this); - - parent::afterRestore(); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return array_merge(parent::defineRules(), [ - [['typeId'], 'number', 'integerOnly' => true], - [['postDate', 'expiryDate'], DateTimeValidator::class], - [['defaultPrice'], 'safe'], - [ - ['variants'], - function() { - if ($this->getVariants(true)->isEmpty()) { - $this->addError('variants', Craft::t('commerce', 'Must have at least one variant.')); - } - }, - 'skipOnEmpty' => false, - 'on' => self::SCENARIO_LIVE, - ], - [ - ['variants'], - function() { - $skus = []; - foreach ($this->getVariants(true) as $variant) { - if (isset($skus[$variant->sku])) { - $this->addError('variants', Craft::t('commerce', 'Not all SKUs are unique.')); - break; - } - $skus[$variant->sku] = true; - } - }, - 'on' => self::SCENARIO_LIVE, - ], - [ - ['variants'], - function() { - foreach ($this->getVariants(true) as $variant) { - if (!$variant->sku || PurchasableHelper::isTempSku($variant->sku)) { - $this->addError('variants', Craft::t('commerce', 'All variants must have a SKU.')); - break; - } - } - }, - 'on' => self::SCENARIO_LIVE, - ], - [ - ['variants'], - function() { - if ($this->getType()->maxVariants) { - $variantCount = count($this->getVariants(true)); - if ($variantCount > $this->getType()->maxVariants) { - $this->addError('variants', Craft::t('commerce', 'Too many variants for this product.')); - } - } - }, - ], - ]); - } - - /** - * @inheritdoc - */ - public function setAttributesFromRequest(array $values): void - { - // this is needed for Craft.NestedElementManager::markAsDirty() - if (isset($values['variants']) && $values['variants'] === '*') { - $this->setDirtyAttributes(['variants']); - unset($values['variants']); - } - - parent::setAttributesFromRequest($values); - } - - /** - * @inheritdoc - */ - public function getFieldLayout(): ?FieldLayout - { - try { - return $this->getType()->getProductFieldLayout(); - } catch (InvalidConfigException) { - // The product type was probably deleted - return null; - } - } - - /** - * @inheritdoc - */ - public function beforeSave(bool $isNew): bool - { - // Make sure the entry has at least one revision if the section has versioning enabled - if ($this->_shouldSaveRevision()) { - $hasRevisions = self::find() - ->revisionOf($this) - ->site('*') - ->status(null) - ->exists(); - if (!$hasRevisions) { - /** @var self|null $currentProduct */ - $currentProduct = self::find() - ->id($this->id) - ->site('*') - ->status(null) - ->one(); - - // May be null if the product is currently stored as an unpublished draft - if ($currentProduct) { - $revisionNotes = 'Revision from ' . Craft::$app->getFormatter()->asDatetime($currentProduct->dateUpdated); - Craft::$app->getRevisions()->createRevision($currentProduct, notes: $revisionNotes); - } - } - } - - $productType = $this->getType(); - // Set the structure ID for Element::attributes() and afterSave() - if ($productType->isStructure) { - $this->structureId = $productType->structureId; - - // Has the entry been assigned to a new parent? - if (!$this->duplicateOf && $this->hasNewParent()) { - if ($parentId = $this->getParentId()) { - $parentProduct = Plugin::getInstance()->getProducts()->getProductById($parentId, '*', [ - 'preferSites' => [$this->siteId], - 'drafts' => null, - 'draftOf' => false, - ]); - - if (!$parentProduct) { - throw new InvalidConfigException("Invalid parent ID: $parentId"); - } - } else { - $parentProduct = null; - } - - $this->setParent($parentProduct); - } - } - - // Make sure the field layout is set correctly - $this->fieldLayoutId = $this->getType()->fieldLayoutId; - - if ($this->enabled && !$this->postDate) { - // Default the post date to the current date/time - $this->postDate = new DateTime(); - // ...without the seconds - $this->postDate->setTimestamp($this->postDate->getTimestamp() - ($this->postDate->getTimestamp() % 60)); - } - - $this->updateTitle(); - - return parent::beforeSave($isNew); - } - - /** - * @inheritdoc - */ - protected static function defineSearchableAttributes(): array - { - return [ - 'defaultSku', - 'sku', - ]; - } - - /** - * @param Product $product - * @return VariantQuery - */ - private static function createVariantQuery(Product $product): VariantQuery - { - $query = Variant::find() - ->productId($product->id) - ->siteId($product->siteId) - ->orderBy(['sortOrder' => SORT_ASC]); - - if ($product->getIsRevision()) { - $query->revisions(null)->trashed(null); - } - - return $query; - } - - /** - * @inheritdoc - */ - protected function route(): array|string|null - { - // Make sure that the product is actually live - if (!$this->previewing && $this->getStatus() != self::STATUS_LIVE) { - return null; - } - - // Make sure the product type is set to have URLs for this site - $siteId = Craft::$app->getSites()->currentSite->id; - $productTypeSiteSettings = $this->getType()->getSiteSettings(); - - if (!isset($productTypeSiteSettings[$siteId]) || !$productTypeSiteSettings[$siteId]->hasUrls) { - return null; - } - - return [ - 'templates/render', [ - 'template' => $productTypeSiteSettings[$siteId]->template, - 'variables' => [ - 'product' => $this, - ], - ], - ]; - } - - /** - * @inheritdoc - */ - protected function previewTargets(): array - { - return array_map(function($previewTarget) { - $previewTarget['label'] = Craft::t('site', $previewTarget['label']); - return $previewTarget; - }, $this->getType()->previewTargets ?? []); - } - - /** - * @inheritdoc - */ - protected function attributeHtml(string $attribute): string - { - $productType = $this->getType(); - - switch ($attribute) { - case 'type': - { - return Craft::t('site', Html::encode($productType->name)); - } - case 'defaultSku': - { - if ($this->defaultSku === null) { - return ''; - } - - return Html::tag('code', PurchasableHelper::isTempSku($this->defaultSku) ? '' : Html::encode($this->defaultSku)); - } - case 'defaultPrice': - { - return $this->defaultBasePrice ? $this->defaultBasePriceAsCurrency : ''; - } - case 'defaultPromotionalPrice': - { - return $this->defaultBasePromotionalPrice ? $this->defaultBasePromotionalPriceAsCurrency : ''; - } - case 'stock': - { - $stock = 0; - $hasUnlimited = false; - - foreach ($this->getVariants(true) as $variant) { - $stock += $variant->getStock(); - if (!$variant->inventoryTracked) { - $hasUnlimited = true; - } - } - return $hasUnlimited ? '∞' . ($stock ? ' & ' . $stock : '') : ($stock ?: '0'); - } - case 'defaultWeight': - { - if ($productType->hasDimensions) { - return Craft::$app->getFormattingLocale()->getFormatter()->asDecimal($this->$attribute) . ' ' . Plugin::getInstance()->getSettings()->weightUnits; - } - - return ''; - } - case 'defaultLength': - case 'defaultWidth': - case 'defaultHeight': - { - if ($productType->hasDimensions) { - return Craft::$app->getFormattingLocale()->getFormatter()->asDecimal($this->$attribute) . ' ' . Plugin::getInstance()->getSettings()->dimensionUnits; - } - - return ''; - } - case 'variants': - { - $value = $this->getVariants(true); - /** @var Variant|null $first */ - $first = $value->first(); - $html = $first ? Cp::elementChipHtml($first) : ''; - - if ($value->isNotEmpty() && $value->count() > 1) { - $otherItems = $value->filter(fn($v, $k) => $k > 0); - $otherHtml = $otherItems->map(fn($v) => Cp::elementChipHtml($v))->join(''); - - $html .= Html::tag('span', '+' . Craft::$app->getFormatter()->asInteger($otherItems->count()), [ - 'title' => $otherItems->map(fn($v) => $v->title)->join(', '), - 'class' => 'btn small', - 'role' => 'button', - 'onclick' => 'jQuery(this).replaceWith(' . Json::encode($otherHtml) . ')', - ]); - } - - return $html; - } - default: - { - return parent::attributeHtml($attribute); - } - } - } - - /** - * @inheritDoc - */ - public function setScenario($value): void - { - foreach ($this->getVariants() as $variant) { - $variant->setScenario($value); - } - - parent::setScenario($value); - } - - /** - * @inheritDoc - */ - public function afterPropagate(bool $isNew): void - { - $this->getVariantManager()->maintainNestedElements($this, $isNew); - parent::afterPropagate($isNew); - - // @TODO Collate purchasable IDs updated across the request and queue a single catalog pricing job, rather than one per product propagate - if (!$this->getIsDraft()) { - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => $this->getVariants()->pluck('id')->all(), - 'storeId' => $this->storeId, - ]); - } - - // Save a new revision? - if ($this->_shouldSaveRevision()) { - Craft::$app->getRevisions()->createRevision($this, notes: $this->revisionNotes); - } - } - - /** - * Returns whether the product should be saving revisions on save. - * - * @return bool - */ - private function _shouldSaveRevision(): bool - { - return ( - $this->id && - !$this->propagating && - !$this->resaving && - !$this->getIsDraft() && - !$this->getIsRevision() && - $this->getType()->enableVersioning - ); - } -} diff --git a/src/elements/Subscription.php b/src/elements/Subscription.php deleted file mode 100644 index 77807d34a5..0000000000 --- a/src/elements/Subscription.php +++ /dev/null @@ -1,751 +0,0 @@ - - * @copyright Copyright (c) 2015, Pixel & Tonic, Inc. - * @since 2.0 - */ -class Subscription extends Element -{ - /** - * @var string - */ - public const STATUS_ACTIVE = 'active'; - - /** - * @var string - */ - public const STATUS_EXPIRED = 'expired'; - - /** - * @var string - */ - public const STATUS_SUSPENDED = 'suspended'; - - /** - * @var int|null User id - */ - public ?int $userId = null; - - /** - * @var int|null Plan id - */ - public ?int $planId = null; - - /** - * @var int|null Gateway id - */ - public ?int $gatewayId = null; - - /** - * @var int|null Order id - */ - public ?int $orderId = null; - - /** - * @var string Subscription reference on the gateway - */ - public string $reference = ''; - - /** - * @var int Trial days granted - */ - public int $trialDays = 0; - - /** - * @var DateTime|null Date of next payment - */ - public ?DateTime $nextPaymentDate = null; - - /** - * @var bool Whether the subscription is canceled - */ - public bool $isCanceled = false; - - /** - * @var DateTime|null Time when subscription was canceled - */ - public ?DateTime $dateCanceled = null; - - /** - * @var bool Whether the subscription has expired - */ - public bool $isExpired = false; - - /** - * @var DateTime|null Time when subscription expired - */ - public ?DateTime $dateExpired = null; - - /** - * @var bool Whether the subscription has started - */ - public bool $hasStarted = false; - - /** - * @var bool Whether the subscription is on hold due to payment issues - */ - public bool $isSuspended = false; - - /** - * @var DateTime|null Time when subscription was put on hold - */ - public ?DateTime $dateSuspended = null; - - /** - * @var string|null The URL to return to after a subscription is created - */ - public ?string $returnUrl = null; - - /** - * @var SubscriptionGatewayInterface|null - */ - private ?SubscriptionGatewayInterface $_gateway = null; - - /** - * @var Plan|null - */ - private ?Plan $_plan = null; - - /** - * @var User|null - */ - private ?User $_user = null; - - /** - * @var Order|null - */ - private ?Order $_order = null; - - /** - * @var array|null The subscription data from gateway - */ - public ?array $_subscriptionData = null; - - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Subscription'); - } - - /** - * @inheritdoc - */ - public static function lowerDisplayName(): string - { - return Craft::t('commerce', 'subscription'); - } - - /** - * @inheritdoc - */ - public static function pluralDisplayName(): string - { - return Craft::t('commerce', 'Subscriptions'); - } - - /** - * @inheritdoc - */ - public static function pluralLowerDisplayName(): string - { - return Craft::t('commerce', 'subscriptions'); - } - - /** - * @return string - */ - public function __toString(): string - { - $plan = $this->getPlan(); - return Craft::t('commerce', 'Subscription to “{plan}”', ['plan' => $plan->name ?? '']); - } - - public function canView(User $user): bool - { - return parent::canView($user) || $user->can('commerce-manageSubscriptions'); - } - - public function canSave(User $user): bool - { - return parent::canView($user) || $user->can('commerce-manageSubscriptions'); - } - - /** - * Returns whether this subscription can be reactivated. - * - * @throws InvalidConfigException if gateway misconfigured - */ - public function canReactivate(): bool - { - return $this->isCanceled && !$this->isExpired && $this->getGateway()->supportsReactivation(); - } - - /** - * @inheritdoc - */ - public function getFieldLayout(): ?FieldLayout - { - return Craft::$app->getFields()->getLayoutByType(static::class); - } - - /** - * Returns whether this subscription is on trial. - * - * @throws Exception - */ - public function getIsOnTrial(): bool - { - if ($this->isExpired) { - return false; - } - - return $this->trialDays > 0 && time() <= $this->getTrialExpires()->getTimestamp(); - } - - /** - * Returns the subscription plan for this subscription - */ - public function getPlan(): ?Plan - { - if (!isset($this->_plan) && $this->planId) { - $this->_plan = Plugin::getInstance()->getPlans()->getPlanById($this->planId); - } - - return $this->_plan; - } - - /** - * Returns the User that is subscribed. - */ - public function getSubscriber(): ?User - { - if (!isset($this->_user) && $this->userId) { - // Include trashed users so soft-deleted subscribers still resolve. - $this->_user = Craft::$app->getElements()->getElementById($this->userId, User::class, criteria: ['trashed' => null]); - } - - return $this->_user; - } - - public function getSubscriptionData(): array - { - return $this->_subscriptionData ?? []; - } - - public function setSubscriptionData(array|string $data): void - { - $data = Json::decodeIfJson($data); - - $this->_subscriptionData = $data; - } - - /** - * Returns the datetime of trial expiry. - * - * @throws Exception - */ - public function getTrialExpires(): ?DateTIme - { - $created = clone $this->dateCreated; - return $created->add(new DateInterval('P' . $this->trialDays . 'D')); - } - - /** - * Returns the next payment amount with currency code as a string. - * - * @throws InvalidConfigException - */ - public function getNextPaymentAmount(): string - { - return $this->getGateway()->getNextPaymentAmount($this); - } - - /** - * Returns the order that included this subscription, if any. - */ - public function getOrder(): ?Order - { - if ($this->_order) { - return $this->_order; - } - - if ($this->orderId) { - return $this->_order = Plugin::getInstance()->getOrders()->getOrderById($this->orderId); - } - - return null; - } - - /** - * Returns the product type for the product tied to the license. - * - * @throws InvalidConfigException if gateway misconfigured - */ - public function getGateway(): ?SubscriptionGatewayInterface - { - if (!isset($this->_gateway) && $this->gatewayId) { - $gateway = Plugin::getInstance()->getGateways()->getGatewayById($this->gatewayId); - if (!$gateway instanceof SubscriptionGatewayInterface) { - throw new InvalidConfigException('The gateway set for subscription does not support subscriptions.'); - } - $this->_gateway = $gateway; - } - - return $this->_gateway; - } - - public function getPlanName(): string - { - return $this->getPlan()?->__toString() ?? ''; - } - - /** - * Returns possible alternative plans for this subscription - * - * @return Plan[] - */ - public function getAlternativePlans(): array - { - if ($this->gatewayId === null) { - return []; - } - - $plans = Plugin::getInstance()->getPlans()->getPlansByGatewayId($this->gatewayId); - - $currentPlan = $this->getPlan(); - - $alternativePlans = []; - - foreach ($plans as $plan) { - // For all plans that are not the current plan - if ($currentPlan && $plan->id !== $currentPlan->id && $plan->canSwitchFrom($currentPlan)) { - $alternativePlans[] = $plan; - } - } - - return $alternativePlans; - } - - /** - * @inheritdoc - */ - public function getCpEditUrl(): ?string - { - return UrlHelper::cpUrl('commerce/subscriptions/' . $this->id); - } - - /** - * Returns the link for editing the order that purchased this license. - */ - public function getOrderEditUrl(): string - { - if ($this->orderId) { - return UrlHelper::cpUrl('commerce/orders/' . $this->orderId); - } - - return ''; - } - - /** - * Returns an array of all payments for this subscription. - * - * @return SubscriptionPayment[] - * @throws InvalidConfigException - */ - public function getAllPayments(): array - { - return $this->getGateway()->getSubscriptionPayments($this); - } - - public function getName(): ?string - { - return Craft::t('commerce', 'Subscription to “{plan}”', ['plan' => $this->getPlanName()]); - } - - /** - * @inheritdoc - */ - public static function hasStatuses(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function getStatus(): ?string - { - if ($this->isExpired) { - return self::STATUS_EXPIRED; - } - - return $this->isSuspended ? self::STATUS_SUSPENDED : self::STATUS_ACTIVE; - } - - - /** - * @inheritdoc - */ - public static function defineSources(string $context = null): array - { - $plans = Plugin::getInstance()->getPlans()->getAllPlans(); - - $planIds = []; - - foreach ($plans as $plan) { - $planIds[] = $plan->id; - } - - - $sources = [ - '*' => [ - 'key' => '*', - 'label' => Craft::t('commerce', 'All active subscriptions'), - 'criteria' => ['planId' => $planIds], - 'defaultSort' => ['dateCreated', 'desc'], - ], - ]; - - $sources[] = ['heading' => Craft::t('commerce', 'Subscription plans')]; - - foreach ($plans as $plan) { - $key = 'plan:' . $plan->id; - - $sources[$key] = [ - 'key' => $key, - 'label' => $plan->name, - 'data' => [ - 'handle' => $plan->handle, - ], - 'criteria' => ['planId' => $plan->id], - ]; - } - - $sources[] = ['heading' => Craft::t('commerce', 'Subscriptions on hold')]; - - $criteriaFailedToStart = ['isSuspended' => true, 'hasStarted' => false]; - $sources[] = [ - 'key' => 'carts:failed-to-start', - 'label' => Craft::t('commerce', 'Failed to start'), - 'criteria' => $criteriaFailedToStart, - 'defaultSort' => ['commerce_subscriptions.dateUpdated', 'desc'], - ]; - - $criteriaPaymentIssue = ['isSuspended' => true, 'hasStarted' => true]; - $sources[] = [ - 'key' => 'carts:payment-issue', - 'label' => Craft::t('commerce', 'Payment method issue'), - 'criteria' => $criteriaPaymentIssue, - 'defaultSort' => ['commerce_subscriptions.dateUpdated', 'desc'], - ]; - - return $sources; - } - - /** - * @inheritdoc - */ - public static function eagerLoadingMap(array $sourceElements, string $handle): array|null|false - { - $sourceElementIds = ArrayHelper::getColumn($sourceElements, 'id'); - - if ($handle === 'subscriber') { - $map = (new Query()) - ->select('id as source, userId as target') - ->from(Table::SUBSCRIPTIONS) - ->where(['in', 'id', $sourceElementIds]) - ->all(); - - return [ - 'elementType' => User::class, - 'map' => $map, - ]; - } - - return parent::eagerLoadingMap($sourceElements, $handle); - } - - /** - * @inheritdoc - */ - public function setEagerLoadedElements(string $handle, array $elements, EagerLoadPlan $plan): void - { - if ($handle === 'order') { - $order = $elements[0] ?? null; - $this->_order = $order instanceof Order ? $order : null; - - return; - } - - if ($handle === 'subscriber') { - $user = $elements[0] ?? null; - $this->_user = $user instanceof User ? $user : null; - - return; - } - - parent::setEagerLoadedElements($handle, $elements, $plan); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return array_merge(parent::defineRules(), [ - [['userId', 'planId', 'gatewayId', 'reference', 'subscriptionData'], 'required'], - ]); - } - - /** - * @inheritdocs - */ - public static function statuses(): array - { - return [ - self::STATUS_ACTIVE => Craft::t('commerce', 'Active'), - self::STATUS_EXPIRED => Craft::t('commerce', 'Expired'), - ]; - } - - /** - * @inheritdoc - * @return SubscriptionQuery The newly created [[SubscriptionQuery]] instance. - */ - public static function find(): SubscriptionQuery - { - return new SubscriptionQuery(static::class); - } - - /** - * @inheritdoc - */ - public function afterSave(bool $isNew): void - { - if (!$isNew) { - $subscriptionRecord = SubscriptionRecord::findOne($this->id); - - if (!$subscriptionRecord) { - throw new InvalidConfigException('Invalid subscription id: ' . $this->id); - } - } else { - $subscriptionRecord = new SubscriptionRecord(); - $subscriptionRecord->id = $this->id; - } - - $subscriptionRecord->planId = $this->planId; - $subscriptionRecord->nextPaymentDate = $this->nextPaymentDate; - $subscriptionRecord->subscriptionData = $this->subscriptionData; - $subscriptionRecord->isCanceled = $this->isCanceled; - $subscriptionRecord->dateCanceled = $this->dateCanceled; - $subscriptionRecord->isExpired = $this->isExpired; - $subscriptionRecord->dateExpired = $this->dateExpired; - $subscriptionRecord->hasStarted = $this->hasStarted; - $subscriptionRecord->isSuspended = $this->isSuspended; - $subscriptionRecord->dateSuspended = $this->dateSuspended; - $subscriptionRecord->returnUrl = $this->returnUrl; - - // We want to always have the same date as the element table, based on the logic for updating these in the element service i.e resaving - $subscriptionRecord->dateUpdated = $this->dateUpdated; - $subscriptionRecord->dateCreated = $this->dateCreated; - - // Some properties of the subscription are immutable - if ($isNew) { - $subscriptionRecord->gatewayId = $this->gatewayId; - $subscriptionRecord->orderId = $this->orderId; - $subscriptionRecord->reference = $this->reference; - $subscriptionRecord->trialDays = $this->trialDays; - $subscriptionRecord->userId = $this->userId; - } - - $subscriptionRecord->save(false); - - parent::afterSave($isNew); - } - - /** - * Return a description of the billing issue (if any) with this subscription. - * - * @throws InvalidConfigException if not a subscription gateway anymore - * @noinspection PhpUnused - */ - public function getBillingIssueDescription(): string - { - return $this->getGateway()->getBillingIssueDescription($this); - } - - /** - * Return the form HTML for resolving the billing issue (if any) with this subscription. - * - * @throws InvalidConfigException if not a subscription gateway anymore - * @noinspection PhpUnused - */ - public function getBillingIssueResolveFormHtml(): string - { - return $this->getGateway()->getBillingIssueResolveFormHtml($this); - } - - /** - * Return whether this subscription has billing issues. - * - * @throws InvalidConfigException if not a subscription gateway anymore - */ - public function getHasBillingIssues(): bool - { - return $this->getGateway()->getHasBillingIssues($this); - } - - /** - * @inheritdoc - */ - protected static function defineTableAttributes(): array - { - return [ - 'title' => ['label' => Craft::t('commerce', 'Subscription plan')], - 'subscriber' => ['label' => Craft::t('commerce', 'Subscribing user')], - 'reference' => ['label' => Craft::t('commerce', 'Subscription reference')], - 'dateCanceled' => ['label' => Craft::t('commerce', 'Cancellation date')], - 'dateCreated' => ['label' => Craft::t('commerce', 'Subscription date')], - 'dateExpired' => ['label' => Craft::t('commerce', 'Expiry date')], - 'trialExpires' => ['label' => Craft::t('commerce', 'Trial expiry date')], - ]; - } - - /** - * @inheritdoc - */ - protected static function defineDefaultTableAttributes(string $source): array - { - $attributes = []; - - $attributes[] = 'subscriber'; - $attributes[] = 'orderLink'; - $attributes[] = 'dateCreated'; - - return $attributes; - } - - /** - * @inheritdoc - */ - protected static function defineSearchableAttributes(): array - { - return [ - 'subscriber', - 'plan', - ]; - } - - /** - * @inheritdoc - */ - protected function attributeHtml(string $attribute): string - { - switch ($attribute) { - case 'plan': - return $this->getPlanName(); - - case 'subscriber': - $subscriber = $this->getSubscriber(); - if (!$subscriber) { - return ''; - } - $url = $subscriber->getCpEditUrl(); - - return '' . Html::encode($subscriber) . ''; - - case 'orderLink': - $url = $this->getOrderEditUrl(); - - return $url ? '' . Craft::t('commerce', 'View order') . '' : ''; - - default: - { - return parent::attributeHtml($attribute); - } - } - } - - /** - * @inheritdoc - */ - protected static function defineSortOptions(): array - { - return [ - [ - 'label' => Craft::t('commerce', 'Subscription date'), - 'orderBy' => 'commerce_subscriptions.dateCreated', - 'attribute' => 'dateCreated', - 'defaultDir' => 'desc', - ], - [ - 'label' => Craft::t('app', 'ID'), - 'orderBy' => 'elements.id', - 'attribute' => 'id', - ], - ]; - } - - - /** - * @inheritdoc - */ - protected static function prepElementQueryForTableAttribute(ElementQueryInterface $elementQuery, string $attribute): void - { - match ($attribute) { - 'subscriber' => $elementQuery->andWith('subscriber'), - 'orderLink' => $elementQuery->andWith('order'), - default => parent::prepElementQueryForTableAttribute($elementQuery, $attribute), - }; - } -} diff --git a/src/elements/Transfer.php b/src/elements/Transfer.php deleted file mode 100644 index ec288053d1..0000000000 --- a/src/elements/Transfer.php +++ /dev/null @@ -1,937 +0,0 @@ -getOriginLocation() === null && $this->getDestinationLocation() === null) { - return Craft::t('commerce', 'Transfer'); - } - - return (string)Craft::t('commerce', '{from} to {to}', [ - 'from' => $this->getOriginLocation()->getUiLabel(), - 'to' => $this->getDestinationLocation()->getUiLabel(), - ]); - } - - /** - * @inheritdoc - */ - public static function hasDrafts(): bool - { - return false; - } - - /** - * @inheritdoc - */ - protected function metadata(): array - { - $additionalMeta = []; - - $additionalMeta[] = [ - Craft::t('commerce', 'Transfer Status') => \craft\helpers\Cp::statusIndicatorHtml($this->getTransferStatus()->label(), [ - 'color' => $this->getTransferStatus()->color(), - ]) . ' ' . Html::tag('span', $this->getTransferStatus()->label()), - ]; - - if ($this->getIsDraft() && !$this->isProvisionalDraft) { - $additionalMeta[] = [ - Craft::t('app', 'Status') => function() { - $icon = Html::tag('span', '', [ - 'data' => ['icon' => 'draft'], - 'aria' => ['hidden' => 'true'], - ]); - $label = Craft::t('app', 'Draft'); - return $icon . Html::tag('span', $label); - }, - ]; - } - - $additionalMeta[] = [ - Craft::t('commerce', 'Transfer Status') => \craft\helpers\Cp::statusIndicatorHtml($this->getTransferStatus()->label(), [ - 'color' => $this->getTransferStatus()->color(), - ]) . ' ' . Html::tag('span', $this->getTransferStatus()->label()), - ]; - - return ArrayHelper::merge(parent::metadata(), ...$additionalMeta); // @TODO Verify metadata merge order is correct (leftover IDE-generated stub comment) - } - - - /** - * @return ?InventoryLocation - * @throws \yii\base\InvalidConfigException - */ - public function getOriginLocation(): ?InventoryLocation - { - if (!$this->originLocationId) { - return null; - } - - return Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($this->originLocationId); - } - - /** - * @return ?InventoryLocation - * @throws \yii\base\InvalidConfigException - */ - public function getDestinationLocation(): ?InventoryLocation - { - if (!$this->destinationLocationId) { - return null; - } - - return Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($this->destinationLocationId); - } - - /** - * @return TransferStatusType - */ - public function getTransferStatus(): TransferStatusType - { - return $this->transferStatus; - } - - /** - * Updates the status to partial or received if all items have been received. - * - * @return void - */ - public function updateTransferStatus(): void - { - // only pending can being partial or received. - if ($this->isTransferDraft()) { - return; - } else { - $this->setTransferStatus(TransferStatusType::PENDING); - } - - if ($this->isAllReceived()) { - $this->setTransferStatus(TransferStatusType::RECEIVED); - } - - if ($this->getTotalReceived() > 0 && $this->getTotalReceived() < $this->getTotalQuantity()) { - $this->setTransferStatus(TransferStatusType::PARTIAL); - } - } - - /** - * @param TransferStatusType|string $status - * @return void - */ - public function setTransferStatus(TransferStatusType|string $status): void - { - if (is_string($status)) { - $status = TransferStatusType::from($status); - } - - $this->transferStatus = $status; - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Transfer'); - } - - /** - * @inheritdoc - */ - public static function lowerDisplayName(): string - { - return Craft::t('commerce', 'transfer'); - } - - /** - * @inheritdoc - */ - public static function pluralDisplayName(): string - { - return Craft::t('commerce', 'Transfers'); - } - - /** - * @inheritdoc - */ - public static function pluralLowerDisplayName(): string - { - return Craft::t('commerce', 'transfers'); - } - - /** - * @inheritdoc - */ - public static function refHandle(): ?string - { - return 'transfer'; - } - - /** - * @inheritdoc - */ - public static function trackChanges(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public static function hasTitles(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public static function hasContent(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public static function hasUris(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public static function isLocalized(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public static function hasStatuses(): bool - { - return false; - } - - /** - * @return TransferQuery - * @inheritdoc - */ - public static function find(): ElementQueryInterface - { - return Craft::createObject(TransferQuery::class, [static::class]); - } - - /** - * @inheritdoc - */ - public static function createCondition(): ElementConditionInterface - { - return Craft::createObject(TransferCondition::class, [static::class]); - } - - /** - * @inheritdoc - */ - protected static function includeSetStatusAction(): bool - { - return false; - } - - protected static function defineSortOptions(): array - { - return [ - 'title' => Craft::t('app', 'Title'), - 'slug' => Craft::t('app', 'Slug'), - 'uri' => Craft::t('app', 'URI'), - [ - 'label' => Craft::t('app', 'Date Created'), - 'orderBy' => 'elements.dateCreated', - 'attribute' => 'dateCreated', - 'defaultDir' => 'desc', - ], - [ - 'label' => Craft::t('app', 'Date Updated'), - 'orderBy' => 'elements.dateUpdated', - 'attribute' => 'dateUpdated', - 'defaultDir' => 'desc', - ], - [ - 'label' => Craft::t('app', 'ID'), - 'orderBy' => 'elements.id', - 'attribute' => 'id', - ], - // ... - ]; - } - - /** - * @inheritdoc - */ - protected static function defineTableAttributes(): array - { - return [ - 'id' => ['label' => Craft::t('app', 'ID')], - 'uid' => ['label' => Craft::t('app', 'UID')], - 'originLocation' => ['label' => Craft::t('commerce', 'Origin')], - 'destinationLocation' => ['label' => Craft::t('commerce', 'Destination')], - 'dateCreated' => ['label' => Craft::t('app', 'Date Created')], - 'dateUpdated' => ['label' => Craft::t('app', 'Date Updated')], - 'received' => ['label' => Craft::t('commerce', 'Received')], - ]; - } - - /** - * @inheritdoc - */ - protected static function defineDefaultTableAttributes(string $source): array - { - return [ - 'id', - 'dateCreated', - 'received', - ]; - } - - /** - * @inheritdoc - */ - protected function attributeHtml(string $attribute): string - { - return match ($attribute) { - 'originLocation' => $this->getOriginLocation()?->getUiLabel() ?? '', - 'destinationLocation' => $this->getDestinationLocation()?->getUiLabel() ?? '', - 'received' => $this->getTotalReceived() . '/' . $this->getTotalQuantity(), - default => parent::attributeHtml($attribute), - }; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - - if ($this->scenario == static::SCENARIO_LIVE) { - $rules = ArrayHelper::merge($rules, [ - [['originLocationId', 'destinationLocationId'], 'number', 'integerOnly' => true], - [['originLocationId', 'destinationLocationId'], 'required'], - ]); - - $rules[] = [['originLocationId'], 'validateLocations']; - $rules[] = [['details'], 'validateDetails']; - } - - return $rules; - } - - /** - * @param $attribute - * @param $params - * @param $validator - * @return void - */ - public function validateDetails($attribute, $params, $validator) - { - if ($this->sumDetailsQuanity() < 1) { - $this->addError($attribute, Craft::t('commerce', 'Transfer must have at least one item.')); - } - - foreach ($this->getDetails() as $detail) { - if (!$detail->validate()) { - $this->addModelErrors($detail, 'details'); - } - } - } - - /** - * @param $attribute - * @param $params - * @param $validator - * @return void - */ - public function validateLocations($attribute, $params, $validator) - { - if ($this->originLocationId == $this->destinationLocationId) { - $this->addError($attribute, Craft::t('commerce', 'Origin and destination cannot be the same.')); - } - } - - /** - * @inheritdoc - */ - public function getUriFormat(): ?string - { - return null; - } - - /** - * Define the sources for the transfer element index - * - * @param string|null $context - * @return array - */ - protected static function defineSources(string $context = null): array - { - $transferStatuses = TransferStatusType::cases(); - $transferStatusSources = []; - foreach ($transferStatuses as $status) { - $transferStatusSources[] = [ - 'key' => $status->value, - 'status' => $status->color(), - 'label' => Craft::t('commerce', $status->label()), - 'badgeCount' => Transfer::find()->transferStatus($status->value)->count(), - 'criteria' => [ - 'transferStatus' => $status->value, - ], - ]; - } - - return [ - [ - 'key' => '*', - 'label' => Craft::t('commerce', 'All Transfers'), - 'criteria' => [], - ], - [ - 'heading' => Craft::t('commerce', 'Transfer Status'), - ], - ...$transferStatusSources, - ]; - } - - /** - * - * @inheritdoc - */ - protected function previewTargets(): array - { - $previewTargets = []; - $url = $this->getUrl(); - if ($url) { - $previewTargets[] = [ - 'label' => Craft::t('app', 'Primary {type} page', [ - 'type' => self::lowerDisplayName(), - ]), - 'url' => $url, - ]; - } - return $previewTargets; - } - - /** - * @inheritdoc - */ - protected function safeActionMenuItems(): array - { - $safeActions = parent::safeActionMenuItems(); - - if ($this->isTransferDraft() && count($this->getDetails()) > 0) { - $safeActions['mark-as-pending'] = [ - 'action' => 'commerce/transfers/mark-as-pending', - 'label' => Craft::t('commerce', 'Mark as Pending'), - 'confirm' => Craft::t('commerce', 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.'), - 'params' => [ - 'transferId' => $this->id, - ], - 'redirect' => 'commerce/inventory/transfers/' . $this->id, - ]; - } - - return $safeActions; - } - - /** - * @inheritdoc - */ - protected function route(): array|string|null - { - // Define how transfers should be routed when their URLs are requested - return [ - 'templates/render', - [ - 'template' => 'site/template/path', - 'variables' => ['transfer' => $this], - ], - ]; - } - - /** - * @inheritdoc - */ - public function canView(User $user): bool - { - if (parent::canView($user)) { - return true; - } - - return $user->can('commerce-manageTransfers'); - } - - /** - * @inheritdoc - */ - public function canSave(User $user): bool - { - if (parent::canSave($user)) { - return true; - } - - return $user->can('commerce-manageTransfers'); - } - - /** - * @inheritdoc - */ - public function canDuplicate(User $user): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function canDelete(User $user): bool - { - $canDelete = false; - - if (parent::canSave($user)) { - $canDelete = true; - } - - if ($this->getTransferStatus() === TransferStatusType::DRAFT) { - $canDelete = true; - } - - return $canDelete && $user->can('commerce-manageTransfers'); - } - - /** - * @inheritdoc - */ - public function canCreateDrafts(User $user): bool - { - return false; - } - - /** - * @inheritdoc - */ - protected function cpEditUrl(): ?string - { - return UrlHelper::cpUrl("commerce/inventory/transfers/{$this->getCanonicalId()}"); - } - - /** - * @inheritdoc - */ - public function getPostEditUrl(): ?string - { - return UrlHelper::cpUrl('commerce/inventory/transfers'); - } - - /** - * @inheritdoc - */ - public function prepareEditScreen(Response $response, string $containerId): void - { - $view = Craft::$app->getView(); - $view->registerAssetBundle(TransfersAsset::class); - - $view->registerJsWithVars(fn($containerId, $settingsJs) => <<registerJsWithVars(fn($id, $settings) => << { - e.preventDefault(); - const modal = new Craft.Commerce.ReceiveTransferScreen($settings); - modal.on('close', (e) => { - console.log('closed'); - }); -}); -JS, [ - $receiveInventoryButtonId, - ['params' => ['transferId' => $this->id]], - ]); - - if (!$this->isTransferDraft()) { - - /** @var Response|CpScreenResponseBehavior $response */ - $response->additionalButtonsHtml(Html::a( - Craft::t('commerce', 'Receive Inventory'), - '#', - [ - 'id' => $receiveInventoryButtonId, - 'class' => 'btn', - ] - )); - } - - /** @var Response|CpScreenResponseBehavior $response */ - $response->crumbs([ - [ - 'label' => Craft::t('commerce', 'Commerce'), - 'url' => UrlHelper::cpUrl('commerce'), - ], - [ - 'label' => self::pluralDisplayName(), - 'url' => UrlHelper::cpUrl('commerce/inventory/transfers'), - ], - ]); - - $response->selectedSubnavItem('inventory-transfers'); - } - - /** - * @return TransferDetail[] - */ - public function getDetails(): array - { - if ($this->_details === null) { - $this->_details = Plugin::getInstance()->getTransfers()->getTransferDetailsByTransferId($this->id); - } - - return $this->_details; - } - - public function addDetails(TransferDetail $details): void - { - $this->_details = $this->getDetails(); - $this->_details[] = $details; - } - - /** - * @param TransferDetail[]|array $value - * - * @return void - */ - public function setDetails(array $value): void - { - foreach ($value as $key => $detail) { - if (!$detail instanceof TransferDetail) { - $value[$key] = new TransferDetail($detail); - } - - $value[$key]->setTransfer($this); - - if (!$value[$key]->inventoryItemId) { - unset($value[$key]); - } - } - - $this->_details = $value; - } - - /** - * @return int - */ - public function sumDetailsQuanity(): int - { - $sum = 0; - foreach ($this->getDetails() as $detail) { - $sum += $detail->quantity; - } - return $sum; - } - - /** - * @param TransferDetail $detail - * @return void - */ - public function addDetail(TransferDetail $detail): void - { - if (!$this->_details) { - $this->_details = []; - } - - foreach ($this->_details as $existingDetail) { - if ($existingDetail->inventoryItemId == $detail->inventoryItemId) { - $existingDetail->quantity += $detail->quantity; - return; - } - } - - $this->_details[] = $detail; - } - - /** - * @inheritdoc - */ - public function getFieldLayout(): ?FieldLayout - { - return Plugin::getInstance()->getTransfers()->getFieldLayout(); - } - - /** - * @inheritdoc - */ - public function beforeValidate() - { - if ($this->transferStatus === null) { - $this->transferStatus = TransferStatusType::DRAFT; - } - - return parent::beforeValidate(); - } - - /** - * @inheritdoc - */ - public function afterSave(bool $isNew): void - { - if (!$this->propagating) { - $transferId = $this->getCanonicalId(); - $transferRecord = TransferRecord::findOne($transferId); - - if (!$transferRecord) { - $transferRecord = new TransferRecord(); - } - - $originalTransferStatus = $transferRecord->transferStatus; - - $transferRecord->id = $this->id; - $transferRecord->originLocationId = $this->originLocationId; - $transferRecord->destinationLocationId = $this->destinationLocationId; - $transferRecord->transferStatus = $this->getTransferStatus()->value ?? TransferStatusType::DRAFT->value; - - $transferRecord->save(false); - - if ($this->getTransferStatus() === TransferStatusType::PENDING && $originalTransferStatus == TransferStatusType::DRAFT->value) { - $inventoryUpdateCollection = new UpdateInventoryLevelCollection(); - foreach ($this->getDetails() as $detail) { - $inventoryUpdate1 = new UpdateInventoryLevelInTransfer(); - $inventoryUpdate1->type = InventoryTransactionType::INCOMING->value; - $inventoryUpdate1->updateAction = InventoryUpdateQuantityType::ADJUST; - $inventoryUpdate1->inventoryItemId = $detail->inventoryItemId; - $inventoryUpdate1->transferId = $this->id; - $inventoryUpdate1->inventoryLocationId = $this->destinationLocationId; - $inventoryUpdate1->quantity = $detail->quantity; - $inventoryUpdate1->note = Craft::t('commerce', 'Incoming transfer from Transfer ID: ') . $this->id; - - $inventoryUpdateCollection->push($inventoryUpdate1); - - $inventoryUpdate2 = new UpdateInventoryLevelInTransfer(); - $inventoryUpdate2->type = 'onHand'; - $inventoryUpdate2->updateAction = InventoryUpdateQuantityType::ADJUST; - $inventoryUpdate2->inventoryItemId = $detail->inventoryItemId; - $inventoryUpdate2->transferId = $this->id; - $inventoryUpdate2->inventoryLocationId = $this->originLocationId; - $inventoryUpdate2->quantity = $detail->quantity * -1; - $inventoryUpdate2->note = Craft::t('commerce', 'Outgoing transfer from Transfer ID: ') . $this->id; - - $inventoryUpdateCollection->push($inventoryUpdate2); - } - - Plugin::getInstance()->getInventory()->executeUpdateInventoryLevels($inventoryUpdateCollection); - } - - $existingDetailIds = (new Query()) - ->select('id') - ->from('{{%commerce_transferdetails}}') - ->where(['transferId' => $this->id]) - ->column(); - - $currentDetailIds = []; - - foreach ($this->getDetails() as $detail) { - if ($detail->id) { - $detailRecord = TransferDetailRecord::findOne($detail->id); - } else { - $detailRecord = new TransferDetailRecord(); - } - $detailRecord->transferId = $this->id; - $detailRecord->inventoryItemId = $detail->inventoryItemId; - $inventoryItem = $detail->inventoryItemId ? Plugin::getInstance()->getInventory()->getInventoryItemById($detail->inventoryItemId) : null; - $detailRecord->inventoryItemDescription = $inventoryItem?->sku ?? ''; - $detailRecord->quantity = $detail->quantity; - $detailRecord->quantityAccepted = $detail->quantityAccepted; - $detailRecord->quantityRejected = $detail->quantityRejected; - - $detailRecord->save(); - $detail->id = $detailRecord->id; - - $currentDetailIds[] = $detailRecord->id; - } - - $deletedDetailIds = array_diff($existingDetailIds, $currentDetailIds); - if (!empty($deletedDetailIds)) { - TransferDetailRecord::deleteAll(['id' => $deletedDetailIds]); - } - - $this->updateTransferStatus(); - $transferRecord->transferStatus = $this->getTransferStatus()->value; - - $transferRecord->save(false); - } - - parent::afterSave($isNew); - } - - /** - * @return bool - */ - public function isTransferDraft(): bool - { - return $this->getTransferStatus() === TransferStatusType::DRAFT; - } - - /** - * @return bool - */ - public function isTransferPending(): bool - { - return $this->getTransferStatus() === TransferStatusType::PENDING; - } - - /** - * @return bool - */ - public function isTransferPartial(): bool - { - return $this->getTransferStatus() === TransferStatusType::PARTIAL; - } - - /** - * @return bool - */ - public function isTransferReceived(): bool - { - return $this->getTransferStatus() === TransferStatusType::RECEIVED; - } - - /** - * @return int - */ - public function getTotalRejected(): int - { - $totalRejected = 0; - foreach ($this->getDetails() as $detail) { - $totalRejected += $detail->quantityRejected; - } - return $totalRejected; - } - - /** - * @return int - */ - public function getTotalAccepted(): int - { - $totalAccepted = 0; - foreach ($this->getDetails() as $detail) { - $totalAccepted += $detail->quantityAccepted; - } - return $totalAccepted; - } - - /** - * @return int - */ - public function getTotalReceived(): int - { - return $this->getTotalAccepted() + $this->getTotalRejected(); - } - - /** - * @return bool - */ - public function isAllReceived(): bool - { - foreach ($this->getDetails() as $detail) { - if ($detail->getReceived() < $detail->quantity) { - return false; - } - } - - return true; - } - - /** - * @return int - */ - public function getTotalQuantity(): int - { - $totalQuantity = 0; - foreach ($this->getDetails() as $detail) { - $totalQuantity += $detail->quantity; - } - return $totalQuantity; - } -} diff --git a/src/elements/Variant.php b/src/elements/Variant.php deleted file mode 100755 index d6583b6805..0000000000 --- a/src/elements/Variant.php +++ /dev/null @@ -1,1554 +0,0 @@ - - * @since 2.0 - */ -class Variant extends Purchasable implements NestedElementInterface -{ - use NestedElementTrait { - eagerLoadingMap as traitEagerLoadingMap; - setPrimaryOwner as traitSetPrimaryOwner; - setOwner as traitSetOwner; - setEagerLoadedElements as traitSetEagerLoadedElements; - extraFields as traitExtraFields; - } - - /** - * @event craft\commerce\events\CustomizeVariantSnapshotFieldsEvent The event that is triggered before a variant’s field data is captured, which makes it possible to customize which fields are included in the snapshot. Custom fields are not included by default. - * - * This example adds every custom field to the variant snapshot: - * - * ```php - * use craft\commerce\elements\Variant; - * use craft\commerce\events\CustomizeVariantSnapshotFieldsEvent; - * use yii\base\Event; - * - * Event::on( - * Variant::class, - * Variant::EVENT_BEFORE_CAPTURE_VARIANT_SNAPSHOT, - * function(CustomizeVariantSnapshotFieldsEvent $event) { - * // @var Variant $variant - * $variant = $event->variant; - * // @var array|null $fields - * $fields = $event->fields; - * - * // Add every custom field to the snapshot - * if (($fieldLayout = $variant->getFieldLayout()) !== null) { - * foreach ($fieldLayout->getFields() as $field) { - * $fields[] = $field->handle; - * } - * } - * - * $event->fields = $fields; - * } - * ); - * ``` - */ - public const EVENT_BEFORE_CAPTURE_VARIANT_SNAPSHOT = 'beforeCaptureVariantSnapshot'; - - /** - * @event craft\commerce\events\CustomizeVariantSnapshotDataEvent The event that is triggered after a variant’s field data is captured. This makes it possible to customize, extend, or redact the data to be persisted on the variant instance. - * - * ```php - * use craft\commerce\elements\Variant; - * use craft\commerce\events\CustomizeVariantSnapshotDataEvent; - * use yii\base\Event; - * - * Event::on( - * Variant::class, - * Variant::EVENT_AFTER_CAPTURE_VARIANT_SNAPSHOT, - * function(CustomizeVariantSnapshotDataEvent $event) { - * // @var Variant $variant - * $variant = $event->variant; - * // @var array|null $fields - * $fields = $event->fields; - * - * // Modify or redact captured `$data` - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_CAPTURE_VARIANT_SNAPSHOT = 'afterCaptureVariantSnapshot'; - - /** - * @event craft\commerce\events\CustomizeProductSnapshotFieldsEvent The event that is triggered before a product’s field data is captured. This makes it possible to customize which fields are included in the snapshot. Custom fields are not included by default. - * - * This example adds every custom field to the product snapshot: - * - * ```php - * use craft\commerce\elements\Variant; - * use craft\commerce\elements\Product; - * use craft\commerce\events\CustomizeProductSnapshotFieldsEvent; - * use yii\base\Event; - * - * Event::on( - * Variant::class, - * Variant::EVENT_BEFORE_CAPTURE_PRODUCT_SNAPSHOT, - * function(CustomizeProductSnapshotFieldsEvent $event) { - * // @var Product $product - * $product = $event->product; - * // @var array|null $fields - * $fields = $event->fields; - * - * // Add every custom field to the snapshot - * if (($fieldLayout = $product->getFieldLayout()) !== null) { - * foreach ($fieldLayout->getFields() as $field) { - * $fields[] = $field->handle; - * } - * } - * - * $event->fields = $fields; - * } - * ); - * ``` - * - * ::: warning - * Add with care! A huge amount of custom fields/data will increase your database size. - * ::: - */ - public const EVENT_BEFORE_CAPTURE_PRODUCT_SNAPSHOT = 'beforeCaptureProductSnapshot'; - - /** - * @event craft\commerce\events\CustomizeProductSnapshotDataEvent The event that is triggered after a product’s field data is captured, which can be used to customize, extend, or redact the data to be persisted on the product instance. - * - * ```php - * use craft\commerce\elements\Variant; - * use craft\commerce\elements\Product; - * use craft\commerce\events\CustomizeProductSnapshotDataEvent; - * use yii\base\Event; - * - * Event::on( - * Variant::class, - * Variant::EVENT_AFTER_CAPTURE_PRODUCT_SNAPSHOT, - * function(CustomizeProductSnapshotDataEvent $event) { - * // @var Product $product - * $product = $event->product; - * // @var array $data - * $data = $event->fieldData; - * - * // Modify or redact captured `$data` - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_CAPTURE_PRODUCT_SNAPSHOT = 'afterCaptureProductSnapshot'; - - /** - * @var bool $isDefault - */ - public bool $isDefault = false; - - /** - * @var int|null $sortOrder - */ - public ?int $sortOrder = null; - - /** - * @var string|null - * @see getProductSlug() - * @see setProductSlug() - */ - private ?string $_productSlug = null; - - /** - * @var string|null - * @see getProductTypeHandle() - * @see setProductTypeHandle() - */ - private ?string $_productTypeHandle = null; - - /** - * @var bool Whether the SKU was auto-generated from a `skuFormat` containing `{id}` before this element had an ID, - * meaning it needs to be regenerated in [[afterAssignedId()]] once the ID is available. - */ - private bool $_regenerateSkuAfterIdAssigned = false; - - /** - * @throws InvalidConfigException - */ - public function behaviors(): array - { - $behaviors = parent::behaviors(); - - $behaviors['currencyAttributes'] = [ - 'class' => CurrencyAttributeBehavior::class, - 'currencyAttributes' => $this->currencyAttributes(), - ]; - - return $behaviors; - } - - public function safeAttributes() - { - $attributes = parent::safeAttributes(); - $attributes[] = 'productId'; - - return $attributes; - } - - /** - * @inheritdoc - */ - public function init(): void - { - parent::init(); - $this->ownerType = Product::class; - } - - /** - * @inheritdoc - */ - protected function uiLabel(): ?string - { - $owner = $this->getOwner(); - if ($owner) { - $uiLabelFormat = $owner->getType()->variantUiLabelFormat; - if ($uiLabelFormat !== '{title}') { - $uiLabel = Craft::$app->getView()->renderSandboxedObjectTemplate($uiLabelFormat, $this); - if ($uiLabel !== '') { - return $uiLabel; - } - } - } - - return null; - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Product Variant'); - } - - /** - * @inheritdoc - */ - public static function lowerDisplayName(): string - { - return Craft::t('commerce', 'product variant'); - } - - /** - * @inheritdoc - */ - public static function pluralDisplayName(): string - { - return Craft::t('commerce', 'Product Variants'); - } - - /** - * @inheritdoc - */ - public static function pluralLowerDisplayName(): string - { - return Craft::t('commerce', 'product variants'); - } - - /** - * @inheritdoc - */ - public static function refHandle(): ?string - { - return 'variant'; - } - - /** - * @inheritdoc - */ - public function getIsTitleTranslatable(): bool - { - return ($this->getOwner()->getType()->variantTitleTranslationMethod !== Field::TRANSLATION_METHOD_NONE); - } - - /** - * @inheritdoc - */ - public function getTitleTranslationDescription(): ?string - { - return ElementHelper::translationDescription($this->getOwner()->getType()->variantTitleTranslationMethod); - } - - /** - * @inheritdoc - */ - public function getTitleTranslationKey(): string - { - $type = $this->getOwner()->getType(); - return ElementHelper::translationKey($this, $type->variantTitleTranslationMethod, $type->variantTitleTranslationKeyFormat); - } - - /** - * @inheritdoc - */ - public function canSave(User $user): bool - { - if (parent::canSave($user)) { - return true; - } - - $product = $this->getOwner(); - if ($product === null) { - return false; - } - - return $product->canSave($user); - } - - /** - * @inheritdoc - */ - public function canCopy(User $user): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function canDelete(User $user): bool - { - if (parent::canDelete($user)) { - return true; - } - - return $this->canSave($user); - } - - /** - * @return bool - * @todo Remove in Commerce 6.0 along with the deprecated `deletedWithProduct` property (use `deletedWithOwner` instead) - */ - public function getDeletedWithProduct(): bool - { - Craft::$app->getDeprecator()->log('Variant::getDeletedWithProduct()', 'The “deletedWithProduct” property has been deprecated. Use “deletedWithOwner” instead.'); - - return $this->deletedWithOwner; - } - - /** - * @param $value - * @return void - * @todo Remove in Commerce 6.0 along with the deprecated `deletedWithProduct` property (use `deletedWithOwner` instead) - */ - public function setDeletedWithProduct($value): void - { - return; - } - - /** - * @inheritdoc - */ - public function canDuplicate(User $user): bool - { - if (parent::canDuplicate($user)) { - return true; - } - - return $this->canSave($user); - } - - /** - * @inheritdoc - */ - protected static function includeSetStatusAction(): bool - { - return true; - } - - /** - * @inheritdoc - * @throws InvalidConfigException - */ - public function getIsAvailable(): bool - { - if ($this->getIsRevision()) { - return false; - } - - if ($this->getIsDraft()) { - return false; - } - - if ($this->getPrimaryOwner()->getIsDraft()) { - return false; - } - - if ($this->getPrimaryOwner()->status != Product::STATUS_LIVE) { - return false; - } - - return parent::getIsAvailable(); - } - - /** - * @inheritdoc - * @return VariantCondition - * @throws InvalidConfigException - */ - public static function createCondition(): ElementConditionInterface - { - return Craft::createObject(VariantCondition::class, [static::class]); - } - - /** - * @return void - * @noinspection PhpUnused - */ - public function validateMinQtyRange() - { - if ($this->minQty && $this->maxQty && $this->minQty > $this->maxQty) { - $this->addError('minQty', Craft::t('commerce', 'Min quantity must be less than max.')); - } - } - - /** - * @return void - * @noinspection PhpUnused - */ - public function validateMaxQtyRange() - { - if ($this->minQty && $this->maxQty && $this->maxQty < $this->minQty) { - $this->addError('maxQty', Craft::t('commerce', 'Max quantity must greater than min.')); - } - } - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $names = $this->traitExtraFields(); - $names[] = 'product'; - - return $names; - } - - /** - * @inheritdoc - */ - public function getFieldLayout(): ?FieldLayout - { - $fieldLayout = parent::getFieldLayout(); - - // If we have a field layout, try to set its provider from product type - if ($fieldLayout) { - $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); - $productType = collect($productTypes)->firstWhere('variantFieldLayoutId', $fieldLayout->id); - - if ($productType) { - $fieldLayout->provider = $productType; - return $fieldLayout; - } - } - - // Try to get field layout from owner's product type - try { - $owner = $this->getOwner(); - - return $owner === null - ? $fieldLayout - : $owner->getType()->getVariantFieldLayout(); - } catch (InvalidConfigException) { - // Product type was likely deleted - return null; - } - } - - /** - * @inheritdoc - */ - protected function metadata(): array - { - $metadata = parent::metadata(); - - $product = $this->getOwner(); - - if ($product) { - $metadata[Craft::t('commerce', 'Product')] = Cp::elementChipHtml($product, ['showActionMenu' => true]); - } - - return $metadata; - } - - /** - * @param int|null $productId - * @return void - * @since 5.0.0 - * @deprecated in 5.0.0. Use [[setOwnerId()]] instead. - */ - public function setProductId(?int $productId) - { - $this->setOwnerId($productId); - } - - /** - * @return int|null - * @throws InvalidConfigException - * @deprecated in 5.0.0. Use [[getOwnerId()]] instead. - * @since 5.0.0 - */ - public function getProductId(): ?int - { - return $this->getOwnerId(); - } - - /** - * @inheritdoc - */ - public function setPrimaryOwner(?ElementInterface $owner): void - { - if (!$owner instanceof Product) { - throw new InvalidArgumentException('Product variants can only be assigned to products.'); - } - - if ($owner->siteId) { - $this->siteId = $owner->siteId; - } - - $this->fieldLayoutId = $owner->getType()->variantFieldLayoutId; - - $this->traitSetPrimaryOwner($owner); - } - - /** - * @inheritdoc - */ - public function setOwner(?ElementInterface $owner): void - { - if (!$owner instanceof Product) { - throw new InvalidArgumentException('Product variants can only be assigned to products.'); - } - - if ($owner->siteId) { - $this->siteId = $owner->siteId; - } - - $this->fieldLayoutId = $owner->getType()->variantFieldLayoutId; - - $this->traitSetOwner($owner); - } - - /** - * Returns the product associated with this variant. - * - * @return Product|null The product associated with this variant, or null if it isn’t known - * @deprecated in 5.0.0. Use [[getOwner()]] instead. - */ - public function getProduct(): ?Product - { - /** @var Product|null */ - return $this->getOwner(); - } - - /** - * Sets the product associated with this variant. - * - * @param Product $product The product associated with this variant - * @deprecated in 5.0.0. Use [[setOwner()]] instead. - */ - public function setProduct(Product $product): void - { - $this->setOwner($product); - } - - /** - * @param string|null $productSlug - * @return void - * @since 5.0.0 - */ - public function setProductSlug(?string $productSlug): void - { - $this->_productSlug = $productSlug; - } - - /** - * @return string|null - * @throws InvalidConfigException - * @since 5.0.0 - */ - public function getProductSlug(): ?string - { - if ($this->_productSlug === null) { - $product = $this->getOwner(); - - $this->_productSlug = $product?->slug ?? null; - } - - return $this->_productSlug; - } - - /** - * @param string|null $productTypeHandle - * @return void - * @since 5.0.0 - */ - public function setProductTypeHandle(?string $productTypeHandle): void - { - $this->_productTypeHandle = $productTypeHandle; - } - - /** - * @return string|null - * @throws InvalidConfigException - * @since 5.0.0 - */ - public function getProductTypeHandle(): ?string - { - if ($this->_productTypeHandle === null) { - $product = $this->getOwner(); - - $this->_productTypeHandle = $product ? ($product->getType()?->handle ?? null) : null; - } - - return $this->_productTypeHandle; - } - - /** - * Returns the product title and variants title together for variable products. - * - * @throws Exception - * @throws InvalidConfigException - * @throws Throwable - */ - public function getDescription(): string - { - $description = $this->title; - - if ($format = $this->getOwner()->getType()->descriptionFormat) { - if ($rendered = Craft::$app->getView()->renderSandboxedObjectTemplate($format, $this)) { - $description = $rendered; - } - } - - // If title is not set yet default to blank string - return (string)$description; - } - - /** - * Updates the title based on titleFormat, or sets it to the same title as the product. - * - * @throws Exception - * @throws InvalidConfigException - * @throws Throwable - * @see \craft\elements\Entry::updateTitle - */ - public function updateTitle(Product $product): void - { - $type = $product->getType(); - // Use the product type's titleFormat if the title field is not shown - if (!$type->hasVariantTitleField && $type->variantTitleFormat) { - // Make sure that the locale has been loaded in case the title format has any Date/Time fields - Craft::$app->getLocale(); - // Set Craft to the product's site's language, in case the title format has any static translations - $language = Craft::$app->language; - Craft::$app->language = $this->getSite()->language; - $this->title = Craft::$app->getView()->renderSandboxedObjectTemplate($type->variantTitleFormat, $this); - Craft::$app->language = $language; - } - } - - - /** - * @throws Throwable - */ - public function updateSku(Product $product): void - { - $type = $product->getType(); - // If we have a blank SKU, generate from product type’s skuFormat - if (!$this->sku && $type->skuFormat) { - // Make sure that the locale has been loaded in case the title format has any Date/Time fields - Craft::$app->getLocale(); - // Set Craft to the product’s site’s language, in case the title format has any static translations - $language = Craft::$app->language; - Craft::$app->language = $this->getSite()->language; - $this->sku = Craft::$app->getView()->renderSandboxedObjectTemplate($type->skuFormat, $this); - - // If the format references the element's own ID but it doesn't have one yet, the rendered SKU will be - // missing that value — flag it for regeneration once afterAssignedId() runs. - if (!$this->id && str_contains($type->skuFormat, '{id}')) { - $this->_regenerateSkuAfterIdAssigned = true; - } - - $skuExistsQuery = function(string $sku, ?int $id) { - $query = (new Query()) - ->select(['sku']) - ->from(Table::PURCHASABLES) - ->where(['sku' => $sku]); - - // Make sure it isn't for the purchasable we are currently saving - if ($id) { - $query->andWhere(['not', ['id' => $id]]); - } - - return $query; - }; - - // Ensure there isn't a clash with an existing SKU when using auto formats - if ($skuExistsQuery($this->getSku(), $this->id)->exists()) { - // If there is a clash, we need to append a number to the end. - do { - $seq = Sequence::next('sku::' . $this->sku); - $newSku = $this->sku . '-' . $seq; - } while ($skuExistsQuery($newSku, $this->id)->exists()); - - $this->sku = $newSku; - } - - Craft::$app->language = $language; - } - } - - /** - * @inheritdoc - */ - protected function cacheTags(): array - { - $tags = []; - - if ($primaryOwnerId = $this->getPrimaryOwnerId()) { - $tags[] = "element::{$primaryOwnerId}"; - $tags[] = "product:{$primaryOwnerId}"; - } - - $ownerId = $this->getOwnerId(); - if ($ownerId && $ownerId !== $primaryOwnerId) { - $tags[] = "element::{$ownerId}"; - } - - return $tags; - } - - /** - * @inheritdoc - */ - public function canView(User $user): bool - { - if (parent::canView($user)) { - return true; - } - - $product = $this->getOwner(); - if ($product === null) { - return false; - } - - return $product->canView($user); - } - - /** - * @inheritdoc - */ - public function getUrl(): ?string - { - if ($url = parent::getUrl()) { - return $url; - } - - // Default URL is the product's URL with the variant ID as a query parameter - $productUrl = $this->getOwner()?->getUrl(); - return $productUrl ? UrlHelper::urlWithParams($productUrl, ['variant' => $this->id]) : null; - } - - /** - * - * @throws InvalidConfigException - */ - public function getSnapshot(): array - { - $data = parent::getSnapshot(); - $data['cpEditUrl'] = $this->getCpEditUrl(); - - // Default Product custom field handles - $productFields = []; - $productFieldsEvent = new CustomizeProductSnapshotFieldsEvent([ - 'product' => $this->getOwner(), - 'fields' => $productFields, - ]); - - // Allow plugins to modify Product fields to be fetched - if ($this->hasEventHandlers(self::EVENT_BEFORE_CAPTURE_PRODUCT_SNAPSHOT)) { - $this->trigger(self::EVENT_BEFORE_CAPTURE_PRODUCT_SNAPSHOT, $productFieldsEvent); - } - - // Product Attributes - if ($product = $this->getOwner()) { - $productAttributes = $product->attributes(); - - // Remove custom fields - if (($fieldLayout = $product->getFieldLayout()) !== null) { - foreach ($fieldLayout->getCustomFields() as $field) { - ArrayHelper::removeValue($productAttributes, $field->handle); - } - } - - // Add back the custom fields they want - foreach ($productFieldsEvent->fields as $field) { - $productAttributes[] = $field; - } - - $data['product'] = $this->getOwner()->toArray($productAttributes, [], false); - - $productDataEvent = new CustomizeProductSnapshotDataEvent([ - 'product' => $this->getOwner(), - 'fieldData' => $data['product'], - ]); - } else { - $productDataEvent = new CustomizeProductSnapshotDataEvent([ - 'product' => $this->getOwner(), - 'fieldData' => [], - ]); - } - - // Allow plugins to modify captured Product data - if ($this->hasEventHandlers(self::EVENT_AFTER_CAPTURE_PRODUCT_SNAPSHOT)) { - $this->trigger(self::EVENT_AFTER_CAPTURE_PRODUCT_SNAPSHOT, $productDataEvent); - } - - $data['product'] = $productDataEvent->fieldData; - - // Default Variant custom field handles - $variantFields = []; - $variantFieldsEvent = new CustomizeVariantSnapshotFieldsEvent([ - 'variant' => $this, - 'fields' => $variantFields, - ]); - - // Allow plugins to modify fields to be fetched - if ($this->hasEventHandlers(self::EVENT_BEFORE_CAPTURE_VARIANT_SNAPSHOT)) { - $this->trigger(self::EVENT_BEFORE_CAPTURE_VARIANT_SNAPSHOT, $variantFieldsEvent); - } - - $variantAttributes = $this->attributes(); - - // Remove custom fields - if (($fieldLayout = $this->getFieldLayout()) !== null) { - foreach ($fieldLayout->getCustomFields() as $field) { - ArrayHelper::removeValue($variantAttributes, $field->handle); - } - } - - // Add back the custom fields they want - foreach ($variantFieldsEvent->fields as $field) { - $variantAttributes[] = $field; - } - - $variantData = $this->toArray($variantAttributes, [], false); - - $variantDataEvent = new CustomizeVariantSnapshotDataEvent([ - 'variant' => $this, - 'fieldData' => $variantData, - ]); - - // Allow plugins to modify captured Variant data - if ($this->hasEventHandlers(self::EVENT_AFTER_CAPTURE_VARIANT_SNAPSHOT)) { - $this->trigger(self::EVENT_AFTER_CAPTURE_VARIANT_SNAPSHOT, $variantDataEvent); - } - - return array_merge($variantDataEvent->fieldData, $data); - } - - /** - * @inheritdoc - * @throws InvalidConfigException - */ - public function hasFreeShipping(): bool - { - $isShippable = $this->getIsShippable(); // Same as Plugin::getInstance()->getPurchasables()->isPurchasableShippable since this has no context - return $isShippable && $this->freeShipping; - } - - /** - * @inheritdoc - * @return VariantQuery The newly created [[VariantQuery]] instance. - */ - public static function find(): VariantQuery - { - return new VariantQuery(static::class); - } - - /** - * @inheritdoc - */ - public static function hasStatuses(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public static function eagerLoadingMap(array $sourceElements, string $handle): array|null|false - { - switch ($handle) { - case 'product': - // Get the source element IDs - $sourceElementIds = []; - - foreach ($sourceElements as $sourceElement) { - $sourceElementIds[] = $sourceElement->id; - } - - $map = (new Query()) - ->select('id as source, primaryOwnerId as target') - ->from(Table::VARIANTS) - ->where(['in', 'id', $sourceElementIds]) - ->all(); - - return [ - 'elementType' => Product::class, - 'map' => $map, - 'criteria' => [ - 'status' => null, - ], - ]; - case 'owner': - case 'primaryOwner': - return array_merge( - self::traitEagerLoadingMap($sourceElements, $handle), - ['elementType' => Product::class], - ); - default: - return self::traitEagerLoadingMap($sourceElements, $handle); - } - } - - /** - * Returns a promotion category related to this element if the category is related to the product OR the variant. - * - * @throws InvalidConfigException - */ - public function getPromotionRelationSource(): array - { - return [$this->id, $this->getOwner()->id]; - } - - /** - * @throws InvalidConfigException - * @since 3.1 - */ - public function getGqlTypeName(): string - { - $product = $this->getOwner(); - - if (!$product) { - return 'Variant'; - } - - try { - $productType = $product->getType(); - } catch (Exception) { - return 'Variant'; - } - - return static::gqlTypeNameByContext($productType); - } - - /** - * @return string - * @since 3.1 - */ - public static function gqlTypeNameByContext(mixed $context): string - { - return $context->handle . '_Variant'; - } - - /** - * @param mixed $context - * @return array - * @since 3.1 - */ - public static function gqlScopesByContext(mixed $context): array - { - /** @var ProductType $context */ - return ['productTypes.' . $context->uid]; - } - - /** - * @inheritdoc - */ - public function getSupportedSites(): array - { - $owner = $this->getOwner(); - - if (!$owner) { - return [Craft::$app->getSites()->getPrimarySite()->id]; - } - - return $this->getOwner()->getSupportedSites(); - } - - /** - * @inheritdoc - * @throws Exception - */ - public function afterSave(bool $isNew): void - { - $ownerId = $this->getOwnerId(); - - if (!$this->propagating) { - if (!$isNew) { - $record = VariantRecord::findOne($this->id); - - if (!$record) { - throw new Exception('Invalid variant ID: ' . $this->id); - } - } else { - $record = new VariantRecord(); - $record->id = $this->id; - } - - $record->primaryOwnerId = $this->getPrimaryOwnerId(); - - if ($this->getOwner()->getIsCanonical()) { - $record->isDefault = $this->isDefault; - } - - // We want to always have the same date as the element table, based on the logic for updating these in the element service i.e resaving - $record->dateUpdated = $this->dateUpdated; - $record->dateCreated = $this->dateCreated; - - $record->save(false); - - if ($ownerId && $this->saveOwnership) { - if (!isset($this->sortOrder) && (!$isNew || $this->duplicateOf)) { - // figure out if we should proceed this way - // if we're dealing with an element that's being duplicated, and it has a draftId - // it means we're creating a draft of something - // if we're duplicating element via duplicate action - draftId would be empty - // Same as https://github.com/craftcms/cms/pull/14497/files - $elementId = null; - if ($this->duplicateOf) { - if ($this->draftId) { - $elementId = $this->duplicateOf->id; - } - } else { - // if we're not duplicating - use element's id - $elementId = $this->id; - } - if ($elementId) { - $this->sortOrder = (new Query()) - ->select('sortOrder') - ->from(CraftTable::ELEMENTS_OWNERS) - ->where([ - 'elementId' => $elementId, - 'ownerId' => $ownerId, - ]) - ->scalar() ?: null; - } - } - if (!isset($this->sortOrder)) { - $max = (new Query()) - ->from(['eo' => CraftTable::ELEMENTS_OWNERS]) - ->innerJoin(['v' => Table::VARIANTS], '[[v.id]] = [[eo.elementId]]') - ->where([ - 'eo.ownerId' => $ownerId, - ]) - ->max('[[eo.sortOrder]]'); - $this->sortOrder = $max ? $max + 1 : 1; - } - - $ownerIds = array_unique([ - $ownerId, - $this->getPrimaryOwnerId(), - ]); - - if (!$isNew) { - Db::delete(CraftTAble::ELEMENTS_OWNERS, [ - 'elementId' => $this->id, - 'ownerId' => $ownerIds, - ]); - } - - foreach ($ownerIds as $ownerId) { - Db::insert(CraftTAble::ELEMENTS_OWNERS, [ - 'elementId' => $this->id, - 'ownerId' => $ownerId, - 'sortOrder' => $this->sortOrder, - ]); - } - } - } - - parent::afterSave($isNew); - - if (!$this->propagating && $this->isDefault && $ownerId && $this->duplicateOf === null) { - // @TODO Remove this denormalized default-variant data write in Commerce 6.0; the product query now joins this data directly - $defaultData = [ - 'defaultVariantId' => $this->id, - 'defaultSku' => $this->getSkuAsText(), - 'defaultPrice' => $this->getBasePrice(), - 'defaultHeight' => $this->height, - 'defaultLength' => $this->length, - 'defaultWidth' => $this->width, - 'defaultWeight' => $this->weight, - ]; - // Update the product that owns this variant - Db::update(Table::PRODUCTS, $defaultData, ['id' => $ownerId]); - // Update any other product that references this variant as its default (split from the above to avoid deadlocks from non-deterministic lock ordering with OR-clauses) - Db::update(Table::PRODUCTS, $defaultData, ['and', ['defaultVariantId' => $this->id], ['not', ['id' => $ownerId]]]); - } - } - - /** - * @inheritdoc - * @throws InvalidConfigException - */ - public function setEagerLoadedElements(string $handle, array $elements, EagerLoadPlan $plan): void - { - if (in_array($handle, ['product', 'owner', 'primaryOwner'])) { - $product = $elements[0] ?? null; - if ($product instanceof Product) { - if ($handle == 'primaryOwner') { - $this->setPrimaryOwner($product); - } else { - $this->setOwner($product); - } - } - } else { - $this->traitSetEagerLoadedElements($handle, $elements, $plan); - } - } - - /** - * @inheritdoc - */ - public static function hasTitles(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public static function isSelectable(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public static function isLocalized(): bool - { - return true; - } - - /** - * @inheritdoc - * @throws Throwable - * @throws InvalidConfigException - */ - public function beforeValidate(): bool - { - $product = $this->getOwner(); - - // hold off on updating the title and SKU if we are creating the shell of the variant ready for editing - /** @phpstan-ignore-next-line don't need the `$this->getIsDraft()` on the right side but leaving for readability */ - if (!$this->getIsDraft() || ($this->getIsDraft() && $this->getScenario() !== self::SCENARIO_ESSENTIALS)) { - $this->updateTitle($product); - $this->updateSku($product); - } - - if (!$this->sku && $this->getScenario() === self::SCENARIO_DEFAULT) { - $this->setSku(PurchasableHelper::tempSku()); - } - - return parent::beforeValidate(); - } - - /** - * @throws InvalidConfigException - */ - public function beforeSave(bool $isNew): bool - { - $product = $this->getOwner(); - - // hold off on updating the title and SKU if we are creating the shell of the variant ready for editing - /** @phpstan-ignore-next-line don't need the `$this->getIsDraft()` on the right side but leaving for readability */ - if (!$this->getIsDraft() || ($this->getIsDraft() && $this->getScenario() !== self::SCENARIO_ESSENTIALS)) { - $this->updateTitle($product); - $this->updateSku($product); - } - - // Set the field layout - $productType = $product->getType(); - $this->fieldLayoutId = $productType->variantFieldLayoutId; - - // Validate shipping category ID is available for this product type - $availableShippingCategories = $this->availableShippingCategories(); - $availableShippingCategoryIds = ArrayHelper::getColumn($availableShippingCategories, 'id'); - - // If the current shipping category ID is not in the available categories, set it to the default one - $currentShippingCategoryId = $this->getShippingCategoryId(); - if (!in_array($currentShippingCategoryId, $availableShippingCategoryIds)) { - $defaultShippingCategory = Plugin::getInstance()->getShippingCategories()->getDefaultShippingCategory($this->getStoreId()); - $this->setShippingCategoryId($defaultShippingCategory->id); - } - - return parent::beforeSave($isNew); - } - - /** - * @inheritdoc - */ - public function afterAssignedId(): void - { - if (ElementHelper::isDraftOrRevision($this)) { - return; - } - - $product = $this->getOwner(); - $this->updateTitle($product); - - if ($this->_regenerateSkuAfterIdAssigned) { - $this->_regenerateSkuAfterIdAssigned = false; - $this->sku = ''; - $this->updateSku($product); - } - } - - /** - * @inheritdoc - * @throws \yii\db\Exception - */ - public function beforeRestore(): bool - { - if (!parent::beforeRestore()) { - return false; - } - - // Check to see if any other purchasable has the same SKU and update this one before restore - $found = (new Query())->select(['[[p.sku]]', '[[e.id]]']) - ->from(Table::PURCHASABLES . ' p') - ->leftJoin(CraftTable::ELEMENTS . ' e', '[[p.id]]=[[e.id]]') - ->where(['[[e.dateDeleted]]' => null, '[[p.sku]]' => $this->getSku()]) - ->andWhere(['not', ['[[e.id]]' => $this->getId()]]) - ->count(); - - if ($found) { - // Set new SKU in memory - $this->sku = $this->getSku() . '-1'; - - // Update purchasable table with new SKU - Craft::$app->getDb()->createCommand()->update(Table::PURCHASABLES, - ['sku' => $this->sku], - ['id' => $this->getId()] - )->execute(); - } - - return true; - } - - /** - * @throws InvalidConfigException - * @since 2.2 - */ - public function getSearchKeywords(string $attribute): string - { - if ($attribute == 'productTitle') { - return $this->getOwner()->title ?? ''; - } - - return parent::getSearchKeywords($attribute); - } - - public function defineRules(): array - { - return array_merge(parent::defineRules(), [ - [['sku'], 'string', 'max' => 255], - [['sku'], 'required', 'on' => self::SCENARIO_LIVE], - [['basePrice'], 'validatePrice', 'on' => self::SCENARIO_LIVE, 'skipOnEmpty' => false], - [['price', 'weight', 'width', 'height', 'length'], 'number'], - // maxQty must be greater than minQty and minQty must be less than maxQty - [['minQty'], 'validateMinQtyRange', 'skipOnEmpty' => true], - [['maxQty'], 'validateMaxQtyRange', 'skipOnEmpty' => true], - [['stock', 'fieldId', 'ownerId', 'primaryOwnerId'], 'number'], - [['ownerId', 'primaryOwnerId', 'isDefault', 'deletedWithProduct'], 'safe'], - ]); - } - - /** - * @param string $attribute - * @param $params - * @param Validator $validator - */ - public function validatePrice(string $attribute, $params, Validator $validator): void - { - if ($this->$attribute === null) { - $message = Craft::t('yii', '{attribute} cannot be blank.', ['attribute' => $this->getAttributeLabel('price')]); - $validator->addError($this, 'price', $message); - } - } - - /** - * @inheritdoc - */ - protected function availableShippingCategories(): array - { - $allAvailableShippingCategories = parent::availableShippingCategories(); - - $productTypeId = $this->getPrimaryOwner()?->getType()->id; - - if (!$productTypeId) { - return [Plugin::getInstance()->getShippingCategories()->getDefaultShippingCategory($this->storeId)]; - } - - // Limit to only those for this product type - $categoryIds = collect(Plugin::getInstance()->getShippingCategories()->getShippingCategoriesByProductTypeId($productTypeId))->pluck('id')->toArray(); - $available = collect($allAvailableShippingCategories)->filter(fn(ShippingCategory $category) => in_array($category->id, $categoryIds)); - - if ($available->isEmpty()) { - return [Plugin::getInstance()->getShippingCategories()->getDefaultShippingCategory($this->storeId)]; - } - - return $available->toArray(); - } - - /** - * @inheritdoc - */ - protected function availableTaxCategories(): array - { - $allAvailableTaxCategories = parent::availableTaxCategories(); - - $productTypeId = $this->getPrimaryOwner()?->getType()->id; - - if (!$productTypeId) { - return [Plugin::getInstance()->getTaxCategories()->getDefaultTaxCategory()]; - } - - // Limit to only those for this product type - $categoryIds = collect(Plugin::getInstance()->getTaxCategories()->getTaxCategoriesByProductTypeId($productTypeId))->pluck('id')->toArray(); - $available = collect($allAvailableTaxCategories)->filter(fn(TaxCategory $category) => in_array($category->id, $categoryIds)); - - if ($available->isEmpty()) { - return [Plugin::getInstance()->getTaxCategories()->getDefaultTaxCategory()]; - } - - return $available->toArray(); - } - - /** - * @inheritdoc - */ - protected static function defineSources(string $context = null): array - { - $sources = Product::defineSources($context); - - // Ensure we don't inherit any product structure things from products. - foreach ($sources as $key => $source) { - $sources[$key]['defaultSort'] = ['postDate', 'desc']; - foreach (['structureId', 'structureEditable'] as $unsetKey) { - if (isset($sources[$key][$unsetKey])) { - unset($sources[$key][$unsetKey]); - } - } - } - - return $sources; - } - - protected static function defineActions(string $source): array - { - $actions = parent::defineActions($source); - // Restore - $actions[] = Craft::$app->getElements()->createAction([ - 'type' => Restore::class, - 'successMessage' => Craft::t('commerce', 'Variants restored.'), - 'partialSuccessMessage' => Craft::t('commerce', 'Some variants restored.'), - 'failMessage' => Craft::t('commerce', 'Variants not restored.'), - ]); - - if ($source === '__IMP__') { - $actions[] = ['type' => SetDefaultVariant::class]; - } - - // In case they are not running Craft 5.7+ - if (class_exists(Copy::class)) { - $actions[] = ['type' => Copy::class]; - } - - return $actions; - } - - /** - * @inheritdoc - */ - protected static function defineTableAttributes(): array - { - return array_merge(parent::defineTableAttributes(), [ - 'product' => Craft::t('commerce', 'Product'), - 'isDefault' => Craft::t('commerce', 'Default'), - 'promotable' => Craft::t('commerce', 'Promotable'), - ]); - } - - /** - * @inheritdoc - */ - protected static function defineDefaultTableAttributes(string $source): array - { - // Only add product as a `product` if we are viewing an implicit table - if ($source !== "__IMP__") { - $extras[] = 'product'; - } - $extras = ['isDefault']; - - return [...parent::defineDefaultTableAttributes($source), ...$extras]; - } - - /** - * @inheritdoc - */ - protected static function defineSearchableAttributes(): array - { - return [...parent::defineSearchableAttributes(), ...['productTitle']]; - } - - /** - * @inheritdoc - */ - protected static function defineCardAttributes(): array - { - return array_merge(parent::defineCardAttributes(), [ - 'product' => [ - 'label' => Craft::t('commerce', 'Product'), - ], - 'isDefault' => [ - 'label' => Craft::t('commerce', 'Default'), - ], - 'promotable' => [ - 'label' => Craft::t('commerce', 'Promotable'), - ], - ]); - } - - /** - * @inheritdoc - */ - protected function attributeHtml(string $attribute): string - { - if ($attribute === 'product') { - $product = $this->getOwner(); - if (!$product) { - return ''; - } - - return sprintf(' %s', $product->getStatus(), Html::encode($product->title)); - } - - if ($attribute === 'isDefault') { - if ($this->isDefault) { - $isDefault = Html::tag('span', '', [ - 'class' => 'checkbox-icon', - 'role' => 'img', - 'title' => Craft::t('app', 'Enabled'), - 'aria' => [ - 'label' => Craft::t('app', 'Enabled'), - ], - ]); - return $isDefault . Html::tag('span', ' ' . Craft::t('commerce', 'Default'), [ - 'class' => 'card-only-label', - 'style' => 'display:none;', - ]) . Html::tag('style', '.card-content .card-only-label { display: inline !important; }'); - } - } - - if ($attribute === 'promotable') { - if ($this->promotable) { - $promotable = Html::tag('span', '', [ - 'class' => 'checkbox-icon', - 'role' => 'img', - 'title' => Craft::t('app', 'Enabled'), - 'aria' => [ - 'label' => Craft::t('app', 'Enabled'), - ], - ]); - return $promotable . Html::tag('span', ' ' . Craft::t('commerce', 'Promotable'), [ - 'class' => 'card-only-label', - 'style' => 'display:none;', - ]) . Html::tag('style', '.card-content .card-only-label { display: inline !important; }'); - } - } - - return parent::attributeHtml($attribute); - } - - /** - * @inheritdoc - */ - protected function ownerType(): ?string - { - return Product::class; - } -} diff --git a/src/elements/VariantCollection.php b/src/elements/VariantCollection.php deleted file mode 100644 index c259c5d6b0..0000000000 --- a/src/elements/VariantCollection.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * @author Pixel & Tonic, Inc. - * @since 5.0.0 - */ -class VariantCollection extends ElementCollection -{ - /** - * Creates a VariantCollection from an array of Variant attributes. - * - * @param array $items - * @return static - */ - public static function make($items = []) - { - foreach ($items as &$item) { - if ($item instanceof Variant) { - continue; - } - - $item += ['class' => Variant::class]; - $item = \Craft::createObject($item); - } - - /** @var static $collection */ - $collection = parent::make($items); - return $collection; - } - - /** - * Returns the cheapest variant in the collection. - * - * @return Variant|null The cheapest variant in the collection, or null if there aren't any - */ - public function cheapest(): ?Variant - { - return $this->reduce(fn(?Variant $cheapest, Variant $variant) => !$cheapest || $variant->getSalePrice() < $cheapest->getSalePrice() ? $variant : $cheapest); - } -} diff --git a/src/elements/actions/CopyLoadCartUrl.php b/src/elements/actions/CopyLoadCartUrl.php deleted file mode 100644 index 23486ce695..0000000000 --- a/src/elements/actions/CopyLoadCartUrl.php +++ /dev/null @@ -1,71 +0,0 @@ - - * @since 3.3 - * - * @property-read null $triggerHtml - * @property-read string $triggerLabel - */ -class CopyLoadCartUrl extends ElementAction -{ - // Public Methods - // ========================================================================= - - /** - * @inheritdoc - */ - public function getTriggerLabel(): string - { - return Craft::t('commerce', 'Share cart…'); - } - - /** - * @inheritdoc - */ - public function getTriggerHtml(): ?string - { - $type = Json::encode(static::class); - $actionUrl = Json::encode(UrlHelper::actionUrl('commerce/orders/get-load-cart-url')); - - $jsTemplate = <<<'JS' -(() => { - new Craft.ElementActionTrigger({ - type: %s, - batch: false, - validateSelection: function($selectedItems) - { - return !!$selectedItems.find('.element').data('number'); - }, - activate: function($selectedItems) - { - var number = $selectedItems.find('.element').data('number'); - Craft.sendActionRequest('GET', %s, {params: {number: number}}).then(function(response) { - Craft.ui.createCopyTextPrompt({ - label: Craft.t('commerce', 'Copy the URL'), - instructions: Craft.t('commerce', "This URL will load the cart into the user's session, making it the active cart."), - value: response.data.url, - }); - }); - } - }); -})(); -JS; - Craft::$app->getView()->registerJs(sprintf($jsTemplate, $type, $actionUrl)); - return null; - } -} diff --git a/src/elements/actions/CreateDiscount.php b/src/elements/actions/CreateDiscount.php deleted file mode 100644 index 05a1560e18..0000000000 --- a/src/elements/actions/CreateDiscount.php +++ /dev/null @@ -1,60 +0,0 @@ - - * @since 2.0 - */ -class CreateDiscount extends ElementAction -{ - /** - * @inheritdoc - */ - public function getTriggerLabel(): string - { - return Craft::t('commerce', 'Create discount…'); - } - - - /** - * @inheritdoc - */ - public function getTriggerHtml(): ?string - { - $currentStore = Plugin::getInstance()->getStores()->getCurrentStore(); - $type = Json::encode(static::class); - $url = Json::encode('commerce/store-management/' . $currentStore->handle . '/discounts/new'); - $js = <<getView()->registerJs($js); - - return null; - } -} diff --git a/src/elements/actions/CreateSale.php b/src/elements/actions/CreateSale.php deleted file mode 100644 index a2aa94d78b..0000000000 --- a/src/elements/actions/CreateSale.php +++ /dev/null @@ -1,59 +0,0 @@ - - * @since 2.0 - */ -class CreateSale extends ElementAction -{ - /** - * @inheritdoc - */ - public function getTriggerLabel(): string - { - return Craft::t('commerce', 'Create sale…'); - } - - /** - * @inheritdoc - */ - public function getTriggerHtml(): ?string - { - $currentStore = Plugin::getInstance()->getStores()->getCurrentStore(); - $type = Json::encode(static::class); - $url = Json::encode('commerce/store-management/' . $currentStore->handle . '/sales/new'); - $js = <<getView()->registerJs($js); - - return null; - } -} diff --git a/src/elements/actions/DownloadOrderPdfAction.php b/src/elements/actions/DownloadOrderPdfAction.php deleted file mode 100644 index 8909b6774d..0000000000 --- a/src/elements/actions/DownloadOrderPdfAction.php +++ /dev/null @@ -1,202 +0,0 @@ - - * @since 3.2 - */ -class DownloadOrderPdfAction extends ElementAction -{ - public const TYPE_ZIP_ARCHIVE = 'zipArchive'; - public const TYPE_PDF_COLLATED = 'pdfCollated'; - - /** - * @inheritdoc - */ - public static function isDownload(): bool - { - return true; - } - - /** - * @var int|null - */ - public ?int $pdfId = null; - - /** - * @var string - */ - public string $downloadType = 'pdfCollated'; - - /** - * @var int|null - * @since 5.0.0 - */ - public ?int $storeId = null; - - /** - * @inheritdoc - */ - public function getTriggerLabel(): string - { - return Craft::t('commerce', 'Download PDF'); - } - - /** - * @inheritdoc - */ - public function getTriggerHtml(): ?string - { - if ($this->storeId === null) { - return ''; - } - - $allPdfs = Plugin::getInstance()->getPdfs()->getAllEnabledPdfs($this->storeId); - - $pdfs = []; - foreach ($allPdfs as $pdf) { - $pdfs[] = ['label' => Craft::t('site', $pdf->name), 'value' => $pdf->id]; - } - $pdfOptions = Json::encode($pdfs); - - $typeOptions = Json::encode([ - ['label' => Craft::t('commerce', 'ZIP file'), 'value' => self::TYPE_ZIP_ARCHIVE], - ['label' => Craft::t('commerce', 'Collated PDF'), 'value' => self::TYPE_PDF_COLLATED], - ]); - - $action = Json::encode(static::class); - - if (count($allPdfs) > 0) { - $js = << { - new Craft.Commerce.DownloadOrderPdfAction($('#download-order-pdf'), $pdfOptions, $typeOptions, $action); -})(); -JS; - Craft::$app->getView()->registerJs($js); - return Craft::$app->getView()->renderTemplate('commerce/_components/elementactions/DownloadOrderPdf/trigger'); - } - - return ''; - } - - /** - * @inheritdoc - * @throws Exception - * @throws HttpException - * @throws InvalidConfigException - * @throws RangeNotSatisfiableHttpException - * @throws Throwable - */ - public function performAction(ElementQueryInterface $query): bool - { - if ($this->storeId === null) { - throw new InvalidConfigException('Invalid store ID'); - } - - $pdfsService = Plugin::getInstance()->getPdfs(); - - $pdfId = $this->pdfId; - if ($pdfId === null) { - throw new InvalidConfigException("Invalid PDF ID"); - } - - $pdf = $pdfsService->getPdfById($pdfId, $this->storeId); - - if (!$pdf) { - throw new InvalidConfigException("Invalid PDF ID: '" . $pdfId . "'"); - } - - /** @var Order[] $orders */ - $orders = $query->all(); - - if (empty($orders)) { - return false; - } - - $response = Craft::$app->getResponse(); - - // Only one order, download single PDF - if (count($orders) === 1 && $this->downloadType == self::TYPE_PDF_COLLATED) { - $order = reset($orders); - $renderedPdf = $pdfsService->renderPdfForOrder($order, '', null, [], $pdf); - $filename = $this->_pdfFileName($pdf, $order); - $response->sendContentAsFile($renderedPdf, $filename); - return true; - } - - // Download collated in single PDF file - $merger = new Merger(); - if ($this->downloadType == self::TYPE_PDF_COLLATED) { - foreach ($orders as $order) { - $renderedPdf = $pdfsService->renderPdfForOrder($order, '', null, [], $pdf); - $merger->addRaw($renderedPdf); - } - $mergedPdf = $merger->merge(); - $response->sendContentAsFile($mergedPdf, 'Orders.pdf'); - return true; - } - - // If it is not collated, then it is a zip request - $zip = new ZipArchive(); - $zipPath = Craft::$app->getPath()->getTempPath() . '/' . StringHelper::UUID() . '.zip'; - - if ($zip->open($zipPath, ZipArchive::CREATE) !== true) { - throw new Exception('Cannot create zip at ' . $zipPath); - } - - foreach ($orders as $order) { - $renderedPdf = $pdfsService->renderPdfForOrder($order, '', null, [], $pdf); - $filename = $this->_pdfFileName($pdf, $order); - $zip->addFromString($filename, $renderedPdf); - } - - $zip->close(); - Craft::$app->getResponse()->sendContentAsFile(file_get_contents($zipPath), 'Orders.zip'); - FileHelper::unlink($zipPath); - - return true; - } - - /** - * Returns a PDF’s file name - * - * @throws Exception - * @throws Throwable - */ - private function _pdfFileName(Pdf $pdf, Order $order): string - { - $fileName = Craft::$app->getView()->renderSandboxedObjectTemplate($pdf->fileNameFormat, $order, $order->getObjectTemplateVariables()); - if (!$fileName) { - $fileName = $pdf->handle . '-' . $order->number; - } - - return $fileName . '.pdf'; - } -} diff --git a/src/elements/actions/SetDefaultVariant.php b/src/elements/actions/SetDefaultVariant.php deleted file mode 100644 index 1aea16d4a7..0000000000 --- a/src/elements/actions/SetDefaultVariant.php +++ /dev/null @@ -1,113 +0,0 @@ - - * @since 5.0.0 - */ -class SetDefaultVariant extends ElementAction -{ - /** - * @inheritdoc - */ - public function getTriggerLabel(): string - { - return Craft::t('commerce', 'Set default variant'); - } - - /** - * @inheritdoc - */ - public function getTriggerHtml(): ?string - { - $type = Json::encode(static::class); - - $js = <<getView()->registerJs($js); - - return null; - } - - /** - * @inheritdoc - */ - public function performAction(ElementQueryInterface $query): bool - { - /** @var Variant|null $variant */ - $variant = $query->one(); - if (!$variant) { - $this->setMessage(Craft::t('commerce', 'Unable to find variant.')); - return false; - } - - $product = $variant->getOwner(); - if (!$product) { - $this->setMessage(Craft::t('commerce', 'Variant has no product.')); - return false; - } - - // Update product row - Craft::$app->getDb()->createCommand()->update( - Table::PRODUCTS, - [ - 'defaultVariantId' => $variant->id, - 'defaultSku' => $variant->sku, - 'defaultPrice' => $variant->getBasePrice(), - 'defaultHeight' => $variant->height, - 'defaultLength' => $variant->length, - 'defaultWidth' => $variant->width, - 'defaultWeight' => $variant->weight, - ], - ['id' => $product->id] - )->execute(); - - if ($product->getIsCanonical()) { - // Remove previous default - Craft::$app->getDb()->createCommand()->update( - Table::VARIANTS, - ['isDefault' => false], - ['primaryOwnerId' => $product->id] - )->execute(); - - // Add new default - Craft::$app->getDb()->createCommand()->update( - Table::VARIANTS, - ['isDefault' => true], - ['id' => $variant->id] - )->execute(); - } - - Craft::$app->getElements()->invalidateCachesForElement($product); - Craft::$app->getElements()->invalidateCachesForElement($variant); - - $this->setMessage(Craft::t('commerce', 'Default variant updated.')); - return true; - } -} diff --git a/src/elements/actions/UpdateOrderStatus.php b/src/elements/actions/UpdateOrderStatus.php deleted file mode 100644 index 4a37b32ba9..0000000000 --- a/src/elements/actions/UpdateOrderStatus.php +++ /dev/null @@ -1,142 +0,0 @@ - - * @since 2.0 - */ -class UpdateOrderStatus extends ElementAction -{ - /** - * @var int|null - */ - public ?int $orderStatusId = null; - - /** - * @var string - */ - public string $message = ''; - - /** - * @var bool Whether to suppress the sending of related order status emails - */ - public bool $suppressEmails = false; - - /** - * @inheritdoc - */ - public function getTriggerLabel(): string - { - return Craft::t('commerce', 'Update Order Status…'); - } - - /** - * @inheritdoc - */ - public function getTriggerHtml(): ?string - { - /** @var Site|StoreBehavior $cpSite */ - $cpSite = Cp::requestedSite(); - $orderStatuses = Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses($cpSite->getStore()->id) - ->map(function(OrderStatus $orderStatus) { - // Encode for output in JS - $orderStatus->name = Html::encode($orderStatus->name); - $orderStatus->color = Html::encode($orderStatus->color); - $orderStatus->description = Html::encode($orderStatus->description); - - return $orderStatus; - }); - - $orderStatuses = Json::encode(array_values($orderStatuses->all())); - $type = Json::encode(static::class); - - $js = <<getView()->registerJs($js); - - return null; - } - - /** - * @inheritdoc - */ - public function performAction(ElementQueryInterface $query): bool - { - $orders = $query->all(); - $orderCount = count($orders); - - $failureCount = 0; - foreach ($orders as $order) { - /** @var Order $order */ - $order->orderStatusId = $this->orderStatusId; - $order->message = $this->message; - $order->suppressEmails = $this->suppressEmails; - if (!Craft::$app->getElements()->saveElement($order)) { - $failureCount++; - } - } - - if ($failureCount > 0) { - $message = Craft::t('commerce', 'Failed updating order status on {num, plural, =1{order} other{orders}}.', ['num' => $failureCount]); - if ($orderCount === $failureCount) { - $message = Craft::t('commerce', 'Failed to update {num, plural, =1{order status} other{order statuses}}.', ['num' => $failureCount]); - } - - $this->setMessage($message); - return false; - } - - $this->setMessage(Craft::t('commerce', '{num, plural, =1{Order} other{Orders}} updated.', ['num' => $orderCount])); - - return true; - } -} diff --git a/src/elements/conditions/addresses/DiscountAddressCondition.php b/src/elements/conditions/addresses/DiscountAddressCondition.php deleted file mode 100644 index 536cd182dd..0000000000 --- a/src/elements/conditions/addresses/DiscountAddressCondition.php +++ /dev/null @@ -1,43 +0,0 @@ - - * @since 4.0.0 - */ -class DiscountAddressCondition extends ElementAddressCondition -{ - /** - * @inheritdoc - */ - public ?string $elementType = Address::class; - - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - return array_merge(parent::selectableConditionRules(), - [ - PostalCodeFormulaConditionRule::class, - ]); - } - - /** - * @param ElementQueryInterface $query - * @return void - * @throws NotSupportedException - */ - public function modifyQuery(ElementQueryInterface $query): void - { - throw new NotSupportedException('Discount Address Condition does not support element queries.'); - } -} diff --git a/src/elements/conditions/addresses/GatewayAddressCondition.php b/src/elements/conditions/addresses/GatewayAddressCondition.php deleted file mode 100644 index 857be5cdb6..0000000000 --- a/src/elements/conditions/addresses/GatewayAddressCondition.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @since 5.5 - */ -class GatewayAddressCondition extends AddressCondition -{ - public function getBuilderHtml($readOnly = false): string - { - if ($readOnly) { - return Html::disableInputs(fn() => parent::getBuilderHtml()); - } - return parent::getBuilderHtml(); - } -} diff --git a/src/elements/conditions/addresses/PostalCodeFormulaConditionRule.php b/src/elements/conditions/addresses/PostalCodeFormulaConditionRule.php deleted file mode 100644 index 5edef57cac..0000000000 --- a/src/elements/conditions/addresses/PostalCodeFormulaConditionRule.php +++ /dev/null @@ -1,88 +0,0 @@ - - * @since 4.0.0 - * - */ -class PostalCodeFormulaConditionRule extends BaseTextConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Postal Code Formula'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return []; - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - throw new NotSupportedException('Discount Address Condition does not support element queries.'); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Address $address */ - $address = $element; - $formulasService = Plugin::getInstance()->getFormulas(); - $formula = $this->value; - $postalCode = $address->postalCode; - - try { - return (bool)$formulasService->evaluateCondition($formula, ['postalCode' => $postalCode], 'Postal code formula matching address'); - } catch (\Throwable) { - Craft::error('Error evaluating postal code formula: ' . $formula,'commerce'); - return false; - } - } - - public function operators(): array - { - return [ - self::OPERATOR_EQ, - ]; - } - - public function inputHtml(): string - { - return Html::hiddenLabel($this->getLabel(), 'value') . - Cp::textareaHtml([ - 'type' => $this->inputType(), - 'id' => 'value', - 'name' => 'value', - 'code' => 'value', - 'value' => $this->value, - 'autocomplete' => false, - 'class' => 'fullwidth code', - ]); - } -} diff --git a/src/elements/conditions/addresses/ZoneAddressCondition.php b/src/elements/conditions/addresses/ZoneAddressCondition.php deleted file mode 100644 index 0dc33e7fab..0000000000 --- a/src/elements/conditions/addresses/ZoneAddressCondition.php +++ /dev/null @@ -1,43 +0,0 @@ - - * @since 4.0.0 - */ -class ZoneAddressCondition extends ElementAddressCondition -{ - /** - * @inheritdoc - */ - public ?string $elementType = Address::class; - - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - return array_merge(parent::selectableConditionRules(), - [ - PostalCodeFormulaConditionRule::class, - ]); - } - - /** - * @param ElementQueryInterface $query - * @return void - * @throws NotSupportedException - */ - public function modifyQuery(ElementQueryInterface $query): void - { - throw new NotSupportedException('Discount Address Condition does not support element queries.'); - } -} diff --git a/src/elements/conditions/customers/CatalogPricingRuleCustomerCondition.php b/src/elements/conditions/customers/CatalogPricingRuleCustomerCondition.php deleted file mode 100644 index f45f1767cd..0000000000 --- a/src/elements/conditions/customers/CatalogPricingRuleCustomerCondition.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @since 5.0.0 - */ -class CatalogPricingRuleCustomerCondition extends UserCondition -{ - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - return array_merge( - array_filter(parent::selectableConditionRules(), static fn($type) => !in_array($type, [ - // Remove rules that don't make sense in this context - LastLoginDateConditionRule::class, - SiteConditionRule::class, - ], true) - ), - // Add additional rules - [ - CatalogPricingRuleCustomerConditionRule::class, - ] - ); - } -} diff --git a/src/elements/conditions/customers/CatalogPricingRuleCustomerConditionRule.php b/src/elements/conditions/customers/CatalogPricingRuleCustomerConditionRule.php deleted file mode 100644 index 57b5b2700b..0000000000 --- a/src/elements/conditions/customers/CatalogPricingRuleCustomerConditionRule.php +++ /dev/null @@ -1,75 +0,0 @@ - - * @since 5.5.0 - */ -class CatalogPricingRuleCustomerConditionRule extends BaseElementSelectConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - protected function elementType(): string - { - return User::class; - } - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Customer'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['id']; - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - /** @var UserQuery $query */ - $query->id($this->getElementIds()); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - /** @var User $element */ - return $this->matchValue($element->getId()); - } - - /** - * @inheritdoc - */ - protected function allowMultiple(): bool - { - return true; - } -} diff --git a/src/elements/conditions/customers/DiscountCustomerCondition.php b/src/elements/conditions/customers/DiscountCustomerCondition.php deleted file mode 100644 index a43437e0e1..0000000000 --- a/src/elements/conditions/customers/DiscountCustomerCondition.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @since 4.0.0 - */ -class DiscountCustomerCondition extends UserElementCondition -{ - /** - * @inheritdoc - */ - public ?string $elementType = User::class; - - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - return array_merge(parent::selectableConditionRules(), [ - HasOrdersConditionRule::class, - SignedInConditionRule::class, - DiscountGroupConditionRule::class, - ]); - } -} diff --git a/src/elements/conditions/customers/HasOrdersConditionRule.php b/src/elements/conditions/customers/HasOrdersConditionRule.php deleted file mode 100644 index dee24165c2..0000000000 --- a/src/elements/conditions/customers/HasOrdersConditionRule.php +++ /dev/null @@ -1,172 +0,0 @@ - - * @since 4.2.0 - * - * @property null|array|OrderCondition $orderCondition - */ -class HasOrdersConditionRule extends BaseNumberConditionRule implements ElementConditionRuleInterface -{ - /** - * @var array|OrderCondition|null - */ - private OrderCondition|array|null $_orderCondition = null; - - /** - * @var array - */ - private static array $_orderConditionResults = []; - - public function getConfig(): array - { - return array_merge(parent::getConfig(), [ - 'orderCondition' => $this->getOrderCondition()->getConfig(), - ]); - } - - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['orderCondition'], 'safe']; - - return $rules; - } - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Has Orders'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['hasOrders']; - } - - /** - * @param ElementQueryInterface $query - * @return void - * @throws NotSupportedException - */ - public function modifyQuery(ElementQueryInterface $query): void - { - throw new NotSupportedException('Has orders condition rule does not support queries'); - } - - /** - * @return string - * @throws InvalidConfigException - */ - public function getHtml(): string - { - $html = Html::label(Craft::t('commerce', 'Total Orders'), options: [ - 'style' => [ - 'padding-top' => '0.25rem', - 'padding-bottom' => '0.5rem', - 'font-weight' => 'bold', - 'color' => '#596673', - 'display' => 'block', - ], - ]); - $html .= parent::getHtml(); - $html .= Html::tag('div', Craft::t('commerce', 'Match Orders'), [ - 'style' => [ - 'margin-top' => '1rem', - 'font-weight' => 'bold', - 'color' => '#596673', - ], - ]); - $html .= Html::tag('div', $this->getOrderCondition()->getBuilderHtml(), ['style' => ['margin-top' => '0.5rem']]); - - return $html; - } - - /** - * @param ElementInterface $element - * @return bool - * @throws InvalidConfigException - */ - public function matchElement(ElementInterface $element): bool - { - $orderQuery = Order::find()->customerId($element->id); - $this->getOrderCondition()->modifyQuery($orderQuery); - $key = md5(implode('||', [ - $element->id, - Json::encode($this), - Json::encode($orderQuery), - ])); - - if (!isset(self::$_orderConditionResults[$key])) { - self::$_orderConditionResults[$key] = $this->matchValue($orderQuery->count()); - } - - return self::$_orderConditionResults[$key]; - } - - /** - * @return OrderCondition - * @throws InvalidConfigException - */ - public function getOrderCondition(): OrderCondition - { - if ($this->_orderCondition === null) { - $this->_orderCondition = Craft::$app->getConditions()->createCondition(['class' => OrderCondition::class]); - - // Set default rules - /** @var CompletedConditionRule $completedConditionRule */ - $completedConditionRule = Craft::$app->getConditions()->createConditionRule([ - 'class' => CompletedConditionRule::class, - ]); - $completedConditionRule->value = true; - - $this->_orderCondition->addConditionRule($completedConditionRule); - } elseif (is_array($this->_orderCondition)) { - /** @var OrderCondition $orderCondition */ - $orderCondition = Craft::$app->getConditions()->createCondition($this->_orderCondition); - $this->_orderCondition = $orderCondition; - } - - $this->_orderCondition->id = 'hasOrdersOrderCondition'; - $this->_orderCondition->mainTag = 'div'; - $this->_orderCondition->name = 'orderCondition'; - // Exclude unwanted condition rules - $this->_orderCondition->queryParams = ['customerId']; - return $this->_orderCondition; - } - - /** - * @param OrderCondition|array|null $condition - */ - public function setOrderCondition(OrderCondition|array|null $condition): void - { - $this->_orderCondition = $condition; - } -} diff --git a/src/elements/conditions/customers/ShippingMethodCustomerCondition.php b/src/elements/conditions/customers/ShippingMethodCustomerCondition.php deleted file mode 100644 index b95bcacc43..0000000000 --- a/src/elements/conditions/customers/ShippingMethodCustomerCondition.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 5.4.0 - */ -class ShippingMethodCustomerCondition extends UserCondition -{ -} diff --git a/src/elements/conditions/customers/ShippingRuleCustomerCondition.php b/src/elements/conditions/customers/ShippingRuleCustomerCondition.php deleted file mode 100644 index 93b2a70654..0000000000 --- a/src/elements/conditions/customers/ShippingRuleCustomerCondition.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 5.4.0 - */ -class ShippingRuleCustomerCondition extends UserCondition -{ -} diff --git a/src/elements/conditions/customers/SignedInConditionRule.php b/src/elements/conditions/customers/SignedInConditionRule.php deleted file mode 100644 index 0ca4c9ce66..0000000000 --- a/src/elements/conditions/customers/SignedInConditionRule.php +++ /dev/null @@ -1,85 +0,0 @@ - - * @since 4.2.6 - * - * @property null|array|OrderCondition $orderCondition - */ -class SignedInConditionRule extends BaseLightswitchConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Signed In'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return []; - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - throw new NotSupportedException('Signed in condition rule does not support element queries.'); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - /** @var User $element */ - $currentUser = Craft::$app->getUser()->getIdentity(); - $isStoreAdministrator = $currentUser && $currentUser->can('accessCp') && $currentUser->can('commerce-editOrders'); - - // If the current user is a store admin, and they are editing an order - if ($isStoreAdministrator) { - if ($this->value && $element->getIsCredentialed()) { - return true; - } - - if (!$this->value && !$element->getIsCredentialed()) { - return true; - } - - return false; - } - - if (!$this->value && !$currentUser) { - return true; - } - - if ($this->value && $currentUser && $currentUser->id === $element->id) { - return true; - } - - return false; - } -} diff --git a/src/elements/conditions/orders/CompletedConditionRule.php b/src/elements/conditions/orders/CompletedConditionRule.php deleted file mode 100644 index fb028ef4d6..0000000000 --- a/src/elements/conditions/orders/CompletedConditionRule.php +++ /dev/null @@ -1,58 +0,0 @@ - - * @since 4.2.0 - */ -class CompletedConditionRule extends BaseLightswitchConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Completed'); - } - - public function getExclusiveQueryParams(): array - { - return ['isCompleted']; - } - - /** - * @param ElementQueryInterface $query - * @return void - */ - public function modifyQuery(ElementQueryInterface $query): void - { - /** @var OrderQuery $query */ - $query->isCompleted($this->value); - } - - /** - * @param ElementInterface $element - * @return bool - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Order $element */ - return $element->isCompleted === $this->value; - } -} diff --git a/src/elements/conditions/orders/ContainsPurchasablesConditionRule.php b/src/elements/conditions/orders/ContainsPurchasablesConditionRule.php deleted file mode 100644 index bc293b89d3..0000000000 --- a/src/elements/conditions/orders/ContainsPurchasablesConditionRule.php +++ /dev/null @@ -1,223 +0,0 @@ - - * @since 5.7.0 - * - * @method array|string|null paramValue(?callable $normalizeValue = null) - */ -class ContainsPurchasablesConditionRule extends BaseElementSelectConditionRule implements ElementConditionRuleInterface -{ - /** - * @var string - */ - public string $purchasableType = Variant::class; - - /** - * @var ContainsPurchasablesMatch - * @see getMatch() - * @see setMatch() - */ - private ContainsPurchasablesMatch $_match = ContainsPurchasablesMatch::Any; - - /** - * @return ContainsPurchasablesMatch - */ - public function getMatch(): ContainsPurchasablesMatch - { - return $this->_match; - } - - /** - * Yii2 setter — converts stored string values back to the enum on load. - */ - public function setMatch(ContainsPurchasablesMatch|string $value): void - { - $this->_match = $value instanceof ContainsPurchasablesMatch ? $value : ContainsPurchasablesMatch::from($value); - } - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Contains Purchasables'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['hasPurchasable']; - } - - /** - * @inheritdoc - */ - protected function elementType(): string - { - return $this->purchasableType; - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - $ids = $this->getElementIds(); - if (empty($ids)) { - return; - } - - /** @var OrderQuery $query */ - $query->containsPurchasables(['purchasables' => $ids, 'match' => $this->getMatch()]); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Order $element */ - return $element->hasPurchasables($this->getElementIds(), $this->getMatch()); - } - - /** - * @inheritdoc - */ - protected function allowMultiple(): bool - { - return true; - } - - public function getConfig(): array - { - return array_merge(parent::getConfig(), [ - 'purchasableType' => $this->purchasableType, - 'match' => $this->getMatch()->value, - ]); - } - - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['purchasableType', 'match'], 'safe']; - - return $rules; - } - - /** - * @inheritdoc - */ - protected function inputHtml(): string - { - $matchId = 'match'; - $purchasableTypeOptions = $this->_purchasableTypeOptions(); - - $purchasableTypeHtml = count($purchasableTypeOptions) === 1 - ? Html::hiddenInput('purchasableType', $purchasableTypeOptions[0]['value']) - : Cp::selectHtml([ - 'id' => 'purchasable-type', - 'name' => 'purchasableType', - 'options' => $purchasableTypeOptions, - 'value' => $this->purchasableType, - 'inputAttributes' => [ - 'hx' => [ - 'post' => UrlHelper::actionUrl('conditions/render'), - ], - ], - ]); - - return Html::hiddenLabel($this->getLabel(), $matchId) . - Html::tag('div', - Cp::selectHtml([ - 'id' => $matchId, - 'name' => 'match', - 'options' => $this->_matchOptions(), - 'value' => $this->getMatch()->value, - 'inputAttributes' => [ - 'hx' => [ - 'post' => UrlHelper::actionUrl('conditions/render'), - ], - ], - ]) . - $purchasableTypeHtml . - parent::inputHtml(), - [ - 'class' => ['flex', 'flex-start'], - ] - ); - } - - protected function selectionCondition(): ?ElementConditionInterface - { - return Craft::$app->getConditions()->createCondition(['class' => OrderCondition::class]); - } - - /** - * @return array - * @throws InvalidConfigException - */ - private function _purchasableTypeOptions(): array - { - $options = []; - - foreach (Plugin::getInstance()->getPurchasables()->getAllPurchasableElementTypes() as $elementType) { - /** @var string|ElementInterface $elementType */ - /** @phpstan-var class-string|ElementInterface $elementType */ - $options[] = [ - 'value' => $elementType, - 'label' => $elementType::displayName(), - ]; - } - - return $options; - } - - /** - * @return array - */ - private function _matchOptions(): array - { - return array_map( - fn(ContainsPurchasablesMatch $m) => ['value' => $m->value, 'label' => $m->label()], - ContainsPurchasablesMatch::cases() - ); - } - - /** - * @inheritdoc - */ - protected function elementSelectConfig(): array - { - return array_merge(parent::elementSelectConfig(), [ - 'showSiteMenu' => true, - ]); - } -} diff --git a/src/elements/conditions/orders/CouponCodeConditionRule.php b/src/elements/conditions/orders/CouponCodeConditionRule.php deleted file mode 100644 index a6c231899f..0000000000 --- a/src/elements/conditions/orders/CouponCodeConditionRule.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @since 5.3.0 - */ -class CouponCodeConditionRule extends OrderTextValuesAttributeConditionRule -{ - public string $orderAttribute = 'couponCode'; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Coupon Code'); - } - - /** - * @inheritdoc - */ - protected function matchValue(mixed $value): bool - { - switch ($this->operator) { - case self::OPERATOR_EMPTY: - return !$value; - case self::OPERATOR_NOT_EMPTY: - return (bool)$value; - } - - if ($this->value === '') { - return true; - } - - return match ($this->operator) { - self::OPERATOR_EQ => strcasecmp($value, $this->value) === 0, - self::OPERATOR_NE => strcasecmp($value, $this->value) !== 0, - self::OPERATOR_BEGINS_WITH => is_string($value) && StringHelper::startsWith($value, $this->value, false), - self::OPERATOR_ENDS_WITH => is_string($value) && StringHelper::endsWith($value, $this->value, false), - self::OPERATOR_CONTAINS => is_string($value) && StringHelper::contains($value, $this->value, false), - default => throw new InvalidConfigException("Invalid operator: $this->operator"), - }; - } -} diff --git a/src/elements/conditions/orders/CustomerConditionRule.php b/src/elements/conditions/orders/CustomerConditionRule.php deleted file mode 100644 index 915c14d2ff..0000000000 --- a/src/elements/conditions/orders/CustomerConditionRule.php +++ /dev/null @@ -1,104 +0,0 @@ - - * @since 4.2.0 - * @todo Switch parent class to `BaseElementSelectConditionRule` in Commerce 6.0 once it supports negative matching (it currently lacks `OPERATOR_NOT_IN` support that this rule needs) - */ -class CustomerConditionRule extends BaseMultiSelectConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Customer'); - } - - /** - * @return array - * @deprecated in 4.3.1. - */ - protected function options(): array - { - return User::find() - ->status(null) - ->limit(null) - ->indexBy('id') - ->collect() - ->map(fn(User $customer) => $customer->fullName ? sprintf('%s (%s)', $customer->fullName, $customer->email) : $customer->email) - ->all(); - } - - /** - * @inheritDoc - */ - protected function inputHtml(): string - { - $users = User::find()->status(null)->limit(null)->id($this->values)->all(); - - return Cp::elementSelectHtml([ - 'name' => 'values', - 'elements' => $users, - 'elementType' => User::class, - 'sources' => null, - 'criteria' => null, - 'condition' => null, - 'single' => false, - ]); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['customerId']; - } - - /** - * @throws InvalidConfigException - */ - public function modifyQuery(ElementQueryInterface $query): void - { - /** @var OrderQuery $query */ - $paramValue = $this->paramValue(); - if ($this->operator === self::OPERATOR_NOT_IN) { - // Account for the fact the querying using a combination of `not` and `in` doesn't match `null` in the column - $query->andWhere(Db::parseParam(new Expression('coalesce([[commerce_orders.customerId]], -1)'), $paramValue)); - } else { - $query->customerId($paramValue); - } - } - - /** - * @throws InvalidConfigException - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Order $element */ - return $this->matchValue((string)$element->getCustomerId()); - } -} diff --git a/src/elements/conditions/orders/DateOrderedConditionRule.php b/src/elements/conditions/orders/DateOrderedConditionRule.php deleted file mode 100644 index eb791854e4..0000000000 --- a/src/elements/conditions/orders/DateOrderedConditionRule.php +++ /dev/null @@ -1,47 +0,0 @@ - - * @since 4.2.0 - */ -class DateOrderedConditionRule extends BaseDateRangeConditionRule implements ElementConditionRuleInterface -{ - public function getLabel(): string - { - return Craft::t('commerce', 'Date Ordered'); - } - - public function getExclusiveQueryParams(): array - { - return ['dateOrdered']; - } - - public function modifyQuery(ElementQueryInterface $query): void - { - /** @var OrderQuery $query */ - $query->dateOrdered($this->queryParamValue()); - } - - public function matchElement(ElementInterface $element): bool - { - /** @var Order $element */ - return $this->matchValue($element->dateOrdered); - } -} diff --git a/src/elements/conditions/orders/DiscountOrderCondition.php b/src/elements/conditions/orders/DiscountOrderCondition.php deleted file mode 100644 index 4b35288076..0000000000 --- a/src/elements/conditions/orders/DiscountOrderCondition.php +++ /dev/null @@ -1,62 +0,0 @@ - - * @since 4.0.0 - */ -class DiscountOrderCondition extends OrderCondition implements HasStoreInterface -{ - use StoreTrait; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['storeId'], 'safe']; - - return $rules; - } - - /** - * @return array - */ - protected function config(): array - { - return array_merge(parent::config(), $this->toArray(['storeId'])); - } - - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - $rules = array_merge(parent::selectableConditionRules(), []); - - // We don't need the condition to have the coupon code rule - ArrayHelper::removeValue($rules, CouponCodeConditionRule::class); - - return $rules; - } - - /** - * @param ElementQueryInterface $query - * @return void - * @throws NotSupportedException - */ - public function modifyQuery(ElementQueryInterface $query): void - { - throw new NotSupportedException('Discount Order Condition does not support element queries.'); - } -} diff --git a/src/elements/conditions/orders/DiscountedItemSubtotalConditionRule.php b/src/elements/conditions/orders/DiscountedItemSubtotalConditionRule.php deleted file mode 100644 index 5984f08bcd..0000000000 --- a/src/elements/conditions/orders/DiscountedItemSubtotalConditionRule.php +++ /dev/null @@ -1,64 +0,0 @@ - - * @since 5.0.0 - * - * @property-read float|int $orderAttributeValue - */ -class DiscountedItemSubtotalConditionRule extends OrderCurrencyValuesAttributeConditionRule -{ - public string $orderAttribute = 'itemSubtotal'; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Discounted Item Subtotal'); - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface|\yii\db\QueryInterface $query): void - { - throw new NotSupportedException('Discounted Item Subtotal condition rule does not support queries'); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Order $element */ - $discountAdjustments = []; - $discountAdjusters = Plugin::getInstance()->getOrderAdjustments()->getDiscountAdjusters(); - foreach ($discountAdjusters as $discountAdjuster) { - /** @var AdjusterInterface $discountAdjuster */ - $adjuster = new $discountAdjuster(); - $discountAdjustments = array_merge($discountAdjustments, $adjuster->adjust($element)); - } - - $discountAmount = 0; - foreach ($discountAdjustments as $adjustment) { - $discountAmount += $adjustment->amount; - } - - $itemTotal = $element->getItemSubtotal() + $discountAmount; - - return $this->matchValue($itemTotal); - } -} diff --git a/src/elements/conditions/orders/GatewayOrderCondition.php b/src/elements/conditions/orders/GatewayOrderCondition.php deleted file mode 100644 index 59ce77bed3..0000000000 --- a/src/elements/conditions/orders/GatewayOrderCondition.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @since 5.4.0 - */ -class GatewayOrderCondition extends OrderCondition -{ - public function getBuilderHtml($readOnly = false): string - { - if ($readOnly) { - return Html::disableInputs(fn() => parent::getBuilderHtml()); - } - return parent::getBuilderHtml(); - } -} diff --git a/src/elements/conditions/orders/HasAdminNoticesConditionRule.php b/src/elements/conditions/orders/HasAdminNoticesConditionRule.php deleted file mode 100644 index cea7112ac1..0000000000 --- a/src/elements/conditions/orders/HasAdminNoticesConditionRule.php +++ /dev/null @@ -1,47 +0,0 @@ - - * @since 5.x - */ -class HasAdminNoticesConditionRule extends BaseLightswitchConditionRule implements ElementConditionRuleInterface -{ - public function getLabel(): string - { - return Craft::t('commerce', 'Has Admin Notices'); - } - - public function getExclusiveQueryParams(): array - { - return ['hasAdminNotices']; - } - - public function modifyQuery(ElementQueryInterface $query): void - { - /** @var OrderQuery $query */ - $query->hasAdminNotices($this->value); - } - - public function matchElement(ElementInterface $element): bool - { - /** @var Order $element */ - return $element->hasAdminNotices() === $this->value; - } -} diff --git a/src/elements/conditions/orders/HasPurchasableConditionRule.php b/src/elements/conditions/orders/HasPurchasableConditionRule.php deleted file mode 100644 index 247ed3ef2c..0000000000 --- a/src/elements/conditions/orders/HasPurchasableConditionRule.php +++ /dev/null @@ -1,164 +0,0 @@ - - * @since 4.2.0 - * - * @method array|string|null paramValue(?callable $normalizeValue = null) - */ -class HasPurchasableConditionRule extends BaseElementSelectConditionRule implements ElementConditionRuleInterface -{ - /** - * @var string - */ - public string $purchasableType = Variant::class; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Has Purchasable'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['hasPurchasable']; - } - - /** - * @inheritdoc - */ - protected function elementType(): string - { - return $this->purchasableType; - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - if ($this->getElementId() === null) { - return; - } - - /** @var OrderQuery $query */ - $query->hasPurchasables([$this->getElementId()]); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - return Order::find() - ->id($element->id) - ->hasPurchasables([$this->getElementId()]) - ->exists(); - } - - public function getConfig(): array - { - return array_merge(parent::getConfig(), [ - 'purchasableType' => $this->purchasableType, - ]); - } - - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['purchasableType'], 'safe']; - - return $rules; - } - - /** - * @inheritdoc - */ - protected function inputHtml(): string - { - $id = 'purchasable-type'; - return Html::hiddenLabel($this->getLabel(), $id) . - Html::tag('div', - Cp::selectHtml([ - 'id' => $id, - 'name' => 'purchasableType', - 'options' => $this->_purchasableTypeOptions(), - 'value' => $this->purchasableType, - 'inputAttributes' => [ - 'hx' => [ - 'post' => UrlHelper::actionUrl('conditions/render'), - ], - ], - ]) . - parent::inputHtml(), - [ - 'class' => ['flex', 'flex-start'], - ] - ); - } - - - protected function selectionCondition(): ?ElementConditionInterface - { - return Craft::$app->getConditions()->createCondition(['class' => OrderCondition::class]); - } - - /** - * @return array - * @throws InvalidConfigException - */ - private function _purchasableTypeOptions(): array - { - $options = []; - - foreach (Plugin::getInstance()->getPurchasables()->getAllPurchasableElementTypes() as $elementType) { - /** @var string|ElementInterface $elementType */ - /** @phpstan-var class-string|ElementInterface $elementType */ - $options[] = [ - 'value' => $elementType, - 'label' => $elementType::displayName(), - ]; - } - - return $options; - } - - /** - * @inerhitdoc - */ - protected function elementSelectConfig(): array - { - return array_merge(parent::elementSelectConfig(), [ - 'showSiteMenu' => true, - ]); - } -} diff --git a/src/elements/conditions/orders/ItemSubtotalConditionRule.php b/src/elements/conditions/orders/ItemSubtotalConditionRule.php deleted file mode 100644 index b574e78bc3..0000000000 --- a/src/elements/conditions/orders/ItemSubtotalConditionRule.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @since 4.2.0 - * - * @property-read float|int $orderAttributeValue - */ -class ItemSubtotalConditionRule extends OrderCurrencyValuesAttributeConditionRule -{ - public string $orderAttribute = 'itemSubtotal'; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Item Subtotal'); - } -} diff --git a/src/elements/conditions/orders/ItemTotalConditionRule.php b/src/elements/conditions/orders/ItemTotalConditionRule.php deleted file mode 100644 index 3c6cbab538..0000000000 --- a/src/elements/conditions/orders/ItemTotalConditionRule.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @since 4.2.0 - * - * @property-read float|int $orderAttributeValue - */ -class ItemTotalConditionRule extends OrderCurrencyValuesAttributeConditionRule -{ - public string $orderAttribute = 'itemTotal'; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Item Total'); - } -} diff --git a/src/elements/conditions/orders/OrderCondition.php b/src/elements/conditions/orders/OrderCondition.php deleted file mode 100644 index 9f38630eb7..0000000000 --- a/src/elements/conditions/orders/OrderCondition.php +++ /dev/null @@ -1,56 +0,0 @@ - - * @since 4.0.0 - */ -class OrderCondition extends ElementCondition -{ - /** - * @inheritdoc - */ - public ?string $elementType = Order::class; - - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - return array_merge(parent::selectableConditionRules(), [ - DateOrderedConditionRule::class, - CompletedConditionRule::class, - CouponCodeConditionRule::class, - CustomerConditionRule::class, - HasAdminNoticesConditionRule::class, - PaidConditionRule::class, - HasPurchasableConditionRule::class, - ContainsPurchasablesConditionRule::class, - ItemSubtotalConditionRule::class, - ItemTotalConditionRule::class, - OrderStatusConditionRule::class, - OrderSiteConditionRule::class, - PaymentGatewayConditionRule::class, - ReferenceConditionRule::class, - ShippingMethodConditionRule::class, - TotalDiscountConditionRule::class, - TotalPaidConditionRule::class, - TotalPriceConditionRule::class, - TotalQtyConditionRule::class, - TotalTaxConditionRule::class, - TotalConditionRule::class, - TotalWeightConditionRule::class, - ]); - } -} diff --git a/src/elements/conditions/orders/OrderCurrencyValuesAttributeConditionRule.php b/src/elements/conditions/orders/OrderCurrencyValuesAttributeConditionRule.php deleted file mode 100644 index da52e79b67..0000000000 --- a/src/elements/conditions/orders/OrderCurrencyValuesAttributeConditionRule.php +++ /dev/null @@ -1,130 +0,0 @@ - - * @since 4.2.0 - * - * @method ElementConditionInterface|HasStoreInterface getCondition() - * @property-read float|int $orderAttributeValue - */ -abstract class OrderCurrencyValuesAttributeConditionRule extends MoneyFieldConditionRule -{ - /** - * @var string - */ - public string $orderAttribute = ''; - - /** - * @var Currency|null - */ - public ?Currency $currency = null; - - /** - * @var int|null - */ - public ?int $subUnit = null; - - public function __construct($config = []) - { - $this->setFieldUid('not-applicable'); - parent::__construct($config); - } - - /** - * @inheritdoc - */ - public function getGroupLabel(): ?string - { - return null; - } - - /** - * @inheritdoc - */ - public function setCondition(ConditionInterface $condition): void - { - parent::setCondition($condition); - - if ($this->getCondition() instanceof HasStoreInterface) { - $this->currency = $this->getCondition()->getStore()->getCurrency(); - } else { - /** @var Site|StoreBehavior|null $currentSite */ - $currentSite = Craft::$app->getSites()->getCurrentSite(); - - if ($currentSite->getBehavior(StoreBehavior::class)) { - $this->currency = $currentSite?->getStore()->getCurrency(); - } - } - - if ($this->currency) { - $this->subUnit = Plugin::getInstance()->getCurrencies()->getSubunitFor($this->currency); - } - } - - /** - * @inheritdoc - */ - protected function field(): FieldInterface - { - // Mock a Money field - $field = new Money(); - $field->currency = $this->currency?->getCode() ?? $field->currency; - - return $field; - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return [$this->orderAttribute]; - } - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return 'Label not implemented'; - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - return $this->matchValue($element->{$this->orderAttribute}); - } - - /** - * @inheritdoc - */ - public function modifyQuery(QueryInterface $query): void - { - $query->{$this->orderAttribute}($this->paramValue()); - } -} diff --git a/src/elements/conditions/orders/OrderSiteConditionRule.php b/src/elements/conditions/orders/OrderSiteConditionRule.php deleted file mode 100644 index c6b1f69b5b..0000000000 --- a/src/elements/conditions/orders/OrderSiteConditionRule.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @since 4.2.7 - */ -class OrderSiteConditionRule extends BaseMultiSelectConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Order Site'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['orderSiteId']; - } - - /** - * @inheritdoc - */ - protected function options(): array - { - return ArrayHelper::map(Craft::$app->getSites()->getAllSites(), 'id', 'name'); - } - - /** - * @inheritdoc - */ - public function modifyQuery(QueryInterface $query): void - { - /** @var OrderQuery $query */ - $query->orderSiteId($this->paramValue()); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Order $element */ - return $this->matchValue((string)$element->orderSiteId); - } -} diff --git a/src/elements/conditions/orders/OrderStatusConditionRule.php b/src/elements/conditions/orders/OrderStatusConditionRule.php deleted file mode 100644 index 69e3c9a6d9..0000000000 --- a/src/elements/conditions/orders/OrderStatusConditionRule.php +++ /dev/null @@ -1,76 +0,0 @@ - - * @since 4.2.0 - * - * @method array|string|null paramValue(?callable $normalizeValue = null) - */ -class OrderStatusConditionRule extends BaseMultiSelectConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Order Status'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['orderStatus']; - } - - /** - * @param ElementQueryInterface $query - * @return void - * @throws InvalidConfigException - */ - public function modifyQuery(ElementQueryInterface $query): void - { - $orderStatuses = Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses(); - - /** @var OrderQuery $query */ - $query->orderStatus($this->paramValue(fn(string $value) => ArrayHelper::firstWhere($orderStatuses, 'uid', $value)?->handle)); - } - - /** - * @param ElementInterface $element - * @return bool - * @throws InvalidConfigException - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Order $element */ - $orderStatusUid = $element->getOrderStatus()?->uid; - return $this->matchValue($orderStatusUid); - } - - protected function options(): array - { - return Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses()->mapWithKeys(fn($status) => [$status->uid => $status->name])->all(); - } -} diff --git a/src/elements/conditions/orders/OrderTextValuesAttributeConditionRule.php b/src/elements/conditions/orders/OrderTextValuesAttributeConditionRule.php deleted file mode 100644 index 0f94f88902..0000000000 --- a/src/elements/conditions/orders/OrderTextValuesAttributeConditionRule.php +++ /dev/null @@ -1,58 +0,0 @@ - - * @since 4.2.0 - * - * @property-read float|int $orderAttributeValue - */ -abstract class OrderTextValuesAttributeConditionRule extends BaseTextConditionRule implements ElementConditionRuleInterface -{ - public string $orderAttribute = ''; - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return [$this->orderAttribute]; - } - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return 'Label not implemented'; - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - return $this->matchValue($element->{$this->orderAttribute}); - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - $query->{$this->orderAttribute}($this->paramValue()); - } -} diff --git a/src/elements/conditions/orders/OrderValuesAttributeConditionRule.php b/src/elements/conditions/orders/OrderValuesAttributeConditionRule.php deleted file mode 100644 index 965003f8f7..0000000000 --- a/src/elements/conditions/orders/OrderValuesAttributeConditionRule.php +++ /dev/null @@ -1,58 +0,0 @@ - - * @since 4.0.0 - * - * @property-read float|int $orderAttributeValue - */ -abstract class OrderValuesAttributeConditionRule extends BaseNumberConditionRule implements ElementConditionRuleInterface -{ - public string $orderAttribute = ''; - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return [$this->orderAttribute]; - } - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return 'Label not implemented'; - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - return $this->matchValue($element->{$this->orderAttribute}); - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - $query->{$this->orderAttribute}($this->paramValue()); - } -} diff --git a/src/elements/conditions/orders/PaidConditionRule.php b/src/elements/conditions/orders/PaidConditionRule.php deleted file mode 100644 index afcf7c9bb8..0000000000 --- a/src/elements/conditions/orders/PaidConditionRule.php +++ /dev/null @@ -1,62 +0,0 @@ - - * @since 4.2.0 - */ -class PaidConditionRule extends BaseLightswitchConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Paid'); - } - - public function getExclusiveQueryParams(): array - { - return ['paid']; - } - - /** - * @param ElementQueryInterface $query - * @return void - */ - public function modifyQuery(ElementQueryInterface $query): void - { - /** @var OrderQuery $query */ - if ($this->value) { - $query->isPaid(); - } else { - $query->isUnpaid(); - } - } - - /** - * @param ElementInterface $element - * @return bool - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Order $element */ - return $this->value ? $element->getIsPaid() : $element->getIsUnpaid(); - } -} diff --git a/src/elements/conditions/orders/PaymentGatewayConditionRule.php b/src/elements/conditions/orders/PaymentGatewayConditionRule.php deleted file mode 100644 index 0c708e7c09..0000000000 --- a/src/elements/conditions/orders/PaymentGatewayConditionRule.php +++ /dev/null @@ -1,136 +0,0 @@ - - * @since 5.3.0 - */ -class PaymentGatewayConditionRule extends BaseMultiSelectConditionRule implements ElementConditionRuleInterface -{ - /** - * @var string|null Legacy single value property for backwards compatibility - * @deprecated Use getValues() instead - */ - public $value; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Payment Gateway'); - } - - /** - * @inheritdoc - */ - public function getConfig(): array - { - $config = parent::getConfig(); - - // For backwards compatibility: if there's a legacy 'value' property, convert it to 'values' - if (isset($config['value']) && !isset($config['values'])) { - $config['values'] = [$config['value']]; - unset($config['value']); - } - - return $config; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - - // For backwards compatibility: accept 'value' property and convert to 'values' - $rules[] = [['value'], 'safe']; - - return $rules; - } - - /** - * @inheritdoc - */ - public function setAttributes($values, $safeOnly = true): void - { - // For backwards compatibility: convert single 'value' to 'values' array - if (isset($values['value']) && !isset($values['values'])) { - $values['values'] = is_array($values['value']) ? $values['value'] : [$values['value']]; - unset($values['value']); - } - - parent::setAttributes($values, $safeOnly); - } - - /** - * Returns the single value for backwards compatibility - * @deprecated Use getValues() instead - * @return string|null - */ - public function getValue(): ?string - { - $values = $this->getValues(); - return !empty($values) ? reset($values) : null; - } - - /** - * Sets a single value for backwards compatibility - * @deprecated Use setValues() instead - * @param string|null $value - */ - public function setValue(?string $value): void - { - $this->setValues($value ? [$value] : []); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['gatewayId']; - } - - /** - * @inheritdoc - */ - protected function options(): array - { - return Plugin::getInstance()->getGateways()->getAllGateways()->mapWithKeys(fn($gateway) => [$gateway->uid => $gateway->name])->all(); - } - - /** - * @inheritdoc - */ - public function modifyQuery(QueryInterface $query): void - { - $gateways = Plugin::getInstance()->getGateways()->getAllGateways(); - - /** @var OrderQuery $query */ - $query->gatewayId($this->paramValue(fn($uid) => $gateways->firstWhere('uid', $uid)?->id)); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Order $element */ - $gatewayUid = $element->getGateway()?->uid ?? ''; - return $this->matchValue($gatewayUid); - } -} diff --git a/src/elements/conditions/orders/ReferenceConditionRule.php b/src/elements/conditions/orders/ReferenceConditionRule.php deleted file mode 100644 index 28bd84bcf6..0000000000 --- a/src/elements/conditions/orders/ReferenceConditionRule.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @since 4.2.0 - */ -class ReferenceConditionRule extends OrderTextValuesAttributeConditionRule -{ - public string $orderAttribute = 'reference'; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Reference'); - } -} diff --git a/src/elements/conditions/orders/ShippingAddressZoneConditionRule.php b/src/elements/conditions/orders/ShippingAddressZoneConditionRule.php deleted file mode 100644 index cbf8c0e9e5..0000000000 --- a/src/elements/conditions/orders/ShippingAddressZoneConditionRule.php +++ /dev/null @@ -1,86 +0,0 @@ - - * @since 5.0.0 - */ -class ShippingAddressZoneConditionRule extends BaseMultiSelectConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Shipping Address Zone'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['shippingZone']; - } - - /** - * @inheritdoc - */ - protected function options(): array - { - /** @var ShippingRuleOrderCondition $condition */ - $condition = $this->getCondition(); - - return Plugin::getInstance()->getShippingZones()->getAllShippingZones($condition->storeId)->mapWithKeys(fn(ShippingAddressZone $zone) => [$zone->id => $zone->name])->all(); - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - throw new NotSupportedException('Shipping Address Zone condition rule does not support queries'); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - /** @var ShippingRuleOrderCondition $condition */ - $condition = $this->getCondition(); - /** @var Order $element */ - $shippingAddress = $element->getShippingAddress() ?? $element->getEstimatedShippingAddress(); - - if (!$shippingAddress) { - return false; - } - - /** @var ShippingAddressZone[] $shippingZones */ - $shippingZones = Plugin::getInstance()->getShippingZones()->getAllShippingZones($condition->storeId)->whereIn('id', $this->getValues())->all(); - - // Start on `true` or `false` depending on the operator - $match = $this->operator !== self::OPERATOR_IN; - foreach ($shippingZones as $shippingZone) { - if ($shippingZone->getCondition()->matchElement($shippingAddress)) { - $match = $this->operator === self::OPERATOR_IN; - break; - } - } - - return $match; - } -} diff --git a/src/elements/conditions/orders/ShippingMethodConditionRule.php b/src/elements/conditions/orders/ShippingMethodConditionRule.php deleted file mode 100644 index c20ed7407a..0000000000 --- a/src/elements/conditions/orders/ShippingMethodConditionRule.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @since 4.2.0 - */ -class ShippingMethodConditionRule extends BaseMultiSelectConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Shipping Method'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return []; - } - - /** - * @inheritdoc - */ - protected function options(): array - { - return Plugin::getInstance()->getShippingMethods()->getAllShippingMethods()->mapWithKeys(fn($method) => [$method->handle => $method->name])->all(); - } - - /** - * @inheritdoc - */ - public function modifyQuery(QueryInterface $query): void - { - /** @var OrderQuery $query */ - $query->shippingMethodHandle($this->paramValue()); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Order $element */ - return $this->matchValue($element->shippingMethodHandle); - } -} diff --git a/src/elements/conditions/orders/ShippingMethodOrderCondition.php b/src/elements/conditions/orders/ShippingMethodOrderCondition.php deleted file mode 100644 index 85934b4bb5..0000000000 --- a/src/elements/conditions/orders/ShippingMethodOrderCondition.php +++ /dev/null @@ -1,84 +0,0 @@ - - * @since 5.0.0 - */ -class ShippingMethodOrderCondition extends OrderCondition implements HasStoreInterface -{ - use StoreTrait; - - /** - * @inheritdoc - */ - public ?string $elementType = Order::class; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['storeId'], 'safe']; - - return $rules; - } - - /** - * @return array - */ - protected function config(): array - { - return array_merge(parent::config(), $this->toArray(['storeId'])); - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - throw new NotSupportedException('Shipping Method Order Condition does not support queries'); - } - - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - $ruleTypes = parent::selectableConditionRules(); - - - foreach ($ruleTypes as $key => $ruleType) { - if (in_array($ruleType, [ - CompletedConditionRule::class, - DateOrderedConditionRule::class, - PaidConditionRule::class, - OrderStatusConditionRule::class, - ShippingMethodConditionRule::class, - TotalPaidConditionRule::class, - ])) { - unset($ruleTypes[$key]); - } - } - - $ruleTypes[] = DiscountedItemSubtotalConditionRule::class; - $ruleTypes[] = ShippingAddressZoneConditionRule::class; - - return $ruleTypes; - } -} diff --git a/src/elements/conditions/orders/ShippingRuleOrderCondition.php b/src/elements/conditions/orders/ShippingRuleOrderCondition.php deleted file mode 100644 index 1f7e37d584..0000000000 --- a/src/elements/conditions/orders/ShippingRuleOrderCondition.php +++ /dev/null @@ -1,84 +0,0 @@ - - * @since 5.0.0 - */ -class ShippingRuleOrderCondition extends OrderCondition implements HasStoreInterface -{ - use StoreTrait; - - /** - * @inheritdoc - */ - public ?string $elementType = Order::class; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['storeId'], 'safe']; - - return $rules; - } - - /** - * @return array - */ - protected function config(): array - { - return array_merge(parent::config(), $this->toArray(['storeId'])); - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - throw new NotSupportedException('Shipping Rule Order Condition does not support queries'); - } - - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - $ruleTypes = parent::selectableConditionRules(); - - - foreach ($ruleTypes as $key => $ruleType) { - if (in_array($ruleType, [ - CompletedConditionRule::class, - DateOrderedConditionRule::class, - PaidConditionRule::class, - OrderStatusConditionRule::class, - ShippingMethodConditionRule::class, - TotalPaidConditionRule::class, - ])) { - unset($ruleTypes[$key]); - } - } - - $ruleTypes[] = DiscountedItemSubtotalConditionRule::class; - $ruleTypes[] = ShippingAddressZoneConditionRule::class; - - return $ruleTypes; - } -} diff --git a/src/elements/conditions/orders/TotalConditionRule.php b/src/elements/conditions/orders/TotalConditionRule.php deleted file mode 100644 index d15adf0d02..0000000000 --- a/src/elements/conditions/orders/TotalConditionRule.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 4.2.0 - * - * @property-read float|int $orderAttributeValue - */ -class TotalConditionRule extends OrderCurrencyValuesAttributeConditionRule -{ - public string $orderAttribute = 'total'; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Total'); - } -} diff --git a/src/elements/conditions/orders/TotalDiscountConditionRule.php b/src/elements/conditions/orders/TotalDiscountConditionRule.php deleted file mode 100644 index 5ba674a3a8..0000000000 --- a/src/elements/conditions/orders/TotalDiscountConditionRule.php +++ /dev/null @@ -1,90 +0,0 @@ - - * @since 4.2.0 - * - * @property-read float|int $orderAttributeValue - */ -class TotalDiscountConditionRule extends OrderCurrencyValuesAttributeConditionRule -{ - public string $orderAttribute = 'totalDiscount'; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Total Discount'); - } - - /** - * @inheritdoc - */ - protected function operatorLabel(string $operator): string - { - return match ($operator) { - self::OPERATOR_EQ => Craft::t('app', 'equals'), - self::OPERATOR_NE => Craft::t('app', 'does not equal'), - self::OPERATOR_GT => Craft::t('app', 'is less than'), - self::OPERATOR_GTE => Craft::t('app', 'is less than or equals'), - self::OPERATOR_LT => Craft::t('app', 'is greater than'), - self::OPERATOR_LTE => Craft::t('app', 'is greater than or equals'), - default => $operator, - }; - } - - /** - * @inheritdoc - */ - protected function paramValue(): ?string - { - if ($this->value === '') { - return null; - } - - $value = $this->value; - if (is_numeric($value)) { - $value *= -1; - } - - $value = Db::escapeParam($value); - - return "$this->operator $value"; - } - - protected function matchValue(mixed $value): bool - { - if ($this->value === '') { - return true; - } - - $ruleValue = $this->value; - if (is_numeric($ruleValue)) { - $ruleValue *= -1; - } - - return match ($this->operator) { - self::OPERATOR_EQ => $value == $ruleValue, - self::OPERATOR_NE => $value != $ruleValue, - self::OPERATOR_LT => $value < $ruleValue, - self::OPERATOR_LTE => $value <= $ruleValue, - self::OPERATOR_GT => $value > $ruleValue, - self::OPERATOR_GTE => $value >= $ruleValue, - default => throw new InvalidConfigException("Invalid operator: $this->operator"), - }; - } -} diff --git a/src/elements/conditions/orders/TotalPaidConditionRule.php b/src/elements/conditions/orders/TotalPaidConditionRule.php deleted file mode 100644 index 2208b03eee..0000000000 --- a/src/elements/conditions/orders/TotalPaidConditionRule.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 4.2.0 - * - * @property-read float|int $orderAttributeValue - */ -class TotalPaidConditionRule extends OrderCurrencyValuesAttributeConditionRule -{ - public string $orderAttribute = 'totalPaid'; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Total Paid'); - } -} diff --git a/src/elements/conditions/orders/TotalPriceConditionRule.php b/src/elements/conditions/orders/TotalPriceConditionRule.php deleted file mode 100644 index 97852eb460..0000000000 --- a/src/elements/conditions/orders/TotalPriceConditionRule.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 4.0.0 - * - * @property-read float|int $orderAttributeValue - */ -class TotalPriceConditionRule extends OrderCurrencyValuesAttributeConditionRule -{ - public string $orderAttribute = 'totalPrice'; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Total Price'); - } -} diff --git a/src/elements/conditions/orders/TotalQtyConditionRule.php b/src/elements/conditions/orders/TotalQtyConditionRule.php deleted file mode 100644 index 1711291e5c..0000000000 --- a/src/elements/conditions/orders/TotalQtyConditionRule.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @since 4.2.0 - */ -class TotalQtyConditionRule extends OrderValuesAttributeConditionRule -{ - public string $orderAttribute = 'totalQty'; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Total Qty'); - } -} diff --git a/src/elements/conditions/orders/TotalTaxConditionRule.php b/src/elements/conditions/orders/TotalTaxConditionRule.php deleted file mode 100644 index bc779e746d..0000000000 --- a/src/elements/conditions/orders/TotalTaxConditionRule.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 4.2.0 - * - * @property-read float|int $orderAttributeValue - */ -class TotalTaxConditionRule extends OrderCurrencyValuesAttributeConditionRule -{ - public string $orderAttribute = 'totalTax'; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Total Tax'); - } -} diff --git a/src/elements/conditions/orders/TotalWeightConditionRule.php b/src/elements/conditions/orders/TotalWeightConditionRule.php deleted file mode 100644 index f33e7a26c6..0000000000 --- a/src/elements/conditions/orders/TotalWeightConditionRule.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @since 4.2.0 - */ -class TotalWeightConditionRule extends OrderValuesAttributeConditionRule -{ - public string $orderAttribute = 'totalWeight'; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Total Weight'); - } -} diff --git a/src/elements/conditions/products/CatalogPricingRuleProductCondition.php b/src/elements/conditions/products/CatalogPricingRuleProductCondition.php deleted file mode 100644 index e2828dfd28..0000000000 --- a/src/elements/conditions/products/CatalogPricingRuleProductCondition.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @since 5.1.0 - */ -class CatalogPricingRuleProductCondition extends ProductCondition -{ - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - $rules = parent::selectableConditionRules(); - - return ArrayHelper::withoutValue($rules, ProductVariantHasUnlimitedStockConditionRule::class); - } -} diff --git a/src/elements/conditions/products/ProductCondition.php b/src/elements/conditions/products/ProductCondition.php deleted file mode 100644 index 169d8cade2..0000000000 --- a/src/elements/conditions/products/ProductCondition.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @since 4.0.0 - */ -class ProductCondition extends ElementCondition -{ - /** - * @inheritdoc - */ - public ?string $elementType = Product::class; - - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - return array_merge(parent::selectableConditionRules(), [ - ProductTypeConditionRule::class, - ProductVariantSearchConditionRule::class, - ProductVariantSkuConditionRule::class, - ProductVariantStockConditionRule::class, - ProductVariantHasUnlimitedStockConditionRule::class, - ProductVariantPriceConditionRule::class, - ]); - } -} diff --git a/src/elements/conditions/products/ProductTypeConditionRule.php b/src/elements/conditions/products/ProductTypeConditionRule.php deleted file mode 100644 index b93a2085f7..0000000000 --- a/src/elements/conditions/products/ProductTypeConditionRule.php +++ /dev/null @@ -1,78 +0,0 @@ - - * @since 4.3.0 - */ -class ProductTypeConditionRule extends BaseMultiSelectConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Product Type'); - } - - /** - * @return array - */ - protected function options(): array - { - return collect(Plugin::getInstance()->getProductTypes()->getAllProductTypes()) - ->map(fn(ProductType $productType) => ['value' => $productType->uid, 'label' => $productType->name]) - ->all(); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['type']; - } - - /** - * @throws InvalidConfigException - */ - public function modifyQuery(ElementQueryInterface $query): void - { - $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); - - /** @var string[] $value */ - $value = $this->paramValue(fn(string $value) => ArrayHelper::firstWhere($productTypes, 'uid', $value)?->handle); - - /** @var ProductQuery $query */ - $query->type($value); - } - - /** - * @throws InvalidConfigException - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Product $element */ - return $this->matchValue($element->getType()->uid); - } -} diff --git a/src/elements/conditions/products/ProductVariantHasUnlimitedStockConditionRule.php b/src/elements/conditions/products/ProductVariantHasUnlimitedStockConditionRule.php deleted file mode 100644 index 8ddb457ca5..0000000000 --- a/src/elements/conditions/products/ProductVariantHasUnlimitedStockConditionRule.php +++ /dev/null @@ -1,71 +0,0 @@ - - * @since 4.3.0 - * @deprecated 5.0.0 - */ -class ProductVariantHasUnlimitedStockConditionRule extends BaseLightswitchConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Variant Has Untracked Stock'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['variantStock']; - } - - /** - * @param ElementQueryInterface $query - */ - public function modifyQuery(ElementQueryInterface $query): void - { - $variantQuery = Variant::find(); - $variantQuery->select(['commerce_variants.primaryOwnerId as id']); - $variantQuery->inventoryTracked(!$this->value); - - /** @var ProductQuery $query */ - $query->andWhere(['elements.id' => $variantQuery]); - } - - /** - * @param Product $element - */ - public function matchElement(ElementInterface $element): bool - { - foreach ($element->getVariants() as $variant) { - if ($this->matchValue(!$variant->inventoryTracked)) { - // Skip out early if we have a match - return true; - } - } - - return false; - } -} diff --git a/src/elements/conditions/products/ProductVariantInventoryTrackedConditionRule.php b/src/elements/conditions/products/ProductVariantInventoryTrackedConditionRule.php deleted file mode 100644 index fc8d73692f..0000000000 --- a/src/elements/conditions/products/ProductVariantInventoryTrackedConditionRule.php +++ /dev/null @@ -1,70 +0,0 @@ - - * @since 5.0.0 - */ -class ProductVariantInventoryTrackedConditionRule extends BaseLightswitchConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Variant Tracks Stock'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['variantStock']; - } - - /** - * @param ElementQueryInterface $query - */ - public function modifyQuery(ElementQueryInterface $query): void - { - $variantQuery = Variant::find(); - $variantQuery->select(['commerce_variants.primaryOwnerId as id']); - $variantQuery->inventoryTracked($this->value); - - /** @var ProductQuery $query */ - $query->andWhere(['elements.id' => $variantQuery]); - } - - /** - * @param Product $element - */ - public function matchElement(ElementInterface $element): bool - { - foreach ($element->getVariants() as $variant) { - if ($this->matchValue($variant->inventoryTracked)) { - // Skip out early if we have a match - return true; - } - } - - return false; - } -} diff --git a/src/elements/conditions/products/ProductVariantPriceConditionRule.php b/src/elements/conditions/products/ProductVariantPriceConditionRule.php deleted file mode 100644 index c1ffdec2e7..0000000000 --- a/src/elements/conditions/products/ProductVariantPriceConditionRule.php +++ /dev/null @@ -1,70 +0,0 @@ - - * @since 4.3.0 - */ -class ProductVariantPriceConditionRule extends BaseNumberConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Variant Price'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['variantPrice']; - } - - /** - * @param ElementQueryInterface $query - */ - public function modifyQuery(ElementQueryInterface $query): void - { - $variantQuery = Variant::find(); - $variantQuery->select(['commerce_variants.primaryOwnerId as id']); - $variantQuery->price($this->paramValue()); - - /** @var ProductQuery $query */ - $query->andWhere(['elements.id' => $variantQuery]); - } - - /** - * @param Product $element - */ - public function matchElement(ElementInterface $element): bool - { - foreach ($element->getVariants() as $variant) { - if ($this->matchValue($variant->price)) { - // Skip out early if we have a match - return true; - } - } - - return false; - } -} diff --git a/src/elements/conditions/products/ProductVariantSearchConditionRule.php b/src/elements/conditions/products/ProductVariantSearchConditionRule.php deleted file mode 100644 index a8f2eacdf4..0000000000 --- a/src/elements/conditions/products/ProductVariantSearchConditionRule.php +++ /dev/null @@ -1,98 +0,0 @@ - - * @since 4.7.0 - */ -class ProductVariantSearchConditionRule extends BaseTextConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Variant Search'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return []; - } - - /** - * @inheritdoc - */ - protected function operators(): array - { - return []; - } - - /** - * Returns the raw search value. - * - * Note we can't use [[paramValue()]] here because it prepends the operator - * (e.g. `=`) intended for [[\craft\helpers\Db::parseParam()]], which would - * corrupt the value once it's passed to [[\craft\elements\db\ElementQuery::search()]]. - * - * @return string - */ - private function searchValue(): string - { - return trim((string)$this->value); - } - - /** - * @param ElementQueryInterface $query - */ - public function modifyQuery(ElementQueryInterface $query): void - { - $variantQuery = Variant::find(); - $variantQuery->select(['commerce_variants.primaryOwnerId as id']); - $variantQuery->search($this->searchValue()); - - /** @var ProductQuery $query */ - $query->andWhere(['elements.id' => $variantQuery]); - } - - /** - * @param Product $element - * @return bool - * @throws InvalidConfigException - */ - public function matchElement(ElementInterface $element): bool - { - $variantIds = $element->getVariants()->pluck('id')->all(); - if (empty($variantIds)) { - return false; - } - - // Perform a variant query search to ensure it is the same process as `modifyQuery` - $variantQuery = Variant::find(); - $variantQuery->search($this->searchValue()); - $variantQuery->id($variantIds); - - return $variantQuery->count() > 0; - } -} diff --git a/src/elements/conditions/products/ProductVariantSkuConditionRule.php b/src/elements/conditions/products/ProductVariantSkuConditionRule.php deleted file mode 100644 index 18cbfd6216..0000000000 --- a/src/elements/conditions/products/ProductVariantSkuConditionRule.php +++ /dev/null @@ -1,70 +0,0 @@ - - * @since 4.3.0 - */ -class ProductVariantSkuConditionRule extends BaseTextConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Variant SKU'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return []; - } - - /** - * @param ElementQueryInterface $query - */ - public function modifyQuery(ElementQueryInterface $query): void - { - $variantQuery = Variant::find(); - $variantQuery->select(['commerce_variants.primaryOwnerId as id']); - $variantQuery->sku($this->paramValue()); - - /** @var ProductQuery $query */ - $query->andWhere(['elements.id' => $variantQuery]); - } - - /** - * @param Product $element - */ - public function matchElement(ElementInterface $element): bool - { - foreach ($element->getVariants() as $variant) { - if ($this->matchValue($variant->sku)) { - // Skip out early if we have a match - return true; - } - } - - return false; - } -} diff --git a/src/elements/conditions/products/ProductVariantStockConditionRule.php b/src/elements/conditions/products/ProductVariantStockConditionRule.php deleted file mode 100644 index 0cb9bd5183..0000000000 --- a/src/elements/conditions/products/ProductVariantStockConditionRule.php +++ /dev/null @@ -1,78 +0,0 @@ - - * @since 4.3.0 - */ -class ProductVariantStockConditionRule extends BaseNumberConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Variant Stock'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['variantStock']; - } - - /** - * @param ElementQueryInterface $query - */ - public function modifyQuery(ElementQueryInterface $query): void - { - /** @var VariantQuery $variantQuery */ - $variantQuery = Variant::find(); - $variantQuery->select(['commerce_variants.primaryOwnerId as id']); - $variantQuery->inventoryTracked(true); - $variantQuery->stock($this->paramValue()); - - /** @var ProductQuery $query */ - $query->andWhere(['elements.id' => $variantQuery]); - } - - /** - * @param Product $element - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Variant $variant */ - foreach ($element->getVariants() as $variant) { - if (!$variant::hasInventory()) { - return true; - } - - if ($variant->inventoryTracked === true && $this->matchValue($variant->getStock())) { - // Skip out early if we have a match - return true; - } - } - - return false; - } -} diff --git a/src/elements/conditions/purchasables/CatalogPricingCondition.php b/src/elements/conditions/purchasables/CatalogPricingCondition.php deleted file mode 100644 index 3a3c2a0fe9..0000000000 --- a/src/elements/conditions/purchasables/CatalogPricingCondition.php +++ /dev/null @@ -1,152 +0,0 @@ - - * @since 5.0.0 - */ -class CatalogPricingCondition extends BaseCondition -{ - /** - * @var string[] The query params that available rules shouldn’t compete with. - */ - public array $queryParams = []; - - /** - * @var bool - */ - public bool $allPrices = false; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['allPrices'], 'safe']; - - return $rules; - } - - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - return [ - CatalogPricingPurchasableConditionRule::class, - CatalogPricingCustomerConditionRule::class, - ]; - } - - /** - * @inheritdoc - */ - protected function isConditionRuleSelectable(ConditionRuleInterface $rule): bool - { - if (!parent::isConditionRuleSelectable($rule)) { - return false; - } - - // Make sure the rule doesn't conflict with the existing params - $queryParams = array_merge($this->queryParams); - foreach ($this->getConditionRules() as $existingRule) { - /** @var CatalogPricingConditionRuleInterface $existingRule */ - array_push($queryParams, ...$existingRule->getExclusiveQueryParams()); - } - - $queryParams = array_flip($queryParams); - - if (method_exists($rule, 'getExclusiveQueryParams')) { - foreach ($rule->getExclusiveQueryParams() as $param) { - if (isset($queryParams[$param])) { - return false; - } - } - } - - return true; - } - - /** - * @inheritdoc - */ - public function getConfig(): array - { - $config = parent::getConfig(); - $config['allPrices'] = $this->allPrices; - - return $config; - } - - /** - * @inheritdoc - */ - public function modifyQuery(Query $query): void - { - $catalogPricingRuleIdWhere = ['or']; - - // If we are looking for all prices, we don't need to worry about the user's table - if (!$this->allPrices) { - $catalogPricingRuleIdWhere[] = ['catalogPricingRuleId' => null]; - $catalogPricingRuleIdWhere[] = ['catalogPricingRuleId' => (new Query()) - ->select(['cpr.id as cprid']) - ->from([Table::CATALOG_PRICING_RULES . ' cpr']) - ->leftJoin([Table::CATALOG_PRICING_RULES_USERS . ' cpru'], '[[cpr.id]] = [[cpru.catalogPricingRuleId]]') - ->where(['[[cpru.id]]' => null]) - ->groupBy(['[[cpr.id]]']), - ]; - } - - $rules = $this->getConditionRules(); - - if ($customerRule = ArrayHelper::firstWhere($rules, fn(ConditionRuleInterface $rule) => $rule instanceof CatalogPricingCustomerConditionRule)) { - /** @var CatalogPricingCustomerConditionRule $customerRule */ - // Sub query to figure out which catalog pricing rules are using user conditions - $catalogPricingRuleIdWhere[] = ['catalogPricingRuleId' => (new Query()) - ->select(['cpr.id as cprid']) - ->from([Table::CATALOG_PRICING_RULES . ' cpr']) - ->leftJoin([Table::CATALOG_PRICING_RULES_USERS . ' cpru'], '[[cpr.id]] = [[cpru.catalogPricingRuleId]]') - ->where(['[[cpru.userId]]' => $customerRule->customerId]) - ->andWhere(['not', ['[[cpru.id]]' => null]]) - ->groupBy(['[[cpr.id]]']), - ]; - - foreach ($rules as $key => $rule) { - if ($rule instanceof CatalogPricingCustomerConditionRule) { - unset($rules[$key]); - - // Can break here because there is only one customer condition rule - break; - } - } - } - - // Deal with all prices and filtering by customer - if (count($catalogPricingRuleIdWhere) > 1) { - $query->andWhere($catalogPricingRuleIdWhere); - } - - // Apply the rest of the rules - foreach ($rules as $rule) { - /** @var CatalogPricingConditionRuleInterface $rule */ - $rule->modifyQuery($query); - } - } -} diff --git a/src/elements/conditions/purchasables/CatalogPricingCustomerConditionRule.php b/src/elements/conditions/purchasables/CatalogPricingCustomerConditionRule.php deleted file mode 100644 index cb14a371b0..0000000000 --- a/src/elements/conditions/purchasables/CatalogPricingCustomerConditionRule.php +++ /dev/null @@ -1,99 +0,0 @@ - - * @since 5.0.0 - */ -class CatalogPricingCustomerConditionRule extends BaseConditionRule implements CatalogPricingConditionRuleInterface -{ - /** - * @var int|null - */ - public ?int $customerId = null; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Customer'); - } - - /** - * @inheritdoc - */ - public function getConfig(): array - { - return array_merge(parent::getConfig(), [ - 'customerId' => $this->customerId, - ]); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['customerId'], 'safe']; - - return $rules; - } - - /** - * @inheritdoc - */ - protected function inputHtml(): string - { - return Html::hiddenLabel($this->getLabel(), 'customer') . - Html::tag('div', - Cp::elementSelectHtml([ - 'name' => 'customerId', - 'elements' => array_filter([$this->customerId]), - 'elementType' => User::class, - 'sources' => null, - 'criteria' => null, - 'single' => true, - ]), - [ - 'class' => ['flex', 'flex-start'], - ] - ); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['customer']; - } - - /** - * @inheritdoc - */ - public function modifyQuery(Query $query): void - { - return; - - // Doesn't modify the query as the modification - // of the query happens in `CatalogPricingCondition::modifyQuery()` for this rule - } -} diff --git a/src/elements/conditions/purchasables/CatalogPricingPurchasableConditionRule.php b/src/elements/conditions/purchasables/CatalogPricingPurchasableConditionRule.php deleted file mode 100644 index 23a7b90853..0000000000 --- a/src/elements/conditions/purchasables/CatalogPricingPurchasableConditionRule.php +++ /dev/null @@ -1,151 +0,0 @@ - - * @since 5.0.0 - */ -class CatalogPricingPurchasableConditionRule extends BaseConditionRule implements CatalogPricingConditionRuleInterface -{ - /** - * @var array|null - * @see getElementIds() - * @see setElementIds - */ - private ?array $_elementIds = null; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Purchasable'); - } - - /** - * @param $value - * @return void - */ - public function setElementIds($value): void - { - $this->_elementIds = $value; - } - - /** - * @return array|null - */ - public function getElementIds(): ?array - { - if ($this->_elementIds === null) { - return null; - } - - $elementIds = []; - foreach ($this->_elementIds as $ids) { - $elementIds = array_merge($elementIds, $ids); - } - - return $elementIds; - } - - /** - * @inheritdoc - */ - public function getConfig(): array - { - return array_merge(parent::getConfig(), [ - 'elementIds' => $this->_elementIds, - ]); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['elementIds'], 'safe']; - - return $rules; - } - - /** - * @inheritdoc - */ - protected function inputHtml(): string - { - $id = 'purchasable'; - - $html = ''; - foreach (Plugin::getInstance()->getPurchasables()->getAllPurchasableElementTypes() as $purchasableType) { - /** @var PurchasableInterface|string $purchasableType */ - $elements = null; - if (!empty($this->_elementIds) && isset($this->_elementIds[$purchasableType]) && !empty($this->_elementIds[$purchasableType])) { - $elements = $purchasableType::find() - ->id($this->_elementIds[$purchasableType]) - ->status(null) - ->all(); - } - - $html .= Html::tag('div', - Html::beginTag('div') . - Html::tag('strong', $purchasableType::displayName()) . - Html::endTag('div') . - Cp::elementSelectHtml([ - 'name' => Html::namespaceInputName($purchasableType, 'elementIds'), - 'elements' => $elements, - 'elementType' => $purchasableType, - 'sources' => null, - 'criteria' => null, - 'single' => false, - ]) - ); - } - - return Html::hiddenLabel($this->getLabel(), $id) . - Html::tag('div', - $html, - [ - 'class' => ['flex', 'flex-start'], - ] - ); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['id']; - } - - /** - * @inheritdoc - */ - public function modifyQuery(Query $query): void - { - $ids = $this->getElementIds(); - if ($ids === null) { - return; - } - - $query->andWhere(['purchasableId' => $ids]); - } -} diff --git a/src/elements/conditions/purchasables/CatalogPricingRulePurchasableCategoryConditionRule.php b/src/elements/conditions/purchasables/CatalogPricingRulePurchasableCategoryConditionRule.php deleted file mode 100644 index dd56127f86..0000000000 --- a/src/elements/conditions/purchasables/CatalogPricingRulePurchasableCategoryConditionRule.php +++ /dev/null @@ -1,168 +0,0 @@ - - * @since 5.0.0 - */ -class CatalogPricingRulePurchasableCategoryConditionRule extends BaseConditionRule implements ElementConditionRuleInterface -{ - public const CATEGORY_RELATIONSHIP_TYPE_SOURCE = 'sourceElement'; - public const CATEGORY_RELATIONSHIP_TYPE_TARGET = 'targetElement'; - public const CATEGORY_RELATIONSHIP_TYPE_BOTH = 'element'; - - /** - * @var string - */ - public string $categoryRelationshipType = self::CATEGORY_RELATIONSHIP_TYPE_BOTH; - - /** - * @var array|null - */ - public ?array $elementIds = null; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Purchasable Categories'); - } - - - /** - * @inheritdoc - */ - public function getConfig(): array - { - return array_merge(parent::getConfig(), [ - 'elementIds' => $this->elementIds, - 'categoryRelationshipType' => $this->categoryRelationshipType, - ]); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['elementIds', 'categoryRelationshipType'], 'safe']; - - return $rules; - } - - /** - * @inheritdoc - */ - protected function inputHtml(): string - { - $id = 'cpr-purchasable-category'; - - $elements = !empty($this->elementIds) ? Category::find()->id($this->elementIds)->all() : []; - return Html::hiddenLabel($this->getLabel(), $id) . - Html::tag('div', - Html::tag('div', - Cp::elementSelectHtml([ - 'name' => 'elementIds', - 'elements' => $elements, - 'elementType' => Category::class, - 'sources' => null, - 'criteria' => null, - 'single' => false, - ]) - ), - [ - 'class' => ['flex', 'flex-start'], - ] - ) . - Html::tag('div', - Html::a(Craft::t('app', 'Advanced'), null, [ - 'class' => array_filter(['fieldtoggle', $this->categoryRelationshipType !== self::CATEGORY_RELATIONSHIP_TYPE_BOTH ? 'expanded' : '']), - 'data-target' => 'category-relationship-type-advanced', - ]) . - Html::tag('div', - Cp::selectHtml([ - 'id' => 'categoryRelationshipType', - 'name' => 'categoryRelationshipType', - 'label' => Craft::t('commerce', 'Categories Relationship Type'), - 'instructions' => Craft::t('commerce', 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).', [ - 'link' => 'https://craftcms.com/docs/4.x/relations.html#terminology', - ]), - 'options' => [ - self::CATEGORY_RELATIONSHIP_TYPE_SOURCE => Craft::t('commerce', 'Source - The purchasable relationship field is on the category'), - self::CATEGORY_RELATIONSHIP_TYPE_TARGET => Craft::t('commerce', 'Target - The category relationship field is on the purchasable'), - self::CATEGORY_RELATIONSHIP_TYPE_BOTH => Craft::t('commerce', 'Either (Default) - The relationship field is on the purchasable or the category'), - ], - 'value' => $this->categoryRelationshipType, - ]), - [ - 'class' => $this->categoryRelationshipType === self::CATEGORY_RELATIONSHIP_TYPE_BOTH ? 'hidden' : '', - 'id' => 'category-relationship-type-advanced', - ] - ), - ['style' => ['width' => '100%']] - ) - ; - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return []; - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - if ($this->elementIds === null) { - return; - } - - $query->andRelatedTo([$this->categoryRelationshipType => $this->elementIds]); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - if ($this->elementIds === null) { - return true; - } - - return Purchasable::find() - ->id($element->id ?: false) - ->site('*') - ->drafts($element->getIsDraft()) - ->provisionalDrafts($element->isProvisionalDraft) - ->revisions($element->getIsRevision()) - ->status(null) - ->relatedTo([$this->categoryRelationshipType => $this->elementIds]) - ->exists(); - } -} diff --git a/src/elements/conditions/purchasables/CatalogPricingRulePurchasableCondition.php b/src/elements/conditions/purchasables/CatalogPricingRulePurchasableCondition.php deleted file mode 100644 index 1d8566dc69..0000000000 --- a/src/elements/conditions/purchasables/CatalogPricingRulePurchasableCondition.php +++ /dev/null @@ -1,38 +0,0 @@ - - * @since 5.0.0 - */ -class CatalogPricingRulePurchasableCondition extends ElementCondition -{ - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - $types = array_filter(parent::selectableConditionRules(), static fn($type) => !in_array($type, [ - SiteConditionRule::class, - ], true)); - - $types[] = PurchasableConditionRule::class; - $types[] = SkuConditionRule::class; - $types[] = PurchasableTypeConditionRule::class; - $types[] = CatalogPricingRulePurchasableCategoryConditionRule::class; - - return $types; - } -} diff --git a/src/elements/conditions/purchasables/PurchasableConditionRule.php b/src/elements/conditions/purchasables/PurchasableConditionRule.php deleted file mode 100644 index c118f96465..0000000000 --- a/src/elements/conditions/purchasables/PurchasableConditionRule.php +++ /dev/null @@ -1,177 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasableConditionRule extends BaseConditionRule implements ElementConditionRuleInterface -{ - /** - * @var array|null - * @see getElementIds() - * @see setElementIds - */ - private ?array $_elementIds = null; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Purchasable'); - } - - /** - * @param $value - * @return void - */ - public function setElementIds($value): void - { - $this->_elementIds = $value; - } - - /** - * @return array|null - */ - public function getElementIds(): ?array - { - if ($this->_elementIds === null) { - return null; - } - - $elementIds = []; - foreach ($this->_elementIds as $ids) { - if (!is_array($ids) || empty($ids)) { - continue; - } - - $elementIds = array_merge($elementIds, $ids); - } - - return $elementIds; - } - - /** - * @inheritdoc - */ - public function getConfig(): array - { - return array_merge(parent::getConfig(), [ - 'elementIds' => $this->_elementIds, - ]); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['elementIds'], 'safe']; - - return $rules; - } - - /** - * @inheritdoc - */ - protected function inputHtml(): string - { - $id = 'purchasable'; - - $html = ''; - foreach (Plugin::getInstance()->getPurchasables()->getAllPurchasableElementTypes() as $purchasableType) { - /** @var PurchasableInterface|string $purchasableType */ - $elements = null; - if (!empty($this->_elementIds) && isset($this->_elementIds[$purchasableType]) && !empty($this->_elementIds[$purchasableType])) { - $elements = $purchasableType::find() - ->id($this->_elementIds[$purchasableType]) - ->site('*') - ->preferSites(array_filter([Cp::requestedSite()?->id])) - ->status(null) - ->unique() - ->all(); - } - - $html .= Html::tag('div', - Html::beginTag('div') . - Html::tag('strong', $purchasableType::displayName()) . - Html::endTag('div') . - Cp::elementSelectHtml([ - 'name' => Html::namespaceInputName($purchasableType, 'elementIds'), - 'elements' => $elements, - 'elementType' => $purchasableType, - 'sources' => null, - 'criteria' => null, - 'single' => false, - 'showSiteMenu' => true, - ]) - ); - } - - return Html::hiddenLabel($this->getLabel(), $id) . - Html::tag('div', - $html, - [ - 'class' => ['flex', 'flex-start'], - ] - ); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['id']; - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - $ids = $this->getElementIds(); - if ($ids === null) { - return; - } - - $query->id($ids); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - $ids = $this->getElementIds(); - if ($ids === null) { - return true; - } - - if (!is_array($ids)) { - return false; - } - - return in_array($element->id, $ids); - } -} diff --git a/src/elements/conditions/purchasables/PurchasableTypeConditionRule.php b/src/elements/conditions/purchasables/PurchasableTypeConditionRule.php deleted file mode 100644 index 770ffbb126..0000000000 --- a/src/elements/conditions/purchasables/PurchasableTypeConditionRule.php +++ /dev/null @@ -1,65 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasableTypeConditionRule extends BaseMultiSelectConditionRule implements ElementConditionRuleInterface -{ - public function getLabel(): string - { - return Craft::t('commerce', 'Purchasable Type'); - } - - public function getExclusiveQueryParams(): array - { - return ['purchasableType']; - } - - public function modifyQuery(ElementQueryInterface $query): void - { - $query->andWhere(Db::parseParam('type',$this->paramValue())); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Purchasable $element */ - return $this->matchValue($element::class); - } - - /** - * @inheritdoc - */ - protected function options(): array - { - $elementTypes = Plugin::getInstance()->getPurchasables()->getAllPurchasableElementTypes(); - - $types = []; - foreach ($elementTypes as $elementType) { - $types[$elementType] = $elementType::displayName(); - } - - return $types; - } -} diff --git a/src/elements/conditions/purchasables/SkuConditionRule.php b/src/elements/conditions/purchasables/SkuConditionRule.php deleted file mode 100644 index d27fefd9c3..0000000000 --- a/src/elements/conditions/purchasables/SkuConditionRule.php +++ /dev/null @@ -1,46 +0,0 @@ -leftJoin(Table::PURCHASABLES . ' skuconpurch', '[[skuconpurch.id]] = [[elements.id]]'); - $query->andWhere(Db::parseParam('[[skuconpurch.sku]]',$this->paramValue())); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Purchasable $element */ - return $this->matchValue($element->getSku()); - } -} diff --git a/src/elements/conditions/transfers/TransferCondition.php b/src/elements/conditions/transfers/TransferCondition.php deleted file mode 100644 index 6ed8d2c7f9..0000000000 --- a/src/elements/conditions/transfers/TransferCondition.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @since 4.0.0 - */ -class DiscountGroupConditionRule extends GroupConditionRule -{ - protected const OPERATOR_IN_ALL = 'inAll'; - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return \Craft::t('app', 'User Groups'); - } - - /** - * @inheritDoc - */ - protected function operators(): array - { - return array_merge(parent::operators(), [ - self::OPERATOR_IN_ALL, - ]); - } - - /** - * @inheritDoc - */ - protected function operatorLabel(string $operator): string - { - return match ($operator) { - self::OPERATOR_IN_ALL => 'is in all of', - default => parent::operatorLabel($operator) - }; - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - throw new NotSupportedException('Discount user group rule does not support element queries.'); - } - - public function getExclusiveQueryParams(): array - { - return []; - } - - /** - * Returns whether the condition rule matches the given value. - * - * @param string|string[]|null $value - * @return bool - */ - protected function matchValue(array|string|null $value): bool - { - if (!$this->getValues()) { - return true; - } - - if ($value === '' || $value === null) { - $value = []; - } else { - $value = (array)$value; - } - - return match ($this->operator) { - self::OPERATOR_IN => !empty(array_intersect($value, $this->getValues())), - self::OPERATOR_NOT_IN => empty(array_intersect($value, $this->getValues())), - self::OPERATOR_IN_ALL => empty(array_diff($this->getValues(), $value)), - default => throw new InvalidConfigException("Invalid operator: $this->operator"), - }; - } -} diff --git a/src/elements/conditions/variants/CatalogPricingRuleVariantCondition.php b/src/elements/conditions/variants/CatalogPricingRuleVariantCondition.php deleted file mode 100644 index 680f2c7376..0000000000 --- a/src/elements/conditions/variants/CatalogPricingRuleVariantCondition.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @since 5.1.0 - */ -class CatalogPricingRuleVariantCondition extends VariantCondition -{ - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - return array_merge(parent::selectableConditionRules(), [ - CatalogPricingRuleVariantConditionRule::class, - ]); - } -} diff --git a/src/elements/conditions/variants/CatalogPricingRuleVariantConditionRule.php b/src/elements/conditions/variants/CatalogPricingRuleVariantConditionRule.php deleted file mode 100644 index 18f5129875..0000000000 --- a/src/elements/conditions/variants/CatalogPricingRuleVariantConditionRule.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @since 5.5.0 - */ -class CatalogPricingRuleVariantConditionRule extends VariantConditionRule -{ -} diff --git a/src/elements/conditions/variants/ProductConditionRule.php b/src/elements/conditions/variants/ProductConditionRule.php deleted file mode 100644 index 87a693aec5..0000000000 --- a/src/elements/conditions/variants/ProductConditionRule.php +++ /dev/null @@ -1,86 +0,0 @@ - - * @since 5.3.0 - */ -class ProductConditionRule extends BaseElementSelectConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - protected function elementType(): string - { - return Product::class; - } - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Product'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['product', 'productId', 'primaryOwnerId', 'primaryOwner', 'owner', 'ownerId']; - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - /** @var VariantQuery $query */ - $query->ownerId($this->getElementIds()); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Variant $element */ - return $this->matchValue($element->getOwnerId()); - } - - /** - * @inheritdoc - */ - protected function allowMultiple(): bool - { - return true; - } - - /** - * @inerhitdoc - */ - protected function elementSelectConfig(): array - { - return array_merge(parent::elementSelectConfig(), [ - 'showSiteMenu' => true, - ]); - } -} diff --git a/src/elements/conditions/variants/VariantCondition.php b/src/elements/conditions/variants/VariantCondition.php deleted file mode 100644 index 49c3530b53..0000000000 --- a/src/elements/conditions/variants/VariantCondition.php +++ /dev/null @@ -1,32 +0,0 @@ - - * @since 4.0.0 - */ -class VariantCondition extends ElementCondition -{ - /** - * @inheritdoc - */ - public ?string $elementType = Variant::class; - - /** - * @inheritdoc - */ - protected function selectableConditionRules(): array - { - return array_merge(parent::selectableConditionRules(), [ - ProductConditionRule::class, - SkuConditionRule::class, - ]); - } -} diff --git a/src/elements/conditions/variants/VariantConditionRule.php b/src/elements/conditions/variants/VariantConditionRule.php deleted file mode 100644 index ddbc0c92c3..0000000000 --- a/src/elements/conditions/variants/VariantConditionRule.php +++ /dev/null @@ -1,85 +0,0 @@ - - * @since 5.5.0 - */ -class VariantConditionRule extends BaseElementSelectConditionRule implements ElementConditionRuleInterface -{ - /** - * @inheritdoc - */ - protected function elementType(): string - { - return Variant::class; - } - - /** - * @inheritdoc - */ - public function getLabel(): string - { - return Craft::t('commerce', 'Product Variant'); - } - - /** - * @inheritdoc - */ - public function getExclusiveQueryParams(): array - { - return ['id']; - } - - /** - * @inheritdoc - */ - public function modifyQuery(ElementQueryInterface $query): void - { - /** @var VariantQuery $query */ - $query->id($this->getElementIds()); - } - - /** - * @inheritdoc - */ - public function matchElement(ElementInterface $element): bool - { - /** @var Variant $element */ - return $this->matchValue($element->getId()); - } - - /** - * @inheritdoc - */ - protected function allowMultiple(): bool - { - return true; - } - - /** - * @inerhitdoc - */ - protected function elementSelectConfig(): array - { - return array_merge(parent::elementSelectConfig(), [ - 'showSiteMenu' => true, - ]); - } -} diff --git a/src/elements/db/DonationQuery.php b/src/elements/db/DonationQuery.php deleted file mode 100644 index 0852931c46..0000000000 --- a/src/elements/db/DonationQuery.php +++ /dev/null @@ -1,43 +0,0 @@ - - * @since 2.0 - * @doc-path donations.md - */ -class DonationQuery extends PurchasableQuery -{ - /** - * @inheritdoc - */ - protected function beforePrepare(): bool - { - $this->joinElementTable('commerce_donations'); - - $this->query->select([ - 'commerce_donations.id', - ]); - - if ($this->sku) { - $this->subQuery->andWhere(['commerce_donations.sku' => $this->sku]); - } - - return parent::beforePrepare(); - } -} diff --git a/src/elements/db/OrderQuery.php b/src/elements/db/OrderQuery.php deleted file mode 100644 index c3900848a3..0000000000 --- a/src/elements/db/OrderQuery.php +++ /dev/null @@ -1,1968 +0,0 @@ - - * @since 2.0 - * @doc-path orders-carts.md - * @replace {element} order - * @replace {elements} orders - * @replace {twig-method} craft.orders() - * @replace {myElement} myOrder - * @replace {element-class} \craft\commerce\elements\Order - */ -class OrderQuery extends ElementQuery -{ - /** - * @var mixed The order number of the resulting order. - */ - public mixed $number = null; - - /** - * @var mixed The short order number of the resulting order. - */ - public mixed $shortNumber = null; - - /** - * @var mixed The order reference of the resulting order. - * @used-by reference() - */ - public mixed $reference = null; - - /** - * @var mixed The order reference of the resulting order. - * @used-by couponCode() - */ - public mixed $couponCode = null; - - /** - * @var mixed The email address the resulting orders must have. - */ - public mixed $email = null; - - /** - * @var bool The completion status that the resulting orders must have. - */ - public ?bool $isCompleted = null; - - /** - * @var mixed The Date Ordered date that the resulting orders must have. - */ - public mixed $dateOrdered = null; - - /** - * @var mixed The Expiry Date that the resulting orders must have. - */ - public mixed $expiryDate = null; - - /** - * @var mixed The date the order was paid in full. - */ - public mixed $datePaid = null; - - /** - * @var mixed The date the order was first paid in full. - */ - public mixed $dateFirstPaid = null; - - /** - * @var mixed The date the order was authorized in full. - */ - public mixed $dateAuthorized = null; - - /** - * @var mixed The Order Status ID that the resulting orders must have. - */ - public mixed $orderStatusId = null; - - /** - * @var mixed The language the order was made that the resulting the order must have. - */ - public mixed $orderLanguage = null; - - /** - * @var mixed The Order Site ID that the resulting orders must have. - */ - public mixed $orderSiteId = null; - - /** - * @var mixed The origin the resulting orders must have. - */ - public mixed $origin = null; - - /** - * @var mixed The user ID that the resulting orders must have. - */ - public mixed $customerId = null; - - /** - * @var mixed The gateway ID that the resulting orders must have. - */ - public mixed $gatewayId = null; - - /** - * @var int|null The store ID that the resulting orders must have. - */ - public ?int $storeId = null; - - /** - * @var mixed The total of the order resulting orders must have. - * @since 4.2.0 - */ - public mixed $total = null; - - /** - * @var mixed The total price of the order resulting orders must have. - * @since 4.2.0 - */ - public mixed $totalPrice = null; - - /** - * @var mixed The total paid amount of the order resulting orders must have. - * @since 4.2.0 - */ - public mixed $totalPaid = null; - - /** - * @var mixed The total qty of the order resulting orders must have. - * @since 4.2.0 - */ - public mixed $totalQty = null; - - /** - * @var mixed The total weight of the order resulting orders must have. - * @since 5.0.0 - */ - public mixed $totalWeight = null; - - /** - * @var mixed The total discount of the order resulting orders must have. - * @since 4.2.0 - */ - public mixed $totalDiscount = null; - - /** - * @var mixed The total tax resulting orders must have. - * @since 4.2.0 - */ - public mixed $totalTax = null; - - /** - * @var mixed The total price of the items resulting orders must have. - * @since 4.2.0 - */ - public mixed $itemTotal = null; - - /** - * @var mixed The subtotal price of the items resulting orders must have. - * @since 4.2.0 - */ - public mixed $itemSubtotal = null; - - /** - * @var mixed The shipping method handle the resulting orders must have. - * @since 4.2.0 - */ - public mixed $shippingMethodHandle = null; - - /** - * @var bool|null Whether the order is paid - */ - public ?bool $isPaid = null; - - /** - * @var bool|null Whether the order is unpaid - */ - public ?bool $isUnpaid = null; - - /** - * @var mixed The resulting orders must contain these Purchasables. - */ - public mixed $hasPurchasables = null; - - /** - * @var array{purchasables: array, match: ContainsPurchasablesMatch}|null - */ - public ?array $containsPurchasables = null; - - /** - * @var bool|null Whether the order has any transactions - */ - public ?bool $hasTransactions = null; - - /** - * @var bool|null Whether the order has any line items. - */ - public ?bool $hasLineItems = null; - - /** - * @var bool|null Whether the order has any admin notices. - */ - public ?bool $hasAdminNotices = null; - - /** - * @var bool Eager loads all relational data (addresses, adjustments, users, line items, transactions) for the resulting orders. - */ - public bool $withAll = false; - - /** - * @var bool Eager loads the shipping and billing addressees on the resulting orders. - */ - public bool $withAddresses = false; - - /** - * @var bool Eager loads the order adjustments on the resulting orders. - */ - public bool $withAdjustments = false; - - /** - * @var bool Eager load the user on to the order. - */ - public bool $withCustomer = false; - - /** - * @var bool Eager loads the line items on the resulting orders. - */ - public bool $withLineItems = false; - - /** - * @var bool Eager loads the transactions on the resulting orders. - */ - public bool $withTransactions = false; - - /** - * @inheritdoc - */ - protected array $defaultOrderBy = ['commerce_orders.id' => SORT_ASC]; - - /** - * @inheritdoc - */ - public function __construct($elementType, array $config = []) - { - // Default orderBy - if (!isset($config['orderBy'])) { - $config['orderBy'] = 'commerce_orders.id'; - } - - parent::__construct($elementType, $config); - } - - /** - * Narrows the query results based on the order number. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'` | with a matching order number - * - * --- - * - * ```twig - * {# Fetch the requested {element} #} - * {% set orderNumber = craft.app.request.getQueryParam('number') %} - * {% set {element-var} = {twig-method} - * .number(orderNumber) - * .one() %} - * ``` - * - * ```php - * // Fetch the requested {element} - * $orderNumber = Craft::$app->request->getQueryParam('number'); - * ${element-var} = {php-method} - * ->number($orderNumber) - * ->one(); - * ``` - * - * @param string|array|null $value The property value. - * @return static self reference - */ - public function number(mixed $value): OrderQuery - { - $this->number = $value; - return $this; - } - - /** - * Narrows the query results based on the order short number. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'xxxxxxx'` | with a matching order number - * - * --- - * - * ```twig - * {# Fetch the requested {element} #} - * {% set orderNumber = craft.app.request.getQueryParam('shortNumber') %} - * {% set {element-var} = {twig-method} - * .shortNumber(orderNumber) - * .one() %} - * ``` - * - * ```php - * // Fetch the requested {element} - * $orderNumber = Craft::$app->request->getQueryParam('shortNumber'); - * ${element-var} = {php-method} - * ->shortNumber($orderNumber) - * ->one(); - * ``` - * - * @param string|array|null $value The property value. - * @return static self reference - * @since 2.2 - */ - public function shortNumber(mixed $value): OrderQuery - { - $this->shortNumber = $value; - return $this; - } - - /** - * Narrows the query results based on the order reference. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'Foo'` | with a reference of `Foo`. - * | `'Foo*'` | with a reference that begins with `Foo`. - * | `'*Foo'` | with a reference that ends with `Foo`. - * | `'*Foo*'` | with a reference that contains `Foo`. - * | `'not *Foo*'` | with a reference that doesn’t contain `Foo`. - * | `['*Foo*', '*Bar*']` | with a reference that contains `Foo` or `Bar`. - * | `['not', '*Foo*', '*Bar*']` | with a reference that doesn’t contain `Foo` or `Bar`. - * - * --- - * - * ```twig - * {# Fetch the requested {element} #} - * {% set orderReference = craft.app.request.getQueryParam('ref') %} - * {% set {element-var} = {twig-method} - * .reference(orderReference) - * .one() %} - * ``` - * - * ```php - * // Fetch the requested {element} - * $orderReference = Craft::$app->request->getQueryParam('ref'); - * ${element-var} = {php-method} - * ->reference($orderReference) - * ->one(); - * ``` - * - * @param string|null $value The property value - * @return static self reference - */ - public function reference(mixed $value): OrderQuery - { - $this->reference = $value; - return $this; - } - - /** - * Narrows the query results based on the order's coupon code. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `':empty:'` | that don’t have a coupon code. - * | `':notempty:'` | that have a coupon code. - * | `'Foo'` | with a coupon code of `Foo`. - * | `'Foo*'` | with a coupon code that begins with `Foo`. - * | `'*Foo'` | with a coupon code that ends with `Foo`. - * | `'*Foo*'` | with a coupon code that contains `Foo`. - * | `'not *Foo*'` | with a coupon code that doesn’t contain `Foo`. - * | `['*Foo*', '*Bar*']` | with a coupon code that contains `Foo` or `Bar`. - * | `['not', '*Foo*', '*Bar*']` | with a coupon code that doesn’t contain `Foo` or `Bar`. - * - * --- - * - * ```twig - * {# Fetch the requested {element} #} - * {% set {element-var} = {twig-method} - * .reference('foo') - * .one() %} - * ``` - * - * ```php - * // Fetch the requested {element} - * ${element-var} = {php-method} - * ->reference('foo') - * ->one(); - * ``` - * - * @param string|null $value The property value - * @return static self reference - */ - public function couponCode(mixed $value): OrderQuery - { - $this->couponCode = $value; - return $this; - } - - /** - * Narrows the query results based on the customers’ email addresses. - * - * Possible values include: - * - * | Value | Fetches {elements} with customers… - * | - | - - * | `'foo@bar.baz'` | with an email of `foo@bar.baz`. - * | `'not foo@bar.baz'` | not with an email of `foo@bar.baz`. - * | `'*@bar.baz'` | with an email that ends with `@bar.baz`. - * - * --- - * - * ```twig - * {# Fetch orders from customers with a .co.uk domain on their email address #} - * {% set {elements-var} = {twig-method} - * .email('*.co.uk') - * .all() %} - * ``` - * - * ```php - * // Fetch orders from customers with a .co.uk domain on their email address - * ${elements-var} = {php-method} - * ->email('*.co.uk') - * ->all(); - * ``` - * - * @param string|string[]|null $value The property value - * @return static self reference - */ - public function email(mixed $value): OrderQuery - { - $this->email = $value; - return $this; - } - - /** - * Narrows the query results to only orders that are completed. - * - * --- - * - * ```twig - * {# Fetch completed orders #} - * {% set {elements-var} = {twig-method} - * .isCompleted() - * .all() %} - * ``` - * - * ```php - * // Fetch completed orders - * ${elements-var} = {element-class}::find() - * ->isCompleted() - * ->all(); - * ``` - * - * @param bool $value The property value - * @return static self reference - */ - public function isCompleted(?bool $value = true): OrderQuery - { - $this->isCompleted = $value; - return $this; - } - - /** - * Narrows the query results based on the orders’ completion dates. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'>= 2018-04-01'` | that were completed on or after 2018-04-01. - * | `'< 2018-05-01'` | that were completed before 2018-05-01 - * | `['and', '>= 2018-04-04', '< 2018-05-01']` | that were completed between 2018-04-01 and 2018-05-01. - * - * --- - * - * ```twig - * {# Fetch {elements} that were completed recently #} - * {% set aWeekAgo = date('7 days ago')|atom %} - * - * {% set {elements-var} = {twig-method} - * .dateOrdered(">= #{aWeekAgo}") - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} that were completed recently - * $aWeekAgo = new \DateTime('7 days ago')->format(\DateTime::ATOM); - * - * ${elements-var} = {php-method} - * ->dateOrdered(">= {$aWeekAgo}") - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function dateOrdered(mixed $value): OrderQuery - { - $this->dateOrdered = $value; - return $this; - } - - /** - * Narrows the query results based on the orders’ paid dates. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'>= 2018-04-01'` | that were paid on or after 2018-04-01. - * | `'< 2018-05-01'` | that were paid before 2018-05-01 - * | `['and', '>= 2018-04-04', '< 2018-05-01']` | that were paid between 2018-04-01 and 2018-05-01. - * - * --- - * - * ```twig - * {# Fetch {elements} that were paid for recently #} - * {% set aWeekAgo = date('7 days ago')|atom %} - * - * {% set {elements-var} = {twig-method} - * .datePaid(">= #{aWeekAgo}") - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} that were paid for recently - * $aWeekAgo = new \DateTime('7 days ago')->format(\DateTime::ATOM); - * - * ${elements-var} = {php-method} - * ->datePaid(">= {$aWeekAgo}") - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function datePaid(mixed $value): OrderQuery - { - $this->datePaid = $value; - return $this; - } - - /** - * Narrows the query results based on the orders’ first paid dates. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'>= 2018-04-01'` | that were first paid on or after 2018-04-01. - * | `'< 2018-05-01'` | that were first paid before 2018-05-01 - * | `['and', '>= 2018-04-04', '< 2018-05-01']` | that were first paid between 2018-04-01 and 2018-05-01. - * - * --- - * - * ```twig - * {# Fetch {elements} that were first paid for recently #} - * {% set aWeekAgo = date('7 days ago')|atom %} - * - * {% set {elements-var} = {twig-method} - * .dateFirstPaid(">= #{aWeekAgo}") - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} that were first paid for recently - * $aWeekAgo = new \DateTime('7 days ago')->format(\DateTime::ATOM); - * - * ${elements-var} = {php-method} - * ->dateFirstPaid(">= {$aWeekAgo}") - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function dateFirstPaid(mixed $value): OrderQuery - { - $this->dateFirstPaid = $value; - return $this; - } - - /** - * Narrows the query results based on the orders’ authorized dates. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'>= 2018-04-01'` | that were authorized on or after 2018-04-01. - * | `'< 2018-05-01'` | that were authorized before 2018-05-01 - * | `['and', '>= 2018-04-04', '< 2018-05-01']` | that were completed between 2018-04-01 and 2018-05-01. - * - * --- - * - * ```twig - * {# Fetch {elements} that were authorized recently #} - * {% set aWeekAgo = date('7 days ago')|atom %} - * - * {% set {elements-var} = {twig-method} - * .dateAuthorized(">= #{aWeekAgo}") - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} that were authorized recently - * $aWeekAgo = new \DateTime('7 days ago')->format(\DateTime::ATOM); - * - * ${elements-var} = {php-method} - * ->dateAuthorized(">= {$aWeekAgo}") - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function dateAuthorized(mixed $value): OrderQuery - { - $this->dateAuthorized = $value; - return $this; - } - - /** - * Narrows the query results based on the orders’ expiry dates. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'>= 2020-04-01'` | that will expire on or after 2020-04-01. - * | `'< 2020-05-01'` | that will expire before 2020-05-01 - * | `['and', '>= 2020-04-04', '< 2020-05-01']` | that will expire between 2020-04-01 and 2020-05-01. - * - * --- - * - * ```twig - * {# Fetch {elements} expiring this month #} - * {% set nextMonth = date('first day of next month')|atom %} - * - * {% set {elements-var} = {twig-method} - * .expiryDate("< #{nextMonth}") - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} expiring this month - * $nextMonth = new \DateTime('first day of next month')->format(\DateTime::ATOM); - * - * ${elements-var} = {php-method} - * ->expiryDate("< {$nextMonth}") - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function expiryDate(mixed $value): OrderQuery - { - $this->expiryDate = $value; - return $this; - } - - /** - * Narrows the query results based on the order statuses. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'foo'` | with an order status with a handle of `foo`. - * | `'not foo'` | not with an order status with a handle of `foo`. - * | `['foo', 'bar']` | with an order status with a handle of `foo` or `bar`. - * | `['not', 'foo', 'bar']` | not with an order status with a handle of `foo` or `bar`. - * | a [[OrderStatus|OrderStatus]] object | with an order status represented by the object. - * - * --- - * - * ```twig - * {# Fetch shipped {elements} #} - * {% set {elements-var} = {twig-method} - * .orderStatus('shipped') - * .all() %} - * ``` - * - * ```php - * // Fetch shipped {elements} - * ${elements-var} = {php-method} - * ->orderStatus('shipped') - * ->all(); - * ``` - * - * @param string|string[]|OrderStatus|null $value The property value - * @return static self reference - */ - public function orderStatus(mixed $value): OrderQuery - { - if ($value instanceof OrderStatus) { - $this->orderStatusId = $value->id; - } elseif ($value !== null) { - $this->orderStatusId = (new Query()) - ->select(['id']) - ->from([Table::ORDERSTATUSES]) - ->where(Db::parseParam('handle', $value)) - ->column(); - } else { - $this->orderStatusId = null; - } - - return $this; - } - - /** - * Narrows the query results based on the shipping method handle. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'foo'` | with a shipping method with a handle of `foo`. - * | `'not foo'` | not with a shipping method with a handle of `foo`. - * | `['foo', 'bar']` | with a shipping method with a handle of `foo` or `bar`. - * | `['not', 'foo', 'bar']` | not with a shipping method with a handle of `foo` or `bar`. - * | a [[ShippingMethod|ShippingMethod]] object | with a shipping method represented by the object. - * - * --- - * - * ```twig - * {# Fetch collection shipping method {elements} #} - * {% set {elements-var} = {twig-method} - * .shippingMethodHandle('collection') - * .all() %} - * ``` - * - * ```php - * // Fetch collection shipping method {elements} - * ${elements-var} = {php-method} - * ->shippingMethodHandle('collection') - * ->all(); - * ``` - * - * @param string|string[]|null $value The property value - * @return static self reference - */ - public function shippingMethodHandle(mixed $value): OrderQuery - { - $this->shippingMethodHandle = $value; - return $this; - } - - /** - * Narrows the query results based on the order statuses, per their IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | with an order status with an ID of 1. - * | `'not 1'` | not with an order status with an ID of 1. - * | `[1, 2]` | with an order status with an ID of 1 or 2. - * | `['not', 1, 2]` | not with an order status with an ID of 1 or 2. - * - * --- - * - * ```twig - * {# Fetch {elements} with an order status with an ID of 1 #} - * {% set {elements-var} = {twig-method} - * .orderStatusId(1) - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} with an order status with an ID of 1 - * ${elements-var} = {php-method} - * ->orderStatusId(1) - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function orderStatusId(mixed $value): OrderQuery - { - $this->orderStatusId = $value; - return $this; - } - - /** - * Narrows the query results based on the order language, per the language string provided. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'en'` | with an order language that is `'en'`. - * | `'not en'` | not with an order language that is not `'en'`. - * | `['en', 'en-us']` | with an order language that is `'en'` or `'en-us'`. - * | `['not', 'en']` | not with an order language that is not `'en'`. - * - * --- - * - * ```twig - * {# Fetch {elements} with an order language that is `'en'` #} - * {% set {elements-var} = {twig-method} - * .orderLanguage('en') - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} with an order language that is `'en'` - * ${elements-var} = {php-method} - * ->orderLanguage('en') - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function orderLanguage(mixed $value): OrderQuery - { - $this->orderLanguage = $value; - return $this; - } - - /** - * Narrows the query results based on the order language, per the language string provided. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | with an order site ID of 1. - * | `'not 1'` | not with an order site ID that is no 1. - * | `[1, 2]` | with an order site ID of 1 or 2. - * | `['not', 1, 2]` | not with an order site ID of 1 or 2. - * - * --- - * - * ```twig - * {# Fetch {elements} with an order site ID of 1 #} - * {% set {elements-var} = {twig-method} - * .orderSiteId(1) - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} with an order site ID of 1 - * ${elements-var} = {php-method} - * ->orderSiteId(1) - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function orderSiteId(mixed $value): OrderQuery - { - $this->orderSiteId = $value; - return $this; - } - - /** - * Narrows the query results based on the origin. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'web'` | with an origin of `web`. - * | `'not remote'` | not with an origin of `remote`. - * | `['web', 'cp']` | with an order origin of `web` or `cp`. - * | `['not', 'remote', 'cp']` | not with an origin of `web` or `cp`. - * - * --- - * - * ```twig - * {# Fetch shipped {elements} #} - * {% set {elements-var} = {twig-method} - * .origin('web') - * .all() %} - * ``` - * - * ```php - * // Fetch shipped {elements} - * ${elements-var} = {php-method} - * ->origin('web') - * ->all(); - * ``` - * - * @param string|string[]|null $value The property value - * @return static self reference - */ - public function origin(mixed $value): OrderQuery - { - $this->origin = $value; - - return $this; - } - - /** - * Narrows the query results based on the gateway. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | a [[Gateway|Gateway]] object | with a gateway represented by the object. - * - * @param GatewayInterface|null $value The property value - * @return static self reference - */ - public function gateway(?GatewayInterface $value): OrderQuery - { - if ($value) { - /** @var Gateway $value */ - $this->gatewayId = $value->id; - } else { - $this->gatewayId = null; - } - - return $this; - } - - /** - * Narrows the query results based on the gateway, per its ID. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | with a gateway with an ID of 1. - * | `'not 1'` | not with a gateway with an ID of 1. - * | `[1, 2]` | with a gateway with an ID of 1 or 2. - * | `['not', 1, 2]` | not with a gateway with an ID of 1 or 2. - * - * @param mixed $value The property value - * @return static self reference - */ - public function gatewayId(mixed $value): OrderQuery - { - $this->gatewayId = $value; - return $this; - } - - /** - * Narrows the query results based on the customer’s user account. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | with a customer with a user account ID of 1. - * | a [[User|User]] object | with a customer with a user account represented by the object. - * - * --- - * - * ```twig - * {# Fetch the current user's orders #} - * {% set {elements-var} = {twig-method} - * .user(currentUser) - * .all() %} - * ``` - * - * ```php - * // Fetch the current user's orders - * $user = Craft::$app->user->getIdentity(); - * ${elements-var} = {php-method} - * ->user($user) - * ->all(); - * ``` - * - * @param User|int|null $value The property value - * @return static self reference - * @deprecated 4.0.0 in favor of [[customer()]] - */ - public function user(int|User|null $value): OrderQuery - { - Craft::$app->getDeprecator()->log('OrderQuery::user()', 'The `OrderQuery::user()` method is deprecated, use the `OrderQuery::customer()` method instead.'); - return $this->customer($value); - } - - /** - * Narrows the query results based on the customer’s user account. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | with a customer with a user account ID of 1. - * | a [[User|User]] object | with a customer with a user account represented by the object. - * | `'not 1'` | not the user account with an ID 1. - * | `[1, 2]` | with an user account ID of 1 or 2. - * | `['not', 1, 2]` | not with a user account ID of 1 or 2. - * - * --- - * - * ```twig - * {# Fetch the current user's orders #} - * {% set {elements-var} = {twig-method} - * .customer(currentUser) - * .all() %} - * ``` - * - * ```php - * // Fetch the current user's orders - * $user = Craft::$app->user->getIdentity(); - * ${elements-var} = {php-method} - * ->customer($user) - * ->all(); - * ``` - * - * @param User|int|null $value The property value - * @return static self reference - */ - public function customer(int|User|null $value): OrderQuery - { - if ($value instanceof User) { - $this->customerId = $value->id; - } else { - $this->customerId = $value; - } - - return $this; - } - - /** - * Narrows the query results based on the customer, per their user ID. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | with a user with an ID of 1. - * | `'not 1'` | not with a user with an ID of 1. - * | `[1, 2]` | with a user with an ID of 1 or 2. - * | `['not', 1, 2]` | not with a user with an ID of 1 or 2. - * - * --- - * - * ```twig - * {# Fetch the current user's orders #} - * {% set {elements-var} = {twig-method} - * .customerId(currentUser.id) - * .all() %} - * ``` - * - * ```php - * // Fetch the current user's orders - * $user = Craft::$app->user->getIdentity(); - * ${elements-var} = {php-method} - * ->customerId($user->id) - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function customerId(mixed $value): OrderQuery - { - $this->customerId = $value; - return $this; - } - - /** - * Narrows the query results based on the total. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `10` | with a total price of $10. - * | `['and', 10, 20]` | an order with a total of $10 or $20. - * - * @param mixed $value The property value - * @return static self reference - * @since 4.2.0 - */ - public function total(mixed $value): OrderQuery - { - $this->total = $value; - return $this; - } - - /** - * Narrows the query results based on the total price. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `10` | with a total price of $10. - * | `['and', 10, 20]` | an order with a total price of $10 or $20. - * - * @param mixed $value The property value - * @return static self reference - * @since 4.2.0 - */ - public function totalPrice(mixed $value): OrderQuery - { - $this->totalPrice = $value; - return $this; - } - - /** - * Narrows the query results based on the total paid amount. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `10` | with a total paid amount of $10. - * | `['and', 10, 20]` | an order with a total paid amount of $10 or $20. - * - * @param mixed $value The property value - * @return static self reference - * @since 4.2.0 - */ - public function totalPaid(mixed $value): OrderQuery - { - $this->totalPaid = $value; - return $this; - } - - /** - * Narrows the query results based on the total qty of items. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `10` | with a total qty of 10. - * | `[10, 20]` | an order with a total qty of 10 or 20. - * - * @param mixed $value The property value - * @return static self reference - * @since 4.2.0 - */ - public function totalQty(mixed $value): OrderQuery - { - $this->totalQty = $value; - return $this; - } - - /** - * Narrows the query results based on the total weight of items. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `10` | with a total weight of 10. - * | `[10, 20]` | an order with a total weight of 10 or 20. - * - * @param mixed $value The property value - * @return static self reference - * @since 4.2.0 - */ - public function totalWeight(mixed $value): OrderQuery - { - $this->totalWeight = $value; - return $this; - } - - /** - * Narrows the query results based on the total discount. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `10` | with a total discount of 10. - * | `[10, 20]` | an order with a total discount of 10 or 20. - * - * @param mixed $value The property value - * @return static self reference - * @since 4.2.0 - */ - public function totalDiscount(mixed $value): OrderQuery - { - $this->totalDiscount = $value; - return $this; - } - - /** - * Narrows the query results based on the total tax. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `10` | with a total tax of 10. - * | `[10, 20]` | an order with a total tax of 10 or 20. - * - * @param mixed $value The property value - * @return static self reference - * @since 4.2.0 - */ - public function totalTax(mixed $value): OrderQuery - { - $this->totalTax = $value; - return $this; - } - - /** - * Narrows the query results based on the order’s item total. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `100` | with an item total of $100. - * | `'< 1000000'` | with an item total of less than $1,000,000. - * | `['>= 10', '< 100']` | with an item total of between $10 and $100. - - * - * @param mixed $value The property value - * @return static self reference - * @since 4.2.0 - */ - public function itemTotal(mixed $value): OrderQuery - { - $this->itemTotal = $value; - return $this; - } - - /** - * Narrows the query results based on the order’s item subtotal. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `100` | with an item subtotal of $100. - * | `'< 1000000'` | with an item subtotal of less than $1,000,000. - * | `['>= 10', '< 100']` | with an item subtotal of between $10 and $100. - - * - * @param mixed $value The property value - * @return static self reference - * @since 4.2.0 - */ - public function itemSubtotal(mixed $value): OrderQuery - { - $this->itemSubtotal = $value; - return $this; - } - - /** - * Narrows the query results to only orders that are paid. - * - * --- - * - * ```twig - * {# Fetch paid orders #} - * {% set {elements-var} = {twig-method} - * .isPaid() - * .all() %} - * ``` - * - * ```php - * // Fetch paid orders - * ${elements-var} = {element-class}::find() - * ->isPaid() - * ->all(); - * ``` - * - * @param bool|null $value The property value - * @return static self reference - */ - public function isPaid(?bool $value = true): OrderQuery - { - $this->isPaid = $value; - return $this; - } - - /** - * Narrows the query results to only orders that are not paid. - * - * --- - * - * ```twig - * {# Fetch unpaid orders #} - * {% set {elements-var} = {twig-method} - * .isUnpaid() - * .all() %} - * ``` - * - * ```php - * // Fetch unpaid orders - * ${elements-var} = {element-class}::find() - * ->isUnpaid() - * ->all(); - * ``` - * - * @param bool|null $value The property value - * @return static self reference - */ - public function isUnpaid(?bool $value = true): OrderQuery - { - $this->isUnpaid = $value; - return $this; - } - - /** - * Narrows the query results to only orders that have line items. - * - * --- - * - * ```twig - * {# Fetch orders that do or do not have line items #} - * {% set {elements-var} = {twig-method} - * .hasLineItems() - * .all() %} - * ``` - * - * ```php - * // Fetch unpaid orders - * ${elements-var} = {element-class}::find() - * ->hasLineItems() - * ->all(); - * ``` - * - * @param bool|null $value The property value - * @return static self reference - */ - public function hasLineItems(?bool $value = true): OrderQuery - { - $this->hasLineItems = $value; - return $this; - } - - public function hasAdminNotices(?bool $value = true): static - { - $this->hasAdminNotices = $value; - return $this; - } - - /** - * Narrows the query results to only carts that have at least one transaction. - * - * --- - * - * ```twig - * {# Fetch carts that have attempted payments #} - * {% set {elements-var} = {twig-method} - * .hasTransactions() - * .all() %} - * ``` - * - * ```php - * // Fetch carts that have attempted payments - * ${elements-var} = {element-class}::find() - * ->hasTransactions() - * ->all(); - * ``` - * - * @param bool|null $value The property value - * @return static self reference - */ - public function hasTransactions(?bool $value = true): OrderQuery - { - $this->hasTransactions = $value; - return $this; - } - - /** - * Narrows the query results to only orders that have certain purchasables. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | a [[PurchasableInterface|PurchasableInterface]] object | with a purchasable represented by the object. - * | an array of [[PurchasableInterface|PurchasableInterface]] objects | with all the purchasables represented by the objects. - * - * @param PurchasableInterface|array|null $value The property value - * @return static self reference - */ - public function hasPurchasables(mixed $value): OrderQuery - { - $this->hasPurchasables = $value; - - return $this; - } - - /** - * Narrows the query results based on whether orders contain specific purchasables, - * with support for 'any', 'all', and 'only' match modes. - * - * The `purchasables` key accepts a mixed array of integer IDs and/or - * [[PurchasableInterface]] objects: - * - * ```php - * // IDs only - * ->containsPurchasables(['purchasables' => [1, 2, 3], 'match' => 'any']) - * - * // Objects only - * ->containsPurchasables(['purchasables' => [$variant1, $variant2], 'match' => 'all']) - * - * // Mixed - * ->containsPurchasables(['purchasables' => [1, $variant2, 3], 'match' => 'only']) - * ``` - * - * @param array{purchasables: array, match: ContainsPurchasablesMatch} $value - * @return static self reference - */ - public function containsPurchasables(array $value): OrderQuery - { - $this->containsPurchasables = $value; - - return $this; - } - - /** - * Narrows the query results to only orders that are related to the given store. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | with a `storeId` of `1`. - * - * @param int|null $value - * @return static self reference - */ - public function storeId(?int $value): OrderQuery - { - $this->storeId = $value; - - return $this; - } - - /** - * Eager loads all relational data (addresses, adjustments, customers, line items, transactions) for the resulting orders. - * - * Possible values include: - * - * | Value | Fetches addresses, adjustments, customers, line items, transactions - * | - | - - * | bool | `true` to eager-load, `false` to not eager load. - * - * @param bool $value The property value - * @return static self reference - * - * @used-by withAll() - */ - public function withAll(bool $value = true): OrderQuery - { - $this->withAll = $value; - - return $this; - } - - /** - * Eager loads the shipping and billing addressees on the resulting orders. - * - * Possible values include: - * - * | Value | Fetches addresses - * | - | - - * | bool | `true` to eager-load, `false` to not eager load. - * - * @param bool $value The property value - * @return static self reference - * - * @used-by withAddresses() - */ - public function withAddresses(bool $value = true): OrderQuery - { - $this->withAddresses = $value; - - return $this; - } - - /** - * Eager loads the order adjustments on the resulting orders. - * - * Possible values include: - * - * | Value | Fetches adjustments - * | - | - - * | bool | `true` to eager-load, `false` to not eager load. - * - * @param bool $value The property value - * @return static self reference - * - * @used-by withAdjustments() - */ - public function withAdjustments(bool $value = true): OrderQuery - { - $this->withAdjustments = $value; - - return $this; - } - - /** - * Eager loads the user on the resulting orders. - * - * Possible values include: - * - * | Value | Fetches adjustments - * | - | - - * | bool | `true` to eager-load, `false` to not eager load. - * - * @param bool $value The property value - * @return static self reference - * - * @used-by withCustomer() - */ - public function withCustomer(bool $value = true): OrderQuery - { - $this->withCustomer = $value; - - return $this; - } - - /** - * Eager loads the line items on the resulting orders. - * - * Possible values include: - * - * | Value | Fetches line items - * | - | - - * | bool | `true` to eager-load, `false` to not eager load. - * - * @param bool $value The property value - * @return static self reference - * - * @used-by withLineItems() - */ - public function withLineItems(bool $value = true): OrderQuery - { - $this->withLineItems = $value; - - return $this; - } - - /** - * Eager loads the transactions on the resulting orders. - * - * Possible values include: - * - * | Value | Fetches transactions… - * | - | - - * | bool | `true` to eager-load, `false` to not eager load. - * - * @param bool $value The property value - * @return static self reference - * - * @used-by withTransactions() - */ - public function withTransactions(bool $value = true): OrderQuery - { - $this->withTransactions = $value; - - return $this; - } - - /** - * @inheritdoc - */ - public function populate($rows): array - { - // @TODO Remove in Commerce 6.0 once the `email` column is dropped from `commerce_orders` (email now lives on the customer) - // Remove `email` key from each row. - array_walk($rows, function(&$row) { - if (array_key_exists('email', $row)) { - unset($row['email']); - } - }); - - /** @var Order[] $orders */ - $orders = parent::populate($rows); - - // Eager-load anything? - if (!empty($orders) && !$this->asArray) { - - // Eager-load line items? - if ($this->withLineItems === true || $this->withAll) { - $orders = Plugin::getInstance()->getLineItems()->eagerLoadLineItemsForOrders($orders); - } - - // Eager-load transactions? - if ($this->withTransactions === true || $this->withAll) { - $orders = Plugin::getInstance()->getTransactions()->eagerLoadTransactionsForOrders($orders); - } - - // Eager-load adjustments? - if ($this->withAdjustments === true || $this->withAll) { - $orders = Plugin::getInstance()->getOrderAdjustments()->eagerLoadOrderAdjustmentsForOrders($orders); - } - - // Eager-load users? - if ($this->withCustomer === true || $this->withAll) { - $orders = Plugin::getInstance()->getCustomers()->eagerLoadCustomerForOrders($orders); - } - - // Eager-load addresses? - if ($this->withAddresses === true || $this->withAll) { - $orders = Plugin::getInstance()->getOrders()->eagerLoadAddressesForOrders($orders); - } - - $orders = Plugin::getInstance()->getOrderNotices()->eagerLoadOrderNoticesForOrders($orders); - } - - return $orders; - } - - /** - * @inheritdoc - */ - protected function beforePrepare(): bool - { - $this->joinElementTable('commerce_orders'); - - $this->query->select([ - 'commerce_orders.id', - 'commerce_orders.storeId', - 'commerce_orders.number', - 'commerce_orders.reference', - 'commerce_orders.couponCode', - 'commerce_orders.orderStatusId', - 'commerce_orders.dateOrdered', - - // @TODO Remove in Commerce 6.0 once the `email` column is dropped from `commerce_orders` (email now lives on the customer) - 'commerce_orders.email', - - 'commerce_orders.isCompleted', - 'commerce_orders.datePaid', - 'commerce_orders.dateFirstPaid', - 'commerce_orders.currency', - 'commerce_orders.paymentCurrency', - 'commerce_orders.lastIp', - 'commerce_orders.orderLanguage', - 'commerce_orders.message', - 'commerce_orders.returnUrl', - 'commerce_orders.cancelUrl', - 'commerce_orders.billingAddressId', - 'commerce_orders.shippingAddressId', - 'commerce_orders.estimatedBillingAddressId', - 'commerce_orders.estimatedShippingAddressId', - 'commerce_orders.sourceBillingAddressId', - 'commerce_orders.sourceShippingAddressId', - 'commerce_orders.shippingMethodHandle', - 'commerce_orders.gatewayId', - 'commerce_orders.paymentSourceId', - 'commerce_orders.customerId', - 'commerce_orders.customerDeleted', - 'commerce_orders.dateUpdated', - 'commerce_orders.registerUserOnOrderComplete', - 'commerce_orders.saveBillingAddressOnOrderComplete', - 'commerce_orders.saveShippingAddressOnOrderComplete', - 'commerce_orders.saveShippingAddressOnOrderComplete', - 'commerce_orders.makePrimaryShippingAddress', - 'commerce_orders.makePrimaryBillingAddress', - 'commerce_orders.recalculationMode', - 'commerce_orders.origin', - 'commerce_orders.dateAuthorized', - 'storedTotalPrice' => 'commerce_orders.totalPrice', - 'storedTotalPaid' => 'commerce_orders.totalPaid', - 'storedItemTotal' => 'commerce_orders.itemTotal', - 'storedTotalDiscount' => 'commerce_orders.totalDiscount', - 'storedTotalShippingCost' => 'commerce_orders.totalShippingCost', - 'storedTotalTax' => 'commerce_orders.totalTax', - 'storedTotalTaxIncluded' => 'commerce_orders.totalTaxIncluded', - 'storedItemSubtotal' => 'commerce_orders.itemSubtotal', - 'storedTotalQty' => 'commerce_orders.totalQty', - 'commerce_orders.shippingMethodName', - 'commerce_orders.orderSiteId', - 'commerce_orders.orderLanguage', - 'commerce_orders.orderCompletedEmail', - ]); - - // Addresses table joined for sorting purposes - $this->query->leftJoin(CraftTable::ADDRESSES . ' billing_address', '[[billing_address.id]] = [[commerce_orders.billingAddressId]]'); - $this->subQuery->leftJoin(CraftTable::ADDRESSES . ' billing_address', '[[billing_address.id]] = [[commerce_orders.billingAddressId]]'); - $this->query->leftJoin(CraftTable::ADDRESSES . ' shipping_address', '[[shipping_address.id]] = [[commerce_orders.shippingAddressId]]'); - $this->subQuery->leftJoin(CraftTable::ADDRESSES . ' shipping_address', '[[shipping_address.id]] = [[commerce_orders.shippingAddressId]]'); - - if (isset($this->number)) { - // If it's set to anything besides a non-empty string, abort the query - if (!is_string($this->number) || $this->number === '') { - return false; - } - $this->subQuery->andWhere(['commerce_orders.number' => $this->number]); - } - - if (isset($this->shortNumber)) { - // If it's set to anything besides a non-empty string, abort the query - if (!is_string($this->shortNumber) || $this->shortNumber === '') { - return false; - } - - $this->subQuery->andWhere(new Expression('LEFT([[commerce_orders.number]], 7) = :shortNumber', [':shortNumber' => $this->shortNumber])); - } - - if (isset($this->storeId) && $this->storeId) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.storeId', $this->storeId)); - } - - if (isset($this->origin) && $this->origin) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.origin', $this->origin)); - } - - if (isset($this->reference) && $this->reference) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.reference', $this->reference)); - } - - if (isset($this->couponCode)) { - // Coupon code criteria is case-insensitive like in the adjuster - $this->subQuery->andWhere(Db::parseParam('commerce_orders.couponCode', $this->couponCode, caseInsensitive: true)); - } - - if (isset($this->email) && $this->email) { - // Join and search the users table for email address - $this->subQuery->leftJoin(CraftTable::USERS . ' users', '[[users.id]] = [[commerce_orders.customerId]]'); - $this->subQuery->andWhere(Db::parseParam('users.email', $this->email, '=', true)); - } - - if (isset($this->isCompleted)) { - $this->subQuery->andWhere(Db::parseBooleanParam('commerce_orders.isCompleted', $this->isCompleted, false)); - } - - if (isset($this->dateAuthorized)) { - $this->subQuery->andWhere(Db::parseDateParam('commerce_orders.dateAuthorized', $this->datePaid)); - } - - if (isset($this->dateOrdered)) { - $this->subQuery->andWhere(Db::parseDateParam('commerce_orders.dateOrdered', $this->dateOrdered)); - } - - if (isset($this->datePaid)) { - $this->subQuery->andWhere(Db::parseDateParam('commerce_orders.datePaid', $this->datePaid)); - } - - if (isset($this->dateFirstPaid)) { - $this->subQuery->andWhere(Db::parseDateParam('commerce_orders.dateFirstPaid', $this->dateFirstPaid)); - } - - if (isset($this->expiryDate)) { - $this->subQuery->andWhere(Db::parseDateParam('commerce_orders.expiryDate', $this->expiryDate)); - } - - if (isset($this->orderStatusId)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.orderStatusId', $this->orderStatusId)); - } - - if (isset($this->shippingMethodHandle)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.shippingMethodHandle', $this->shippingMethodHandle)); - } - - if (isset($this->orderLanguage)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.orderLanguage', $this->orderLanguage)); - } - - if (isset($this->orderSiteId)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.orderSiteId', $this->orderSiteId)); - } - - if (isset($this->customerId)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.customerId', $this->customerId)); - } - - if (isset($this->gatewayId)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.gatewayId', $this->gatewayId)); - } - - if (isset($this->total)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.total', $this->total)); - } - - if (isset($this->totalPrice)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.totalPrice', $this->totalPrice)); - } - - if (isset($this->totalPaid)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.totalPaid', $this->totalPaid)); - } - - if (isset($this->itemTotal)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.itemTotal', $this->itemTotal)); - } - - if (isset($this->itemSubtotal)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.itemSubtotal', $this->itemSubtotal)); - } - - if (isset($this->totalQty)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.totalQty', $this->totalQty)); - } - - if (isset($this->totalWeight)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.totalWeight', $this->totalWeight)); - } - - if (isset($this->totalDiscount)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.totalDiscount', $this->totalDiscount)); - } - - if (isset($this->totalTax)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.totalTax', $this->totalTax)); - } - - // Allow true but not null - if (isset($this->isPaid) && $this->isPaid) { - $this->subQuery->andWhere(new Expression('[[commerce_orders.totalPaid]] >= [[commerce_orders.totalPrice]]')); - } - - // Allow true but not null - if (isset($this->isUnpaid) && $this->isUnpaid) { - $this->subQuery->andWhere(new Expression('[[commerce_orders.totalPaid]] < [[commerce_orders.totalPrice]]')); - } - - // Allow integer/PurchasableInterface object or array of integers/PurchasableInterface objects - if (isset($this->hasPurchasables)) { - $purchasableIds = []; - - if (!is_array($this->hasPurchasables)) { - $this->hasPurchasables = [$this->hasPurchasables]; - } - - foreach ($this->hasPurchasables as $purchasable) { - if ($purchasable instanceof PurchasableInterface) { - $purchasableIds[] = $purchasable->getId(); - } elseif (is_numeric($purchasable)) { - $purchasableIds[] = $purchasable; - } - } - - // Remove any blank purchasable IDs (if any) - $purchasableIds = array_filter($purchasableIds); - - $this->subQuery->andWhere([ - 'exists', - (new Query()) - ->from(['lineitems' => Table::LINEITEMS]) - ->where(new Expression('[[lineitems.orderId]] = [[elements.id]]')) - ->andWhere(['[[lineitems.purchasableId]]' => $purchasableIds]), - ]); - } - - if (isset($this->containsPurchasables)) { - $purchasableIds = []; - $purchasables = $this->containsPurchasables['purchasables']; - $match = $this->containsPurchasables['match']; - - if (!is_array($purchasables)) { - $purchasables = [$purchasables]; - } - - foreach ($purchasables as $purchasable) { - if ($purchasable instanceof PurchasableInterface) { - $purchasableIds[] = $purchasable->getId(); - } elseif (is_numeric($purchasable)) { - $purchasableIds[] = $purchasable; - } - } - - $purchasableIds = array_values(array_filter($purchasableIds)); - - if ($match === ContainsPurchasablesMatch::All || $match === ContainsPurchasablesMatch::Only) { - // Every requested purchasable must have its own line item (AND logic) - foreach ($purchasableIds as $id) { - $this->subQuery->andWhere([ - 'exists', - (new Query()) - ->from(['lineitems' => Table::LINEITEMS]) - ->where(new Expression('[[lineitems.orderId]] = [[elements.id]]')) - ->andWhere(['[[lineitems.purchasableId]]' => $id]), - ]); - } - - if ($match === ContainsPurchasablesMatch::Only) { - // No line items with a purchasable outside the set, and no custom line items - $this->subQuery->andWhere([ - 'not exists', - (new Query()) - ->from(['lineitems' => Table::LINEITEMS]) - ->where(new Expression('[[lineitems.orderId]] = [[elements.id]]')) - ->andWhere(['or', ['[[lineitems.purchasableId]]' => null], ['not', ['[[lineitems.purchasableId]]' => $purchasableIds]]]), - ]); - } - } else { - // ContainsPurchasablesMatch::Any: at least one of the purchasables must be in the order - $this->subQuery->andWhere([ - 'exists', - (new Query()) - ->from(['lineitems' => Table::LINEITEMS]) - ->where(new Expression('[[lineitems.orderId]] = [[elements.id]]')) - ->andWhere(['[[lineitems.purchasableId]]' => $purchasableIds]), - ]); - } - } - - // Allow true or false but not null - if (isset($this->hasTransactions)) { - $this->subQuery->andWhere([ - $this->hasTransactions ? 'exists' : 'not exists', - (new Query()) - ->from(['transactions' => Table::TRANSACTIONS]) - ->where(new Expression('[[transactions.orderId]] = [[elements.id]]')), - ]); - } - - // Allow true or false but not null - if (isset($this->hasLineItems)) { - $this->subQuery->andWhere([ - $this->hasLineItems ? 'exists' : 'not exists', - (new Query()) - ->from(['lineitems' => Table::LINEITEMS]) - ->where(new Expression('[[lineitems.orderId]] = [[elements.id]]')), - ]); - } - - if (isset($this->hasAdminNotices)) { - $this->subQuery->andWhere([ - $this->hasAdminNotices ? 'exists' : 'not exists', - (new Query()) - ->select([new Expression('1')]) - ->from(['adminNotices' => Table::ORDERNOTICES]) - ->where(new Expression('[[adminNotices.orderId]] = [[elements.id]]')) - ->andWhere(['adminNotices.noticeType' => OrderNoticeType::Admin->value]), - ]); - } - - return parent::beforePrepare(); - } -} diff --git a/src/elements/db/ProductQuery.php b/src/elements/db/ProductQuery.php deleted file mode 100644 index 17e51f00c7..0000000000 --- a/src/elements/db/ProductQuery.php +++ /dev/null @@ -1,1058 +0,0 @@ - - * - * @method Product[]|array all($db = null) - * @method Product|array|null one($db = null) - * @method Product|array|null nth(int $n, Connection $db = null) - * @author Pixel & Tonic, Inc. - * @since 2.0 - * @doc-path products-variants.md - * @prefix-doc-params - * @replace {element} product - * @replace {elements} products - * @replace {twig-method} craft.products() - * @replace {myElement} myProduct - * @replace {element-class} \craft\commerce\elements\Product - * @supports-site-params - * @supports-title-param - * @supports-slug-param - * @supports-uri-param - * @supports-status-param - * @supports-structure-params - */ -class ProductQuery extends ElementQuery -{ - /** - * @var bool|null Whether to only return products that the user has permission to view. - * @used-by editable() - */ - public ?bool $editable = null; - - /** - * @var bool|null Whether to only return products that the user has permission to save. - * @used-by savable() - * @since 5.6.0 - */ - public ?bool $savable = null; - - /** - * @var mixed The Post Date that the resulting products must have. - */ - public mixed $expiryDate = null; - - /** - * @var mixed The default price the resulting products must have. - */ - public mixed $defaultPrice = null; - - /** - * @var mixed The default height the resulting products must have. - */ - public mixed $defaultHeight = null; - - /** - * @var mixed The default length the resulting products must have. - */ - public mixed $defaultLength = null; - - /** - * @var mixed The default width the resulting products must have. - */ - public mixed $defaultWidth = null; - - /** - * @var mixed The default weight the resulting products must have. - */ - public mixed $defaultWeight = null; - - /** - * @var mixed The default sku the resulting products must have. - */ - public mixed $defaultSku = null; - - /** - * @var mixed only return products that match the resulting variant query. - */ - public mixed $hasVariant = null; - - /** - * @var mixed The Post Date that the resulting products must have. - */ - public mixed $postDate = null; - - /** - * @var mixed The product type ID(s) that the resulting products must have. - */ - public mixed $typeId = null; - - /** - * @inheritdoc - */ - protected array $defaultOrderBy = [ - 'commerce_products.postDate' => SORT_DESC, - 'elements.id' => SORT_DESC, - ]; - - /** - * @inheritdoc - */ - public function __construct($elementType, array $config = []) - { - // Default status - if (!isset($config['status'])) { - $config['status'] = 'live'; - } - - parent::__construct($elementType, $config); - } - - /** - * @inheritdoc - */ - public function init(): void - { - if (!isset($this->withStructure)) { - $this->withStructure = true; - } - - parent::init(); - } - - - /** - * @inheritdoc - */ - public function __set($name, $value) - { - match ($name) { - 'type' => $this->type($value), - 'before' => $this->before($value), - 'after' => $this->after($value), - 'defaultHeight' => $this->defaultHeight($value), - 'defaultLength' => $this->defaultLength($value), - 'defaultWidth' => $this->defaultWidth($value), - 'defaultWeight' => $this->defaultWeight($value), - 'defaultSku' => $this->defaultSku($value), - default => parent::__set($name, $value), - }; - } - - /** - * Narrows the query results based on the products’ default variant price. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `10` | of a price of 10. - * | `['and', '>= ' ~ 100, '<= ' ~ 2000]` | of a default variant price between 100 and 2000 - * - * --- - * - * ```twig - * {# Fetch {elements} of the product type with an ID of 1 #} - * {% set {elements-var} = {twig-method} - * .defaultPrice(1) - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} of the product type with an ID of 1 - * ${elements-var} = {php-method} - * ->defaultPrice(1) - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function defaultPrice(mixed $value): static - { - $this->defaultPrice = $value; - - return $this; - } - - /** - * Narrows the query results based on the products’ default variant height dimension IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | of a type with a dimension of 1. - * | `'not 1'` | not a dimension of 1. - * | `[1, 2]` | of a a dimension 1 or 2. - * | `['and', '>= ' ~ 100, '<= ' ~ 2000]` | of a dimension between 100 and 2000 - * - * --- - * - * ```twig - * {# Fetch {elements} of the product default dimension of 1 #} - * {% set {elements-var} = {twig-method} - * .defaultHeight(1) - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} of the product default dimension of 1 - * ${elements-var} = {php-method} - * ->defaultHeight(1) - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function defaultHeight(mixed $value): static - { - $this->defaultHeight = $value; - - return $this; - } - - /** - * Narrows the query results based on the products’ default variant length dimension IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | of a type with a dimension of 1. - * | `'not 1'` | not a dimension of 1. - * | `[1, 2]` | of a a dimension 1 or 2. - * | `['and', '>= ' ~ 100, '<= ' ~ 2000]` | of a dimension between 100 and 2000 - * - * --- - * - * ```twig - * {# Fetch {elements} of the product default dimension of 1 #} - * {% set {elements-var} = {twig-method} - * .defaultLength(1) - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} of the product default dimension of 1 - * ${elements-var} = {php-method} - * ->defaultLength(1) - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function defaultLength(mixed $value): static - { - $this->defaultLength = $value; - - return $this; - } - - /** - * Narrows the query results based on the products’ default variant width dimension IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | of a type with a dimension of 1. - * | `'not 1'` | not a dimension of 1. - * | `[1, 2]` | of a a dimension 1 or 2. - * | `['and', '>= ' ~ 100, '<= ' ~ 2000]` | of a dimension between 100 and 2000 - * - * --- - * - * ```twig - * {# Fetch {elements} of the product default dimension of 1 #} - * {% set {elements-var} = {twig-method} - * .defaultWidth(1) - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} of the product default dimension of 1 - * ${elements-var} = {php-method} - * ->defaultWidth(1) - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function defaultWidth(mixed $value): static - { - $this->defaultWidth = $value; - - return $this; - } - - /** - * Narrows the query results based on the products’ default variant weight dimension IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | of a type with a dimension of 1. - * | `'not 1'` | not a dimension of 1. - * | `[1, 2]` | of a a dimension 1 or 2. - * | `['and', '>= ' ~ 100, '<= ' ~ 2000]` | of a dimension between 100 and 2000 - * - * --- - * - * ```twig - * {# Fetch {elements} of the product default dimension of 1 #} - * {% set {elements-var} = {twig-method} - * .defaultWeight(1) - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} of the product default dimension of 1 - * ${elements-var} = {php-method} - * ->defaultWeight(1) - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function defaultWeight(mixed $value): static - { - $this->defaultWeight = $value; - - return $this; - } - - /** - * Narrows the query results based on the default productvariants defaultSku - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `xxx-001` | of products default SKU of `xxx-001`. - * | `'not xxx-001'` | not a default SKU of `xxx-001`. - * | `['not xxx-001', 'not xxx-002']` | of a default SKU of xxx-001 or xxx-002. - * | `['not', `xxx-001`, `xxx-002`]` | not a product default SKU of `xxx-001` or `xxx-001`. - * - * --- - * - * ```twig - * {# Fetch {elements} of the product default SKU of `xxx-001` #} - * {% set {elements-var} = {twig-method} - * .defaultSku('xxx-001') - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} of the product default SKU of `xxx-001` - * ${elements-var} = {php-method} - * ->defaultSku('xxx-001') - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function defaultSku(mixed $value): static - { - $this->defaultSku = $value; - - return $this; - } - - /** - * Narrows the query results based on the products’ types. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'foo'` | of a type with a handle of `foo`. - * | `'not foo'` | not of a type with a handle of `foo`. - * | `['foo', 'bar']` | of a type with a handle of `foo` or `bar`. - * | `['not', 'foo', 'bar']` | not of a type with a handle of `foo` or `bar`. - * | an [[ProductType|ProductType]] object | of a type represented by the object. - * - * --- - * - * ```twig - * {# Fetch {elements} with a Foo product type #} - * {% set {elements-var} = {twig-method} - * .type('foo') - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} with a Foo product type - * ${elements-var} = {php-method} - * ->type('foo') - * ->all(); - * ``` - * - * @param ProductType|string|null|array $value The property value - * @return static self reference - */ - public function type(mixed $value): static - { - // If the value is a product type handle, swap it with the product type - if (is_string($value) && ($productType = Plugin::getInstance()->getProductTypes()->getProductTypeByHandle($value))) { - $value = $productType; - } - - if ($value instanceof ProductType) { - $this->typeId = [$value->id]; - } elseif ($value !== null) { - $this->typeId = (new Query()) - ->select(['id']) - ->from([Table::PRODUCTTYPES]) - ->where(Db::parseParam('handle', $value)) - ->column(); - } else { - $this->typeId = null; - } - - return $this; - } - - /** - * Narrows the query results to only products that were posted before a certain date. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'2018-04-01'` | that were posted before 2018-04-01. - * | a [[\DateTime|DateTime]] object | that were posted before the date represented by the object. - * - * --- - * - * ```twig - * {# Fetch {elements} posted before this month #} - * {% set firstDayOfMonth = date('first day of this month') %} - * - * {% set {elements-var} = {twig-method} - * .before(firstDayOfMonth) - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} posted before this month - * $firstDayOfMonth = new \DateTime('first day of this month'); - * - * ${elements-var} = {php-method} - * ->before($firstDayOfMonth) - * ->all(); - * ``` - * - * @param string|DateTime $value The property value - * @return static self reference - */ - public function before(DateTime|string $value): static - { - if ($value instanceof DateTime) { - $value = $value->format(DateTime::W3C); - } - - $this->postDate = ArrayHelper::toArray($this->postDate); - $this->postDate[] = '<' . $value; - - return $this; - } - - /** - * Narrows the query results to only products that were posted on or after a certain date. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'2018-04-01'` | that were posted after 2018-04-01. - * | a [[\DateTime|DateTime]] object | that were posted after the date represented by the object. - * - * --- - * - * ```twig - * {# Fetch {elements} posted this month #} - * {% set firstDayOfMonth = date('first day of this month') %} - * - * {% set {elements-var} = {twig-method} - * .after(firstDayOfMonth) - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} posted this month - * $firstDayOfMonth = new \DateTime('first day of this month'); - * - * ${elements-var} = {php-method} - * ->after($firstDayOfMonth) - * ->all(); - * ``` - * - * @param string|DateTime $value The property value - * @return static self reference - */ - public function after(DateTime|string $value): static - { - if ($value instanceof DateTime) { - $value = $value->format(DateTime::W3C); - } - - $this->postDate = ArrayHelper::toArray($this->postDate); - $this->postDate[] = '>=' . $value; - - return $this; - } - - /** - * Sets the [[$editable]] property. - * - * @param bool|null $value The property value (defaults to true) - * @return static self reference - * @uses $editable - */ - public function editable(?bool $value = true): static - { - $this->editable = $value; - return $this; - } - - /** - * Sets the [[$savable]] property. - * - * @param bool|null $value The property value (defaults to true) - * @return static self reference - * @uses $savable - * @since 5.6.0 - */ - public function savable(?bool $value = true): static - { - $this->savable = $value; - return $this; - } - - /** - * Narrows the query results based on the products’ types, per the types’ IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | of a type with an ID of 1. - * | `'not 1'` | not of a type with an ID of 1. - * | `[1, 2]` | of a type with an ID of 1 or 2. - * | `['not', 1, 2]` | not of a type with an ID of 1 or 2. - * - * --- - * - * ```twig - * {# Fetch {elements} of the product type with an ID of 1 #} - * {% set {elements-var} = {twig-method} - * .typeId(1) - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} of the product type with an ID of 1 - * ${elements-var} = {php-method} - * ->typeId(1) - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function typeId(mixed $value): static - { - $this->typeId = $value; - return $this; - } - - /** - * Narrows the query results to only products that have certain variants. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | a [[VariantQuery]] object | with variants that match the query. - * | a configuration [[array]] for a [[VariantQuery]] | with variants that match the criteria. - * - * @param VariantQuery|array $value The property value - * @return static self reference - * @noinspection PhpUnused - */ - public function hasVariant(mixed $value): static - { - $this->hasVariant = $value; - return $this; - } - - /** - * Narrows the query results based on the products’ post dates. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'>= 2018-04-01'` | that were posted on or after 2018-04-01. - * | `'< 2018-05-01'` | that were posted before 2018-05-01 - * | `['and', '>= 2018-04-04', '< 2018-05-01']` | that were posted between 2018-04-01 and 2018-05-01. - * - * --- - * - * ```twig - * {# Fetch {elements} posted last month #} - * {% set start = date('first day of last month')|atom %} - * {% set end = date('first day of this month')|atom %} - * - * {% set {elements-var} = {twig-method} - * .postDate(['and', ">= #{start}", "< #{end}"]) - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} posted last month - * $start = new \DateTime('first day of next month')->format(\DateTime::ATOM); - * $end = new \DateTime('first day of this month')->format(\DateTime::ATOM); - * - * ${elements-var} = {php-method} - * ->postDate(['and', ">= {$start}", "< {$end}"]) - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function postDate(mixed $value): static - { - $this->postDate = $value; - return $this; - } - - /** - * Narrows the query results based on the products’ expiry dates. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'>= 2020-04-01'` | that will expire on or after 2020-04-01. - * | `'< 2020-05-01'` | that will expire before 2020-05-01 - * | `['and', '>= 2020-04-04', '< 2020-05-01']` | that will expire between 2020-04-01 and 2020-05-01. - * - * --- - * - * ```twig - * {# Fetch {elements} expiring this month #} - * {% set nextMonth = date('first day of next month')|atom %} - * - * {% set {elements-var} = {twig-method} - * .expiryDate("< #{nextMonth}") - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} expiring this month - * $nextMonth = new \DateTime('first day of next month')->format(\DateTime::ATOM); - * - * ${elements-var} = {php-method} - * ->expiryDate("< {$nextMonth}") - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function expiryDate(mixed $value): static - { - $this->expiryDate = $value; - return $this; - } - - /** - * Narrows the query results based on the {elements}’ statuses. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'live'` _(default)_ | that are live. - * | `'pending'` | that are pending (enabled with a Post Date in the future). - * | `'expired'` | that are expired (enabled with an Expiry Date in the past). - * | `'disabled'` | that are disabled. - * | `['live', 'pending']` | that are live or pending. - * - * --- - * - * ```twig - * {# Fetch disabled {elements} #} - * {% set {elements-var} = {twig-method} - * .status('disabled') - * .all() %} - * ``` - * - * ```php - * // Fetch disabled {elements} - * ${elements-var} = {element-class}::find() - * ->status('disabled') - * ->all(); - * ``` - */ - public function status(array|string|null $value): static - { - parent::status($value); - return $this; - } - - /** - * @inheritdoc - */ - protected function afterPrepare(): bool - { - // Store dependent related joins to the sub query need to be done after the `elements_sites` is joined in the base `ElementQuery` class. - $customerId = Craft::$app->getUser()->getIdentity()?->id; - - $this->subQuery->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]'); - - if (Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { - $catalogPricesQuery = Plugin::getInstance() - ->getCatalogPricing() - ->createCatalogPricesQuery(userId: $customerId) - ->addSelect(['cp.purchasableId', 'cp.storeId']); - - $this->subQuery->leftJoin(['catalogprices' => $catalogPricesQuery], '[[catalogprices.purchasableId]] = [[commerce_products.defaultVariantId]] AND [[catalogprices.storeId]] = [[sitestores.storeId]]'); - } else { - // For speed in Postgres we only need this if the `defaultPrice` criteria is being used. - if (isset($this->defaultPrice)) { - $this->subQuery->leftJoin(['purchasablesstores' => Table::PURCHASABLES_STORES], '[[purchasablesstores.storeId]] = [[sitestores.storeId]] AND [[purchasablesstores.purchasableId]] = [[commerce_products.defaultVariantId]]'); - } - } - - return parent::afterPrepare(); - } - - /** - * @inheritdoc - * @throws QueryAbortedException - */ - protected function beforePrepare(): bool - { - $this->_normalizeTypeId(); - - // See if 'type' were set to invalid handles - if ($this->typeId === []) { - return false; - } - - $this->joinElementTable('commerce_products'); - - $this->query->select([ - 'commerce_products.id', - 'commerce_products.typeId', - 'commerce_products.postDate', - 'commerce_products.expiryDate', - 'purchasablesstores.basePrice as defaultBasePrice', - 'purchasablesstores.basePromotionalPrice as defaultBasePromotionalPrice', - 'commerce_products.defaultVariantId', - 'purchasables.sku as defaultSku', - 'purchasables.weight as defaultWeight', - 'purchasables.length as defaultLength', - 'purchasables.width as defaultWidth', - 'purchasables.height as defaultHeight', - 'sitestores.storeId', - ]); - - // Join in sites stores to get product's store for current request - $this->query->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]'); - $this->query->leftJoin(['purchasables' => Table::PURCHASABLES], '[[purchasables.id]] = [[commerce_products.defaultVariantId]]'); - $this->query->leftJoin(['purchasablesstores' => Table::PURCHASABLES_STORES], '[[purchasablesstores.purchasableId]] = [[commerce_products.defaultVariantId]] and [[sitestores.storeId]] = [[purchasablesstores.storeId]]'); - - // Tailor the query based on whether or not there is catalog pricing rules - if (Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { - $this->query->addSelect(['subquery.price as defaultPrice']); - $this->subQuery->addSelect(['catalogprices.price']); - - if (isset($this->defaultPrice)) { - $this->subQuery->andWhere(Db::parseParam('catalogprices.price', $this->defaultPrice)); - } - } else { - $this->query->addSelect(['purchasablesstores.basePrice as defaultPrice']); - - if (isset($this->defaultPrice)) { - $this->subQuery->andWhere(Db::parseParam('purchasablesstores.basePrice', $this->defaultPrice)); - } - } - - if (isset($this->postDate)) { - $this->subQuery->andWhere(Db::parseDateParam('commerce_products.postDate', $this->postDate)); - } - - if (isset($this->expiryDate)) { - $this->subQuery->andWhere(Db::parseDateParam('commerce_products.expiryDate', $this->expiryDate)); - } - - $this->_applyProductTypeIdParam(); - - if (isset($this->defaultHeight) || isset($this->defaultLength) || isset($this->defaultWidth) || isset($this->defaultWeight) || isset($this->defaultSku)) { - $this->subQuery->leftJoin(['purchasables' => Table::PURCHASABLES], '[[purchasables.id]] = [[commerce_products.defaultVariantId]]'); - } - - if (isset($this->defaultHeight)) { - $this->subQuery->andWhere(Db::parseParam('purchasables.height', $this->defaultHeight)); - } - - if (isset($this->defaultLength)) { - $this->subQuery->andWhere(Db::parseParam('purchasables.length', $this->defaultLength)); - } - - if (isset($this->defaultWidth)) { - $this->subQuery->andWhere(Db::parseParam('purchasables.width', $this->defaultWidth)); - } - - if (isset($this->defaultWeight)) { - $this->subQuery->andWhere(Db::parseParam('purchasables.weight', $this->defaultWeight)); - } - - if (isset($this->defaultSku)) { - $this->subQuery->andWhere(Db::parseParam('purchasables.sku', $this->defaultSku)); - } - - $this->_applyHasVariantParam(); - // Mirrors EntryQuery: "editable" means accessible in the editing UI (view permission), - // not necessarily savable. Use ->savable() to filter by save permission. - $this->_applyPermissionParam($this->editable, 'commerce-viewProductType'); - $this->_applyPermissionParam($this->savable, 'commerce-saveProductType'); - $this->_applyRefParam(); - - return parent::beforePrepare(); - } - - /** - * @inheritdoc - */ - protected function statusCondition(string $status): mixed - { - return ProductQueryHelper::statusCondition($status); - } - - /** - * Normalizes the typeId param to an array of IDs or null - */ - private function _normalizeTypeId(): void - { - if (empty($this->typeId)) { - $this->typeId = is_array($this->typeId) ? [] : null; - } elseif (is_numeric($this->typeId)) { - $this->typeId = [$this->typeId]; - } elseif (!is_array($this->typeId) || !ArrayHelper::isNumeric($this->typeId)) { - $this->typeId = (new Query()) - ->select(['id']) - ->from([Table::PRODUCTTYPES]) - ->where(Db::parseParam('id', $this->typeId)) - ->column(); - } - } - - /** - * Applies an authorization param to the query being prepared. - * - * @param bool|null $value - * @param string $permissionPrefix - * @throws QueryAbortedException - */ - private function _applyPermissionParam(?bool $value, string $permissionPrefix): void - { - if ($value === null) { - return; - } - - $user = Craft::$app->getUser()->getIdentity(); - - if (!$user) { - throw new QueryAbortedException(); - } - - $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); - - if (empty($productTypes)) { - return; - } - - $authorizedTypeIds = []; - - foreach ($productTypes as $productType) { - if ($user->can("$permissionPrefix:$productType->uid")) { - $authorizedTypeIds[] = $productType->id; - } - } - - if (count($authorizedTypeIds) === count($productTypes)) { - // They have access to everything - if (!$value) { - throw new QueryAbortedException(); - } - return; - } - - if (empty($authorizedTypeIds)) { - // They don't have access to anything - if ($value) { - throw new QueryAbortedException(); - } - return; - } - - $condition = ['commerce_products.typeId' => $authorizedTypeIds]; - - if (!$value) { - $condition = ['not', $condition]; - } - - $this->subQuery->andWhere($condition); - } - - /** - * Applies the 'productTypeId' param to the query being prepared. - */ - private function _applyProductTypeIdParam(): void - { - if ($this->typeId) { - $this->subQuery->andWhere(['commerce_products.typeId' => $this->typeId]); - - // Should we set the structureId param? - if ( - $this->withStructure !== false && - !isset($this->structureId) && - count($this->typeId) === 1 - ) { - $productType = Plugin::getInstance()->getProductTypes()->getProductTypeById(reset($this->typeId)); - if ($productType && $productType->isStructure) { - $this->structureId = $productType->structureId; - } else { - $this->withStructure = false; - } - } - } - } - - /** - * Applies the hasVariant query condition - */ - private function _applyHasVariantParam(): void - { - if ($this->hasVariant === null) { - return; - } - - if ($this->hasVariant instanceof VariantQuery) { - $variantQuery = $this->hasVariant; - } elseif (is_array($this->hasVariant)) { - $query = Variant::find(); - - $criteria = ProductQueryHelper::cleanseQueryCriteria($this->hasVariant); - - $variantQuery = Craft::configure($query, $criteria); - } else { - throw new QueryAbortedException('Invalid param used. ProductQuery::hasVariant param only expects a variant query or variant query config.'); - } - - $variantQuery->limit = null; - $variantQuery->select('commerce_variants.primaryOwnerId'); - - // Remove any blank product IDs (if any) - $variantQuery->andWhere(['not', ['commerce_variants.primaryOwnerId' => null]]); - - // Uses exists subquery for speed to check for the variant - $existsQuery = (new Query()) - ->from(['existssub' => $variantQuery]) - ->where(['existssub.primaryOwnerId' => new Expression('[[commerce_products.id]]')]); - $this->subQuery->andWhere(['exists', $existsQuery]); - } - - /** - * Applies the 'ref' param to the query being prepared. - */ - private function _applyRefParam(): void - { - if (!$this->ref) { - return; - } - - $refs = ArrayHelper::toArray($this->ref); - $joinSections = false; - $condition = ['or']; - - foreach ($refs as $ref) { - $parts = array_filter(explode('/', $ref)); - - if (!empty($parts)) { - if (count($parts) == 1) { - $condition[] = Db::parseParam('elements_sites.slug', $parts[0]); - } else { - $condition[] = [ - 'and', - Db::parseParam('commerce_producttypes.handle', $parts[0]), - Db::parseParam('elements_sites.slug', $parts[1]), - ]; - $joinSections = true; - } - } - } - - $this->subQuery->andWhere($condition); - - if ($joinSections) { - $this->subQuery->innerJoin(Table::PRODUCTTYPES . ' commerce_producttypes', '[[producttypes.id]] = [[products.typeId]]'); - } - } - - /** - * @inheritdoc - * @since 3.5.0 - */ - protected function cacheTags(): array - { - $tags = []; - - if ($this->typeId) { - foreach ($this->typeId as $typeId) { - $tags[] = "productType:$typeId"; - } - } - - return $tags; - } -} diff --git a/src/elements/db/PurchasableQuery.php b/src/elements/db/PurchasableQuery.php deleted file mode 100755 index 0b15d89ffa..0000000000 --- a/src/elements/db/PurchasableQuery.php +++ /dev/null @@ -1,923 +0,0 @@ - - * - * @method Purchasable[]|array all($db = null) - * @method Purchasable|array|null one($db = null) - * @method Purchasable|array|null nth(int $n, Connection $db = null) - * @since 5.0.0 - */ -abstract class PurchasableQuery extends ElementQuery -{ - protected array $defaultOrderBy = ['commerce_purchasables.sku' => SORT_ASC]; - - /** - * @var bool|null Whether the purchasable is available for purchase - */ - public ?bool $availableForPurchase = null; - - /** - * @var mixed the SKU of the variant - */ - public mixed $sku = null; - - /** - * @var mixed|null - */ - public mixed $price = null; - - /** - * @var mixed|null - */ - public mixed $promotionalPrice = null; - - /** - * @var bool|null - * @since 5.2.0 - */ - public bool|null $onPromotion = null; - - /** - * @var mixed|null - */ - public mixed $salePrice = null; - - /** - * @var mixed - */ - public mixed $width = false; - - /** - * @var mixed - */ - public mixed $height = false; - - /** - * @var mixed - */ - public mixed $length = false; - - /** - * @var mixed - */ - public mixed $weight = false; - - /** - * @var mixed - */ - public mixed $stock = null; - - /** - * @var bool|null - */ - public ?bool $hasStock = null; - - /** - * @var bool|null - */ - public ?bool $hasUnlimitedStock = null; - - /** - * @var mixed The shipping category ID(s) that the resulting products must have. - */ - public mixed $shippingCategoryId = null; - - /** - * @var mixed The tax category ID(s) that the resulting products must have. - */ - public mixed $taxCategoryId = null; - - /** - * @var int|false|null - */ - public int|false|null $forCustomer = null; - - /** - * @inheritdoc - */ - public function __set($name, $value) - { - match ($name) { - 'shippingCategory' => $this->shippingCategory($value), - default => parent::__set($name, $value), - }; - } - - /** - * Narrows the query results to only purchasables that are available for purchase. - * - * --- - * - * ```twig - * {# Fetch purchasables that are available for purchase #} - * {% set {elements-var} = {twig-method} - * .availableForPurchase() - * .all() %} - * ``` - * - * ```php - * // Fetch purchasables that are available for purchase - * ${elements-var} = {element-class}::find() - * ->availableForPurchase() - * ->all(); - * ``` - * - * @param bool|null $value The property value - * @return static self reference - */ - public function availableForPurchase(?bool $value = true): static - { - $this->availableForPurchase = $value; - return $this; - } - - /** - * Narrows the query results based on the {elements}’ SKUs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'foo'` | with a SKU of `foo`. - * | `'foo*'` | with a SKU that begins with `foo`. - * | `'*foo'` | with a SKU that ends with `foo`. - * | `'*foo*'` | with a SKU that contains `foo`. - * | `'not *foo*'` | with a SKU that doesn’t contain `foo`. - * | `['*foo*', '*bar*'` | with a SKU that contains `foo` or `bar`. - * | `['not', '*foo*', '*bar*']` | with a SKU that doesn’t contain `foo` or `bar`. - * - * --- - * - * ```twig - * {# Get the requested {element} SKU from the URL #} - * {% set requestedSlug = craft.app.request.getSegment(3) %} - * - * {# Fetch the {element} with that slug #} - * {% set {element-var} = {twig-method} - * .sku(requestedSlug|literal) - * .one() %} - * ``` - * - * ```php - * // Get the requested {element} SKU from the URL - * $requestedSlug = \Craft::$app->request->getSegment(3); - * - * // Fetch the {element} with that slug - * ${element-var} = {php-method} - * ->sku(\craft\helpers\Db::escapeParam($requestedSlug)) - * ->one(); - * ``` - * - * @return static self reference - */ - public function sku(mixed $value): static - { - $this->sku = $value; - return $this; - } - - /** - * Narrows the query results to only variants that have been set to unlimited stock. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `true` | with unlimited stock checked. - * | `false` | with unlimited stock not checked. - * - * @param bool|null $value - * @return static self reference - * @noinspection PhpUnused - */ - public mixed $inventoryTracked = null; - - /** - * Narrows the query results based on the variants’ stock. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `0` | with no stock. - * | `'>= 5'` | with a stock of at least 5. - * | `'< 10'` | with a stock of less than 10. - * - * @param mixed $value The property value - * @return static self reference - */ - public function stock(mixed $value): static - { - $this->stock = $value; - return $this; - } - - /** - * Narrows the query results to only variants that have stock. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `true` | with stock. - * | `false` | with no stock. - * - * @param bool|null $value - * @return static self reference - */ - public function hasStock(?bool $value = true): static - { - $this->hasStock = $value; - return $this; - } - - /** - * Narrows the pricing query results to only prices related for the specified customer. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | with user ID of `1`. - * | `false` | with prices for guest customers. - * | `null` | with prices for current user scenario. - * - * @param int|false|null $value - * @return static self reference - * @noinspection PhpUnused - */ - public function forCustomer(int|false|null $value = null): static - { - $this->forCustomer = $value; - return $this; - } - - /** - * Narrows the query results based on the variants’ width dimension. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `100` | with a width of 100. - * | `'>= 100'` | with a width of at least 100. - * | `'< 100'` | with a width of less than 100. - * - * @param mixed $value The property value - * @return static self reference - */ - public function width(mixed $value): static - { - $this->width = $value; - return $this; - } - - /** - * Narrows the query results based on the variants’ height dimension. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `100` | with a height of 100. - * | `'>= 100'` | with a height of at least 100. - * | `'< 100'` | with a height of less than 100. - * - * @param mixed $value The property value - * @return static self reference - */ - public function height(mixed $value): static - { - $this->height = $value; - return $this; - } - - /** - * Narrows the query results based on the variants’ length dimension. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `100` | with a length of 100. - * | `'>= 100'` | with a length of at least 100. - * | `'< 100'` | with a length of less than 100. - * - * @param mixed $value The property value - * @return static self reference - */ - public function length(mixed $value): static - { - $this->length = $value; - return $this; - } - - /** - * Narrows the query results based on the variants’ weight dimension. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `100` | with a weight of 100. - * | `'>= 100'` | with a weight of at least 100. - * | `'< 100'` | with a weight of less than 100. - * - * @param mixed $value The property value - * @return static self reference - */ - public function weight(mixed $value): static - { - $this->weight = $value; - return $this; - } - - /** - * Narrows the query results based on the purchasable’s price. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `100` | with a price of 100. - * | `'>= 100'` | with a price of at least 100. - * | `'< 100'` | with a price of less than 100. - * - * @param mixed $value The property value - * @return static self reference - */ - public function price(mixed $value): static - { - $this->price = $value; - return $this; - } - - /** - * Narrows the query results to only variants that have been set to not track stock. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `true` | with inventory tracked not checked. - * | `false` | with inventory tracked checked. - * - * @param bool|null $value - * @return static self reference - * @since 3.3.4 - * @noinspection PhpUnused - * @deprecated in 5.0.0. Use `inventoryTracked` instead. - */ - public function hasUnlimitedStock(?bool $value = true): static - { - $this->inventoryTracked = !$value; // reverse for backward compatibility - return $this; - } - - /** - * Narrows the query results to only variants that have been set to track stock. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `true` | with inventory tracked checked. - * | `false` | with inventory tracked not checked. - * - * @param bool|null $value - * @return static self reference - * @since 3.3.4 - * @noinspection PhpUnused - */ - public function inventoryTracked(?bool $value = true): static - { - $this->inventoryTracked = $value; - return $this; - } - - /** - * Narrows the query results based on the purchasable’s promotional price. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `100` | with a promotional price of 100. - * | `'>= 100'` | with a promotional price of at least 100. - * | `'< 100'` | with a promotional price of less than 100. - * - * @param mixed $value The property value - * @return static self reference - */ - public function promotionalPrice(mixed $value): static - { - $this->promotionalPrice = $value; - return $this; - } - - /** - * Narrows the query results based on the purchasable’s sale price. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `100` | with a sale price of 100. - * | `'>= 100'` | with a sale price of at least 100. - * | `'< 100'` | with a sale price of less than 100. - * - * @param mixed $value The property value - * @return static self reference - */ - public function salePrice(mixed $value): static - { - $this->salePrice = $value; - return $this; - } - - /** - * Narrows the query results based on the products’ shipping categories, per the shipping categories’ IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | of a shipping category with an ID of 1. - * | `'not 1'` | not of a shipping category with an ID of 1. - * | `[1, 2]` | of a shipping category with an ID of 1 or 2. - * | `['not', 1, 2]` | not of a shipping category with an ID of 1 or 2. - * - * --- - * - * ```twig - * {# Fetch {elements} of the shipping category with an ID of 1 #} - * {% set {elements-var} = {twig-method} - * .shippingCategoryId(1) - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} of the shipping category with an ID of 1 - * ${elements-var} = {php-method} - * ->shippingCategoryId(1) - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function shippingCategoryId(mixed $value): static - { - $this->shippingCategoryId = $value; - return $this; - } - - /** - * Narrows the query results based on the products’ shipping category. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'foo'` | of a shipping category with a handle of `foo`. - * | `'not foo'` | not of a shipping category with a handle of `foo`. - * | `['foo', 'bar']` | of a shipping category with a handle of `foo` or `bar`. - * | `['not', 'foo', 'bar']` | not of a shipping category with a handle of `foo` or `bar`. - * | an [[ShippingCategory|ShippingCategory]] object | of a shipping category represented by the object. - * - * --- - * - * ```twig - * {# Fetch {elements} with a Foo shipping category #} - * {% set {elements-var} = {twig-method} - * .shippingCategory('foo') - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} with a Foo shipping category - * ${elements-var} = {php-method} - * ->shippingCategory('foo') - * ->all(); - * ``` - * - * @param ShippingCategory|string|null|array $value The property value - * @return static self reference - */ - public function shippingCategory(mixed $value): static - { - if ($value instanceof ShippingCategory) { - $this->shippingCategoryId = [$value->id]; - } elseif ($value !== null) { - $this->shippingCategoryId = (new Query()) - ->from(['shippingcategories' => Table::SHIPPINGCATEGORIES]) - ->where(['shippingcategories.id' => new Expression('[[purchasables_stores.shippingCategoryId]]')]) - ->andWhere(Db::parseParam('handle', $value)); - } else { - $this->shippingCategoryId = null; - } - - return $this; - } - - /** - * Narrows the query results based on the products’ tax categories, per the tax categories’ IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | of a tax category with an ID of 1. - * | `'not 1'` | not of a tax category with an ID of 1. - * | `[1, 2]` | of a tax category with an ID of 1 or 2. - * | `['not', 1, 2]` | not of a tax category with an ID of 1 or 2. - * - * --- - * - * ```twig - * {# Fetch {elements} of the tax category with an ID of 1 #} - * {% set {elements-var} = {twig-method} - * .taxCategoryId(1) - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} of the tax category with an ID of 1 - * ${elements-var} = {php-method} - * ->taxCategoryId(1) - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function taxCategoryId(mixed $value): static - { - $this->taxCategoryId = $value; - return $this; - } - - /** - * Narrows the query results based on the products’ tax category. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'foo'` | of a tax category with a handle of `foo`. - * | `'not foo'` | not of a tax category with a handle of `foo`. - * | `['foo', 'bar']` | of a tax category with a handle of `foo` or `bar`. - * | `['not', 'foo', 'bar']` | not of a tax category with a handle of `foo` or `bar`. - * | an [[ShippingCategory|ShippingCategory]] object | of a tax category represented by the object. - * - * --- - * - * ```twig - * {# Fetch {elements} with a Foo tax category #} - * {% set {elements-var} = {twig-method} - * .taxCategory('foo') - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} with a Foo tax category - * ${elements-var} = {php-method} - * ->taxCategory('foo') - * ->all(); - * ``` - * - * @param TaxCategory|string|null|array $value The property value - * @return static self reference - */ - public function taxCategory(mixed $value): static - { - if ($value instanceof TaxCategory) { - $this->taxCategoryId = [$value->id]; - } elseif ($value !== null) { - $this->taxCategoryId = (new Query()) - ->from(['taxcategories' => Table::TAXCATEGORIES]) - ->where(['taxcategories.id' => new Expression('[[commerce_purchasables.taxCategoryId]]')]) - ->andWhere(Db::parseParam('handle', $value)); - } else { - $this->taxCategoryId = null; - } - - return $this; - } - - /** - * Return only purchasables with an active promotional price via catalog pricing rules (or which *do not* have an active promotional price). - * - * | Value | Fetches {elements}… - * | - | - - * | `true` | with a promotional price. - * | `false` | without a promotional price. - * | `null` | without taking into consideration the relationship between their price and promotional price. - * - * @param bool|null $value The property value - * @return static self reference - * @since 5.2.0 - */ - public function onPromotion(bool|null $value = true): static - { - $this->onPromotion = $value; - return $this; - } - - /** - * @inheritdoc - */ - protected function afterPrepare(): bool - { - // Store dependent related joins to the sub query need to be done after the `elements_sites` is joined in the base `ElementQuery` class. - $this->subQuery->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]'); - $this->subQuery->leftJoin(['purchasables_stores' => Table::PURCHASABLES_STORES], '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]'); - - // Only do the extra catalog pricing query join if we have catalog pricing rules. - if (Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { - $customerId = $this->forCustomer; - if ($customerId === null) { - $customerId = Craft::$app->getUser()->getIdentity()?->id; - } elseif ($customerId === false) { - $customerId = null; - } - - $catalogPricesQuery = Plugin::getInstance() - ->getCatalogPricing() - ->createCatalogPricesQuery(userId: $customerId) - ->addSelect(['cp.purchasableId', 'cp.storeId']); - - $this->subQuery->leftJoin(['catalogprices' => $catalogPricesQuery], '[[catalogprices.purchasableId]] = [[commerce_purchasables.id]] AND [[catalogprices.storeId]] = [[sitestores.storeId]]'); - } - - $this->subQuery->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]]'); - - return parent::afterPrepare(); - } - - /** - * @inheritdoc - */ - protected function beforePrepare(): bool - { - $this->joinElementTable('commerce_purchasables'); - $this->query->addSelect([ - 'commerce_purchasables.sku', - 'commerce_purchasables.width', - 'commerce_purchasables.height', - 'commerce_purchasables.length', - 'commerce_purchasables.weight', - 'commerce_purchasables.taxCategoryId', - 'purchasables_stores.availableForPurchase', - 'purchasables_stores.basePrice', - 'purchasables_stores.basePromotionalPrice', - 'purchasables_stores.freeShipping', - 'purchasables_stores.maxQty', - 'purchasables_stores.minQty', - 'purchasables_stores.inventoryTracked', - 'purchasables_stores.allowOutOfStockPurchases', - 'purchasables_stores.promotable', - 'purchasables_stores.shippingCategoryId', - 'inventoryitems.id as inventoryItemId', - ]); - - $this->query->leftJoin(Table::SITESTORES . ' sitestores', '[[elements_sites.siteId]] = [[sitestores.siteId]]'); - $this->query->leftJoin(Table::PURCHASABLES_STORES . ' purchasables_stores', '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]'); - $this->query->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]]'); - - // Only do the extra catalog pricing query join if we have catalog pricing rules. - if (Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { - $this->query->addSelect([ - 'subquery.price', - 'subquery.promotionalPrice as promotionalPrice', - 'subquery.salePrice as salePrice', - ]); - $this->subQuery->addSelect([ - 'catalogprices.price', - 'catalogprices.promotionalPrice', - 'catalogprices.salePrice', - ]); - - if (isset($this->price)) { - $this->subQuery->andWhere(Db::parseNumericParam('catalogprices.price', $this->price)); - } - - if (isset($this->promotionalPrice)) { - $this->subQuery->andWhere(Db::parseNumericParam('catalogprices.promotionalPrice', $this->promotionalPrice)); - } - - if (isset($this->onPromotion)) { - if ($this->onPromotion) { - $this->subQuery->andWhere(new Expression('[[catalogprices.promotionalPrice]] < [[catalogprices.price]]')); - } else { - // Commerce normalizes these when selecting/aggregating, so the values will actually be the same when a promotional price doesn't exist. This means it's not technically possible to distinguish between an *unset* promotional price and a promotional price that ended up being the same as the regular price. It’s also ambiguous when a pricing rule sets a `promotionalPrice` based on the original `price`! - $this->subQuery->andWhere(new Expression('[[catalogprices.price]] = [[catalogprices.promotionalPrice]]')); - } - } - - if (isset($this->salePrice)) { - $this->subQuery->andWhere(Db::parseNumericParam('catalogprices.salePrice' , $this->salePrice)); - } - } else { - // If Catalog pricing rules are not being used - $this->query->addSelect([ - 'purchasables_stores.basePrice as price', - 'purchasables_stores.basePromotionalPrice as promotionalPrice', - new Expression('CASE WHEN [[purchasables_stores.basePromotionalPrice]] < [[purchasables_stores.basePrice]] THEN [[purchasables_stores.basePromotionalPrice]] ELSE [[purchasables_stores.basePrice]] END as [[salePrice]]'), - new Expression('null as [[catalogPricingRuleId]]'), - ]); - - $this->subQuery->addSelect([ - 'purchasables_stores.basePrice as price', - 'purchasables_stores.basePromotionalPrice as promotionalPrice', - new Expression('CASE WHEN [[purchasables_stores.basePromotionalPrice]] < [[purchasables_stores.basePrice]] THEN [[purchasables_stores.basePromotionalPrice]] ELSE [[purchasables_stores.basePrice]] END as [[salePrice]]'), - ]); - - if (isset($this->price)) { - $this->subQuery->andWhere(Db::parseNumericParam('purchasables_stores.basePrice', $this->price)); - } - - if (isset($this->promotionalPrice)) { - $this->subQuery->andWhere(Db::parseNumericParam('purchasables_stores.basePromotionalPrice', $this->promotionalPrice)); - } - - if (isset($this->onPromotion)) { - if ($this->onPromotion) { - $this->subQuery->andWhere(new Expression('[[purchasables_stores.basePromotionalPrice]] < [[purchasables_stores.basePrice]]')); - } else { - $this->subQuery->andWhere(new Expression('[[purchasables_stores.basePrice]] < [[purchasables_stores.basePromotionalPrice]]')); - } - } - - if (isset($this->salePrice)) { - $this->subQuery->andWhere(Db::parseNumericParam(new Expression('CASE WHEN [[purchasables_stores.basePromotionalPrice]] < [[purchasables_stores.basePrice]] THEN [[purchasables_stores.basePromotionalPrice]] ELSE [[purchasables_stores.basePrice]] END') , $this->salePrice)); - } - } - - if (isset($this->sku)) { - $this->subQuery->andWhere(Db::parseParam('commerce_purchasables.sku', $this->sku)); - } - - // We don't join the inventory levels table, and rely on the caches store available total. - if (isset($this->stock)) { - $this->subQuery->andWhere(Db::parseParam('purchasables_stores.stock', $this->stock)); - } - - if (isset($this->inventoryTracked)) { - $this->subQuery->andWhere(Db::parseParam('purchasables_stores.inventoryTracked', $this->inventoryTracked)); - } - - if (isset($this->availableForPurchase)) { - $this->subQuery->andWhere(['purchasables_stores.availableForPurchase' => $this->availableForPurchase]); - } - - if (isset($this->sku)) { - $this->subQuery->andWhere(Db::parseParam('commerce_purchasables.sku', $this->sku)); - } - - if (isset($this->shippingCategoryId)) { - if ($this->shippingCategoryId instanceof Query) { - $shippingCategoryWhere = ['exists', $this->shippingCategoryId]; - } else { - $shippingCategoryWhere = Db::parseParam('purchasables_stores.shippingCategoryId', $this->shippingCategoryId); - } - - $this->subQuery->andWhere($shippingCategoryWhere); - } - - if (isset($this->taxCategoryId)) { - if ($this->taxCategoryId instanceof Query) { - $taxCategoryWhere = ['exists', $this->taxCategoryId]; - } else { - $taxCategoryWhere = Db::parseParam('commerce_purchasables.taxCategoryId', $this->taxCategoryId); - } - - $this->subQuery->andWhere($taxCategoryWhere); - } - - if ($this->width !== false) { - if ($this->width === null) { - $this->subQuery->andWhere(['commerce_purchasables.width' => $this->width]); - } else { - $this->subQuery->andWhere(Db::parseParam('commerce_purchasables.width', $this->width)); - } - } - - if ($this->height !== false) { - if ($this->height === null) { - $this->subQuery->andWhere(['commerce_purchasables.height' => $this->height]); - } else { - $this->subQuery->andWhere(Db::parseParam('commerce_purchasables.height', $this->height)); - } - } - - if ($this->length !== false) { - if ($this->length === null) { - $this->subQuery->andWhere(['commerce_purchasables.length' => $this->length]); - } else { - $this->subQuery->andWhere(Db::parseParam('commerce_purchasables.length', $this->length)); - } - } - - if ($this->weight !== false) { - if ($this->weight === null) { - $this->subQuery->andWhere(['commerce_purchasables.weight' => $this->weight]); - } else { - $this->subQuery->andWhere(Db::parseParam('commerce_purchasables.weight', $this->weight)); - } - } - - if (isset($this->hasStock)) { - if ($this->hasStock) { - $this->subQuery->andWhere([ - 'or', - ['purchasables_stores.inventoryTracked' => false], - [ - 'and', - ['not', ['purchasables_stores.inventoryTracked' => false]], - ['>', 'purchasables_stores.stock', 0], - ], - ]); - } else { - $this->subQuery->andWhere([ - 'and', - ['not', ['purchasables_stores.inventoryTracked' => false]], - ['<', 'purchasables_stores.stock', 1], - ]); - } - } - - return parent::beforePrepare(); - } - - /** - * @inheritdoc - */ - public function populate($rows): array - { - if (!empty($rows) && Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { - $row = ArrayHelper::firstValue($rows); - $store = Plugin::getInstance()->getStores()->getStoreBySiteId($row['siteId']); - $purchasableIds = ArrayHelper::getColumn($rows, 'id'); - $customerId = $this->forCustomer; - if ($customerId === null) { - $customerId = Craft::$app->getUser()->getIdentity()?->id; - } elseif ($customerId === false) { - $customerId = null; - } - $cprIds = Plugin::getInstance() - ->getCatalogPricing() - ->createCatalogPricesQuery(userId: $customerId, storeId: $store->id) - ->select([ - 'purchasableId', - 'storeId', - 'price', - new Expression('MIN([[catalogPricingRuleId]]) as [[catalogPricingRuleId]]'), - ]) - ->andWhere(['purchasableId' => $purchasableIds]) - ->andWhere(['not', ['catalogPricingRuleId' => null]]) - ->groupBy(['cp.purchasableId', 'cp.storeId', 'cp.price']) - ->all(); - - foreach ($cprIds as $cprId) { - foreach ($rows as &$row) { - if ($row['id'] == $cprId['purchasableId']) { - $row['catalogPricingRuleId'] = $cprId['catalogPricingRuleId']; - break; - } - } - } - } - - foreach ($rows as &$row) { - unset($row['salePrice']); - } - - return parent::populate($rows); - } -} diff --git a/src/elements/db/SubscriptionQuery.php b/src/elements/db/SubscriptionQuery.php deleted file mode 100644 index 6800c71062..0000000000 --- a/src/elements/db/SubscriptionQuery.php +++ /dev/null @@ -1,852 +0,0 @@ - - * @since 2.0 - * @doc-path subscriptions.md - * @replace {element} subscription - * @replace {elements} subscriptions - * @replace {twig-method} craft.subscriptions() - * @replace {myElement} mySubscription - * @replace {element-class} \craft\commerce\elements\Subscription - * @supports-status-param - */ -class SubscriptionQuery extends ElementQuery -{ - /** - * @var mixed The user id of the subscriber - */ - public mixed $userId = null; - - /** - * @var mixed The subscription plan id - */ - public mixed $planId = null; - - /** - * @var mixed The gateway id - */ - public mixed $gatewayId = null; - - /** - * @var mixed The id of the order that the license must be a part of. - */ - public mixed $orderId = null; - - /** - * @var mixed The gateway reference for subscription - */ - public mixed $reference = null; - - /** - * @var mixed Number of trial days for the subscription - */ - public mixed $trialDays = null; - - /** - * @var bool|null Whether the subscription is currently on trial. - */ - public ?bool $onTrial = null; - - /** - * @var mixed Time of next payment for the subscription - */ - public mixed $nextPaymentDate = null; - - /** - * @var bool|null Whether the subscription is canceled - */ - public ?bool $isCanceled = null; - - /** - * @var bool|null Whether the subscription is suspended - */ - public ?bool $isSuspended = null; - - /** - * @var mixed The date the subscription ceased to be active - */ - public mixed $dateSuspended = null; - - /** - * @var bool|null Whether the subscription has started - */ - public ?bool $hasStarted = null; - - /** - * @var mixed The time the subscription was canceled - */ - public mixed $dateCanceled = null; - - /** - * @var bool|null Whether the subscription has expired - */ - public ?bool $isExpired = null; - - /** - * @var mixed The date the subscription ceased to be active - */ - public mixed $dateExpired = null; - - /** - * @var array - */ - protected array $defaultOrderBy = ['commerce_subscriptions.dateCreated' => SORT_DESC]; - - /** - * @inheritdoc - */ - public function __construct(string $elementType, array $config = []) - { - // Default status - if (!array_key_exists('status', $config)) { - $config['status'] = Subscription::STATUS_ACTIVE; - } - - parent::__construct($elementType, $config); - } - - /** - * @inheritdoc - */ - public function __set($name, $value) - { - match ($name) { - 'user' => $this->user($value), - 'plan' => $this->plan($value), - default => parent::__set($name, $value), - }; - } - - /** - * Narrows the query results based on the subscriptions’ user accounts. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'foo'` | for a user account with a username of `foo` - * | `['foo', 'bar']` | for user accounts with a username of `foo` or `bar`. - * | a [[User|User]] object | for a user account represented by the object. - * - * --- - * - * ```twig - * {# Fetch the current user's subscriptions #} - * {% set {elements-var} = {twig-method} - * .user(currentUser) - * .all() %} - * ``` - * - * ```php - * // Fetch the current user's subscriptions - * $user = Craft::$app->user->getIdentity(); - * ${elements-var} = {php-method} - * ->user($user) - * ->all(); - * ``` - * - * @return static self reference - */ - public function user(mixed $value): SubscriptionQuery - { - if ($value instanceof User) { - $this->userId = $value->id; - } elseif ($value !== null) { - $this->userId = (new Query()) - ->select(['id']) - ->from(['{{%users}}']) - ->where(Db::parseParam('username', $value)) - ->column(); - } else { - $this->userId = null; - } - - return $this; - } - - /** - * Narrows the query results based on the subscription plan. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'foo'` | for a plan with a handle of `foo`. - * | `['foo', 'bar']` | for plans with a handle of `foo` or `bar`. - * | a [[Plan|Plan]] object | for a plan represented by the object. - * - * --- - * - * ```twig - * {# Fetch Supporter plan subscriptions #} - * {% set {elements-var} = {twig-method} - * .plan('supporter') - * .all() %} - * ``` - * - * ```php - * // Fetch Supporter plan subscriptions - * ${elements-var} = {php-method} - * ->plan('supporter') - * ->all(); - * ``` - * - * @return static self reference - */ - public function plan(mixed $value): SubscriptionQuery - { - if ($value instanceof Plan) { - $this->planId = $value->id; - } elseif ($value !== null) { - $this->planId = (new Query()) - ->select(['id']) - ->from([Table::PLANS]) - ->where(Db::parseParam('handle', $value)) - ->column(); - } else { - $this->planId = null; - } - - return $this; - } - - /** - * Narrows the query results based on the subscriptions’ user accounts’ IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | for a user account with an ID of 1. - * | `[1, 2]` | for user accounts with an ID of 1 or 2. - * | `['not', 1, 2]` | for user accounts not with an ID of 1 or 2. - * - * --- - * - * ```twig - * {# Fetch the current user's subscriptions #} - * {% set {elements-var} = {twig-method} - * .userId(currentUser.id) - * .all() %} - * ``` - * - * ```php - * // Fetch the current user's subscriptions - * $user = Craft::$app->user->getIdentity(); - * ${elements-var} = {php-method} - * ->userId($user->id) - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function userId(mixed $value): SubscriptionQuery - { - $this->userId = $value; - return $this; - } - - /** - * Narrows the query results based on the subscription plans’ IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | for a plan with an ID of 1. - * | `[1, 2]` | for plans with an ID of 1 or 2. - * | `['not', 1, 2]` | for plans not with an ID of 1 or 2. - * - * @param mixed $value The property value - * @return static self reference - */ - public function planId(mixed $value): SubscriptionQuery - { - $this->planId = $value; - return $this; - } - - /** - * Narrows the query results based on the gateway, per its ID. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | with a gateway with an ID of 1. - * | `'not 1'` | not with a gateway with an ID of 1. - * | `[1, 2]` | with a gateway with an ID of 1 or 2. - * | `['not', 1, 2]` | not with a gateway with an ID of 1 or 2. - * - * @param mixed $value The property value - * @return static self reference - */ - public function gatewayId(mixed $value): SubscriptionQuery - { - $this->gatewayId = $value; - return $this; - } - - /** - * Narrows the query results based on the order, per its ID. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | with an order with an ID of 1. - * | `'not 1'` | not with an order with an ID of 1. - * | `[1, 2]` | with an order with an ID of 1 or 2. - * | `['not', 1, 2]` | not with an order with an ID of 1 or 2. - * - * @param mixed $value The property value - * @return static self reference - */ - public function orderId(mixed $value): SubscriptionQuery - { - $this->orderId = $value; - return $this; - } - - /** - * Narrows the query results based on the reference. - * - * @param mixed $value The property value - * @return static self reference - */ - public function reference(mixed $value): SubscriptionQuery - { - $this->reference = $value; - return $this; - } - - /** - * Narrows the query results based on the number of trial days. - * - * @param mixed $value The property value - * @return static self reference - */ - public function trialDays(mixed $value): SubscriptionQuery - { - $this->trialDays = $value; - return $this; - } - - /** - * Narrows the query results to only subscriptions that are on trial. - * - * --- - * - * ```twig - * {# Fetch trialed subscriptions #} - * {% set {elements-var} = {twig-method} - * .onTrial() - * .all() %} - * ``` - * - * ```php - * // Fetch trialed subscriptions - * ${elements-var} = {element-class}::find() - * ->isPaid() - * ->all(); - * ``` - * - * @param bool|null $value The property value - * @return static self reference - */ - public function onTrial(?bool $value = true): SubscriptionQuery - { - $this->onTrial = $value; - return $this; - } - - /** - * Narrows the query results based on the subscriptions’ next payment dates. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'>= 2018-04-01'` | with a next payment on or after 2018-04-01. - * | `'< 2018-05-01'` | with a next payment before 2018-05-01 - * | `['and', '>= 2018-04-04', '< 2018-05-01']` | with a next payment between 2018-04-01 and 2018-05-01. - * - * --- - * - * ```twig - * {# Fetch {elements} with a payment due soon #} - * {% set aWeekFromNow = date('+7 days')|atom %} - * - * {% set {elements-var} = {twig-method} - * .nextPaymentDate("< #{aWeekFromNow}") - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} with a payment due soon - * $aWeekFromNow = new \DateTime('+7 days')->format(\DateTime::ATOM); - * - * ${elements-var} = {php-method} - * ->nextPaymentDate("< {$aWeekFromNow}") - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function nextPaymentDate(mixed $value): SubscriptionQuery - { - $this->nextPaymentDate = $value; - return $this; - } - - /** - * Narrows the query results to only subscriptions that are canceled. - * - * --- - * - * ```twig - * {# Fetch canceled subscriptions #} - * {% set {elements-var} = {twig-method} - * .isCanceled() - * .all() %} - * ``` - * - * ```php - * // Fetch canceled subscriptions - * ${elements-var} = {element-class}::find() - * ->isCanceled() - * ->all(); - * ``` - * - * @param bool|null $value The property value - * @return static self reference - */ - public function isCanceled(?bool $value = true): SubscriptionQuery - { - $this->isCanceled = $value; - return $this; - } - - /** - * Narrows the query results based on the subscriptions’ cancellation date. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'>= 2018-04-01'` | that were canceled on or after 2018-04-01. - * | `'< 2018-05-01'` | that were canceled before 2018-05-01 - * | `['and', '>= 2018-04-04', '< 2018-05-01']` | that were canceled between 2018-04-01 and 2018-05-01. - * - * --- - * - * ```twig - * {# Fetch {elements} that were canceled recently #} - * {% set aWeekAgo = date('7 days ago')|atom %} - * - * {% set {elements-var} = {twig-method} - * .dateCanceled(">= #{aWeekAgo}") - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} that were canceled recently - * $aWeekAgo = new \DateTime('7 days ago')->format(\DateTime::ATOM); - * - * ${elements-var} = {php-method} - * ->dateCanceled(">= {$aWeekAgo}") - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function dateCanceled(mixed $value): SubscriptionQuery - { - $this->dateCanceled = $value; - return $this; - } - - /** - * Narrows the query results to only subscriptions that have started. - * - * --- - * - * ```twig - * {# Fetch started subscriptions #} - * {% set {elements-var} = {twig-method} - * .hasStarted() - * .all() %} - * ``` - * - * ```php - * // Fetch started subscriptions - * ${elements-var} = {element-class}::find() - * ->hasStarted() - * ->all(); - * ``` - * - * @param bool|null $value The property value - * @return static self reference - */ - public function hasStarted(?bool $value = true): SubscriptionQuery - { - $this->hasStarted = $value; - return $this; - } - - /** - * Narrows the query results to only subscriptions that are suspended. - * - * --- - * - * ```twig - * {# Fetch suspended subscriptions #} - * {% set {elements-var} = {twig-method} - * .isSuspended() - * .all() %} - * ``` - * - * ```php - * // Fetch suspended subscriptions - * ${elements-var} = {element-class}::find() - * ->isSuspended() - * ->all(); - * ``` - * - * @param bool|null $value The property value - * @return static self reference - */ - public function isSuspended(?bool $value = true): SubscriptionQuery - { - $this->isSuspended = $value; - return $this; - } - - /** - * Narrows the query results based on the subscriptions’ suspension date. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'>= 2018-04-01'` | that were suspended on or after 2018-04-01. - * | `'< 2018-05-01'` | that were suspended before 2018-05-01 - * | `['and', '>= 2018-04-04', '< 2018-05-01']` | that were suspended between 2018-04-01 and 2018-05-01. - * --- - * - * ```twig - * {# Fetch {elements} that were suspended recently #} - * {% set aWeekAgo = date('7 days ago')|atom %} - * - * {% set {elements-var} = {twig-method} - * .dateSuspended(">= #{aWeekAgo}") - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} that were suspended recently - * $aWeekAgo = new \DateTime('7 days ago')->format(\DateTime::ATOM); - * - * ${elements-var} = {php-method} - * ->dateSuspended(">= {$aWeekAgo}") - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function dateSuspended(mixed $value): SubscriptionQuery - { - $this->dateSuspended = $value; - return $this; - } - - /** - * Narrows the query results to only subscriptions that have expired. - * - * --- - * - * ```twig - * {# Fetch expired subscriptions #} - * {% set {elements-var} = {twig-method} - * .isExpired() - * .all() %} - * ``` - * - * ```php - * // Fetch expired subscriptions - * ${elements-var} = {element-class}::find() - * ->isExpired() - * ->all(); - * ``` - * - * @param bool|null $value The property value - * @return static self reference - */ - public function isExpired(?bool $value = true): SubscriptionQuery - { - $this->isExpired = $value; - - return $this; - } - - /** - * Narrows the query results based on the subscriptions’ expiration date. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'>= 2018-04-01'` | that expired on or after 2018-04-01. - * | `'< 2018-05-01'` | that expired before 2018-05-01 - * | `['and', '>= 2018-04-04', '< 2018-05-01']` | that expired between 2018-04-01 and 2018-05-01. - * - * --- - * - * ```twig - * {# Fetch {elements} that expired recently #} - * {% set aWeekAgo = date('7 days ago')|atom %} - * - * {% set {elements-var} = {twig-method} - * .dateExpired(">= #{aWeekAgo}") - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} that expired recently - * $aWeekAgo = new \DateTime('7 days ago')->format(\DateTime::ATOM); - * - * ${elements-var} = {php-method} - * ->dateExpired(">= {$aWeekAgo}") - * ->all(); - * ``` - * - * @param mixed $value The property value - * @return static self reference - */ - public function dateExpired(mixed $value): SubscriptionQuery - { - $this->dateExpired = $value; - - return $this; - } - - /** - * Narrows the query results based on the {elements}’ statuses. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'active'` _(default)_ | that are active. - * | `'expired'` | that have expired. - * - * --- - * - * ```twig - * {# Fetch expired {elements} #} - * {% set {elements-var} = {twig-method} - * .status('expired') - * .all() %} - * ``` - * - * ```php - * // Fetch expired {elements} - * ${elements-var} = {element-class}::find() - * ->status('expired') - * ->all(); - * ``` - */ - public function status(array|string|null $value): static - { - parent::status($value); - if ($value === null) { - unset($this->isSuspended, $this->hasStarted); - } - - return $this; - } - - /** - * @inheritdoc - */ - protected function beforePrepare(): bool - { - // See if 'plan' were set to invalid handles - if ($this->planId === []) { - return false; - } - - $this->joinElementTable('commerce_subscriptions'); - $this->subQuery->innerJoin('{{%users}} users', '[[commerce_subscriptions.userId]] = [[users.id]]'); - - $this->query->select([ - 'commerce_subscriptions.dateCanceled', - 'commerce_subscriptions.dateExpired', - 'commerce_subscriptions.dateSuspended', - 'commerce_subscriptions.gatewayId', - 'commerce_subscriptions.hasStarted', - 'commerce_subscriptions.id', - 'commerce_subscriptions.isCanceled', - 'commerce_subscriptions.isExpired', - 'commerce_subscriptions.isSuspended', - 'commerce_subscriptions.nextPaymentDate', - 'commerce_subscriptions.orderId', - 'commerce_subscriptions.planId', - 'commerce_subscriptions.reference', - 'commerce_subscriptions.subscriptionData', - 'commerce_subscriptions.trialDays', - 'commerce_subscriptions.userId', - 'commerce_subscriptions.returnUrl', - ]); - - if (isset($this->userId)) { - $this->subQuery->andWhere(Db::parseParam('commerce_subscriptions.userId', $this->userId)); - } - - if (isset($this->planId)) { - $this->subQuery->andWhere(Db::parseParam('commerce_subscriptions.planId', $this->planId)); - } - - if (isset($this->gatewayId)) { - $this->subQuery->andWhere(Db::parseParam('commerce_subscriptions.gatewayId', $this->gatewayId)); - } - - if (isset($this->orderId)) { - $this->subQuery->andWhere(Db::parseParam('commerce_subscriptions.orderId', $this->orderId)); - } - - if (isset($this->reference)) { - $this->subQuery->andWhere(Db::parseParam('commerce_subscriptions.reference', $this->reference)); - } - - if (isset($this->trialDays)) { - $this->subQuery->andWhere(Db::parseParam('commerce_subscriptions.trialDays', $this->trialDays)); - } - - if (isset($this->nextPaymentDate)) { - $this->subQuery->andWhere(Db::parseDateParam('commerce_subscriptions.nextPaymentDate', $this->nextPaymentDate)); - } - - if (isset($this->isCanceled)) { - $this->subQuery->andWhere(Db::parseBooleanParam('commerce_subscriptions.isCanceled', $this->isCanceled, false)); - } - - if (isset($this->dateCanceled)) { - $this->subQuery->andWhere(Db::parseDateParam('commerce_subscriptions.dateCanceled', $this->dateCanceled)); - } - - // Apply default hasStarted/isSuspended filters when status is set (not null) - // and they haven't been explicitly overridden - if ($this->status !== null) { - $this->hasStarted ??= true; - $this->isSuspended ??= false; - } - - if (isset($this->hasStarted)) { - $this->subQuery->andWhere(Db::parseBooleanParam('commerce_subscriptions.hasStarted', $this->hasStarted, false)); - } - - if (isset($this->isSuspended)) { - $this->subQuery->andWhere(Db::parseBooleanParam('commerce_subscriptions.isSuspended', $this->isSuspended, false)); - } - - if (isset($this->dateSuspended)) { - $this->subQuery->andWhere(Db::parseDateParam('commerce_subscriptions.dateSuspended', $this->dateSuspended)); - } - - if (isset($this->isExpired)) { - $this->subQuery->andWhere(Db::parseBooleanParam('commerce_subscriptions.isExpired', $this->isExpired, false)); - } - - if (isset($this->dateExpired)) { - $this->subQuery->andWhere(Db::parseDateParam('commerce_subscriptions.dateExpired', $this->dateExpired)); - } - - if (isset($this->onTrial) && $this->onTrial === true) { - $this->subQuery->andWhere($this->_getTrialCondition(true)); - } elseif (isset($this->onTrial) && $this->onTrial === false) { - $this->subQuery->andWhere($this->_getTrialCondition(false)); - } - - return parent::beforePrepare(); - } - - /** - * @inheritdoc - */ - protected function statusCondition(string $status): mixed - { - return match ($status) { - Subscription::STATUS_ACTIVE => [ - 'commerce_subscriptions.isExpired' => '0', - ], - Subscription::STATUS_EXPIRED => [ - 'commerce_subscriptions.isExpired' => '1', - ], - default => parent::statusCondition($status), - }; - } - - /** - * @inheritdoc - * @deprecated in 4.0.0. `status(null)` should be used instead. - */ - public function anyStatus(): static - { - parent::status(null); - unset($this->isSuspended, $this->hasStarted); - - return $this; - } - - /** - * Returns the SQL condition to use for trial status. - * - * @param bool $onTrial - * @return mixed - */ - private function _getTrialCondition(bool $onTrial): mixed - { - if ($onTrial) { - if (Craft::$app->getDb()->getIsPgsql()) { - return new Expression("NOW() <= [[commerce_subscriptions.dateCreated]] + [[commerce_subscriptions.trialDays]] * INTERVAL '1 day'"); - } - - return new Expression('NOW() <= ADDDATE([[commerce_subscriptions.dateCreated]], [[commerce_subscriptions.trialDays]])'); - } - - if (Craft::$app->getDb()->getIsPgsql()) { - return new Expression("NOW() > [[commerce_subscriptions.dateCreated]] + [[commerce_subscriptions.trialDays]] * INTERVAL '1 day'"); - } - - return new Expression('NOW() > ADDDATE([[commerce_subscriptions.dateCreated]], [[commerce_subscriptions.trialDays]])'); - } -} diff --git a/src/elements/db/TransferQuery.php b/src/elements/db/TransferQuery.php deleted file mode 100644 index bdf77487c2..0000000000 --- a/src/elements/db/TransferQuery.php +++ /dev/null @@ -1,116 +0,0 @@ -value; - } - - $this->transferStatus = $value; - return $this; - } - - /** - * @param string|int|InventoryLocation|null $value - * @return static - */ - public function originLocation($value): self - { - if ($value instanceof InventoryLocation) { - $value = $value->id; - } - - $this->originLocation = $value; - return $this; - } - - /** - * @param string|int|InventoryLocation|null $value - * @return static - */ - public function destinationLocation($value): self - { - if ($value instanceof InventoryLocation) { - $value = $value->id; - } - - $this->destinationLocation = $value; - return $this; - } - - /** - * @var bool|null Whether to only return entries that the user has permission to save. - * @used-by savable() - * @since 4.4.0 - */ - public ?bool $savable = null; - - protected function beforePrepare(): bool - { - $this->joinElementTable(Table::TRANSFERS); - - // add selects - $this->query->select([ - 'commerce_transfers.transferStatus', - 'commerce_transfers.originLocationId', - 'commerce_transfers.destinationLocationId', - ]); - - if ($this->transferStatus) { - $this->subQuery->andWhere(['transferStatus' => $this->transferStatus]); - } - - if ($this->originLocation) { - $this->subQuery->andWhere(['originLocationId' => $this->originLocation]); - } - - if ($this->destinationLocation) { - $this->subQuery->andWhere(['destinationLocationId' => $this->destinationLocation]); - } - - return parent::beforePrepare(); - } - - /** - * @inheritdoc - */ - public function populate($rows): array - { - foreach ($rows as &$row) { - $row['transferStatus'] = TransferStatusType::from($row['transferStatus']); - } - return parent::populate($rows); - } -} diff --git a/src/elements/db/VariantQuery.php b/src/elements/db/VariantQuery.php deleted file mode 100755 index 5cb6fe0d75..0000000000 --- a/src/elements/db/VariantQuery.php +++ /dev/null @@ -1,985 +0,0 @@ - - * @since 2.0 - * @doc-path products-variants.md - * @prefix-doc-params - * @replace {element} variant - * @replace {elements} variants - * @replace {twig-method} craft.variants() - * @replace {myElement} myVariant - * @replace {element-class} \craft\commerce\elements\Variant - * @supports-site-params - * @supports-status-param - * @supports-title-param - */ -class VariantQuery extends PurchasableQuery -{ - use NestedElementQueryTrait { - cacheTags as nestedTraitCacheTags; - } - /** - * @inheritdoc - */ - protected array $defaultOrderBy = ['elements_owners.sortOrder' => SORT_ASC]; - - /** - * @var bool|null Whether to only return variants that the user has permission to view. - * @used-by editable() - */ - public ?bool $editable = null; - - /** - * @var bool|null Whether to only return variants that the user has permission to save. - * @used-by savable() - * @since 5.6.0 - */ - public ?bool $savable = null; - - /** - * @var bool|null - */ - public ?bool $hasSales = null; - - /** - * @var mixed only return variants that match the resulting product query. - */ - public mixed $hasProduct = null; - - /** - * @var bool|null - */ - public ?bool $isDefault = null; - - - /** - * @var mixed The primary owner element ID(s) that the resulting entries must belong to. - * @used-by primaryOwner() - * @used-by primaryOwnerId() - * @since 5.0.0 - */ - public mixed $primaryOwnerId = null; - - /** - * @var mixed|null - * @used-by owner() - * @used-by ownerId() - * @since 5.0.0 - */ - public mixed $ownerId = null; - - /** - * @var array|string|null The status the owner product must have. - * @used-by productStatus() - * @since 5.5.0 - */ - public array|string|null $productStatus = null; - - /** - * @var mixed - */ - public mixed $typeId = null; - - /** - * @var mixed - */ - public mixed $minQty = null; - - /** - * @var mixed - */ - public mixed $maxQty = null; - - /** - * @inheritdoc - */ - public function __construct($elementType, array $config = []) - { - // Default status - if (!isset($config['status'])) { - $config['status'] = Element::STATUS_ENABLED; - } - - parent::__construct($elementType, $config); - } - - /** - * @inheritdoc - */ - public function __set($name, $value) - { - match ($name) { - 'product' => $this->product($value), - 'productId' => $this->ownerId($value), - 'owner' => $this->owner($value), - 'primaryOwner' => $this->primaryOwner($value), - default => parent::__set($name, $value), - }; - } - - /** - * Narrows the query results based on the variants’ product. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | a [[Product|Product]] object | for a product represented by the object. - * - * @return static self reference - */ - public function product(mixed $value): VariantQuery - { - if ($value instanceof Product) { - $this->ownerId = [$value->id]; - } else { - $this->ownerId = $value; - } - return $this; - } - - /** - * Narrows the query results based on the variants’ owner. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | a [[Product|Product]] object | for a product represented by the object. - * - * @return static self reference - */ - public function owner(mixed $value): VariantQuery - { - if ($value instanceof ElementInterface) { - $this->ownerId = [$value->id]; - } else { - $this->ownerId = $value; - } - return $this; - } - - /** - * Narrows the query results based on the variants’ primary owner. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | a [[ElementInterface|ElementInterface]] object | for a product represented by the object. - * - * @return static self reference - */ - public function primaryOwner(mixed $value): VariantQuery - { - if ($value instanceof ElementInterface) { - $this->primaryOwnerId = [$value->id]; - } else { - $this->primaryOwnerId = $value; - } - return $this; - } - - /** - * Narrows the query results based on the variants’ products’ IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | for a product with an ID of 1. - * | `[1, 2]` | for product with an ID of 1 or 2. - * | `['not', 1, 2]` | for product not with an ID of 1 or 2. - * - * @return static self reference - */ - public function productId(mixed $value): VariantQuery - { - $this->ownerId = $value; - return $this; - } - - /** - * Narrows the query results based on the variants’ primary owners’ IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | for a primary owner with an ID of 1. - * | `[1, 2]` | for primary owner with an ID of 1 or 2. - * | `['not', 1, 2]` | for primary owner not with an ID of 1 or 2. - * - * @return static self reference - */ - public function primaryOwnerId(mixed $value): VariantQuery - { - $this->primaryOwnerId = $value; - return $this; - } - - /** - * Narrows the query results based on the variants’ owners’ IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | for an owner with an ID of 1. - * | `[1, 2]` | for owner with an ID of 1 or 2. - * | `['not', 1, 2]` | for owner not with an ID of 1 or 2. - * - * @return static self reference - */ - public function ownerId(mixed $value): VariantQuery - { - $this->ownerId = $value; - return $this; - } - - /** - * Narrows the query results based on the {elements}’ product’s statuses. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `'enabled'` _(default)_ | that are enabled. - * | `'disabled'` | that are disabled. - * | `['not', 'disabled']` | that are not disabled. - * - * --- - * - * ```twig - * {# Fetch {elements} with disabled products #} - * {% set {elements-var} = {twig-method} - * .productStatus('disabled') - * .all() %} - * ``` - * - * ```php - * // Fetch {elements} with disabled products - * ${elements-var} = {php-method} - * ->productStatus('disabled') - * ->all(); - * ``` - * - * @param string|string[]|null $value The property value - * @return static self reference - * @since 5.5.0 - */ - public function productStatus(array|string|null $value): VariantQuery - { - $this->productStatus = $value; - return $this; - } - - /** - * Narrows the query results based on the variants’ product types, per their IDs. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `1` | for a product of a type with an ID of 1. - * | `[1, 2]` | for product of a type with an ID of 1 or 2. - * | `['not', 1, 2]` | for product of a type not with an ID of 1 or 2. - * - * @return static self reference - */ - public function typeId(mixed $value): VariantQuery - { - $this->typeId = $value; - return $this; - } - - /** - * Narrows the query results to only default variants. - * - * --- - * - * ```twig - * {# Fetch default variants #} - * {% set {elements-var} = {twig-method} - * .isDefault() - * .all() %} - * ``` - * - * ```php - * // Fetch default variants - * ${elements-var} = {element-class}::find() - * ->isDefault() - * ->all(); - * ``` - * - * @param bool|null $value The property value - * @return static self reference - */ - public function isDefault(?bool $value = true): VariantQuery - { - $this->isDefault = $value; - return $this; - } - - /** - * Narrows the query results to only variants that are on sale. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `true` | on sale - * | `false` | not on sale - * - * @param bool|null $value - * @return static self reference - */ - public function hasSales(?bool $value = true): VariantQuery - { - $this->hasSales = $value; - return $this; - } - - /** - * Narrows the query results to only variants for certain products. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | a [[ProductQuery|ProductQuery]] object | for products that match the query. - * - * @param mixed $value The property value - * @return static self reference - */ - public function hasProduct(mixed $value = []): VariantQuery - { - $this->hasProduct = $value; - return $this; - } - - /** - * Narrows the query results based on the variants’ min quantity. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `100` | with a minQty of 100. - * | `'>= 100'` | with a minQty of at least 100. - * | `'< 100'` | with a minQty of less than 100. - * - * @param mixed $value The property value - * @return static self reference - */ - public function minQty(mixed $value): VariantQuery - { - $this->minQty = $value; - return $this; - } - - /** - * Narrows the query results based on the variants’ max quantity. - * - * Possible values include: - * - * | Value | Fetches {elements}… - * | - | - - * | `100` | with a maxQty of 100. - * | `'>= 100'` | with a maxQty of at least 100. - * | `'< 100'` | with a maxQty of less than 100. - * - * @param mixed $value The property value - * @return static self reference - */ - public function maxQty(mixed $value): VariantQuery - { - $this->maxQty = $value; - return $this; - } - - /** - * Sets the [[$editable]] property. - * - * @param bool|null $value The property value (defaults to true) - * @return static self reference - * @uses $editable - */ - public function editable(?bool $value = true): static - { - $this->editable = $value; - return $this; - } - - /** - * Sets the [[$savable]] property. - * - * @param bool|null $value The property value (defaults to true) - * @return static self reference - * @uses $savable - * @since 5.6.0 - */ - public function savable(?bool $value = true): static - { - $this->savable = $value; - return $this; - } - - /** - * @param Connection|null $db - * @return VariantCollection - * @phpstan-ignore-next-line - */ - public function collect(?Connection $db = null): VariantCollection - { - /** @phpstan-ignore-next-line */ - return VariantCollection::make(parent::collect($db)); - } - - /** - * @inheritdoc - */ - protected function beforePrepare(): bool - { - try { - $this->primaryOwnerId = $this->_normalizeOwnerId($this->primaryOwnerId); - } catch (InvalidArgumentException) { - throw new InvalidConfigException('Invalid primaryOwnerId param value'); - } - - try { - $this->ownerId = $this->_normalizeOwnerId($this->ownerId); - } catch (InvalidArgumentException) { - throw new InvalidConfigException('Invalid ownerId param value'); - } - - $this->joinElementTable('commerce_variants'); - - $this->query->select([ - 'commerce_variants.id', - 'commerce_variants.primaryOwnerId', - 'isDefault' => new Expression('CASE WHEN [[commerce_variants]].[[id]] = [[commerce_products]].[[defaultVariantId]] THEN TRUE ELSE FALSE END'), - 'commerce_products_elements_sites.slug as productSlug', - 'commerce_producttypes.handle as productTypeHandle', - ]); - - // Join in the elements_owners table - $ownersCondition = [ - 'and', - '[[elements_owners.elementId]] = [[elements.id]]', - $this->ownerId ? ['elements_owners.ownerId' => $this->ownerId] : '[[elements_owners.ownerId]] = [[commerce_variants.primaryOwnerId]]', - ]; - - $this->query - ->addSelect([ - 'elements_owners.ownerId', - 'elements_owners.sortOrder', - ]) - ->innerJoin(['elements_owners' => CraftTable::ELEMENTS_OWNERS], $ownersCondition); - - $sortOrderIndex = Db::findIndex(CraftTable::ELEMENTS_OWNERS, ['sortOrder'], false); - // Forcing the use of the `sortOrder` index only when listing (no specific element/owner filter), - // so MySQL doesn't prefer it over targeted indexes when querying by ID. - $hasSpecificFilter = !empty($this->id) || !empty($this->ownerId) || !empty($this->primaryOwnerId); - if (Craft::$app->getDb()->getIsMysql() && $sortOrderIndex !== null && empty($this->orderBy) && !$hasSpecificFilter) { - $elementOwnersTable = Craft::$app->getDb()->schema->getRawTableName(\craft\db\Table::ELEMENTS_OWNERS); - $this->subQuery->innerJoin([new Expression('[[' . $elementOwnersTable . ']] AS elements_owners USE INDEX (' . $sortOrderIndex . ')')], $ownersCondition); - } else { - $this->subQuery->innerJoin(['elements_owners' => CraftTable::ELEMENTS_OWNERS], $ownersCondition); - } - - if ($this->primaryOwnerId) { - $this->subQuery->andWhere(['commerce_variants.primaryOwnerId' => $this->primaryOwnerId]); - } - - $this->query->leftJoin(Table::PRODUCTS . ' commerce_products', '[[elements_owners.ownerId]] = [[commerce_products.id]]'); - $this->query->leftJoin(Table::PRODUCTTYPES . ' commerce_producttypes', '[[commerce_products.typeId]] = [[commerce_producttypes.id]]'); - $this->query->leftJoin(CraftTable::ELEMENTS_SITES . ' commerce_products_elements_sites', '[[elements_owners.ownerId]] = [[commerce_products_elements_sites.elementId]] and [[commerce_products_elements_sites.siteId]] = [[elements_sites.siteId]]'); - - $this->subQuery->leftJoin(Table::PRODUCTS . ' commerce_products', '[[elements_owners.ownerId]] = [[commerce_products.id]]'); - $this->subQuery->leftJoin(Table::PRODUCTTYPES . ' commerce_producttypes', '[[commerce_products.typeId]] = [[commerce_producttypes.id]]'); - - if (isset($this->typeId)) { - $this->subQuery->andWhere(Db::parseParam('commerce_products.typeId', $this->typeId)); - } - - if (isset($this->productId)) { - $this->subQuery->andWhere(['commerce_variants.primaryOwnerId' => $this->productId]); - } - - if (isset($this->productStatus)) { - $this->_applyProductStatusParam(); - } - - if (isset($this->isDefault)) { - $this->subQuery->andWhere(Db::parseBooleanParam('isDefault', $this->isDefault, false)); - } - - if (isset($this->minQty)) { - $this->subQuery->andWhere(Db::parseParam('commerce_variants.minQty', $this->minQty)); - } - - if (isset($this->maxQty)) { - $this->subQuery->andWhere(Db::parseParam('commerce_variants.maxQty', $this->maxQty)); - } - - // If width, height or length is specified in the query we should only be looking for products that - // have a type which supports dimensions - if ($this->width !== false || $this->height !== false || $this->length !== false || $this->weight !== false) { - $this->subQuery->andWhere(Db::parseParam('commerce_producttypes.hasDimensions', 1)); - } - - if (isset($this->hasSales)) { - if (!Plugin::getInstance()->getSales()->canUseSales()) { - Craft::$app->getDeprecator()->log('VariantQuery::hasSales', 'The `hasSales` parameter and Sales have been deprecated, use Pricing Rules instead.'); - return false; - } - - $now = new DateTime(); - $activeSales = (new Query())->select([ - 'sales.id', - 'sales.allGroups', - 'sales.allPurchasables', - 'sales.allCategories', - 'sales.categoryRelationshipType', - ]) - ->from(Table::SALES . ' sales') - ->where([ - 'or', - // Only a from date - [ - 'and', - ['dateTo' => null], - ['not', ['dateFrom' => null]], - ['<=', 'dateFrom', Db::prepareDateForDb($now)], - ], - // Only a to date - [ - 'and', - ['dateFrom' => null], - ['not', ['dateTo' => null]], - ['>=', 'dateTo', Db::prepareDateForDb($now)], - ], - // no dates - [ - 'dateFrom' => null, - 'dateTo' => null, - ], - // to and from dates - [ - 'and', - ['not', ['dateFrom' => null]], - ['not', ['dateTo' => null]], - ['<=', 'dateFrom', Db::prepareDateForDb($now)], - ['>=', 'dateTo', Db::prepareDateForDb($now)], - ], - ]) - ->andWhere(['enabled' => true]) - ->orderBy('sortOrder asc') - ->all(); - - $allVariantsMatch = false; - foreach ($activeSales as $activeSale) { - if ($activeSale['allGroups'] == 1 && $activeSale['allPurchasables'] == 1 && $activeSale['allCategories'] == 1) { - $allVariantsMatch = true; - break; - } - } - - if (!$allVariantsMatch) { - $activeSaleIds = ArrayHelper::getColumn($activeSales, 'id'); - - // Only force user group restriction on site requests - if (Craft::$app->getRequest()->isSiteRequest) { - $user = Craft::$app->getUser()->getIdentity(); - $userGroupIds = []; - - if ($user) { - $userGroupIds = ArrayHelper::getColumn($user->getGroups(), 'id'); - } - - // If the user doesn't belong to any groups, remove sales that - // restrict by user group as these would never match - if (empty($userGroupIds)) { - foreach ($activeSales as $activeSale) { - if ($activeSale['allGroups'] == 0) { - ArrayHelper::removeValue($activeSaleIds, $activeSale['id']); - break; - } - } - } else { - // Exclude any sales that have a user group restriction that the current user is not part of - $userGroupSalesIds = (new Query()) - ->select('sales.id') - ->from(Table::SALES . ' sales') - ->leftJoin(Table::SALE_USERGROUPS . ' su', '[[su.saleId]] = [[sales.id]]') - ->where([ - 'sales.id' => $activeSaleIds, - 'userGroupId' => $userGroupIds, - ]) - ->column(); - - foreach ($activeSales as $activeSale) { - if ($activeSale['allGroups'] == 0 && !in_array($activeSale['id'], $userGroupSalesIds, false)) { - ArrayHelper::removeValue($activeSaleIds, $activeSale['id']); - } - } - } - } - - $activeSales = ArrayHelper::whereMultiple($activeSales, ['id' => $activeSaleIds]); - - // Check to see if we have any sales that match all products and categories - // so we can skip extra processing if needed - $allProductsAndCategoriesSales = ArrayHelper::whereMultiple($activeSales, ['allPurchasables' => 1, 'allCategories' => 1]); - $hasSalesVariantConditions = []; - $hasSalesProductConditions = []; - - if (empty($allProductsAndCategoriesSales)) { - $purchasableRestrictedSales = ArrayHelper::whereMultiple($activeSales, ['allPurchasables' => 0]); - $categoryRestrictedSales = ArrayHelper::whereMultiple($activeSales, ['allCategories' => 0]); - - $purchasableRestrictedQuery = (new Query()) - ->select('purchasableId') - ->from(Table::SALE_PURCHASABLES . ' sp') - ->where([ - 'saleId' => ArrayHelper::getColumn($purchasableRestrictedSales, 'id'), - ]); - $hasSalesVariantConditions[] = ['commerce_variants.id' => $purchasableRestrictedQuery]; - - if (!empty($categoryRestrictedSales)) { - $sourceSales = ArrayHelper::whereMultiple($categoryRestrictedSales, [ - 'categoryRelationshipType' => [ - Sale::CATEGORY_RELATIONSHIP_TYPE_SOURCE, - Sale::CATEGORY_RELATIONSHIP_TYPE_BOTH, - ], - ]); - $targetSales = ArrayHelper::whereMultiple($categoryRestrictedSales, [ - 'categoryRelationshipType' => [ - Sale::CATEGORY_RELATIONSHIP_TYPE_TARGET, - Sale::CATEGORY_RELATIONSHIP_TYPE_BOTH, - ], - ]); - - // Source relationships - if (!empty($sourceSales)) { - $sourceQueryProduct = (new Query()) - ->select('rel.sourceId') - ->from(Table::SALE_CATEGORIES . ' sc') - ->leftJoin(CraftTable::RELATIONS . ' rel', '[[rel.targetId]] = [[sc.categoryId]]') - ->leftJoin(CraftTable::ELEMENTS . ' elements', '[[elements.id]] = [[rel.sourceId]]') - ->leftJoin(CraftTable::ELEMENTS_SITES . ' es', '[[es.elementId]] = [[sc.categoryId]]') - ->where(['saleId' => ArrayHelper::getColumn($sourceSales, 'id')]) - ->andWhere(['elements.type' => Product::class]) - ->andWhere(Db::parseParam('es.siteId', $this->siteId)) - ->andWhere(['es.enabled' => true]); - $hasSalesProductConditions[] = ['commerce_variants.primaryOwnerId' => $sourceQueryProduct]; - - $sourceQueryVariant = (new Query()) - ->select('rel.sourceId') - ->from(Table::SALE_CATEGORIES . ' sc') - ->leftJoin(CraftTable::RELATIONS . ' rel', '[[rel.targetId]] = [[sc.categoryId]]') - ->leftJoin(CraftTable::ELEMENTS . ' elements', '[[elements.id]] = [[rel.sourceId]]') - ->leftJoin(CraftTable::ELEMENTS_SITES . ' es', '[[es.elementId]] = [[sc.categoryId]]') - ->where(['saleId' => ArrayHelper::getColumn($sourceSales, 'id')]) - ->andWhere(['elements.type' => Variant::class]) - ->andWhere(Db::parseParam('es.siteId', $this->siteId)) - ->andWhere(['es.enabled' => true]); - $hasSalesVariantConditions[] = ['commerce_variants.id' => $sourceQueryVariant]; - } - - // Target relationships - if (!empty($targetSales)) { - $targetQueryProduct = (new Query()) - ->select('rel.targetId') - ->from(Table::SALE_CATEGORIES . ' sc') - ->leftJoin(CraftTable::RELATIONS . ' rel', '[[rel.sourceId]] = [[sc.categoryId]]') - ->leftJoin(CraftTable::ELEMENTS . ' elements', '[[elements.id]] = [[rel.targetId]]') - ->leftJoin(CraftTable::ELEMENTS_SITES . ' es', '[[es.elementId]] = [[sc.categoryId]]') - ->where(['saleId' => ArrayHelper::getColumn($targetSales, 'id')]) - ->andWhere(['elements.type' => Product::class]) - ->andWhere(Db::parseParam('es.siteId', $this->siteId)) - ->andWhere(['es.enabled' => true]); - $hasSalesProductConditions[] = ['commerce_variants.primaryOwnerId' => $targetQueryProduct]; - - $targetQueryVariant = (new Query()) - ->select('rel.targetId') - ->from(Table::SALE_CATEGORIES . ' sc') - ->leftJoin(CraftTable::RELATIONS . ' rel', '[[rel.sourceId]] = [[sc.categoryId]]') - ->leftJoin(CraftTable::ELEMENTS . ' elements', '[[elements.id]] = [[rel.targetId]]') - ->leftJoin(CraftTable::ELEMENTS_SITES . ' es', '[[es.elementId]] = [[sc.categoryId]]') - ->where(['saleId' => ArrayHelper::getColumn($targetSales, 'id')]) - ->andWhere(['elements.type' => Variant::class]) - ->andWhere(Db::parseParam('es.siteId', $this->siteId)) - ->andWhere(['es.enabled' => true]); - $hasSalesVariantConditions[] = ['commerce_variants.id' => $targetQueryVariant]; - } - } - } - } - - $hasSalesCondition = ['or']; - if (!empty($hasSalesVariantConditions)) { - $hasSalesCondition[] = array_merge(['or'], $hasSalesVariantConditions); - } - - if (!empty($hasSalesProductConditions)) { - $hasSalesCondition[] = array_merge(['or'], $hasSalesProductConditions); - } - - if ($this->hasSales) { - $this->subQuery->andWhere(['purchasables_stores.promotable' => true]); - $this->subQuery->andWhere($hasSalesCondition); - } else { - $this->subQuery->andWhere(['not', $hasSalesCondition]); - } - } - - $this->_applyHasProductParam(); - $this->_applyEditableParam($this->editable, 'commerce-viewProductType'); - $this->_applyEditableParam($this->savable, 'commerce-saveProductType'); - - return parent::beforePrepare(); - } - - protected function afterPrepare(): bool - { - if (!parent::afterPrepare()) { - return false; - } - - // Due to how the element sites table are joined in the subquery we need to do this later in the process - if ($this->productStatus) { - $this->subQuery->leftJoin(CraftTable::ELEMENTS . ' product_elements', '[[product_elements.id]] = [[commerce_variants.primaryOwnerId]]'); - $this->subQuery->leftJoin(CraftTable::ELEMENTS_SITES . ' product_elements_sites', '[[product_elements_sites.elementId]] = [[commerce_variants.primaryOwnerId]] and [[product_elements_sites.siteId]] = [[elements_sites.siteId]]'); - } - - return true; - } - - /** - * Normalizes the primaryOwnerId param to an array of IDs or null - * - * @return int[]|null - * @throws InvalidArgumentException - */ - private function _normalizeOwnerId(mixed $value): ?array - { - if (empty($value)) { - return null; - } - if (is_numeric($value)) { - return [$value]; - } - if (!is_array($value) || !ArrayHelper::isNumeric($value)) { - throw new InvalidArgumentException(); - } - return $value; - } - - - /** - * Applies the hasProduct query condition - */ - private function _applyHasProductParam(): void - { - if (!isset($this->hasProduct)) { - return; - } - - if ($this->hasProduct instanceof ProductQuery) { - $productQuery = $this->hasProduct; - } elseif (is_array($this->hasProduct)) { - $productQuery = Product::find(); - - $criteria = ProductQueryHelper::cleanseQueryCriteria($this->hasProduct); - - $productQuery = Craft::configure($productQuery, $criteria); - } else { - return; - } - - $productQuery->limit = null; - $productQuery->select('commerce_products.id'); - - // Remove any blank product IDs (if any) - $productQuery->andWhere(['not', ['commerce_products.id' => null]]); - - $this->subQuery->andWhere(['commerce_variants.primaryOwnerId' => $productQuery]); - } - - /** - * Applies an authorization param to the query being prepared. - * - * @param bool|null $value - * @param string $permissionPrefix - * @throws QueryAbortedException - */ - private function _applyEditableParam(?bool $value, string $permissionPrefix): void - { - if ($value === null) { - return; - } - - $user = Craft::$app->getUser()->getIdentity(); - - if (!$user) { - throw new QueryAbortedException(); - } - - $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); - - if (empty($productTypes)) { - return; - } - - $authorizedTypeIds = []; - - foreach ($productTypes as $productType) { - if ($user->can("$permissionPrefix:$productType->uid")) { - $authorizedTypeIds[] = $productType->id; - } - } - - if (count($authorizedTypeIds) === count($productTypes)) { - // They have access to everything - if (!$value) { - throw new QueryAbortedException(); - } - return; - } - - if (empty($authorizedTypeIds)) { - // They don't have access to anything - if ($value) { - throw new QueryAbortedException(); - } - return; - } - - $condition = ['commerce_products.typeId' => $authorizedTypeIds]; - - if (!$value) { - $condition = ['not', $condition]; - } - - $this->subQuery->andWhere($condition); - } - - /** - * Applies the 'productStatus' param to the query being prepared. - * - * @since 5.5.0 - */ - private function _applyProductStatusParam(): void - { - if (!$this->productStatus) { - return; - } - - // Normalize the product status param - if (!is_array($this->productStatus)) { - $this->productStatus = StringHelper::split($this->productStatus); - } - - $statuses = array_merge($this->productStatus); - - $firstVal = strtolower(reset($statuses)); - if (in_array($firstVal, ['not', 'or'])) { - $glue = $firstVal; - array_shift($statuses); - if (!$statuses) { - return; - } - } else { - $glue = 'or'; - } - - if ($negate = ($glue === 'not')) { - $glue = 'and'; - } - - $condition = [$glue]; - - foreach ($statuses as $status) { - $status = strtolower($status); - - $statusCondition = ProductQueryHelper::statusCondition($status, 'product_'); - - if ($statusCondition === false) { - throw new QueryAbortedException('Unsupported status: ' . $status); - } - - if ($statusCondition !== null) { - if ($negate) { - $condition[] = ['not', $statusCondition]; - } else { - $condition[] = $statusCondition; - } - } - } - - $this->subQuery->andWhere($condition); - } - - /** - * @inheritdoc - * @since 3.5.0 - */ - protected function cacheTags(): array - { - $tags = []; - - if ($this->ownerId) { - foreach ($this->ownerId as $ownerId) { - $tags[] = "product:$ownerId"; - } - } - - array_push($tags, ...$this->nestedTraitCacheTags()); - - return $tags; - } -} diff --git a/src/elements/deletionblockers/OrderCustomersDeletionBlocker.php b/src/elements/deletionblockers/OrderCustomersDeletionBlocker.php deleted file mode 100644 index 31d73d2253..0000000000 --- a/src/elements/deletionblockers/OrderCustomersDeletionBlocker.php +++ /dev/null @@ -1,145 +0,0 @@ - - * @since 5.7.0 - */ -class OrderCustomersDeletionBlocker extends BaseDeletionBlocker -{ - /** - * @var Collection - */ - public Collection $orderIds; - - public function init() - { - $this->orderIds = Order::find() - ->customerId($this->elements->ids()->all()) - ->isCompleted() - ->status(null) - ->limit(null) - ->collectIds(); - - parent::init(); - } - - public function isActive(): bool - { - return $this->orderIds->isNotEmpty(); - } - - public function getSummary(): string - { - return Craft::t('commerce', '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.', [ - 'numOrders' => $this->orderIds->count(), - 'numUsers' => $this->elements->count(), - ]); - } - - public function getActions(): array - { - $numOrders = $this->orderIds->count(); - - return [ - [ - 'icon' => 'user-plus', - 'label' => Craft::t('commerce', 'Reassign {numOrders, plural, =1{order} other{orders}}', [ - 'numOrders' => $numOrders, - ]), - 'callback' => Html::jsWithVars(fn($userIds) => << { - resolve(ev.response.data.message); - }, - onCancel: () => { - reject(); - }, - }); - JS, [ - $this->elements->ids()->all(), - ]), - ], - [ - 'icon' => 'user-minus', - 'label' => Craft::t('commerce', 'Remove customer data'), - 'callback' => Html::jsWithVars(fn($orderIds) => << { - resolve(ev.response.data.message); - }, - onCancel: () => { - reject(); - }, - }); - JS, [ - $this->orderIds->all(), - ]), - ], - [ - 'icon' => 'trash', - 'label' => Craft::t('app', 'Delete {type}', [ - 'type' => $numOrders === 1 ? Order::lowerDisplayName() : Order::pluralLowerDisplayName(), - ]), - 'destructive' => true, - 'callback' => Html::jsWithVars(fn($elementType, $entryIds, $message) => << { - resolve($message); - }, - onCancel: () => { - reject(); - }, - }); - JS, [ - Order::class, - $this->orderIds->all(), - Craft::t('app', '{type} deleted.', [ - 'type' => $numOrders === 1 ? Order::displayName() : Order::pluralDisplayName(), - ]), - ]), - ], - ]; - } - - public function getDetails(): ?string - { - return Cp::elementIndexHtml(Order::class, [ - 'context' => 'pane', - 'defaultTableColumns' => [ - ['customer'], - ['orderStatus'], - ['dateOrdered'], - ], - 'defaultSort' => ['dateOrdered', 'desc'], - 'sources' => false, - 'jsSettings' => [ - 'criteria' => [ - 'customerId' => $this->elements->ids()->all(), - 'status' => null, - ], - ], - ]); - } -} diff --git a/src/elements/deletionblockers/SubscriptionCustomersDeletionBlocker.php b/src/elements/deletionblockers/SubscriptionCustomersDeletionBlocker.php deleted file mode 100644 index 71790c26a4..0000000000 --- a/src/elements/deletionblockers/SubscriptionCustomersDeletionBlocker.php +++ /dev/null @@ -1,87 +0,0 @@ - - * @since 5.7.0 - */ -class SubscriptionCustomersDeletionBlocker extends BaseDeletionBlocker -{ - public int $gatewayId; - public Collection $subscriptions; - - public function isActive(): bool - { - return $this->subscriptions->isNotEmpty(); - } - - public function getSummary(): string - { - return Craft::t('commerce', '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.', [ - 'numSubscriptions' => $this->subscriptions->count(), - 'numUsers' => $this->elements->count(), - ]); - } - - public function getActions(): array - { - $numSubscriptions = $this->subscriptions->count(); - $subscriptionIds = $this->subscriptions->map(fn(Subscription $subscription) => $subscription->id)->all(); - - return [ - [ - 'icon' => 'trash', - 'label' => Craft::t('app', 'Delete {type}', [ - 'type' => $numSubscriptions === 1 ? Subscription::lowerDisplayName() : Subscription::pluralLowerDisplayName(), - ]), - 'destructive' => true, - 'callback' => Html::jsWithVars(fn($subscriptionIds, $gatewayId) => << { - resolve(ev.response.data.message); - }, - onCancel: () => { - reject(); - }, - }); - JS, [ - $subscriptionIds, - $this->gatewayId, - ]), - ], - ]; - } - - public function getDetails(): ?string - { - return Cp::elementIndexHtml(Subscription::class, [ - 'context' => 'pane', - 'sources' => false, - 'jsSettings' => [ - 'criteria' => [ - 'id' => $this->subscriptions->map(fn(Subscription $subscription) => $subscription->id)->all(), - 'status' => null, - ], - ], - ]); - } -} diff --git a/src/elements/traits/OrderElementTrait.php b/src/elements/traits/OrderElementTrait.php deleted file mode 100644 index 5f04251d32..0000000000 --- a/src/elements/traits/OrderElementTrait.php +++ /dev/null @@ -1,860 +0,0 @@ -getFields()->getLayoutByType(self::class); - } - - /** - * @inheritdoc - */ - protected function htmlAttributes(string $context): array - { - $attributes = parent::htmlAttributes($context); - $attributes['data'] = ['number' => $this->number]; - return $attributes; - } - - /** - * @inheritdoc - */ - protected function attributeHtml(string $attribute): string - { - switch ($attribute) { - case 'orderStatus': - { - return $this->getOrderStatus() ? $this->getOrderStatus()->getLabelHtml() : ''; - } - case 'customer': - { - return $this->getCustomerLinkHtml(); - } - case 'shippingFullName': - { - return $this->getShippingAddress() ? Html::encode($this->getShippingAddress()->fullName ?? '') : ''; - } - case 'shippingFirstName': - { - return $this->getShippingAddress() ? Html::encode($this->getShippingAddress()->firstName ?? '') : ''; - } - case 'shippingLastName': - { - return $this->getShippingAddress() ? Html::encode($this->getShippingAddress()->lastName ?? '') : ''; - } - case 'billingFullName': - { - return $this->getBillingAddress() ? Html::encode($this->getBillingAddress()->fullName ?? '') : ''; - } - case 'billingFirstName': - { - return $this->getBillingAddress() ? Html::encode($this->getBillingAddress()->firstName ?? '') : ''; - } - case 'billingLastName': - { - return $this->getBillingAddress() ? Html::encode($this->getBillingAddress()->lastName ?? '') : ''; - } - case 'shippingOrganizationName': - { - return $this->getShippingAddress() ? Html::encode($this->getShippingAddress()->organization ?? '') : ''; - } - case 'billingOrganizationName': - { - return $this->getBillingAddress() ? Html::encode($this->getBillingAddress()->organization ?? '') : ''; - } - case 'shippingMethodName': - { - return Html::encode($this->shippingMethodName ?? ''); - } - case 'gatewayName': - { - return Html::encode($this->getGateway()->name ?? ''); - } - case 'paidStatus': - { - return $this->getPaidStatusHtml(); - } - case 'totalPaid': - { - return $this->storedTotalPaidAsCurrency; - } - case 'itemTotal': - { - return $this->storedItemTotalAsCurrency; - } - case 'itemSubtotal': - { - return $this->storedItemSubtotalAsCurrency; - } - case 'totalQty': - { - return (string)$this->storedTotalQty; - } - case 'total': - { - return $this->totalAsCurrency; - } - case 'totalPrice': - { - return $this->storedTotalPriceAsCurrency; - } - case 'totalShippingCost': - { - return $this->storedTotalShippingCostAsCurrency; - } - case 'totalDiscount': - { - return $this->storedTotalDiscountAsCurrency; - } - case 'totalTax': - { - return $this->storedTotalTaxAsCurrency; - } - case 'totalIncludedTax': - { - return $this->storedTotalTaxIncludedAsCurrency; - } - case 'totals': - { - $miniTable = []; - - $miniTable[] = [ - 'label' => Craft::t('commerce', 'Qty'), - 'value' => $this->storedTotalQty, - ]; - - if ($this->itemSubtotal > 0) { - $miniTable[] = [ - 'label' => Craft::t('commerce', 'Items'), - 'value' => $this->itemSubtotalAsCurrency, - ]; - } - - if ($this->storedTotalDiscount < 0) { - $miniTable[] = [ - 'label' => Craft::t('commerce', 'Discounts'), - 'value' => $this->storedTotalDiscountAsCurrency, - ]; - } - - if ($this->storedTotalShippingCost > 0) { - $miniTable[] = [ - 'label' => Craft::t('commerce', 'Shipping'), - 'value' => $this->storedTotalShippingCostAsCurrency, - ]; - } - - if ($this->storedTotalTaxIncluded > 0) { - $miniTable[] = [ - 'label' => Craft::t('commerce', 'Tax (inc)'), - 'value' => $this->storedTotalTaxIncludedAsCurrency, - ]; - } - - if ($this->storedTotalTax > 0) { - $miniTable[] = [ - 'label' => Craft::t('commerce', 'Tax'), - 'value' => $this->storedTotalTaxAsCurrency, - ]; - } - - if ($this->storedTotalPrice > 0) { - $miniTable[] = [ - 'label' => Craft::t('commerce', 'Price'), - 'value' => $this->storedTotalPriceAsCurrency, - ]; - } - - return $this->_miniTable($miniTable); - } - case 'orderSite': - { - $site = Craft::$app->getSites()->getSiteById($this->orderSiteId); - return Html::encode($site->name ?? ''); - } - case 'hasAdminNotices': - { - if (!$this->hasAdminNotices()) { - return ''; - } - return Cp::statusLabelHtml(['color' => 'red', 'label' => Craft::t('commerce', 'Yes')]); - } - default: - { - return parent::attributeHtml($attribute); - } - } - } - - /** - * @inheritdoc - */ - protected static function defineSearchableAttributes(): array - { - return [ - 'billingFirstName', - 'billingLastName', - 'billingFullName', - 'billingAddress', - 'email', - 'number', - 'shippingFirstName', - 'shippingLastName', - 'shippingFullName', - 'shippingAddress', - 'shortNumber', - 'transactionReference', - 'username', - 'reference', - 'skus', - 'lineItemDescriptions', - 'customerName', - ]; - } - - /** - * @inheritdoc - * @noinspection PhpUnused - */ - public function getSearchKeywords(string $attribute): string - { - switch ($attribute) { - case 'billingFirstName': - return $this->billingAddress->firstName ?? ''; - case 'billingLastName': - return $this->billingAddress->lastName ?? ''; - case 'billingFullName': - return $this->billingAddress->fullName ?? ''; - case 'billingAddress': - $address = $this->getBillingAddress(); - return $address ? Craft::$app->getAddresses()->formatAddress($address) : ''; - case 'shippingFirstName': - return $this->shippingAddress->firstName ?? ''; - case 'shippingLastName': - return $this->shippingAddress->lastName ?? ''; - case 'shippingFullName': - return $this->shippingAddress->fullName ?? ''; - case 'shippingAddress': - $address = $this->getShippingAddress(); - return $address ? Craft::$app->getAddresses()->formatAddress($address) : ''; - case 'transactionReference': - return implode(' ', ArrayHelper::getColumn($this->getTransactions(), 'reference')); - case 'username': - return $this->getCustomer()->username ?? ''; - case 'skus': - return implode(' ', ArrayHelper::getColumn($this->getLineItems(), 'sku')); - case 'lineItemDescriptions': - return implode(' ', ArrayHelper::getColumn($this->getLineItems(), 'description')); - case 'customerName': - return $this->getCustomer()->fullName ?? ''; - default: - return parent::getSearchKeywords($attribute); - } - } - - - /** - * @inheritdoc - * @throws Exception - */ - protected static function defineSources(string $context = null): array - { - $siteHandle = Craft::$app->getRequest()->getParam('site'); - $site = $siteHandle ? Craft::$app->getSites()->getSiteByHandle($siteHandle) : Craft::$app->getSites()->getCurrentSite(); - /** @var StoreBehavior $site */ - $store = $site->getStore(); - $orderCriteria = ['isCompleted' => true, 'storeId' => $store->id]; - - $sources = [ - '*' => [ - 'key' => '*', - 'label' => Craft::t('commerce', 'All Orders'), - 'criteria' => $orderCriteria, - 'defaultSort' => ['dateOrdered', 'desc'], - 'data' => [ - 'date-attr' => 'dateOrdered', - ], - ], - ]; - - $edge = Plugin::getInstance()->getCarts()->getActiveCartEdgeDuration(); - - $criteriaActive = ['dateUpdated' => ['>= ' . $edge], 'isCompleted' => false]; - $criteriaInactive = ['dateUpdated' => ['< ' . $edge], 'isCompleted' => false]; - $criteriaAttemptedPayment = ['hasTransactions' => true, 'isCompleted' => false]; - - $orderStatuses = Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses($store->id)->all(); - - $sources[] = ['heading' => $store->getName()]; - - foreach ($orderStatuses as $orderStatus) { - $key = 'orderStatus:' . $orderStatus->handle; - - $sources[$key] = [ - 'key' => $key, - 'status' => $orderStatus->color, - 'label' => Craft::t('site', $orderStatus->name), - 'badgeCount' => 0, - 'criteria' => ArrayHelper::merge($orderCriteria, ['orderStatusId' => $orderStatus->id]), - 'defaultSort' => ['dateOrdered', 'desc'], - 'data' => [ - 'handle' => $orderStatus->handle, - 'date-attr' => 'dateOrdered', - ], - ]; - } - - $sources[] = [ - 'key' => 'carts:active:' . $store->handle, - 'label' => Craft::t('commerce', 'Active Carts'), - 'criteria' => ArrayHelper::merge($criteriaActive, ['storeId' => $store->id]), - 'defaultSort' => ['commerce_orders.dateUpdated', 'asc'], - 'data' => [ - 'handle' => 'cartsActive', - 'date-attr' => 'dateUpdated', - ], - ]; - - $sources[] = [ - 'key' => 'carts:inactive:' . $store->handle, - 'label' => Craft::t('commerce', 'Inactive Carts'), - 'criteria' => ArrayHelper::merge($criteriaInactive, ['storeId' => $store->id]), - 'defaultSort' => ['commerce_orders.dateUpdated', 'desc'], - 'data' => [ - 'handle' => 'cartsInactive', - 'date-attr' => 'dateUpdated', - ], - ]; - - $sources[] = [ - 'key' => 'carts:attempted-payment:' . $store->handle, - 'label' => Craft::t('commerce', 'Attempted Payments'), - 'criteria' => ArrayHelper::merge($criteriaAttemptedPayment, ['storeId' => $store->id]), - 'defaultSort' => ['commerce_orders.dateUpdated', 'desc'], - 'data' => [ - 'handle' => 'cartsAttemptedPayment', - 'date-attr' => 'dateUpdated', - ], - ]; - - return $sources; - } - - /** - * @inheritdoc - */ - protected static function defineActions(string $source): array - { - $actions = parent::defineActions($source); - - if (Craft::$app->getUser()->checkPermission('commerce-manageOrders')) { - /** @var StoreBehavior|Site $site */ - $site = Cp::requestedSite(); - $store = $site->getStore(); - // Remove nested "all" prefix if it exists at the start of the string - $source = str_starts_with($source, '*/') ? substr($source, 2) : $source; - - - $elementService = Craft::$app->getElements(); - - if ($store && Plugin::getInstance()->getPdfs()->getHasEnabledPdf($store->id)) { - $actions[] = $elementService->createAction([ - 'type' => DownloadOrderPdfAction::class, - 'storeId' => $store->id, - ]); - } - - if (Craft::$app->getUser()->checkPermission('commerce-deleteOrders')) { - $deleteAction = $elementService->createAction( - [ - 'type' => Delete::class, - 'confirmationMessage' => Craft::t('commerce', 'Are you sure you want to delete the selected orders?'), - 'successMessage' => Craft::t('commerce', 'Orders deleted.'), - ] - ); - $actions[] = $deleteAction; - } - - if (Craft::$app->getUser()->checkPermission('commerce-editOrders')) { - // Only allow mass updating order status when all selected are of the same status, and not carts. - $isStatus = strpos($source, 'orderStatus:'); - if ($isStatus === 0) { - $updateOrderStatusAction = $elementService->createAction([ - 'type' => UpdateOrderStatus::class, - ]); - $actions[] = $updateOrderStatusAction; - } - - $isStatus = strpos($source, 'carts:'); - if ($isStatus === 0) { - $updateOrderStatusAction = $elementService->createAction([ - 'type' => CopyLoadCartUrl::class, - ]); - $actions[] = $updateOrderStatusAction; - } - } - - if (Craft::$app->getUser()->checkPermission('commerce-deleteOrders')) { - // Restore - $actions[] = Craft::$app->getElements()->createAction([ - 'type' => Restore::class, - 'successMessage' => Craft::t('commerce', 'Orders restored.'), - 'partialSuccessMessage' => Craft::t('commerce', 'Some orders restored.'), - 'failMessage' => Craft::t('commerce', 'Orders not restored.'), - ]); - } - } - - return $actions; - } - - /** - * @inheritDoc - */ - protected static function defineExporters(string $source): array - { - $default = parent::defineExporters($source); - // Remove the standard expanded exporter and use our own - ArrayHelper::removeValue($default, CraftExpanded::class); - $default[] = Expanded::class; - - return $default; - } - - /** - * @inheritdoc - */ - protected static function defineTableAttributes(): array - { - return array_merge(parent::defineTableAttributes(), [ - 'reference' => ['label' => Craft::t('commerce', 'Reference')], - 'shortNumber' => ['label' => Craft::t('commerce', 'Short Number')], - 'number' => ['label' => Craft::t('commerce', 'Number')], - 'id' => ['label' => Craft::t('commerce', 'ID')], - 'orderStatus' => ['label' => Craft::t('commerce', 'Status')], - 'totals' => ['label' => Craft::t('commerce', 'All Totals')], - 'totalQty' => ['label' => Craft::t('commerce', 'Total Qty')], - 'total' => ['label' => Craft::t('commerce', 'Total')], - 'totalPrice' => ['label' => Craft::t('commerce', 'Total Price')], - 'totalPaid' => ['label' => Craft::t('commerce', 'Total Paid')], - 'totalDiscount' => ['label' => Craft::t('commerce', 'Total Discount')], - 'totalShippingCost' => ['label' => Craft::t('commerce', 'Total Shipping')], - 'totalTax' => ['label' => Craft::t('commerce', 'Total Tax')], - 'totalIncludedTax' => ['label' => Craft::t('commerce', 'Total Included Tax')], - 'dateOrdered' => ['label' => Craft::t('commerce', 'Date Ordered')], - 'datePaid' => ['label' => Craft::t('commerce', 'Date Paid')], - 'dateFirstPaid' => ['label' => Craft::t('commerce', 'Date First Paid')], - 'dateCreated' => ['label' => Craft::t('commerce', 'Date Created')], - 'dateUpdated' => ['label' => Craft::t('commerce', 'Date Updated')], - 'email' => ['label' => Craft::t('commerce', 'Email')], - 'customer' => ['label' => Craft::t('commerce', 'Customer')], - 'shippingFullName' => ['label' => Craft::t('commerce', 'Shipping Full Name')], - 'shippingFirstName' => ['label' => Craft::t('commerce', 'Shipping First Name')], - 'shippingLastName' => ['label' => Craft::t('commerce', 'Shipping Last Name')], - 'billingFullName' => ['label' => Craft::t('commerce', 'Billing Full Name')], - 'billingFirstName' => ['label' => Craft::t('commerce', 'Billing First Name')], - 'billingLastName' => ['label' => Craft::t('commerce', 'Billing Last Name')], - 'shippingOrganizationName' => ['label' => Craft::t('commerce', 'Shipping Business Name')], - 'billingOrganizationName' => ['label' => Craft::t('commerce', 'Billing Business Name')], - 'shippingMethodName' => ['label' => Craft::t('commerce', 'Shipping Method')], - 'gatewayName' => ['label' => Craft::t('commerce', 'Gateway')], - 'paidStatus' => ['label' => Craft::t('commerce', 'Paid Status')], - 'couponCode' => ['label' => Craft::t('commerce', 'Coupon Code')], - 'itemTotal' => ['label' => Craft::t('commerce', 'Item Total')], - 'itemSubtotal' => ['label' => Craft::t('commerce', 'Item Subtotal')], - 'orderSite' => ['label' => Craft::t('commerce', 'Order Site')], - 'hasAdminNotices' => ['label' => Craft::t('commerce', 'Admin Notices')], - ]); - } - - /** - * @inheritdoc - */ - protected static function defineDefaultTableAttributes(string $source = null): array - { - $attributes = []; - $attributes[] = 'order'; - - if (!str_starts_with($source, 'carts:')) { - // For orders (including order status sources) - $attributes[] = 'reference'; - if (!str_starts_with($source, 'orderStatus:')) { - // Only show status column when not filtered by status - $attributes[] = 'orderStatus'; - } - $attributes[] = 'customer'; - $attributes[] = 'dateOrdered'; - $attributes[] = 'datePaid'; - $attributes[] = 'dateFirstPaid'; - $attributes[] = 'totalPaid'; - $attributes[] = 'paidStatus'; - $attributes[] = 'totals'; - } else { - // For carts - $attributes[] = 'shortNumber'; - $attributes[] = 'dateUpdated'; - $attributes[] = 'totalPrice'; - } - - return $attributes; - } - - /** - * @inheritdoc - */ - public static function prepElementQueryForTableAttribute(ElementQueryInterface $elementQuery, string $attribute): void - { - /** @var OrderQuery $elementQuery */ - - match ($attribute) { - 'totals', 'total', 'totalPrice', 'totalDiscount', 'totalShippingCost', 'totalTax', 'totalIncludedTax' => $elementQuery->withAdjustments(), - 'totalPaid', 'paidStatus' => $elementQuery->withTransactions(), - 'shippingFullName', 'shippingFirstName', 'shippingLastName', 'billingFullName', 'billingFirstName', 'billingLastName', 'shippingOrganizationName', 'billingOrganizationName', 'shippingMethodName' => $elementQuery->withAddresses(), - 'email', 'customer' => $elementQuery->withCustomer(), - 'itemTotal', 'itemSubtotal' => $elementQuery->withLineItems(), - default => parent::prepElementQueryForTableAttribute($elementQuery, $attribute), - }; - } - - /** - * @inheritdoc - * @return OrderCondition - */ - public static function createCondition(): ElementConditionInterface - { - return Craft::createObject(OrderCondition::class, [static::class]); - } - - /** - * @inheritdoc - */ - protected static function defineSortOptions(): array - { - return [ - 'number' => Craft::t('commerce', 'Number'), - 'reference' => Craft::t('commerce', 'Reference'), - 'orderStatusId' => Craft::t('commerce', 'Order Status'), - 'totalPrice' => Craft::t('commerce', 'Total Price'), - 'totalPaid' => Craft::t('commerce', 'Total Paid'), - [ - 'label' => Craft::t('commerce', 'Shipping First Name'), - 'orderBy' => 'shipping_address.firstName', - 'attribute' => 'shippingFirstName', - ], - [ - 'label' => Craft::t('commerce', 'Shipping Last Name'), - 'orderBy' => 'shipping_address.lastName', - 'attribute' => 'shippingLastName', - ], - [ - 'label' => Craft::t('commerce', 'Shipping Full Name'), - 'orderBy' => 'shipping_address.fullName', - 'attribute' => 'shippingFullName', - ], - [ - 'label' => Craft::t('commerce', 'Billing First Name'), - 'orderBy' => 'billing_address.firstName', - 'attribute' => 'billingFirstName', - ], - [ - 'label' => Craft::t('commerce', 'Billing Last Name'), - 'orderBy' => 'billing_address.lastName', - 'attribute' => 'billingLastName', - ], - [ - 'label' => Craft::t('commerce', 'Billing Full Name'), - 'orderBy' => 'billing_address.fullName', - 'attribute' => 'billingFullName', - ], - [ - 'label' => Craft::t('commerce', 'Date Ordered'), - 'orderBy' => 'dateOrdered', - 'defaultDir' => 'desc', - ], - [ - 'label' => Craft::t('commerce', 'Date Updated'), - 'orderBy' => 'commerce_orders.dateUpdated', - 'attribute' => 'dateUpdated', - 'defaultDir' => 'desc', - ], - [ - 'label' => Craft::t('commerce', 'Date Paid'), - 'orderBy' => 'datePaid', - 'defaultDir' => 'desc', - ], - [ - 'label' => Craft::t('commerce', 'Date First Paid'), - 'orderBy' => 'dateFirstPaid', - 'defaultDir' => 'desc', - ], - 'couponCode' => Craft::t('commerce', 'Coupon Code'), - [ - 'label' => Craft::t('app', 'ID'), - 'orderBy' => 'elements.id', - 'attribute' => 'id', - ], - ]; - } - - /** - * @param array $miniTable Expects an array with rows of 'label', 'value' keys values. - */ - private function _miniTable(array $miniTable): string - { - $output = ''; - foreach ($miniTable as $row) { - $output .= ''; - $output .= ''; - $output .= ''; - $output .= ''; - } - $output .= '
' . $row['label'] . '' . $row['value'] . '
'; - - return $output; - } - - /** - * @inheritdoc - */ - public static function modifyCustomSource(array $config): array - { - try { - /** @var OrderCondition $condition */ - $condition = Craft::$app->getConditions()->createCondition($config['condition']); - } catch (InvalidConfigException) { - return $config; - } - - $rules = $condition->getConditionRules(); - - // see if it's limited to one product type - /** @var OrderStatusConditionRule|null $orderStatusConditionRule */ - $orderStatusConditionRule = ArrayHelper::firstWhere($rules, fn($rule) => $rule instanceof OrderStatusConditionRule); - $orderStatusOptions = $orderStatusConditionRule?->getValues(); - - /** @var StoreBehavior $currentSite */ - $currentSite = Cp::requestedSite(); - $store = $currentSite->getStore(); - - - if ($orderStatusOptions && count($orderStatusOptions) === 1) { - $orderStatus = Plugin::getInstance()->getOrderStatuses()->getOrderStatusByUid(reset($orderStatusOptions)); - - if ($store->id != $orderStatus->storeId) { - $config['disabled'] = true; - } - - if ($orderStatus) { - $config['status'] = $orderStatus->color; - } - } - - return $config; - } - - /** - * @inheritdoc - */ - protected static function defineCardAttributes(): array - { - /** @var OrderStatus $status */ - $status = Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses()->first(); - $site = Craft::$app->getSites()->getCurrentSite(); - $number = Plugin::getInstance()->getCarts()->generateCartNumber(); - - return array_merge(parent::defineCardAttributes(), [ - 'shortNumber' => [ - 'label' => Craft::t('commerce', 'Short Number'), - 'placeholder' => substr($number, 0, 7), - ], - 'number' => [ - 'label' => Craft::t('commerce', 'Number'), - 'placeholder' => $number, - ], - 'id' => [ - 'label' => Craft::t('commerce', 'ID'), - 'placeholder' => '12345', - ], - 'orderStatus' => [ - 'label' => Craft::t('commerce', 'Status'), - 'placeholder' => $status->getLabelHtml(), - ], - 'totalQty' => [ - 'label' => Craft::t('commerce', 'Total Qty'), - 'placeholder' => '10', - ], - 'total' => [ - 'label' => Craft::t('commerce', 'Total'), - 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), - ], - 'totalPrice' => [ - 'label' => Craft::t('commerce', 'Total Price'), - 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), - ], - 'totalPaid' => [ - 'label' => Craft::t('commerce', 'Total Paid'), - 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), - ], - 'totalDiscount' => [ - 'label' => Craft::t('commerce', 'Total Discount'), - 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(12.99), - ], - 'totalShippingCost' => [ - 'label' => Craft::t('commerce', 'Total Shipping'), - 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(9.99), - ], - 'totalTax' => [ - 'label' => Craft::t('commerce', 'Total Tax'), - 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(19.99), - ], - 'totalIncludedTax' => [ - 'label' => Craft::t('commerce', 'Total Included Tax'), - 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(19.99), - ], - 'dateOrdered' => [ - 'label' => Craft::t('commerce', 'Date Ordered'), - 'placeholder' => Craft::$app->getFormattingLocale()->getFormatter()->asDate(time(), 'short'), - ], - 'datePaid' => [ - 'label' => Craft::t('commerce', 'Date Paid'), - 'placeholder' => Craft::$app->getFormattingLocale()->getFormatter()->asDate(time(), 'short'), - ], - 'dateFirstPaid' => [ - 'label' => Craft::t('commerce', 'Date First Paid'), - 'placeholder' => Craft::$app->getFormattingLocale()->getFormatter()->asDate(time(), 'short'), - ], - 'dateUpdated' => [ - 'label' => Craft::t('commerce', 'Date Updated'), - 'placeholder' => Craft::$app->getFormattingLocale()->getFormatter()->asDate(time(), 'short'), - ], - 'email' => [ - 'label' => Craft::t('commerce', 'Email'), - 'placeholder' => 'user@example.com', - ], - 'customer' => [ - 'label' => Craft::t('commerce', 'Customer'), - 'placeholder' => Craft::t('commerce', 'Customer'), - ], - 'shippingFullName' => [ - 'label' => Craft::t('commerce', 'Shipping Full Name'), - 'placeholder' => Craft::t('commerce', 'Shipping Full Name'), - ], - 'shippingFirstName' => [ - 'label' => Craft::t('commerce', 'Shipping First Name'), - 'placeholder' => Craft::t('commerce', 'Shipping First Name'), - ], - 'shippingLastName' => [ - 'label' => Craft::t('commerce', 'Shipping Last Name'), - 'placeholder' => Craft::t('commerce', 'Shipping Last Name'), - ], - 'billingFullName' => [ - 'label' => Craft::t('commerce', 'Billing Full Name'), - 'placeholder' => Craft::t('commerce', 'Billing Full Name'), - ], - 'billingFirstName' => [ - 'label' => Craft::t('commerce', 'Billing First Name'), - 'placeholder' => Craft::t('commerce', 'Billing First Name'), - ], - 'billingLastName' => [ - 'label' => Craft::t('commerce', 'Billing Last Name'), - 'placeholder' => Craft::t('commerce', 'Billing Last Name'), - ], - 'shippingOrganizationName' => [ - 'label' => Craft::t('commerce', 'Shipping Business Name'), - 'placeholder' => Craft::t('commerce', 'Shipping Business Name'), - ], - 'billingOrganizationName' => [ - 'label' => Craft::t('commerce', 'Billing Business Name'), - 'placeholder' => Craft::t('commerce', 'Billing Business Name'), - ], - 'shippingMethodName' => [ - 'label' => Craft::t('commerce', 'Shipping Method'), - 'placeholder' => Craft::t('commerce', 'Shipping Method'), - ], - 'gatewayName' => [ - 'label' => Craft::t('commerce', 'Gateway'), - 'placeholder' => Craft::t('commerce', 'Gateway'), - ], - 'paidStatus' => [ - 'label' => Craft::t('commerce', 'Paid Status'), - 'placeholder' => Cp::statusLabelHtml(['color' => 'green', 'label' => Craft::t('commerce', 'Paid')]), - ], - 'couponCode' => [ - 'label' => Craft::t('commerce', 'Coupon Code'), - 'placeholder' => 'SAVE10', - ], - 'itemTotal' => [ - 'label' => Craft::t('commerce', 'Item Total'), - 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(99.99), - ], - 'itemSubtotal' => [ - 'label' => Craft::t('commerce', 'Item Subtotal'), - 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(89.99), - ], - 'orderSite' => [ - 'label' => Craft::t('commerce', 'Order Site'), - 'placeholder' => $site->name, - ], - 'reference' => [ - 'label' => Craft::t('commerce', 'Reference'), - 'placeholder' => 'ORD-XXXXX', - ], - ]); - } - - /** - * @inheritdoc - */ - protected static function defineDefaultCardAttributes(): array - { - return array_merge(parent::defineDefaultCardAttributes(), [ - 'reference', - 'orderStatus', - 'totalPrice', - ]); - } -} diff --git a/src/elements/traits/OrderNoticesTrait.php b/src/elements/traits/OrderNoticesTrait.php deleted file mode 100644 index da33130b01..0000000000 --- a/src/elements/traits/OrderNoticesTrait.php +++ /dev/null @@ -1,173 +0,0 @@ -_notices, fn(OrderNotice $n) => $n->noticeType === OrderNoticeType::Customer)); - return $this->_filterNotices($notices, $type, $attribute); - } - - /** - * Returns admin-only notices, optionally filtered by type and/or attribute. - * - * @param string|null $type - * @param string|null $attribute - * @return OrderNotice[] - * @since 5.x - */ - public function getAdminNotices(?string $type = null, ?string $attribute = null): array - { - $notices = array_values(array_filter($this->_notices, fn(OrderNotice $n) => $n->noticeType === OrderNoticeType::Admin)); - return $this->_filterNotices($notices, $type, $attribute); - } - - /** - * Adds a new notice - * - * @since 3.3 - */ - public function addNotice(OrderNotice $notice): void - { - $notice->setOrder($this); - $this->_notices[] = $notice; - } - - /** - * Returns the first non-admin notice matching the specified type or attribute. - * - * @param null $type - * @param null $attribute - * @since 3.3 - */ - public function getFirstNotice($type = null, $attribute = null): ?OrderNotice - { - return ArrayHelper::firstValue($this->getNotices($type, $attribute)); - } - - /** - * Adds a list of notices. - * - * @param OrderNotice[] $notices an array of notices. - * @since 3.3 - */ - public function addNotices(array $notices): void - { - foreach ($notices as $notice) { - $this->addNotice($notice); - } - } - - /** - * Removes notices matching the given criteria, scoped to the specified notice types. - * - * By default only customer notices are cleared, preserving admin notices for backwards compatibility. - * Pass one or more {@see OrderNoticeType} values to control which notice types are affected. - * - * @param string|null $type type name. Use null to remove notices for all types. - * @param string|null $attribute attribute name. Use null to remove notices for all attributes. - * @param OrderNoticeType|OrderNoticeType[]|null $noticeTypes Notice type(s) to clear. Defaults to customer notices only. - * @since 3.3 - */ - public function clearNotices(?string $type = null, ?string $attribute = null, array|OrderNoticeType|null $noticeTypes = null): void - { - if ($noticeTypes === null) { - $noticeTypes = [OrderNoticeType::Customer]; - } elseif ($noticeTypes instanceof OrderNoticeType) { - $noticeTypes = [$noticeTypes]; - } - - $targetNotices = array_values(array_filter($this->_notices, fn(OrderNotice $n) => in_array($n->noticeType, $noticeTypes))); - $preservedNotices = array_values(array_filter($this->_notices, fn(OrderNotice $n) => !in_array($n->noticeType, $noticeTypes))); - - if ($type === null && $attribute === null) { - $remaining = []; - } elseif ($type !== null && $attribute === null) { - $remaining = array_values(array_filter($targetNotices, fn(OrderNotice $n) => $n->type !== $type)); - } elseif ($type === null && $attribute !== null) { - $remaining = array_values(array_filter($targetNotices, fn(OrderNotice $n) => $n->attribute !== $attribute)); - } else { - $remaining = array_values(array_filter($targetNotices, fn(OrderNotice $n) => !($n->type === $type && $n->attribute === $attribute))); - } - - $this->_notices = array_merge($preservedNotices, $remaining); - } - - /** - * Returns a value indicating whether there are any non-admin notices. - * - * @param string|null $type type name. Use null to check all types. - * @param string|null $attribute attribute name. Use null to check all attributes. - * @return bool whether there is any notices. - * @since 3.3 - */ - public function hasNotices(?string $type = null, ?string $attribute = null): bool - { - return !empty($this->getNotices($type, $attribute)); - } - - /** - * Returns whether there are any admin notices. - * - * @since 5.x - */ - public function hasAdminNotices(): bool - { - return !empty($this->getAdminNotices()); - } - - /** - * Filters an array of notices by type and/or attribute. - * - * @param OrderNotice[] $notices - * @param string|null $type - * @param string|null $attribute - * @return OrderNotice[] - */ - private function _filterNotices(array $notices, ?string $type, ?string $attribute): array - { - if ($type === null && $attribute === null) { - return $notices; - } - - if ($type !== null && $attribute === null) { - return ArrayHelper::where($notices, 'type', $type); - } - - if ($type === null && $attribute !== null) { - return ArrayHelper::where($notices, 'attribute', $attribute); - } - - return ArrayHelper::where($notices, fn(OrderNotice $n) => $n->attribute === $attribute && $n->type === $type, true, true, true); - } -} diff --git a/src/elements/traits/OrderValidatorsTrait.php b/src/elements/traits/OrderValidatorsTrait.php deleted file mode 100644 index 038a14e8dd..0000000000 --- a/src/elements/traits/OrderValidatorsTrait.php +++ /dev/null @@ -1,188 +0,0 @@ - - */ -trait OrderValidatorsTrait -{ - /** - * @param string $attribute - * @param $params - * @param Validator $validator - */ - public function validateGatewayId(string $attribute, $params, Validator $validator): void - { - if ($this->gatewayId && !$this->getGateway()) { - $validator->addError($this, $attribute, Craft::t('commerce', 'Invalid gateway: {value}')); - } - } - - /** - * @param string $attribute - * @param $params - * @param Validator $validator - */ - public function validatePaymentSourceId(string $attribute, $params, Validator $validator): void - { - try { - // this will confirm the payment source is valid and belongs to the orders customer - $this->getPaymentSource(); - } catch (InvalidConfigException $e) { - Craft::$app->getErrorHandler()->logException($e); - $validator->addError($this, $attribute, Craft::t('commerce', 'Invalid payment source ID: {value}')); - } - } - - /** - * @param string $attribute - * @param $params - * @param Validator $validator - * @noinspection PhpUnused - */ - public function validatePaymentCurrency(string $attribute, $params, Validator $validator): void - { - try { - // this will confirm the payment source is valid and belongs to the orders customer - $this->getPaymentCurrency(); - } catch (InvalidConfigException) { - $validator->addError($this, $attribute, Craft::t('commerce', 'Invalid payment source ID: {value}')); - } - } - - /** - * Validates addresses, and also adds prefixed validation errors to order - * - * @param string $attribute the attribute being validated - * @throws InvalidConfigException - * @noinspection PhpUnused - * @throws InvalidConfigException - */ - public function validateAddress(string $attribute): void - { - /** @var Address|null $address */ - $address = $this->$attribute; - - // Set live scenario for addresses to match CP - $address?->setScenario(Address::SCENARIO_LIVE); - - if ($address && !$address->validate()) { - $this->addModelErrors($address, $attribute); - } - - $marketLocationCondition = $this->getStore()->getSettings()->getMarketAddressCondition(); - if ($address && count($marketLocationCondition->getConditionRules()) > 0 && !$marketLocationCondition->matchElement($address)) { - $this->addError($attribute, Craft::t('commerce', 'The address provided is outside the store’s market.')); - } - } - - /** - * Validates that address country is in the allowed list. - * - * @param string $attribute the attribute being validated - */ - public function validateAddressCountry(string $attribute): void - { - $address = $this->$attribute; - if ($address && $address->countryCode) { - $countriesList = array_keys($this->getStore()->getSettings()->getCountriesList()); - if (count($countriesList) && !in_array($address->countryCode, $countriesList, false)) { - $this->addError($attribute, Craft::t('commerce', 'Country not allowed.')); - } - } - } - - /** - * Validates that shipping address isn't being set to be the same as billing address, when billing address is set to be shipping address - * - * @param string $attribute the attribute being validated - */ - public function validateAddressReuse(string $attribute): void - { - if ($this->shippingSameAsBilling && $this->billingSameAsShipping) { - $this->addError($attribute, Craft::t('commerce', 'shippingSameAsBilling and billingSameAsShipping can’t both be set.')); - } - } - - /** - * Validates line items, and also adds prefixed validation errors to order - * - */ - public function validateLineItems(): void - { - OrderHelper::normalizeLineItemPurchasableAvailability($this); - OrderHelper::mergeDuplicateLineItems($this); - - foreach ($this->getLineItems() as $key => $lineItem) { - if (!$lineItem->validate()) { - $this->addModelErrors($lineItem, "lineItems.$key"); - } - } - } - - /** - * @param $attribute - * @throws InvalidConfigException - * @noinspection PhpUnused - */ - public function validateCouponCode($attribute): void - { - $recalculateAll = $this->recalculationMode == Order::RECALCULATION_MODE_ALL; - $recalculateAll = $recalculateAll || $this->recalculationMode == Order::RECALCULATION_MODE_ADJUSTMENTS_ONLY; - if ($recalculateAll && $this->$attribute && !Plugin::getInstance()->getDiscounts()->orderCouponAvailable($this, $explanation)) { - /** @var OrderNotice $notice */ - $notice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'invalidCouponRemoved', - 'attribute' => $attribute, - 'message' => Craft::t('commerce', 'Coupon removed: {explanation}', [ - 'explanation' => $explanation, - ]), - ], - ]); - $this->addNotice($notice); - $this->$attribute = null; - } - } - - /** - * @param $attribute - * @return void - * @throws InvalidConfigException - * @since 5.0.0 - */ - public function validateOrganizationTaxIdAsVatId($attribute): void - { - $address = $this->$attribute; - - // Skip on empty - if (!$address->organizationTaxId) { - return; - } - - if (Plugin::getInstance()->getVat()->isValidVatId($address->organizationTaxId)) { - return; - } - - $address->addError('organizationTaxId', Craft::t('commerce', 'Invalid VAT ID.')); - $this->addModelErrors($address, $attribute); - } -} diff --git a/src/engines/Tax.php b/src/engines/Tax.php deleted file mode 100644 index 4e25393f5d..0000000000 --- a/src/engines/Tax.php +++ /dev/null @@ -1,160 +0,0 @@ - - * @since 5.7.0 - */ -enum ContainsPurchasablesMatch: string -{ - use EnumHelpersTrait; - - case Any = 'any'; - case All = 'all'; - case Only = 'only'; - - public function label(): string - { - return match ($this) { - self::Any => Craft::t('commerce', 'any'), - self::All => Craft::t('commerce', 'all'), - self::Only => Craft::t('commerce', 'only'), - }; - } -} diff --git a/src/enums/InventoryTransactionType.php b/src/enums/InventoryTransactionType.php deleted file mode 100644 index 53c4b00621..0000000000 --- a/src/enums/InventoryTransactionType.php +++ /dev/null @@ -1,148 +0,0 @@ - Craft::t('commerce', 'Available'), - self::RESERVED => Craft::t('commerce', 'Reserved'), - self::DAMAGED => Craft::t('commerce', 'Damaged'), - self::SAFETY => Craft::t('commerce', 'Safety'), - self::QUALITY_CONTROL => Craft::t('commerce', 'Quality Control'), - self::COMMITTED => Craft::t('commerce', 'Committed'), - self::INCOMING => Craft::t('commerce', 'Incoming'), - self::FULFILLED => Craft::t('commerce', 'Fulfilled') - }; - } - - /** - * Can this transaction type go into the negative sum? - * - * @return bool - */ - public function canBeNegative(): bool - { - return $this === self::AVAILABLE || $this === self::COMMITTED || $this === self::INCOMING; - } - - /** - * @return InventoryTransactionType[] - */ - public static function onHand(): array - { - // on hand is unavailable + available + committed - return array_merge( - self::unavailable(), - self::available(), - self::committed() - ); - } - - /** - * @return InventoryTransactionType[] - */ - public static function unavailable(): array - { - return [ - self::RESERVED, - self::DAMAGED, - self::SAFETY, - self::QUALITY_CONTROL, - ]; - } - - /** - * @return InventoryTransactionType[] - */ - public static function available(): array - { - return [ - self::AVAILABLE, - ]; - } - - /** - * @return InventoryTransactionType[] - */ - public static function incoming(): array - { - return [ - self::INCOMING, - ]; - } - - /** - * @return InventoryTransactionType[] - */ - public static function committed(): array - { - return [ - self::COMMITTED, - ]; - } - - /** - * These are the types that can be manually moved between (Outside a transfer or purchase order or fulfillment). - * - * @return InventoryTransactionType[] - */ - public static function allowedManualMoveTransactionTypes(): array - { - return [ - // Unavailable - ...self::unavailable(), - - //available - ...self::available(), - ]; - } - - /** - * These are the types that can be manually moved between (Outside a transfer or purchase order or fulfillment). - * - * @return InventoryTransactionType[] - */ - public static function allowedManualAdjustmentTypes(): array - { - return [ - // Unavailable - ...self::unavailable(), - - //available - ...self::available(), - ]; - } -} diff --git a/src/enums/InventoryUpdateQuantityType.php b/src/enums/InventoryUpdateQuantityType.php deleted file mode 100644 index 34ee729fa8..0000000000 --- a/src/enums/InventoryUpdateQuantityType.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @since 5.1.0 - */ -enum LineItemType: string -{ - use EnumHelpersTrait; - - case Custom = 'custom'; - - case Purchasable = 'purchasable'; - - /** - * @return array - */ - public static function types(): array - { - return array_combine(self::names(), self::cases()); - } - - /** - * @return string - */ - public function typeAsLabel(): string - { - return match ($this) { - self::Custom => Craft::t('commerce', 'Custom'), - self::Purchasable => Craft::t('commerce', 'Purchasable'), - }; - } -} diff --git a/src/enums/OrderNoticeType.php b/src/enums/OrderNoticeType.php deleted file mode 100644 index d74c30ca0b..0000000000 --- a/src/enums/OrderNoticeType.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @since 5.7.0 - */ -enum OrderNoticeType: string -{ - use EnumHelpersTrait; - - case Customer = 'customer'; - - case Admin = 'admin'; -} diff --git a/src/enums/TransferStatusType.php b/src/enums/TransferStatusType.php deleted file mode 100644 index f68a8f8515..0000000000 --- a/src/enums/TransferStatusType.php +++ /dev/null @@ -1,43 +0,0 @@ - Craft::t('commerce', 'Draft'), - self::PENDING => Craft::t('commerce', 'Pending'), - self::PARTIAL => Craft::t('commerce', 'Partial'), - self::RECEIVED => Craft::t('commerce', 'Received'), - }; - } - - public function color(): string - { - // for each case, return a nicer label - return match ($this) { - self::DRAFT => 'blue', - self::PENDING => 'yellow', - self::PARTIAL => 'orange', - self::RECEIVED => 'green', - }; - } -} diff --git a/src/errors/CurrencyException.php b/src/errors/CurrencyException.php deleted file mode 100644 index 428777695c..0000000000 --- a/src/errors/CurrencyException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class CurrencyException extends Exception -{ -} diff --git a/src/errors/EmailException.php b/src/errors/EmailException.php deleted file mode 100644 index 0717e4aa3b..0000000000 --- a/src/errors/EmailException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class EmailException extends Exception -{ -} diff --git a/src/errors/GatewayException.php b/src/errors/GatewayException.php deleted file mode 100644 index 120b7c6e71..0000000000 --- a/src/errors/GatewayException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class GatewayException extends Exception -{ -} diff --git a/src/errors/LineItemException.php b/src/errors/LineItemException.php deleted file mode 100644 index 9c939f4cc6..0000000000 --- a/src/errors/LineItemException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class LineItemException extends Exception -{ -} diff --git a/src/errors/LineItemNotFoundException.php b/src/errors/LineItemNotFoundException.php deleted file mode 100644 index b37faf8c63..0000000000 --- a/src/errors/LineItemNotFoundException.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @since 4.9 - */ -class LineItemNotFoundException extends Exception -{ - /** - * @return string the user-friendly name of this exception - */ - public function getName(): string - { - return 'Line Item not found'; - } -} diff --git a/src/errors/NotImplementedException.php b/src/errors/NotImplementedException.php deleted file mode 100644 index dbf01d22cf..0000000000 --- a/src/errors/NotImplementedException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class NotImplementedException extends BadMethodCallException -{ -} diff --git a/src/errors/OrderAdjustmentNotFoundException.php b/src/errors/OrderAdjustmentNotFoundException.php deleted file mode 100644 index b43f66c53b..0000000000 --- a/src/errors/OrderAdjustmentNotFoundException.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @since 4.9 - */ -class OrderAdjustmentNotFoundException extends Exception -{ - /** - * @return string the user-friendly name of this exception - */ - public function getName(): string - { - return 'Line Item not found'; - } -} diff --git a/src/errors/OrderStatusException.php b/src/errors/OrderStatusException.php deleted file mode 100644 index ed14c63313..0000000000 --- a/src/errors/OrderStatusException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class OrderStatusException extends Exception -{ -} diff --git a/src/errors/PaymentException.php b/src/errors/PaymentException.php deleted file mode 100644 index 944155f86d..0000000000 --- a/src/errors/PaymentException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class PaymentException extends Exception -{ -} diff --git a/src/errors/PaymentSourceCreatedLaterException.php b/src/errors/PaymentSourceCreatedLaterException.php deleted file mode 100644 index a14d0a2142..0000000000 --- a/src/errors/PaymentSourceCreatedLaterException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @since 4.3 - */ -class PaymentSourceCreatedLaterException extends PaymentSourceException -{ -} diff --git a/src/errors/PaymentSourceException.php b/src/errors/PaymentSourceException.php deleted file mode 100644 index 01be0bfbd6..0000000000 --- a/src/errors/PaymentSourceException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class PaymentSourceException extends Exception -{ -} diff --git a/src/errors/ProductTypeNotFoundException.php b/src/errors/ProductTypeNotFoundException.php deleted file mode 100644 index 3bb7c4f3ad..0000000000 --- a/src/errors/ProductTypeNotFoundException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class ProductTypeNotFoundException extends Exception -{ -} diff --git a/src/errors/RefundException.php b/src/errors/RefundException.php deleted file mode 100644 index 3308044f3d..0000000000 --- a/src/errors/RefundException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class RefundException extends Exception -{ -} diff --git a/src/errors/ShippingMethodException.php b/src/errors/ShippingMethodException.php deleted file mode 100644 index b3cc94ca65..0000000000 --- a/src/errors/ShippingMethodException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class ShippingMethodException extends Exception -{ -} diff --git a/src/errors/StoreNotFoundException.php b/src/errors/StoreNotFoundException.php deleted file mode 100644 index b03496f570..0000000000 --- a/src/errors/StoreNotFoundException.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @since 5.0.0 - */ -class StoreNotFoundException extends Exception -{ - /** - * @return string the user-friendly name of this exception - */ - public function getName(): string - { - return 'Store not found'; - } -} diff --git a/src/errors/SubscriptionException.php b/src/errors/SubscriptionException.php deleted file mode 100644 index 8e35e4991b..0000000000 --- a/src/errors/SubscriptionException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class SubscriptionException extends Exception -{ -} diff --git a/src/errors/TransactionException.php b/src/errors/TransactionException.php deleted file mode 100644 index 16a14e4ec5..0000000000 --- a/src/errors/TransactionException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class TransactionException extends Exception -{ -} diff --git a/src/etc/commands.php b/src/etc/commands.php deleted file mode 100644 index 87738cbbe0..0000000000 --- a/src/etc/commands.php +++ /dev/null @@ -1,32 +0,0 @@ - 'Commerce Orders', - 'type' => 'Link', - 'url' => UrlHelper::cpUrl('commerce/orders'), - ], - [ - 'name' => 'Commerce Products', - 'type' => 'Link', - 'url' => UrlHelper::cpUrl('commerce/products'), - ], - [ - 'name' => 'Commerce Promotions', - 'type' => 'Link', - 'url' => UrlHelper::cpUrl('commerce/promotions'), - ], - [ - 'name' => 'Commerce Settings', - 'type' => 'Link', - 'url' => UrlHelper::cpUrl('commerce/settings'), - ], -]; diff --git a/src/etc/currencies.php b/src/etc/currencies.php deleted file mode 100644 index ebf0edd3db..0000000000 --- a/src/etc/currencies.php +++ /dev/null @@ -1,1254 +0,0 @@ - [ - 'alphabeticCode' => 'AFN', - 'currency' => 'Afghani', - 'entity' => 'AFGHANISTAN', - 'minorUnit' => 2, - 'numericCode' => 971, - ], - 'EUR' => [ - 'alphabeticCode' => 'EUR', - 'currency' => 'Euro', - 'entity' => 'SPAIN', - 'minorUnit' => 2, - 'numericCode' => 978, - ], - 'ALL' => [ - 'alphabeticCode' => 'ALL', - 'currency' => 'Lek', - 'entity' => 'ALBANIA', - 'minorUnit' => 2, - 'numericCode' => 8, - ], - 'DZD' => [ - 'alphabeticCode' => 'DZD', - 'currency' => 'Algerian Dinar', - 'entity' => 'ALGERIA', - 'minorUnit' => 2, - 'numericCode' => 12, - ], - 'USD' => [ - 'alphabeticCode' => 'USD', - 'currency' => 'US Dollar', - 'entity' => 'VIRGIN ISLANDS (U.S.)', - 'minorUnit' => 2, - 'numericCode' => 840, - ], - 'AOA' => [ - 'alphabeticCode' => 'AOA', - 'currency' => 'Kwanza', - 'entity' => 'ANGOLA', - 'minorUnit' => 2, - 'numericCode' => 973, - ], - 'XCD' => [ - 'alphabeticCode' => 'XCD', - 'currency' => 'East Caribbean Dollar', - 'entity' => 'SAINT VINCENT AND THE GRENADINES', - 'minorUnit' => 2, - 'numericCode' => 951, - ], - 'ARS' => [ - 'alphabeticCode' => 'ARS', - 'currency' => 'Argentine Peso', - 'entity' => 'ARGENTINA', - 'minorUnit' => 2, - 'numericCode' => 32, - ], - 'AMD' => [ - 'alphabeticCode' => 'AMD', - 'currency' => 'Armenian Dram', - 'entity' => 'ARMENIA', - 'minorUnit' => 2, - 'numericCode' => 51, - ], - 'AWG' => [ - 'alphabeticCode' => 'AWG', - 'currency' => 'Aruban Florin', - 'entity' => 'ARUBA', - 'minorUnit' => 2, - 'numericCode' => 533, - ], - 'AUD' => [ - 'alphabeticCode' => 'AUD', - 'currency' => 'Australian Dollar', - 'entity' => 'TUVALU', - 'minorUnit' => 2, - 'numericCode' => 36, - ], - 'AZN' => [ - 'alphabeticCode' => 'AZN', - 'currency' => 'Azerbaijanian Manat', - 'entity' => 'AZERBAIJAN', - 'minorUnit' => 2, - 'numericCode' => 944, - ], - 'BSD' => [ - 'alphabeticCode' => 'BSD', - 'currency' => 'Bahamian Dollar', - 'entity' => 'BAHAMAS (THE)', - 'minorUnit' => 2, - 'numericCode' => 44, - ], - 'BHD' => [ - 'alphabeticCode' => 'BHD', - 'currency' => 'Bahraini Dinar', - 'entity' => 'BAHRAIN', - 'minorUnit' => 3, - 'numericCode' => 48, - ], - 'BDT' => [ - 'alphabeticCode' => 'BDT', - 'currency' => 'Taka', - 'entity' => 'BANGLADESH', - 'minorUnit' => 2, - 'numericCode' => 50, - ], - 'BBD' => [ - 'alphabeticCode' => 'BBD', - 'currency' => 'Barbados Dollar', - 'entity' => 'BARBADOS', - 'minorUnit' => 2, - 'numericCode' => 52, - ], - 'BYN' => [ - 'alphabeticCode' => 'BYN', - 'currency' => 'Belarusian Ruble', - 'entity' => 'BELARUS', - 'minorUnit' => 2, - 'numericCode' => 933, - ], - 'BYR' => [ - 'alphabeticCode' => 'BYR', - 'currency' => 'Belarusian Ruble', - 'entity' => 'BELARUS', - 'minorUnit' => 0, - 'numericCode' => 974, - ], - 'BZD' => [ - 'alphabeticCode' => 'BZD', - 'currency' => 'Belize Dollar', - 'entity' => 'BELIZE', - 'minorUnit' => 2, - 'numericCode' => 84, - ], - 'XOF' => [ - 'alphabeticCode' => 'XOF', - 'currency' => 'CFA Franc BCEAO', - 'entity' => 'TOGO', - 'minorUnit' => 0, - 'numericCode' => 952, - ], - 'BMD' => [ - 'alphabeticCode' => 'BMD', - 'currency' => 'Bermudian Dollar', - 'entity' => 'BERMUDA', - 'minorUnit' => 2, - 'numericCode' => 60, - ], - 'INR' => [ - 'alphabeticCode' => 'INR', - 'currency' => 'Indian Rupee', - 'entity' => 'INDIA', - 'minorUnit' => 2, - 'numericCode' => 356, - ], - 'BTN' => [ - 'alphabeticCode' => 'BTN', - 'currency' => 'Ngultrum', - 'entity' => 'BHUTAN', - 'minorUnit' => 2, - 'numericCode' => 64, - ], - 'BOB' => [ - 'alphabeticCode' => 'BOB', - 'currency' => 'Boliviano', - 'entity' => 'BOLIVIA (PLURINATIONAL STATE OF)', - 'minorUnit' => 2, - 'numericCode' => 68, - ], - 'BOV' => [ - 'alphabeticCode' => 'BOV', - 'currency' => 'Mvdol', - 'entity' => 'BOLIVIA (PLURINATIONAL STATE OF)', - 'minorUnit' => 2, - 'numericCode' => 984, - ], - 'BAM' => [ - 'alphabeticCode' => 'BAM', - 'currency' => 'Convertible Mark', - 'entity' => 'BOSNIA AND HERZEGOVINA', - 'minorUnit' => 2, - 'numericCode' => 977, - ], - 'BWP' => [ - 'alphabeticCode' => 'BWP', - 'currency' => 'Pula', - 'entity' => 'BOTSWANA', - 'minorUnit' => 2, - 'numericCode' => 72, - ], - 'NOK' => [ - 'alphabeticCode' => 'NOK', - 'currency' => 'Norwegian Krone', - 'entity' => 'SVALBARD AND JAN MAYEN', - 'minorUnit' => 2, - 'numericCode' => 578, - ], - 'BRL' => [ - 'alphabeticCode' => 'BRL', - 'currency' => 'Brazilian Real', - 'entity' => 'BRAZIL', - 'minorUnit' => 2, - 'numericCode' => 986, - ], - 'BND' => [ - 'alphabeticCode' => 'BND', - 'currency' => 'Brunei Dollar', - 'entity' => 'BRUNEI DARUSSALAM', - 'minorUnit' => 2, - 'numericCode' => 96, - ], - 'BGN' => [ - 'alphabeticCode' => 'BGN', - 'currency' => 'Bulgarian Lev', - 'entity' => 'BULGARIA', - 'minorUnit' => 2, - 'numericCode' => 975, - ], - 'BIF' => [ - 'alphabeticCode' => 'BIF', - 'currency' => 'Burundi Franc', - 'entity' => 'BURUNDI', - 'minorUnit' => 0, - 'numericCode' => 108, - ], - 'CVE' => [ - 'alphabeticCode' => 'CVE', - 'currency' => 'Cabo Verde Escudo', - 'entity' => 'CABO VERDE', - 'minorUnit' => 2, - 'numericCode' => 132, - ], - 'KHR' => [ - 'alphabeticCode' => 'KHR', - 'currency' => 'Riel', - 'entity' => 'CAMBODIA', - 'minorUnit' => 2, - 'numericCode' => 116, - ], - 'XAF' => [ - 'alphabeticCode' => 'XAF', - 'currency' => 'CFA Franc BEAC', - 'entity' => 'GABON', - 'minorUnit' => 0, - 'numericCode' => 950, - ], - 'CAD' => [ - 'alphabeticCode' => 'CAD', - 'currency' => 'Canadian Dollar', - 'entity' => 'CANADA', - 'minorUnit' => 2, - 'numericCode' => 124, - ], - 'KYD' => [ - 'alphabeticCode' => 'KYD', - 'currency' => 'Cayman Islands Dollar', - 'entity' => 'CAYMAN ISLANDS (THE)', - 'minorUnit' => 2, - 'numericCode' => 136, - ], - 'CLP' => [ - 'alphabeticCode' => 'CLP', - 'currency' => 'Chilean Peso', - 'entity' => 'CHILE', - 'minorUnit' => 0, - 'numericCode' => 152, - ], - 'CLF' => [ - 'alphabeticCode' => 'CLF', - 'currency' => 'Unidad de Fomento', - 'entity' => 'CHILE', - 'minorUnit' => 4, - 'numericCode' => 990, - ], - 'CNY' => [ - 'alphabeticCode' => 'CNY', - 'currency' => 'Yuan Renminbi', - 'entity' => 'CHINA', - 'minorUnit' => 2, - 'numericCode' => 156, - ], - 'COP' => [ - 'alphabeticCode' => 'COP', - 'currency' => 'Colombian Peso', - 'entity' => 'COLOMBIA', - 'minorUnit' => 2, - 'numericCode' => 170, - ], - 'COU' => [ - 'alphabeticCode' => 'COU', - 'currency' => 'Unidad de Valor Real', - 'entity' => 'COLOMBIA', - 'minorUnit' => 2, - 'numericCode' => 970, - ], - 'KMF' => [ - 'alphabeticCode' => 'KMF', - 'currency' => 'Comoro Franc', - 'entity' => 'COMOROS (THE)', - 'minorUnit' => 0, - 'numericCode' => 174, - ], - 'CDF' => [ - 'alphabeticCode' => 'CDF', - 'currency' => 'Congolese Franc', - 'entity' => 'CONGO (THE DEMOCRATIC REPUBLIC OF THE)', - 'minorUnit' => 2, - 'numericCode' => 976, - ], - 'NZD' => [ - 'alphabeticCode' => 'NZD', - 'currency' => 'New Zealand Dollar', - 'entity' => 'TOKELAU', - 'minorUnit' => 2, - 'numericCode' => 554, - ], - 'CRC' => [ - 'alphabeticCode' => 'CRC', - 'currency' => 'Costa Rican Colon', - 'entity' => 'COSTA RICA', - 'minorUnit' => 2, - 'numericCode' => 188, - ], - 'HRK' => [ - 'alphabeticCode' => 'HRK', - 'currency' => 'Kuna', - 'entity' => 'CROATIA', - 'minorUnit' => 2, - 'numericCode' => 191, - ], - 'CUP' => [ - 'alphabeticCode' => 'CUP', - 'currency' => 'Cuban Peso', - 'entity' => 'CUBA', - 'minorUnit' => 2, - 'numericCode' => 192, - ], - 'CUC' => [ - 'alphabeticCode' => 'CUC', - 'currency' => 'Peso Convertible', - 'entity' => 'CUBA', - 'minorUnit' => 2, - 'numericCode' => 931, - ], - 'ANG' => [ - 'alphabeticCode' => 'ANG', - 'currency' => 'Netherlands Antillean Guilder', - 'entity' => 'SINT MAARTEN (DUTCH PART)', - 'minorUnit' => 2, - 'numericCode' => 532, - ], - 'CZK' => [ - 'alphabeticCode' => 'CZK', - 'currency' => 'Czech Koruna', - 'entity' => 'CZECH REPUBLIC (THE)', - 'minorUnit' => 2, - 'numericCode' => 203, - ], - 'DKK' => [ - 'alphabeticCode' => 'DKK', - 'currency' => 'Danish Krone', - 'entity' => 'GREENLAND', - 'minorUnit' => 2, - 'numericCode' => 208, - ], - 'DJF' => [ - 'alphabeticCode' => 'DJF', - 'currency' => 'Djibouti Franc', - 'entity' => 'DJIBOUTI', - 'minorUnit' => 0, - 'numericCode' => 262, - ], - 'DOP' => [ - 'alphabeticCode' => 'DOP', - 'currency' => 'Dominican Peso', - 'entity' => 'DOMINICAN REPUBLIC (THE)', - 'minorUnit' => 2, - 'numericCode' => 214, - ], - 'EGP' => [ - 'alphabeticCode' => 'EGP', - 'currency' => 'Egyptian Pound', - 'entity' => 'EGYPT', - 'minorUnit' => 2, - 'numericCode' => 818, - ], - 'SVC' => [ - 'alphabeticCode' => 'SVC', - 'currency' => 'El Salvador Colon', - 'entity' => 'EL SALVADOR', - 'minorUnit' => 2, - 'numericCode' => 222, - ], - 'ERN' => [ - 'alphabeticCode' => 'ERN', - 'currency' => 'Nakfa', - 'entity' => 'ERITREA', - 'minorUnit' => 2, - 'numericCode' => 232, - ], - 'ETB' => [ - 'alphabeticCode' => 'ETB', - 'currency' => 'Ethiopian Birr', - 'entity' => 'ETHIOPIA', - 'minorUnit' => 2, - 'numericCode' => 230, - ], - 'FKP' => [ - 'alphabeticCode' => 'FKP', - 'currency' => 'Falkland Islands Pound', - 'entity' => 'FALKLAND ISLANDS (THE) [MALVINAS]', - 'minorUnit' => 2, - 'numericCode' => 238, - ], - 'FJD' => [ - 'alphabeticCode' => 'FJD', - 'currency' => 'Fiji Dollar', - 'entity' => 'FIJI', - 'minorUnit' => 2, - 'numericCode' => 242, - ], - 'XPF' => [ - 'alphabeticCode' => 'XPF', - 'currency' => 'CFP Franc', - 'entity' => 'WALLIS AND FUTUNA', - 'minorUnit' => 0, - 'numericCode' => 953, - ], - 'GMD' => [ - 'alphabeticCode' => 'GMD', - 'currency' => 'Dalasi', - 'entity' => 'GAMBIA (THE)', - 'minorUnit' => 2, - 'numericCode' => 270, - ], - 'GEL' => [ - 'alphabeticCode' => 'GEL', - 'currency' => 'Lari', - 'entity' => 'GEORGIA', - 'minorUnit' => 2, - 'numericCode' => 981, - ], - 'GHS' => [ - 'alphabeticCode' => 'GHS', - 'currency' => 'Ghana Cedi', - 'entity' => 'GHANA', - 'minorUnit' => 2, - 'numericCode' => 936, - ], - 'GIP' => [ - 'alphabeticCode' => 'GIP', - 'currency' => 'Gibraltar Pound', - 'entity' => 'GIBRALTAR', - 'minorUnit' => 2, - 'numericCode' => 292, - ], - 'GTQ' => [ - 'alphabeticCode' => 'GTQ', - 'currency' => 'Quetzal', - 'entity' => 'GUATEMALA', - 'minorUnit' => 2, - 'numericCode' => 320, - ], - 'GBP' => [ - 'alphabeticCode' => 'GBP', - 'currency' => 'Pound Sterling', - 'entity' => 'UNITED KINGDOM OF GREAT BRITAIN AND NORTHERN IRELAND (THE)', - 'minorUnit' => 2, - 'numericCode' => 826, - ], - 'GNF' => [ - 'alphabeticCode' => 'GNF', - 'currency' => 'Guinea Franc', - 'entity' => 'GUINEA', - 'minorUnit' => 0, - 'numericCode' => 324, - ], - 'GYD' => [ - 'alphabeticCode' => 'GYD', - 'currency' => 'Guyana Dollar', - 'entity' => 'GUYANA', - 'minorUnit' => 2, - 'numericCode' => 328, - ], - 'HTG' => [ - 'alphabeticCode' => 'HTG', - 'currency' => 'Gourde', - 'entity' => 'HAITI', - 'minorUnit' => 2, - 'numericCode' => 332, - ], - 'HNL' => [ - 'alphabeticCode' => 'HNL', - 'currency' => 'Lempira', - 'entity' => 'HONDURAS', - 'minorUnit' => 2, - 'numericCode' => 340, - ], - 'HKD' => [ - 'alphabeticCode' => 'HKD', - 'currency' => 'Hong Kong Dollar', - 'entity' => 'HONG KONG', - 'minorUnit' => 2, - 'numericCode' => 344, - ], - 'HUF' => [ - 'alphabeticCode' => 'HUF', - 'currency' => 'Hungarian Forint', - 'entity' => 'HUNGARY', - 'minorUnit' => 2, - 'numericCode' => 348, - ], - 'ISK' => [ - 'alphabeticCode' => 'ISK', - 'currency' => 'Iceland Krona', - 'entity' => 'ICELAND', - 'minorUnit' => 0, - 'numericCode' => 352, - ], - 'IDR' => [ - 'alphabeticCode' => 'IDR', - 'currency' => 'Rupiah', - 'entity' => 'INDONESIA', - 'minorUnit' => 2, - 'numericCode' => 360, - ], - 'XDR' => [ - 'alphabeticCode' => 'XDR', - 'currency' => 'SDR (Special Drawing Right)', - 'entity' => 'INTERNATIONAL MONETARY FUND (IMF) ', - 'minorUnit' => 0, - 'numericCode' => 960, - ], - 'IRR' => [ - 'alphabeticCode' => 'IRR', - 'currency' => 'Iranian Rial', - 'entity' => 'IRAN (ISLAMIC REPUBLIC OF)', - 'minorUnit' => 2, - 'numericCode' => 364, - ], - 'IQD' => [ - 'alphabeticCode' => 'IQD', - 'currency' => 'Iraqi Dinar', - 'entity' => 'IRAQ', - 'minorUnit' => 3, - 'numericCode' => 368, - ], - 'ILS' => [ - 'alphabeticCode' => 'ILS', - 'currency' => 'New Israeli Sheqel', - 'entity' => 'ISRAEL', - 'minorUnit' => 2, - 'numericCode' => 376, - ], - 'JMD' => [ - 'alphabeticCode' => 'JMD', - 'currency' => 'Jamaican Dollar', - 'entity' => 'JAMAICA', - 'minorUnit' => 2, - 'numericCode' => 388, - ], - 'JPY' => [ - 'alphabeticCode' => 'JPY', - 'currency' => 'Yen', - 'entity' => 'JAPAN', - 'minorUnit' => 0, - 'numericCode' => 392, - ], - 'JOD' => [ - 'alphabeticCode' => 'JOD', - 'currency' => 'Jordanian Dinar', - 'entity' => 'JORDAN', - 'minorUnit' => 3, - 'numericCode' => 400, - ], - 'KZT' => [ - 'alphabeticCode' => 'KZT', - 'currency' => 'Tenge', - 'entity' => 'KAZAKHSTAN', - 'minorUnit' => 2, - 'numericCode' => 398, - ], - 'KES' => [ - 'alphabeticCode' => 'KES', - 'currency' => 'Kenyan Shilling', - 'entity' => 'KENYA', - 'minorUnit' => 2, - 'numericCode' => 404, - ], - 'KPW' => [ - 'alphabeticCode' => 'KPW', - 'currency' => 'North Korean Won', - 'entity' => 'KOREA (THE DEMOCRATIC PEOPLE’S REPUBLIC OF)', - 'minorUnit' => 2, - 'numericCode' => 408, - ], - 'KRW' => [ - 'alphabeticCode' => 'KRW', - 'currency' => 'Won', - 'entity' => 'KOREA (THE REPUBLIC OF)', - 'minorUnit' => 0, - 'numericCode' => 410, - ], - 'KWD' => [ - 'alphabeticCode' => 'KWD', - 'currency' => 'Kuwaiti Dinar', - 'entity' => 'KUWAIT', - 'minorUnit' => 3, - 'numericCode' => 414, - ], - 'KGS' => [ - 'alphabeticCode' => 'KGS', - 'currency' => 'Som', - 'entity' => 'KYRGYZSTAN', - 'minorUnit' => 2, - 'numericCode' => 417, - ], - 'LAK' => [ - 'alphabeticCode' => 'LAK', - 'currency' => 'Kip', - 'entity' => 'LAO PEOPLE’S DEMOCRATIC REPUBLIC (THE)', - 'minorUnit' => 2, - 'numericCode' => 418, - ], - 'LBP' => [ - 'alphabeticCode' => 'LBP', - 'currency' => 'Lebanese Pound', - 'entity' => 'LEBANON', - 'minorUnit' => 2, - 'numericCode' => 422, - ], - 'LSL' => [ - 'alphabeticCode' => 'LSL', - 'currency' => 'Loti', - 'entity' => 'LESOTHO', - 'minorUnit' => 2, - 'numericCode' => 426, - ], - 'ZAR' => [ - 'alphabeticCode' => 'ZAR', - 'currency' => 'Rand', - 'entity' => 'SOUTH AFRICA', - 'minorUnit' => 2, - 'numericCode' => 710, - ], - 'LRD' => [ - 'alphabeticCode' => 'LRD', - 'currency' => 'Liberian Dollar', - 'entity' => 'LIBERIA', - 'minorUnit' => 2, - 'numericCode' => 430, - ], - 'LYD' => [ - 'alphabeticCode' => 'LYD', - 'currency' => 'Libyan Dinar', - 'entity' => 'LIBYA', - 'minorUnit' => 3, - 'numericCode' => 434, - ], - 'CHF' => [ - 'alphabeticCode' => 'CHF', - 'currency' => 'Swiss Franc', - 'entity' => 'SWITZERLAND', - 'minorUnit' => 2, - 'numericCode' => 756, - ], - 'MOP' => [ - 'alphabeticCode' => 'MOP', - 'currency' => 'Pataca', - 'entity' => 'MACAO', - 'minorUnit' => 2, - 'numericCode' => 446, - ], - 'MKD' => [ - 'alphabeticCode' => 'MKD', - 'currency' => 'Denar', - 'entity' => 'MACEDONIA (THE FORMER YUGOSLAV REPUBLIC OF)', - 'minorUnit' => 2, - 'numericCode' => 807, - ], - 'MGA' => [ - 'alphabeticCode' => 'MGA', - 'currency' => 'Malagasy Ariary', - 'entity' => 'MADAGASCAR', - 'minorUnit' => 2, - 'numericCode' => 969, - ], - 'MWK' => [ - 'alphabeticCode' => 'MWK', - 'currency' => 'Malawi Kwacha', - 'entity' => 'MALAWI', - 'minorUnit' => 2, - 'numericCode' => 454, - ], - 'MYR' => [ - 'alphabeticCode' => 'MYR', - 'currency' => 'Malaysian Ringgit', - 'entity' => 'MALAYSIA', - 'minorUnit' => 2, - 'numericCode' => 458, - ], - 'MVR' => [ - 'alphabeticCode' => 'MVR', - 'currency' => 'Rufiyaa', - 'entity' => 'MALDIVES', - 'minorUnit' => 2, - 'numericCode' => 462, - ], - 'MRO' => [ - 'alphabeticCode' => 'MRO', - 'currency' => 'Ouguiya', - 'entity' => 'MAURITANIA', - 'minorUnit' => 2, - 'numericCode' => 478, - ], - 'MUR' => [ - 'alphabeticCode' => 'MUR', - 'currency' => 'Mauritius Rupee', - 'entity' => 'MAURITIUS', - 'minorUnit' => 2, - 'numericCode' => 480, - ], - 'XUA' => [ - 'alphabeticCode' => 'XUA', - 'currency' => 'ADB Unit of Account', - 'entity' => 'MEMBER COUNTRIES OF THE AFRICAN DEVELOPMENT BANK GROUP', - 'minorUnit' => 0, - 'numericCode' => 965, - ], - 'MXN' => [ - 'alphabeticCode' => 'MXN', - 'currency' => 'Mexican Peso', - 'entity' => 'MEXICO', - 'minorUnit' => 2, - 'numericCode' => 484, - ], - 'MXV' => [ - 'alphabeticCode' => 'MXV', - 'currency' => 'Mexican Unidad de Inversion (UDI)', - 'entity' => 'MEXICO', - 'minorUnit' => 2, - 'numericCode' => 979, - ], - 'MDL' => [ - 'alphabeticCode' => 'MDL', - 'currency' => 'Moldovan Leu', - 'entity' => 'MOLDOVA (THE REPUBLIC OF)', - 'minorUnit' => 2, - 'numericCode' => 498, - ], - 'MNT' => [ - 'alphabeticCode' => 'MNT', - 'currency' => 'Tugrik', - 'entity' => 'MONGOLIA', - 'minorUnit' => 2, - 'numericCode' => 496, - ], - 'MAD' => [ - 'alphabeticCode' => 'MAD', - 'currency' => 'Moroccan Dirham', - 'entity' => 'WESTERN SAHARA', - 'minorUnit' => 2, - 'numericCode' => 504, - ], - 'MZN' => [ - 'alphabeticCode' => 'MZN', - 'currency' => 'Mozambique Metical', - 'entity' => 'MOZAMBIQUE', - 'minorUnit' => 2, - 'numericCode' => 943, - ], - 'MMK' => [ - 'alphabeticCode' => 'MMK', - 'currency' => 'Kyat', - 'entity' => 'MYANMAR', - 'minorUnit' => 2, - 'numericCode' => 104, - ], - 'NAD' => [ - 'alphabeticCode' => 'NAD', - 'currency' => 'Namibia Dollar', - 'entity' => 'NAMIBIA', - 'minorUnit' => 2, - 'numericCode' => 516, - ], - 'NPR' => [ - 'alphabeticCode' => 'NPR', - 'currency' => 'Nepalese Rupee', - 'entity' => 'NEPAL', - 'minorUnit' => 2, - 'numericCode' => 524, - ], - 'NIO' => [ - 'alphabeticCode' => 'NIO', - 'currency' => 'Cordoba Oro', - 'entity' => 'NICARAGUA', - 'minorUnit' => 2, - 'numericCode' => 558, - ], - 'NGN' => [ - 'alphabeticCode' => 'NGN', - 'currency' => 'Naira', - 'entity' => 'NIGERIA', - 'minorUnit' => 2, - 'numericCode' => 566, - ], - 'OMR' => [ - 'alphabeticCode' => 'OMR', - 'currency' => 'Rial Omani', - 'entity' => 'OMAN', - 'minorUnit' => 3, - 'numericCode' => 512, - ], - 'PKR' => [ - 'alphabeticCode' => 'PKR', - 'currency' => 'Pakistan Rupee', - 'entity' => 'PAKISTAN', - 'minorUnit' => 2, - 'numericCode' => 586, - ], - 'PAB' => [ - 'alphabeticCode' => 'PAB', - 'currency' => 'Balboa', - 'entity' => 'PANAMA', - 'minorUnit' => 2, - 'numericCode' => 590, - ], - 'PGK' => [ - 'alphabeticCode' => 'PGK', - 'currency' => 'Kina', - 'entity' => 'PAPUA NEW GUINEA', - 'minorUnit' => 2, - 'numericCode' => 598, - ], - 'PYG' => [ - 'alphabeticCode' => 'PYG', - 'currency' => 'Guarani', - 'entity' => 'PARAGUAY', - 'minorUnit' => 0, - 'numericCode' => 600, - ], - 'PEN' => [ - 'alphabeticCode' => 'PEN', - 'currency' => 'Sol', - 'entity' => 'PERU', - 'minorUnit' => 2, - 'numericCode' => 604, - ], - 'PHP' => [ - 'alphabeticCode' => 'PHP', - 'currency' => 'Philippine Peso', - 'entity' => 'PHILIPPINES (THE)', - 'minorUnit' => 2, - 'numericCode' => 608, - ], - 'PLN' => [ - 'alphabeticCode' => 'PLN', - 'currency' => 'Zloty', - 'entity' => 'POLAND', - 'minorUnit' => 2, - 'numericCode' => 985, - ], - 'QAR' => [ - 'alphabeticCode' => 'QAR', - 'currency' => 'Qatari Rial', - 'entity' => 'QATAR', - 'minorUnit' => 2, - 'numericCode' => 634, - ], - 'RON' => [ - 'alphabeticCode' => 'RON', - 'currency' => 'Romanian Leu', - 'entity' => 'ROMANIA', - 'minorUnit' => 2, - 'numericCode' => 946, - ], - 'RUB' => [ - 'alphabeticCode' => 'RUB', - 'currency' => 'Russian Ruble', - 'entity' => 'RUSSIAN FEDERATION (THE)', - 'minorUnit' => 2, - 'numericCode' => 643, - ], - 'RWF' => [ - 'alphabeticCode' => 'RWF', - 'currency' => 'Rwanda Franc', - 'entity' => 'RWANDA', - 'minorUnit' => 0, - 'numericCode' => 646, - ], - 'SHP' => [ - 'alphabeticCode' => 'SHP', - 'currency' => 'Saint Helena Pound', - 'entity' => 'SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA', - 'minorUnit' => 2, - 'numericCode' => 654, - ], - 'WST' => [ - 'alphabeticCode' => 'WST', - 'currency' => 'Tala', - 'entity' => 'SAMOA', - 'minorUnit' => 2, - 'numericCode' => 882, - ], - 'STD' => [ - 'alphabeticCode' => 'STD', - 'currency' => 'Dobra', - 'entity' => 'SAO TOME AND PRINCIPE', - 'minorUnit' => 2, - 'numericCode' => 678, - ], - 'SAR' => [ - 'alphabeticCode' => 'SAR', - 'currency' => 'Saudi Riyal', - 'entity' => 'SAUDI ARABIA', - 'minorUnit' => 2, - 'numericCode' => 682, - ], - 'RSD' => [ - 'alphabeticCode' => 'RSD', - 'currency' => 'Serbian Dinar', - 'entity' => 'SERBIA', - 'minorUnit' => 2, - 'numericCode' => 941, - ], - 'SCR' => [ - 'alphabeticCode' => 'SCR', - 'currency' => 'Seychelles Rupee', - 'entity' => 'SEYCHELLES', - 'minorUnit' => 2, - 'numericCode' => 690, - ], - 'SLL' => [ - 'alphabeticCode' => 'SLL', - 'currency' => 'Leone', - 'entity' => 'SIERRA LEONE', - 'minorUnit' => 2, - 'numericCode' => 694, - ], - 'SGD' => [ - 'alphabeticCode' => 'SGD', - 'currency' => 'Singapore Dollar', - 'entity' => 'SINGAPORE', - 'minorUnit' => 2, - 'numericCode' => 702, - ], - 'XSU' => [ - 'alphabeticCode' => 'XSU', - 'currency' => 'Sucre', - 'entity' => 'SISTEMA UNITARIO DE COMPENSACION REGIONAL DE PAGOS SUCRE', - 'minorUnit' => 0, - 'numericCode' => 994, - ], - 'SBD' => [ - 'alphabeticCode' => 'SBD', - 'currency' => 'Solomon Islands Dollar', - 'entity' => 'SOLOMON ISLANDS', - 'minorUnit' => 2, - 'numericCode' => 90, - ], - 'SOS' => [ - 'alphabeticCode' => 'SOS', - 'currency' => 'Somali Shilling', - 'entity' => 'SOMALIA', - 'minorUnit' => 2, - 'numericCode' => 706, - ], - 'SSP' => [ - 'alphabeticCode' => 'SSP', - 'currency' => 'South Sudanese Pound', - 'entity' => 'SOUTH SUDAN', - 'minorUnit' => 2, - 'numericCode' => 728, - ], - 'LKR' => [ - 'alphabeticCode' => 'LKR', - 'currency' => 'Sri Lanka Rupee', - 'entity' => 'SRI LANKA', - 'minorUnit' => 2, - 'numericCode' => 144, - ], - 'SDG' => [ - 'alphabeticCode' => 'SDG', - 'currency' => 'Sudanese Pound', - 'entity' => 'SUDAN (THE)', - 'minorUnit' => 2, - 'numericCode' => 938, - ], - 'SRD' => [ - 'alphabeticCode' => 'SRD', - 'currency' => 'Surinam Dollar', - 'entity' => 'SURINAME', - 'minorUnit' => 2, - 'numericCode' => 968, - ], - 'SZL' => [ - 'alphabeticCode' => 'SZL', - 'currency' => 'Lilangeni', - 'entity' => 'SWAZILAND', - 'minorUnit' => 2, - 'numericCode' => 748, - ], - 'SEK' => [ - 'alphabeticCode' => 'SEK', - 'currency' => 'Swedish Krona', - 'entity' => 'SWEDEN', - 'minorUnit' => 2, - 'numericCode' => 752, - ], - 'CHE' => [ - 'alphabeticCode' => 'CHE', - 'currency' => 'WIR Euro', - 'entity' => 'SWITZERLAND', - 'minorUnit' => 2, - 'numericCode' => 947, - ], - 'CHW' => [ - 'alphabeticCode' => 'CHW', - 'currency' => 'WIR Franc', - 'entity' => 'SWITZERLAND', - 'minorUnit' => 2, - 'numericCode' => 948, - ], - 'SYP' => [ - 'alphabeticCode' => 'SYP', - 'currency' => 'Syrian Pound', - 'entity' => 'SYRIAN ARAB REPUBLIC', - 'minorUnit' => 2, - 'numericCode' => 760, - ], - 'TWD' => [ - 'alphabeticCode' => 'TWD', - 'currency' => 'New Taiwan Dollar', - 'entity' => 'TAIWAN (PROVINCE OF CHINA)', - 'minorUnit' => 2, - 'numericCode' => 901, - ], - 'TJS' => [ - 'alphabeticCode' => 'TJS', - 'currency' => 'Somoni', - 'entity' => 'TAJIKISTAN', - 'minorUnit' => 2, - 'numericCode' => 972, - ], - 'TZS' => [ - 'alphabeticCode' => 'TZS', - 'currency' => 'Tanzanian Shilling', - 'entity' => 'TANZANIA, UNITED REPUBLIC OF', - 'minorUnit' => 2, - 'numericCode' => 834, - ], - 'THB' => [ - 'alphabeticCode' => 'THB', - 'currency' => 'Baht', - 'entity' => 'THAILAND', - 'minorUnit' => 2, - 'numericCode' => 764, - ], - 'TOP' => [ - 'alphabeticCode' => 'TOP', - 'currency' => 'Pa’anga', - 'entity' => 'TONGA', - 'minorUnit' => 2, - 'numericCode' => 776, - ], - 'TTD' => [ - 'alphabeticCode' => 'TTD', - 'currency' => 'Trinidad and Tobago Dollar', - 'entity' => 'TRINIDAD AND TOBAGO', - 'minorUnit' => 2, - 'numericCode' => 780, - ], - 'TND' => [ - 'alphabeticCode' => 'TND', - 'currency' => 'Tunisian Dinar', - 'entity' => 'TUNISIA', - 'minorUnit' => 3, - 'numericCode' => 788, - ], - 'TRY' => [ - 'alphabeticCode' => 'TRY', - 'currency' => 'Turkish Lira', - 'entity' => 'TURKEY', - 'minorUnit' => 2, - 'numericCode' => 949, - ], - 'TMT' => [ - 'alphabeticCode' => 'TMT', - 'currency' => 'Turkmenistan New Manat', - 'entity' => 'TURKMENISTAN', - 'minorUnit' => 2, - 'numericCode' => 934, - ], - 'UGX' => [ - 'alphabeticCode' => 'UGX', - 'currency' => 'Uganda Shilling', - 'entity' => 'UGANDA', - 'minorUnit' => 0, - 'numericCode' => 800, - ], - 'UAH' => [ - 'alphabeticCode' => 'UAH', - 'currency' => 'Hryvnia', - 'entity' => 'UKRAINE', - 'minorUnit' => 2, - 'numericCode' => 980, - ], - 'AED' => [ - 'alphabeticCode' => 'AED', - 'currency' => 'UAE Dirham', - 'entity' => 'UNITED ARAB EMIRATES (THE)', - 'minorUnit' => 2, - 'numericCode' => 784, - ], - 'USN' => [ - 'alphabeticCode' => 'USN', - 'currency' => 'US Dollar (Next day)', - 'entity' => 'UNITED STATES OF AMERICA (THE)', - 'minorUnit' => 2, - 'numericCode' => 997, - ], - 'UYU' => [ - 'alphabeticCode' => 'UYU', - 'currency' => 'Peso Uruguayo', - 'entity' => 'URUGUAY', - 'minorUnit' => 2, - 'numericCode' => 858, - ], - 'UYI' => [ - 'alphabeticCode' => 'UYI', - 'currency' => 'Uruguay Peso en Unidades Indexadas (URUIURUI)', - 'entity' => 'URUGUAY', - 'minorUnit' => 0, - 'numericCode' => 940, - ], - 'UZS' => [ - 'alphabeticCode' => 'UZS', - 'currency' => 'Uzbekistan Sum', - 'entity' => 'UZBEKISTAN', - 'minorUnit' => 2, - 'numericCode' => 860, - ], - 'VUV' => [ - 'alphabeticCode' => 'VUV', - 'currency' => 'Vatu', - 'entity' => 'VANUATU', - 'minorUnit' => 0, - 'numericCode' => 548, - ], - 'VEF' => [ - 'alphabeticCode' => 'VEF', - 'currency' => 'Bolívar', - 'entity' => 'VENEZUELA (BOLIVARIAN REPUBLIC OF)', - 'minorUnit' => 2, - 'numericCode' => 937, - ], - 'VND' => [ - 'alphabeticCode' => 'VND', - 'currency' => 'Đồng', - 'entity' => 'VIET NAM', - 'minorUnit' => 0, - 'numericCode' => 704, - ], - 'YER' => [ - 'alphabeticCode' => 'YER', - 'currency' => 'Yemeni Rial', - 'entity' => 'YEMEN', - 'minorUnit' => 2, - 'numericCode' => 886, - ], - 'ZMW' => [ - 'alphabeticCode' => 'ZMW', - 'currency' => 'Zambian Kwacha', - 'entity' => 'ZAMBIA', - 'minorUnit' => 2, - 'numericCode' => 967, - ], - 'ZWL' => [ - 'alphabeticCode' => 'ZWL', - 'currency' => 'Zimbabwe Dollar', - 'entity' => 'ZIMBABWE', - 'minorUnit' => 2, - 'numericCode' => 932, - ], - 'XBA' => [ - 'alphabeticCode' => 'XBA', - 'currency' => 'Bond Markets Unit European Composite Unit (EURCO)', - 'entity' => 'ZZ01_Bond Markets Unit European_EURCO', - 'minorUnit' => 0, - 'numericCode' => 955, - ], - 'XBB' => [ - 'alphabeticCode' => 'XBB', - 'currency' => 'Bond Markets Unit European Monetary Unit (E.M.U.-6)', - 'entity' => 'ZZ02_Bond Markets Unit European_EMU-6', - 'minorUnit' => 0, - 'numericCode' => 956, - ], - 'XBC' => [ - 'alphabeticCode' => 'XBC', - 'currency' => 'Bond Markets Unit European Unit of Account 9 (E.U.A.-9)', - 'entity' => 'ZZ03_Bond Markets Unit European_EUA-9', - 'minorUnit' => 0, - 'numericCode' => 957, - ], - 'XBD' => [ - 'alphabeticCode' => 'XBD', - 'currency' => 'Bond Markets Unit European Unit of Account 17 (E.U.A.-17)', - 'entity' => 'ZZ04_Bond Markets Unit European_EUA-17', - 'minorUnit' => 0, - 'numericCode' => 958, - ], - 'XTS' => [ - 'alphabeticCode' => 'XTS', - 'currency' => 'Codes specifically reserved for testing purposes', - 'entity' => 'ZZ06_Testing_Code', - 'minorUnit' => 0, - 'numericCode' => 963, - ], - 'XAU' => [ - 'alphabeticCode' => 'XAU', - 'currency' => 'Gold', - 'entity' => 'ZZ08_Gold', - 'minorUnit' => 0, - 'numericCode' => 959, - ], - 'XPD' => [ - 'alphabeticCode' => 'XPD', - 'currency' => 'Palladium', - 'entity' => 'ZZ09_Palladium', - 'minorUnit' => 0, - 'numericCode' => 964, - ], - 'XPT' => [ - 'alphabeticCode' => 'XPT', - 'currency' => 'Platinum', - 'entity' => 'ZZ10_Platinum', - 'minorUnit' => 0, - 'numericCode' => 962, - ], - 'XAG' => [ - 'alphabeticCode' => 'XAG', - 'currency' => 'Silver', - 'entity' => 'ZZ11_Silver', - 'minorUnit' => 0, - 'numericCode' => 961, - ], -]; diff --git a/src/events/AddLineItemEvent.php b/src/events/AddLineItemEvent.php deleted file mode 100644 index aea85f50cd..0000000000 --- a/src/events/AddLineItemEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 2.0 - */ -class AddLineItemEvent extends CancelableEvent -{ - /** - * @var LineItem The line item model. - */ - public LineItem $lineItem; - - /** - * @var bool If this is a new line item. - */ - public bool $isNew = false; -} diff --git a/src/events/CancelSubscriptionEvent.php b/src/events/CancelSubscriptionEvent.php deleted file mode 100644 index 6ce4e3e711..0000000000 --- a/src/events/CancelSubscriptionEvent.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 2.0 - */ -class CancelSubscriptionEvent extends CancelableEvent -{ - /** - * @var Subscription Subscription - */ - public Subscription $subscription; - - /** - * @var CancelSubscriptionForm parameters - */ - public CancelSubscriptionForm $parameters; -} diff --git a/src/events/CartEvent.php b/src/events/CartEvent.php deleted file mode 100644 index 3b1d7eac09..0000000000 --- a/src/events/CartEvent.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 2.0 - */ -class CartEvent extends CancelableEvent -{ - /** - * @var LineItem The line item model. - */ - public LineItem $lineItem; - - /** - * @var Order The order element - */ - public Order $order; -} diff --git a/src/events/CartPurgeEvent.php b/src/events/CartPurgeEvent.php deleted file mode 100644 index c0846e5d54..0000000000 --- a/src/events/CartPurgeEvent.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @since 5.3 - */ -class CartPurgeEvent extends CancelableEvent -{ - /** - * @var Query The query that identifies the order IDs to be purged. - */ - public Query $inactiveCartsQuery; -} diff --git a/src/events/CommerceDebugPanelDataEvent.php b/src/events/CommerceDebugPanelDataEvent.php deleted file mode 100644 index c15ebb06a0..0000000000 --- a/src/events/CommerceDebugPanelDataEvent.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @since 4.0 - */ -class CommerceDebugPanelDataEvent extends Event -{ - /** - * @var array - */ - public array $nav; - - /** - * @var array - */ - public array $content; -} diff --git a/src/events/CreateSubscriptionEvent.php b/src/events/CreateSubscriptionEvent.php deleted file mode 100644 index f3a39d2e9a..0000000000 --- a/src/events/CreateSubscriptionEvent.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @since 2.0 - */ -class CreateSubscriptionEvent extends CancelableEvent -{ - /** - * @var User The subscribing user - */ - public User $user; - - /** - * @var Plan The subscription plan - */ - public Plan $plan; - - /** - * @var SubscriptionForm Additional parameters - */ - public SubscriptionForm $parameters; -} diff --git a/src/events/CustomizeProductSnapshotDataEvent.php b/src/events/CustomizeProductSnapshotDataEvent.php deleted file mode 100644 index 3217e5cf82..0000000000 --- a/src/events/CustomizeProductSnapshotDataEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 2.0 - */ -class CustomizeProductSnapshotDataEvent extends Event -{ - /** - * @var Product The product - */ - public Product $product; - - /** - * @var array The captured data - */ - public array $fieldData; -} diff --git a/src/events/CustomizeProductSnapshotFieldsEvent.php b/src/events/CustomizeProductSnapshotFieldsEvent.php deleted file mode 100644 index abeec25687..0000000000 --- a/src/events/CustomizeProductSnapshotFieldsEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 2.0 - */ -class CustomizeProductSnapshotFieldsEvent extends Event -{ - /** - * @var Product The product - */ - public Product $product; - - /** - * @var array|null The fields to be captured - */ - public ?array $fields = null; -} diff --git a/src/events/CustomizeVariantSnapshotDataEvent.php b/src/events/CustomizeVariantSnapshotDataEvent.php deleted file mode 100644 index 2331d07d1e..0000000000 --- a/src/events/CustomizeVariantSnapshotDataEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 2.0 - */ -class CustomizeVariantSnapshotDataEvent extends Event -{ - /** - * @var Variant The variant - */ - public Variant $variant; - - /** - * @var array The captured data - */ - public array $fieldData; -} diff --git a/src/events/CustomizeVariantSnapshotFieldsEvent.php b/src/events/CustomizeVariantSnapshotFieldsEvent.php deleted file mode 100644 index 15dab1ae02..0000000000 --- a/src/events/CustomizeVariantSnapshotFieldsEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 2.0 - */ -class CustomizeVariantSnapshotFieldsEvent extends Event -{ - /** - * @var Variant The variant - */ - public Variant $variant; - - /** - * @var array|null The fields to be captured - */ - public ?array $fields = null; -} diff --git a/src/events/DefaultLineItemStatusEvent.php b/src/events/DefaultLineItemStatusEvent.php deleted file mode 100644 index 7b8cd33ab5..0000000000 --- a/src/events/DefaultLineItemStatusEvent.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 2.0 - */ -class DefaultLineItemStatusEvent extends Event -{ - /** - * @var LineItemStatus|null The default line item status based on the line item - */ - public ?LineItemStatus $lineItemStatus = null; - - /** - * @var LineItem The line item used to determine the line item status. - */ - public LineItem $lineItem; -} diff --git a/src/events/DefaultOrderStatusEvent.php b/src/events/DefaultOrderStatusEvent.php deleted file mode 100644 index 9a7b0bb502..0000000000 --- a/src/events/DefaultOrderStatusEvent.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 2.0 - */ -class DefaultOrderStatusEvent extends Event -{ - /** - * @var OrderStatus The default order status based on the order - */ - public OrderStatus $orderStatus; - - /** - * @var Order The order used to determine the order status. - */ - public Order $order; -} diff --git a/src/events/DeleteStoreEvent.php b/src/events/DeleteStoreEvent.php deleted file mode 100644 index 985741abbd..0000000000 --- a/src/events/DeleteStoreEvent.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @since 5.0.0 - */ -class DeleteStoreEvent extends StoreEvent -{ -} diff --git a/src/events/DiscountAdjustmentsEvent.php b/src/events/DiscountAdjustmentsEvent.php deleted file mode 100644 index ad7f1b4c60..0000000000 --- a/src/events/DiscountAdjustmentsEvent.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @since 2.0 - */ -class DiscountAdjustmentsEvent extends CancelableEvent -{ - /** - * @var Order The order the discount generated the adjustments for. Do not mutate. - */ - public Order $order; - - /** - * @var Discount The discount that matched. - */ - public Discount $discount; - - /** - * @var OrderAdjustment[] The adjustments generated by the discount. - */ - public array $adjustments; -} diff --git a/src/events/DiscountEvent.php b/src/events/DiscountEvent.php deleted file mode 100644 index f31af08488..0000000000 --- a/src/events/DiscountEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 2.0 - */ -class DiscountEvent extends Event -{ - /** - * @var Discount The discount model - */ - public Discount $discount; - - /** - * @var bool If this is a new discount - */ - public bool $isNew; -} diff --git a/src/events/EmailEvent.php b/src/events/EmailEvent.php deleted file mode 100644 index 06a6582409..0000000000 --- a/src/events/EmailEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 2.0 - */ -class EmailEvent extends Event -{ - /** - * @var Email Email - */ - public Email $email; - - /** - * @var bool Whether the email is brand new. - */ - public bool $isNew = false; -} diff --git a/src/events/InventoryMovementEvent.php b/src/events/InventoryMovementEvent.php deleted file mode 100644 index 1c091c5d8d..0000000000 --- a/src/events/InventoryMovementEvent.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @since 5.5.0 - */ -class InventoryMovementEvent extends Event -{ - /** - * @var InventoryMovementInterface The inventory movement that was executed - */ - public InventoryMovementInterface $inventoryMovement; -} diff --git a/src/events/LineItemEvent.php b/src/events/LineItemEvent.php deleted file mode 100644 index a498dcb90b..0000000000 --- a/src/events/LineItemEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 2.0 - */ -class LineItemEvent extends Event -{ - /** - * @var LineItem The line item model. - */ - public LineItem $lineItem; - - /** - * @var bool If this is a new line item. - */ - public bool $isNew = false; -} diff --git a/src/events/MailEvent.php b/src/events/MailEvent.php deleted file mode 100644 index c60810389d..0000000000 --- a/src/events/MailEvent.php +++ /dev/null @@ -1,48 +0,0 @@ - - * @since 2.0 - */ -class MailEvent extends CancelableEvent -{ - /** - * @var Message Craft email object - */ - public Message $craftEmail; - - /** - * @var Email Commerce email object - */ - public Email $commerceEmail; - - /** - * @var Order Commerce order - */ - public Order $order; - - /** - * @var OrderHistory|null The order history - */ - public ?OrderHistory $orderHistory = null; - - /** - * @var array Order data at the time the email sends. - */ - public ?array $orderData = null; -} diff --git a/src/events/MatchLineItemEvent.php b/src/events/MatchLineItemEvent.php deleted file mode 100644 index 46659563fc..0000000000 --- a/src/events/MatchLineItemEvent.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 2.0 - */ -class MatchLineItemEvent extends CancelableEvent -{ - /** - * @var LineItem The matched line item. - */ - public LineItem $lineItem; - - /** - * @var Discount The discount that matched. - */ - public Discount $discount; -} diff --git a/src/events/MatchOrderEvent.php b/src/events/MatchOrderEvent.php deleted file mode 100644 index 9aa7da004f..0000000000 --- a/src/events/MatchOrderEvent.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 3.1.5 - */ -class MatchOrderEvent extends CancelableEvent -{ - /** - * @var Order The matched order. - */ - public Order $order; - - /** - * @var Discount The discount that matched. - */ - public Discount $discount; -} diff --git a/src/events/ModifyCartInfoEvent.php b/src/events/ModifyCartInfoEvent.php deleted file mode 100644 index 6aeea47c47..0000000000 --- a/src/events/ModifyCartInfoEvent.php +++ /dev/null @@ -1,34 +0,0 @@ - - * @since 2.2 - */ -class ModifyCartInfoEvent extends Event -{ - /** - * @var array The cart info that is allowed to be modified - */ - public array $cartInfo = []; - - /** - * The cart object that can be used to modify the cart info. - * Do not mutate this object. - * - * @var Order|null - * @since 3.1.11 - */ - public ?Order $cart = null; -} diff --git a/src/events/ModifyPurchasablesTableQueryEvent.php b/src/events/ModifyPurchasablesTableQueryEvent.php deleted file mode 100644 index 723d36b22b..0000000000 --- a/src/events/ModifyPurchasablesTableQueryEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 4.3.0 - */ -class ModifyPurchasablesTableQueryEvent extends Event -{ - /** - * @var Query - */ - public Query $query; - - /** - * @var string|null The search term that is being used in the query, if any - */ - public ?string $search = null; -} diff --git a/src/events/OrderLineItemsRefreshEvent.php b/src/events/OrderLineItemsRefreshEvent.php deleted file mode 100644 index 4e8b85a9fb..0000000000 --- a/src/events/OrderLineItemsRefreshEvent.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @since 5.1.0 - */ -class OrderLineItemsRefreshEvent extends Event -{ - /** - * @var array - */ - public array $lineItems; - - public bool $recalculate = false; -} diff --git a/src/events/OrderNoticeEvent.php b/src/events/OrderNoticeEvent.php deleted file mode 100644 index a8cd54fe81..0000000000 --- a/src/events/OrderNoticeEvent.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @since 4.1.0 - */ -class OrderNoticeEvent extends CancelableEvent -{ - /** - * @var OrderNotice The line item model. - */ - public $orderNotice; -} diff --git a/src/events/OrderStatusEmailsEvent.php b/src/events/OrderStatusEmailsEvent.php deleted file mode 100644 index b18d895a13..0000000000 --- a/src/events/OrderStatusEmailsEvent.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @since 4.0 - */ -class OrderStatusEmailsEvent extends CancelableEvent -{ - /** - * @var OrderHistory The order history - */ - public OrderHistory $orderHistory; - - /** - * @var Order The order - */ - public Order $order; - - /** - * @var array The emails to send - */ - public array $emails; -} diff --git a/src/events/OrderStatusEvent.php b/src/events/OrderStatusEvent.php deleted file mode 100644 index abe7893696..0000000000 --- a/src/events/OrderStatusEvent.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 2.0 - */ -class OrderStatusEvent extends Event -{ - /** - * @var OrderHistory The order history - */ - public OrderHistory $orderHistory; - - /** - * @var Order The order - */ - public Order $order; -} diff --git a/src/events/PaymentCurrencyRateEvent.php b/src/events/PaymentCurrencyRateEvent.php deleted file mode 100644 index 87200be420..0000000000 --- a/src/events/PaymentCurrencyRateEvent.php +++ /dev/null @@ -1,35 +0,0 @@ - - * @since 2.0 - */ -class PaymentSourceEvent extends CancelableEvent -{ - /** - * @var PaymentSource Payment source - */ - public PaymentSource $paymentSource; -} diff --git a/src/events/PdfEvent.php b/src/events/PdfEvent.php deleted file mode 100644 index a2179b04a2..0000000000 --- a/src/events/PdfEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 2.0 - */ -class PdfEvent extends Event -{ - /** - * @var Pdf The PDF model associated with the event. - */ - public Pdf $pdf; - - /** - * @var bool Whether the PDF is brand new - */ - public bool $isNew = false; -} diff --git a/src/events/PdfRenderEvent.php b/src/events/PdfRenderEvent.php deleted file mode 100644 index 2b46d5364b..0000000000 --- a/src/events/PdfRenderEvent.php +++ /dev/null @@ -1,52 +0,0 @@ - - * @since 4.0 - */ -class PdfRenderEvent extends Event -{ - /** - * @var Order - */ - public Order $order; - - /** - * @var string - */ - public string $option; - - /** - * @var string - */ - public string $template; - - /** - * @var array - */ - public array $variables; - - /** - * @var string|null The rendered PDF - */ - public ?string $pdf = null; - - /** - * @var Pdf|null The configured PDF model used to render the PDF - * @since 5.0.12 - */ - public ?Pdf $sourcePdf = null; -} diff --git a/src/events/PdfRenderOptionsEvent.php b/src/events/PdfRenderOptionsEvent.php deleted file mode 100644 index 65e1b904c7..0000000000 --- a/src/events/PdfRenderOptionsEvent.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @since 3.2.10 - */ -class PdfRenderOptionsEvent extends Event -{ - /** - * @var Options - */ - public Options $options; -} diff --git a/src/events/PlanEvent.php b/src/events/PlanEvent.php deleted file mode 100644 index a6852084a1..0000000000 --- a/src/events/PlanEvent.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @since 2.0 - */ -class PlanEvent extends Event -{ - /** - * @var Plan Plan - */ - public Plan $plan; -} diff --git a/src/events/ProcessPaymentEvent.php b/src/events/ProcessPaymentEvent.php deleted file mode 100644 index a772638723..0000000000 --- a/src/events/ProcessPaymentEvent.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @since 2.0 - */ -class ProductEvent extends CancelableEvent -{ - /** - * @var Product The address model - */ - public Product $product; - - /** - * @var bool If this is a new product - */ - public bool $isNew; -} diff --git a/src/events/ProductTypeEvent.php b/src/events/ProductTypeEvent.php deleted file mode 100644 index ca8fae2c92..0000000000 --- a/src/events/ProductTypeEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 2.0 - */ -class ProductTypeEvent extends Event -{ - /** - * @var ProductType|null The product type model associated with the event. - */ - public ?ProductType $productType = null; - - /** - * @var bool Whether the product type is brand new - */ - public bool $isNew = false; -} diff --git a/src/events/PurchasableAvailableEvent.php b/src/events/PurchasableAvailableEvent.php deleted file mode 100644 index 5bbf0a93fc..0000000000 --- a/src/events/PurchasableAvailableEvent.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @since 3.3.1 - */ -class PurchasableAvailableEvent extends Event -{ - /** - * @var Order|null The order element. - */ - public ?Order $order = null; - - /** - * @var PurchasableInterface The purchasable element. - */ - public PurchasableInterface $purchasable; - - /** - * @var User|null The user performing the check. - */ - public ?User $currentUser = null; - - /** - * @var bool Is this purchasable available to the order and current user. Default is: $event->purchasable->getIsAvailable() - */ - public bool $isAvailable; -} diff --git a/src/events/PurchasableOutOfStockPurchasesAllowedEvent.php b/src/events/PurchasableOutOfStockPurchasesAllowedEvent.php deleted file mode 100644 index 5b24e97113..0000000000 --- a/src/events/PurchasableOutOfStockPurchasesAllowedEvent.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @since 5.3.0 - */ -class PurchasableOutOfStockPurchasesAllowedEvent extends Event -{ - /** - * @var Order|null The order element. - */ - public ?Order $order = null; - - /** - * @var PurchasableInterface The purchasable element. - */ - public PurchasableInterface $purchasable; - - /** - * @var User|null The user performing the check. - */ - public ?User $currentUser = null; - - /** - * @var bool Is this purchasable available to be purchased when out of stock - */ - public bool $outOfStockPurchasesAllowed = false; -} diff --git a/src/events/PurchasableShippableEvent.php b/src/events/PurchasableShippableEvent.php deleted file mode 100644 index 4e60f5be85..0000000000 --- a/src/events/PurchasableShippableEvent.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @since 3.3.2 - */ -class PurchasableShippableEvent extends Event -{ - /** - * @var Order|null The order element. - */ - public ?Order $order = null; - - /** - * @var PurchasableInterface The purchasable element. - */ - public PurchasableInterface $purchasable; - - /** - * @var User|null The user performing the check. - */ - public ?User $currentUser = null; - - /** - * @var bool Is this purchasable shippable within the order and current user. Default is: $event->purchasable->getIsShippable() - */ - public bool $isShippable; -} diff --git a/src/events/PurchaseVariantEvent.php b/src/events/PurchaseVariantEvent.php deleted file mode 100644 index 129463f79c..0000000000 --- a/src/events/PurchaseVariantEvent.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @since 2.0 - */ -class PurchaseVariantEvent extends Event -{ - /** - * @var Variant The variant model - */ - public Variant $variant; -} diff --git a/src/events/PurgeAddressesEvent.php b/src/events/PurgeAddressesEvent.php deleted file mode 100644 index 1cb415bf2f..0000000000 --- a/src/events/PurgeAddressesEvent.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @since 3.3 - */ -class PurgeAddressesEvent extends CancelableEvent -{ - /** - * @var Query|null The query to get the purgeable addresses - */ - public ?Query $addressesQuery = null; -} diff --git a/src/events/RefundTransactionEvent.php b/src/events/RefundTransactionEvent.php deleted file mode 100644 index 0f89006ffd..0000000000 --- a/src/events/RefundTransactionEvent.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @since 2.0 - */ -class RefundTransactionEvent extends TransactionEvent -{ - /** - * @var float The amount to refund - */ - public float $amount; - - /** - * @var Transaction The transaction created which is the refund - */ - public Transaction $refundTransaction; -} diff --git a/src/events/RegisterAvailableShippingMethodsEvent.php b/src/events/RegisterAvailableShippingMethodsEvent.php deleted file mode 100644 index 5753650b82..0000000000 --- a/src/events/RegisterAvailableShippingMethodsEvent.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @since 3.0 - * - * @property array|\Illuminate\Support\Collection $shippingMethods - */ -class RegisterAvailableShippingMethodsEvent extends Event -{ - /** - * @var Order The order the shipping method should be available for - */ - public Order $order; - - /** - * @var Collection|null The shipping methods available to the order. - * @see getShippingMethods() - * @see setShippingMethods() - */ - private ?Collection $_shippingMethods = null; - - /** - * @param Collection|array $shippingMethods - * @return void - * @since 5.0.0 - */ - public function setShippingMethods(Collection|array $shippingMethods): void - { - if (!$shippingMethods instanceof Collection) { - $shippingMethods = collect($shippingMethods); - } - - $this->_shippingMethods = $shippingMethods; - } - - /** - * @return Collection - * @since 5.0.0 - */ - public function getShippingMethods(): Collection - { - if ($this->_shippingMethods === null) { - $this->_shippingMethods = collect(); - } - - return $this->_shippingMethods; - } -} diff --git a/src/events/ReportEvent.php b/src/events/ReportEvent.php deleted file mode 100644 index 311058ad09..0000000000 --- a/src/events/ReportEvent.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @since 2.0 - */ -class ReportEvent extends Event -{ - public mixed $startDate = null; - public mixed $endDate = null; - public mixed $status = null; - public mixed $orderQuery = null; - public mixed $columns = null; - public mixed $orders = null; - public mixed $format = null; -} diff --git a/src/events/SaleEvent.php b/src/events/SaleEvent.php deleted file mode 100644 index 5b01b45cef..0000000000 --- a/src/events/SaleEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 2.2 - */ -class SaleEvent extends Event -{ - /** - * @var Sale sale - */ - public Sale $sale; - - /** - * @var bool Whether the sale is brand new - */ - public bool $isNew = false; -} diff --git a/src/events/SaleMatchEvent.php b/src/events/SaleMatchEvent.php deleted file mode 100644 index 18c0505700..0000000000 --- a/src/events/SaleMatchEvent.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @since 2.0 - */ -class SaleMatchEvent extends CancelableEvent -{ - /** - * @var Sale The sale - */ - public Sale $sale; - - /** - * @var PurchasableInterface The purchasable matched - */ - public PurchasableInterface $purchasable; - - /** - * @var bool If this is a new sale - */ - public bool $isNew; -} diff --git a/src/events/StoreEvent.php b/src/events/StoreEvent.php deleted file mode 100644 index 2d4f011a55..0000000000 --- a/src/events/StoreEvent.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 5.0.0 - */ -class StoreEvent extends CancelableEvent -{ - /** - * @var Store The store model associated with the event. - */ - public Store $store; - - /** - * @var bool Whether the store is brand new - */ - public bool $isNew = false; -} diff --git a/src/events/SubscriptionEvent.php b/src/events/SubscriptionEvent.php deleted file mode 100644 index f99f387141..0000000000 --- a/src/events/SubscriptionEvent.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @since 2.0 - */ -class SubscriptionEvent extends CancelableEvent -{ - /** - * @var Subscription Subscription - */ - public Subscription $subscription; -} diff --git a/src/events/SubscriptionPaymentEvent.php b/src/events/SubscriptionPaymentEvent.php deleted file mode 100644 index b11bd6bc36..0000000000 --- a/src/events/SubscriptionPaymentEvent.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @since 2.0 - */ -class SubscriptionPaymentEvent extends Event -{ - /** - * @var Subscription Subscription - */ - public Subscription $subscription; - - /** - * @var SubscriptionPayment Subscription payment - */ - public SubscriptionPayment $payment; - - /** - * @var DateTime Date subscription paid until - */ - public DateTime $paidUntil; -} diff --git a/src/events/SubscriptionSwitchPlansEvent.php b/src/events/SubscriptionSwitchPlansEvent.php deleted file mode 100644 index 80426ac5da..0000000000 --- a/src/events/SubscriptionSwitchPlansEvent.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @since 2.0 - */ -class SubscriptionSwitchPlansEvent extends CancelableEvent -{ - /** - * @var Plan The plan user is switching from - */ - public Plan $oldPlan; - - /** - * @var Subscription Subscription - */ - public Subscription $subscription; - - /** - * @var Plan The plan user is switching to - */ - public Plan $newPlan; - - /** - * @var SwitchPlansForm parameters - */ - public SwitchPlansForm $parameters; -} diff --git a/src/events/TaxEngineEvent.php b/src/events/TaxEngineEvent.php deleted file mode 100644 index 227685cfdc..0000000000 --- a/src/events/TaxEngineEvent.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @since 3.1 - */ -class TaxEngineEvent extends Event -{ - /** - * @var TaxEngineInterface The tax engine - */ - public TaxEngineInterface $engine; -} diff --git a/src/events/TaxIdValidatorsEvent.php b/src/events/TaxIdValidatorsEvent.php deleted file mode 100644 index dedfc8aca1..0000000000 --- a/src/events/TaxIdValidatorsEvent.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @since 5.3.0 - */ -class TaxIdValidatorsEvent extends Event -{ - /** - * @var TaxIdValidatorInterface[] Holds the registered tax ID validators. - */ - public array $validators = []; -} diff --git a/src/events/TransactionEvent.php b/src/events/TransactionEvent.php deleted file mode 100644 index 494cc0448b..0000000000 --- a/src/events/TransactionEvent.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @since 2.0 - */ -class TransactionEvent extends Event -{ - /** - * @var Transaction The transaction model - */ - public Transaction $transaction; -} diff --git a/src/events/UpdateInventoryLevelEvent.php b/src/events/UpdateInventoryLevelEvent.php deleted file mode 100644 index 4580f57082..0000000000 --- a/src/events/UpdateInventoryLevelEvent.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @since 5.5.0 - */ -class UpdateInventoryLevelEvent extends Event -{ - /** - * @var UpdateInventoryLevel The inventory level update that was executed - */ - public UpdateInventoryLevel $updateInventoryLevel; -} diff --git a/src/events/UpdatePrimaryPaymentSourceEvent.php b/src/events/UpdatePrimaryPaymentSourceEvent.php deleted file mode 100644 index 6fa0baa14a..0000000000 --- a/src/events/UpdatePrimaryPaymentSourceEvent.php +++ /dev/null @@ -1,35 +0,0 @@ - - * @since 4.2.8 - */ -class UpdatePrimaryPaymentSourceEvent extends Event -{ - /** - * @var ?int The previous payment source ID - */ - public ?int $previousPrimaryPaymentSourceId = null; - - /** - * @var ?int The new payment source ID - */ - public ?int $newPrimaryPaymentSourceId = null; - - /** - * @var User The user that the payment source belongs to - */ - public User $customer; -} diff --git a/src/events/UpgradeEvent.php b/src/events/UpgradeEvent.php deleted file mode 100644 index ff38f42d83..0000000000 --- a/src/events/UpgradeEvent.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ -class UpgradeEvent extends Event -{ - /** - * @var array $columns - */ - public array $v3columnMap = []; - - /** - * @var array $v3tables - */ - public array $v3tables = []; -} diff --git a/src/events/WebhookEvent.php b/src/events/WebhookEvent.php deleted file mode 100644 index 561c6e48ca..0000000000 --- a/src/events/WebhookEvent.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 3.2.9 - */ -class WebhookEvent extends Event -{ - /** - * @var GatewayInterface - */ - public GatewayInterface $gateway; - - /** - * @var Response - */ - public Response $response; -} diff --git a/src/exports/Expanded.php b/src/exports/Expanded.php deleted file mode 100644 index 42b07322c5..0000000000 --- a/src/exports/Expanded.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @since 3.2.7 - */ -class Expanded extends CraftExpanded -{ - /** - * @inheritdoc - */ - public function export(ElementQueryInterface $query): mixed - { - // This export should be identical to the parent, except for the additional extra fields - $extraAttributes = ['adjustments', 'billingAddress', 'shippingAddress', 'transactions']; - - // Eager-load as much as we can - $eagerLoadableFields = []; - foreach (Craft::$app->getFields()->getAllFields() as $field) { - if ($field instanceof EagerLoadingFieldInterface) { - $eagerLoadableFields[] = $field->handle; - } - } - - $data = []; - - /** @var OrderQuery $query */ - $query->with($eagerLoadableFields); - $query->withAll(); - - foreach ($query->each() as $element) { - // Get the basic array representation excluding custom fields - $attributes = array_flip($element->attributes()); - if (($fieldLayout = $element->getFieldLayout()) !== null) { - foreach ($fieldLayout->getCustomFields() as $field) { - unset($attributes[$field->handle]); - } - } - $elementArr = $element->toArray(array_keys($attributes), $extraAttributes); - if ($fieldLayout !== null) { - foreach ($fieldLayout->getCustomFields() as $field) { - $value = $element->getFieldValue($field->handle); - $elementArr[$field->handle] = $field->serializeValue($value, $element); - } - } - $data[] = $elementArr; - } - - return $data; - } -} diff --git a/src/exports/LineItemExport.php b/src/exports/LineItemExport.php deleted file mode 100644 index 4c9223a880..0000000000 --- a/src/exports/LineItemExport.php +++ /dev/null @@ -1,94 +0,0 @@ -ids(); - - $columns = [ - 'lineitems.id', - 'lineitems.orderId', - 'lineitems.purchasableId', - 'lineitems.description', - 'lineitems.sku', - 'lineitems.taxCategoryId', - 'lineitems.lineItemStatusId', - 'lineitems.shippingCategoryId', - 'lineitems.options', - 'lineitems.optionsSignature', - 'lineitems.price', - 'lineitems.promotionalAmount', - 'lineitems.salePrice', - 'lineitems.qty', - 'lineitems.subtotal', - 'totalTax' => (new CraftQuery()) - ->select('SUM([[amount]])') - ->from(Table::ORDERADJUSTMENTS . ' adjustments') - ->where(['and', '[[adjustments.orderId]] = [[lineitems.orderId]]', '[[adjustments.lineItemId]] = [[lineitems.id]]']) - ->andWhere(['type' => Tax::ADJUSTMENT_TYPE]) - ->andWhere(['included' => 0]), - 'totalTaxIncluded' => (new CraftQuery()) - ->select('SUM([[amount]])') - ->from(Table::ORDERADJUSTMENTS . ' adjustments') - ->where(['and', '[[adjustments.orderId]] = [[lineitems.orderId]]', '[[lineItemId]] = [[lineitems.id]]']) - ->andWhere(['type' => Tax::ADJUSTMENT_TYPE]) - ->andWhere(['included' => 1]), - 'totalShipping' => (new CraftQuery()) - ->select('SUM([[amount]])') - ->from(Table::ORDERADJUSTMENTS . ' adjustments') - ->where(['and', '[[adjustments.orderId]] = [[lineitems.orderId]]', '[[lineItemId]] = [[lineitems.id]]']) - ->andWhere(['type' => Shipping::ADJUSTMENT_TYPE]), - 'totalDiscount' => (new CraftQuery()) - ->select('SUM([[amount]])') - ->from(Table::ORDERADJUSTMENTS . ' adjustments') - ->where(['and', '[[adjustments.orderId]] = [[lineitems.orderId]]', '[[adjustments.lineItemId]] = [[lineitems.id]]']) - ->andWhere(['type' => Discount::ADJUSTMENT_TYPE]), - 'lineitems.total', - 'lineitems.weight', - 'lineitems.height', - 'lineitems.length', - 'lineitems.width', - 'lineitems.note', - 'lineitems.privateNote', - 'lineitems.snapshot', - 'lineitems.dateCreated', - 'lineitems.dateUpdated', - 'lineitems.uid', - ]; - - return (new CraftQuery()) - ->select($columns) - ->from(Table::LINEITEMS . ' lineitems') - ->leftJoin(Table::ORDERS . ' orders', '[[lineitems.orderId]] = [[orders.id]]') - ->where(['[[lineitems.orderId]]' => $orderIds]) - ->all(); - } -} diff --git a/src/exports/OrderExport.php b/src/exports/OrderExport.php deleted file mode 100644 index 1c8d2ce21f..0000000000 --- a/src/exports/OrderExport.php +++ /dev/null @@ -1,88 +0,0 @@ -ids(); - - $columns = [ - 'id', - 'number', - 'email', - 'gatewayId', - 'paymentSourceId', - 'customerId', - 'orderStatusId', - 'couponCode', - 'itemTotal', - 'totalTax' => (new CraftQuery()) - ->select('SUM([[amount]])') - ->from(Table::ORDERADJUSTMENTS) - ->where('[[orderId]] = ' . Table::ORDERS . '.[[id]]') - ->andWhere(['type' => Tax::ADJUSTMENT_TYPE]) - ->andWhere(['included' => 0]), - 'totalTaxIncluded' => (new CraftQuery()) - ->select('SUM([[amount]])') - ->from(Table::ORDERADJUSTMENTS) - ->where('[[orderId]] = ' . Table::ORDERS . '.[[id]]') - ->andWhere(['type' => Tax::ADJUSTMENT_TYPE]) - ->andWhere(['included' => 1]), - 'totalShipping' => (new CraftQuery()) - ->select('SUM([[amount]])') - ->from(Table::ORDERADJUSTMENTS) - ->where('[[orderId]] = ' . Table::ORDERS . '.[[id]]') - ->andWhere(['type' => Shipping::ADJUSTMENT_TYPE]), - 'totalDiscount' => (new CraftQuery()) - ->select('SUM([[amount]])') - ->from(Table::ORDERADJUSTMENTS) - ->where('[[orderId]] = ' . Table::ORDERS . '.[[id]]') - ->andWhere(['type' => Discount::ADJUSTMENT_TYPE]), - 'totalPrice', - 'totalPaid', - 'paidStatus', - 'isCompleted', - 'dateOrdered', - 'datePaid', - 'currency', - 'paymentCurrency', - 'lastIp', - 'orderLanguage', - 'message', - 'shippingMethodHandle', - ]; - - return (new CraftQuery()) - ->select($columns) - ->from(Table::ORDERS) - ->where(['id' => $orderIds]) - ->all(); - } -} diff --git a/src/fieldlayoutelements/ProductTitleField.php b/src/fieldlayoutelements/ProductTitleField.php deleted file mode 100644 index 372c182cea..0000000000 --- a/src/fieldlayoutelements/ProductTitleField.php +++ /dev/null @@ -1,77 +0,0 @@ - - * @since 3.2.0 - */ -class ProductTitleField extends TitleField -{ - /** - * @inheritdoc - */ - protected function selectorInnerHtml(): string - { - return - Html::tag('span', '', [ - 'class' => ['fld-product-title-field-icon', 'fld-field-hidden', 'hidden'], - ]) . - parent::selectorInnerHtml(); - } - - /** - * @inheritdoc - */ - protected function translatable(?ElementInterface $element = null, bool $static = false): bool - { - if (!$element instanceof Product) { - throw new \InvalidArgumentException(sprintf('%s can only be used in product field layouts.', self::class)); - } - - return $element->getType()->productTitleTranslationMethod !== Field::TRANSLATION_METHOD_NONE; - } - - /** - * @inheritdoc - */ - protected function translationDescription(?ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Product) { - throw new \InvalidArgumentException(sprintf('%s can only be used in product field layouts.', self::class)); - } - - return ElementHelper::translationDescription($element->getType()->productTitleTranslationMethod); - } - - /** - * @inheritdoc - */ - public function inputHtml(ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Product) { - throw new InvalidArgumentException('ProductTitleField can only be used in product field layouts.'); - } - - if (!$element->getType()->hasProductTitleField) { - return null; - } - - return parent::inputHtml($element, $static); - } -} diff --git a/src/fieldlayoutelements/PurchasableAllowedQtyField.php b/src/fieldlayoutelements/PurchasableAllowedQtyField.php deleted file mode 100644 index 302b2f29cb..0000000000 --- a/src/fieldlayoutelements/PurchasableAllowedQtyField.php +++ /dev/null @@ -1,86 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasableAllowedQtyField extends BaseNativeField -{ - /** - * @inheritdoc - */ - public bool $mandatory = true; - - /** - * @inheritdoc - */ - public string $attribute = 'allowedQty'; - - /** - * @inheritdoc - */ - public function __construct(array $config = []) - { - unset($config['required']); - parent::__construct($config); - } - - /** - * @inheritdoc - */ - public function inputHtml(ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Purchasable) { - throw new InvalidArgumentException(static::class . ' can only be used in purchasable field layouts.'); - } - - return Html::beginTag('div', ['class' => 'flex']) . - Html::beginTag('div', ['class' => 'textwrapper']) . - Cp::textHtml([ - 'id' => 'minQty', - 'name' => 'minQty', - 'value' => $element->minQty, - 'placeholder' => Craft::t('commerce', 'Any'), - 'title' => Craft::t('commerce', 'Minimum allowed quantity'), - 'disabled' => $static, - ]) . - Html::endTag('div') . - Html::tag('div', Craft::t('commerce', 'to'), ['class' => 'label light']) . - Html::beginTag('div', ['class' => 'textwrapper']) . - Cp::textHtml([ - 'id' => 'maxQty', - 'name' => 'maxQty', - 'value' => $element->maxQty, - 'placeholder' => Craft::t('commerce', 'Any'), - 'title' => Craft::t('commerce', 'Maximum allowed quantity'), - 'disabled' => $static, - ]) . - Html::endTag('div') . - Html::endTag('div'); - } - - /** - * @inheritdoc - */ - protected function defaultLabel(?ElementInterface $element = null, bool $static = false): ?string - { - return Craft::t('commerce', 'Allowed Qty'); - } -} diff --git a/src/fieldlayoutelements/PurchasableAvailableForPurchaseField.php b/src/fieldlayoutelements/PurchasableAvailableForPurchaseField.php deleted file mode 100644 index 455c920f7c..0000000000 --- a/src/fieldlayoutelements/PurchasableAvailableForPurchaseField.php +++ /dev/null @@ -1,83 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasableAvailableForPurchaseField extends BaseNativeField -{ - /** - * @inheritdoc - */ - public bool $mandatory = true; - - /** - * @inheritdoc - */ - public string $attribute = 'availableForPurchase'; - - /** - * @var bool Whether the field should be checked by default when creating a new purchasable. - */ - public bool $defaultAvailableForPurchase = false; - - /** - * @inheritdoc - */ - public function __construct(array $config = []) - { - unset($config['required']); - parent::__construct($config); - } - - /** - * @inheritdoc - */ - public function inputHtml(ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Purchasable) { - throw new InvalidArgumentException(static::class . ' can only be used in purchasable field layouts.'); - } - - return PurchasableHelper::availableForPurchaseInputHtml($element->getIsFresh() ? $this->defaultAvailableForPurchase : $element->availableForPurchase, [ - 'disabled' => $static, - ]); - } - - public function settingsHtml(): string - { - return parent::settingsHtml() . Cp::lightswitchHtml( - [ - 'id' => 'defaultAvailableForPurchase', - 'name' => 'defaultAvailableForPurchase', - 'label' => Craft::t('app', 'Default Value'), - 'on' => $this->defaultAvailableForPurchase, - ] - ); - } - - /** - * @inheritdoc - */ - protected function defaultLabel(?ElementInterface $element = null, bool $static = false): ?string - { - return Craft::t('commerce', 'Available for purchase'); - } -} diff --git a/src/fieldlayoutelements/PurchasableDimensionsField.php b/src/fieldlayoutelements/PurchasableDimensionsField.php deleted file mode 100644 index 89c693f4f2..0000000000 --- a/src/fieldlayoutelements/PurchasableDimensionsField.php +++ /dev/null @@ -1,105 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasableDimensionsField extends BaseNativeField -{ - /** - * @inheritdoc - */ - public bool $mandatory = true; - - /** - * @inheritdoc - */ - public string $attribute = 'dimensions'; - - /** - * @inheritdoc - */ - protected function showLabel(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function showInForm(?ElementInterface $element = null): bool - { - if ($element instanceof Variant && !$element->getOwner()->getType()->hasDimensions) { - return false; - } - - return parent::showInForm($element); - } - - /** - * @inheritdoc - */ - public function inputHtml(ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Purchasable) { - throw new InvalidArgumentException(static::class . ' can only be used in purchasable field layouts.'); - } - - return Html::beginTag('div' , ['class' => 'flex']) . - Cp::fieldHtml(Cp::textHtml([ - 'id' => 'length', - 'name' => 'length', - 'value' => $element->length !== null ? Craft::$app->getFormattingLocale()->getFormatter()->asDecimal($element->length) : '', - 'class' => 'text', - 'size' => 10, - 'unit' => Plugin::getInstance()->getSettings()->dimensionUnits, - 'disabled' => $static, - ]), ['id' => 'length', 'label' => Craft::t('commerce', 'Length')]) . - Cp::fieldHtml(Cp::textHtml([ - 'id' => 'width', - 'name' => 'width', - 'value' => $element->width !== null ? Craft::$app->getFormattingLocale()->getFormatter()->asDecimal($element->width) : '', - 'class' => 'text', - 'size' => 10, - 'unit' => Plugin::getInstance()->getSettings()->dimensionUnits, - 'disabled' => $static, - ]), ['id' => 'width', 'label' => Craft::t('commerce', 'Width')]) . - Cp::fieldHtml(Cp::textHtml([ - 'id' => 'height', - 'name' => 'height', - 'value' => $element->height !== null ? Craft::$app->getFormattingLocale()->getFormatter()->asDecimal($element->height) : '', - 'class' => 'text', - 'size' => 10, - 'unit' => Plugin::getInstance()->getSettings()->dimensionUnits, - 'disabled' => $static, - ]), ['id' => 'height', 'label' => Craft::t('commerce', 'Height')]) . - Html::endTag('div'); - } - - /** - * @inheritdoc - */ - protected function defaultLabel(?ElementInterface $element = null, bool $static = false): ?string - { - return Craft::t('commerce', 'Dimensions'); - } -} diff --git a/src/fieldlayoutelements/PurchasableFreeShippingField.php b/src/fieldlayoutelements/PurchasableFreeShippingField.php deleted file mode 100644 index 3158fcd551..0000000000 --- a/src/fieldlayoutelements/PurchasableFreeShippingField.php +++ /dev/null @@ -1,69 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasableFreeShippingField extends BaseNativeField -{ - /** - * @inheritdoc - */ - public bool $mandatory = true; - - /** - * @inheritdoc - */ - public string $attribute = 'freeShipping'; - - /** - * @inheritdoc - */ - public function __construct(array $config = []) - { - unset($config['required']); - parent::__construct($config); - } - - /** - * @inheritdoc - */ - public function inputHtml(ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Purchasable) { - throw new InvalidArgumentException(static::class . ' can only be used in purchasable field layouts.'); - } - - return Cp::lightswitchHtml([ - 'id' => 'free-shipping', - 'name' => 'freeShipping', - 'small' => true, - 'on' => $element->freeShipping, - 'disabled' => $static, - ]); - } - - /** - * @inheritdoc - */ - protected function defaultLabel(?ElementInterface $element = null, bool $static = false): ?string - { - return Craft::t('commerce', 'Free Shipping'); - } -} diff --git a/src/fieldlayoutelements/PurchasablePriceField.php b/src/fieldlayoutelements/PurchasablePriceField.php deleted file mode 100755 index 9a987c23bb..0000000000 --- a/src/fieldlayoutelements/PurchasablePriceField.php +++ /dev/null @@ -1,241 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasablePriceField extends BaseNativeField -{ - /** - * @inheritdoc - */ - public bool $mandatory = true; - - /** - * @inheritdoc - */ - public ?string $label = '__blank__'; - - /** - * @inheritdoc - */ - public string $attribute = 'price'; - - /** - * @inheritdoc - */ - public bool $required = true; - - /** - * @inheritdoc - */ - protected function defaultLabel(?ElementInterface $element = null, bool $static = false): ?string - { - return Craft::t('commerce', 'Price'); - } - - /** - * @inheritdoc - */ - public function inputHtml(ElementInterface $element = null, bool $static = false): ?string - { - $view = Craft::$app->getView(); - $view->registerAssetBundle(HtmxAsset::class); - - if (!$element instanceof Purchasable) { - throw new InvalidArgumentException(static::class . ' can only be used in purchasable field layouts.'); - } - - $basePrice = $element->basePrice; - if (empty($element->getErrors('basePrice'))) { - if ($basePrice === null) { - $basePrice = 0; - } - - $basePrice = Craft::$app->getFormatter()->asDecimal($basePrice); - } - - $basePromotionalPrice = $element->basePromotionalPrice; - if (empty($element->getErrors('basePromotionalPrice')) && $basePromotionalPrice !== null) { - $basePromotionalPrice = Craft::$app->getFormatter()->asDecimal($basePromotionalPrice); - } - - $id = $view->namespaceInputId('commerce-purchasable-price-field'); - $priceNamespace = $view->namespaceInputName('basePrice'); - $promotionalPriceNamespace = $view->namespaceInputName('basePromotionalPrice'); - - /** @var CatalogPricingCondition $catalogPricingCondition */ - $catalogPricingCondition = Craft::$app->getConditions()->createCondition([ - 'class' => CatalogPricingCondition::class, - 'allPrices' => true, - ]); - - $purchasableConditionRule = Craft::$app->getConditions()->createConditionRule([ - 'class' => CatalogPricingPurchasableConditionRule::class, - 'elementIds' => [$element::class => [$element->id]], - ]); - $catalogPricingCondition->addConditionRule($purchasableConditionRule); - $conditionBuilderConfig = Json::encode($catalogPricingCondition->getConfig()); - - $view->registerAssetBundle(PurchasablePriceFieldAsset::class); - - $js = << { - new Craft.Commerce.PurchasablePriceField('$id', { - siteId: $element->siteId, - conditionBuilderConfig: $conditionBuilderConfig, - fieldNames: { - price: '$priceNamespace', - promotionalPrice: '$promotionalPriceNamespace', - } - }); -})(); -JS; - $view->registerJs($js, $view::POS_END); - - $canUseCatalogPricingRules = Plugin::getInstance()->getCatalogPricingRules()->canUseCatalogPricingRules(); - $toggleTitle = Craft::t('commerce', 'Show related sales'); - $toggleAttributes = ['class' => 'js-purchasable-toggle-container', 'style' => ['position' => 'relative']]; - $toggleContent = null; - - if ($canUseCatalogPricingRules) { - $toggleTitle = Craft::t('commerce', 'Show all prices'); - $toggleAttributes['data-init-prices'] = 'true'; - $toggleContent = PurchasableHelper::catalogPricingRulesTableByPurchasableId($element->id, $element->storeId) . - Html::beginTag('div', ['class' => 'flex']) . - // New catalog price button - Html::button(Craft::t('commerce', 'Add catalog price'), [ - 'class' => 'btn icon add js-cpr-slideout', - 'data-icon' => 'plus', - 'data-store-id' => $element->storeId, - 'data-store-handle' => $element->getStore()->handle, - 'data-purchasable-id' => $element->id, - ]) . - Cp::renderTemplate('commerce/prices/_status', [ - 'areCatalogPricingJobsRunning' => Plugin::getInstance()->getCatalogPricing()->areCatalogPricingJobsRunning(), - ]) . - Html::endTag('div'); - } else { - /** @var Sale[] $relatedSales */ - $relatedSales = Plugin::getInstance()->getSales()->getSalesRelatedToPurchasable($element); - - if (!empty($relatedSales)) { - $salesTags = []; - foreach ($relatedSales as $sale) { - $salesTags[] = Html::a($sale->name, $sale->getCpEditUrl()); - } - - $toggleContent = Html::tag('div', implode(', ', $salesTags)); - } - } - - $toggleContent = $static ? null : $toggleContent; - - $currency = $element->getStore()->getCurrency(); - - return Html::beginTag('div', [ - 'id' => 'commerce-purchasable-price-field', - 'class' => 'js-purchasable-price-field', - ]) . - Html::beginTag('div', ['class' => 'flex']) . - Cp::fieldHtml(Currency::moneyInputHtml($basePrice, [ - 'id' => 'base-price', - 'name' => 'basePrice', - 'currency' => $currency->getCode(), - 'currencyLabel' => $currency->getCode(), - 'required' => true, - 'errors' => $element->getErrors('basePrice'), - 'disabled' => $static, - 'size' => 12, - ]), [ - 'id' => 'base-price', - 'required' => true, - 'label' => Craft::t('commerce', 'Price'), - ]) . - - // Don't show base promotional price field if the system is still using sales - ($canUseCatalogPricingRules ? - Cp::fieldHtml(Currency::moneyInputHtml($basePromotionalPrice, [ - 'id' => 'base-promotional-price', - 'name' => 'basePromotionalPrice', - 'currency' => $currency->getCode(), - 'currencyLabel' => $currency->getCode(), - 'errors' => $element->getErrors('basePromotionalPrice'), - 'disabled' => $static, - 'size' => 12, - ]), [ - 'id' => 'promotional-price', - 'label' => Craft::t('commerce', 'Promotional Price'), - ]) : '') . - - Html::endTag('div') . - - // Hide the prices table if the element is a draft - ($toggleContent ? Html::beginTag('div', ['class' => $element->getIsDraft() ? 'hidden' : '' ]) . - Html::tag('div', - Html::tag('a', $toggleTitle, ['class' => 'fieldtoggle', 'data-target' => 'purchasable-toggle']) . - Html::beginTag('div', $toggleAttributes) . - Html::tag( - 'div', - // Prices table - $toggleContent, - [ - 'id' => 'purchasable-toggle', - 'class' => 'hidden', - ] - ) . - Html::tag('div', '', [ - 'class' => 'js-purchasable-toggle-loading hidden', - 'style' => [ - 'position' => 'absolute', - 'top' => 0, - 'left' => 0, - 'width' => '100%', - 'height' => '100%', - 'background-color' => 'rgba(255, 255, 255, 0.5)', - ], - ]) . - Html::tag('div', Html::tag('span', '', ['class' => 'spinner']), [ - 'class' => 'js-purchasable-toggle-loading flex hidden', - 'style' => [ - 'position' => 'absolute', - 'top' => 0, - 'left' => 0, - 'width' => '100%', - 'height' => '100%', - 'align-items' => 'center', - 'justify-content' => 'center', - ], - ]) . - Html::endTag('div') - ) . - Html::endTag('div') : '') . - Html::endTag('div'); - } -} diff --git a/src/fieldlayoutelements/PurchasablePromotableField.php b/src/fieldlayoutelements/PurchasablePromotableField.php deleted file mode 100644 index f3789bb2f8..0000000000 --- a/src/fieldlayoutelements/PurchasablePromotableField.php +++ /dev/null @@ -1,86 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasablePromotableField extends BaseNativeField -{ - /** - * @inheritdoc - */ - public bool $mandatory = true; - - /** - * @inheritdoc - */ - public string $attribute = 'promotable'; - - /** - * @var bool Whether the field should be checked by default when creating a new purchasable. - */ - public bool $defaultPromotable = false; - - /** - * @inheritdoc - */ - public function __construct(array $config = []) - { - unset($config['required']); - parent::__construct($config); - } - - /** - * @inheritdoc - */ - public function inputHtml(ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Purchasable) { - throw new InvalidArgumentException(static::class . ' can only be used in purchasable field layouts.'); - } - - return Cp::lightswitchHtml([ - 'id' => 'promotable', - 'name' => 'promotable', - 'small' => true, - 'on' => $element->getIsFresh() ? $this->defaultPromotable : $element->promotable, - 'disabled' => $static, - ]); - } - - public function settingsHtml(): string - { - return parent::settingsHtml() . Cp::lightswitchHtml( - [ - 'id' => 'defaultPromotable', - 'name' => 'defaultPromotable', - 'label' => Craft::t('app', 'Default Value'), - 'on' => $this->defaultPromotable, - ] - ); - } - - /** - * @inheritdoc - */ - protected function defaultLabel(?ElementInterface $element = null, bool $static = false): ?string - { - return Craft::t('commerce', 'Promotable'); - } -} diff --git a/src/fieldlayoutelements/PurchasableSkuField.php b/src/fieldlayoutelements/PurchasableSkuField.php deleted file mode 100644 index 3e44319074..0000000000 --- a/src/fieldlayoutelements/PurchasableSkuField.php +++ /dev/null @@ -1,68 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasableSkuField extends BaseNativeField -{ - /** - * @inheritdoc - */ - public bool $mandatory = true; - - /** - * @inheritdoc - */ - public bool $required = true; - - /** - * @inheritdoc - */ - public string $attribute = 'sku'; - - /** - * @inheritdoc - */ - public function inputHtml(ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Purchasable) { - throw new InvalidArgumentException(static::class . ' can only be used in purchasable field layouts.'); - } - - $variantWithSkuFormula = $element instanceof Variant && $element->getOwner()->getType()->skuFormat !== null; - if ($variantWithSkuFormula && $element->getIsDraft() && $this->getScenario() === Element::SCENARIO_DEFAULT) { - return null; - } - - return PurchasableHelper::skuInputHtml($element->getSkuAsText(), [ - 'disabled' => $static, - ]); - } - - /** - * @inheritdoc - */ - protected function defaultLabel(?ElementInterface $element = null, bool $static = false): ?string - { - return Craft::t('commerce', 'SKU'); - } -} diff --git a/src/fieldlayoutelements/PurchasableStockField.php b/src/fieldlayoutelements/PurchasableStockField.php deleted file mode 100644 index acf42ed71b..0000000000 --- a/src/fieldlayoutelements/PurchasableStockField.php +++ /dev/null @@ -1,262 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasableStockField extends BaseNativeField -{ - /** - * @inheritdoc - */ - public bool $mandatory = true; - - /** - * @inheritdoc - */ - public string $attribute = 'stock'; - - /** - * @var bool Whether inventory should be tracked by default when creating a new purchasable. - */ - public bool $defaultInventoryTracked = false; - - /** - * @var bool Whether out of stock purchases should be allowed by default when creating a new purchasable. - */ - public bool $defaultAllowOutOfStockPurchases = false; - - /** - * @inheritdoc - */ - public function __construct(array $config = []) - { - unset($config['required']); - parent::__construct($config); - } - - /** - * @inheritdoc - */ - public function inputHtml(ElementInterface $element = null, bool $static = false): ?string - { - // If this is a revision get the canonical element to show the stock for. - // @TODO Re-evaluate swapping in the canonical element once revisions support tracking inventory independently - if ($element->getIsRevision()) { - $element = $element->getCanonical(); - } - - $view = Craft::$app->getView(); - $view->registerAssetBundle(InventoryAsset::class); - - /** @var Purchasable|null $element */ - if (!$element instanceof Purchasable) { - throw new InvalidArgumentException(static::class . ' can only be used in purchasable field layouts.'); - } - - $view = Craft::$app->getView(); - - $totalStock = $element->getStock(); - $inventoryLevels = Plugin::getInstance()->getInventory()->getInventoryLevelsForPurchasable($element); - - $availableStockLabel = Craft::t('commerce', '{total} saleable across {locationCount} location(s)', [ - 'total' => $totalStock, - 'locationCount' => $inventoryLevels->count(), - ]); - - $editInventoryItemId = sprintf('action-edit-inventory-item-%s', mt_rand()); - $view->registerJsWithVars(fn($id, $settings) => << { - e.preventDefault(); - const slideout = new Craft.CpScreenSlideout('commerce/inventory/item-edit', $settings); -}); -JS, [ - $view->namespaceInputId($editInventoryItemId), - ['params' => ['inventoryItemId' => $element->getInventoryItem()->id]], - ]); - - $inventoryLevelTableRows = ''; - /** @var InventoryLevel $inventoryLevel */ - foreach ($inventoryLevels as $inventoryLevel) { - - // Update the quantity button - $editUpdateQuantityInventoryItemId = sprintf('action-update-qty-%s', mt_rand()); - $updatedValueId = sprintf('updated-value-%s', mt_rand()); - $settings = [ - 'params' => [ - 'inventoryLocationId' => $inventoryLevel->getInventoryLocation()->id, - 'ids[]' => [$element->inventoryItemId], - 'type' => 'available', - ], - ]; - - $view->registerJsWithVars(fn($id, $updatedValueId, $settings) => << { - e.preventDefault(); - const slideout = new Craft.Commerce.UpdateInventoryLevelModal($settings); - slideout.on('submit', (e) => { - if(e.response.data.updatedItems.length > 0 && e.response.data.updatedItems[0].availableTotal !== undefined) { - $('#' + $updatedValueId).html(e.response.data.updatedItems[0].availableTotal); - } - }); -}); -JS, [ - $view->namespaceInputId($editUpdateQuantityInventoryItemId), - $view->namespaceInputId($updatedValueId), - $settings, - ]); - - $inventoryLevelTableRows .= Html::beginTag('tr') . - Html::beginTag('td') . - Html::encode($inventoryLevel->getInventoryLocation()->getUiLabel()) . - Html::endTag('td') . - Html::beginTag('td') . - Html::beginTag('div', ['class' => 'flex']) . - Html::tag('div', (string)$inventoryLevel->availableTotal, [ - 'id' => $updatedValueId, - ]) . - (!$static ? Html::tag('div', Html::button(Craft::t('commerce', ''), - [ - 'class' => 'btn menubtn action-btn', - 'id' => $editUpdateQuantityInventoryItemId, - ])) : '') . - Html::endTag('div') . - Html::endTag('td') . - (!$static ? Html::beginTag('td') . - (Craft::$app->getUser()->checkPermission('commerce-manageInventoryStockLevels') ? - Html::a( - Craft::t('commerce', 'Manage'), - UrlHelper::cpUrl('commerce/inventory/levels/' . $inventoryLevel->getInventoryLocation()->handle, [ - 'inventoryItemId' => $inventoryLevel->getInventoryItem()->id, - ]), - [ - 'target' => '_blank', - 'class' => 'btn small', - 'id' => $editUpdateQuantityInventoryItemId, - 'aria-label' => Craft::t('app', 'Open in a new tab'), - 'data-icon' => 'external', - ] - ) : '') : '') . - Html::endTag('td') . - Html::endTag('tr'); - } - - $inventoryLevelsTable = Html::beginTag('table', ['class' => 'data fullwidth', 'style' => 'margin-top:5px;']) . - Html::beginTag('thead') . - Html::beginTag('tr') . - Html::beginTag('th') . - Craft::t('commerce', 'Location') . - Html::endTag('th') . - Html::beginTag('th') . - Craft::t('commerce', 'Available') . - Html::endTag('th') . - - - (!$static ? Html::beginTag('th') . - Craft::t('commerce', 'Manage') . - Html::endTag('th') : '') . - - - Html::endTag('tr') . - Html::endTag('thead') . - - Html::beginTag('tbody') . - $inventoryLevelTableRows . - Html::beginTag('tr') . - Html::beginTag('td', ['colspan' => '2']) . - $availableStockLabel . - Html::endTag('td') . - - (!$static ? Html::beginTag('td') . - Html::a( - Craft::t('commerce', 'Edit'), - '#', - [ - 'class' => 'btn small', - 'id' => $editInventoryItemId, - 'aria-label' => Craft::t('app', 'Edit Inventory Item'), - 'data-icon' => 'edit', - ] - ) . - Html::endTag('td') : '') . - - Html::endTag('tr') . - Html::endTag('tbody') . - Html::endTag('table'); - - $inventoryItemTrackedId = sprintf('store-inventory-item-tracked-%s', mt_rand()); - $storeInventoryTrackedLightswitchConfig = [ - 'id' => 'store-inventory-item-tracked', - 'name' => 'inventoryTracked', - 'small' => true, - 'on' => $element->getIsFresh() ? $this->defaultInventoryTracked : $element->inventoryTracked, - 'toggle' => $inventoryItemTrackedId, - 'disabled' => $static, - ]; - - $storeAllowOutOfStockPurchasesLightswitchConfig = [ - 'label' => Craft::t('commerce', 'Allow out of stock purchases'), - 'id' => 'store-backorder-allowed', - 'name' => 'allowOutOfStockPurchases', - 'small' => true, - 'on' => $element->getIsFresh() ? $this->defaultAllowOutOfStockPurchases : $element->getIsOutOfStockPurchasingAllowed(), - 'disabled' => $static, - ]; - - - return Html::beginTag('div') . - Cp::lightswitchHtml($storeInventoryTrackedLightswitchConfig) . - Html::beginTag('div', ['id' => $inventoryItemTrackedId, 'class' => 'hidden']) . - $inventoryLevelsTable . - Cp::lightswitchFieldHtml($storeAllowOutOfStockPurchasesLightswitchConfig) . - Html::endTag('div') . - Html::endTag('div'); - } - - public function settingsHtml(): string - { - $lightSwitches = Cp::lightswitchHtml([ - 'id' => 'defaultInventoryTracked', - 'name' => 'defaultInventoryTracked', - 'label' => Craft::t('commerce', 'Track Inventory'), - 'on' => $this->defaultInventoryTracked, - ]) . - Cp::lightswitchHtml([ - 'id' => 'defaultAllowOutOfStockPurchases', - 'name' => 'defaultAllowOutOfStockPurchases', - 'label' => Craft::t('commerce', 'Allow out of stock purchases'), - 'on' => $this->defaultAllowOutOfStockPurchases, - ]); - - return parent::settingsHtml() . Cp::fieldHtml($lightSwitches, ['label' => Craft::t('app', 'Default Value')]); - } - - /** - * @inheritdoc - */ - protected function defaultLabel(?ElementInterface $element = null, bool $static = false): ?string - { - return Craft::t('commerce', 'Track Inventory'); - } -} diff --git a/src/fieldlayoutelements/PurchasableWeightField.php b/src/fieldlayoutelements/PurchasableWeightField.php deleted file mode 100755 index 92429e9698..0000000000 --- a/src/fieldlayoutelements/PurchasableWeightField.php +++ /dev/null @@ -1,85 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasableWeightField extends BaseNativeField -{ - /** - * @inheritdoc - */ - public bool $mandatory = true; - - /** - * @inheritdoc - */ - public string $attribute = 'weight'; - - /** - * @inheritdoc - */ - protected function showLabel(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function showInForm(?ElementInterface $element = null): bool - { - if ($element instanceof Variant && !$element->getOwner()->getType()->hasDimensions) { - return false; - } - - return parent::showInForm($element); - } - - /** - * @inheritdoc - */ - public function inputHtml(ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Purchasable) { - throw new InvalidArgumentException(static::class . ' can only be used in purchasable field layouts.'); - } - - return Cp::textHtml([ - 'id' => 'weight', - 'name' => 'weight', - 'value' => $element->weight !== null ? Craft::$app->getFormattingLocale()->getFormatter()->asDecimal($element->weight) : '', - 'class' => 'text', - 'size' => 10, - 'unit' => Plugin::getInstance()->getSettings()->weightUnits, - 'placeholder' => Craft::t('commerce', 'Weight'), - 'disabled' => $static, - ]); - } - - /** - * @inheritdoc - */ - protected function defaultLabel(?ElementInterface $element = null, bool $static = false): ?string - { - return Craft::t('commerce', 'Weight'); - } -} diff --git a/src/fieldlayoutelements/TransferManagementField.php b/src/fieldlayoutelements/TransferManagementField.php deleted file mode 100644 index 37866dce1a..0000000000 --- a/src/fieldlayoutelements/TransferManagementField.php +++ /dev/null @@ -1,306 +0,0 @@ - - * @since 5.0.0 - */ -class TransferManagementField extends BaseNativeField -{ - /** - * @inheritdoc - */ - public bool $mandatory = true; - - /** - * @inheritdoc - */ - public ?string $label = '__blank__'; - - /** - * @inheritdoc - */ - public bool $required = true; - - /** - * @inheritdoc - */ - public string $attribute = 'transfer-management'; - - /** - * @inheritdoc - */ - public function inputHtml(ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Transfer) { - throw new InvalidArgumentException('TransferLocationsField can only be used in transfer field layouts.'); - } - - if ($static) { - return self::renderStaticFieldHtml($element); - } else { - return self::renderFieldHtml($element); - } - } - - public static function renderStaticFieldHtml(Transfer $element, bool $static = false): string - { - $html = ''; - $currentUser = Craft::$app->getUser()->getIdentity(); - - $origin = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($element->originLocationId); - $destination = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($element->destinationLocationId); - - $html .= Html::tag('div', - Html::tag('div', - Cp::elementCardHtml($origin->getAddress()), ['class' => 'flex-grow']) . - Html::tag('div', - Cp::elementCardHtml($destination->getAddress()), ['class' => 'flex-grow']) - , ['class' => 'flex']); - - - $tableRows = ''; - - foreach ($element->getDetails() as $detail) { - $purchasable = $detail->getInventoryItem()?->getPurchasable(Cp::requestedSite()->id); - $tableRows .= Html::tag('tr', - Html::tag('td', ($purchasable ? Cp::chipHtml($purchasable, ['showActionMenu' => !$purchasable->getIsDraft() && $purchasable->canSave($currentUser)]) : Html::tag('span', $detail->inventoryItemDescription))) . - Html::tag('td', (string)$detail->quantityRejected, ['class' => 'rightalign']) . - Html::tag('td', (string)$detail->quantityAccepted, ['class' => 'rightalign']) . - Html::tag('td', $detail->getReceived() . '/' . $detail->quantity, ['class' => 'rightalign']) - ); - }; - - $totalRow = Html::tag('tr', - Html::tag('td') . - Html::tag('td', '') . - Html::tag('td', '') . - Html::tag('td', Craft::t('commerce', 'Total ') . ' ' . $element->getTotalReceived() . '/' . $element->getTotalQuantity(), ['class' => 'rightalign']) - ); - - $table = Html::tag('table', - Html::tag('thead', - Html::tag('tr', - Html::tag('th', Craft::t('commerce', 'Inventory Item')) . - Html::tag('th', Craft::t('commerce', 'Rejected'), ['class' => 'rightalign', 'style' => "width: 20%;"]) . - Html::tag('th', Craft::t('commerce', 'Accepted'), ['class' => 'rightalign', 'style' => "width: 20%;"]) . - Html::tag('th', Craft::t('commerce', 'Total'), ['class' => 'rightalign', 'style' => "width: 20%;"]) - ) - ) . - Html::tag('tbody', $tableRows . $totalRow) - , ['class' => 'data fullwidth'] - ); - - - $html .= Html::tag('hr') . $table; - - return $html; - } - - public static function renderFieldHtml(Transfer $element): string - { - // Only draft is editable - if (!$element->isTransferDraft()) { - return self::renderStaticFieldHtml($element); - } - - $currentUser = Craft::$app->getUser()->getIdentity(); - $view = Craft::$app->getView(); - $inventoryLocationOptions = Plugin::getInstance()->getInventoryLocations()->getAllInventoryLocationsAsList(false); - $isHtmxRequest = Craft::$app->getRequest()->getHeaders()->has('HX-Request'); - - $allLocations = Plugin::getInstance()->getInventoryLocations()->getAllInventoryLocations(); - $defaultFirstLocation = $allLocations->first(); - $defaultSecondLocation = $allLocations->skip(1)->first(); - - Craft::$app->getView()->registerAssetBundle(TransfersAsset::class); - - $namespacedId = $view->namespaceInputId('transfer-management'); - - $html = Html::beginTag('div', [ - 'id' => $namespacedId, - 'hx' => [ - 'ext' => 'craft-cp', - 'target' => '#' . $namespacedId, - 'include' => '#' . $namespacedId, - 'vals' => [ - 'action' => 'commerce/transfers/render-management', - 'transferId' => $element->id, - ], - ], - ]); - - $originLocationSelectFieldConfig = [ - 'label' => Craft::t('commerce', 'Origin'), - 'name' => 'originLocationId', // 'name' => 'fields[locations][originLocationId] - 'options' => $inventoryLocationOptions, - 'errors' => $element->getErrors('originLocationId'), - 'value' => $element->originLocationId ?? $defaultFirstLocation->id, - 'inputAttributes' => [ - 'hx' => [ - 'post' => '', - 'trigger' => 'change', - ], - ], - ]; - - $destinationLocationSelectFieldConfig = [ - 'label' => Craft::t('commerce', 'Destination'), - 'name' => 'destinationLocationId', - 'errors' => $element->getErrors('destinationLocationId'), - 'options' => $inventoryLocationOptions, - 'value' => $element->destinationLocationId ?? $defaultSecondLocation->id, - 'inputAttributes' => [ - 'hx' => [ - 'post' => '', - 'trigger' => 'change', - ], - ], - ]; - - $destinationLocationSelectField = Html::tag('div', Cp::selectFieldHtml($destinationLocationSelectFieldConfig), ['class' => 'flex-grow']); - $originLocationSelectField = Html::tag('div', Cp::selectFieldHtml($originLocationSelectFieldConfig), ['class' => 'flex-grow']); - - $html .= Html::tag('div', $originLocationSelectField . $destinationLocationSelectField, ['class' => 'flex']); - - $tableRows = ''; - $loop = 1; - - foreach ($element->getDetails() as $detail) { - $key = $detail->uid ?? StringHelper::UUID(); - $purchasable = $detail->getInventoryItem()?->getPurchasable(Cp::requestedSite()->id); - $tableRows .= Html::tag('tr', - Html::hiddenInput('details[' . $key . '][id]', (string)$detail->id) . - Html::hiddenInput('details[' . $key . '][uid]', $detail->uid) . - Html::hiddenInput('details[' . $key . '][inventoryItemId]', (string)$detail->inventoryItemId) . - Html::tag('td', ($purchasable ? Cp::chipHtml($purchasable, ['showActionMenu' => !$purchasable->getIsDraft() && $purchasable->canSave($currentUser)]) : Html::tag('span', $detail->inventoryItemDescription))) . - Html::tag('td', Cp::textHtml([ - 'type' => 'number', - 'name' => 'details[' . $key . '][quantity]', - 'value' => (string)$detail->quantity, - 'class' => 'text fullwidth', - 'errors' => $element->getErrors('details.' . $key . '.quantity'), - 'inputAttributes' => [ - 'hx' => [ - 'post' => '', - ], - ], - ])) . - Html::tag('td', Html::a('', '#', [ - 'hx' => [ - 'post' => '', - 'trigger' => 'click', - 'vals' => [ - 'removeInventoryItemUid' => $key, - ], - ], - 'class' => 'delete icon', - 'title' => Craft::t('app', 'Delete'), - 'aria-label' => Craft::t('app', 'Delete'), - 'role' => 'button', - ]), ['class' => 'thin']) - ); - }; - - // sum row - $tableRows .= Html::tag('tr', - Html::tag('td') . - Html::tag('td', $element->sumDetailsQuanity() . ' ' . Craft::t('commerce', 'Total')) . - Html::tag('td',) - ); - - $table = Html::tag('table', - Html::tag('thead', - Html::tag('tr', - Html::tag('th', Craft::t('commerce', 'Inventory Item')) . - Html::tag('th', Craft::t('commerce', 'Quantity'), ['style' => "width: 20%;"]) . - Html::tag('th', '') - ) - ) . - Html::tag('tbody', $tableRows) - , ['class' => 'data fullwidth'] - ); - - $html .= Cp::fieldHtml($table, [ - 'label' => Craft::t('commerce', 'Transfer Items'), - ]); - - if ($element->originLocationId) { - $sourceLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($element->originLocationId); - } else { - $sourceLocation = $defaultFirstLocation; - } - - $inventoryLevels = Plugin::getInstance()->getInventory()->getInventoryLocationLevels($sourceLocation)->sortByDesc([ - fn(InventoryLevel $level) => $level->onHandTotal, - ]); - $inventoryItemOptions = []; - - - /** @var InventoryLevel $level */ - foreach ($inventoryLevels as $level) { - $inventoryItemOptions[] = [ - 'label' => $level->getInventoryItem()->getSku() . ' (' . ($level->onHandTotal ? $level->onHandTotal . ' ' . Craft::t('commerce', 'on hand') : Craft::t('commerce', 'None on hand')) . ')', - 'value' => $level->getInventoryItem()->id, - 'disabled' => !($level->onHandTotal > 0), - ]; - } - - Craft::$app->getView()->startJsBuffer(); - - $addToItems = Html::tag('div', - - Cp::selectizeHtml([ - 'name' => 'newInventoryItemId', - 'options' => $inventoryItemOptions, - 'value' => '', - 'placeholder' => Craft::t('commerce', 'Select an item'), - ]) . - - Html::button(Craft::t('commerce', 'Add an item'), [ - 'class' => 'btn secondary', - 'hx' => [ - 'post' => '', - 'target' => '#' . $namespacedId, - 'trigger' => 'click', - 'vals' => [ - 'addItem' => true, - ], - ], - ]) - , ['class' => 'flex']); - - $html .= $addToItems; - $fieldJs = (string)$view->clearJsBuffer(false); - - if ($fieldJs) { - if ($isHtmxRequest) { - $html .= html::tag('script', $fieldJs, ['type' => 'text/javascript']); - } else { - $view->registerJs($fieldJs); - } - } - - return $html . Html::endTag('div'); - } -} diff --git a/src/fieldlayoutelements/UserAddressSettings.php b/src/fieldlayoutelements/UserAddressSettings.php deleted file mode 100644 index c234d43e59..0000000000 --- a/src/fieldlayoutelements/UserAddressSettings.php +++ /dev/null @@ -1,92 +0,0 @@ - - * @since 4.0.0 - */ -class UserAddressSettings extends BaseField -{ - /** - * @inheritdoc - */ - public function attribute(): string - { - return 'commerceSettings'; - } - - /** - * @inheritdoc - */ - public function mandatory(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function hasCustomWidth(): bool - { - return false; - } - - protected function useFieldset(): bool - { - return true; - } - - /** - * @inheritdoc - */ - protected function defaultLabel(ElementInterface $element = null, bool $static = false): ?string - { - return Craft::t('commerce', 'Commerce Settings'); - } - - /** - * @inheritdoc - */ - protected function inputHtml(?ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Address) { - throw new InvalidArgumentException('UserAddressSettings can only be used in the address field layout.'); - } - - /** @var Address|CustomerAddressBehavior $element */ - $owner = $element->getOwner(); - - if (!$owner instanceof User) { - return null; - } - - return - Cp::lightswitchFieldHtml([ - 'fieldLabel' => Craft::t('commerce', 'Use as the primary billing address'), - 'name' => 'isPrimaryBilling', - 'on' => $element->getIsPrimaryBilling(), - ]) . - Cp::lightswitchFieldHtml([ - 'fieldLabel' => Craft::t('commerce', 'Use as the primary shipping address'), - 'name' => 'isPrimaryShipping', - 'on' => $element->getIsPrimaryShipping(), - ]); - } -} diff --git a/src/fieldlayoutelements/VariantTitleField.php b/src/fieldlayoutelements/VariantTitleField.php deleted file mode 100644 index 2ca44fa4ab..0000000000 --- a/src/fieldlayoutelements/VariantTitleField.php +++ /dev/null @@ -1,77 +0,0 @@ - - * @since 3.2.0 - */ -class VariantTitleField extends TitleField -{ - /** - * @inheritdoc - */ - protected function selectorInnerHtml(): string - { - return - Html::tag('span', '', [ - 'class' => ['fld-variant-title-field-icon', 'fld-field-hidden', 'hidden'], - ]) . - parent::selectorInnerHtml(); - } - - /** - * @inheritdoc - */ - protected function translatable(?ElementInterface $element = null, bool $static = false): bool - { - if (!$element instanceof Variant) { - throw new \InvalidArgumentException(sprintf('%s can only be used in variant field layouts.', self::class)); - } - - return $element->getOwner()->getType()->variantTitleTranslationMethod !== Field::TRANSLATION_METHOD_NONE; - } - - /** - * @inheritdoc - */ - protected function translationDescription(?ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Variant) { - throw new \InvalidArgumentException(sprintf('%s can only be used in variant field layouts.', self::class)); - } - - return ElementHelper::translationDescription($element->getOwner()->getType()->variantTitleTranslationMethod); - } - - /** - * @inheritdoc - */ - public function inputHtml(ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Variant) { - throw new InvalidArgumentException('VariantTitleField can only be used in variant field layouts.'); - } - - if (!$element->getOwner()->getType()->hasVariantTitleField) { - return null; - } - - return parent::inputHtml($element, $static); - } -} diff --git a/src/fieldlayoutelements/VariantsField.php b/src/fieldlayoutelements/VariantsField.php deleted file mode 100644 index ceef5f02d6..0000000000 --- a/src/fieldlayoutelements/VariantsField.php +++ /dev/null @@ -1,74 +0,0 @@ - - * @since 3.2.0 - */ -class VariantsField extends BaseNativeField -{ - /** - * @inheritdoc - */ - public bool $mandatory = true; - - /** - * @inheritdoc - */ - public string $attribute = 'variants'; - - /** - * @inheritdoc - */ - public function hasCustomWidth(): bool - { - return false; - } - - /** - * @inheritdoc - */ - protected function defaultLabel(ElementInterface $element = null, bool $static = false): ?string - { - return Craft::t('commerce', 'Variants'); - } - - /** - * @inheritdoc - */ - protected function inputHtml(ElementInterface $element = null, bool $static = false): ?string - { - if (!$element instanceof Product) { - throw new InvalidArgumentException('ProductTitleField can only be used in product field layouts.'); - } - - Craft::$app->getView()->registerDeltaName($this->attribute()); - - $maxVariants = $element->getType()->maxVariants; - - return $element->getVariantManager()->getIndexHtml($element, [ - 'canCreate' => !$static, - 'canPaste' => !$static, - 'minElements' => 0, - 'maxElements' => $maxVariants ?? null, - 'allowedViewModes' => [ElementIndexViewMode::Cards, ElementIndexViewMode::Table], - 'sortable' => !$static, - 'fieldLayouts' => [$element->getType()->getVariantFieldLayout()], - ]); - } -} diff --git a/src/fields/Products.php b/src/fields/Products.php deleted file mode 100644 index 3bc9c204a0..0000000000 --- a/src/fields/Products.php +++ /dev/null @@ -1,114 +0,0 @@ - - * @since 2.0 - * - * @property-read array $contentGqlType - */ -class Products extends BaseRelationField -{ - /** - * @inheritdoc - */ - protected ?string $inputJsClass = 'Craft.Commerce.ProductSelectInput'; - - public function __construct(array $config = []) - { - // Never needed and allows us to instantiate the field while ignoring old setting until the Product field migration has run. - unset($config['targetLocale']); - parent::__construct($config); - } - - /** - * @inheritdoc - */ - public static function icon(): string - { - return 'tag'; - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Commerce Products'); - } - - /** - * @inheritdoc - */ - public static function defaultSelectionLabel(): string - { - return Craft::t('commerce', 'Add a product'); - } - - /** - * @inheritdoc - */ - protected function inputTemplateVariables(array|ElementQueryInterface $value = null, ?ElementInterface $element = null): array - { - Craft::$app->getView()->registerAssetBundle(CommerceCpAsset::class); - Craft::$app->getView()->registerAssetBundle(ProductIndexAsset::class); - - $variables = parent::inputTemplateVariables($value, $element); - - $sources = $this->getInputSources($element); - if (is_array($sources) && preg_match('/^productType:(.+)$/', reset($sources), $matches)) { - $productType = Plugin::getInstance()->getProductTypes()->getProductTypeByUid($matches[1]); - if ($productType) { - $variables['jsSettings']['productTypeId'] = (int)$productType->id; - } - } - - return $variables; - } - - /** - * @inheritdoc - * @since 3.1.4 - */ - public function getContentGqlType(): array|Type - { - return [ - 'name' => $this->handle, - 'type' => Type::listOf(ProductInterface::getType()), - 'args' => ProductArguments::getArguments(), - 'resolve' => ProductResolver::class . '::resolve', - 'complexity' => GqlHelper::relatedArgumentComplexity(GqlService::GRAPHQL_COMPLEXITY_EAGER_LOAD), - ]; - } - - /** - * @inheritdoc - */ - public static function elementType(): string - { - return Product::class; - } -} diff --git a/src/fields/Variants.php b/src/fields/Variants.php deleted file mode 100644 index 004536430c..0000000000 --- a/src/fields/Variants.php +++ /dev/null @@ -1,76 +0,0 @@ - - * @since 2.0 - * - * @property-read array $contentGqlType - */ -class Variants extends BaseRelationField -{ - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Commerce Variants'); - } - - /** - * @inheritdoc - */ - public static function icon(): string - { - return 'tags'; - } - - /** - * @inheritdoc - */ - public static function defaultSelectionLabel(): string - { - return Craft::t('commerce', 'Add a variant'); - } - - /** - * @inheritdoc - * @since 3.1.4 - */ - public function getContentGqlType(): array|Type - { - return [ - 'name' => $this->handle, - 'type' => Type::listOf(VariantInterface::getType()), - 'args' => VariantArguments::getArguments(), - 'resolve' => VariantResolver::class . '::resolve', - 'complexity' => GqlHelper::relatedArgumentComplexity(GqlService::GRAPHQL_COMPLEXITY_EAGER_LOAD), - ]; - } - - /** - * @inheritdoc - */ - public static function elementType(): string - { - return Variant::class; - } -} diff --git a/src/gateways/Dummy.php b/src/gateways/Dummy.php deleted file mode 100644 index 8add1f0790..0000000000 --- a/src/gateways/Dummy.php +++ /dev/null @@ -1,397 +0,0 @@ - - * @since 2.0 - */ -class Dummy extends SubscriptionGateway -{ - /** - * @inheritdoc - */ - public function getPaymentFormHtml(array $params): ?string - { - $paymentFormModel = $this->getPaymentFormModel(); - - if (Craft::$app->getConfig()->general->devMode) { - $paymentFormModel->firstName = 'Jenny'; - $paymentFormModel->lastName = 'Andrews'; - $paymentFormModel->number = '4242424242424242'; - $paymentFormModel->expiry = '01/' . date('Y', strtotime('+1 year')); - $paymentFormModel->cvv = '123'; - } - - $defaults = [ - 'paymentForm' => $paymentFormModel, - ]; - - $params = array_merge($defaults, $params); - - $view = Craft::$app->getView(); - $previousMode = $view->getTemplateMode(); - $view->setTemplateMode(View::TEMPLATE_MODE_CP); - $html = Craft::$app->getView()->renderTemplate('commerce/_components/gateways/_creditCardFields', $params); - $view->setTemplateMode($previousMode); - - return $html; - } - - /** - * @inheritdoc - */ - public function getPaymentFormModel(): DummyPaymentForm - { - return new DummyPaymentForm(); - } - - /** - * @inheritdoc - */ - public function authorize(Transaction $transaction, BasePaymentForm $form): RequestResponseInterface - { - if (!$form instanceof CreditCardPaymentForm) { - throw new InvalidArgumentException(sprintf('%s only accepts %s objects passed to $form.', __METHOD__, CreditCardPaymentForm::class)); - } - - return new DummyRequestResponse($form); - } - - /** - * @inheritdoc - */ - public function capture(Transaction $transaction, string $reference): RequestResponseInterface - { - return new DummyRequestResponse(); - } - - /** - * @inheritdoc - */ - public function completeAuthorize(Transaction $transaction): RequestResponseInterface - { - return new DummyRequestResponse(); - } - - /** - * @inheritdoc - */ - public function completePurchase(Transaction $transaction): RequestResponseInterface - { - return new DummyRequestResponse(); - } - - /** - * @inheritdoc - */ - public function createPaymentSource(BasePaymentForm $sourceData, int $customerId): PaymentSource - { - /** @var CreditCardPaymentForm $sourceData */ - - $paymentSource = new PaymentSource(); - $paymentSource->customerId = $customerId; - $paymentSource->gatewayId = $this->id; - $paymentSource->token = StringHelper::randomString(); - $paymentSource->response = ''; - $paymentSource->description = 'Card ending with ' . StringHelper::last($sourceData->number, 4); - - return $paymentSource; - } - - /** - * @inheritdoc - */ - public function deletePaymentSource(string $token): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function purchase(Transaction $transaction, BasePaymentForm $form): RequestResponseInterface - { - if (!$form instanceof CreditCardPaymentForm) { - throw new InvalidArgumentException(sprintf('%s only accepts %s objects passed to $form.', __METHOD__, CreditCardPaymentForm::class)); - } - - return new DummyRequestResponse($form); - } - - /** - * @inheritdoc - */ - public function processWebHook(): WebResponse - { - throw new NotSupportedException(self::class . ' does not support processWebhook()'); - } - - /** - * @inheritdoc - */ - public function refund(Transaction $transaction): RequestResponseInterface - { - $form = new DummyPaymentForm(); - - if ($transaction->note != 'fail') { - $form->number = '4242424242424242'; - } else { - $form->number = '378282246310005'; - } - - return new DummyRequestResponse($form); - } - - /** - * @inheritdoc - */ - public function supportsAuthorize(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function supportsCapture(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function supportsCompleteAuthorize(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function supportsCompletePurchase(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function supportsPaymentSources(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function supportsPurchase(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function supportsRefund(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function supportsPartialRefund(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function supportsWebhooks(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function getCancelSubscriptionFormHtml(Subscription $subscription): string - { - return ''; - } - - /** - * @inheritdoc - */ - public function getCancelSubscriptionFormModel(): CancelSubscriptionForm - { - return new CancelSubscriptionForm(); - } - - /** - * @inheritdoc - */ - public function getPlanSettingsHtml(array $params = []): ?string - { - return ''; - } - - /** - * @inheritdoc - */ - public function getPlanModel(): Plan - { - return new DummyPlan(); - } - - /** - * @inheritdoc - */ - public function getSubscriptionFormModel(): SubscriptionForm - { - return new SubscriptionForm(); - } - - /** - * @inheritdoc - */ - public function getSwitchPlansFormModel(): SwitchPlansForm - { - return new SwitchPlansForm(); - } - - /** - * @inheritdoc - */ - public function cancelSubscription(Subscription $subscription, CancelSubscriptionForm $parameters): SubscriptionResponseInterface - { - $response = new DummySubscriptionResponse(); - $response->setIsCanceled(true); - return $response; - } - - /** - * @inheritdoc - */ - public function getNextPaymentAmount(Subscription $subscription): string - { - return '-'; - } - - /** - * @inheritdoc - */ - public function getSubscriptionPayments(Subscription $subscription): array - { - return []; - } - - /** - * @inheritdoc - */ - public function getSubscriptionPlanByReference(string $reference): string - { - return 'dummy.plan'; - } - - /** - * @inheritdoc - */ - public function getSubscriptionPlans(): array - { - return []; - } - - /** - * @inheritdoc - */ - public function subscribe(User $user, Plan $plan, SubscriptionForm $parameters): SubscriptionResponseInterface - { - $subscription = new DummySubscriptionResponse(); - $subscription->setTrialDays($parameters->trialDays); - - return $subscription; - } - - /** - * @inheritdoc - */ - public function switchSubscriptionPlan(Subscription $subscription, Plan $plan, SwitchPlansForm $parameters): SubscriptionResponseInterface - { - return new DummySubscriptionResponse(); - } - - /** - * @inheritdoc - */ - public function supportsReactivation(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function supportsPlanSwitch(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function getBillingIssueDescription(Subscription $subscription): string - { - return ''; - } - - /** - * @inheritdoc - */ - public function getBillingIssueResolveFormHtml(Subscription $subscription): string - { - return ''; - } - - /** - * @inheritdoc - */ - public function getHasBillingIssues(Subscription $subscription): bool - { - return false; - } -} diff --git a/src/gateways/Manual.php b/src/gateways/Manual.php deleted file mode 100644 index ee794e20c1..0000000000 --- a/src/gateways/Manual.php +++ /dev/null @@ -1,256 +0,0 @@ - - * @since 2.0 - * - * @property bool|string $onlyAllowForZeroPriceOrders - * @property-read null|string $settingsHtml - */ -class Manual extends Gateway -{ - /** - * @var bool - */ - private string|bool $_onlyAllowForZeroPriceOrders = false; - - public function getSettings(): array - { - $settings = parent::getSettings(); - $settings['onlyAllowForZeroPriceOrders'] = $this->getOnlyAllowForZeroPriceOrders(false); - - return $settings; - } - - /** - * @inheritdoc - */ - public function getPaymentFormHtml(array $params): ?string - { - return ''; - } - - /** - * @inheritdoc - */ - public function getPaymentFormModel(): BasePaymentForm - { - return new OffsitePaymentForm(); - } - - /** - * @inheritdoc - */ - public function getSettingsHtml(): ?string - { - return Craft::$app->getView()->renderTemplate('commerce/gateways/manualGatewaySettings', ['gateway' => $this]); - } - - /** - * @inheritdoc - */ - public function authorize(Transaction $transaction, BasePaymentForm $form): RequestResponseInterface - { - return new ManualRequestResponse(); - } - - /** - * @inheritdoc - */ - public function capture(Transaction $transaction, string $reference): RequestResponseInterface - { - return new ManualRequestResponse(); - } - - /** - * @inheritdoc - */ - public function completeAuthorize(Transaction $transaction): RequestResponseInterface - { - throw new NotImplementedException(Craft::t('commerce', 'This gateway does not support that functionality.')); - } - - /** - * @inheritdoc - */ - public function completePurchase(Transaction $transaction): RequestResponseInterface - { - throw new NotImplementedException(Craft::t('commerce', 'This gateway does not support that functionality.')); - } - - /** - * @inheritdoc - */ - public function createPaymentSource(BasePaymentForm $sourceData, int $customerId): PaymentSource - { - throw new NotImplementedException(Craft::t('commerce', 'This gateway does not support that functionality.')); - } - - /** - * @inheritdoc - */ - public function deletePaymentSource(string $token): bool - { - throw new NotImplementedException(Craft::t('commerce', 'This gateway does not support that functionality.')); - } - - /** - * @inheritdoc - */ - public function getPaymentTypeOptions(): array - { - return [ - 'authorize' => Craft::t('commerce', 'Authorize Only (Manually Capture)'), - ]; - } - - /** - * @inheritdoc - */ - public function purchase(Transaction $transaction, BasePaymentForm $form): RequestResponseInterface - { - throw new NotImplementedException(Craft::t('commerce', 'This gateway does not support that functionality.')); - } - - /** - * @inheritdoc - */ - public function processWebHook(): WebResponse - { - throw new NotImplementedException(Craft::t('commerce', 'This gateway does not support that functionality.')); - } - - /** - * @inheritdoc - */ - public function refund(Transaction $transaction): RequestResponseInterface - { - return new ManualRequestResponse(); - } - - /** - * @inheritdoc - */ - public function supportsAuthorize(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function supportsCapture(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function supportsCompleteAuthorize(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function supportsCompletePurchase(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function supportsPaymentSources(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function supportsPurchase(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function supportsRefund(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function supportsPartialRefund(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function supportsWebhooks(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function availableForUseWithOrder(Order $order): bool - { - if ($this->getOnlyAllowForZeroPriceOrders() && $order->getTotalPrice() != 0) { - return false; - } - - return parent::availableForUseWithOrder($order); - } - - /** - * @param bool $parse - * @return bool|string - * @since 4.1.1 - */ - public function getOnlyAllowForZeroPriceOrders(bool $parse = true): bool|string - { - return $parse ? (App::parseBooleanEnv($this->_onlyAllowForZeroPriceOrders) ?? false) : $this->_onlyAllowForZeroPriceOrders; - } - - /** - * @param bool|string $onlyAllowForZeroPriceOrders - * @return void - * @since 4.1.1 - */ - public function setOnlyAllowForZeroPriceOrders(bool|string $onlyAllowForZeroPriceOrders): void - { - $this->_onlyAllowForZeroPriceOrders = $onlyAllowForZeroPriceOrders; - } -} diff --git a/src/gateways/MissingGateway.php b/src/gateways/MissingGateway.php deleted file mode 100644 index 7af34dc659..0000000000 --- a/src/gateways/MissingGateway.php +++ /dev/null @@ -1,193 +0,0 @@ - - * @since 2.0 - */ -class MissingGateway extends Gateway implements MissingComponentInterface -{ - use MissingComponentTrait; - - public function __set($name, $value) - { - } - - /** - * @inheritdoc - */ - public function getPaymentFormHtml(array $params): ?string - { - throw new NotSupportedException(); - } - - /** - * @inheritdoc - */ - public function getPaymentFormModel(): BasePaymentForm - { - throw new NotSupportedException(); - } - - /** - * @inheritdoc - */ - public function authorize(Transaction $transaction, BasePaymentForm $form): RequestResponseInterface - { - throw new NotSupportedException(); - } - - /** - * @inheritdoc - */ - public function capture(Transaction $transaction, string $reference): RequestResponseInterface - { - throw new NotSupportedException(); - } - - /** - * @inheritdoc - */ - public function completeAuthorize(Transaction $transaction): RequestResponseInterface - { - throw new NotSupportedException(); - } - - /** - * @inheritdoc - */ - public function completePurchase(Transaction $transaction): RequestResponseInterface - { - throw new NotSupportedException(); - } - - /** - * @inheritdoc - */ - public function createPaymentSource(BasePaymentForm $sourceData, int $userId): PaymentSource - { - throw new NotSupportedException(); - } - - /** - * @inheritdoc - */ - public function deletePaymentSource(string $token): bool - { - throw new NotSupportedException(); - } - - /** - * @inheritdoc - */ - public function purchase(Transaction $transaction, BasePaymentForm $form): RequestResponseInterface - { - throw new NotSupportedException(); - } - - /** - * @inheritdoc - */ - public function processWebHook(): WebResponse - { - throw new NotSupportedException(); - } - - /** - * @inheritdoc - */ - public function refund(Transaction $transaction): RequestResponseInterface - { - throw new NotSupportedException(); - } - - /** - * @inheritdoc - */ - public function supportsAuthorize(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function supportsCapture(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function supportsCompleteAuthorize(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function supportsCompletePurchase(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function supportsPaymentSources(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function supportsPurchase(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function supportsRefund(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function supportsWebhooks(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function supportsPartialRefund(): bool - { - return false; - } -} diff --git a/src/gql/arguments/elements/Product.php b/src/gql/arguments/elements/Product.php deleted file mode 100644 index 562697cc7d..0000000000 --- a/src/gql/arguments/elements/Product.php +++ /dev/null @@ -1,95 +0,0 @@ - - * @since 3.0 - */ -class Product extends ElementArguments -{ - /** - * @inheritdoc - */ - public static function getArguments(): array - { - return array_merge(parent::getArguments(), self::getContentArguments(), [ - 'defaultSku' => [ - 'name' => 'defaultSku', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the default SKU on the product.', - ], - 'defaultPrice' => [ - 'name' => 'defaultPrice', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the default price on the product.', - ], - 'defaultHeight' => [ - 'name' => 'defaultHeight', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the default height on the product.', - ], - 'defaultLength' => [ - 'name' => 'defaultLength', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the default length on the product.', - ], - 'defaultWidth' => [ - 'name' => 'defaultWidth', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the default width on the product.', - ], - 'defaultWeight' => [ - 'name' => 'defaultWeight', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the default weight on the product.', - ], - 'editable' => [ - 'name' => 'editable', - 'type' => Type::boolean(), - 'description' => 'Whether to only return products that the user has permission to edit.', - ], - 'type' => [ - 'name' => 'type', - 'type' => Type::listOf(Type::string()), - 'description' => 'Narrows the query results based on the product type the products belong to per the product type’s handles.', - ], - 'typeId' => [ - 'name' => 'typeId', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the product types the products belong to, per the product type IDs.', - ], - 'hasVariant' => [ - 'name' => 'hasVariant', - 'type' => Variant::getType(), - 'description' => 'Narrows the query results to only products that have certain variants.', - ], - ]); - } - - /** - * @inheritdoc - * @since 3.1.2 - */ - public static function getContentArguments(): array - { - $productTypeFieldArguments = Craft::$app->getGql()->getContentArguments(Plugin::getInstance()->getProductTypes()->getAllProductTypes(), ProductElement::class); - - return array_merge(parent::getContentArguments(), $productTypeFieldArguments); - } -} diff --git a/src/gql/arguments/elements/Variant.php b/src/gql/arguments/elements/Variant.php deleted file mode 100644 index 0ce460a3fb..0000000000 --- a/src/gql/arguments/elements/Variant.php +++ /dev/null @@ -1,167 +0,0 @@ - - * @since 3.1 - */ -class Variant extends ElementArguments -{ - /** - * @inheritdoc - */ - public static function getArguments(): array - { - return array_merge(parent::getArguments(), self::getContentArguments(), [ - 'promotable' => [ - 'name' => 'promotable', - 'type' => Type::boolean(), - 'description' => 'Whether to only return products that are promotable.', - ], - 'availableForPurchase' => [ - 'name' => 'availableForPurchase', - 'type' => Type::boolean(), - 'description' => 'Whether to only return products that are available to purchase.', - ], - 'freeShipping' => [ - 'name' => 'freeShipping', - 'type' => Type::boolean(), - 'description' => 'Whether to only return products that have free shipping.', - ], - 'hasProduct' => [ - 'name' => 'hasProduct', - 'type' => Product::getType(), - 'description' => 'Narrows the query results to only variants for certain products.', - ], - 'hasSales' => [ - 'name' => 'hasSales', - 'type' => Type::boolean(), - 'description' => 'Narrows the query results based on whether the variant has sales applied.', - ], - 'hasStock' => [ - 'name' => 'hasStock', - 'type' => Type::boolean(), - 'description' => 'Narrows the query results based on whether the variant has stock available.', - ], - 'isDefault' => [ - 'name' => 'isDefault', - 'type' => Type::boolean(), - 'description' => 'Narrows the query results based on the variants default status.', - ], - 'maxQty' => [ - 'name' => 'maxQty', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the variant’s maximum allowed quantity to be purchased.', - ], - 'minQty' => [ - 'name' => 'minQty', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the variant’s minimum allowed quantity to be purchased.', - ], - 'price' => [ - 'name' => 'price', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the variant’s price.', - ], - 'promotionalPrice' => [ - 'name' => 'promotionalPrice', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the variant’s promotional price.', - ], - 'onPromotion' => [ - 'name' => 'onPromotion', - 'type' => Type::boolean(), - 'description' => 'Narrows the query results based on whether the variant has a promotional price.', - ], - 'forCustomer' => [ - 'name' => 'forCustomer', - 'type' => IntFalse::getType(), - 'description' => 'Narrows the pricing query results to only prices related for the specified customer.', - ], - 'productId' => [ - 'name' => 'productId', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the variant’s product ID.', - ], - 'sku' => [ - 'name' => 'sku', - 'type' => Type::listOf(Type::string()), - 'description' => 'Narrows the query results based on the variant SKU.', - ], - 'stock' => [ - 'name' => 'stock', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on variant stock level.', - ], - 'typeId' => [ - 'name' => 'typeId', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the variant’s product’s type ID.', - ], - 'width' => [ - 'name' => 'width', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the variant’s width dimension.', - ], - 'height' => [ - 'name' => 'height', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the variant’s height dimension.', - ], - 'length' => [ - 'name' => 'length', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the variant’s length dimension.', - ], - 'weight' => [ - 'name' => 'weight', - 'type' => Type::listOf(QueryArgument::getType()), - 'description' => 'Narrows the query results based on the variant’s weight dimension.', - ], - ]); - } - - /** - * @inheritdoc - * @since 3.1.2 - */ - public static function getContentArguments(): array - { - return array_merge(parent::getContentArguments(), Plugin::getInstance()->getVariants()->getVariantGqlContentArguments()); - } - - /** - * @inheritdoc - * @since 5.5.0 - */ - public static function getStatusArguments(): array - { - $statusArguments = parent::getStatusArguments(); - - if (Gql::canQueryInactiveElements()) { - $statusArguments['productStatus'] = [ - 'name' => 'productStatus', - 'type' => Type::listOf(Type::string()), - 'description' => 'Narrows the query results based on the variants’ product’s statuses.', - ]; - } - - return $statusArguments; - } -} diff --git a/src/gql/handlers/HasProduct.php b/src/gql/handlers/HasProduct.php deleted file mode 100644 index a1b5e543ff..0000000000 --- a/src/gql/handlers/HasProduct.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @since 5.6.5 - */ -class HasProduct extends ArgumentHandler -{ - protected string $argumentName = 'hasProduct'; - - /** - * @inheritdoc - */ - protected function handleArgument(mixed $argumentValue): mixed - { - if (is_array($argumentValue)) { - return $this->argumentManager->prepareArguments($argumentValue); - } - - return $argumentValue; - } -} diff --git a/src/gql/handlers/HasVariant.php b/src/gql/handlers/HasVariant.php deleted file mode 100644 index e36b4180d1..0000000000 --- a/src/gql/handlers/HasVariant.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @since 5.6.5 - */ -class HasVariant extends ArgumentHandler -{ - protected string $argumentName = 'hasVariant'; - - /** - * @inheritdoc - */ - protected function handleArgument(mixed $argumentValue): mixed - { - if (is_array($argumentValue)) { - return $this->argumentManager->prepareArguments($argumentValue); - } - - return $argumentValue; - } -} diff --git a/src/gql/handlers/RelatedProducts.php b/src/gql/handlers/RelatedProducts.php deleted file mode 100644 index 49714dc334..0000000000 --- a/src/gql/handlers/RelatedProducts.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 5.6.0 - */ -class RelatedProducts extends RelationArgumentHandler -{ - protected string $argumentName = 'relatedToProducts'; - - /** - * @inheritdoc - */ - protected function handleArgument($argumentValue): mixed - { - $argumentValue = parent::handleArgument($argumentValue); - return $this->getIds(Product::class, $argumentValue); - } -} diff --git a/src/gql/handlers/RelatedVariants.php b/src/gql/handlers/RelatedVariants.php deleted file mode 100644 index d30a4c99de..0000000000 --- a/src/gql/handlers/RelatedVariants.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 5.6.0 - */ -class RelatedVariants extends RelationArgumentHandler -{ - protected string $argumentName = 'relatedToVariants'; - - /** - * @inheritdoc - */ - protected function handleArgument($argumentValue): mixed - { - $argumentValue = parent::handleArgument($argumentValue); - return $this->getIds(Variant::class, $argumentValue); - } -} diff --git a/src/gql/interfaces/elements/Product.php b/src/gql/interfaces/elements/Product.php deleted file mode 100644 index 8a3b5a49ac..0000000000 --- a/src/gql/interfaces/elements/Product.php +++ /dev/null @@ -1,179 +0,0 @@ - - * @since 3.0 - */ -class Product extends Element -{ - /** - * @inheritdoc - */ - public static function getTypeGenerator(): string - { - return ProductType::class; - } - - /** - * @inheritdoc - */ - public static function getType($fields = null): Type - { - if ($type = GqlEntityRegistry::getEntity(self::getName())) { - return $type; - } - - $type = GqlEntityRegistry::createEntity(self::getName(), new InterfaceType([ - 'name' => static::getName(), - 'fields' => self::class . '::getFieldDefinitions', - 'description' => 'This is the interface implemented by all products.', - 'resolveType' => fn(ProductElement $value) => $value->getGqlTypeName(), - ])); - - ProductType::generateTypes(); - - return $type; - } - - /** - * @inheritdoc - */ - public static function getName(): string - { - return 'ProductInterface'; - } - - /** - * @inheritdoc - */ - public static function getFieldDefinitions(): array - { - $productArguments = ProductArguments::getArguments(); - $structureProductTypeFieldArguments = [...$productArguments]; - - foreach (Gql::getSchemaContainedProductTypes() as $productType) { - $productTypeArguments = Craft::$app->getGql()->getFieldLayoutArguments($productType->getProductFieldLayout()); - if ($productType->isStructure) { - $structureProductTypeFieldArguments += $productTypeArguments; - } - } - - return Craft::$app->getGql()->prepareFieldDefinitions(array_merge(parent::getFieldDefinitions(), [ - 'defaultSku' => [ - 'name' => 'defaultSku', - 'type' => Type::string(), - 'description' => 'The SKU of the default variant for the product.', - ], - 'defaultPrice' => [ - 'name' => 'defaultPrice', - 'type' => Type::float(), - 'description' => 'The price of the default variant for the product.', - ], - 'defaultPriceAsCurrency' => [ - 'name' => 'defaultPriceAsCurrency', - 'type' => Type::string(), - 'description' => 'The formatted price of the default variant for the product.', - ], - 'defaultHeight' => [ - 'name' => 'defaultHeight', - 'type' => Type::float(), - 'description' => 'The height of the default variant for the product.', - ], - 'defaultLength' => [ - 'name' => 'defaultLength', - 'type' => Type::float(), - 'description' => 'The length of the default variant for the product.', - ], - 'defaultWidth' => [ - 'name' => 'defaultWidth', - 'type' => Type::float(), - 'description' => 'The width of the default variant for the product.', - ], - 'defaultWeight' => [ - 'name' => 'defaultWeight', - 'type' => Type::float(), - 'description' => 'The weight of the default variant for the product.', - ], - 'defaultVariant' => [ - 'name' => 'defaultVariant', - 'type' => Variant::getType(), - 'description' => 'The default variant for the product.', - ], - 'productTypeId' => [ - 'name' => 'productTypeId', - 'type' => Type::int(), - 'description' => 'The ID of the product type that contains the product.', - ], - 'productTypeHandle' => [ - 'name' => 'productTypeHandle', - 'type' => Type::string(), - 'description' => 'The handle of the product type that contains the product.', - ], - 'url' => [ - 'name' => 'url', - 'type' => Type::string(), - 'description' => 'The product’s full URL', - ], - 'variants' => [ - 'name' => 'variants', - 'type' => Type::listOf(Variant::getType()), - 'description' => 'The product’s variants.', - ], - 'localized' => [ - 'name' => 'localized', - 'args' => $productArguments, - 'type' => Type::nonNull(Type::listOf(Type::nonNull(static::getType()))), - 'description' => 'The same element in other locales.', - 'complexity' => Gql::eagerLoadComplexity(), - ], - 'children' => [ - 'name' => 'children', - 'args' => $structureProductTypeFieldArguments, - 'type' => Type::nonNull(Type::listOf(Type::nonNull(static::getType()))), - 'description' => 'The products’s children, if the product type is a structure. Accepts the same arguments as the `products` query.', - 'complexity' => Gql::relatedArgumentComplexity(GqlService::GRAPHQL_COMPLEXITY_EAGER_LOAD), - ], - 'descendants' => [ - 'name' => 'descendants', - 'args' => $structureProductTypeFieldArguments, - 'type' => Type::nonNull(Type::listOf(Type::nonNull(static::getType()))), - 'description' => 'The products’s descendants, if the product type is a structure. Accepts the same arguments as the `products` query.', - 'complexity' => Gql::relatedArgumentComplexity(GqlService::GRAPHQL_COMPLEXITY_EAGER_LOAD), - ], - 'parent' => [ - 'name' => 'parent', - 'args' => $structureProductTypeFieldArguments, - 'type' => static::getType(), - 'description' => 'The products’s parent, if the product type is a structure.', - 'complexity' => Gql::relatedArgumentComplexity(GqlService::GRAPHQL_COMPLEXITY_EAGER_LOAD), - ], - 'ancestors' => [ - 'name' => 'ancestors', - 'args' => $structureProductTypeFieldArguments, - 'type' => Type::nonNull(Type::listOf(Type::nonNull(static::getType()))), - 'description' => 'The products’s ancestors, if the product type is a structure. Accepts the same arguments as the `products` query.', - 'complexity' => Gql::relatedArgumentComplexity(GqlService::GRAPHQL_COMPLEXITY_EAGER_LOAD), - ], - ]), self::getName()); - } -} diff --git a/src/gql/interfaces/elements/Variant.php b/src/gql/interfaces/elements/Variant.php deleted file mode 100644 index ac95975b1b..0000000000 --- a/src/gql/interfaces/elements/Variant.php +++ /dev/null @@ -1,212 +0,0 @@ - - * @since 3.1 - */ -class Variant extends Element -{ - /** - * @inheritdoc - */ - public static function getTypeGenerator(): string - { - return VariantType::class; - } - - /** - * @inheritdoc - */ - public static function getType($fields = null): Type - { - if ($type = GqlEntityRegistry::getEntity(self::getName())) { - return $type; - } - - $type = GqlEntityRegistry::createEntity(self::getName(), new InterfaceType([ - 'name' => static::getName(), - 'fields' => self::class . '::getFieldDefinitions', - 'description' => 'This is the interface implemented by all variants.', - 'resolveType' => fn(VariantElement $value) => $value->getGqlTypeName(), - ])); - - VariantType::generateTypes(); - - return $type; - } - - /** - * @inheritdoc - */ - public static function getName(): string - { - return 'VariantInterface'; - } - - /** - * @inheritdoc - */ - public static function getFieldDefinitions(): array - { - return Craft::$app->getGql()->prepareFieldDefinitions(array_merge(parent::getFieldDefinitions(), [ - 'isDefault' => [ - 'name' => 'isDefault', - 'type' => Type::boolean(), - 'description' => 'If the variant is the default for the product.', - ], - 'isAvailable' => [ - 'name' => 'isAvailable', - 'type' => Type::boolean(), - 'description' => 'If the variant is available to be purchased.', - ], - 'price' => [ - 'name' => 'price', - 'type' => Type::float(), - 'description' => 'The price of the variant.', - ], - 'priceAsCurrency' => [ - 'name' => 'priceAsCurrency', - 'type' => Type::string(), - 'description' => 'The formatted price of the variant.', - ], - 'promotionalPrice' => [ - 'name' => 'promotionalPrice', - 'type' => Type::float(), - 'description' => 'The promotional price of the variant.', - ], - 'promotionalPriceAsCurrency' => [ - 'name' => 'promotionalPriceAsCurrency', - 'type' => Type::string(), - 'description' => 'The formatted promotional price of the variant.', - ], - 'salePrice' => [ - 'name' => 'salePrice', - 'type' => Type::float(), - 'description' => 'The sale price of the variant. CAUTION: This will not take into account sales that utilize user group conditions.', - ], - 'salePriceAsCurrency' => [ - 'name' => 'salePriceAsCurrency', - 'type' => Type::string(), - 'description' => 'The formatted sale price of the variant. CAUTION: This will not take into account sales that utilize user group conditions.', - ], - 'sales' => [ - 'name' => 'sales', - 'type' => Type::listOf(SaleType::getType()), - 'description' => 'The sales that apply to the variant. CAUTION: This will not take into account sales that utilize user group conditions.', - ], - 'sortOrder' => [ - 'name' => 'sortOrder', - 'type' => Type::int(), - 'description' => 'The sort order of the variant.', - ], - 'width' => [ - 'name' => 'width', - 'type' => Type::float(), - 'description' => 'The width of the variant.', - ], - 'height' => [ - 'name' => 'height', - 'type' => Type::float(), - 'description' => 'The height of the variant.', - ], - 'length' => [ - 'name' => 'length', - 'type' => Type::float(), - 'description' => 'The length of the variant.', - ], - 'weight' => [ - 'name' => 'weight', - 'type' => Type::float(), - 'description' => 'The weight of the variant.', - ], - 'stock' => [ - 'name' => 'stock', - 'type' => Type::int(), - 'description' => 'The stock level of the variant.', - ], - 'hasUnlimitedStock' => [ - 'name' => 'hasUnlimitedStock', - 'type' => Type::boolean(), - 'description' => 'If the variant has unlimited stock.', - ], - 'minQty' => [ - 'name' => 'minQty', - 'type' => Type::int(), - 'description' => 'The minimum allowed quantity to be purchased.', - ], - 'maxQty' => [ - 'name' => 'maxQty', - 'type' => Type::int(), - 'description' => 'The maximum allowed quantity to be purchased.', - ], - 'promotable' => [ - 'name' => 'promotable', - 'type' => Type::boolean(), - 'description' => 'If the product is promotable.', - ], - 'availableForPurchase' => [ - 'name' => 'availableForPurchase', - 'type' => Type::boolean(), - 'description' => 'If the product is available for purchase.', - ], - 'freeShipping' => [ - 'name' => 'freeShipping', - 'type' => Type::boolean(), - 'description' => 'If the product has free shipping.', - ], - 'shippingCategoryId' => [ - 'name' => 'shippingCategoryId', - 'type' => Type::int(), - 'description' => 'The ID of the variants’s shipping category.', - ], - 'productId' => [ - 'name' => 'productId', - 'type' => Type::int(), - 'description' => 'The ID of the variant’s parent product.', - ], - 'product' => [ - 'name' => 'product', - 'type' => Product::getType(), - 'description' => 'The variant’s parent product.', - ], - 'productTitle' => [ - 'name' => 'productTitle', - 'type' => Type::string(), - 'description' => 'The title of the variant’s parent product.', - ], - 'productTypeId' => [ - 'name' => 'productTypeId', - 'type' => Type::int(), - 'description' => 'The product type ID of the variant’s parent product.', - ], - 'sku' => [ - 'name' => 'sku', - 'type' => Type::string(), - 'description' => 'The SKU of the variant.', - ], - 'storeId' => [ - 'name' => 'storeId', - 'type' => Type::int(), - 'description' => 'The ID of the variant’s store.', - ], - ]), self::getName()); - } -} diff --git a/src/gql/queries/Product.php b/src/gql/queries/Product.php deleted file mode 100644 index de0d09aeec..0000000000 --- a/src/gql/queries/Product.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @since 3.0 - */ -class Product extends Query -{ - /** - * @inheritdoc - */ - public static function getQueries(bool $checkToken = true): array - { - if ($checkToken && !GqlHelper::canQueryProducts()) { - return []; - } - - return [ - 'products' => [ - 'type' => Type::listOf(ProductInterface::getType()), - 'args' => ProductArguments::getArguments(), - 'resolve' => ProductResolver::class . '::resolve', - 'description' => 'This query is used to query for products.', - ], - 'productCount' => [ - 'type' => Type::nonNull(Type::int()), - 'args' => ProductArguments::getArguments(), - 'resolve' => ProductResolver::class . '::resolveCount', - 'description' => 'This query is used to return the number of products.', - ], - 'product' => [ - 'type' => ProductInterface::getType(), - 'args' => ProductArguments::getArguments(), - 'resolve' => ProductResolver::class . '::resolveOne', - 'description' => 'This query is used to query for a product.', - ], - ]; - } -} diff --git a/src/gql/queries/Variant.php b/src/gql/queries/Variant.php deleted file mode 100644 index 4b7caf4fa6..0000000000 --- a/src/gql/queries/Variant.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @since 3.1 - */ -class Variant extends Query -{ - /** - * @inheritdoc - */ - public static function getQueries(bool $checkToken = true): array - { - if ($checkToken && !GqlHelper::canQueryProducts()) { - return []; - } - - return [ - 'variants' => [ - 'type' => Type::listOf(VariantInterface::getType()), - 'args' => VariantArguments::getArguments(), - 'resolve' => VariantResolver::class . '::resolve', - 'description' => 'This query is used to query for variants.', - ], - 'variantCount' => [ - 'type' => Type::nonNull(Type::int()), - 'args' => VariantArguments::getArguments(), - 'resolve' => VariantResolver::class . '::resolveCount', - 'description' => 'This query is used to return the number of variants.', - ], - 'variant' => [ - 'type' => VariantInterface::getType(), - 'args' => VariantArguments::getArguments(), - 'resolve' => VariantResolver::class . '::resolveOne', - 'description' => 'This query is used to query for a variant.', - ], - ]; - } -} diff --git a/src/gql/resolvers/elements/Product.php b/src/gql/resolvers/elements/Product.php deleted file mode 100644 index 050779e754..0000000000 --- a/src/gql/resolvers/elements/Product.php +++ /dev/null @@ -1,64 +0,0 @@ - - * @since 3.0 - */ -class Product extends ElementResolver -{ - /** - * @inheritdoc - */ - public static function prepareQuery(mixed $source, array $arguments, $fieldName = null): mixed - { - // If this is the beginning of a resolver chain, start fresh - if ($source === null) { - $query = ProductElement::find(); - // If not, get the prepared element query - } else { - $query = $source->$fieldName; - } - - // If it's preloaded, it's preloaded. - if (!$query instanceof ElementQuery) { - return $query; - } - - foreach ($arguments as $key => $value) { - if (method_exists($query, $key)) { - $query->$key($value); - } elseif (property_exists($query, $key)) { - $query->$key = $value; - } else { - // Catch custom field queries - $query->$key($value); - } - } - - $pairs = GqlHelper::extractAllowedEntitiesFromSchema(); - - if (!GqlHelper::canQueryProducts()) { - return []; - } - - $query->andWhere(['in', 'typeId', array_values(Db::idsByUids(Table::PRODUCTTYPES, $pairs['productTypes']))]); - - return $query; - } -} diff --git a/src/gql/resolvers/elements/Variant.php b/src/gql/resolvers/elements/Variant.php deleted file mode 100644 index 8b082487bd..0000000000 --- a/src/gql/resolvers/elements/Variant.php +++ /dev/null @@ -1,73 +0,0 @@ - - * @since 3.1 - */ -class Variant extends ElementResolver -{ - /** - * @inheritdoc - */ - public static function prepareQuery(mixed $source, array $arguments, $fieldName = null): mixed - { - // If this is the beginning of a resolver chain, start fresh - if ($source === null) { - $query = VariantElement::find(); - // If not, get the prepared element query - } else { - $query = $source->$fieldName; - } - - // If it's preloaded, it's preloaded. - if (!$query instanceof ElementQuery) { - return $query; - } - - foreach ($arguments as $key => $value) { - if (method_exists($query, $key)) { - $query->$key($value); - } elseif (property_exists($query, $key)) { - $query->$key = $value; - } else { - // Catch custom field queries - $query->$key($value); - } - } - - $pairs = GqlHelper::extractAllowedEntitiesFromSchema(); - - if (!GqlHelper::canQueryProducts()) { - return []; - } - - // For variant queries make sure we are only return those that have live products - // unless the schema allows querying inactive elements - if (!GqlHelper::canQueryInactiveElements() && $query instanceof VariantQuery) { - $query->productStatus(ProductElement::STATUS_LIVE); - } - - $query->innerJoin(Table::PRODUCTS . ' p', '[[p.id]] = [[commerce_variants.primaryOwnerId]]'); - $query->andWhere(['in', '[[p.typeId]]', array_values(Db::idsByUids(Table::PRODUCTTYPES, $pairs['productTypes']))]); - - return $query; - } -} diff --git a/src/gql/types/SaleType.php b/src/gql/types/SaleType.php deleted file mode 100644 index 9fb8610448..0000000000 --- a/src/gql/types/SaleType.php +++ /dev/null @@ -1,90 +0,0 @@ - - * @since 3.1.10 - */ -class SaleType extends ObjectType -{ - /** - * @return string - */ - public static function getName(): string - { - return 'Sale'; - } - - public static function getType(): Type - { - if ($type = GqlEntityRegistry::getEntity(self::getName())) { - return $type; - } - - return GqlEntityRegistry::createEntity(self::getName(), new self([ - 'name' => static::getName(), - 'fields' => self::class . '::getFieldDefinitions', - 'description' => '', - ])); - } - - public static function getFieldDefinitions(): array - { - return Craft::$app->getGql()->prepareFieldDefinitions([ - 'name' => [ - 'name' => 'name', - 'type' => Type::string(), - 'description' => 'The name of the sale as described in the control panel.', - ], - 'description' => [ - 'name' => 'description', - 'type' => Type::string(), - 'description' => 'Description of the sale.', - ], - 'apply' => [ - 'name' => 'apply', - 'type' => Type::string(), - 'description' => 'How the sale should be applied.', - ], - 'applyAmount' => [ - 'name' => 'applyAmount', - 'type' => Type::float(), - 'description' => 'The amount applied used by the apply option.', - ], - 'applyAmountAsPercent' => [ - 'name' => 'applyAmountAsPercent', - 'type' => Type::string(), - 'description' => 'The amount applied used by the apply option.', - ], - 'applyAmountAsFlat' => [ - 'name' => 'applyAmountAsFlat', - 'type' => Type::float(), - 'description' => 'The amount applied used by the apply option.', - ], - 'dateFrom' => [ - 'name' => 'dateFrom', - 'type' => DateTime::getType(), - 'description' => 'Start date of the sale.', - ], - 'dateTo' => [ - 'name' => 'dateTo', - 'type' => DateTime::getType(), - 'description' => 'Start date of the sale.', - ], - ], self::getName()); - } -} diff --git a/src/gql/types/elements/Product.php b/src/gql/types/elements/Product.php deleted file mode 100644 index 2db424b5c2..0000000000 --- a/src/gql/types/elements/Product.php +++ /dev/null @@ -1,48 +0,0 @@ - - * @since 3.0 - */ -class Product extends ElementType -{ - /** - * @inheritdoc - */ - public function __construct(array $config) - { - $config['interfaces'] = [ - ProductInterface::getType(), - ]; - - parent::__construct($config); - } - - /** - * @inheritdoc - */ - protected function resolve(mixed $source, array $arguments, mixed $context, ResolveInfo $resolveInfo): mixed - { - /** @var ProductElement $source */ - $fieldName = $resolveInfo->fieldName; - return match ($fieldName) { - 'productTypeHandle' => $source->getType()->handle, - 'productTypeId' => $source->getType()->id, - default => parent::resolve($source, $arguments, $context, $resolveInfo), - }; - } -} diff --git a/src/gql/types/elements/Variant.php b/src/gql/types/elements/Variant.php deleted file mode 100644 index 9e125b3b98..0000000000 --- a/src/gql/types/elements/Variant.php +++ /dev/null @@ -1,49 +0,0 @@ - - * @since 3.1 - */ -class Variant extends ElementType -{ - /** - * @inheritdoc - */ - public function __construct(array $config) - { - $config['interfaces'] = [ - VariantInterface::getType(), - ]; - - parent::__construct($config); - } - - /** - * @inheritdoc - */ - protected function resolve(mixed $source, array $arguments, mixed $context, ResolveInfo $resolveInfo): mixed - { - /** @var VariantElement $source */ - $fieldName = $resolveInfo->fieldName; - $product = $source->getOwner(); - return match ($fieldName) { - 'productTitle' => $product->title ?? '', - 'productTypeId' => $product->typeId ?? null, - default => parent::resolve($source, $arguments, $context, $resolveInfo), - }; - } -} diff --git a/src/gql/types/generators/ProductType.php b/src/gql/types/generators/ProductType.php deleted file mode 100644 index dea91814c1..0000000000 --- a/src/gql/types/generators/ProductType.php +++ /dev/null @@ -1,65 +0,0 @@ - - * @since 3.0 - */ -class ProductType implements GeneratorInterface -{ - /** - * @inheritdoc - */ - public static function generateTypes(mixed $context = null): array - { - $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); - $gqlTypes = []; - - foreach ($productTypes as $productType) { - /** @var ProductTypeModel $productType */ - $typeName = ProductElement::gqlTypeNameByContext($productType); - $requiredContexts = ProductElement::gqlScopesByContext($productType); - - if (!CommerceGqlHelper::isSchemaAwareOf($requiredContexts)) { - continue; - } - - $contentFields = $productType->getCustomFields(); - $contentFieldGqlTypes = []; - - /** @var Field $contentField */ - foreach ($contentFields as $contentField) { - $contentFieldGqlTypes[$contentField->handle] = $contentField->getContentGqlType(); - } - - $productTypeFields = Craft::$app->getGql()->prepareFieldDefinitions(array_merge(ProductInterface::getFieldDefinitions(), $contentFieldGqlTypes), $typeName); - - // Generate a type for each product type - $gqlTypes[$typeName] = GqlEntityRegistry::getEntity($typeName) ?: GqlEntityRegistry::createEntity($typeName, new ProductTypeElement([ - 'name' => $typeName, - 'fields' => fn() => $productTypeFields, - ])); - } - - return $gqlTypes; - } -} diff --git a/src/gql/types/generators/VariantType.php b/src/gql/types/generators/VariantType.php deleted file mode 100644 index 971c051c00..0000000000 --- a/src/gql/types/generators/VariantType.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @since 3.1 - */ -class VariantType implements GeneratorInterface -{ - /** - * @inheritdoc - */ - public static function generateTypes(mixed $context = null): array - { - $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); - $gqlTypes = []; - - foreach ($productTypes as $productType) { - /** @var ProductTypeModel $productType */ - $typeName = VariantElement::gqlTypeNameByContext($productType); - $requiredContexts = VariantElement::gqlScopesByContext($productType); - - if (!Gql::isSchemaAwareOf($requiredContexts)) { - continue; - } - - $layout = $productType->getVariantFieldLayout(); - $contentFields = $layout->getCustomFields(); - $contentFieldGqlTypes = []; - - /** @var Field $contentField */ - foreach ($contentFields as $contentField) { - $contentFieldGqlTypes[$contentField->handle] = $contentField->getContentGqlType(); - } - - $fields = Craft::$app->getGql()->prepareFieldDefinitions(array_merge(VariantInterface::getFieldDefinitions(), $contentFieldGqlTypes), $typeName); - - // Generate a type for each product type - $gqlTypes[$typeName] = GqlEntityRegistry::getEntity($typeName) ?: GqlEntityRegistry::createEntity($typeName, new Variant([ - 'name' => $typeName, - 'fields' => fn() => $fields, - ])); - } - - return $gqlTypes; - } -} diff --git a/src/gql/types/input/IntFalse.php b/src/gql/types/input/IntFalse.php deleted file mode 100644 index 38ce007df3..0000000000 --- a/src/gql/types/input/IntFalse.php +++ /dev/null @@ -1,109 +0,0 @@ - - * @since 5.0.7 - */ -class IntFalse extends ScalarType -{ - public $name = 'IntFalse'; - - /** @var string */ - public $description = - 'The `IntFalse` scalar type represents non-fractional signed whole numeric -values. Int can represent values between -(2^31) and 2^31 - 1 Or `false`'; - - /** - * @var IntType|null - */ - private ?IntType $_intType = null; - - public function __construct(array $config = []) - { - $this->_intType = new IntType(); - - parent::__construct($config); - } - - /** - * Returns a singleton instance to ensure one type per schema. - * - * @return IntFalse - */ - public static function getType(): IntFalse - { - return GqlEntityRegistry::getOrCreate(static::getName(), fn() => new self()); - } - - /** - * @return string - */ - public static function getName(): string - { - return 'IntFalse'; - } - - /** - * @param $value - * @return false|int|mixed|null - * @throws Error - */ - public function serialize($value) - { - if (is_bool($value) && $value === false) { - return false; - } - - // If it isn't `false` use the `IntType` to serialize the value - return $this->_intType->serialize($value); - } - - /** - * @param $value - * @return int|false - * @throws Error - */ - public function parseValue($value): int|false - { - if (is_bool($value) && $value === false) { - return false; - } - - return $this->_intType->parseValue($value); - } - - /** - * @param $valueNode - * @param array|null $variables - * @return false|int|mixed - * @throws Error - */ - public function parseLiteral($valueNode, ?array $variables = null) - { - if ($valueNode instanceof BooleanValueNode) { - $val = $valueNode->value; - if ($val === false) { - return false; - } - - throw new Error(); - } - - return $this->_intType->parseLiteral($valueNode, $variables); - } -} diff --git a/src/gql/types/input/Product.php b/src/gql/types/input/Product.php deleted file mode 100644 index e7a416b095..0000000000 --- a/src/gql/types/input/Product.php +++ /dev/null @@ -1,34 +0,0 @@ - - * @since 3.2.4 - */ -class Product extends InputObjectType -{ - /** - * @return mixed - */ - public static function getType(): mixed - { - $typeName = 'ProductInput'; - - return GqlEntityRegistry::getEntity($typeName) ?: GqlEntityRegistry::createEntity($typeName, new InputObjectType([ - 'name' => $typeName, - 'fields' => fn() => ProductArguments::getArguments(), - ])); - } -} diff --git a/src/gql/types/input/Variant.php b/src/gql/types/input/Variant.php deleted file mode 100644 index 6de000cc79..0000000000 --- a/src/gql/types/input/Variant.php +++ /dev/null @@ -1,34 +0,0 @@ - - * @since 3.1.11 - */ -class Variant extends InputObjectType -{ - /** - * @return mixed - */ - public static function getType(): mixed - { - $typeName = 'VariantInput'; - - return GqlEntityRegistry::getEntity($typeName) ?: GqlEntityRegistry::createEntity($typeName, new InputObjectType([ - 'name' => $typeName, - 'fields' => fn() => VariantArguments::getArguments(), - ])); - } -} diff --git a/src/gql/types/input/criteria/ProductRelation.php b/src/gql/types/input/criteria/ProductRelation.php deleted file mode 100644 index 0bb5243aba..0000000000 --- a/src/gql/types/input/criteria/ProductRelation.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @since 5.6.0 - */ -class ProductRelation extends InputObjectType -{ - /** - * @return mixed - */ - public static function getType(): mixed - { - $typeName = 'ProductRelationCriteriaInput'; - - return GqlEntityRegistry::getOrCreate($typeName, fn() => new InputObjectType([ - 'name' => $typeName, - 'fields' => fn() => [ - ...ProductArguments::getArguments(), - ...ProductArguments::getContentArguments(), - ...RelationCriteria::getArguments(), - ], - ])); - } -} diff --git a/src/gql/types/input/criteria/VariantRelation.php b/src/gql/types/input/criteria/VariantRelation.php deleted file mode 100644 index 24e6344d30..0000000000 --- a/src/gql/types/input/criteria/VariantRelation.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @since 5.6.0 - */ -class VariantRelation extends InputObjectType -{ - /** - * @return mixed - */ - public static function getType(): mixed - { - $typeName = 'VariantRelationCriteriaInput'; - - return GqlEntityRegistry::getOrCreate($typeName, fn() => new InputObjectType([ - 'name' => $typeName, - 'fields' => fn() => [ - ...VariantArguments::getArguments(), - ...VariantArguments::getContentArguments(), - ...RelationCriteria::getArguments(), - ], - ])); - } -} diff --git a/src/helpers/Cp.php b/src/helpers/Cp.php deleted file mode 100644 index 9098e72dc3..0000000000 --- a/src/helpers/Cp.php +++ /dev/null @@ -1,71 +0,0 @@ - - * @since 5.0 - */ -class Cp -{ - /** - * Renders an inventory locations select field's HTML. - * - * @param array $config - * @return string - * @since 5.0.0 - */ - public static function inventoryLocationFieldHtml(array $config): string - { - $config['id'] ??= 'inventorylocationselect' . mt_rand(); - return CraftCp::fieldHtml('template:commerce/_includes/forms/inventoryLocationSelect.twig', $config); - } - - /** - * Renders a tax zone select field's HTML. - * - * @param array $config - * @return string - * @since 5.0.0 - */ - public static function taxZoneFieldHtml(array $config): string - { - $config['id'] ??= 'taxzoneselect' . mt_rand(); - return CraftCp::fieldHtml('template:commerce/_includes/forms/taxZoneSelect.twig', $config); - } - - /** - * Renders a tax category select field's HTML. - * - * @param array $config - * @return string - * @since 5.5.0 - */ - public static function taxCategoryFieldHtml(array $config): string - { - $config['id'] ??= 'taxcategoryselect' . mt_rand(); - return CraftCp::fieldHtml('template:commerce/_includes/forms/taxCategorySelect.twig', $config); - } - - /** - * Renders a shipping category select field's HTML. - * - * @param array $config - * @return string - * @since 5.5.0 - */ - public static function shippingCategoryFieldHtml(array $config): string - { - $config['id'] ??= 'shippingcategoryselect' . mt_rand(); - return CraftCp::fieldHtml('template:commerce/_includes/forms/shippingCategorySelect.twig', $config); - } -} diff --git a/src/helpers/Currency.php b/src/helpers/Currency.php deleted file mode 100644 index 9e6c044647..0000000000 --- a/src/helpers/Currency.php +++ /dev/null @@ -1,146 +0,0 @@ - - * @since 2.0 - */ -class Currency -{ - /** - * Rounds the amount as per the currency minor unit information. Not passing - * a currency model results in rounding in default currency. - * - * @param float $amount The amount as a decimal/float - * @param PaymentCurrency|string|MoneyCurrency|null $currency - * @return float - */ - public static function round(float $amount, PaymentCurrency|string|MoneyCurrency|null $currency = null): float - { - if (!$currency) { - $currency = Plugin::getInstance()->getStores()->getCurrentStore()->getCurrency(); - } - - if ($currency instanceof PaymentCurrency) { - $currency = new MoneyCurrency($currency->getAlphabeticCode()); - } - - if (is_string($currency)) { - $currency = new MoneyCurrency($currency); - } - - $moneyFormatter = new DecimalMoneyFormatter(new ISOCurrencies()); - return (float)$moneyFormatter->format(Plugin::getInstance()->getCurrencies()->getTeller($currency)->convertToMoney($amount)); - } - - /** - * @return int - * @throws CurrencyException - * @throws InvalidConfigException - */ - public static function defaultDecimals(): int - { - return Plugin::getInstance()->getPaymentCurrencies()->getPrimaryPaymentCurrency()->getSubUnit(); - } - - /** - * Formats and optionally converts a currency amount into the supplied valid payment currency as per the rate setup in payment currencies. - * - * @param $amount - * @param bool $convert - * @param bool $format - * @param bool $stripZeros - * @return string - * @throws CurrencyException - * @throws InvalidConfigException - */ - public static function formatAsCurrency($amount, mixed $currency = null, bool $convert = false, bool $format = true, bool $stripZeros = false): string - { - // return input if no currency passed, and both convert and format are false. - if (!$convert && !$format) { - return $amount; - } - - $currencyIso = Plugin::getInstance()->getStores()->getCurrentStore()->getCurrency(); - - if (is_string($currency)) { - $currencyIso = $currency; - } - - if ($currency instanceof PaymentCurrency) { - $currencyIso = $currency->iso; - } - - if ($convert) { - $currency = Plugin::getInstance()->getPaymentCurrencies()->getPaymentCurrencyByIso($currencyIso); - if (!$currency) { - throw new InvalidCallException('Trying to convert to a currency that is not configured'); - } - } - - if ($convert && $currencyIso !== Plugin::getInstance()->getStores()->getCurrentStore()->getCurrency()) { - $amount = Plugin::getInstance()->getPaymentCurrencies()->convert((float)$amount, $currencyIso); - } - - if ($format) { - $numberFormatter = new \NumberFormatter(Craft::$app->getFormattingLocale(), \NumberFormatter::CURRENCY); - - // Strip zeros if requested and only if the amount won't have any decimal places - if ($stripZeros && (int)$amount == $amount) { - $numberFormatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, 0); - $numberFormatter->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, 0); - } - - $moneyFormatter = new IntlMoneyFormatter($numberFormatter, new ISOCurrencies()); - $money = Plugin::getInstance()->getCurrencies()->getTeller($currencyIso)->convertToMoney($amount); - - return $moneyFormatter->format($money); - } - - return (string)$amount; - } - - /** - * @param array $config - * @return string - * @throws InvalidConfigException - * @throws TemplateLoaderException - * @since 5.0.0 - */ - public static function moneyInputHtml(mixed $value, array $config = []): string - { - $config += [ - 'showCurrency' => true, - 'size' => 6, - 'decimals' => 2, - 'value' => $value, - ]; - - if (isset($config['currency'])) { - $config['decimals'] = Plugin::getInstance()->getCurrencies()->getSubunitFor($config['currency']); - } - - return Cp::moneyInputHtml($config); - } -} diff --git a/src/helpers/DebugPanel.php b/src/helpers/DebugPanel.php deleted file mode 100644 index 57a1d42a12..0000000000 --- a/src/helpers/DebugPanel.php +++ /dev/null @@ -1,92 +0,0 @@ - - * @since 4.0 - */ -class DebugPanel -{ - /** - * @param object $model - * @param string|null $name Name of the tab to be displayed. - * @param bool $prepend Whether to prepend the content tab. - * @return void - */ - public static function prependOrAppendModelTab(object $model, ?string $name = null, bool $prepend = false): void - { - if (!$name) { - $classSegments = explode('\\', $model::class); - $name = array_pop($classSegments); - - if (property_exists($model, 'id')) { - $name .= $model->id ? sprintf(' (ID: %s)', $model->id) : ' (New)'; - } - } - - $user = Craft::$app->getUser()->getIdentity(); - - // Skip out if there is no user or `devMode` isn't enabled - if (!$user || !Craft::$app->getConfig()->getGeneral()->devMode) { - return; - } - - // Skip out if this is a CP request and the user doesn't have the preference set to `true` - if ((Craft::$app->getRequest()->getIsCpRequest() && !$user->getPreference('enableDebugToolbarForCp'))) { - return; - } - - // Skip out if this is a site request and the user doesn't have the preference set to `true` - if (!Craft::$app->getRequest()->getIsCpRequest() && !$user->getPreference('enableDebugToolbarForSite')) { - return; - } - - Event::on(CommercePanel::class, CommercePanel::EVENT_AFTER_DATA_PREPARE, function(CommerceDebugPanelDataEvent $event) use ($name, $model, $prepend) { - $content = Craft::$app->getView()->render('@craft/commerce/views/debug/commerce/model', compact('model')); - - ArrayHelper::prependOrAppend($event->nav, $name, $prepend); - ArrayHelper::prependOrAppend($event->content, $content, $prepend); - }); - } - - /** - * @param string $attr - * @param string|null $label - * @return string - */ - public static function renderModelAttributeRow(string $attr, mixed $value, ?string $label = null): string - { - $label = $label ?: $attr; - - if (is_string($value)) { - if (str_contains($attr, 'html') || str_contains($attr, 'Html')) { - $output = Html::encode($value); - } else { - $output = $value; - } - } else { - $output = VarDumper::dumpAsString($value); - } - - return Html::tag('tr', - Html::tag('th', $label) - . Html::tag('td', Html::tag('code', $output)) - ); - } -} diff --git a/src/helpers/Gql.php b/src/helpers/Gql.php deleted file mode 100644 index 4472461ae2..0000000000 --- a/src/helpers/Gql.php +++ /dev/null @@ -1,46 +0,0 @@ - - * @since 3.0 - */ -class Gql extends GqlHelper -{ - /** - * Return true if active schema can query products. - */ - public static function canQueryProducts(): bool - { - $allowedEntities = self::extractAllowedEntitiesFromSchema(); - return isset($allowedEntities['productTypes']); - } - - /** - * @param GqlSchema|null $schema - * @return array|ProductType[] - * @throws InvalidConfigException - * @since 5.5.0 - */ - public static function getSchemaContainedProductTypes(?GqlSchema $schema = null): array - { - return array_filter( - Plugin::getInstance()->getProductTypes()->getAllProductTypes(), - fn(ProductType $productType) => static::isSchemaAwareOf("productTypes.$productType->uid", $schema), - ); - } -} diff --git a/src/helpers/LineItem.php b/src/helpers/LineItem.php deleted file mode 100644 index 72629a1728..0000000000 --- a/src/helpers/LineItem.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 2.1 - */ -class LineItem -{ - /** - * @return string The generated options signature - */ - public static function generateOptionsSignature(array $options = [], ?int $lineItemId = null): string - { - if ($lineItemId) { - $options['lineItemId'] = $lineItemId; - } - ksort($options); - return md5(Json::encode($options)); - } -} diff --git a/src/helpers/Locale.php b/src/helpers/Locale.php deleted file mode 100644 index 9c3ea73220..0000000000 --- a/src/helpers/Locale.php +++ /dev/null @@ -1,79 +0,0 @@ - - * @since 3.2.13 - */ -class Locale -{ - /** - * Set language of the application - * - * @param string $toLanguage - * @param string|null $formattingLocale - * @throws InvalidConfigException - * @todo rename the `$toLanguage` parameter to `$locale` in Commerce 6.0 - */ - public static function switchAppLanguage(string $toLanguage, ?string $formattingLocale = null): void - { - Craft::$app->language = $toLanguage; - $locale = Craft::$app->getI18n()->getLocaleById($toLanguage); - Craft::$app->set('locale', $locale); - - if ($formattingLocale !== null) { - $locale = Craft::$app->getI18n()->getLocaleById($formattingLocale); - } - - Craft::$app->set('formattingLocale', $locale); - } - - /** - * Get the created sites languages and all languages. - * - * @throws Exception - */ - public static function getSiteAndOtherLanguages(): array - { - $pdfLanguageOptions['siteLanguages']['optgroup'] = Craft::t('commerce', 'Site Languages'); - - $siteLanguageOptions = []; - // Get current site's locale - foreach (Craft::$app->getSites()->getAllSites() as $site) { - $locale = Craft::$app->getI18n()->getLocaleById($site->language); - - $siteLanguageOptions[$locale->getLanguageID()] = $site->name . ' - ' . $locale->getDisplayName(); - } - - $pdfLanguageOptions = array_merge($pdfLanguageOptions, $siteLanguageOptions); - - $pdfLanguageOptions['otherLanguages']['optgroup'] = Craft::t('commerce', 'Other Languages'); - - /** @var \craft\i18n\Locale[] $allLocales */ - $allLocales = ArrayHelper::index(Craft::$app->getI18n()->getAppLocales(), 'id'); - ArrayHelper::multisort($allLocales, 'displayName'); - - $allLocaleOptions = []; - - foreach ($allLocales as $locale) { - $allLocaleOptions[$locale->id] = $locale->getDisplayName(); - } - - $otherLocaleOptions = array_diff_key($allLocaleOptions, $siteLanguageOptions); - - return array_merge($pdfLanguageOptions, $otherLocaleOptions); - } -} diff --git a/src/helpers/Localization.php b/src/helpers/Localization.php deleted file mode 100644 index 16d50d40d4..0000000000 --- a/src/helpers/Localization.php +++ /dev/null @@ -1,46 +0,0 @@ - - * @since 3.4.10 - */ -abstract class Localization extends \craft\helpers\Localization -{ - /** - * Normalizes a percentage value into a float. - * - * @param int|float|string|null $number - * @return float|null - */ - public static function normalizePercentage(mixed $number): ?float - { - if ($number === null) { - return 0.0; - } - - if (!is_string($number)) { - return (float)$number; - } - - $pct = Craft::$app->getFormattingLocale()->getNumberSymbol(Locale::SYMBOL_PERCENT); - $number = trim($number, "$pct \t\n\r\0\x0B"); - - if ($number === '') { - return 0.0; - } - - return static::normalizeNumber($number) / 100; - } -} diff --git a/src/helpers/Order.php b/src/helpers/Order.php deleted file mode 100644 index 8d4b41e1e7..0000000000 --- a/src/helpers/Order.php +++ /dev/null @@ -1,114 +0,0 @@ - - * @since 2.1 - */ -class Order -{ - /** - * @return bool Were any line items merged? - */ - public static function mergeDuplicateLineItems(OrderElement $order): bool - { - $lineItems = $order->getLineItems(); - $lineItemsByKey = []; - - foreach ($lineItems as $lineItem) { - // Generate a key depending on line item type - if ($lineItem->type === LineItemType::Purchasable) { - $key = $lineItem->orderId . '-' . LineItemType::Purchasable->value . '-' . $lineItem->purchasableId . '-' . $lineItem->getOptionsSignature(); - } else { - $key = $lineItem->orderId . '-' . LineItemType::Custom->value . '-' . $lineItem->getSku() . '-' . $lineItem->getOptionsSignature(); - } - - if (!isset($lineItemsByKey[$key])) { - $lineItemsByKey[$key] = $lineItem; - continue; - } - - $lineItemsByKey[$key]->qty += $lineItem->qty; - $lineItemsByKey[$key]->note = trim(($lineItemsByKey[$key]->note ? $lineItemsByKey[$key]->note . ' - ' : '') . $lineItem->note, ' -'); - } - - $order->setLineItems(array_values($lineItemsByKey)); - - return count($lineItems) > count($lineItemsByKey); - } - - /** - * Removes any line items from the cart that are no longer available. - * If a line item is available but the quantity is more than the available stock, - * the quantity will be reduced to the available stock. - * A notice will be added to the cart for each change. - * - * @param OrderElement $order - * @return void - * @throws InvalidConfigException - * @since 4.9.3 - */ - public static function normalizeLineItemPurchasableAvailability(OrderElement $order): void - { - if ($order->isCompleted) { - return; - } - - foreach ($order->getLineItems() as $lineItem) { - if ($lineItem->type !== LineItemType::Purchasable) { - continue; - } - - /* @var $purchasable Purchasable */ - $purchasable = $lineItem->getPurchasable(); - if (!$purchasable || !Plugin::getInstance()->getPurchasables()->isPurchasableAvailable($purchasable, $order)) { - $message = Craft::t('commerce', '{description} is no longer available.', ['description' => $lineItem->getDescription()]); - /** @var OrderNotice $notice */ - $notice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'message' => $message, - 'type' => 'lineItemRemoved', - 'attribute' => 'lineItems', - ], - ]); - $order->addNotice($notice); - $order->removeLineItem($lineItem); - } elseif ($purchasable::hasInventory() && - !$purchasable->getIsOutOfStockPurchasingAllowed() && - $purchasable->inventoryTracked && - ($lineItem->qty > $purchasable->getStock()) && - $purchasable->getStock() > 0 - ) { - $message = Craft::t('commerce', '{description} only has {stock} in stock.', ['description' => $lineItem->getDescription(), 'stock' => $purchasable->getStock()]); - /** @var OrderNotice $notice */ - $notice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'lineItemSalePriceChanged', - 'attribute' => "lineItems.$lineItem->id.qty", - 'message' => $message, - ], - ]); - $order->addNotice($notice); - $lineItem->qty = $purchasable->getStock(); - } - } - } -} diff --git a/src/helpers/PaymentForm.php b/src/helpers/PaymentForm.php deleted file mode 100644 index c30cba785e..0000000000 --- a/src/helpers/PaymentForm.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @since 4.0 - */ -class PaymentForm -{ - public const PAYMENT_FORM_NAMESPACE = 'paymentForm'; - - /** - * Generate the payment form namespace prefix. - * - * @param string $gatewayHandle - * @return string - */ - public static function getPaymentFormNamespace(string $gatewayHandle): string - { - return sprintf('%s[%s]', self::PAYMENT_FORM_NAMESPACE, $gatewayHandle); - } - - /** - * Generate the payment form namespace for retrieve request params. - * - * @param string $gatewayHandle - * @return string - */ - public static function getPaymentFormParamName(string $gatewayHandle): string - { - return sprintf('%s.%s', self::PAYMENT_FORM_NAMESPACE, $gatewayHandle); - } -} diff --git a/src/helpers/ProductQuery.php b/src/helpers/ProductQuery.php deleted file mode 100644 index d557e28e95..0000000000 --- a/src/helpers/ProductQuery.php +++ /dev/null @@ -1,103 +0,0 @@ - - * @since 5.5.0 - */ -class ProductQuery -{ - /** - * @param string $status - * @param string $tablePrefix - * @return array|false - */ - public static function statusCondition(string $status, string $tablePrefix = ''): array|false - { - // Always consider “now” to be the current time @ 59 seconds into the minute. - // This makes entry queries more cacheable, since they only change once every minute (https://github.com/craftcms/cms/issues/5389), - // while not excluding any entries that may have just been published in the past minute (https://github.com/craftcms/cms/issues/7853). - $now = new DateTime(); - $now->setTime((int)$now->format('H'), (int)$now->format('i'), 59); - $currentTimeDb = Db::prepareDateForDb($now); - - return match ($status) { - Product::STATUS_LIVE => [ - 'and', - [ - $tablePrefix . 'elements.enabled' => true, - $tablePrefix . 'elements_sites.enabled' => true, - ], - ['<=', 'commerce_products.postDate', $currentTimeDb], - [ - 'or', - ['commerce_products.expiryDate' => null], - ['>', 'commerce_products.expiryDate', $currentTimeDb], - ], - ], - Product::STATUS_PENDING => [ - 'and', - [ - $tablePrefix . 'elements.enabled' => true, - $tablePrefix . 'elements_sites.enabled' => true, - ], - ['>', 'commerce_products.postDate', $currentTimeDb], - ], - Product::STATUS_EXPIRED => [ - 'and', - [ - $tablePrefix . 'elements.enabled' => true, - $tablePrefix . 'elements_sites.enabled' => true, - ], - ['not', ['commerce_products.expiryDate' => null]], - ['<=', 'commerce_products.expiryDate', $currentTimeDb], - ], - - // Taken from base `ElementQuery::statusCondition()` - Element::STATUS_ENABLED => [ - $tablePrefix . 'elements.enabled' => true, - $tablePrefix . 'elements_sites.enabled' => true, - ], - Element::STATUS_DISABLED => [ - 'or', - [$tablePrefix . 'elements.enabled' => false], - [$tablePrefix . 'elements_sites.enabled' => false], - ], - Element::STATUS_ARCHIVED => [$tablePrefix . 'elements.archived' => true], - default => false, - }; - } - - /** - * @param array $criteria - * @return array - * @since 5.6.0 - */ - public static function cleanseQueryCriteria(array $criteria): array - { - // Figure out if creating the query has come from a request where params are passed to a controller action - $controller = Craft::$app->controller; - if ($controller instanceof ElementIndexesController || $controller instanceof ElementSearchController) { - $criteria = ElementHelper::cleanseQueryCriteria($criteria); - } - - return $criteria; - } -} diff --git a/src/helpers/ProjectConfigData.php b/src/helpers/ProjectConfigData.php deleted file mode 100755 index f1e430e1e6..0000000000 --- a/src/helpers/ProjectConfigData.php +++ /dev/null @@ -1,213 +0,0 @@ - - * @since 2.1.3 - */ -class ProjectConfigData -{ - /** - * @var bool - */ - private static $_processedStores = false; - - /** - * Ensure all stores are processed. - * - * @param bool $force - * @since 5.0.3 - */ - public static function ensureAllStoresProcessed(bool $force = false): void - { - $projectConfig = Craft::$app->getProjectConfig(); - - if (self::$_processedStores || (!$force && !$projectConfig->getIsApplyingExternalChanges())) { - return; - } - - self::$_processedStores = true; - - $allStores = $projectConfig->get(Stores::CONFIG_STORES_KEY, true) ?? []; - - foreach ($allStores as $uid => $storeData) { - // Ensure store is processed - $projectConfig->processConfigChanges(Stores::CONFIG_STORES_KEY . '.' . $uid, $force); - } - } - - /** - * Return a rebuilt project config array - */ - public static function rebuildProjectConfig(): array - { - $output = []; - - $output[self::_getProjectConfigKey(Emails::CONFIG_EMAILS_KEY)] = self::_getEmailData(); - $output[self::_getProjectConfigKey(Pdfs::CONFIG_PDFS_KEY)] = self::_getPdfData(); - $output[self::_getProjectConfigKey(Gateways::CONFIG_GATEWAY_KEY)] = self::_rebuildGatewayProjectConfig(); - $output[self::_getProjectConfigKey(Stores::CONFIG_STORES_KEY)] = self::_getStoresData(); - $output[self::_getProjectConfigKey(Stores::CONFIG_SITESTORES_KEY)] = self::_getSiteStoresData(); - - $orderFieldLayout = Craft::$app->getFields()->getLayoutByType(OrderElement::class); - - if ($orderFieldLayoutConfig = $orderFieldLayout->getConfig()) { - $output['orders'] = [ - 'fieldLayouts' => [ - $orderFieldLayout->uid => $orderFieldLayoutConfig, - ], - ]; - } - - $output[self::_getProjectConfigKey(OrderStatuses::CONFIG_STATUSES_KEY)] = self::_getStatusData(); - $output[self::_getProjectConfigKey(LineItemStatuses::CONFIG_STATUSES_KEY)] = self::_getLineItemStatusData(); - $output[self::_getProjectConfigKey(ProductTypes::CONFIG_PRODUCTTYPES_KEY)] = self::_getProductTypeData(); - - $subscriptionFieldLayout = Craft::$app->getFields()->getLayoutByType(Subscription::class); - - if ($subscriptionFieldLayoutConfig = $subscriptionFieldLayout->getConfig()) { - $output['subscriptions'] = [ - 'fieldLayouts' => [ - $subscriptionFieldLayout->uid => $subscriptionFieldLayoutConfig, - ], - ]; - } - - return array_filter($output); - } - - /** - * @param string $key - * @return string - * @since 5.0.0 - */ - private static function _getProjectConfigKey(string $key): string - { - $configKeyPrefix = 'commerce.'; - return substr($key, strlen($configKeyPrefix)); - } - - /** - * Return gateway data config array. - */ - private static function _rebuildGatewayProjectConfig(): array - { - $data = []; - foreach (Plugin::getInstance()->getGateways()->getAllGateways() as $gateway) { - $data[$gateway->uid] = $gateway->getConfig(); - } - return $data; - } - - /** - * Return stores data config array. - */ - private static function _getStoresData(): array - { - $data = []; - foreach (Plugin::getInstance()->getStores()->getAllStores() as $store) { - $data[$store->uid] = $store->getConfig(); - } - return $data; - } - - private static function _getSiteStoresData(): array - { - $data = []; - foreach (Plugin::getInstance()->getStores()->getAllSiteStores() as $siteStore) { - $data[$siteStore->uid] = $siteStore->getConfig(); - } - return $data; - } - - /** - * Return product type data config array. - */ - private static function _getProductTypeData(): array - { - $data = []; - foreach (Plugin::getInstance()->getProductTypes()->getAllProductTypes() as $productType) { - $data[$productType->uid] = $productType->getConfig(); - } - - return $data; - } - - /** - * Return email data config array. - */ - private static function _getEmailData(): array - { - $data = []; - Plugin::getInstance()->getStores()->getAllStores()->each(function(Store $store) use (&$data) { - foreach (Plugin::getInstance()->getEmails()->getAllEmails($store->id) as $email) { - $data[$email->uid] = $email->getConfig(); - } - }); - return $data; - } - - /** - * Return PDF data config array. - */ - private static function _getPdfData(): array - { - $data = []; - Plugin::getInstance()->getStores()->getAllStores()->each(function(Store $store) use (&$data) { - foreach (Plugin::getInstance()->getPdfs()->getAllPdfs($store->id) as $pdf) { - $data[$pdf->uid] = $pdf->getConfig(); - } - }); - return $data; - } - - /** - * Return line item status data config array. - */ - private static function _getLineItemStatusData(): array - { - $data = []; - Plugin::getInstance()->getStores()->getAllStores()->each(function(Store $store) use (&$data) { - foreach (Plugin::getInstance()->getLineItemStatuses()->getAllLineItemStatuses($store->id) as $status) { - $data[$status->uid] = $status->getConfig(); - } - }); - return $data; - } - - /** - * Return order status data config array. - */ - private static function _getStatusData(): array - { - $data = []; - Plugin::getInstance()->getStores()->getAllStores()->each(function(Store $store) use (&$data) { - foreach (Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses($store->id) as $status) { - $data[$status->uid] = $status->getConfig(); - } - }); - - return $data; - } -} diff --git a/src/helpers/Purchasable.php b/src/helpers/Purchasable.php deleted file mode 100644 index a1aa26f6c4..0000000000 --- a/src/helpers/Purchasable.php +++ /dev/null @@ -1,108 +0,0 @@ - - * @since 3.2.8 - */ -class Purchasable -{ - public const TEMPORARY_SKU_PREFIX = '__temp_'; - - /** - * Generates a new temporary SKU. - * - * @since 3.2.8 - */ - public static function tempSku(): string - { - return static::TEMPORARY_SKU_PREFIX . StringHelper::randomString(); - } - - /** - * Returns whether the given SKU is temporary. - * - * @since 3.2.8 - */ - public static function isTempSku(string $sku): bool - { - return str_starts_with($sku, static::TEMPORARY_SKU_PREFIX); - } - - /** - * @param int $purchasableId - * @param int $storeId - * @param Collection|null $catalogPricing - * @return string - * @throws SiteNotFoundException - * @throws InvalidConfigException - */ - public static function catalogPricingRulesTableByPurchasableId(int $purchasableId, int $storeId, ?Collection $catalogPricing = null): string - { - $catalogPricing ??= Plugin::getInstance()->getCatalogPricing()->getCatalogPricesByPurchasableId($purchasableId, $storeId); - $catalogPricingRules = Plugin::getInstance()->getCatalogPricingRules()->getAllCatalogPricingRulesByPurchasableId($purchasableId, $storeId); - - if ($catalogPricingRules->isEmpty()) { - return ''; - } - - return Cp::renderTemplate('commerce/prices/_table', [ - 'catalogPrices' => $catalogPricing, - 'showPurchasable' => false, - 'removeMargin' => true, - ]); - } - - /** - * @param string|null $value - * @param array $config - * @return string - * @since 5.0.0 - */ - public static function skuInputHtml(?string $value = null, array $config = []): string - { - $config += [ - 'id' => 'sku', - 'name' => 'sku', - 'value' => $value, - 'placeholder' => Craft::t('commerce', 'Enter SKU'), - 'class' => 'code', - ]; - - return Cp::textHtml($config); - } - - /** - * @param bool $value - * @param array $config - * @return string - * @since 5.0.0 - */ - public static function availableForPurchaseInputHtml(bool $value, array $config = []): string - { - $config += [ - 'id' => 'available-for-purchase', - 'name' => 'availableForPurchase', - 'small' => true, - 'on' => $value, - ]; - - return Cp::lightswitchHtml($config); - } -} diff --git a/src/linktypes/Product.php b/src/linktypes/Product.php deleted file mode 100644 index 272bb71ffc..0000000000 --- a/src/linktypes/Product.php +++ /dev/null @@ -1,52 +0,0 @@ - - * @since 5.1.0 - */ -class Product extends BaseElementLinkType -{ - protected static function elementType(): string - { - return ProductElement::class; - } - - protected function availableSourceKeys(): array - { - $sources = []; - $productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes(); - $sites = Craft::$app->getSites()->getAllSites(); - - foreach ($productTypes as $productType) { - $siteSettings = $productType->getSiteSettings(); - foreach ($sites as $site) { - if (isset($siteSettings[$site->id]) && $siteSettings[$site->id]->hasUrls) { - $sources[] = "productType:$productType->uid"; - break; - } - } - } - - $sources = array_values(array_unique($sources)); - - if (!empty($sources)) { - array_unshift($sources, '*'); - } - - return $sources; - } -} diff --git a/src/migrations/Install.php b/src/migrations/Install.php deleted file mode 100644 index e160e9dc97..0000000000 --- a/src/migrations/Install.php +++ /dev/null @@ -1,1523 +0,0 @@ - - * @since 2.0 - */ -class Install extends Migration -{ - /** - * @inheritdoc - */ - public function safeUp(): bool - { - $this->createTables(); - $this->createIndexes(); - $this->addForeignKeys(); - $this->insertDefaultData(); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - $this->dropForeignKeys(); - $this->dropTables(); - $this->dropProjectConfig(); - - $this->delete(CraftTable::FIELDLAYOUTS, ['type' => [ - Order::class, - Product::class, - Variant::class, - Subscription::class, - Transfer::class, - ]]); - - return true; - } - - /** - * Creates the tables for Craft Commerce - */ - public function createTables(): void - { - $this->archiveTableIfExists(Table::CATALOG_PRICING_RULES); - $this->createTable(Table::CATALOG_PRICING_RULES, [ - 'id' => $this->primaryKey(), - 'name' => $this->string()->notNull(), - 'description' => $this->text(), - 'storeId' => $this->integer()->notNull(), - 'dateFrom' => $this->dateTime(), - 'dateTo' => $this->dateTime(), - 'apply' => $this->enum('apply', ['toPercent', 'toFlat', 'byPercent', 'byFlat'])->notNull(), - 'applyAmount' => $this->decimal(14, 4)->notNull(), - 'applyPriceType' => $this->enum('applyPriceType', [CatalogPricingRule::APPLY_PRICE_TYPE_PRICE, CatalogPricingRule::APPLY_PRICE_TYPE_PROMOTIONAL_PRICE])->notNull(), - 'productCondition' => $this->text(), - 'variantCondition' => $this->text(), - 'purchasableCondition' => $this->text(), - 'customerCondition' => $this->text(), - 'enabled' => $this->boolean()->notNull()->defaultValue(true), - 'isPromotionalPrice' => $this->boolean()->notNull()->defaultValue(false), - 'metadata' => $this->text(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::CATALOG_PRICING_RULES_USERS); - $this->createTable(Table::CATALOG_PRICING_RULES_USERS, [ - 'id' => $this->primaryKey(), - 'catalogPricingRuleId' => $this->integer()->notNull(), - 'userId' => $this->integer()->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::CATALOG_PRICING); - $this->createTable(Table::CATALOG_PRICING, [ - 'id' => $this->primaryKey(), - 'price' => $this->decimal(14, 4), // @TODO Consider storing as string to avoid float-precision issues - 'purchasableId' => $this->integer()->notNull(), - 'storeId' => $this->integer(), - 'catalogPricingRuleId' => $this->integer(), - 'userId' => $this->integer(), - 'dateFrom' => $this->dateTime(), - 'dateTo' => $this->dateTime(), - 'isPromotionalPrice' => $this->boolean()->defaultValue(false), - 'hasUpdatePending' => $this->boolean()->defaultValue(false), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::CATALOG_PRICING_QUEUE); - $this->createTable(Table::CATALOG_PRICING_QUEUE, [ - 'id' => $this->primaryKey(), - 'storeId' => $this->integer(), - 'type' => $this->enum('type', [CatalogPricingQueue::TYPE_PURCHASABLE, CatalogPricingQueue::TYPE_RULE])->notNull(), - 'ids' => $this->mediumText(), - 'reserved' => $this->boolean()->notNull()->defaultValue(false), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::CUSTOMERS); - $this->createTable(Table::CUSTOMERS, [ - 'id' => $this->primaryKey(), // Not used in v4 but is the old customerId - 'customerId' => $this->integer()->notNull(), // This is the User element ID - 'primaryBillingAddressId' => $this->integer(), - 'primaryShippingAddressId' => $this->integer(), - 'primaryPaymentSourceId' => $this->integer(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::COUPONS); - $this->createTable(Table::COUPONS, [ - 'id' => $this->primaryKey(), - 'code' => $this->string(), - 'discountId' => $this->integer()->notNull(), - 'uses' => $this->integer()->notNull()->defaultValue(0), - 'maxUses' => $this->integer(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::CUSTOMER_DISCOUNTUSES); - $this->createTable(Table::CUSTOMER_DISCOUNTUSES, [ - 'id' => $this->primaryKey(), - 'discountId' => $this->integer()->notNull(), - 'customerId' => $this->integer()->notNull(), - 'uses' => $this->integer()->notNull()->unsigned(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::EMAIL_DISCOUNTUSES); - $this->createTable(Table::EMAIL_DISCOUNTUSES, [ - 'id' => $this->primaryKey(), - 'discountId' => $this->integer()->notNull(), - 'email' => $this->string()->notNull(), - 'uses' => $this->integer()->notNull()->unsigned(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::DISCOUNT_PURCHASABLES); - $this->createTable(Table::DISCOUNT_PURCHASABLES, [ - 'id' => $this->primaryKey(), - 'discountId' => $this->integer()->notNull(), - 'purchasableId' => $this->integer()->notNull(), - 'purchasableType' => $this->string()->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - // @TODO Rename to `discount_entries` table in Commerce 6.0, or remove if the purchasable condition builder fully replaces it - $this->archiveTableIfExists(Table::DISCOUNT_CATEGORIES); - $this->createTable(Table::DISCOUNT_CATEGORIES, [ - 'id' => $this->primaryKey(), - 'discountId' => $this->integer()->notNull(), - 'categoryId' => $this->integer()->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::DISCOUNTS); - $this->createTable(Table::DISCOUNTS, [ - 'id' => $this->primaryKey(), - 'storeId' => $this->integer()->notNull(), - 'name' => $this->string()->notNull(), - 'description' => $this->text(), - 'couponFormat' => $this->string(20)->notNull()->defaultValue(Coupons::DEFAULT_COUPON_FORMAT), - 'orderCondition' => $this->text(), - 'customerCondition' => $this->text(), - 'shippingAddressCondition' => $this->text(), - 'billingAddressCondition' => $this->text(), - 'requireCouponCode' => $this->boolean()->notNull()->defaultValue(false), - 'perUserLimit' => $this->integer()->notNull()->defaultValue(0)->unsigned(), - 'perEmailLimit' => $this->integer()->notNull()->defaultValue(0)->unsigned(), - 'totalDiscountUses' => $this->integer()->notNull()->defaultValue(0)->unsigned(), - 'totalDiscountUseLimit' => $this->integer()->notNull()->defaultValue(0)->unsigned(), - 'dateFrom' => $this->dateTime(), - 'dateTo' => $this->dateTime(), - 'purchaseQty' => $this->integer()->notNull()->defaultValue(0), - 'purchaseTotal' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'maxPurchaseQty' => $this->integer()->notNull()->defaultValue(0), - 'baseDiscount' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'perItemDiscount' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'percentDiscount' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'percentageOffSubject' => $this->enum('percentageOffSubject', ['original', 'discounted'])->notNull(), - 'excludeOnPromotion' => $this->boolean()->notNull()->defaultValue(false), - 'hasFreeShippingForMatchingItems' => $this->boolean()->notNull()->defaultValue(false), - 'hasFreeShippingForOrder' => $this->boolean()->notNull()->defaultValue(false), - 'allPurchasables' => $this->boolean()->notNull()->defaultValue(false), - 'purchasableIds' => $this->text(), - 'allCategories' => $this->boolean()->notNull()->defaultValue(false), - 'categoryIds' => $this->text(), - 'appliedTo' => $this->enum('appliedTo', ['matchingLineItems', 'allLineItems'])->notNull()->defaultValue('matchingLineItems'), - 'categoryRelationshipType' => $this->enum('categoryRelationshipType', ['element', 'sourceElement', 'targetElement'])->notNull()->defaultValue('element'), - 'orderConditionFormula' => $this->text(), - 'enabled' => $this->boolean()->notNull()->defaultValue(true), - 'stopProcessing' => $this->boolean()->notNull()->defaultValue(false), - 'ignorePromotions' => $this->boolean()->notNull()->defaultValue(false), - 'sortOrder' => $this->integer(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::DONATIONS); - $this->createTable(Table::DONATIONS, [ - 'id' => $this->primaryKey(), - 'sku' => $this->string()->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::EMAILS); - $this->createTable(Table::EMAILS, [ - 'id' => $this->primaryKey(), - 'storeId' => $this->integer(), - 'name' => $this->string()->notNull(), - 'senderAddress' => $this->string(), - 'senderName' => $this->string(), - 'subject' => $this->string()->notNull(), - 'recipientType' => $this->enum('recipientType', ['customer', 'custom'])->defaultValue('custom'), - 'to' => $this->string(), - 'bcc' => $this->string(), - 'cc' => $this->string(), - 'replyTo' => $this->string(), - 'enabled' => $this->boolean()->notNull()->defaultValue(true), - 'templatePath' => $this->string()->notNull(), - 'plainTextTemplatePath' => $this->string(), - 'pdfId' => $this->integer(), - 'language' => $this->string(), - 'renderSiteId' => $this->integer(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::PDFS); - $this->createTable(Table::PDFS, [ - 'id' => $this->primaryKey(), - 'storeId' => $this->integer(), - 'name' => $this->string()->notNull(), - 'handle' => $this->string()->notNull(), - 'description' => $this->string(), - 'templatePath' => $this->string()->notNull(), - 'fileNameFormat' => $this->string(), - 'paperOrientation' => $this->string()->defaultValue('portrait'), - 'paperSize' => $this->string()->defaultValue('letter'), - 'enabled' => $this->boolean()->notNull()->defaultValue(true), - 'isDefault' => $this->boolean()->notNull()->defaultValue(false), - 'sortOrder' => $this->integer(), - 'language' => $this->string(), - 'linkExpiry' => $this->integer()->notNull()->defaultValue(86400), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::GATEWAYS); - $this->createTable(Table::GATEWAYS, [ - 'id' => $this->primaryKey(), - 'type' => $this->string()->notNull(), - 'name' => $this->string()->notNull(), - 'handle' => $this->string()->notNull(), - 'settings' => $this->text(), - 'paymentType' => $this->enum('paymentType', ['authorize', 'purchase'])->notNull()->defaultValue('purchase'), - 'isFrontendEnabled' => $this->string(500)->notNull()->defaultValue('1'), - 'orderCondition' => $this->text(), - 'shippingAddressCondition' => $this->text(), - 'billingAddressCondition' => $this->text(), - 'isArchived' => $this->boolean()->notNull()->defaultValue(false), - 'dateArchived' => $this->dateTime(), - 'sortOrder' => $this->integer(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::INVENTORYITEMS); - $this->createTable(Table::INVENTORYITEMS, [ - 'id' => $this->primaryKey(), - 'purchasableId' => $this->integer()->notNull(), - 'countryCodeOfOrigin' => $this->string(), - 'administrativeAreaCodeOfOrigin' => $this->string(), - 'harmonizedSystemCode' => $this->string(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::INVENTORYLOCATIONS); - $this->createTable(Table::INVENTORYLOCATIONS, [ - 'id' => $this->primaryKey(), - 'handle' => $this->string()->notNull(), - 'name' => $this->string()->notNull(), - 'addressId' => $this->integer(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'dateDeleted' => $this->dateTime(), - 'uid' => $this->uid(), - ]); - - //INVENTORYLOCATIONS_STORES - $this->archiveTableIfExists(Table::INVENTORYLOCATIONS_STORES); - $this->createTable(Table::INVENTORYLOCATIONS_STORES, [ - 'id' => $this->primaryKey(), - 'inventoryLocationId' => $this->integer()->notNull(), - 'storeId' => $this->integer()->notNull(), - 'sortOrder' => $this->integer(), // per store - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::INVENTORYTRANSACTIONS); - $this->createTable(Table::INVENTORYTRANSACTIONS, [ - 'id' => $this->primaryKey(), - 'inventoryLocationId' => $this->integer()->notNull(), - 'inventoryItemId' => $this->integer()->notNull(), - 'movementHash' => $this->string()->notNull(), - 'quantity' => $this->integer()->notNull(), - 'type' => $this->enum('type', [ - 'incoming', - 'available', - 'committed', - 'reserved', - 'damaged', - 'safety', - 'fulfilled', - 'qualityControl', - ])->notNull(), - 'note' => $this->string(), - 'transferId' => $this->integer(), // Can be null - 'lineItemId' => $this->integer(), // Can be null - 'userId' => $this->integer(), // Can be null - 'dateCreated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::LINEITEMS); - $this->createTable(Table::LINEITEMS, [ - 'id' => $this->primaryKey(), - 'orderId' => $this->integer()->notNull(), - 'type' => $this->enum('type', ['purchasable', 'custom'])->defaultValue('purchasable')->notNull(), - 'purchasableId' => $this->integer(), - 'taxCategoryId' => $this->integer()->notNull(), - 'shippingCategoryId' => $this->integer()->notNull(), - 'description' => $this->text(), - 'options' => $this->text(), - 'optionsSignature' => $this->string()->notNull(), - 'price' => $this->decimal(14, 4)->notNull()->unsigned(), - 'promotionalPrice' => $this->decimal(14, 4)->null()->unsigned(), - 'promotionalAmount' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'salePrice' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'sku' => $this->string(), - 'weight' => $this->decimal(14, 4)->notNull()->defaultValue(0)->unsigned(), - 'height' => $this->decimal(14, 4)->notNull()->defaultValue(0)->unsigned(), - 'length' => $this->decimal(14, 4)->notNull()->defaultValue(0)->unsigned(), - 'width' => $this->decimal(14, 4)->notNull()->defaultValue(0)->unsigned(), - 'subtotal' => $this->decimal(14, 4)->notNull()->defaultValue(0)->unsigned(), - 'total' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'qty' => $this->integer()->notNull()->unsigned(), - 'note' => $this->text(), - 'privateNote' => $this->text(), - 'hasFreeShipping' => $this->boolean(), - 'isPromotable' => $this->boolean(), - 'isShippable' => $this->boolean(), - 'isTaxable' => $this->boolean(), - 'snapshot' => $this->longText(), - 'lineItemStatusId' => $this->integer(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::LINEITEMSTATUSES); - $this->createTable(Table::LINEITEMSTATUSES, [ - 'id' => $this->primaryKey(), - 'storeId' => $this->integer(), - 'name' => $this->string()->notNull(), - 'handle' => $this->string()->notNull(), - 'color' => $this->enum('color', ['green', 'orange', 'red', 'blue', 'yellow', 'pink', 'purple', 'turquoise', 'light', 'grey', 'black'])->notNull()->defaultValue('green'), - 'isArchived' => $this->boolean()->notNull()->defaultValue(false), - 'dateArchived' => $this->dateTime(), - 'sortOrder' => $this->integer(), - 'default' => $this->boolean()->notNull()->defaultValue(false), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::ORDERADJUSTMENTS); - $this->createTable(Table::ORDERADJUSTMENTS, [ - 'id' => $this->primaryKey(), - 'orderId' => $this->integer()->notNull(), - 'lineItemId' => $this->integer(), - 'type' => $this->string()->notNull(), - 'name' => $this->string(), - 'description' => $this->string(), - 'amount' => $this->decimal(14, 4)->notNull(), - 'included' => $this->boolean()->notNull()->defaultValue(false), - 'isEstimated' => $this->boolean()->notNull()->defaultValue(false), - 'sourceSnapshot' => $this->longText(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::ORDERNOTICES); - $this->createTable(Table::ORDERNOTICES, [ - 'id' => $this->primaryKey(), - 'orderId' => $this->integer()->notNull(), - 'type' => $this->string(), - 'attribute' => $this->string(), - 'message' => $this->text(), - 'noticeType' => $this->string()->notNull()->defaultValue('customer'), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::ORDERHISTORIES); - $this->createTable(Table::ORDERHISTORIES, [ - 'id' => $this->primaryKey(), - 'orderId' => $this->integer()->notNull(), - 'userId' => $this->integer(), - 'userName' => $this->string(), - 'prevStatusId' => $this->integer(), - 'newStatusId' => $this->integer(), - 'message' => $this->text(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::ORDERS); - $this->createTable(Table::ORDERS, [ - 'id' => $this->integer()->notNull(), - 'storeId' => $this->integer()->notNull(), - 'billingAddressId' => $this->integer(), - 'shippingAddressId' => $this->integer(), - 'estimatedBillingAddressId' => $this->integer(), - 'estimatedShippingAddressId' => $this->integer(), - 'sourceShippingAddressId' => $this->integer(), - 'sourceBillingAddressId' => $this->integer(), - 'gatewayId' => $this->integer(), - 'paymentSourceId' => $this->integer(), - 'customerId' => $this->integer(), // Customer ID is a User element ID - 'customerDeleted' => $this->boolean()->notNull()->defaultValue(false), - 'orderStatusId' => $this->integer(), - 'number' => $this->string(32), - 'reference' => $this->string(), - 'couponCode' => $this->string(), - 'itemTotal' => $this->decimal(14, 4)->defaultValue(0), - 'itemSubtotal' => $this->decimal(14, 4)->defaultValue(0), - 'totalQty' => $this->integer()->unsigned(), - 'totalWeight' => $this->decimal(14, 4)->defaultValue(0)->unsigned(), - 'total' => $this->decimal(14, 4)->defaultValue(0), - 'totalPrice' => $this->decimal(14, 4)->defaultValue(0), - 'totalPaid' => $this->decimal(14, 4)->defaultValue(0), - 'totalDiscount' => $this->decimal(14, 4)->defaultValue(0), - 'totalTax' => $this->decimal(14, 4)->defaultValue(0), - 'totalTaxIncluded' => $this->decimal(14, 4)->defaultValue(0), - 'totalShippingCost' => $this->decimal(14, 4)->defaultValue(0), - 'paidStatus' => $this->enum('paidStatus', ['paid', 'partial', 'unpaid', 'overPaid']), - 'email' => $this->string(), - 'orderCompletedEmail' => $this->string(), - 'isCompleted' => $this->boolean()->notNull()->defaultValue(false), - 'dateOrdered' => $this->dateTime(), - 'datePaid' => $this->dateTime(), - 'dateFirstPaid' => $this->dateTime(), - 'dateAuthorized' => $this->dateTime(), - 'currency' => $this->string(), - 'paymentCurrency' => $this->string(), - 'lastIp' => $this->string(), - 'orderLanguage' => $this->string(12)->notNull(), - 'origin' => $this->enum('origin', ['web', 'cp', 'remote'])->notNull()->defaultValue('web'), - 'message' => $this->text(), - 'registerUserOnOrderComplete' => $this->boolean()->notNull()->defaultValue(false), - 'saveBillingAddressOnOrderComplete' => $this->boolean()->notNull()->defaultValue(false), - 'makePrimaryBillingAddress' => $this->boolean()->notNull()->defaultValue(false), - 'saveShippingAddressOnOrderComplete' => $this->boolean()->notNull()->defaultValue(false), - 'makePrimaryShippingAddress' => $this->boolean()->notNull()->defaultValue(false), - 'recalculationMode' => $this->enum('recalculationMode', ['all', 'none', 'adjustmentsOnly'])->notNull()->defaultValue('all'), - 'returnUrl' => $this->text(), - 'cancelUrl' => $this->text(), - 'shippingMethodHandle' => $this->string()->notNull()->defaultValue(''), - 'shippingMethodName' => $this->string()->notNull()->defaultValue(''), - 'orderSiteId' => $this->integer(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - 'PRIMARY KEY([[id]])', - ]); - - $this->archiveTableIfExists(Table::ORDERSTATUS_EMAILS); - $this->createTable(Table::ORDERSTATUS_EMAILS, [ - 'id' => $this->primaryKey(), - 'orderStatusId' => $this->integer()->notNull(), - 'emailId' => $this->integer()->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::ORDERSTATUSES); - $this->createTable(Table::ORDERSTATUSES, [ - 'id' => $this->primaryKey(), - 'storeId' => $this->integer(), - 'name' => $this->string()->notNull(), - 'handle' => $this->string()->notNull(), - 'color' => $this->enum('color', ['green', 'orange', 'red', 'blue', 'yellow', 'pink', 'purple', 'turquoise', 'light', 'grey', 'black'])->notNull()->defaultValue('green'), - 'description' => $this->string(), - 'dateDeleted' => $this->dateTime(), - 'sortOrder' => $this->integer(), - 'default' => $this->boolean()->notNull()->defaultValue(false), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::PAYMENTCURRENCIES); - $this->createTable(Table::PAYMENTCURRENCIES, [ - 'id' => $this->primaryKey(), - 'storeId' => $this->integer()->notNull(), - 'iso' => $this->string(3)->notNull(), - 'primary' => $this->boolean()->notNull()->defaultValue(false), - 'rate' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::PAYMENTSOURCES); - $this->createTable(Table::PAYMENTSOURCES, [ - 'id' => $this->primaryKey(), - 'customerId' => $this->integer()->notNull(), - 'gatewayId' => $this->integer()->notNull(), - 'token' => $this->string()->notNull(), - 'description' => $this->string(), - 'response' => $this->text(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::PLANS); - $this->createTable(Table::PLANS, [ - 'id' => $this->primaryKey(), - 'gatewayId' => $this->integer(), - 'planInformationId' => $this->integer()->null(), - 'name' => $this->string()->notNull(), - 'handle' => $this->string()->notNull(), - 'reference' => $this->string()->notNull(), - 'enabled' => $this->boolean()->notNull()->defaultValue(false), - 'planData' => $this->text(), - 'isArchived' => $this->boolean()->notNull()->defaultValue(false), - 'dateArchived' => $this->dateTime(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'sortOrder' => $this->integer(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::PRODUCTS); - $this->createTable(Table::PRODUCTS, [ - 'id' => $this->integer()->notNull(), - 'typeId' => $this->integer(), - 'defaultVariantId' => $this->integer(), - 'postDate' => $this->dateTime(), - 'expiryDate' => $this->dateTime(), - 'defaultSku' => $this->string(), - 'defaultPrice' => $this->decimal(14, 4), - 'defaultHeight' => $this->decimal(14, 4), - 'defaultLength' => $this->decimal(14, 4), - 'defaultWidth' => $this->decimal(14, 4), - 'defaultWeight' => $this->decimal(14, 4), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - 'PRIMARY KEY([[id]])', - ]); - - $this->archiveTableIfExists(Table::PRODUCTTYPES); - $this->createTable(Table::PRODUCTTYPES, [ - 'id' => $this->primaryKey(), - 'isStructure' => $this->boolean()->notNull()->defaultValue(false), - 'maxLevels' => $this->smallInteger()->unsigned(), - 'defaultPlacement' => $this->enum('defaultPlacement', [ProductType::DEFAULT_PLACEMENT_BEGINNING, ProductType::DEFAULT_PLACEMENT_END])->defaultValue('end')->notNull(), - 'structureId' => $this->integer(), - 'fieldLayoutId' => $this->integer(), - 'variantFieldLayoutId' => $this->integer(), - 'name' => $this->string()->notNull(), - 'handle' => $this->string()->notNull(), - 'enableVersioning' => $this->boolean()->defaultValue(false)->notNull(), - 'maxVariants' => $this->integer(), - 'hasDimensions' => $this->boolean()->notNull()->defaultValue(false), - - // Variant title stuff - 'hasVariantTitleField' => $this->boolean()->notNull()->defaultValue(true), - 'variantTitleFormat' => $this->string()->notNull(), - 'variantTitleTranslationMethod' => $this->string()->defaultValue('site')->notNull(), - 'variantTitleTranslationKeyFormat' => $this->string(), - 'variantUiLabelFormat' => $this->string()->notNull()->defaultValue('{title}'), - - // Product title stuff - 'hasProductTitleField' => $this->boolean()->notNull()->defaultValue(true), - 'productTitleFormat' => $this->string(), - 'productTitleTranslationMethod' => $this->string()->defaultValue('site')->notNull(), - 'productTitleTranslationKeyFormat' => $this->string(), - 'productUiLabelFormat' => $this->string()->notNull()->defaultValue('{title}'), - - // Slug stuff - 'showSlugField' => $this->boolean()->notNull()->defaultValue(true), - 'slugTranslationMethod' => $this->string()->notNull()->defaultValue('site'), - 'slugTranslationKeyFormat' => $this->string(), - - 'propagationMethod' => $this->string()->defaultValue(PropagationMethod::All->value)->notNull(), - 'previewTargets' => $this->json(), - - 'skuFormat' => $this->string(), - 'descriptionFormat' => $this->string(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::PRODUCTTYPES_SITES); - $this->createTable(Table::PRODUCTTYPES_SITES, [ - 'id' => $this->primaryKey(), - 'productTypeId' => $this->integer()->notNull(), - 'siteId' => $this->integer()->notNull(), - 'uriFormat' => $this->text(), - 'template' => $this->string(500), - 'hasUrls' => $this->boolean()->notNull()->defaultValue(false), - 'enabledByDefault' => $this->boolean()->defaultValue(true)->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::PRODUCTTYPES_SHIPPINGCATEGORIES); - $this->createTable(Table::PRODUCTTYPES_SHIPPINGCATEGORIES, [ - 'id' => $this->primaryKey(), - 'productTypeId' => $this->integer()->notNull(), - 'shippingCategoryId' => $this->integer()->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::PRODUCTTYPES_TAXCATEGORIES); - $this->createTable(Table::PRODUCTTYPES_TAXCATEGORIES, [ - 'id' => $this->primaryKey(), - 'productTypeId' => $this->integer()->notNull(), - 'taxCategoryId' => $this->integer()->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::PURCHASABLES); - $this->createTable(Table::PURCHASABLES, [ - 'id' => $this->primaryKey(), - 'sku' => $this->string()->notNull(), - 'description' => $this->text(), - 'width' => $this->decimal(14, 4), - 'height' => $this->decimal(14, 4), - 'length' => $this->decimal(14, 4), - 'weight' => $this->decimal(14, 4), - 'taxCategoryId' => $this->integer()->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::PURCHASABLES_STORES); - $this->createTable(Table::PURCHASABLES_STORES, [ - 'id' => $this->primaryKey(), - 'purchasableId' => $this->integer()->notNull(), - 'storeId' => $this->integer()->notNull(), - 'basePrice' => $this->decimal(14, 4), // @TODO Consider storing as string to avoid float-precision issues - 'basePromotionalPrice' => $this->decimal(14, 4), // @TODO Consider storing as string to avoid float-precision issues - 'promotable' => $this->boolean()->notNull()->defaultValue(false), - 'availableForPurchase' => $this->boolean()->notNull()->defaultValue(true), - 'freeShipping' => $this->boolean()->notNull()->defaultValue(true), - 'inventoryTracked' => $this->boolean()->notNull()->defaultValue(true), - 'allowOutOfStockPurchases' => $this->boolean()->notNull()->defaultValue(false), - 'stock' => $this->integer(), // This is a summary value used for searching and sorting - 'tracked' => $this->boolean()->notNull()->defaultValue(false), - 'minQty' => $this->integer(), - 'maxQty' => $this->integer(), - 'shippingCategoryId' => $this->integer()->null(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::SALE_PURCHASABLES); - $this->createTable(Table::SALE_PURCHASABLES, [ - 'id' => $this->primaryKey(), - 'saleId' => $this->integer()->notNull(), - 'purchasableId' => $this->integer()->notNull(), - 'purchasableType' => $this->string()->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - // @TODO Rename to `sale_entries` table in Commerce 6.0, or remove if the purchasable condition builder fully replaces it - $this->archiveTableIfExists(Table::SALE_CATEGORIES); - $this->createTable(Table::SALE_CATEGORIES, [ - 'id' => $this->primaryKey(), - 'saleId' => $this->integer()->notNull(), - 'categoryId' => $this->integer()->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::SALE_USERGROUPS); - $this->createTable(Table::SALE_USERGROUPS, [ - 'id' => $this->primaryKey(), - 'saleId' => $this->integer()->notNull(), - 'userGroupId' => $this->integer()->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::SALES); - $this->createTable(Table::SALES, [ - 'id' => $this->primaryKey(), - 'name' => $this->string()->notNull(), - 'description' => $this->text(), - 'dateFrom' => $this->dateTime(), - 'dateTo' => $this->dateTime(), - 'apply' => $this->enum('apply', ['toPercent', 'toFlat', 'byPercent', 'byFlat'])->notNull(), - 'applyAmount' => $this->decimal(14, 4)->notNull(), - 'allGroups' => $this->boolean()->notNull()->defaultValue(false), - 'allPurchasables' => $this->boolean()->notNull()->defaultValue(false), - 'allCategories' => $this->boolean()->notNull()->defaultValue(false), - 'categoryRelationshipType' => $this->enum('categoryRelationshipType', ['element', 'sourceElement', 'targetElement'])->notNull()->defaultValue('element'), - 'enabled' => $this->boolean()->notNull()->defaultValue(true), - 'ignorePrevious' => $this->boolean()->notNull()->defaultValue(false), - 'stopProcessing' => $this->boolean()->notNull()->defaultValue(false), - 'sortOrder' => $this->integer(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::SHIPPINGCATEGORIES); - $this->createTable(Table::SHIPPINGCATEGORIES, [ - 'id' => $this->primaryKey(), - 'storeId' => $this->integer()->notNull(), - 'name' => $this->string()->notNull(), - 'handle' => $this->string()->notNull(), - 'icon' => $this->string(), - 'color' => $this->string(), - 'description' => $this->string(), - 'default' => $this->boolean()->notNull()->defaultValue(false), - 'dateDeleted' => $this->dateTime(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::SHIPPINGMETHODS); - $this->createTable(Table::SHIPPINGMETHODS, [ - 'id' => $this->primaryKey(), - 'storeId' => $this->integer()->notNull(), - 'name' => $this->string()->notNull(), - 'handle' => $this->string()->notNull(), - 'icon' => $this->string(), - 'color' => $this->string(), - 'orderCondition' => $this->text(), - 'customerCondition' => $this->text(), - 'enabled' => $this->boolean()->notNull()->defaultValue(true), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::SHIPPINGRULE_CATEGORIES); - $this->createTable(Table::SHIPPINGRULE_CATEGORIES, [ - 'id' => $this->primaryKey(), - 'shippingRuleId' => $this->integer(), - 'shippingCategoryId' => $this->integer(), - 'condition' => $this->enum('condition', ['allow', 'disallow', 'require'])->notNull(), - 'perItemRate' => $this->decimal(14, 4), - 'weightRate' => $this->decimal(14, 4), - 'percentageRate' => $this->decimal(14, 4), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::SHIPPINGRULES); - $this->createTable(Table::SHIPPINGRULES, [ - 'id' => $this->primaryKey(), - 'methodId' => $this->integer()->notNull(), - 'name' => $this->string()->notNull(), - 'description' => $this->string(), - 'priority' => $this->integer()->notNull()->defaultValue(0), - 'enabled' => $this->boolean()->notNull()->defaultValue(true), - 'orderConditionFormula' => $this->text(), - 'orderCondition' => $this->text(), - 'customerCondition' => $this->text(), - 'baseRate' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'perItemRate' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'weightRate' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'percentageRate' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'minRate' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'maxRate' => $this->decimal(14, 4)->notNull()->defaultValue(0), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::SHIPPINGZONES); - $this->createTable(Table::SHIPPINGZONES, [ - 'id' => $this->primaryKey(), - 'storeId' => $this->integer(), - 'name' => $this->string()->notNull(), - 'description' => $this->string(), - 'condition' => $this->text(), - 'default' => $this->boolean()->notNull()->defaultValue(false), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::SITESTORES); - $this->createTable(Table::SITESTORES, [ - 'siteId' => $this->integer(), - 'storeId' => $this->integer()->null(), // defaults to primary store in app - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - 'PRIMARY KEY([[siteId]])', - ]); - - $this->archiveTableIfExists(Table::STORES); - $this->createTable(Table::STORES, [ - 'id' => $this->primaryKey(), - 'name' => $this->string()->notNull(), - 'handle' => $this->string()->notNull(), - 'primary' => $this->boolean()->notNull(), - 'currency' => $this->string()->notNull()->defaultValue('USD'), - 'autoSetCartShippingMethodOption' => $this->string()->notNull()->defaultValue('false'), - 'autoSetNewCartAddresses' => $this->string()->notNull()->defaultValue('false'), - 'autoSetPaymentSource' => $this->string()->notNull()->defaultValue('false'), - 'allowEmptyCartOnCheckout' => $this->string()->notNull()->defaultValue('false'), - 'allowCheckoutWithoutPayment' => $this->string()->notNull()->defaultValue('false'), - 'allowPartialPaymentOnCheckout' => $this->string()->notNull()->defaultValue('false'), - 'requireShippingAddressAtCheckout' => $this->string()->notNull()->defaultValue('false'), - 'requireBillingAddressAtCheckout' => $this->string()->notNull()->defaultValue('false'), - 'requireShippingMethodSelectionAtCheckout' => $this->string()->notNull()->defaultValue('false'), - 'useBillingAddressForTax' => $this->string()->notNull()->defaultValue('false'), - 'validateOrganizationTaxIdAsVatId' => $this->string()->notNull()->defaultValue('false'), - 'orderReferenceFormat' => $this->string(), - 'freeOrderPaymentStrategy' => $this->string()->defaultValue('complete'), - 'minimumTotalPriceStrategy' => $this->string()->defaultValue('default'), - 'sortOrder' => $this->integer(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::STORESETTINGS); - $this->createTable(Table::STORESETTINGS, [ - 'id' => $this->integer()->notNull(), - 'locationAddressId' => $this->integer(), - 'countries' => $this->text(), - 'marketAddressCondition' => $this->text(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - 'PRIMARY KEY([[id]])', - ]); - - $this->archiveTableIfExists(Table::SUBSCRIPTIONS); - $this->createTable(Table::SUBSCRIPTIONS, [ - 'id' => $this->primaryKey(), - 'userId' => $this->integer()->notNull(), - 'planId' => $this->integer(), - 'gatewayId' => $this->integer(), - 'orderId' => $this->integer(), - 'reference' => $this->string()->notNull(), - 'subscriptionData' => $this->text(), - 'trialDays' => $this->integer()->notNull(), - 'nextPaymentDate' => $this->dateTime(), - 'hasStarted' => $this->boolean()->notNull()->defaultValue(true), - 'isSuspended' => $this->boolean()->notNull()->defaultValue(false), - 'dateSuspended' => $this->dateTime(), - 'isCanceled' => $this->boolean()->notNull()->defaultValue(false), - 'dateCanceled' => $this->dateTime(), - 'isExpired' => $this->boolean()->notNull()->defaultValue(false), - 'returnUrl' => $this->text(), - 'dateExpired' => $this->dateTime(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::TAXCATEGORIES); - $this->createTable(Table::TAXCATEGORIES, [ - 'id' => $this->primaryKey(), - 'name' => $this->string()->notNull(), - 'handle' => $this->string()->notNull(), - 'icon' => $this->string(), - 'color' => $this->string(), - 'description' => $this->string(), - 'default' => $this->boolean()->notNull()->defaultValue(false), - 'dateDeleted' => $this->dateTime(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::TAXRATES); - $this->createTable(Table::TAXRATES, [ - 'id' => $this->primaryKey(), - 'storeId' => $this->integer()->notNull(), - 'taxZoneId' => $this->integer(), - 'isEverywhere' => $this->boolean()->notNull()->defaultValue(true), - 'taxCategoryId' => $this->integer()->null(), - 'name' => $this->string()->notNull(), - 'code' => $this->string(), - 'rate' => $this->decimal(14, 10)->notNull(), - 'include' => $this->boolean()->notNull()->defaultValue(false), - 'isVat' => $this->boolean()->notNull()->defaultValue(false), // Remove in Commerce 6 - 'taxIdValidators' => $this->text(), - 'removeIncluded' => $this->boolean()->notNull()->defaultValue(false), - 'removeVatIncluded' => $this->boolean()->notNull()->defaultValue(false), - 'taxable' => $this->enum('taxable', ['purchasable', 'price', 'shipping', 'price_shipping', 'order_total_shipping', 'order_total_price'])->notNull(), - 'enabled' => $this->boolean()->defaultValue(true)->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::TAXZONES); - $this->createTable(Table::TAXZONES, [ - 'id' => $this->primaryKey(), - 'storeId' => $this->integer()->notNull(), - 'name' => $this->string()->notNull(), - 'description' => $this->string(), - 'condition' => $this->text(), - 'default' => $this->boolean()->notNull()->defaultValue(false), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::TRANSACTIONS); - $this->createTable(Table::TRANSACTIONS, [ - 'id' => $this->primaryKey(), - 'orderId' => $this->integer()->notNull(), - 'parentId' => $this->integer(), - 'gatewayId' => $this->integer(), - 'userId' => $this->integer(), // Stays as userId since it could be a logged-in user or store administrator. So not just a customer. - 'hash' => $this->string(32), - 'type' => $this->enum('type', ['authorize', 'capture', 'purchase', 'refund'])->notNull(), - 'amount' => $this->decimal(14, 4), - 'paymentAmount' => $this->decimal(14, 4), - 'currency' => $this->string(), - 'paymentCurrency' => $this->string(), - 'paymentRate' => $this->decimal(14, 4), - 'status' => $this->enum('status', ['pending', 'redirect', 'success', 'failed', 'processing'])->notNull(), - 'reference' => $this->string(), - 'code' => $this->string(), - 'message' => $this->text(), - 'note' => $this->mediumText(), - 'response' => $this->text(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::TRANSFERS); - $this->createTable(Table::TRANSFERS, [ - 'id' => $this->primaryKey(), - 'transferStatus' => $this->enum('transferStatus', [ - 'draft', - 'pending', - 'partial', - 'received', - ])->notNull(), - 'originLocationId' => $this->integer(), - 'destinationLocationId' => $this->integer(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::TRANSFERDETAILS); - $this->createTable(Table::TRANSFERDETAILS, [ - 'id' => $this->primaryKey(), - 'transferId' => $this->integer()->notNull(), - 'inventoryItemId' => $this->integer(), - 'inventoryItemDescription' => $this->string()->notNull(), - 'quantity' => $this->integer()->notNull(), - 'quantityAccepted' => $this->integer()->notNull(), - 'quantityRejected' => $this->integer()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->archiveTableIfExists(Table::VARIANTS); - $this->createTable(Table::VARIANTS, [ - 'id' => $this->integer()->notNull(), - 'primaryOwnerId' => $this->integer(), - 'isDefault' => $this->boolean()->notNull()->defaultValue(false), - 'deletedWithProduct' => $this->boolean()->notNull()->defaultValue(false), // @TODO Remove in Commerce 6.0 - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - 'PRIMARY KEY([[id]])', - ]); - } - - /** - * Drop the tables - */ - public function dropTables(): void - { - $tables = $this->_getAllTableNames(); - foreach ($tables as $table) { - $this->dropTableIfExists($table); - } - } - - /** - * Deletes the project config entry. - */ - public function dropProjectConfig(): void - { - Craft::$app->projectConfig->remove('commerce'); - } - - /** - * Creates the indexes. - */ - public function createIndexes(): void - { - $this->createIndex(null, Table::CATALOG_PRICING, 'catalogPricingRuleId', false); - $this->createIndex(null, Table::CATALOG_PRICING, 'isPromotionalPrice', false); - $this->createIndex(null, Table::CATALOG_PRICING, 'purchasableId', false); - $this->createIndex(null, Table::CATALOG_PRICING, 'storeId', false); - $this->createIndex(null, Table::CATALOG_PRICING, 'userId', false); - $this->createIndex(null, Table::CATALOG_PRICING, ['purchasableId', 'storeId', 'isPromotionalPrice', 'price', 'catalogPricingRuleId', 'dateFrom', 'dateTo'], false); - $this->createIndex(null, Table::CATALOG_PRICING, ['purchasableId', 'storeId', 'isPromotionalPrice', 'price'], false); - $this->createIndex(null, Table::CATALOG_PRICING, ['purchasableId', 'storeId'], false); - $this->createIndex(null, Table::CATALOG_PRICING_QUEUE, 'reserved', false); - $this->createIndex(null, Table::CATALOG_PRICING_QUEUE, ['storeId', 'type', 'reserved'], false); - $this->createIndex(null, Table::CATALOG_PRICING_RULES, 'storeId', false); - $this->createIndex(null, Table::CATALOG_PRICING_RULES_USERS, 'catalogPricingRuleId', false); - $this->createIndex(null, Table::CATALOG_PRICING_RULES_USERS, 'userId', false); - $this->createIndex(null, Table::COUPONS, 'code', false); - $this->createIndex(null, Table::COUPONS, 'discountId', false); - $this->createIndex(null, Table::CUSTOMERS, 'customerId', true); - $this->createIndex(null, Table::CUSTOMERS, 'primaryBillingAddressId', false); - $this->createIndex(null, Table::CUSTOMERS, 'primaryPaymentSourceId', false); - $this->createIndex(null, Table::CUSTOMERS, 'primaryShippingAddressId', false); - $this->createIndex(null, Table::CUSTOMER_DISCOUNTUSES, 'discountId', false); - $this->createIndex(null, Table::CUSTOMER_DISCOUNTUSES, ['customerId', 'discountId'], true); - $this->createIndex(null, Table::DISCOUNTS, 'dateFrom', false); - $this->createIndex(null, Table::DISCOUNTS, 'dateTo', false); - $this->createIndex(null, Table::DISCOUNT_CATEGORIES, 'categoryId', false); - $this->createIndex(null, Table::DISCOUNT_CATEGORIES, ['discountId', 'categoryId'], true); - $this->createIndex(null, Table::DISCOUNT_PURCHASABLES, 'purchasableId', false); - $this->createIndex(null, Table::DISCOUNT_PURCHASABLES, ['discountId', 'purchasableId'], true); - $this->createIndex(null, Table::EMAILS, 'storeId', false); - $this->createIndex(null, Table::EMAIL_DISCOUNTUSES, ['discountId'], false); - $this->createIndex(null, Table::EMAIL_DISCOUNTUSES, ['email', 'discountId'], true); - $this->createIndex(null, Table::GATEWAYS, 'handle', false); - $this->createIndex(null, Table::GATEWAYS, 'isArchived', false); - $this->createIndex(null, Table::INVENTORYITEMS, 'purchasableId', true); - $this->createIndex(null, Table::INVENTORYTRANSACTIONS, 'inventoryItemId', false); - $this->createIndex(null, Table::INVENTORYTRANSACTIONS, 'lineItemId', false); - $this->createIndex(null, Table::INVENTORYTRANSACTIONS, 'transferId', false); - $this->createIndex(null, Table::INVENTORYTRANSACTIONS, 'userId', false); - $this->createIndex(null, Table::LINEITEMS, 'purchasableId', false); - $this->createIndex(null, Table::LINEITEMS, 'shippingCategoryId', false); - $this->createIndex(null, Table::LINEITEMS, 'taxCategoryId', false); - $this->createIndex(null, Table::LINEITEMS, ['orderId', 'purchasableId', 'optionsSignature'], true); - $this->createIndex(null, Table::LINEITEMSTATUSES, 'storeId', false); - $this->createIndex(null, Table::ORDERADJUSTMENTS, 'orderId', false); - $this->createIndex(null, Table::ORDERHISTORIES, 'newStatusId', false); - $this->createIndex(null, Table::ORDERHISTORIES, 'orderId', false); - $this->createIndex(null, Table::ORDERHISTORIES, 'prevStatusId', false); - $this->createIndex(null, Table::ORDERHISTORIES, 'userId', false); - $this->createIndex(null, Table::ORDERNOTICES, 'orderId', false); - $this->createIndex(null, Table::ORDERS, 'billingAddressId', false); - $this->createIndex(null, Table::ORDERS, 'customerId', false); - $this->createIndex(null, Table::ORDERS, 'email', false); - $this->createIndex(null, Table::ORDERS, 'estimatedBillingAddressId', false); - $this->createIndex(null, Table::ORDERS, 'estimatedShippingAddressId', false); - $this->createIndex(null, Table::ORDERS, 'gatewayId', false); - $this->createIndex(null, Table::ORDERS, 'number', true); - $this->createIndex(null, Table::ORDERS, 'orderStatusId', false); - $this->createIndex(null, Table::ORDERS, 'reference', false); - $this->createIndex(null, Table::ORDERS, 'shippingAddressId', false); - $this->createIndex(null, Table::ORDERS, 'sourceBillingAddressId', false); - $this->createIndex(null, Table::ORDERS, 'sourceShippingAddressId', false); - $this->createIndex(null, Table::ORDERS, 'storeId', false); - $this->createIndex(null, Table::ORDERSTATUSES, 'storeId', false); - $this->createIndex(null, Table::ORDERSTATUS_EMAILS, 'emailId', false); - $this->createIndex(null, Table::ORDERSTATUS_EMAILS, 'orderStatusId', false); - $this->createIndex(null, Table::PAYMENTCURRENCIES, 'iso', false); - $this->createIndex(null, Table::PDFS, 'handle', false); - $this->createIndex(null, Table::PDFS, 'storeId', false); - $this->createIndex(null, Table::PLANS, 'gatewayId', false); - $this->createIndex(null, Table::PLANS, 'handle', true); - $this->createIndex(null, Table::PLANS, 'reference', false); - $this->createIndex(null, Table::PRODUCTS, 'expiryDate', false); - $this->createIndex(null, Table::PRODUCTS, 'postDate', false); - $this->createIndex(null, Table::PRODUCTS, 'typeId', false); - $this->createIndex(null, Table::PRODUCTTYPES, 'structureId', false); - $this->createIndex(null, Table::PRODUCTTYPES, 'fieldLayoutId', false); - $this->createIndex(null, Table::PRODUCTTYPES, 'handle', true); - $this->createIndex(null, Table::PRODUCTTYPES, 'variantFieldLayoutId', false); - $this->createIndex(null, Table::PRODUCTTYPES_SHIPPINGCATEGORIES, 'shippingCategoryId', false); - $this->createIndex(null, Table::PRODUCTTYPES_SHIPPINGCATEGORIES, ['productTypeId', 'shippingCategoryId'], true); - $this->createIndex(null, Table::PRODUCTTYPES_SITES, 'siteId', false); - $this->createIndex(null, Table::PRODUCTTYPES_SITES, ['productTypeId', 'siteId'], true); - $this->createIndex(null, Table::PRODUCTTYPES_TAXCATEGORIES, 'taxCategoryId', false); - $this->createIndex(null, Table::PRODUCTTYPES_TAXCATEGORIES, ['productTypeId', 'taxCategoryId'], true); - $this->createIndex(null, Table::PURCHASABLES, 'sku', false); // Application layer enforces unique - $this->createIndex(null, Table::PURCHASABLES_STORES, 'purchasableId', false); // Application layer enforces unique - $this->createIndex(null, Table::PURCHASABLES_STORES, 'storeId', false); // Application layer enforces unique - $this->createIndex(null, Table::SALE_CATEGORIES, 'categoryId', false); - $this->createIndex(null, Table::SALE_CATEGORIES, ['saleId', 'categoryId'], true); - $this->createIndex(null, Table::SALE_PURCHASABLES, 'purchasableId', false); - $this->createIndex(null, Table::SALE_PURCHASABLES, ['saleId', 'purchasableId'], true); - $this->createIndex(null, Table::SALE_USERGROUPS, 'userGroupId', false); - $this->createIndex(null, Table::SALE_USERGROUPS, ['saleId', 'userGroupId'], true); - $this->createIndex(null, Table::SHIPPINGCATEGORIES, 'storeId', false); - $this->createIndex(null, Table::SHIPPINGMETHODS, 'name', false); - $this->createIndex(null, Table::SHIPPINGMETHODS, 'storeId', false); - $this->createIndex(null, Table::SHIPPINGRULES, 'methodId', false); - $this->createIndex(null, Table::SHIPPINGRULES, 'name', false); - $this->createIndex(null, Table::SHIPPINGRULE_CATEGORIES, 'shippingCategoryId', false); - $this->createIndex(null, Table::SHIPPINGRULE_CATEGORIES, 'shippingRuleId', false); - $this->createIndex(null, Table::SHIPPINGZONES, 'name', false); - $this->createIndex(null, Table::SHIPPINGZONES, 'storeId', false); - $this->createIndex(null, Table::SUBSCRIPTIONS, 'dateCreated', false); - $this->createIndex(null, Table::SUBSCRIPTIONS, 'dateExpired', false); - $this->createIndex(null, Table::SUBSCRIPTIONS, 'gatewayId', false); - $this->createIndex(null, Table::SUBSCRIPTIONS, 'nextPaymentDate', false); - $this->createIndex(null, Table::SUBSCRIPTIONS, 'planId', false); - $this->createIndex(null, Table::SUBSCRIPTIONS, 'reference', true); - $this->createIndex(null, Table::SUBSCRIPTIONS, 'userId', false); - $this->createIndex(null, Table::TAXRATES, 'storeId', false); - $this->createIndex(null, Table::TAXRATES, 'taxCategoryId', false); - $this->createIndex(null, Table::TAXRATES, 'taxZoneId', false); - $this->createIndex(null, Table::TAXZONES, 'name', false); - $this->createIndex(null, Table::TAXZONES, 'storeId', false); - $this->createIndex(null, Table::TRANSACTIONS, 'gatewayId', false); - $this->createIndex(null, Table::TRANSACTIONS, 'orderId', false); - $this->createIndex(null, Table::TRANSACTIONS, 'parentId', false); - $this->createIndex(null, Table::TRANSACTIONS, 'userId', false); - $this->createIndex(null, Table::TRANSACTIONS, 'hash', false); - $this->createIndex(null, Table::TRANSFERS, 'destinationLocationId', false); - $this->createIndex(null, Table::TRANSFERS, 'originLocationId', false); - $this->createIndex(null, Table::TRANSFERDETAILS, 'transferId', false); - $this->createIndex(null, Table::TRANSFERDETAILS, 'inventoryItemId', false); - $this->createIndex(null, Table::VARIANTS, 'primaryOwnerId', false); - } - - /** - * Adds the foreign keys. - */ - public function addForeignKeys(): void - { - $this->addForeignKey(null, Table::CATALOG_PRICING, ['catalogPricingRuleId'], Table::CATALOG_PRICING_RULES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::CATALOG_PRICING, ['purchasableId'], Table::PURCHASABLES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::CATALOG_PRICING, ['storeId'], Table::STORES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::CATALOG_PRICING, ['userId'], CraftTable::USERS, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::CATALOG_PRICING_QUEUE, ['storeId'], Table::STORES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::CATALOG_PRICING_RULES, ['storeId'], Table::STORES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::CATALOG_PRICING_RULES_USERS, ['catalogPricingRuleId'], Table::CATALOG_PRICING_RULES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::CATALOG_PRICING_RULES_USERS, ['userId'], CraftTable::USERS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::COUPONS, ['discountId'], Table::DISCOUNTS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::CUSTOMERS, ['customerId'], CraftTable::ELEMENTS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::CUSTOMERS, ['primaryBillingAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::CUSTOMERS, ['primaryPaymentSourceId'], Table::PAYMENTSOURCES, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::CUSTOMERS, ['primaryShippingAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::CUSTOMER_DISCOUNTUSES, ['customerId'], CraftTable::ELEMENTS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::CUSTOMER_DISCOUNTUSES, ['discountId'], Table::DISCOUNTS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::DISCOUNTS, 'storeId', Table::STORES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::DISCOUNT_CATEGORIES, ['categoryId'], CraftTable::ELEMENTS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::DISCOUNT_CATEGORIES, ['discountId'], Table::DISCOUNTS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::DISCOUNT_PURCHASABLES, ['discountId'], Table::DISCOUNTS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::DISCOUNT_PURCHASABLES, ['purchasableId'], Table::PURCHASABLES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::DONATIONS, ['id'], '{{%elements}}', ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::EMAILS, ['pdfId'], Table::PDFS, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::EMAILS, ['storeId'], Table::STORES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::EMAILS, ['renderSiteId'], CraftTable::SITES, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::EMAIL_DISCOUNTUSES, ['discountId'], Table::DISCOUNTS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::INVENTORYITEMS, 'purchasableId', Table::PURCHASABLES, 'id', 'CASCADE', null); - $this->addForeignKey(null, Table::INVENTORYLOCATIONS, 'addressId', CraftTable::ELEMENTS, 'id', 'CASCADE', null); - $this->addForeignKey(null, Table::INVENTORYLOCATIONS_STORES, 'inventoryLocationId', Table::INVENTORYLOCATIONS, 'id', 'CASCADE', null); - $this->addForeignKey(null, Table::INVENTORYLOCATIONS_STORES, 'storeId', Table::STORES, 'id', 'CASCADE', null); - $this->addForeignKey(null, Table::INVENTORYTRANSACTIONS, 'inventoryItemId', Table::INVENTORYITEMS, 'id', 'CASCADE', null); - $this->addForeignKey(null, Table::INVENTORYTRANSACTIONS, 'inventoryLocationId', Table::INVENTORYLOCATIONS, 'id', 'CASCADE', null); - $this->addForeignKey(null, Table::INVENTORYTRANSACTIONS, 'lineItemId', Table::LINEITEMS, 'id', 'CASCADE', null); - $this->addForeignKey(null, Table::INVENTORYTRANSACTIONS, 'transferId', Table::TRANSFERS, 'id', 'SET NULL', null); - $this->addForeignKey(null, Table::INVENTORYTRANSACTIONS, 'userId', CraftTable::USERS, 'id', 'SET NULL', null); - $this->addForeignKey(null, Table::INVENTORYTRANSACTIONS, 'transferId', Table::TRANSFERS, 'id', 'SET NULL', null); - $this->addForeignKey(null, Table::LINEITEMS, ['orderId'], Table::ORDERS, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::LINEITEMS, ['purchasableId'], '{{%elements}}', ['id'], 'SET NULL', 'CASCADE'); - $this->addForeignKey(null, Table::LINEITEMS, ['shippingCategoryId'], Table::SHIPPINGCATEGORIES, ['id'], null, 'CASCADE'); - $this->addForeignKey(null, Table::LINEITEMS, ['taxCategoryId'], Table::TAXCATEGORIES, ['id'], null, 'CASCADE'); - $this->addForeignKey(null, Table::LINEITEMSTATUSES, ['storeId'], Table::STORES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::ORDERADJUSTMENTS, ['orderId'], Table::ORDERS, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::ORDERHISTORIES, ['newStatusId'], Table::ORDERSTATUSES, ['id'], 'RESTRICT', 'CASCADE'); - $this->addForeignKey(null, Table::ORDERHISTORIES, ['orderId'], Table::ORDERS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::ORDERHISTORIES, ['prevStatusId'], Table::ORDERSTATUSES, ['id'], 'RESTRICT', 'CASCADE'); - $this->addForeignKey(null, Table::ORDERHISTORIES, ['userId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::ORDERNOTICES, ['orderId'], Table::ORDERS, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::ORDERS, ['billingAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::ORDERS, ['customerId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::ORDERS, ['estimatedBillingAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::ORDERS, ['estimatedShippingAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::ORDERS, ['gatewayId'], Table::GATEWAYS, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::ORDERS, ['id'], '{{%elements}}', ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::ORDERS, ['orderStatusId'], Table::ORDERSTATUSES, ['id'], 'RESTRICT', 'CASCADE'); - $this->addForeignKey(null, Table::ORDERS, ['paymentSourceId'], Table::PAYMENTSOURCES, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::ORDERS, ['shippingAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::ORDERS, ['storeId'], Table::STORES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::ORDERSTATUSES, ['storeId'], Table::STORES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::ORDERSTATUS_EMAILS, ['emailId'], Table::EMAILS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::ORDERSTATUS_EMAILS, ['orderStatusId'], Table::ORDERSTATUSES, ['id'], 'RESTRICT', 'CASCADE'); - $this->addForeignKey(null, Table::PAYMENTCURRENCIES, 'storeId', Table::STORES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::PAYMENTSOURCES, ['customerId'], CraftTable::ELEMENTS, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::PAYMENTSOURCES, ['gatewayId'], Table::GATEWAYS, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::PDFS, ['storeId'], Table::STORES, ['id'], 'CASCADE', null); - $this->addForeignKey(null, Table::PLANS, ['gatewayId'], Table::GATEWAYS, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::PLANS, ['planInformationId'], '{{%elements}}', 'id', 'SET NULL'); - $this->addForeignKey(null, Table::PRODUCTS, ['id'], '{{%elements}}', ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::PRODUCTS, ['typeId'], Table::PRODUCTTYPES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::PRODUCTS, ['defaultVariantId'], '{{%elements}}', ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::PRODUCTTYPES, ['fieldLayoutId'], '{{%fieldlayouts}}', ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::PRODUCTTYPES, ['variantFieldLayoutId'], '{{%fieldlayouts}}', ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::PRODUCTTYPES, ['structureId'], CraftTable::STRUCTURES, ['id'], 'SET NULL', null); - $this->addForeignKey(null, Table::PRODUCTTYPES_SHIPPINGCATEGORIES, ['productTypeId'], Table::PRODUCTTYPES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::PRODUCTTYPES_SHIPPINGCATEGORIES, ['shippingCategoryId'], Table::SHIPPINGCATEGORIES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::PRODUCTTYPES_SITES, ['productTypeId'], Table::PRODUCTTYPES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::PRODUCTTYPES_SITES, ['siteId'], '{{%sites}}', ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::PRODUCTTYPES_TAXCATEGORIES, ['productTypeId'], Table::PRODUCTTYPES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::PRODUCTTYPES_TAXCATEGORIES, ['taxCategoryId'], Table::TAXCATEGORIES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::PURCHASABLES, ['id'], '{{%elements}}', ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::PURCHASABLES, ['taxCategoryId'], Table::TAXCATEGORIES, ['id']); - $this->addForeignKey(null, Table::PURCHASABLES_STORES, ['purchasableId'], Table::PURCHASABLES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::PURCHASABLES_STORES, ['shippingCategoryId'], Table::SHIPPINGCATEGORIES, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::PURCHASABLES_STORES, ['purchasableId'], Table::PURCHASABLES, ['id'],'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::PURCHASABLES_STORES, ['storeId'], Table::STORES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::SALE_CATEGORIES, ['categoryId'], CraftTable::ELEMENTS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::SALE_CATEGORIES, ['saleId'], Table::SALES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::SALE_PURCHASABLES, ['purchasableId'], Table::PURCHASABLES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::SALE_PURCHASABLES, ['saleId'], Table::SALES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::SALE_USERGROUPS, ['saleId'], Table::SALES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::SALE_USERGROUPS, ['userGroupId'], '{{%usergroups}}', ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::SHIPPINGCATEGORIES, ['storeId'], Table::STORES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::SHIPPINGMETHODS, ['storeId'], Table::STORES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::SHIPPINGRULES, ['methodId'], Table::SHIPPINGMETHODS, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::SHIPPINGRULE_CATEGORIES, ['shippingCategoryId'], Table::SHIPPINGCATEGORIES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::SHIPPINGRULE_CATEGORIES, ['shippingRuleId'], Table::SHIPPINGRULES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::SHIPPINGZONES, ['storeId'], Table::STORES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::STORESETTINGS, ['locationAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::STORESETTINGS, ['id'], Table::STORES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::SUBSCRIPTIONS, ['gatewayId'], Table::GATEWAYS, ['id'], 'RESTRICT'); - $this->addForeignKey(null, Table::SUBSCRIPTIONS, ['id'], '{{%elements}}', ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::SUBSCRIPTIONS, ['orderId'], Table::ORDERS, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::SUBSCRIPTIONS, ['planId'], Table::PLANS, ['id'], 'RESTRICT'); - $this->addForeignKey(null, Table::SUBSCRIPTIONS, ['userId'], CraftTable::USERS, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::TAXRATES, ['storeId'], Table::STORES, ['id'], 'CASCADE', null); - $this->addForeignKey(null, Table::TAXRATES, ['taxCategoryId'], Table::TAXCATEGORIES, ['id'], null, 'CASCADE'); - $this->addForeignKey(null, Table::TAXRATES, ['taxZoneId'], Table::TAXZONES, ['id'], null, 'CASCADE'); - $this->addForeignKey(null, Table::TAXZONES, ['storeId'], Table::STORES, ['id'], 'CASCADE', null); - $this->addForeignKey(null, Table::TRANSACTIONS, ['gatewayId'], Table::GATEWAYS, ['id'], null, 'CASCADE'); - $this->addForeignKey(null, Table::TRANSACTIONS, ['orderId'], Table::ORDERS, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::TRANSACTIONS, ['parentId'], Table::TRANSACTIONS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::TRANSACTIONS, ['userId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - $this->addForeignKey(null, Table::TRANSFERS, 'id', CraftTable::ELEMENTS, 'id', 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::TRANSFERDETAILS, 'transferId', Table::TRANSFERS, 'id', 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::TRANSFERDETAILS, 'inventoryItemId', Table::INVENTORYITEMS, 'id', 'SET NULL', 'CASCADE'); - $this->addForeignKey(null, Table::VARIANTS, ['id'], '{{%elements}}', ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::VARIANTS, ['primaryOwnerId'], Table::PRODUCTS, ['id'], 'CASCADE'); - } - - /** - * Removes the foreign keys. - */ - public function dropForeignKeys(): void - { - $tables = $this->_getAllTableNames(); - - foreach ($tables as $table) { - $this->_dropForeignKeyToAndFromTable($table); - } - } - - /** - * Insert the default data. - */ - public function insertDefaultData(): void - { - // Don't make the same config changes twice - $projectConfig = Craft::$app->getProjectConfig(); - $installedInProjectConfig = ($projectConfig->get('plugins.commerce', true) !== null); - $configExists = ($projectConfig->get('commerce', true) !== null); - - if (!$installedInProjectConfig && !$configExists) { - $this->_insertPrimaryStore(); - $this->_defaultGateways(); - } elseif ($installedInProjectConfig) { - - // Start fix for a bad commerce project config from the 5.0.0-beta.1 - // @TODO Remove this fix-up for the 5.0.0-beta.1 bad store key in Commerce 6.0 - $commerce = $projectConfig->get('commerce', true); - - foreach (array_keys($commerce) as $key) { - // Look for the bad store key - if (StringHelper::startsWith('stores',$key) && StringHelper::length($key) > 6) { - $uid = substr($key, 7); - // Move the data to the correct location for stores - $projectConfig->set(Stores::CONFIG_STORES_KEY . '.' . $uid, $commerce[$key]); - } - } - // Finish fix for a bad commerce project config from the 5.0.0-beta.1 - - // Install a primary store if it isn't in the config - $stores = $projectConfig->get(Stores::CONFIG_STORES_KEY, true); - if (!$configExists || !$stores || !ArrayHelper::firstWhere($stores, 'primary', true)) { - $this->_insertPrimaryStore(); - } - - // Install the default gateways if they aren't in the config - $gateways = $projectConfig->get(Gateways::CONFIG_GATEWAY_KEY, true); - if (!$configExists || !$gateways) { - $this->_defaultGateways(); - } - } - - // The following defaults are not stored in the project config. - $this->_defaultTaxCategories(); - $this->_defaultInventoryLocation(); - } - - /** - * Add a default Tax category. - */ - private function _defaultTaxCategories(): void - { - $data = [ - 'name' => 'General', - 'handle' => 'general', - 'default' => true, - ]; - $this->insert(TaxCategory::tableName(), $data); - } - - /** - * Add a default Inventory Location. - */ - private function _defaultInventoryLocation(): void - { - $inventoryLocation = new InventoryLocation(); - $inventoryLocation->name = 'Default'; - $inventoryLocation->handle = 'default'; - $inventoryLocation->save(false); - - // get primary store from db query - $storeId = (new Query()) - ->select(['id']) - ->from(Table::STORES) - ->where(['primary' => true]) - ->scalar(); - - if ($storeId) { - $this->insert(Table::INVENTORYLOCATIONS_STORES, [ - 'inventoryLocationId' => $inventoryLocation->id, - 'storeId' => $storeId, - 'sortOrder' => 1, - 'dateCreated' => Db::prepareDateForDb(new \DateTime()), - 'dateUpdated' => Db::prepareDateForDb(new \DateTime()), - ]); - } - } - - private function _insertPrimaryStore(): void - { - $store = Craft::createObject([ - 'class' => Store::class, - 'name' => 'Primary', - 'handle' => 'primary', - 'primary' => true, - 'currency' => 'USD', - ]); - - Plugin::getInstance()->getStores()->saveStore($store); - - foreach (Craft::$app->getSites()->getAllSites() as $site) { - $siteStore = Craft::createObject([ - 'class' => SiteStore::class, - 'siteId' => $site->id, - 'storeId' => $store->id, - ]); - Plugin::getInstance()->getStores()->saveSiteStore($siteStore, false); - } - } - - /** - * Add a payment method. - */ - private function _defaultGateways(): void - { - $data = [ - 'name' => 'Dummy', - 'handle' => 'dummy', - 'isFrontendEnabled' => true, - 'orderCondition' => [], - 'isArchived' => false, - ]; - $gateway = new Dummy($data); - Plugin::getInstance()->getGateways()->saveGateway($gateway); - } - - /** - * Returns if the table exists. - * - * @param string $tableName - * @return bool If the table exists. - * @throws NotSupportedException - */ - private function _tableExists(string $tableName): bool - { - $schema = $this->db->getSchema(); - $schema->refresh(); - - $rawTableName = $schema->getRawTableName($tableName); - $table = $schema->getTableSchema($rawTableName); - - return (bool)$table; - } - - /** - * @param $tableName - * @throws NotSupportedException - */ - private function _dropForeignKeyToAndFromTable($tableName): void - { - if ($this->_tableExists($tableName)) { - $this->dropAllForeignKeysToTable($tableName); - MigrationHelper::dropAllForeignKeysOnTable($tableName, $this); - } - } - - /** - * @return string[] - */ - private function _getAllTableNames(): array - { - $class = new ReflectionClass(Table::class); - return $class->getConstants(); - } -} diff --git a/src/migrations/m210614_073359_detailed_permission.php b/src/migrations/m210614_073359_detailed_permission.php deleted file mode 100644 index 746f598832..0000000000 --- a/src/migrations/m210614_073359_detailed_permission.php +++ /dev/null @@ -1,189 +0,0 @@ -_detailedPromotions(); - $this->_dropManageCustomersPermission(); - $this->_detailedProducts(); - $this->_projectConfigUpdates(); - } - - /** - * @inheritdoc - */ - public function safeDown() - { - echo "m210614_073359_detailed_permission cannot be reverted.\n"; - return false; - } - - private function _detailedPromotions() - { - // Create new promotion permissions - $this->insert(Table::USERPERMISSIONS, ['name' => 'commerce-editsales']); - $editSalesId = $this->db->getLastInsertID(); - - $this->insert(Table::USERPERMISSIONS, ['name' => 'commerce-createsales']); - $createSalesId = $this->db->getLastInsertID(); - - $this->insert(Table::USERPERMISSIONS, ['name' => 'commerce-deletesales']); - $deleteSalesId = $this->db->getLastInsertID(); - - $this->insert(Table::USERPERMISSIONS, ['name' => 'commerce-editdiscounts']); - $editDiscountsId = $this->db->getLastInsertID(); - - $this->insert(Table::USERPERMISSIONS, ['name' => 'commerce-creatediscounts']); - $createDiscountsId = $this->db->getLastInsertID(); - - $this->insert(Table::USERPERMISSIONS, ['name' => 'commerce-deletediscounts']); - $deleteDiscountsId = $this->db->getLastInsertID(); - - $permissionId = (new Query()) - ->select(['id']) - ->from([Table::USERPERMISSIONS]) - ->where(['name' => 'commerce-managepromotions']) - ->scalar(); - - if ($permissionId) { - $userPromotions = (new Query()) - ->select(['id', 'userId']) - ->from([Table::USERPERMISSIONS_USERS]) - ->where(['permissionId' => $permissionId]) - ->all(); - - foreach ($userPromotions as $userPromotion) { - $this->insert(Table::USERPERMISSIONS_USERS, ['userId' => $userPromotion['userId'], 'permissionId' => $editSalesId]); - $this->insert(Table::USERPERMISSIONS_USERS, ['userId' => $userPromotion['userId'], 'permissionId' => $createSalesId]); - $this->insert(Table::USERPERMISSIONS_USERS, ['userId' => $userPromotion['userId'], 'permissionId' => $deleteSalesId]); - $this->insert(Table::USERPERMISSIONS_USERS, ['userId' => $userPromotion['userId'], 'permissionId' => $editDiscountsId]); - $this->insert(Table::USERPERMISSIONS_USERS, ['userId' => $userPromotion['userId'], 'permissionId' => $createDiscountsId]); - $this->insert(Table::USERPERMISSIONS_USERS, ['userId' => $userPromotion['userId'], 'permissionId' => $deleteDiscountsId]); - } - - // Check if manage product type is ticked for user group permissions - $groupPromotions = (new Query()) - ->select(['id', 'permissionId', 'groupId']) - ->from([Table::USERPERMISSIONS_USERGROUPS]) - ->where(['permissionId' => $permissionId]) - ->all(); - - foreach ($groupPromotions as $groupPromotion) { - $this->insert(Table::USERPERMISSIONS_USERGROUPS, ['groupId' => $groupPromotion['groupId'], 'permissionId' => $editSalesId]); - $this->insert(Table::USERPERMISSIONS_USERGROUPS, ['groupId' => $groupPromotion['groupId'], 'permissionId' => $createSalesId]); - $this->insert(Table::USERPERMISSIONS_USERGROUPS, ['groupId' => $groupPromotion['groupId'], 'permissionId' => $deleteSalesId]); - $this->insert(Table::USERPERMISSIONS_USERGROUPS, ['groupId' => $groupPromotion['groupId'], 'permissionId' => $editDiscountsId]); - $this->insert(Table::USERPERMISSIONS_USERGROUPS, ['groupId' => $groupPromotion['groupId'], 'permissionId' => $createDiscountsId]); - $this->insert(Table::USERPERMISSIONS_USERGROUPS, ['groupId' => $groupPromotion['groupId'], 'permissionId' => $deleteDiscountsId]); - } - } - } - - /** - * @return void - */ - private function _dropManageCustomersPermission(): void - { - $this->delete(Table::USERPERMISSIONS, ['name' => 'commerce-managecustomers']); - } - - private function _detailedProducts() - { - // Get existing manage product type permission - $permissions = (new Query()) - ->select(['id', 'name']) - ->from([Table::USERPERMISSIONS]) - ->where(new Expression("LEFT([[name]], 26) = 'commerce-manageproducttype'")) - ->all(); - - if (count($permissions) > 0) { - foreach ($permissions as $permission) { - $permissionName = explode(':', $permission['name']); - $productTypeUid = $permissionName[1]; - - // Rename manage product type to edit product type - $newName = str_replace('commerce-manageproducttype', 'commerce-editproducttype', $permission['name']); - $this->update(Table::USERPERMISSIONS, ['name' => $newName], ['id' => $permission['id']], [], false); - - // Create new create product permission by product type - $this->insert(Table::USERPERMISSIONS, ['name' => 'commerce-createproducts:' . $productTypeUid]); - $createPermissionId = $this->db->getLastInsertID(); - - // Create new delete product permission by product type - $this->insert(Table::USERPERMISSIONS, ['name' => 'commerce-deleteproducts:' . $productTypeUid]); - $deletePermissionId = $this->db->getLastInsertID(); - - // Check if manage product type is ticked for user permissions - $manageProductTypes = (new Query()) - ->select(['id', 'permissionId', 'userId']) - ->from([Table::USERPERMISSIONS_USERS]) - ->where(['permissionId' => $permission['id']]) - ->all(); - // Add the new edit product child permissions for the same users - foreach ($manageProductTypes as $manageProductType) { - $this->insert(Table::USERPERMISSIONS_USERS, ['userId' => $manageProductType['userId'], 'permissionId' => $createPermissionId]); - $this->insert(Table::USERPERMISSIONS_USERS, ['userId' => $manageProductType['userId'], 'permissionId' => $deletePermissionId]); - } - - // Check if manage product type is ticked for user group permissions - $manageProductTypesForGroups = (new Query()) - ->select(['id', 'permissionId', 'groupId']) - ->from([Table::USERPERMISSIONS_USERGROUPS]) - ->where(['permissionId' => $permission['id']]) - ->all(); - // Add the new edit product child permissions for the same groups - foreach ($manageProductTypesForGroups as $manageProductType) { - // Create new create and delete product permission relationship with a group. - $this->insert(Table::USERPERMISSIONS_USERGROUPS, ['groupId' => $manageProductType['groupId'], 'permissionId' => $createPermissionId]); - $this->insert(Table::USERPERMISSIONS_USERGROUPS, ['groupId' => $manageProductType['groupId'], 'permissionId' => $deletePermissionId]); - } - } - - // No longer need this top level permission - $this->delete(Table::USERPERMISSIONS, ['name' => 'commerce-manageproducts']); - } - } - - private function _projectConfigUpdates() - { - // Make project config updates - $projectConfig = Craft::$app->getProjectConfig(); - - $groups = (new Query()) - ->select(['id', 'name', 'uid']) - ->from(['groups' => Table::USERGROUPS]) - ->all(); - - $setGroupPermissions = []; - - foreach ($groups as $group) { - $groupPermissions = (new Query()) - ->select(['up.name']) - ->from(['up_ug' => Table::USERPERMISSIONS_USERGROUPS]) - ->where(['up_ug.groupId' => $group['id']]) - ->innerJoin(['up' => Table::USERPERMISSIONS], '[[up.id]] = [[up_ug.permissionId]]') - ->column(); - - $setGroupPermissions[$group['uid']] = $groupPermissions; - } - - foreach ($setGroupPermissions as $uid => $setGroupPermission) { - $projectConfig->set('users.groups.' . $uid . '.permissions', $setGroupPermission); - } - } -} diff --git a/src/migrations/m210831_080542_rename_variant_title_format_field.php b/src/migrations/m210831_080542_rename_variant_title_format_field.php deleted file mode 100644 index c84e368788..0000000000 --- a/src/migrations/m210831_080542_rename_variant_title_format_field.php +++ /dev/null @@ -1,43 +0,0 @@ -renameColumn('{{%commerce_producttypes}}', 'titleFormat', 'variantTitleFormat'); - - $projectConfig = Craft::$app->getProjectConfig(); - - $productTypes = $projectConfig->get('commerce.productTypes') ?? []; - $muteEvents = $projectConfig->muteEvents; - $projectConfig->muteEvents = true; - - foreach ($productTypes as $uid => $productType) { - $productType['variantTitleFormat'] = $productType['titleFormat']; - unset($productType['titleFormat']); - $projectConfig->set("commerce.productTypes.$uid", $productType); - } - - $projectConfig->muteEvents = $muteEvents; - } - - /** - * @inheritdoc - */ - public function safeDown() - { - echo "m210831_080542_rename_variant_title_format_field cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m210901_211323_not_null_booleans.php b/src/migrations/m210901_211323_not_null_booleans.php deleted file mode 100644 index 0bc28bf671..0000000000 --- a/src/migrations/m210901_211323_not_null_booleans.php +++ /dev/null @@ -1,206 +0,0 @@ -updateColumns(); - $this->updateProjectConfig(); - return true; - } - - private function updateColumns(): void - { - $columns = [ - '{{%commerce_countries}}' => [ - 'isStateRequired' => false, - ], - '{{%commerce_discounts}}' => [ - 'excludeOnSale' => false, - 'hasFreeShippingForMatchingItems' => false, - 'hasFreeShippingForOrder' => false, - 'allPurchasables' => false, - 'allCategories' => false, - 'enabled' => true, - 'stopProcessing' => false, - ], - '{{%commerce_donations}}' => [ - 'availableForPurchase' => false, - ], - '{{%commerce_emails}}' => [ - 'enabled' => true, - ], - '{{%commerce_pdfs}}' => [ - 'enabled' => true, - 'isDefault' => false, - ], - '{{%commerce_gateways}}' => [ - 'isFrontendEnabled' => true, - 'isArchived' => false, - ], - '{{%commerce_lineitemstatuses}}' => [ - 'default' => false, - ], - '{{%commerce_orderadjustments}}' => [ - 'included' => false, - ], - '{{%commerce_orders}}' => [ - 'isCompleted' => false, - 'registerUserOnOrderComplete' => false, - ], - '{{%commerce_orderstatuses}}' => [ - 'default' => false, - ], - '{{%commerce_plans}}' => [ - 'enabled' => false, - 'isArchived' => false, - ], - '{{%commerce_products}}' => [ - 'promotable' => false, - 'availableForPurchase' => true, - 'freeShipping' => false, - ], - '{{%commerce_producttypes}}' => [ - 'hasDimensions' => false, - 'hasVariants' => false, - 'hasVariantTitleField' => true, - 'hasProductTitleField' => true, - ], - '{{%commerce_producttypes_sites}}' => [ - 'hasUrls' => false, - ], - '{{%commerce_sales}}' => [ - 'allGroups' => false, - 'allPurchasables' => false, - 'allCategories' => false, - 'enabled' => true, - 'ignorePrevious' => false, - 'stopProcessing' => false, - ], - '{{%commerce_shippingcategories}}' => [ - 'default' => false, - ], - '{{%commerce_shippingmethods}}' => [ - 'enabled' => true, - 'isLite' => false, - ], - '{{%commerce_shippingrules}}' => [ - 'enabled' => true, - 'isLite' => false, - ], - '{{%commerce_shippingzones}}' => [ - 'isCountryBased' => true, - ], - '{{%commerce_subscriptions}}' => [ - 'isCanceled' => false, - 'isExpired' => false, - ], - '{{%commerce_taxcategories}}' => [ - 'default' => false, - ], - '{{%commerce_taxrates}}' => [ - 'isEverywhere' => true, - 'include' => false, - 'isVat' => false, - 'removeIncluded' => false, - 'removeVatIncluded' => false, - 'isLite' => false, - ], - '{{%commerce_taxzones}}' => [ - 'isCountryBased' => true, - 'default' => false, - ], - '{{%commerce_variants}}' => [ - 'isDefault' => false, - 'hasUnlimitedStock' => false, - 'deletedWithProduct' => false, - ], - ]; - - $isPgsql = $this->db->getIsPgsql(); - - foreach ($columns as $table => $tableColumns) { - foreach ($tableColumns as $column => $defaultValue) { - // Set any null values to false - $this->update($table, [$column => false], [$column => null], [], false); - - // Add a NOT NULL constraint and default value - if ($isPgsql) { - // Manually construct the SQL for Postgres - // (see https://github.com/yiisoft/yii2/issues/12077) - $this->execute("ALTER TABLE $table ALTER COLUMN \"$column\" SET NOT NULL, " . - "ALTER COLUMN \"$column\" SET DEFAULT " . ($defaultValue ? 'TRUE' : 'FALSE')); - } else { - $this->alterColumn($table, $column, $this->boolean()->notNull()->defaultValue($defaultValue)); - } - } - } - } - - private function updateProjectConfig(): void - { - $projectConfig = Craft::$app->getProjectConfig(); - - $projectConfig->muteEvents = true; - - $keys = [ - Gateways::CONFIG_GATEWAY_KEY => [ - 'isFrontendEnabled', - 'isArchived', - ], - ProductTypes::CONFIG_PRODUCTTYPES_KEY => [ - 'hasDimensions', - 'hasVariants', - 'hasVariantTitleField', - 'hasProductTitleField', - ], - OrderStatuses::CONFIG_STATUSES_KEY => [ - 'default', - ], - Emails::CONFIG_EMAILS_KEY => [ - 'enabled', - ], - Pdfs::CONFIG_PDFS_KEY => [ - 'enabled', - 'isDefault', - ], - ]; - - foreach ($keys as $basePath => $itemKeys) { - $items = $projectConfig->get($basePath) ?? []; - foreach ($items as $uid => $item) { - foreach ($itemKeys as $key) { - $item[$key] = (bool)($item[$key] ?? false); - } - $projectConfig->set("$basePath.$uid", $item); - } - } - - $projectConfig->muteEvents = false; - } - - /** - * @inheritdoc - */ - public function safeDown() - { - echo "m210901_211323_not_null_booleans cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m210922_133729_add_discount_order_condition_builder.php b/src/migrations/m210922_133729_add_discount_order_condition_builder.php deleted file mode 100644 index bc653a5f72..0000000000 --- a/src/migrations/m210922_133729_add_discount_order_condition_builder.php +++ /dev/null @@ -1,32 +0,0 @@ -db->columnExists('{{%commerce_discounts}}', 'orderCondition')) { - $this->addColumn('{{%commerce_discounts}}', 'orderCondition', $this->text()->after('description')); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m210922_133729_add_discount_order_condition_builder cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m211118_101920_split_coupon_codes.php b/src/migrations/m211118_101920_split_coupon_codes.php deleted file mode 100644 index 51c7e80308..0000000000 --- a/src/migrations/m211118_101920_split_coupon_codes.php +++ /dev/null @@ -1,104 +0,0 @@ -db->tableExists('{{%commerce_coupons}}')) { - $this->createTable('{{%commerce_coupons}}', [ - 'id' => $this->primaryKey(), - 'code' => $this->string(), - 'discountId' => $this->integer()->notNull(), - 'uses' => $this->integer()->notNull()->defaultValue(0), - 'maxUses' => $this->integer(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->createIndex(null, '{{%commerce_coupons}}', 'discountId', false); - $this->createIndex(null, '{{%commerce_coupons}}', 'code', false); - - $this->addForeignKey(null, '{{%commerce_coupons}}', ['discountId'], '{{%commerce_discounts}}', ['id'], 'CASCADE', 'CASCADE'); - } - - if (!$this->db->columnExists('{{%commerce_discounts}}', 'couponFormat')) { - $this->addColumn('{{%commerce_discounts}}', 'couponFormat', $this->string(20)->notNull()->defaultValue(Coupons::DEFAULT_COUPON_FORMAT)); - } - - if (!(new Query())->from('{{%commerce_coupons}}')->exists()) { - // These could be one query, leaving as separate for now for readability - $discountsWithCodes = (new Query()) - ->select(['id', 'code', 'totalDiscountUseLimit', 'dateCreated', 'dateUpdated']) - ->from('{{%commerce_discounts}}') - ->where(['not', ['code' => null]]) - ->all(); - - $codeUsage = (new Query()) - ->select([new Expression('COUNT(*) as count'), 'couponCode as code']) - ->from('{{%commerce_orders}}') - ->where(['not', ['couponCode' => null]]) - ->groupBy('couponCode') - ->indexBy('code') - ->column(); - - if (!empty($discountsWithCodes)) { - $coupons = array_map(static function($discount) use ($codeUsage) { - $maxUses = $discount['totalDiscountUseLimit'] !== null && $discount['totalDiscountUseLimit'] > 0 - ? $discount['totalDiscountUseLimit'] - : null; - - $row['code'] = $discount['code']; - $row['discountId'] = $discount['id']; - $row['uses'] = $codeUsage[$discount['code']] ?? 0; - $row['maxUses'] = $maxUses; - $row['dateCreated'] = $discount['dateCreated']; - $row['dateUpdated'] = $discount['dateUpdated']; - $row['uid'] = StringHelper::UUID(); - - return $row; - }, $discountsWithCodes); - - $this->batchInsert('{{%commerce_coupons}}', [ - 'code', - 'discountId', - 'uses', - 'maxUses', - 'dateCreated', - 'dateUpdated', - 'uid', - ], $coupons); - } - } - - if ($this->db->columnExists('{{%commerce_discounts}}', 'code')) { - $this->dropIndexIfExists('{{%commerce_discounts}}', 'code', true); - $this->dropColumn('{{%commerce_discounts}}', 'code'); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m211118_101920_split_coupon_codes cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m220301_022054_user_addresses.php b/src/migrations/m220301_022054_user_addresses.php deleted file mode 100644 index 88cabc72ce..0000000000 --- a/src/migrations/m220301_022054_user_addresses.php +++ /dev/null @@ -1,183 +0,0 @@ -db->getIsPgsql(); - - /** - * Order Addresses - */ - $this->addColumn('{{%commerce_orders}}', 'sourceShippingAddressId', $this->integer()->after('estimatedShippingAddressId')); // no need for index as not queryable - $this->addColumn('{{%commerce_orders}}', 'sourceBillingAddressId', $this->integer()->after('estimatedBillingAddressId')); // no need for index as not queryable - - /** - * Zones - */ - $this->renameColumn('{{%commerce_taxzones}}', 'isCountryBased', 'v3isCountryBased'); - $this->renameColumn('{{%commerce_shippingzones}}', 'isCountryBased', 'v3isCountryBased'); - $this->renameColumn('{{%commerce_taxzones}}', 'zipCodeConditionFormula', 'v3zipCodeConditionFormula'); - $this->renameColumn('{{%commerce_shippingzones}}', 'zipCodeConditionFormula', 'v3zipCodeConditionFormula'); - $this->addColumn('{{%commerce_taxzones}}', 'condition', $this->text()); - $this->addColumn('{{%commerce_shippingzones}}', 'condition', $this->text()); - - /* - * Orders - */ - // Move the customerId to a temporary column, and relate the new customerId FK to the user element - $this->dropForeignKeyIfExists('{{%commerce_orders}}', ['customerId']); - $this->dropIndexIfExists('{{%commerce_orders}}', ['customerId']); - $this->renameColumn('{{%commerce_orders}}', 'customerId', 'v3customerId'); // move the data - $this->createIndex(null, '{{%commerce_orders}}', 'v3customerId', false); - - $this->addColumn('{{%commerce_orders}}', 'customerId', $this->integer()); - $this->createIndex(null, '{{%commerce_orders}}', 'customerId', false); - $this->addForeignKey(null, '{{%commerce_orders}}', ['customerId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - - // Move the billingAddressId to a temporary column, and relate the new billingAddressId FK to the address element - $this->dropForeignKeyIfExists('{{%commerce_orders}}', ['billingAddressId']); - $this->dropIndexIfExists('{{%commerce_orders}}', ['billingAddressId']); - $this->renameColumn('{{%commerce_orders}}', 'billingAddressId', 'v3billingAddressId'); // move the data - - $this->addColumn('{{%commerce_orders}}', 'billingAddressId', $this->integer()); - $this->addForeignKey(null, '{{%commerce_orders}}', ['billingAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - - // Move the shippingAddressId to a temporary column, and relate the new shippingAddressId FK to the address element - $this->dropForeignKeyIfExists('{{%commerce_orders}}', ['shippingAddressId']); - $this->dropIndexIfExists('{{%commerce_orders}}', ['shippingAddressId']); - $this->renameColumn('{{%commerce_orders}}', 'shippingAddressId', 'v3shippingAddressId'); // move the data - - $this->addColumn('{{%commerce_orders}}', 'shippingAddressId', $this->integer()); - $this->addForeignKey(null, '{{%commerce_orders}}', ['shippingAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - - // Move the estimatedBillingAddressId to a temporary column, and relate the new estimatedBillingAddressId FK to the address element - $this->dropForeignKeyIfExists('{{%commerce_orders}}', ['estimatedBillingAddressId']); - $this->dropIndexIfExists('{{%commerce_orders}}', ['estimatedBillingAddressId']); - $this->renameColumn('{{%commerce_orders}}', 'estimatedBillingAddressId', 'v3estimatedBillingAddressId'); // move the data - - $this->addColumn('{{%commerce_orders}}', 'estimatedBillingAddressId', $this->integer()); - $this->addForeignKey(null, '{{%commerce_orders}}', ['estimatedBillingAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - - // Move the estimatedShippingAddressId to a temporary column, and relate the new estimatedShippingAddressId FK to the address element - $this->dropForeignKeyIfExists('{{%commerce_orders}}', ['estimatedShippingAddressId']); - $this->dropIndexIfExists('{{%commerce_orders}}', ['estimatedShippingAddressId']); - $this->renameColumn('{{%commerce_orders}}', 'estimatedShippingAddressId', 'v3estimatedShippingAddressId'); // move the data - - $this->addColumn('{{%commerce_orders}}', 'estimatedShippingAddressId', $this->integer()); - $this->addForeignKey(null, '{{%commerce_orders}}', ['estimatedShippingAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - - /* - * Customers - */ - // Move the userId and ID to a temporary column, add the customerId column. - $this->dropForeignKeyIfExists('{{%commerce_customers}}', ['userId']); - $this->dropIndexIfExists('{{%commerce_customers}}', ['userId']); - $this->renameColumn('{{%commerce_customers}}', 'userId', 'v3userId'); // move the data - $this->dropForeignKeyIfExists('{{%commerce_customers}}', ['primaryBillingAddressId']); - $this->dropForeignKeyIfExists('{{%commerce_customers}}', ['primaryShippingAddressId']); - $this->renameColumn('{{%commerce_customers}}', 'primaryBillingAddressId', 'v3primaryBillingAddressId'); // move the data - $this->renameColumn('{{%commerce_customers}}', 'primaryShippingAddressId', 'v3primaryShippingAddressId'); // move the data - $this->addColumn('{{%commerce_customers}}', 'primaryBillingAddressId', $this->integer()); - $this->addColumn('{{%commerce_customers}}', 'primaryShippingAddressId', $this->integer()); - $this->addColumn('{{%commerce_customers}}', 'customerId', $this->integer()->null()); - - $this->addForeignKey(null, '{{%commerce_customers}}', ['primaryBillingAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - $this->addForeignKey(null, '{{%commerce_customers}}', ['primaryShippingAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - // Add the new primary customerId column with will share the same ID the user element ID - //$this->addColumn('{{%commerce_customers}}', 'customerId', $this->integer()); - $this->addForeignKey(null, '{{%commerce_customers}}', ['customerId'], CraftTable::ELEMENTS, ['id'], 'CASCADE', 'CASCADE'); - $this->createIndex(null, '{{%commerce_customers}}', 'customerId', true); - - - /** - * Customer Discount Uses - */ - $this->dropAllForeignKeysToTable('{{%commerce_customer_discountuses}}'); - $this->dropForeignKeyIfExists('{{%commerce_customer_discountuses}}', ['customerId']); - $this->dropForeignKeyIfExists('{{%commerce_customer_discountuses}}', ['discountId']); - $this->dropIndexIfExists('{{%commerce_customer_discountuses}}', ['customerId', 'discountId'], true); - $this->dropIndexIfExists('{{%commerce_customer_discountuses}}', ['discountId']); - $this->renameColumn('{{%commerce_customer_discountuses}}', 'customerId', 'v3customerId'); // move the data - - if ($isPgsql) { - // Manually construct the SQL for Postgres - // (see https://github.com/yiisoft/yii2/issues/12077) - $this->execute('alter table {{%commerce_customer_discountuses}} alter column [[v3customerId]] type integer, alter column [[v3customerId]] drop not null'); - } else { - $this->alterColumn('{{%commerce_customer_discountuses}}', 'v3customerId', $this->integer()->null()); - } - - $this->addColumn('{{%commerce_customer_discountuses}}', 'customerId', $this->integer()); - $this->addForeignKey(null, '{{%commerce_customer_discountuses}}', ['customerId'], CraftTable::ELEMENTS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, '{{%commerce_customer_discountuses}}', ['discountId'], '{{%commerce_discounts}}', ['id'], 'CASCADE', 'CASCADE'); - $this->createIndex(null, '{{%commerce_customer_discountuses}}', ['customerId', 'discountId'], true); - $this->createIndex(null, '{{%commerce_customer_discountuses}}', 'discountId', false); - - /** - * Payment Sources - */ - $this->dropForeignKeyIfExists('{{%commerce_paymentsources}}', ['userId']); - $this->dropIndexIfExists('{{%commerce_paymentsources}}', ['userId']); - $this->renameColumn('{{%commerce_paymentsources}}', 'userId', 'customerId'); // was already a user ID - $this->addForeignKey(null, '{{%commerce_paymentsources}}', ['customerId'], CraftTable::ELEMENTS, ['id'], 'CASCADE'); - - /** - * Order Histories - */ - $this->dropForeignKeyIfExists('{{%commerce_orderhistories}}', ['customerId']); - $this->dropIndexIfExists('{{%commerce_orderhistories}}', ['customerId']); - $this->renameColumn('{{%commerce_orderhistories}}', 'customerId', 'v3customerId'); // move the data - - if ($isPgsql) { - // Manually construct the SQL for Postgres - // (see https://github.com/yiisoft/yii2/issues/12077) - $this->execute('alter table {{%commerce_orderhistories}} alter column [[v3customerId]] type integer, alter column [[v3customerId]] drop not null'); - } else { - $this->alterColumn('{{%commerce_orderhistories}}', 'v3customerId', $this->integer()->null()); - } - $this->createIndex(null, '{{%commerce_orderhistories}}', 'v3customerId', false); - - $this->addColumn('{{%commerce_orderhistories}}', 'userId', $this->integer()->null()); - $this->addForeignKey(null, '{{%commerce_orderhistories}}', ['userId'], CraftTable::ELEMENTS, ['id'], 'CASCADE', 'CASCADE'); - $this->createIndex(null, '{{%commerce_orderhistories}}', 'userId', false); - - $this->addColumn('{{%commerce_addresses}}', 'v4addressId', $this->integer()->null()); - - // Add new Store table - if (!Craft::$app->getDb()->tableExists('{{%commerce_stores}}')) { - $this->createTable('{{%commerce_stores}}', [ - 'id' => $this->primaryKey(), - 'locationAddressId' => $this->integer(), - 'countries' => $this->text(), - 'marketAddressCondition' => $this->text(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m220222_134640_address_user_schema_changes cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m220302_133730_add_discount_user_addresses_condition_builders.php b/src/migrations/m220302_133730_add_discount_user_addresses_condition_builders.php deleted file mode 100644 index 2e661c70e9..0000000000 --- a/src/migrations/m220302_133730_add_discount_user_addresses_condition_builders.php +++ /dev/null @@ -1,40 +0,0 @@ -db->columnExists('{{%commerce_discounts}}', 'customerCondition')) { - $this->addColumn('{{%commerce_discounts}}', 'customerCondition', $this->text()->after('orderCondition')); - } - - if (!$this->db->columnExists('{{%commerce_discounts}}', 'shippingAddressCondition')) { - $this->addColumn('{{%commerce_discounts}}', 'shippingAddressCondition', $this->text()->after('customerCondition')); - } - - if (!$this->db->columnExists('{{%commerce_discounts}}', 'billingAddressCondition')) { - $this->addColumn('{{%commerce_discounts}}', 'billingAddressCondition', $this->text()->after('shippingAddressCondition')); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m220302_133730_add_discount_user_addresses_condition_builders cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m220304_094835_discount_conditions.php b/src/migrations/m220304_094835_discount_conditions.php deleted file mode 100644 index 9aa1931faa..0000000000 --- a/src/migrations/m220304_094835_discount_conditions.php +++ /dev/null @@ -1,110 +0,0 @@ -select(['id', 'userGroupsCondition']) - ->from(['{{%commerce_discounts}}']) - ->indexBy('id') - ->all(); - - foreach ($discounts as $id => $discount) { - - /** - * Order condition - */ - $this->update('{{%commerce_discounts}}', [ - 'orderCondition' => Json::encode($orderCondition->getConfig()), - ], ['id' => $id]); - - /** - * User/Customer condition - */ - $discountsUserGroupIds = (new Query())->select(['dug.userGroupId']) - ->from('{{%commerce_discounts}} discounts') - ->leftJoin('{{%commerce_discount_usergroups}} dug', '[[dug.discountId]] = [[discounts.id]]') - ->where(['discounts.id' => $id]) - ->column(); - - $userGroupUids = Db::uidsByIds('{{%usergroups}}', $discountsUserGroupIds, $this->db); - - if ($discountsUserGroupIds && $userGroupUids && ($discount['userGroupsCondition'] != 'userGroupsAnyOrNone')) { - $userRules = []; - if ($discount['userGroupsCondition'] == 'userGroupsIncludeAll') { - $conditionRule = new DiscountGroupConditionRule(); - $conditionRule->setValues($userGroupUids); - $conditionRule->operator = 'inAll'; - $userRules[] = $conditionRule; - } elseif ($discount['userGroupsCondition'] == 'userGroupsIncludeAny') { - $conditionRule = new DiscountGroupConditionRule(); - $conditionRule->setValues($userGroupUids); - $conditionRule->operator = 'in'; - $userRules[] = $conditionRule; - } elseif ($discount['userGroupsCondition'] == 'userGroupsExcludeAny') { - $conditionRule = new DiscountGroupConditionRule(); - $conditionRule->setValues($userGroupUids); - $conditionRule->operator = 'ni'; - $userRules[] = $conditionRule; - } - $customerCondition->setConditionRules($userRules); - } - - $this->update('{{%commerce_discounts}}', [ - 'customerCondition' => Json::encode($customerCondition->getConfig()), - ], ['id' => $id]); - - /** - * Shipping Address condition - */ - $this->update('{{%commerce_discounts}}', [ - 'shippingAddressCondition' => Json::encode($shippingAddressCondition->getConfig()), - ], ['id' => $id]); - - /** - * Billing Address condition - */ - $this->update('{{%commerce_discounts}}', [ - 'billingAddressCondition' => Json::encode($billingAddressCondition->getConfig()), - ], ['id' => $id]); - } - - // No longer needed now that we have the condition builder - $this->dropTableIfExists('{{%commerce_discount_usergroups}}'); - $this->dropColumn('{{%commerce_discounts}}', 'userGroupsCondition'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m220304_094835_discount_conditions cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m220308_221717_orderhistory_name.php b/src/migrations/m220308_221717_orderhistory_name.php deleted file mode 100644 index abc150be0c..0000000000 --- a/src/migrations/m220308_221717_orderhistory_name.php +++ /dev/null @@ -1,50 +0,0 @@ -db->getIsPgsql(); - - if (!$this->db->columnExists('{{%commerce_orderhistories}}', 'userName')) { - $this->addColumn('{{%commerce_orderhistories}}', 'userName', $this->string()); - } - - // Allow null - $this->dropForeignKeyIfExists('{{%commerce_orderhistories}}', ['userId']); - $this->dropIndexIfExists('{{%commerce_orderhistories}}', ['userId']); - $this->alterColumn('{{%commerce_orderhistories}}', 'userId', $this->integer()); - - if ($isPgsql) { - // Manually construct the SQL for Postgres - // (see https://github.com/yiisoft/yii2/issues/12077) - $this->execute('alter table {{%commerce_orderhistories}} alter column [[userId]] type integer, alter column [[userId]] drop not null'); - } else { - $this->alterColumn('{{%commerce_orderhistories}}', 'userId', $this->integer()->null()); - } - - $this->addForeignKey(null, '{{%commerce_orderhistories}}', ['userId'], '{{%elements}}', ['id'], 'SET NULL'); - $this->createIndex(null, '{{%commerce_orderhistories}}', 'userId', false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m220308_221717_orderhistory_name cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m220329_075053_convert_gateway_frontend_enabled_column.php b/src/migrations/m220329_075053_convert_gateway_frontend_enabled_column.php deleted file mode 100644 index 41b2cb1f3d..0000000000 --- a/src/migrations/m220329_075053_convert_gateway_frontend_enabled_column.php +++ /dev/null @@ -1,29 +0,0 @@ -alterColumn('{{%commerce_gateways}}', 'isFrontendEnabled', $this->string(500)->notNull()->defaultValue('1')); - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m220329_075053_convert_gateway_frontend_enabled_column cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m220706_132118_add_purchasable_tax_type.php b/src/migrations/m220706_132118_add_purchasable_tax_type.php deleted file mode 100644 index 7bd6e53360..0000000000 --- a/src/migrations/m220706_132118_add_purchasable_tax_type.php +++ /dev/null @@ -1,44 +0,0 @@ -db->getIsPgsql()) { - // Manually construct the SQL for Postgres - $check = '[[taxable]] in ('; - foreach ($values as $i => $value) { - if ($i != 0) { - $check .= ','; - } - $check .= $this->db->quoteValue($value); - } - $check .= ')'; - $this->execute("alter table {{%commerce_taxrates}} drop constraint {{%commerce_taxrates_taxable_check}}, add check ({$check})"); - } else { - $this->alterColumn('{{%commerce_taxrates}}', 'taxable', $this->enum('taxable', $values)); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m220706_132118_add_purchasable_tax_type cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m220812_104819_add_primary_payment_source_column.php b/src/migrations/m220812_104819_add_primary_payment_source_column.php deleted file mode 100644 index 69648d4aaf..0000000000 --- a/src/migrations/m220812_104819_add_primary_payment_source_column.php +++ /dev/null @@ -1,35 +0,0 @@ -db->columnExists('{{%commerce_customers}}', 'primaryPaymentSourceId')) { - $this->addColumn('{{%commerce_customers}}', 'primaryPaymentSourceId', $this->integer()->after('primaryShippingAddressId')); - $this->createIndex(null, '{{%commerce_customers}}', 'primaryPaymentSourceId', false); - - $this->addForeignKey(null, '{{%commerce_customers}}', ['primaryPaymentSourceId'], '{{%commerce_paymentsources}}', ['id'], 'SET NULL'); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m220812_104819_add_primary_payment_source_column cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m220817_135050_add_purchase_total_back_if_missing.php b/src/migrations/m220817_135050_add_purchase_total_back_if_missing.php deleted file mode 100644 index 72fd34b54f..0000000000 --- a/src/migrations/m220817_135050_add_purchase_total_back_if_missing.php +++ /dev/null @@ -1,32 +0,0 @@ -db->columnExists('{{%commerce_discounts}}', 'purchaseTotal')) { - $this->addColumn('{{%commerce_discounts}}', 'purchaseTotal', $this->decimal(14, 4)->notNull()->defaultValue(0)); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m220817_135050_add_purchase_total_back_if_missing cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m220912_111800_add_order_total_qty_column.php b/src/migrations/m220912_111800_add_order_total_qty_column.php deleted file mode 100644 index 7dc1f5660e..0000000000 --- a/src/migrations/m220912_111800_add_order_total_qty_column.php +++ /dev/null @@ -1,55 +0,0 @@ -db->columnExists('{{%commerce_orders}}', 'totalQty')) { - $this->addColumn('{{%commerce_orders}}', 'totalQty', $this->integer()->unsigned()); - - if ($this->db->getIsMysql()) { - $this->execute(' - UPDATE {{%commerce_orders}} o - LEFT JOIN ( - SELECT [[orderId]], SUM([[qty]]) AS [[totalQty]] - FROM {{%commerce_lineitems}} - GROUP BY [[orderId]] - ) agg ON agg.[[orderId]] = o.[[id]] - SET o.[[totalQty]] = COALESCE(agg.[[totalQty]], 0) - '); - } else { - $this->execute(' - UPDATE {{%commerce_orders}} o - SET [[totalQty]] = COALESCE(agg.[[totalQty]], 0) - FROM ( - SELECT [[orderId]], SUM([[qty]]) AS [[totalQty]] - FROM {{%commerce_lineitems}} - GROUP BY [[orderId]] - ) agg - WHERE o.[[id]] = agg.[[orderId]] - '); - } - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m220912_111800_add_order_total_qty_column cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m221025_083940_add_purchasables_stores_table.php b/src/migrations/m221025_083940_add_purchasables_stores_table.php deleted file mode 100644 index 8511730b50..0000000000 --- a/src/migrations/m221025_083940_add_purchasables_stores_table.php +++ /dev/null @@ -1,170 +0,0 @@ -select(['id']) - ->from([Table::STORES]) - ->limit(1) - ->orderBy(['id' => SORT_ASC]) - ->scalar(); - - // Variants - $variantsToPurchasables = (new Query()) - ->select([ - 'v.id', - 'v.width', - 'v.height', - 'v.length', - 'v.weight', - 'p.taxCategoryId', - 'p.shippingCategoryId', - ]) - ->from([Table::VARIANTS . ' v']) - ->innerJoin([Table::PRODUCTS . ' p'], '[[p.id]] = [[v.productId]]') - ->all(); - - $variantsToPurchasablesStores = collect((new Query()) - ->select([ - 'v.id as purchasableId', - 'pur.price as basePrice', - 'v.stock', - 'v.hasUnlimitedStock', - 'v.minQty', - 'v.maxQty', - 'p.promotable', - 'p.availableForPurchase', - 'p.freeShipping', - 'v.dateUpdated', - 'v.dateCreated', - ]) - ->from(['v' => Table::VARIANTS]) - ->innerJoin(['p' => Table::PRODUCTS], '[[p.id]] = [[v.productId]]') - ->innerJoin(['pur' => Table::PURCHASABLES], '[[pur.id]] = [[v.id]]') - ->all()); - - $customPurchasablesToPurchasablesStores = collect((new Query()) - ->select([ - 'id as purchasableId', - 'price as basePrice', - ]) - ->from(Table::PURCHASABLES) - ->where(['not', ['id' => (new Query()) - ->select(['id']) - ->from(Table::VARIANTS), ], - ]) - ->all()); - - if (!$this->db->columnExists(Table::PURCHASABLES, 'width')) { - $this->addColumn(Table::PURCHASABLES, 'width', $this->decimal(14, 4)); - } - if (!$this->db->columnExists(Table::PURCHASABLES, 'height')) { - $this->addColumn(Table::PURCHASABLES, 'height', $this->decimal(14, 4)); - } - if (!$this->db->columnExists(Table::PURCHASABLES, 'length')) { - $this->addColumn(Table::PURCHASABLES, 'length', $this->decimal(14, 4)); - } - if (!$this->db->columnExists(Table::PURCHASABLES, 'weight')) { - $this->addColumn(Table::PURCHASABLES, 'weight', $this->decimal(14, 4)); - } - if (!$this->db->columnExists(Table::PURCHASABLES, 'taxCategoryId')) { - $this->addColumn(Table::PURCHASABLES, 'taxCategoryId', $this->integer()); - } - if (!$this->db->columnExists(Table::PURCHASABLES, 'shippingCategoryId')) { - $this->addColumn(Table::PURCHASABLES, 'shippingCategoryId', $this->integer()); - } - - $this->addForeignKey(null, Table::PURCHASABLES, ['taxCategoryId'], Table::TAXCATEGORIES, ['id']); - $this->addForeignKey(null, Table::PURCHASABLES, ['shippingCategoryId'], Table::SHIPPINGCATEGORIES, ['id']); - - $this->createTable(Table::PURCHASABLES_STORES, [ - 'id' => $this->primaryKey(), - 'purchasableId' => $this->integer()->notNull(), - 'storeId' => $this->integer()->notNull(), - 'basePrice' => $this->decimal(14, 4), // @TODO Consider storing as string to avoid float-precision issues - 'basePromotionalPrice' => $this->decimal(14, 4), // @TODO Consider storing as string to avoid float-precision issues - 'promotable' => $this->boolean()->notNull()->defaultValue(false), - 'availableForPurchase' => $this->boolean()->notNull()->defaultValue(true), - 'freeShipping' => $this->boolean()->notNull()->defaultValue(true), - 'stock' => $this->integer(), - 'hasUnlimitedStock' => $this->boolean()->notNull()->defaultValue(false), - 'minQty' => $this->integer(), - 'maxQty' => $this->integer(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->addForeignKey(null, Table::PURCHASABLES_STORES, ['purchasableId'], Table::PURCHASABLES, ['id'], 'CASCADE'); - $this->addForeignKey(null, Table::PURCHASABLES_STORES, ['storeId'], Table::STORES, ['id'], 'CASCADE'); - - if (!empty($variantsToPurchasables)) { - foreach ($variantsToPurchasables as $variantsToPurchasable) { - $this->update(Table::PURCHASABLES, $variantsToPurchasable, ['id' => $variantsToPurchasable['id']]); - } - } - - if ($variantsToPurchasablesStores->isNotEmpty()) { - $variantsToPurchasablesStores->each(function($variantToPurchasableStore) use ($storeId) { - $variantToPurchasableStore['storeId'] = $storeId; - $this->insert(Table::PURCHASABLES_STORES, $variantToPurchasableStore); - }); - } - - if ($customPurchasablesToPurchasablesStores->isNotEmpty()) { - $customPurchasablesToPurchasablesStores->each(function($customPurchasableToPurchasableStore) use ($storeId) { - $customPurchasableToPurchasableStore['storeId'] = $storeId; - $this->insert(Table::PURCHASABLES_STORES, $customPurchasableToPurchasableStore); - }); - } - - $this->dropIndexIfExists(Table::VARIANTS, 'sku', false); - - $this->dropColumn(Table::VARIANTS, 'price'); - $this->dropColumn(Table::VARIANTS, 'width'); - $this->dropColumn(Table::VARIANTS, 'height'); - $this->dropColumn(Table::VARIANTS, 'length'); - $this->dropColumn(Table::VARIANTS, 'weight'); - $this->dropColumn(Table::VARIANTS, 'stock'); - $this->dropColumn(Table::VARIANTS, 'hasUnlimitedStock'); - $this->dropColumn(Table::VARIANTS, 'minQty'); - $this->dropColumn(Table::VARIANTS, 'maxQty'); - $this->dropColumn(Table::VARIANTS, 'sku'); - - $this->dropForeignKeyIfExists(Table::PRODUCTS, 'taxCategoryId'); - $this->dropForeignKeyIfExists(Table::PRODUCTS, 'shippingCategoryId'); - $this->dropIndexIfExists(Table::PRODUCTS, 'taxCategoryId', false); - $this->dropIndexIfExists(Table::PRODUCTS, 'shippingCategoryId', false); - - $this->dropColumn(Table::PRODUCTS, 'promotable'); - $this->dropColumn(Table::PRODUCTS, 'taxCategoryId'); - $this->dropColumn(Table::PRODUCTS, 'shippingCategoryId'); - $this->dropColumn(Table::PRODUCTS, 'availableForPurchase'); - $this->dropColumn(Table::PRODUCTS, 'freeShipping'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m221206_083940_add_purchasables_stores_table cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m221026_105212_add_catalog_pricing_table.php b/src/migrations/m221026_105212_add_catalog_pricing_table.php deleted file mode 100644 index cb4654102f..0000000000 --- a/src/migrations/m221026_105212_add_catalog_pricing_table.php +++ /dev/null @@ -1,132 +0,0 @@ -db->tableExists('{{%commerce_catalogpricingrules}}')) { - $this->createTable('{{%commerce_catalogpricingrules}}', [ - 'id' => $this->primaryKey(), - 'name' => $this->string()->notNull(), - 'description' => $this->text(), - 'storeId' => $this->integer()->notNull(), - 'dateFrom' => $this->dateTime(), - 'dateTo' => $this->dateTime(), - 'apply' => $this->enum('apply', ['toPercent', 'toFlat', 'byPercent', 'byFlat'])->notNull(), - 'applyAmount' => $this->decimal(14, 4)->notNull(), - 'applyPriceType' => $this->enum('applyPriceType', [CatalogPricingRule::APPLY_PRICE_TYPE_PRICE, CatalogPricingRule::APPLY_PRICE_TYPE_PROMOTIONAL_PRICE])->notNull(), - 'purchasableCondition' => $this->text(), - 'customerCondition' => $this->text(), - 'enabled' => $this->boolean()->notNull()->defaultValue(true), - 'isPromotionalPrice' => $this->boolean()->notNull()->defaultValue(false), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->createIndex(null, '{{%commerce_catalogpricingrules}}', 'storeId', false); - $this->addForeignKey(null, '{{%commerce_catalogpricingrules}}', ['storeId'], Table::STORES, ['id'], 'CASCADE'); - } - - if (!$this->db->tableExists('{{%commerce_catalogpricingrules_users}}')) { - $this->createTable('{{%commerce_catalogpricingrules_users}}', [ - 'id' => $this->primaryKey(), - 'catalogPricingRuleId' => $this->integer()->notNull(), - 'userId' => $this->integer()->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->createIndex(null, '{{%commerce_catalogpricingrules_users}}', 'catalogPricingRuleId', false); - $this->createIndex(null, '{{%commerce_catalogpricingrules_users}}', 'userId', false); - $this->addForeignKey(null, '{{%commerce_catalogpricingrules_users}}', ['userId'], \craft\db\Table::USERS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, '{{%commerce_catalogpricingrules_users}}', ['catalogPricingRuleId'], '{{%commerce_catalogpricingrules}}', ['id'], 'CASCADE', 'CASCADE'); - } - - if (!$this->db->tableExists($this->_tableName)) { - $this->createTable($this->_tableName, [ - 'id' => $this->primaryKey(), - 'price' => $this->decimal(14, 4), // @TODO Consider storing as string to avoid float-precision issues - 'purchasableId' => $this->integer()->notNull(), - 'storeId' => $this->integer(), - 'catalogPricingRuleId' => $this->integer(), - 'userId' => $this->integer(), - 'dateFrom' => $this->dateTime(), - 'dateTo' => $this->dateTime(), - 'isPromotionalPrice' => $this->boolean()->defaultValue(false), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->createIndex(null, $this->_tableName, 'purchasableId', false); - $this->createIndex(null, $this->_tableName, 'storeId', false); - $this->createIndex(null, $this->_tableName, 'catalogPricingRuleId', false); - $this->createIndex(null, $this->_tableName, 'userId', false); - - $this->addForeignKey(null, $this->_tableName, ['purchasableId'], Table::PURCHASABLES, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, $this->_tableName, ['storeId'], Table::STORES, ['id'], 'CASCADE'); - $this->addForeignKey(null, $this->_tableName, ['catalogPricingRuleId'], Table::CATALOG_PRICING_RULES, ['id'], 'CASCADE'); - $this->addForeignKey(null, $this->_tableName, ['userId'], \craft\db\Table::USERS, ['id'], 'CASCADE'); - } - - if ($this->db->columnExists('{{%commerce_purchasables}}', 'price')) { - $purchasablePrices = (new Query()) - ->select(['id as purchasableId', 'price', 'dateCreated', 'dateUpdated']) - ->from('{{%commerce_purchasables}}') - ->all(); - - if (!empty($purchasablePrices)) { - $storeId = (new Query()) - ->select(['id']) - ->from('{{%commerce_stores}}') - ->orderBy(['id' => SORT_ASC]) - ->scalar(); - - array_walk($purchasablePrices, function(&$purchasablePrice) use ($storeId) { - $purchasablePrice['storeId'] = $storeId; - $purchasablePrice['uid'] = StringHelper::UUID(); - }); - - // Chunk the insert to avoid memory issues with large datasets - $batchPurchasablePrices = array_chunk($purchasablePrices, 500); - - foreach ($batchPurchasablePrices as $batchPurchasablePrice) { - $this->batchInsert($this->_tableName, ['purchasableId', 'price', 'dateCreated', 'dateUpdated', 'storeId', 'uid'], $batchPurchasablePrice); - } - } - $this->dropColumn('{{%commerce_purchasables}}', 'price'); - } - - if ($this->db->columnExists('{{%commerce_variants}}', 'price')) { - $this->dropColumn('{{%commerce_variants}}', 'price'); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m221026_105212_add_catalog_pricing_table cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m221027_070322_add_tax_shipping_category_soft_delete.php b/src/migrations/m221027_070322_add_tax_shipping_category_soft_delete.php deleted file mode 100644 index 4fb4413e53..0000000000 --- a/src/migrations/m221027_070322_add_tax_shipping_category_soft_delete.php +++ /dev/null @@ -1,36 +0,0 @@ -db->columnExists('{{%commerce_taxcategories}}', 'dateDeleted')) { - $this->addColumn('{{%commerce_taxcategories}}', 'dateDeleted', $this->dateTime()->after('default')); - } - - if (!$this->db->columnExists('{{%commerce_shippingcategories}}', 'dateDeleted')) { - $this->addColumn('{{%commerce_shippingcategories}}', 'dateDeleted', $this->dateTime()->after('default')); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m221027_070322_add_tax_shipping_category_soft_delete cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m221027_074805_update_shipping_tax_category_indexes.php b/src/migrations/m221027_074805_update_shipping_tax_category_indexes.php deleted file mode 100644 index dfccd62737..0000000000 --- a/src/migrations/m221027_074805_update_shipping_tax_category_indexes.php +++ /dev/null @@ -1,31 +0,0 @@ -dropIndexIfExists('{{%commerce_taxcategories}}', 'handle', true); - $this->dropIndexIfExists('{{%commerce_shippingcategories}}', 'handle', true); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m221027_074805_update_shipping_tax_category_indexes cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m221028_192112_add_indexes_to_address_columns_on_orders.php b/src/migrations/m221028_192112_add_indexes_to_address_columns_on_orders.php deleted file mode 100644 index 475d9dac0f..0000000000 --- a/src/migrations/m221028_192112_add_indexes_to_address_columns_on_orders.php +++ /dev/null @@ -1,33 +0,0 @@ -createIndexIfMissing('{{%commerce_orders}}', 'billingAddressId', false); - $this->createIndexIfMissing('{{%commerce_orders}}', 'shippingAddressId', false); - $this->createIndexIfMissing('{{%commerce_orders}}', 'estimatedBillingAddressId', false); - $this->createIndexIfMissing('{{%commerce_orders}}', 'estimatedShippingAddressId', false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m221028_192112_add_indexes_to_address_columns_on_orders cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m221122_055724_move_general_settings_to_per_store_settings.php b/src/migrations/m221122_055724_move_general_settings_to_per_store_settings.php deleted file mode 100644 index e2a03bce68..0000000000 --- a/src/migrations/m221122_055724_move_general_settings_to_per_store_settings.php +++ /dev/null @@ -1,118 +0,0 @@ -db->columnExists(Table::STORES, 'autoSetNewCartAddresses')) { - $this->addColumn(Table::STORES, 'autoSetNewCartAddresses', $this->boolean()->notNull()->defaultValue(false)); - } - - if (!$this->db->columnExists(Table::STORES, 'autoSetCartShippingMethodOption')) { - $this->addColumn(Table::STORES, 'autoSetCartShippingMethodOption', $this->boolean()->notNull()->defaultValue(false)); - } - - if (!$this->db->columnExists(Table::STORES, 'autoSetPaymentSource')) { - $this->addColumn(Table::STORES, 'autoSetPaymentSource', $this->boolean()->notNull()->defaultValue(false)); - } - - if (!$this->db->columnExists(Table::STORES, 'allowEmptyCartOnCheckout')) { - $this->addColumn(Table::STORES, 'allowEmptyCartOnCheckout', $this->boolean()->notNull()->defaultValue(false)); - } - - if (!$this->db->columnExists(Table::STORES, 'allowCheckoutWithoutPayment')) { - $this->addColumn(Table::STORES, 'allowCheckoutWithoutPayment', $this->boolean()->notNull()->defaultValue(false)); - } - - if (!$this->db->columnExists(Table::STORES, 'allowPartialPaymentOnCheckout')) { - $this->addColumn(Table::STORES, 'allowPartialPaymentOnCheckout', $this->boolean()->notNull()->defaultValue(false)); - } - - if (!$this->db->columnExists(Table::STORES, 'requireShippingAddressAtCheckout')) { - $this->addColumn(Table::STORES, 'requireShippingAddressAtCheckout', $this->boolean()->notNull()->defaultValue(false)); - } - - if (!$this->db->columnExists(Table::STORES, 'requireBillingAddressAtCheckout')) { - $this->addColumn(Table::STORES, 'requireBillingAddressAtCheckout', $this->boolean()->notNull()->defaultValue(false)); - } - - if (!$this->db->columnExists(Table::STORES, 'requireShippingMethodSelectionAtCheckout')) { - $this->addColumn(Table::STORES, 'requireShippingMethodSelectionAtCheckout', $this->boolean()->notNull()->defaultValue(false)); - } - - if (!$this->db->columnExists(Table::STORES, 'useBillingAddressForTax')) { - $this->addColumn(Table::STORES, 'useBillingAddressForTax', $this->boolean()->notNull()->defaultValue(false)); - } - - if (!$this->db->columnExists(Table::STORES, 'validateOrganizationTaxIdAsVatId')) { - $this->addColumn(Table::STORES, 'validateOrganizationTaxIdAsVatId', $this->boolean()->notNull()->defaultValue(false)); - } - - if (!$this->db->columnExists(Table::STORES, 'orderReferenceFormat')) { - $this->addColumn(Table::STORES, 'orderReferenceFormat', $this->string()); - } - - if (!$this->db->columnExists(Table::STORES, 'freeOrderPaymentStrategy')) { - $this->addColumn(Table::STORES, 'freeOrderPaymentStrategy', $this->string()->defaultValue('complete')); - } - - if (!$this->db->columnExists(Table::STORES, 'minimumTotalPriceStrategy')) { - $this->addColumn(Table::STORES, 'minimumTotalPriceStrategy', $this->string()->defaultValue('default')); - } - - $projectConfig = Craft::$app->getProjectConfig(); - $commerceConfig = $projectConfig->get('plugins.commerce.settings', true); - $commerceFileConfig = Craft::$app->getConfig()->getConfigFromFile('commerce'); - - $commerceConfig = ArrayHelper::merge($commerceConfig, $commerceFileConfig); - - $data = [ - 'autoSetNewCartAddresses' => $commerceConfig['autoSetNewCartAddresses'] ?? false, - 'autoSetCartShippingMethodOption' => $commerceConfig['autoSetCartShippingMethodOption'] ?? false, - 'autoSetPaymentSource' => $commerceConfig['autoSetPaymentSource'] ?? false, - 'allowEmptyCartOnCheckout' => $commerceConfig['allowEmptyCartOnCheckout'] ?? false, - 'allowCheckoutWithoutPayment' => $commerceConfig['allowCheckoutWithoutPayment'] ?? false, - 'allowPartialPaymentOnCheckout' => $commerceConfig['allowPartialPaymentOnCheckout'] ?? false, - 'requireShippingAddressAtCheckout' => $commerceConfig['requireShippingAddressAtCheckout'] ?? false, - 'requireBillingAddressAtCheckout' => $commerceConfig['requireBillingAddressAtCheckout'] ?? false, - 'requireShippingMethodSelectionAtCheckout' => $commerceConfig['requireShippingMethodSelectionAtCheckout'] ?? false, - 'useBillingAddressForTax' => $commerceConfig['useBillingAddressForTax'] ?? false, - 'validateOrganizationTaxIdAsVatId' => $commerceConfig['validateOrganizationTaxIdAsVatId'] ?? $commerceConfig['validateBusinessTaxIdAsVatId'] ?? false, - 'orderReferenceFormat' => $commerceConfig['orderReferenceFormat'] ?? '{{number[:7]}}', - 'freeOrderPaymentStrategy' => $commerceConfig['freeOrderPaymentStrategy'] ?? 'complete', - 'minimumTotalPriceStrategy' => $commerceConfig['minimumTotalPriceStrategy'] ?? 'default', - ]; - - // set on all rows is safe since we only have one store - $this->update(Table::STORES, $data); - - // No need to update the project config as we only have one store at this stage and the multi-store migration - // will handle this. - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230324_080923_move_general_settings_to_per_store_settings cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m221122_055725_multi_store.php b/src/migrations/m221122_055725_multi_store.php deleted file mode 100644 index 6642694cd0..0000000000 --- a/src/migrations/m221122_055725_multi_store.php +++ /dev/null @@ -1,137 +0,0 @@ -db->tableExists(Table::STORESETTINGS)) { - $this->createTable(Table::STORESETTINGS, [ - 'id' => $this->integer()->notNull(), - 'locationAddressId' => $this->integer(), - 'countries' => $this->text(), - 'marketAddressCondition' => $this->text(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - 'PRIMARY KEY([[id]])', - ]); - - $this->addForeignKey(null, Table::STORESETTINGS, ['id'], Table::STORES, ['id'], 'CASCADE', 'CASCADE'); - } - - // get store settings from db - $storeSettings = (new Query()) - ->select(['id', 'locationAddressId', 'countries', 'marketAddressCondition']) - ->from([Table::STORES]) - ->one(); - - // Add the store settings from the old stores table - $this->insert(Table::STORESETTINGS, $storeSettings); - - $this->dropColumn(Table::STORES, 'locationAddressId'); - $this->dropColumn(Table::STORES, 'countries'); - $this->dropColumn(Table::STORES, 'marketAddressCondition'); - - // if column doesnt exist - if (!$this->db->columnExists(Table::STORES, 'name')) { - $this->addColumn(Table::STORES, 'name', $this->string()->defaultValue('')->notNull()); - } - if (!$this->db->columnExists(Table::STORES, 'handle')) { - $this->addColumn(Table::STORES, 'handle', $this->string()->defaultValue('')->notNull()); - } - if (!$this->db->columnExists(Table::STORES, 'primary')) { - $this->addColumn(Table::STORES, 'primary', $this->boolean()->defaultValue(false)->notNull()); - } - - $config = [ - 'name' => 'Primary Store', - 'handle' => 'primaryStore', - 'primary' => true, - ]; - - $this->update(table: Table::STORES, - columns: $config, - condition: ['id' => $storeSettings['id']], - updateTimestamp: false - ); - - $configKeys = [ - 'name', - 'handle', - 'primary', - 'allowCheckoutWithoutPayment', - 'allowEmptyCartOnCheckout', - 'allowPartialPaymentOnCheckout', - 'autoSetCartShippingMethodOption', - 'autoSetNewCartAddresses', - 'autoSetPaymentSource', - 'freeOrderPaymentStrategy', - 'minimumTotalPriceStrategy', - 'orderReferenceFormat', - 'requireBillingAddressAtCheckout', - 'requireShippingAddressAtCheckout', - 'requireShippingMethodSelectionAtCheckout', - 'useBillingAddressForTax', - 'validateOrganizationTaxIdAsVatId', - ]; - - $config = (new Query()) - ->select($configKeys) - ->from([Table::STORES]) - ->one(); - - $this->update(table: Table::STORES, - columns: $config, - condition: ['id' => $storeSettings['id']], - updateTimestamp: false - ); - - $storeUid = (new Query()) - ->select(['uid']) - ->from([Table::STORES]) - ->scalar(); - - - // Make project config updates - $projectConfig = Craft::$app->getProjectConfig(); - - $originalValue = $projectConfig->muteEvents; - $projectConfig->muteEvents = true; - - $projectConfig->set(Stores::CONFIG_STORES_KEY . '.' . $storeUid, - $config, - 'Migration creating the initial primary store in the project config'); - - $projectConfig->muteEvents = $originalValue; - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m221122_055725_multi_store cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m221122_155735_update_orders_shippingMethodHandle_default.php b/src/migrations/m221122_155735_update_orders_shippingMethodHandle_default.php deleted file mode 100644 index ac70f359d9..0000000000 --- a/src/migrations/m221122_155735_update_orders_shippingMethodHandle_default.php +++ /dev/null @@ -1,55 +0,0 @@ -update( - Table::ORDERS, - ['shippingMethodHandle' => ''], - ['shippingMethodHandle' => null], - updateTimestamp: false, - ); - - $this->update( - Table::ORDERS, - ['shippingMethodName' => ''], - ['shippingMethodName' => null], - updateTimestamp: false, - ); - - if ($this->db->getIsPgsql()) { - // Manually construct the SQL for Postgres - // (see https://github.com/yiisoft/yii2/issues/12077) - $this->execute(sprintf('ALTER TABLE %s ALTER COLUMN [[shippingMethodHandle]] SET NOT NULL', Table::ORDERS)); - $this->execute(sprintf("ALTER TABLE %s ALTER COLUMN [[shippingMethodHandle]] SET DEFAULT ''", Table::ORDERS)); - $this->execute(sprintf('ALTER TABLE %s ALTER COLUMN [[shippingMethodName]] SET NOT NULL', Table::ORDERS)); - $this->execute(sprintf("ALTER TABLE %s ALTER COLUMN [[shippingMethodName]] SET DEFAULT ''", Table::ORDERS)); - } else { - $this->alterColumn(Table::ORDERS, 'shippingMethodHandle', $this->string()->notNull()->defaultValue('')); - $this->alterColumn(Table::ORDERS, 'shippingMethodName', $this->string()->notNull()->defaultValue('')); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m221122_155735_update_orders_shippingMethodHandle_default cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m221124_114239_add_date_deleted_to_stores.php b/src/migrations/m221124_114239_add_date_deleted_to_stores.php deleted file mode 100644 index 7e2cfaff9e..0000000000 --- a/src/migrations/m221124_114239_add_date_deleted_to_stores.php +++ /dev/null @@ -1,33 +0,0 @@ -db->columnExists('{{%commerce_stores}}', 'dateDeleted')) { - $this->addColumn('{{%commerce_stores}}', 'dateDeleted', $this->dateTime()->null()); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m221124_114239_add_date_deleted_to_stores cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m221206_094303_add_store_to_order.php b/src/migrations/m221206_094303_add_store_to_order.php deleted file mode 100644 index 401334b410..0000000000 --- a/src/migrations/m221206_094303_add_store_to_order.php +++ /dev/null @@ -1,43 +0,0 @@ -select(['id']) - ->from(['{{%commerce_stores}}']) - ->where(['primary' => true]) - ->scalar(); - - // Add storeId to order table - if (!$this->db->columnExists('{{%commerce_orders}}', 'storeId')) { - $this->addColumn('{{%commerce_orders}}', 'storeId', $this->integer()->after('id')->defaultValue($primaryStoreId)->notNull()); - $this->addForeignKey(null, '{{%commerce_orders}}', ['storeId'], '{{%commerce_stores}}', ['id'], 'CASCADE', 'CASCADE'); - $this->createIndex(null, '{{%commerce_orders}}', ['storeId'], false); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m221206_094303_add_store_to_order cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m221213_052623_drop_lite.php b/src/migrations/m221213_052623_drop_lite.php deleted file mode 100644 index d0c787d5df..0000000000 --- a/src/migrations/m221213_052623_drop_lite.php +++ /dev/null @@ -1,38 +0,0 @@ -db->columnExists('{{%commerce_shippingmethods}}', 'isLite')) { - $this->dropColumn('{{%commerce_shippingmethods}}', 'isLite'); - } - if ($this->db->columnExists('{{%commerce_taxrates}}', 'isLite')) { - $this->dropColumn('{{%commerce_taxrates}}', 'isLite'); - } - if ($this->db->columnExists('{{%commerce_shippingrules}}', 'isLite')) { - $this->dropColumn('{{%commerce_shippingrules}}', 'isLite'); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m221213_052623_drop_lite cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m221213_070807_initial_storeId_records_transition.php b/src/migrations/m221213_070807_initial_storeId_records_transition.php deleted file mode 100644 index a17793350e..0000000000 --- a/src/migrations/m221213_070807_initial_storeId_records_transition.php +++ /dev/null @@ -1,53 +0,0 @@ -select(['id']) - ->from(['{{%commerce_stores}}']) - ->where(['primary' => true]) - ->scalar(); - - if (!$this->db->columnExists('{{%commerce_paymentcurrencies}}', 'storeId')) { - $this->addColumn('{{%commerce_paymentcurrencies}}', 'storeId', $this->integer()->after('id')->defaultValue($primaryStoreId)->notNull()); - $this->addForeignKey(null, '{{%commerce_paymentcurrencies}}', ['storeId'], '{{%commerce_stores}}', ['id'], 'CASCADE', 'CASCADE'); - $this->createIndex(null, '{{%commerce_paymentcurrencies}}', ['storeId'], false); - } - - if (!$this->db->columnExists('{{%commerce_donations}}', 'storeId')) { - $this->addColumn('{{%commerce_donations}}', 'storeId', $this->integer()->after('id')->defaultValue($primaryStoreId)->notNull()); - $this->addForeignKey(null, '{{%commerce_donations}}', ['storeId'], '{{%commerce_stores}}', ['id'], 'CASCADE', 'CASCADE'); - $this->createIndex(null, '{{%commerce_donations}}', ['storeId'], false); - } - - if (!$this->db->columnExists('{{%commerce_discounts}}', 'storeId')) { - $this->addColumn('{{%commerce_discounts}}', 'storeId', $this->integer()->after('id')->defaultValue($primaryStoreId)->notNull()); - $this->addForeignKey(null, '{{%commerce_discounts}}', ['storeId'], '{{%commerce_stores}}', ['id'], 'CASCADE', 'CASCADE'); - $this->createIndex(null, '{{%commerce_discounts}}', ['storeId'], false); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m221213_070807_initial_storeId_records_transition cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230103_122549_add_product_type_max_variants.php b/src/migrations/m230103_122549_add_product_type_max_variants.php deleted file mode 100755 index 605933eb70..0000000000 --- a/src/migrations/m230103_122549_add_product_type_max_variants.php +++ /dev/null @@ -1,68 +0,0 @@ -db->columnExists('{{%commerce_producttypes}}', 'maxVariants')) { - $this->addColumn('{{%commerce_producttypes}}', 'maxVariants', $this->integer()); - } - - if ($this->db->columnExists('{{%commerce_producttypes}}', 'hasVariants')) { - $this->update('{{%commerce_producttypes}}', ['maxVariants' => 1], ['hasVariants' => false]); - - $this->updateProjectConfig(); - - $this->dropColumn('{{%commerce_producttypes}}', 'hasVariants'); - } - - return true; - } - - private function updateProjectConfig(): void - { - $projectConfig = Craft::$app->getProjectConfig(); - - $projectConfig->muteEvents = true; - - $maxVariantProductTypes = (new Query()) - ->select(['id', 'maxVariants', 'uid']) - ->from(['{{%commerce_producttypes}}']) - ->all(); - - foreach ($maxVariantProductTypes as $productType) { - $config = $projectConfig->get(ProductTypes::CONFIG_PRODUCTTYPES_KEY . '.' . $productType['uid']); - if (array_key_exists('hasVariants', $config)) { - unset($config['hasVariants']); - } - - $config['maxVariants'] = $productType['maxVariants']; - $projectConfig->set(ProductTypes::CONFIG_PRODUCTTYPES_KEY . '.' . $productType['uid'], $config); - } - - $projectConfig->muteEvents = false; - } - - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230103_122549_add_product_type_max_variants cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230110_052712_site_stores.php b/src/migrations/m230110_052712_site_stores.php deleted file mode 100644 index 94ecf42343..0000000000 --- a/src/migrations/m230110_052712_site_stores.php +++ /dev/null @@ -1,74 +0,0 @@ -createTable('{{%commerce_site_stores}}', [ - 'siteId' => $this->integer(), - 'storeId' => $this->integer()->null(), // defaults to primary store in app - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - 'PRIMARY KEY([[siteId]])', - ]); - - // Get the primary store - $primaryStore = (new Query()) - ->select(['id', 'uid']) - ->from([Table::STORES]) - ->where(['primary' => true]) - ->one(); - - // Get all sites - $sites = (new Query()) - ->select(['id', 'handle', 'uid']) - ->from(\craft\db\Table::SITES) - ->where(['dateDeleted' => null]) - ->all(); - - // Create site stores records - foreach ($sites as $site) { - $this->insert('{{%commerce_site_stores}}', [ - 'siteId' => $site['id'], - 'storeId' => $primaryStore['id'], - 'uid' => $site['uid'], - ]); - - $projectConfig = \Craft::$app->getProjectConfig(); - - $configPath = Stores::CONFIG_SITESTORES_KEY . "." . $site['uid']; - $projectConfig->set( - $configPath, - // Mirror what the site store model `getConfig()` method returns - ['store' => $primaryStore['uid']], - "Save the “{$site['handle']}” commerce site store mapping" - ); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230110_052712_site_stores cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230111_112916_update_lineitems_table.php b/src/migrations/m230111_112916_update_lineitems_table.php deleted file mode 100755 index fd195a39ad..0000000000 --- a/src/migrations/m230111_112916_update_lineitems_table.php +++ /dev/null @@ -1,33 +0,0 @@ -addColumn(Table::LINEITEMS, 'promotionalPrice', $this->decimal(14, 4)->after('price')->null()->unsigned()); - - $this->renameColumn(Table::LINEITEMS, 'saleAmount', 'promotionalAmount'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230111_112916_update_lineitems_table cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230113_110914_remove_soft_delete.php b/src/migrations/m230113_110914_remove_soft_delete.php deleted file mode 100644 index 195e3a2bc4..0000000000 --- a/src/migrations/m230113_110914_remove_soft_delete.php +++ /dev/null @@ -1,32 +0,0 @@ -db->columnExists('{{%commerce_stores}}', 'dateDeleted')) { - $this->dropColumn('{{%commerce_stores}}', 'dateDeleted'); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230113_110914_remove_soft_delete cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230118_114424_add_purchasables_stores_indexes.php b/src/migrations/m230118_114424_add_purchasables_stores_indexes.php deleted file mode 100755 index 81d3a7590c..0000000000 --- a/src/migrations/m230118_114424_add_purchasables_stores_indexes.php +++ /dev/null @@ -1,32 +0,0 @@ -createIndexIfMissing(Table::PURCHASABLES_STORES, ['purchasableId']); - $this->createIndexIfMissing(Table::PURCHASABLES_STORES, ['storeId']); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230118_114424_add_purchasables_stores_indexes cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230126_105337_rename_discount_sales_references.php b/src/migrations/m230126_105337_rename_discount_sales_references.php deleted file mode 100755 index 51e54ce67c..0000000000 --- a/src/migrations/m230126_105337_rename_discount_sales_references.php +++ /dev/null @@ -1,32 +0,0 @@ -renameColumn(Table::DISCOUNTS, 'excludeOnSale', 'excludeOnPromotion'); - $this->renameColumn(Table::DISCOUNTS, 'ignoreSales', 'ignorePromotions'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230126_105337_rename_discount_sales_references cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230126_114655_add_catalog_pricing_rule_metadata_column.php b/src/migrations/m230126_114655_add_catalog_pricing_rule_metadata_column.php deleted file mode 100755 index a0fd28c4e0..0000000000 --- a/src/migrations/m230126_114655_add_catalog_pricing_rule_metadata_column.php +++ /dev/null @@ -1,31 +0,0 @@ -addColumn(Table::CATALOG_PRICING_RULES, 'metadata', $this->text()); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230126_114655_add_catalog_pricing_rule_metadata_column cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230208_130445_add_store_id_to_shipping_categories.php b/src/migrations/m230208_130445_add_store_id_to_shipping_categories.php deleted file mode 100644 index 3a8de0c9a4..0000000000 --- a/src/migrations/m230208_130445_add_store_id_to_shipping_categories.php +++ /dev/null @@ -1,41 +0,0 @@ -addColumn(Table::SHIPPINGCATEGORIES, 'storeId', $this->integer()); - $this->createIndex(null, Table::SHIPPINGCATEGORIES, ['storeId'], false); - $this->addForeignKey(null, Table::SHIPPINGCATEGORIES, ['storeId'], Table::STORES, ['id'], 'CASCADE', null); - - $primaryStoreId = (new Query()) - ->select(['id']) - ->from(Table::STORES) - ->where(['primary' => true]) - ->scalar(); - $this->update(Table::SHIPPINGCATEGORIES, ['storeId' => $primaryStoreId]); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230208_130445_add_store_id_to_shipping_categories cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230210_093749_add_store_id_to_shipping_methods.php b/src/migrations/m230210_093749_add_store_id_to_shipping_methods.php deleted file mode 100644 index 58504ebdc0..0000000000 --- a/src/migrations/m230210_093749_add_store_id_to_shipping_methods.php +++ /dev/null @@ -1,41 +0,0 @@ -addColumn(Table::SHIPPINGMETHODS, 'storeId', $this->integer()); - $this->createIndex(null, Table::SHIPPINGMETHODS, ['storeId'], false); - $this->addForeignKey(null, Table::SHIPPINGMETHODS, ['storeId'], Table::STORES, ['id'], 'CASCADE', null); - - $primaryStoreId = (new Query()) - ->select(['id']) - ->from(Table::STORES) - ->where(['primary' => true]) - ->scalar(); - $this->update(Table::SHIPPINGMETHODS, ['storeId' => $primaryStoreId]); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230210_093749_add_store_id_to_shipping_methods cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230210_141514_add_store_id_to_shipping_zones.php b/src/migrations/m230210_141514_add_store_id_to_shipping_zones.php deleted file mode 100644 index 518416776d..0000000000 --- a/src/migrations/m230210_141514_add_store_id_to_shipping_zones.php +++ /dev/null @@ -1,41 +0,0 @@ -addColumn(Table::SHIPPINGZONES, 'storeId', $this->integer()); - $this->createIndex(null, Table::SHIPPINGZONES, ['storeId'], false); - $this->addForeignKey(null, Table::SHIPPINGZONES, ['storeId'], Table::STORES, ['id'], 'CASCADE', null); - - $primaryStoreId = (new Query()) - ->select(['id']) - ->from(Table::STORES) - ->where(['primary' => true]) - ->scalar(); - $this->update(Table::SHIPPINGZONES, ['storeId' => $primaryStoreId]); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230210_141514_add_store_id_to_shipping_zones cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230214_094122_add_total_weight_column_to_orders.php b/src/migrations/m230214_094122_add_total_weight_column_to_orders.php deleted file mode 100644 index 4b453f7779..0000000000 --- a/src/migrations/m230214_094122_add_total_weight_column_to_orders.php +++ /dev/null @@ -1,54 +0,0 @@ -addColumn(Table::ORDERS, 'totalWeight', $this->decimal(14, 4)->defaultValue(0)->unsigned()); - - if ($this->db->getIsMysql()) { - $this->execute(' - UPDATE ' . Table::ORDERS . ' o - LEFT JOIN ( - SELECT [[orderId]], SUM([[weight]]) AS [[totalWeight]] - FROM ' . Table::LINEITEMS . ' - GROUP BY [[orderId]] - ) agg ON agg.[[orderId]] = o.[[id]] - SET o.[[totalWeight]] = COALESCE(agg.[[totalWeight]], 0) - '); - } else { - $this->execute(' - UPDATE ' . Table::ORDERS . ' o - SET [[totalWeight]] = COALESCE(agg.[[totalWeight]], 0) - FROM ( - SELECT [[orderId]], SUM([[weight]]) AS [[totalWeight]] - FROM ' . Table::LINEITEMS . ' - GROUP BY [[orderId]] - ) agg - WHERE o.[[id]] = agg.[[orderId]] - '); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230214_094122_add_total_weight_column_to_orders cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230214_095055_update_name_index_on_shipping_zones.php b/src/migrations/m230214_095055_update_name_index_on_shipping_zones.php deleted file mode 100644 index 8e216063f7..0000000000 --- a/src/migrations/m230214_095055_update_name_index_on_shipping_zones.php +++ /dev/null @@ -1,33 +0,0 @@ -getDb()); - $this->createIndex(null, Table::SHIPPINGZONES, ['name'], false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230214_095055_update_name_index_on_shipping_zones cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230215_083820_add_order_condition_to_shipping_rules.php b/src/migrations/m230215_083820_add_order_condition_to_shipping_rules.php deleted file mode 100644 index f6a76e6eb1..0000000000 --- a/src/migrations/m230215_083820_add_order_condition_to_shipping_rules.php +++ /dev/null @@ -1,31 +0,0 @@ -addColumn(Table::SHIPPINGRULES, 'orderCondition', $this->text()); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230215_083820_add_order_condition_to_shipping_rules cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230215_114552_migrate_shipping_rule_conditions_to_condition_builder.php b/src/migrations/m230215_114552_migrate_shipping_rule_conditions_to_condition_builder.php deleted file mode 100644 index a9d6b946aa..0000000000 --- a/src/migrations/m230215_114552_migrate_shipping_rule_conditions_to_condition_builder.php +++ /dev/null @@ -1,130 +0,0 @@ -select([ - 'id', - 'minQty', - 'maxQty', - 'minTotal', - 'maxTotal', - 'minMaxTotalType', - 'minWeight', - 'maxWeight', - 'shippingZoneId', - ]) - ->from(Table::SHIPPINGRULES) - ->all(); - - if (empty($shippingRules)) { - return true; - } - - $primaryStoreId = (new Query()) - ->select(['id']) - ->from(Table::STORES) - ->where(['primary' => true]) - ->scalar(); - - foreach ($shippingRules as $shippingRule) { - $orderCondition = new ShippingRuleOrderCondition(); - $orderCondition->storeId = $primaryStoreId; - - // Convert min/max qty to order condition rule - if ($shippingRule['minQty'] > 0 || $shippingRule['maxQty'] > 0) { - $orderCondition = $this->_setConditionRule(new TotalQtyConditionRule(), $orderCondition, $shippingRule['minQty'], $shippingRule['maxQty'], true); - } - - // Convert min/max item subtotal to condition rule - if ($shippingRule['minMaxTotalType'] === 'salePrice' && ($shippingRule['minTotal'] > 0 || $shippingRule['maxTotal'] > 0)) { - $orderCondition = $this->_setConditionRule(new ItemSubtotalConditionRule(), $orderCondition, $shippingRule['minTotal'], $shippingRule['maxTotal']); - } - - // Convert min/max item subtotal with discounts to condition rule - if ($shippingRule['minMaxTotalType'] === 'salePriceWithDiscounts' && ($shippingRule['minTotal'] > 0 || $shippingRule['maxTotal'] > 0)) { - $orderCondition = $this->_setConditionRule(new DiscountedItemSubtotalConditionRule(), $orderCondition, $shippingRule['minTotal'], $shippingRule['maxTotal']); - } - - // Convert min/max total weight to condition rule - if ($shippingRule['minWeight'] > 0 || $shippingRule['maxWeight'] > 0) { - $orderCondition = $this->_setConditionRule(new TotalWeightConditionRule(), $orderCondition, $shippingRule['minWeight'], $shippingRule['maxWeight']); - } - - // Convert shipping zone to condition rule - if ($shippingRule['shippingZoneId']) { - $rule = new ShippingAddressZoneConditionRule(); - $rule->values = [$shippingRule['shippingZoneId']]; - - $orderCondition->addConditionRule($rule); - } - - // Update shipping rule - if (!empty($orderCondition->getConditionRules())) { - $this->update(Table::SHIPPINGRULES, [ - 'orderCondition' => Db::prepareValueForDb($orderCondition->getConfig()), - ], [ - 'id' => $shippingRule['id'], - ]); - } - } - - return true; - } - - /** - * @param OrderValuesAttributeConditionRule|OrderCurrencyValuesAttributeConditionRule $rule - * @param ShippingRuleOrderCondition $orderCondition - * @param bool $adjustValues - * @return ShippingRuleOrderCondition - */ - private function _setConditionRule(OrderValuesAttributeConditionRule|OrderCurrencyValuesAttributeConditionRule $rule, ShippingRuleOrderCondition $orderCondition, mixed $min, mixed $max, bool $adjustValues = false): ShippingRuleOrderCondition - { - // Write this manually because at the moment the operator constants are all protected and not public - $rule->operator = 'between'; - - if ($max > 0) { - $rule->maxValue = $adjustValues ? $max - 1 : $max; - } - - if ($min > 0) { - $rule->value = $adjustValues ? $min - 1 : $min; - } - - $orderCondition->addConditionRule($rule); - - return $orderCondition; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230215_114552_migrate_shipping_rule_conditions_to_condition_builder cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230217_095845_remove_shipping_rules_columns.php b/src/migrations/m230217_095845_remove_shipping_rules_columns.php deleted file mode 100644 index 8ba1ef0cc8..0000000000 --- a/src/migrations/m230217_095845_remove_shipping_rules_columns.php +++ /dev/null @@ -1,40 +0,0 @@ -dropColumn(Table::SHIPPINGRULES, 'minQty'); - $this->dropColumn(Table::SHIPPINGRULES, 'maxQty'); - $this->dropColumn(Table::SHIPPINGRULES, 'minTotal'); - $this->dropColumn(Table::SHIPPINGRULES, 'maxTotal'); - $this->dropColumn(Table::SHIPPINGRULES, 'minMaxTotalType'); - $this->dropColumn(Table::SHIPPINGRULES, 'minWeight'); - $this->dropColumn(Table::SHIPPINGRULES, 'maxWeight'); - - $this->dropForeignKeyIfExists(Table::SHIPPINGRULES, 'shippingZoneId'); - $this->dropIndexIfExists(Table::SHIPPINGRULES, 'shippingZoneId'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230217_095845_remove_shipping_rules_columns cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230217_143255_add_shipping_method_order_condition.php b/src/migrations/m230217_143255_add_shipping_method_order_condition.php deleted file mode 100644 index 0d51916687..0000000000 --- a/src/migrations/m230217_143255_add_shipping_method_order_condition.php +++ /dev/null @@ -1,31 +0,0 @@ -addColumn(Table::SHIPPINGMETHODS, 'orderCondition', $this->text()); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230217_143255_add_shipping_method_order_condition cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230220_075106_add_store_id_to_tax_rates.php b/src/migrations/m230220_075106_add_store_id_to_tax_rates.php deleted file mode 100644 index 11cf290af9..0000000000 --- a/src/migrations/m230220_075106_add_store_id_to_tax_rates.php +++ /dev/null @@ -1,43 +0,0 @@ -addColumn(Table::TAXRATES, 'storeId', $this->integer()); - - $primaryStoreId = (new Query()) - ->select(['id']) - ->from(Table::STORES) - ->where(['primary' => true]) - ->scalar(); - - $this->update(Table::TAXRATES, ['storeId' => $primaryStoreId], ['storeId' => null], [], false); - - $this->addForeignKey(null, Table::TAXRATES, ['storeId'], Table::STORES, ['id'], 'CASCADE', null); - $this->createIndex(null, Table::TAXRATES, ['storeId'], false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230220_075106_add_store_id_to_tax_rates cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230220_080107_add_store_id_to_tax_zones.php b/src/migrations/m230220_080107_add_store_id_to_tax_zones.php deleted file mode 100644 index f9db699a4d..0000000000 --- a/src/migrations/m230220_080107_add_store_id_to_tax_zones.php +++ /dev/null @@ -1,43 +0,0 @@ -addColumn(Table::TAXZONES, 'storeId', $this->integer()); - - $primaryStoreId = (new Query()) - ->select(['id']) - ->from(Table::STORES) - ->where(['primary' => true]) - ->scalar(); - - $this->update(Table::TAXZONES, ['storeId' => $primaryStoreId], ['storeId' => null], [], false); - - $this->addForeignKey(null, Table::TAXZONES, ['storeId'], Table::STORES, ['id'], 'CASCADE', null); - $this->createIndex(null, Table::TAXZONES, ['storeId'], false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230220_080107_add_store_id_to_tax_zones cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230307_091520_add_sort_order_to_stores.php b/src/migrations/m230307_091520_add_sort_order_to_stores.php deleted file mode 100644 index 93ec499f7d..0000000000 --- a/src/migrations/m230307_091520_add_sort_order_to_stores.php +++ /dev/null @@ -1,33 +0,0 @@ -addColumn(Table::STORES, 'sortOrder', $this->integer()); - - $this->update(Table::STORES, ['sortOrder' => 1], ['primary' => true], [], false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230307_091520_add_sort_order_to_stores cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230308_084340_add_store_id_to_order_statuses.php b/src/migrations/m230308_084340_add_store_id_to_order_statuses.php deleted file mode 100644 index 72e1512014..0000000000 --- a/src/migrations/m230308_084340_add_store_id_to_order_statuses.php +++ /dev/null @@ -1,57 +0,0 @@ -addColumn(Table::ORDERSTATUSES, 'storeId', $this->integer()); - - $primaryStore = (new Query()) - ->select(['id', 'uid']) - ->from(Table::STORES) - ->where(['primary' => true]) - ->one(); - - $this->update(Table::ORDERSTATUSES, ['storeId' => $primaryStore['id']], ['storeId' => null], [], false); - - $this->addForeignKey(null, Table::ORDERSTATUSES, ['storeId'], Table::STORES, ['id'], 'CASCADE', null); - $this->createIndex(null, Table::ORDERSTATUSES, ['storeId'], false); - - $projectConfig = Craft::$app->getProjectConfig(); - - $orderStatuses = $projectConfig->get('commerce.orderStatuses') ?? []; - $muteEvents = $projectConfig->muteEvents; - $projectConfig->muteEvents = true; - - foreach ($orderStatuses as $statusUid => $orderStatus) { - $orderStatus['store'] = $primaryStore['uid']; - $projectConfig->set("commerce.orderStatuses.$statusUid", $orderStatus); - } - - $projectConfig->muteEvents = $muteEvents; - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230308_084340_add_store_id_to_order_statuses cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230310_102639_add_store_id_to_line_item_statuses.php b/src/migrations/m230310_102639_add_store_id_to_line_item_statuses.php deleted file mode 100644 index 45879eed15..0000000000 --- a/src/migrations/m230310_102639_add_store_id_to_line_item_statuses.php +++ /dev/null @@ -1,57 +0,0 @@ -addColumn(Table::LINEITEMSTATUSES, 'storeId', $this->integer()); - - $primaryStore = (new Query()) - ->select(['id', 'uid']) - ->from(Table::STORES) - ->where(['primary' => true]) - ->one(); - - $this->update(Table::LINEITEMSTATUSES, ['storeId' => $primaryStore['id']], ['storeId' => null], [], false); - - $this->addForeignKey(null, Table::LINEITEMSTATUSES, ['storeId'], Table::STORES, ['id'], 'CASCADE', null); - $this->createIndex(null, Table::LINEITEMSTATUSES, ['storeId'], false); - - $projectConfig = Craft::$app->getProjectConfig(); - - $lineItemStatuses = $projectConfig->get('commerce.lineItemStatuses') ?? []; - $muteEvents = $projectConfig->muteEvents; - $projectConfig->muteEvents = true; - - foreach ($lineItemStatuses as $statusUid => $lineItemStatus) { - $lineItemStatus['store'] = $primaryStore['uid']; - $projectConfig->set("commerce.lineItemStatuses.$statusUid", $lineItemStatus); - } - - $projectConfig->muteEvents = $muteEvents; - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230310_102639_add_store_id_to_line_item_statuses cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230313_095359_add_store_id_to_emails.php b/src/migrations/m230313_095359_add_store_id_to_emails.php deleted file mode 100644 index 7c5e1f8127..0000000000 --- a/src/migrations/m230313_095359_add_store_id_to_emails.php +++ /dev/null @@ -1,58 +0,0 @@ -addColumn(Table::EMAILS, 'storeId', $this->integer()); - - $primaryStore = (new Query()) - ->select(['id', 'uid']) - ->from(Table::STORES) - ->where(['primary' => true]) - ->one(); - - $this->update(Table::EMAILS, ['storeId' => $primaryStore['id']], ['storeId' => null], [], false); - - $this->addForeignKey(null, Table::EMAILS, ['storeId'], Table::STORES, ['id'], 'CASCADE', null); - $this->createIndex(null, Table::EMAILS, ['storeId'], false); - - $projectConfig = Craft::$app->getProjectConfig(); - - $emails = $projectConfig->get('commerce.emails') ?? []; - $muteEvents = $projectConfig->muteEvents; - $projectConfig->muteEvents = true; - - foreach ($emails as $emailUid => $email) { - $email['store'] = $primaryStore['uid']; - $projectConfig->set("commerce.emails.$emailUid", $email); - } - - $projectConfig->muteEvents = $muteEvents; - - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230313_095359_add_store_id_to_emails cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230317_102521_add_store_id_to_pdfs.php b/src/migrations/m230317_102521_add_store_id_to_pdfs.php deleted file mode 100644 index 49ae438562..0000000000 --- a/src/migrations/m230317_102521_add_store_id_to_pdfs.php +++ /dev/null @@ -1,58 +0,0 @@ -addColumn(Table::PDFS, 'storeId', $this->integer()); - - $primaryStore = (new Query()) - ->select(['id', 'uid']) - ->from(Table::STORES) - ->where(['primary' => true]) - ->one(); - - $this->update(Table::PDFS, ['storeId' => $primaryStore['id']], ['storeId' => null], [], false); - - $this->addForeignKey(null, Table::PDFS, ['storeId'], Table::STORES, ['id'], 'CASCADE', null); - $this->createIndex(null, Table::PDFS, ['storeId'], false); - - $projectConfig = Craft::$app->getProjectConfig(); - - $pdfs = $projectConfig->get('commerce.pdfs') ?? []; - $muteEvents = $projectConfig->muteEvents; - $projectConfig->muteEvents = true; - - foreach ($pdfs as $uid => $pdf) { - $pdf['store'] = $primaryStore['uid']; - $projectConfig->set("commerce.pdfs.$uid", $pdf); - } - - $projectConfig->muteEvents = $muteEvents; - - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230317_102521_add_store_id_to_pdfs cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230322_091615_move_email_settings_to_model.php b/src/migrations/m230322_091615_move_email_settings_to_model.php deleted file mode 100644 index 0e5f371ef9..0000000000 --- a/src/migrations/m230322_091615_move_email_settings_to_model.php +++ /dev/null @@ -1,61 +0,0 @@ -addColumn(Table::EMAILS, 'senderName', $this->string()->after('name')); - $this->addColumn(Table::EMAILS, 'senderAddress', $this->string()->after('name')); - - $commerceConfig = Craft::$app->getConfig()->getConfigFromFile('commerce'); - - if (empty($commerceConfig)) { - return true; - } - - $senderAddress = $commerceConfig['emailSenderAddress'] ?? null; - $senderName = $commerceConfig['emailSenderName'] ?? null; - - $this->update(Table::EMAILS, [ - 'senderAddress' => $senderAddress, - 'senderName' => $senderName, - ]); - - $projectConfig = Craft::$app->getProjectConfig(); - - $emails = $projectConfig->get('commerce.emails') ?? []; - $muteEvents = $projectConfig->muteEvents; - $projectConfig->muteEvents = true; - - foreach ($emails as $uid => $email) { - $email['senderAddress'] = $senderAddress; - $email['senderName'] = $senderName; - $projectConfig->set("commerce.emails.$uid", $email); - } - - $projectConfig->muteEvents = $muteEvents; - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230322_091615_move_email_settings_to_model cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230328_130343_move_pdf_settings_to_model.php b/src/migrations/m230328_130343_move_pdf_settings_to_model.php deleted file mode 100644 index 133080e2be..0000000000 --- a/src/migrations/m230328_130343_move_pdf_settings_to_model.php +++ /dev/null @@ -1,59 +0,0 @@ -addColumn(Table::PDFS, 'paperOrientation', $this->string()->defaultValue('portrait')); - $this->addColumn(Table::PDFS, 'paperSize', $this->string()->defaultValue('letter')); - - $commerceConfig = Craft::$app->getConfig()->getConfigFromFile('commerce'); - - if (empty($commerceConfig)) { - return true; - } - - $data = [ - 'paperOrientation' => $commerceConfig['pdfPaperOrientation'] ?? 'portrait', - 'paperSize' => $commerceConfig['pdfPaperSize'] ?? 'letter', - ]; - - $this->update(Table::PDFS, $data); - - $projectConfig = Craft::$app->getProjectConfig(); - - $pdfs = $projectConfig->get('commerce.pdfs') ?? []; - $muteEvents = $projectConfig->muteEvents; - $projectConfig->muteEvents = true; - - foreach ($pdfs as $uid => $pdf) { - $projectConfig->set("commerce.pdfs.$uid", array_merge($pdf, $data)); - } - - $projectConfig->muteEvents = $muteEvents; - - return true; - } - - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230328_130343_move_pdf_settings_to_model cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230525_081243_add_has_update_pending_property.php b/src/migrations/m230525_081243_add_has_update_pending_property.php deleted file mode 100644 index 68dad0cfab..0000000000 --- a/src/migrations/m230525_081243_add_has_update_pending_property.php +++ /dev/null @@ -1,31 +0,0 @@ -addColumn(Table::CATALOG_PRICING, 'hasUpdatePending', $this->boolean()->notNull()->defaultValue(false)); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230525_081243_add_has_update_pending_property cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230530_100604_add_complete_email_column.php b/src/migrations/m230530_100604_add_complete_email_column.php deleted file mode 100644 index 296db6ae22..0000000000 --- a/src/migrations/m230530_100604_add_complete_email_column.php +++ /dev/null @@ -1,36 +0,0 @@ -addColumn(Table::ORDERS, 'orderCompletedEmail', $this->string()); - - // Update existing data - $this->update(Table::ORDERS, ['orderCompletedEmail' => new Expression('email')], ['isCompleted' => true], [], false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230530_100604_add_complete_email_column cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230705_124845_add_save_address_columns.php b/src/migrations/m230705_124845_add_save_address_columns.php deleted file mode 100644 index bcf32d2e04..0000000000 --- a/src/migrations/m230705_124845_add_save_address_columns.php +++ /dev/null @@ -1,32 +0,0 @@ -addColumn(Table::ORDERS, 'saveBillingAddressOnOrderComplete', $this->boolean()->notNull()->defaultValue(false)); - $this->addColumn(Table::ORDERS, 'saveShippingAddressOnOrderComplete', $this->boolean()->notNull()->defaultValue(false)); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230705_124845_add_save_address_columns cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230719_082348_discount_nullable_conditions.php b/src/migrations/m230719_082348_discount_nullable_conditions.php deleted file mode 100644 index b4b8f6b16b..0000000000 --- a/src/migrations/m230719_082348_discount_nullable_conditions.php +++ /dev/null @@ -1,49 +0,0 @@ -getConfig()); - - $this->update(Table::DISCOUNTS, ['orderCondition' => null], ['orderCondition' => $orderConditionConfig], [], false); - - $customerCondition = new DiscountCustomerCondition(); - $customerConditionConfig = Json::encode($customerCondition->getConfig()); - - $this->update(Table::DISCOUNTS, ['customerCondition' => null], ['customerCondition' => $customerConditionConfig], [], false); - - $addressCondition = new DiscountAddressCondition(); - $addressConditionConfig = Json::encode($addressCondition->getConfig()); - - $this->update(Table::DISCOUNTS, ['billingAddressCondition' => null], ['billingAddressCondition' => $addressConditionConfig], [], false); - $this->update(Table::DISCOUNTS, ['shippingAddressCondition' => null], ['shippingAddressCondition' => $addressConditionConfig], [], false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230719_082348_discount_nullable_conditions cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230724_080855_entrify_promotions.php b/src/migrations/m230724_080855_entrify_promotions.php deleted file mode 100644 index 96084443f1..0000000000 --- a/src/migrations/m230724_080855_entrify_promotions.php +++ /dev/null @@ -1,43 +0,0 @@ -db); - Db::dropForeignKeyIfExists(Table::DISCOUNT_CATEGORIES, ['discountId'], $this->db); - Db::dropForeignKeyIfExists(Table::SALE_CATEGORIES, ['categoryId'], $this->db); - Db::dropForeignKeyIfExists(Table::SALE_CATEGORIES, ['saleId'], $this->db); - - // Add the FKs back but to the Elements table not the categories table - $this->addForeignKey(null, Table::DISCOUNT_CATEGORIES, ['categoryId'], CraftTable::ELEMENTS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::DISCOUNT_CATEGORIES, ['discountId'], Table::DISCOUNTS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::SALE_CATEGORIES, ['categoryId'], CraftTable::ELEMENTS, ['id'], 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, Table::SALE_CATEGORIES, ['saleId'], Table::SALES, ['id'], 'CASCADE', 'CASCADE'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230724_080855_entrify_promotions cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230920_051125_move_primary_currency_to_store_settings.php b/src/migrations/m230920_051125_move_primary_currency_to_store_settings.php deleted file mode 100644 index 81d9f96928..0000000000 --- a/src/migrations/m230920_051125_move_primary_currency_to_store_settings.php +++ /dev/null @@ -1,72 +0,0 @@ -db->columnExists('{{%commerce_stores}}', 'currency')) { - $this->addColumn('{{%commerce_stores}}', 'currency', $this->string()->notNull()->defaultValue('USD')); - } - - $primaryCurrencyIso = (new Query()) - ->select('iso') - ->from('{{%commerce_paymentcurrencies}}') - ->where(['primary' => true]) - ->scalar(); - - $storeId = (new Query()) - ->select(['id']) - ->from(['{{%commerce_stores}}']) - ->scalar(); - - // update all stores record with currency - $this->update('{{%commerce_stores}}', ['currency' => $primaryCurrencyIso], ['id' => $storeId]); - - // Make project config updates - $projectConfig = \Craft::$app->getProjectConfig(); - - $storeUid = (new Query()) - ->select(['uid']) - ->from(['{{%commerce_stores}}']) - ->scalar(); - - // delete the primary payment currency and drop primary column from payment currencies - $this->dropColumn('{{%commerce_paymentcurrencies}}', 'primary'); - - $this->dropIndexIfExists(Table::PAYMENTCURRENCIES, 'iso', true); - $this->createIndex(null, Table::PAYMENTCURRENCIES, 'iso', false); - - // get store config - $config = $projectConfig->get(Stores::CONFIG_STORES_KEY . '.' . $storeUid); - - $config['currency'] = $primaryCurrencyIso; - $projectConfig->set(Stores::CONFIG_STORES_KEY . '.' . $storeUid, - $config, - 'Moving the primary currency to the store in the project config'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230920_051125_move_primary_currency_to_store_settings cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230928_095544_fix_unique_on_some_tables.php b/src/migrations/m230928_095544_fix_unique_on_some_tables.php deleted file mode 100644 index 8e15420278..0000000000 --- a/src/migrations/m230928_095544_fix_unique_on_some_tables.php +++ /dev/null @@ -1,35 +0,0 @@ -dropIndexIfExists(Table::SHIPPINGMETHODS, 'name', true); - $this->dropIndexIfExists(Table::TAXZONES, 'name', true); - - $this->createIndex(null, Table::SHIPPINGMETHODS, 'name', false); - $this->createIndex(null, Table::TAXZONES, 'name', false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230928_095544_fix_unique_on_some_tables cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m230928_155052_move_shipping_category_id_to_purchasable_stores.php b/src/migrations/m230928_155052_move_shipping_category_id_to_purchasable_stores.php deleted file mode 100644 index 28d2d91dc9..0000000000 --- a/src/migrations/m230928_155052_move_shipping_category_id_to_purchasable_stores.php +++ /dev/null @@ -1,65 +0,0 @@ -select(['id', 'shippingCategoryId'])->from(Table::PURCHASABLES)->all(); - - $this->dropForeignKeyIfExists(Table::PURCHASABLES, ['shippingCategoryId']); - $this->addColumn(Table::PURCHASABLES_STORES, 'shippingCategoryId', $this->integer()->null()); - - $cases = []; - foreach ($purchasablesShipping as $row) { - if (!$row['shippingCategoryId']) { - continue; - } - $cases[] = 'WHEN purchasableId = ' . $row['id'] . ' THEN ' . $row['shippingCategoryId']; - } - - foreach ($purchasablesShipping as $item) { - $this->update(Table::PURCHASABLES_STORES, ['shippingCategoryId' => $item['shippingCategoryId']], ['purchasableId' => $item['id']], [], false); - } - - // if (!empty($cases)) { - // $batches = array_chunk($cases, 5); - // foreach ($batches as $batch) { - // $this->update( - // Table::PURCHASABLES_STORES, - // ['shippingCategoryId' => new Expression(sprintf('(CASE %s END)', implode(' ', $batch)))], - // [], - // [], - // false, - // ); - // } - // } - - $this->addForeignKey(null, Table::PURCHASABLES_STORES, ['shippingCategoryId'], Table::SHIPPINGCATEGORIES, ['id']); - $this->dropColumn(Table::PURCHASABLES, 'shippingCategoryId'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m230928_155052_move_shipping_category_id_to_purchasable_stores cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m231006_034833_add_indexes_for_source_address_on_order.php b/src/migrations/m231006_034833_add_indexes_for_source_address_on_order.php deleted file mode 100644 index e12544f3fa..0000000000 --- a/src/migrations/m231006_034833_add_indexes_for_source_address_on_order.php +++ /dev/null @@ -1,32 +0,0 @@ -createIndex(null, Table::ORDERS, 'sourceBillingAddressId', false); - $this->createIndex(null, Table::ORDERS, 'sourceShippingAddressId', false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m231006_034833_add_indexes_for_source_address_on_order cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m231019_110814_update_variant_ownership.php b/src/migrations/m231019_110814_update_variant_ownership.php deleted file mode 100644 index 3506ab92e6..0000000000 --- a/src/migrations/m231019_110814_update_variant_ownership.php +++ /dev/null @@ -1,51 +0,0 @@ -select([ - 'id as elementId', - 'productId as ownerId', - new Expression('CASE WHEN [[sortOrder]] is NULL THEN 1 ELSE [[sortOrder]] END as [[sortOrder]]'), - ]) - ->from([Table::VARIANTS]) - ->all(); - - // Insert data in element owners - $this->batchInsert(CraftTable::ELEMENTS_OWNERS, ['elementId', 'ownerId', 'sortOrder'], $data); - - // Rename `productId` column - $this->renameColumn(Table::VARIANTS, 'productId', 'primaryOwnerId'); - - // Remove sort order - $this->dropColumn(Table::VARIANTS, 'sortOrder'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m231019_110814_update_variant_ownership cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m231110_081143_inventory_movement_table.php b/src/migrations/m231110_081143_inventory_movement_table.php deleted file mode 100644 index 1eefb0a2fa..0000000000 --- a/src/migrations/m231110_081143_inventory_movement_table.php +++ /dev/null @@ -1,217 +0,0 @@ -select(['id']) - ->from(Table::STORES) - ->where(['primary' => true]) - ->scalar(); - - // Gather the current purchasable stock counts - $stockCollection = collect((new Query()) - ->select([ - 'p.id as purchasableId', - new Expression('COALESCE([[ps.stock]], 0) as stock'), - new Expression('COALESCE([[ps.hasUnlimitedStock]], false) as unlimitedStock'), - ]) - ->from(['p' => Table::PURCHASABLES]) - ->leftJoin(['ps' => Table::PURCHASABLES_STORES], '[[p.id]] = [[ps.purchasableId]]') - ->where(['ps.storeId' => $primaryStoreId]) - ->limit(null) - ->all()); - - // Create the inventory items table, indexes and FKs - $this->createTable('{{%commerce_inventoryitems}}', [ - 'id' => $this->primaryKey(), - 'purchasableId' => $this->integer()->notNull(), - 'countryCodeOfOrigin' => $this->string(), - 'administrativeAreaCodeOfOrigin' => $this->string(), - 'harmonizedSystemCode' => $this->string(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - $this->createIndex(null, '{{%commerce_inventoryitems}}', 'purchasableId', true); - $this->addForeignKey(null, '{{%commerce_inventoryitems}}', 'purchasableId', '{{%commerce_purchasables}}', 'id', 'CASCADE', null); - - // Add the locations table - $this->createTable('{{%commerce_inventorylocations}}', [ - 'id' => $this->primaryKey(), - 'handle' => $this->string()->notNull(), - 'name' => $this->string()->notNull(), - 'addressId' => $this->integer(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'dateDeleted' => $this->dateTime(), - 'uid' => $this->uid(), - ]); - $this->addForeignKey(null, '{{%commerce_inventorylocations}}', 'addressId', '{{%addresses}}', 'id', 'CASCADE', null); - - // Create the transfers table - $this->createTable('{{%commerce_transfers}}', [ - 'id' => $this->primaryKey(), - 'transferStatus' => $this->enum('transferStatus', [ - 'draft', - 'pending', - 'partial', - 'received', - ])->notNull(), - 'originLocationId' => $this->integer(), - 'destinationLocationId' => $this->integer(), - 'uid' => $this->uid(), - ]); - - $this->createIndex(null, '{{%commerce_transfers}}', 'originLocationId', false); - $this->createIndex(null, '{{%commerce_transfers}}', 'destinationLocationId', false); - - // Create the commerce_inventory_movement table - $this->createTable('{{%commerce_inventorymovements}}', [ - 'id' => $this->primaryKey(), - 'inventoryLocationId' => $this->integer()->notNull(), - 'inventoryItemId' => $this->integer()->notNull(), - 'movementHash' => $this->string()->notNull(), - 'quantity' => $this->integer()->notNull(), - 'type' => $this->enum('type', [ - 'incoming', - 'available', - 'committed', - 'reserved', - 'damaged', - 'safety', - 'qualityControl', - ])->notNull(), - 'note' => $this->string(), - 'transferId' => $this->integer(), // Can be null - 'orderId' => $this->integer(), // Can be null - 'lineItemId' => $this->integer(), // Can be null - 'userId' => $this->integer(), // Can be null - 'dateCreated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - $this->createIndex(null, '{{%commerce_inventorymovements}}', 'inventoryItemId', false); - $this->createIndex(null, '{{%commerce_inventorymovements}}', 'transferId', false); - $this->createIndex(null, '{{%commerce_inventorymovements}}', 'orderId', false); - $this->createIndex(null, '{{%commerce_inventorymovements}}', 'lineItemId', false); - $this->createIndex(null, '{{%commerce_inventorymovements}}', 'userId', false); - - $this->addForeignKey(null, '{{%commerce_inventorymovements}}', 'inventoryItemId', '{{%commerce_inventoryitems}}', 'id', 'CASCADE', null); - $this->addForeignKey(null, '{{%commerce_inventorymovements}}', 'inventoryLocationId', '{{%commerce_inventorylocations}}', 'id', 'CASCADE', null); - $this->addForeignKey(null, '{{%commerce_inventorymovements}}', 'orderId', '{{%commerce_orders}}', 'id', 'SET NULL', null); - $this->addForeignKey(null, '{{%commerce_inventorymovements}}', 'lineItemId', '{{%commerce_lineitems}}', 'id', 'SET NULL', null); - $this->addForeignKey(null, '{{%commerce_inventorymovements}}', 'userId', '{{%users}}', 'id', 'SET NULL', null); - $this->addForeignKey(null, '{{%commerce_inventorymovements}}', 'transferId', '{{%commerce_transfers}}', 'id', 'SET NULL', null); - - - // get primary store - $primaryStore = (new Query()) - ->select(['id']) - ->from(Table::STORES) - ->where(['primary' => true]) - ->one(); - - // if no primaryStore found, use first one - if (!$primaryStore) { - $primaryStore = (new Query()) - ->select(['id']) - ->from(Table::STORES) - ->one(); - } - - // Get locationAddressId from store settings table - $locationAddressId = (new Query()) - ->select(['locationAddressId']) - ->from(Table::STORESETTINGS) - ->where(['id' => $primaryStore['id']]) - ->scalar(); - - // create default location - $this->insert('{{%commerce_inventorylocations}}', [ - 'name' => 'Default', - 'handle' => 'default', - 'addressId' => $locationAddressId ?: null, - 'dateCreated' => Db::prepareDateForDb(new \DateTime()), - 'dateUpdated' => Db::prepareDateForDb(new \DateTime()), - 'dateDeleted' => null, - 'uid' => StringHelper::UUID(), - ]); - $locationId = $this->db->getLastInsertID(); - - // Create an inventory item for each SKU - foreach ($stockCollection as $item) { - $now = Db::prepareDateForDb(new \DateTime()); - $this->insert('{{%commerce_inventoryitems}}', [ - 'purchasableId' => $item['purchasableId'], - 'dateCreated' => $now, - 'dateUpdated' => $now, - ]); - - $this->insert('{{%commerce_inventorymovements}}', [ - 'inventoryLocationId' => $locationId, - 'inventoryItemId' => $this->db->getLastInsertID(), - 'movementHash' => md5(uniqid((string)mt_rand(), true)), - 'quantity' => $item['stock'], - 'type' => 'available', - 'note' => 'count', - ]); - } - - // create inventory locations store relationship table - $this->createTable('{{%commerce_inventorylocations_stores}}', [ - 'id' => $this->primaryKey(), - 'inventoryLocationId' => $this->integer()->notNull(), - 'storeId' => $this->integer()->notNull(), - 'sortOrder' => $this->integer(), // per store - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->addForeignKey(null, '{{%commerce_inventorylocations_stores}}', 'inventoryLocationId', '{{%commerce_inventorylocations}}', 'id', 'CASCADE', null); - $this->addForeignKey(null, '{{%commerce_inventorylocations_stores}}', 'storeId', '{{%commerce_stores}}', 'id', 'CASCADE', null); - - // insert default location into store relationship table - $this->insert(Table::INVENTORYLOCATIONS_STORES, [ - 'inventoryLocationId' => $locationId, - 'storeId' => $primaryStore['id'], - 'sortOrder' => 1, - 'dateCreated' => Db::prepareDateForDb(new \DateTime()), - 'dateUpdated' => Db::prepareDateForDb(new \DateTime()), - ]); - - if ($this->db->columnExists('{{%commerce_purchasables_stores}}', 'hasUnlimitedStock')) { - $this->renameColumn(Table::PURCHASABLES_STORES, 'hasUnlimitedStock', 'inventoryTracked'); - } - - // Flip `inventoryTracked` column in purchasables stores table - $this->update('{{%commerce_purchasables_stores}}', ['inventoryTracked' => new Expression('NOT [[inventoryTracked]]')]); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m231110_081143_inventory_movement_table cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m231201_100454_update_discount_base_discount_type.php b/src/migrations/m231201_100454_update_discount_base_discount_type.php deleted file mode 100644 index bc9e960316..0000000000 --- a/src/migrations/m231201_100454_update_discount_base_discount_type.php +++ /dev/null @@ -1,35 +0,0 @@ -update(Table::DISCOUNTS, ['enabled' => false], ['not', ['baseDiscountType' => 'value']], updateTimestamp: false); - - // Remove `baseDiscountType` column - $this->dropColumn(Table::DISCOUNTS, 'baseDiscountType'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m231201_100454_update_discount_base_discount_type cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240119_073924_content_refactor_elements.php b/src/migrations/m240119_073924_content_refactor_elements.php deleted file mode 100644 index 6f25a04b3d..0000000000 --- a/src/migrations/m240119_073924_content_refactor_elements.php +++ /dev/null @@ -1,56 +0,0 @@ -updateElements( - (new Query())->from(Table::ORDERS), - Craft::$app->getFields()->getLayoutByType(Order::class) - ); - - // Migrate products and variants by product type - foreach (Plugin::getInstance()->getProductTypes()->getAllProductTypes() as $productType) { - // Update Products - $this->updateElements( - (new Query())->from(Table::PRODUCTS)->where(['typeId' => $productType->id]), - $productType->getProductFieldLayout() - ); - - // Update Variants - $this->updateElements( - (new Query())->from(Table::VARIANTS)->where([ - 'primaryOwnerId' => (new Query())->select('id')->from(Table::PRODUCTS)->where(['typeId' => $productType->id]), - ]), - $productType->getVariantFieldLayout() - ); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240119_073924_content_refactor_elements cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240119_075036_content_refactor_subscription_elements.php b/src/migrations/m240119_075036_content_refactor_subscription_elements.php deleted file mode 100644 index 9c8aeff79d..0000000000 --- a/src/migrations/m240119_075036_content_refactor_subscription_elements.php +++ /dev/null @@ -1,38 +0,0 @@ -updateElements( - (new Query())->from(Table::SUBSCRIPTIONS), - Craft::$app->getFields()->getLayoutByType(Subscription::class) - ); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240119_073924_content_refactor_elements cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240208_083054_add_purchasable_stores_purchasable_fk.php b/src/migrations/m240208_083054_add_purchasable_stores_purchasable_fk.php deleted file mode 100644 index d85be4d07e..0000000000 --- a/src/migrations/m240208_083054_add_purchasable_stores_purchasable_fk.php +++ /dev/null @@ -1,48 +0,0 @@ -select('id') - ->from('{{%commerce_purchasables}}'); - - $purchasables = (new Query()) - ->select('purchasableId') - ->from('{{%commerce_purchasables_stores}}') - ->where(['not in', 'purchasableId', $subQuery]) - ->column($this->db); // Assuming $this->db is your database connection - - // delete all purchasables in purchasable_stores table that is not in the purchasables table - $this->delete('{{%commerce_purchasables_stores}}', ['in', 'purchasableId', $purchasables]); - - $this->addForeignKey(null, '{{%commerce_purchasables_stores}}', ['purchasableId'], '{{%commerce_purchasables}}', ['id'],'CASCADE', 'CASCADE'); - - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240208_083054_add_purchasable_stores_purchasable_fk cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240219_194855_donation_multi_store.php b/src/migrations/m240219_194855_donation_multi_store.php deleted file mode 100644 index 70578f9b4b..0000000000 --- a/src/migrations/m240219_194855_donation_multi_store.php +++ /dev/null @@ -1,68 +0,0 @@ -select('id') - ->from(Table::STORES) - ->column(); - - // Get current donation data - $donations = (new Query()) - ->select('*') - ->from(Table::DONATIONS) - ->all(); - - foreach ($donations as $donation) { - foreach ($storeIds as $storeId) { - if (PurchasableStore::findOne(['purchasableId' => $donation['id'], 'storeId' => $storeId])) { - continue; - } - - $this->insert(Table::PURCHASABLES_STORES, [ - 'purchasableId' => $donation['id'], - 'storeId' => $storeId, - 'basePrice' => 0, - 'basePromotionalPrice' => null, - 'stock' => null, - 'inventoryTracked' => false, - 'minQty' => null, - 'maxQty' => null, - 'promotable' => false, - 'availableForPurchase' => $donation['availableForPurchase'], - 'freeShipping' => true, - 'shippingCategoryId' => null, - ]); - } - } - - // Remove `availableForPurchase` column from `commerce_donations` table - $this->dropColumn(Table::DONATIONS, 'availableForPurchase'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240219_194855_donation_multi_store cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240220_045806_product_versioning.php b/src/migrations/m240220_045806_product_versioning.php deleted file mode 100644 index b3b2bdfb67..0000000000 --- a/src/migrations/m240220_045806_product_versioning.php +++ /dev/null @@ -1,30 +0,0 @@ -addColumn(Table::PRODUCTTYPES, 'enableVersioning', $this->boolean()->defaultValue(false)->notNull()->after('handle')); - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240220_045806_product_versioning cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240220_105746_remove_store_from_donations_table.php b/src/migrations/m240220_105746_remove_store_from_donations_table.php deleted file mode 100644 index a31e77d9a7..0000000000 --- a/src/migrations/m240220_105746_remove_store_from_donations_table.php +++ /dev/null @@ -1,34 +0,0 @@ -db->columnExists(Table::DONATIONS, 'storeId')) { - $this->dropForeignKeyIfExists(Table::DONATIONS, 'storeId'); - $this->dropColumn(Table::DONATIONS, 'storeId'); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240220_105746_remove_store_from_donations_table cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240221_030027_transfer_items.php b/src/migrations/m240221_030027_transfer_items.php deleted file mode 100644 index 5dc049e4f2..0000000000 --- a/src/migrations/m240221_030027_transfer_items.php +++ /dev/null @@ -1,46 +0,0 @@ -createTable('{{%commerce_transfers_inventoryitems}}', [ - 'id' => $this->primaryKey(), - 'transferId' => $this->integer()->notNull(), - 'inventoryItemId' => $this->integer()->notNull(), - 'quantity' => $this->integer()->notNull(), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->createIndex(null, '{{%commerce_transfers_inventoryitems}}', 'inventoryItemId', false); - $this->createIndex(null, '{{%commerce_transfers_inventoryitems}}', 'transferId', false); - - $this->addForeignKey(null, '{{%commerce_transfers_inventoryitems}}', ['inventoryItemId'], '{{%commerce_inventoryitems}}', ['id'], 'CASCADE'); - $this->addForeignKey(null, '{{%commerce_transfers_inventoryitems}}', ['transferId'], '{{%commerce_inventoryitems}}', ['id'], 'CASCADE'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240221_030027_transfer_items cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240223_101158_update_recent_orders_widget_settings.php b/src/migrations/m240223_101158_update_recent_orders_widget_settings.php deleted file mode 100644 index fadae9cd62..0000000000 --- a/src/migrations/m240223_101158_update_recent_orders_widget_settings.php +++ /dev/null @@ -1,63 +0,0 @@ -select(['id', 'settings']) - ->from(Table::WIDGETS) - ->where(['type' => Orders::class]) - ->all(); - - // Get all order statuses - $orderStatuses = (new Query()) - ->select(['id', 'uid']) - ->from(\craft\commerce\db\Table::ORDERSTATUSES) - ->all(); - - // Update the widget settings to move from `orderStatusId` to `orderStatuses` - foreach ($widgets as $widget) { - $settings = Json::decodeIfJson($widget['settings']); - $orderStatusId = $settings['orderStatusId'] ?? null; - $settings['orderStatuses'] = []; - unset($settings['orderStatusId']); - - if ($orderStatusId !== null) { - $orderStatus = ArrayHelper::firstWhere($orderStatuses, 'id', $orderStatusId); - if ($orderStatus !== null) { - $settings['orderStatuses'][] = $orderStatus['uid']; - } - } - - $this->update(Table::WIDGETS, ['settings' => Json::encode($settings)], ['id' => $widget['id']]); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240223_101158_update_recent_orders_widget_settings cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240226_002943_remove_lite.php b/src/migrations/m240226_002943_remove_lite.php deleted file mode 100644 index 2f919ae5bf..0000000000 --- a/src/migrations/m240226_002943_remove_lite.php +++ /dev/null @@ -1,39 +0,0 @@ -db->columnExists('{{%commerce_shippingmethods}}', 'isLite')) { - $this->dropColumn('{{%commerce_shippingmethods}}', 'isLite'); - } - if ($this->db->columnExists('{{%commerce_shippingrules}}', 'isLite')) { - $this->dropColumn('{{%commerce_shippingrules}}', 'isLite'); - } - if ($this->db->columnExists('{{%commerce_taxrates}}', 'isLite')) { - $this->dropColumn('{{%commerce_taxrates}}', 'isLite'); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240226_002943_remove_lite cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240228_054005_rename_movements_table.php b/src/migrations/m240228_054005_rename_movements_table.php deleted file mode 100644 index 48074d9949..0000000000 --- a/src/migrations/m240228_054005_rename_movements_table.php +++ /dev/null @@ -1,31 +0,0 @@ -renameTable('{{%commerce_inventorymovements}}', '{{%commerce_inventorytransactions}}'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240228_054005_rename_movements_table cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240228_060604_add_fufilled_type_to_inventorytransactions.php b/src/migrations/m240228_060604_add_fufilled_type_to_inventorytransactions.php deleted file mode 100644 index f62fdaf892..0000000000 --- a/src/migrations/m240228_060604_add_fufilled_type_to_inventorytransactions.php +++ /dev/null @@ -1,30 +0,0 @@ -alterColumn('{{%commerce_inventorytransactions}}', 'type', $this->enum('type', ['available', 'reserved', 'damaged', 'safety', 'qualityControl', 'committed', 'fulfilled', 'incoming'])->notNull()); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240228_060604_add_fufilled_column_to_inventorytransactions cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240228_120911_drop_order_id_and_make_line_item_cascade.php b/src/migrations/m240228_120911_drop_order_id_and_make_line_item_cascade.php deleted file mode 100644 index ee2e7775df..0000000000 --- a/src/migrations/m240228_120911_drop_order_id_and_make_line_item_cascade.php +++ /dev/null @@ -1,40 +0,0 @@ -db->columnExists('{{%commerce_inventorytransactions}}', 'orderId')) { - $this->dropForeignKeyIfExists('{{%commerce_inventorytransactions}}', ['orderId']); - $this->dropColumn('{{%commerce_inventorytransactions}}', 'orderId'); - } - - // Make lineItemId cascade - if ($this->db->columnExists('{{%commerce_inventorytransactions}}', 'lineItemId')) { - $this->dropForeignKeyIfExists('{{%commerce_inventorytransactions}}', ['lineItemId']); - $this->addForeignKey(null, '{{%commerce_inventorytransactions}}', 'lineItemId', '{{%commerce_lineitems}}', 'id', 'CASCADE', null); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240228_120911_drop_order_id_and_make_line_item_cascade cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240301_113924_add_line_item_types.php b/src/migrations/m240301_113924_add_line_item_types.php deleted file mode 100644 index 66c2b23127..0000000000 --- a/src/migrations/m240301_113924_add_line_item_types.php +++ /dev/null @@ -1,34 +0,0 @@ -addColumn(Table::LINEITEMS, 'type', $this->enum('type', [ - 'purchasable', - 'custom', - ])->defaultValue('purchasable')->notNull()); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240301_113924_add_line_item_types cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240306_091057_move_element_ids_on_discount_to_columns.php b/src/migrations/m240306_091057_move_element_ids_on_discount_to_columns.php deleted file mode 100644 index 86a6adf15b..0000000000 --- a/src/migrations/m240306_091057_move_element_ids_on_discount_to_columns.php +++ /dev/null @@ -1,59 +0,0 @@ -addColumn($discountsTable, 'purchasableIds', $this->text()->after('allPurchasables')); - $this->addColumn($discountsTable, 'categoryIds', $this->text()->after('allCategories')); - - $purchasableIdsByDiscountId = (new Query()) - ->select(['discountId', 'purchasableId']) - ->from([$discountPurchasablesTables]) - ->collect(); - - $purchasableIdsByDiscountId = $purchasableIdsByDiscountId->groupBy('discountId')->map(fn($row) => array_column($row->toArray(), 'purchasableId')); - - $categoryIdsByDiscountId = (new Query()) - ->select(['discountId', 'categoryId']) - ->from([$discountCategoriesTable]) - ->collect(); - - $categoryIdsByDiscountId = $categoryIdsByDiscountId->groupBy('discountId')->map(fn($row) => array_column($row->toArray(), 'categoryId')); - - foreach ($purchasableIdsByDiscountId as $discountId => $purchasableIds) { - $this->update($discountsTable, ['purchasableIds' => Json::encode($purchasableIds)], ['id' => $discountId]); - } - - foreach ($categoryIdsByDiscountId as $discountId => $categoryIds) { - $this->update($discountsTable, ['categoryIds' => Json::encode($categoryIds)], ['id' => $discountId]); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240306_091057_move_element_ids_on_discount_to_columns cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240308_133451_tidy_shipping_categories.php b/src/migrations/m240308_133451_tidy_shipping_categories.php deleted file mode 100644 index 38fd250bd1..0000000000 --- a/src/migrations/m240308_133451_tidy_shipping_categories.php +++ /dev/null @@ -1,43 +0,0 @@ -select(['id']) - ->from(Table::STORES) - ->column(); - - $this->delete(Table::SHIPPINGCATEGORIES, ['not', ['storeId' => $storeIds]]); - - $this->dropForeignKeyIfExists(Table::SHIPPINGCATEGORIES, ['storeId']); - - $this->alterColumn(Table::SHIPPINGCATEGORIES, 'storeId', $this->integer()->notNull()); - - $this->addForeignKey(null, Table::SHIPPINGCATEGORIES, ['storeId'], Table::STORES, ['id'], 'CASCADE'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240308_133451_tidy_shipping_categories cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240313_131445_tidy_shipping_methods.php b/src/migrations/m240313_131445_tidy_shipping_methods.php deleted file mode 100644 index 01746a7a83..0000000000 --- a/src/migrations/m240313_131445_tidy_shipping_methods.php +++ /dev/null @@ -1,43 +0,0 @@ -select(['id']) - ->from(Table::STORES) - ->column(); - - $this->delete(Table::SHIPPINGMETHODS, ['not', ['storeId' => $storeIds]]); - - $this->dropForeignKeyIfExists(Table::SHIPPINGMETHODS, ['storeId']); - - $this->alterColumn(Table::SHIPPINGMETHODS, 'storeId', $this->integer()->notNull()); - - $this->addForeignKey(null, Table::SHIPPINGMETHODS, ['storeId'], Table::STORES, ['id'], 'CASCADE'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240313_131445_tidy_shipping_methods cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240315_072659_add_fk_cascade_fixes.php b/src/migrations/m240315_072659_add_fk_cascade_fixes.php deleted file mode 100644 index 051d3e9691..0000000000 --- a/src/migrations/m240315_072659_add_fk_cascade_fixes.php +++ /dev/null @@ -1,37 +0,0 @@ -addForeignKey(null, Table::STORESETTINGS, ['locationAddressId'], CraftTable::ELEMENTS, ['id'], 'SET NULL'); - - // There is no ability to drop the FK without knowing its name. - MigrationHelper::dropAllForeignKeysOnTable(Table::INVENTORYLOCATIONS); - $this->addForeignKey(null, Table::INVENTORYLOCATIONS, 'addressId', CraftTable::ELEMENTS, 'id', 'CASCADE', null); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240315_072659_add_fk_cascade_fixes cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240430_161804_add_index_to_transaction_hash.php b/src/migrations/m240430_161804_add_index_to_transaction_hash.php deleted file mode 100644 index b05e9d0622..0000000000 --- a/src/migrations/m240430_161804_add_index_to_transaction_hash.php +++ /dev/null @@ -1,31 +0,0 @@ -createIndexIfMissing(Table::TRANSACTIONS, 'hash', false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240430_161804_add_index_to_transaction_hash cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240507_081904_fix_store_pc_location.php b/src/migrations/m240507_081904_fix_store_pc_location.php deleted file mode 100644 index f14817a1af..0000000000 --- a/src/migrations/m240507_081904_fix_store_pc_location.php +++ /dev/null @@ -1,49 +0,0 @@ -getProjectConfig(); - - $allStores = $projectConfig->get(Stores::CONFIG_STORES_KEY) ?? []; - - if (!empty($allStores)) { - return true; - } - - $storeUid = (new Query())->select('uid')->from(Table::STORES)->scalar(); - - // Bad config key on purpose - $badCommerceConfig = $projectConfig->get(Stores::CONFIG_STORES_KEY . $storeUid); - - if ($badCommerceConfig) { - $projectConfig->set(Stores::CONFIG_STORES_KEY . '.' . $storeUid, $badCommerceConfig); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240507_081904_fix_store_pc_location cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240516_035616_update_permissions.php b/src/migrations/m240516_035616_update_permissions.php deleted file mode 100644 index b93cd0366e..0000000000 --- a/src/migrations/m240516_035616_update_permissions.php +++ /dev/null @@ -1,80 +0,0 @@ - "commerce-manageSubscriptions", - "commerce-manageDonationSettings" => "commerce-manageStoreSettings", - ]; - - // Now add the new permissions to existing users where applicable - foreach ($newPermissions as $oldPermission => $newPermission) { - $userIds = (new Query()) - ->select(['upu.userId']) - ->from(['upu' => Table::USERPERMISSIONS_USERS]) - ->innerJoin(['up' => Table::USERPERMISSIONS], '[[up.id]] = [[upu.permissionId]]') - ->where(['up.name' => $oldPermission]) - ->column($this->db); - if (!empty($userIds)) { - $insert = []; - foreach ((array)$newPermission as $name) { - $this->insert(Table::USERPERMISSIONS, [ - 'name' => $name, - ]); - $newPermissionId = $this->db->getLastInsertID(Table::USERPERMISSIONS); - foreach ($userIds as $userId) { - $insert[] = [$newPermissionId, $userId]; - } - } - $this->batchInsert(Table::USERPERMISSIONS_USERS, ['permissionId', 'userId'], $insert); - } - } - - // Don't make the same config changes twice - $projectConfig = Craft::$app->getProjectConfig(); - - foreach ($projectConfig->get('users.groups') ?? [] as $uid => $group) { - $groupPermissions = array_flip($group['permissions'] ?? []); - $changed = false; - foreach ($newPermissions as $oldPermission => $newPermission) { - if (isset($groupPermissions[$oldPermission])) { - foreach ((array)$newPermission as $name) { - $groupPermissions[$name] = true; - } - $changed = true; - } - } - if ($changed) { - $projectConfig->set("users.groups.{$uid}.permissions", array_keys($groupPermissions)); - } - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240516_035616_update_permissions cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240516_035617_update_currency_and_store_general_permissions.php b/src/migrations/m240516_035617_update_currency_and_store_general_permissions.php deleted file mode 100644 index 0aaf7c830f..0000000000 --- a/src/migrations/m240516_035617_update_currency_and_store_general_permissions.php +++ /dev/null @@ -1,80 +0,0 @@ - "commerce-manageStoreSettings", - "commerce-manageGeneralStoreSettings" => "commerce-manageStoreSettings", - ]; - - // Now add the new permissions to existing users where applicable - foreach ($newPermissions as $oldPermission => $newPermission) { - $userIds = (new Query()) - ->select(['upu.userId']) - ->from(['upu' => Table::USERPERMISSIONS_USERS]) - ->innerJoin(['up' => Table::USERPERMISSIONS], '[[up.id]] = [[upu.permissionId]]') - ->where(['up.name' => $oldPermission]) - ->column($this->db); - if (!empty($userIds)) { - $insert = []; - foreach ((array)$newPermission as $name) { - $this->insert(Table::USERPERMISSIONS, [ - 'name' => $name, - ]); - $newPermissionId = $this->db->getLastInsertID(Table::USERPERMISSIONS); - foreach ($userIds as $userId) { - $insert[] = [$newPermissionId, $userId]; - } - } - $this->batchInsert(Table::USERPERMISSIONS_USERS, ['permissionId', 'userId'], $insert); - } - } - - // Don't make the same config changes twice - $projectConfig = Craft::$app->getProjectConfig(); - - foreach ($projectConfig->get('users.groups') ?? [] as $uid => $group) { - $groupPermissions = array_flip($group['permissions'] ?? []); - $changed = false; - foreach ($newPermissions as $oldPermission => $newPermission) { - if (isset($groupPermissions[$oldPermission])) { - foreach ((array)$newPermission as $name) { - $groupPermissions[$name] = true; - } - $changed = true; - } - } - if ($changed) { - $projectConfig->set("users.groups.{$uid}.permissions", array_keys($groupPermissions)); - } - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240516_035616_update_permissions cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240528_124101_add_extra_lineitem_columns.php b/src/migrations/m240528_124101_add_extra_lineitem_columns.php deleted file mode 100644 index 53f1185f6d..0000000000 --- a/src/migrations/m240528_124101_add_extra_lineitem_columns.php +++ /dev/null @@ -1,37 +0,0 @@ -addColumn(Table::LINEITEMS, 'hasFreeShipping', $this->boolean()); - - $this->addColumn(Table::LINEITEMS, 'isPromotable', $this->boolean()); - - $this->addColumn(Table::LINEITEMS, 'isShippable', $this->boolean()); - - $this->addColumn(Table::LINEITEMS, 'isTaxable', $this->boolean()); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240528_124101_add_extra_lineitem_columns cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240529_095819_remove_commerce_user_field.php b/src/migrations/m240529_095819_remove_commerce_user_field.php deleted file mode 100644 index 7d08fe584b..0000000000 --- a/src/migrations/m240529_095819_remove_commerce_user_field.php +++ /dev/null @@ -1,53 +0,0 @@ -fields->getLayoutByType(\craft\elements\User::class); - - $tabs = $fieldLayout->getTabs(); - - foreach ($tabs as $tab) { - $newFields = []; - - foreach ($tab->elements as $element) { - if ($element::class !== $fieldClassName) { - $newFields[] = $element; - } - } - - $tab->setElements($newFields); - } - - // Save the modified field layout - Craft::$app->fields->saveLayout($fieldLayout); - - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240529_095819_remove_commerce_user_field cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240605_110755_add_title_translations_product_types.php b/src/migrations/m240605_110755_add_title_translations_product_types.php deleted file mode 100644 index ffe06b9922..0000000000 --- a/src/migrations/m240605_110755_add_title_translations_product_types.php +++ /dev/null @@ -1,34 +0,0 @@ -addColumn('{{%commerce_producttypes}}', 'productTitleTranslationKeyFormat', $this->string()->after('productTitleFormat')); - $this->addColumn('{{%commerce_producttypes}}', 'productTitleTranslationMethod', $this->string()->defaultValue('site')->notNull()->after('productTitleFormat')); - - $this->addColumn('{{%commerce_producttypes}}', 'variantTitleTranslationKeyFormat', $this->string()->after('variantTitleFormat')); - $this->addColumn('{{%commerce_producttypes}}', 'variantTitleTranslationMethod', $this->string()->defaultValue('site')->notNull()->after('variantTitleFormat')); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240605_110755_add_title_translations_product_types cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240619_082224_add_product_and_variant_conditions_to_catalog_pricing_rules.php b/src/migrations/m240619_082224_add_product_and_variant_conditions_to_catalog_pricing_rules.php deleted file mode 100644 index 0a158e5958..0000000000 --- a/src/migrations/m240619_082224_add_product_and_variant_conditions_to_catalog_pricing_rules.php +++ /dev/null @@ -1,32 +0,0 @@ -addColumn(Table::CATALOG_PRICING_RULES, 'productCondition', $this->text()->after('applyPriceType')); - $this->addColumn(Table::CATALOG_PRICING_RULES, 'variantCondition', $this->text()->after('applyPriceType')); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240619_082224_add_product_and_variant_conditions_to_catalog_pricing_rules cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240710_125204_ensure_shippingCategoryId_column_is_nullable.php b/src/migrations/m240710_125204_ensure_shippingCategoryId_column_is_nullable.php deleted file mode 100644 index 40407c2a7b..0000000000 --- a/src/migrations/m240710_125204_ensure_shippingCategoryId_column_is_nullable.php +++ /dev/null @@ -1,30 +0,0 @@ -alterColumn('{{%commerce_purchasables_stores}}', 'shippingCategoryId', $this->integer()->null()); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240710_125204_ensure_shippingCategoryId_column_is_nullable cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240711_092240_fix_fks.php b/src/migrations/m240711_092240_fix_fks.php deleted file mode 100644 index c2d6fe1664..0000000000 --- a/src/migrations/m240711_092240_fix_fks.php +++ /dev/null @@ -1,40 +0,0 @@ -dropForeignKeyIfExists('{{%commerce_catalogpricingrules}}', 'purchasableId'); - $this->dropIndexIfExists('{{%commerce_catalogpricingrules}}', 'purchasableId'); - - if ($this->db->columnExists('{{%commerce_catalogpricingrules}}', 'purchasableId')) { - $this->dropColumn('{{%commerce_catalogpricingrules}}', 'purchasableId'); - } - - // Fix constraint to set not null on delete - $this->dropForeignKeyIfExists('{{%commerce_purchasables_stores}}', 'shippingCategoryId'); - $this->addForeignKey(null, '{{%commerce_purchasables_stores}}', ['shippingCategoryId'], Table::SHIPPINGCATEGORIES, ['id'], 'SET NULL'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240711_092240_fix_fks cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240715_045506_drop_available_if_exists.php b/src/migrations/m240715_045506_drop_available_if_exists.php deleted file mode 100644 index e0d430471f..0000000000 --- a/src/migrations/m240715_045506_drop_available_if_exists.php +++ /dev/null @@ -1,33 +0,0 @@ -db->columnExists('{{%commerce_donations}}', 'availableForPurchase')) { - $this->dropColumn('{{%commerce_donations}}', 'availableForPurchase'); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240715_045506_drop_available_if_exists cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240717_044256_add_return_url_to_subscription.php b/src/migrations/m240717_044256_add_return_url_to_subscription.php deleted file mode 100644 index 42bcb8a594..0000000000 --- a/src/migrations/m240717_044256_add_return_url_to_subscription.php +++ /dev/null @@ -1,31 +0,0 @@ -addColumn('{{%commerce_subscriptions}}', 'returnUrl', $this->text()->after('isExpired')); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240717_044256_add_return_url_to_subscription cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240718_073046_remove_sortOrder_variants_column_if_exists.php b/src/migrations/m240718_073046_remove_sortOrder_variants_column_if_exists.php deleted file mode 100644 index 1683c32911..0000000000 --- a/src/migrations/m240718_073046_remove_sortOrder_variants_column_if_exists.php +++ /dev/null @@ -1,32 +0,0 @@ -db->columnExists('{{%commerce_variants}}', 'sortOrder')) { - $this->dropColumn('{{%commerce_variants}}', 'sortOrder'); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240718_073046_remove_sortOrder_variants_column_if_exists cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240808_090256_cascade_delete_variants_on_product_delete.php b/src/migrations/m240808_090256_cascade_delete_variants_on_product_delete.php deleted file mode 100644 index a0269ceb9f..0000000000 --- a/src/migrations/m240808_090256_cascade_delete_variants_on_product_delete.php +++ /dev/null @@ -1,41 +0,0 @@ -select('id') - ->from('{{%commerce_variants}}') - ->where(['primaryOwnerId' => null]); - $this->delete('{{%elements}}', ['id' => $allVariantsWithNullOwner]); - - // Should cascade delete variants when a product is deleted - $this->dropForeignKeyIfExists('{{%commerce_variants}}', ['primaryOwnerId']); - $this->addForeignKey(null, '{{%commerce_variants}}', ['primaryOwnerId'], '{{%commerce_products}}', ['id'], 'CASCADE'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240808_090256_cascade_delete_variants_on_product_delete cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240808_093934_product_type_propagation.php b/src/migrations/m240808_093934_product_type_propagation.php deleted file mode 100644 index d4cd47f29f..0000000000 --- a/src/migrations/m240808_093934_product_type_propagation.php +++ /dev/null @@ -1,33 +0,0 @@ -addColumn('{{%commerce_producttypes}}', 'propagationMethod', $this->string()->defaultValue(PropagationMethod::All->value)->after('productTitleTranslationKeyFormat')); - $this->addColumn('{{%commerce_producttypes_sites}}', 'enabledByDefault', $this->boolean()->defaultValue(true)->notNull()); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240808_093934_product_type_propagation cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240812_025615_add_transfer_details_table.php b/src/migrations/m240812_025615_add_transfer_details_table.php deleted file mode 100644 index 196ae82e19..0000000000 --- a/src/migrations/m240812_025615_add_transfer_details_table.php +++ /dev/null @@ -1,52 +0,0 @@ -dropTableIfExists('{{%commerce_transfers_inventoryitems}}'); - - $this->createTable('{{%commerce_transferdetails}}', [ - 'id' => $this->primaryKey(), - 'transferId' => $this->integer()->notNull(), - 'inventoryItemId' => $this->integer(), - 'inventoryItemDescription' => $this->string()->notNull(), - 'quantity' => $this->integer()->notNull(), - 'quantityAccepted' => $this->integer()->notNull(), - 'quantityRejected' => $this->integer()->notNull(), - 'uid' => $this->uid(), - ]); - - $this->createIndex(null, '{{%commerce_transferdetails}}', 'transferId', false); - $this->createIndex(null, '{{%commerce_transferdetails}}', 'inventoryItemId', false); - $this->addForeignKey(null, '{{%commerce_transferdetails}}', 'transferId', '{{%commerce_transfers}}', 'id', 'CASCADE', 'CASCADE'); - $this->addForeignKey(null, '{{%commerce_transferdetails}}', 'inventoryItemId', '{{%commerce_inventoryitems}}', 'id', 'SET NULL', 'CASCADE'); - - // Add missing FK - $this->addForeignKey(null, '{{%commerce_transfers}}', 'id', '{{%elements}}', 'id', 'CASCADE', 'CASCADE'); - - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240812_025615_add_transfer_details_table cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240815_035618_fix_transfer_permission.php b/src/migrations/m240815_035618_fix_transfer_permission.php deleted file mode 100644 index 2745d46589..0000000000 --- a/src/migrations/m240815_035618_fix_transfer_permission.php +++ /dev/null @@ -1,79 +0,0 @@ - "commerce-manageTransfers", - ]; - - // Now add the new permissions to existing users where applicable - foreach ($newPermissions as $oldPermission => $newPermission) { - $userIds = (new Query()) - ->select(['upu.userId']) - ->from(['upu' => Table::USERPERMISSIONS_USERS]) - ->innerJoin(['up' => Table::USERPERMISSIONS], '[[up.id]] = [[upu.permissionId]]') - ->where(['up.name' => $oldPermission]) - ->column($this->db); - if (!empty($userIds)) { - $insert = []; - foreach ((array)$newPermission as $name) { - $this->insert(Table::USERPERMISSIONS, [ - 'name' => $name, - ]); - $newPermissionId = $this->db->getLastInsertID(Table::USERPERMISSIONS); - foreach ($userIds as $userId) { - $insert[] = [$newPermissionId, $userId]; - } - } - $this->batchInsert(Table::USERPERMISSIONS_USERS, ['permissionId', 'userId'], $insert); - } - } - - // Don't make the same config changes twice - $projectConfig = Craft::$app->getProjectConfig(); - - foreach ($projectConfig->get('users.groups') ?? [] as $uid => $group) { - $groupPermissions = array_flip($group['permissions'] ?? []); - $changed = false; - foreach ($newPermissions as $oldPermission => $newPermission) { - if (isset($groupPermissions[$oldPermission])) { - foreach ((array)$newPermission as $name) { - $groupPermissions[$name] = true; - } - $changed = true; - } - } - if ($changed) { - $projectConfig->set("users.groups.{$uid}.permissions", array_keys($groupPermissions)); - } - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240516_035616_update_permissions cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240830_081410_add_extra_indexes_to_catalog_pricing.php b/src/migrations/m240830_081410_add_extra_indexes_to_catalog_pricing.php deleted file mode 100644 index 8f1be5d64a..0000000000 --- a/src/migrations/m240830_081410_add_extra_indexes_to_catalog_pricing.php +++ /dev/null @@ -1,31 +0,0 @@ -createIndex(null, '{{%commerce_catalogpricing}}', ['purchasableId', 'storeId', 'isPromotionalPrice', 'price'], false); - $this->createIndex(null, '{{%commerce_catalogpricing}}', ['purchasableId', 'storeId', 'isPromotionalPrice', 'price', 'catalogPricingRuleId', 'dateFrom', 'dateTo'], false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240830_081410_add_extra_indexes_to_catalog_pricing cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240905_130549_add_require_coupon_code_discount_setting.php b/src/migrations/m240905_130549_add_require_coupon_code_discount_setting.php deleted file mode 100644 index 16ff03640f..0000000000 --- a/src/migrations/m240905_130549_add_require_coupon_code_discount_setting.php +++ /dev/null @@ -1,30 +0,0 @@ -addColumn('{{%commerce_discounts}}', 'requireCouponCode', $this->boolean()->notNull()->defaultValue(false)->after('billingAddressCondition')); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240905_130549_add_require_coupon_code_discount_setting cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240906_105809_update_existing_coupon_discounts.php b/src/migrations/m240906_105809_update_existing_coupon_discounts.php deleted file mode 100644 index 9ed9d6b602..0000000000 --- a/src/migrations/m240906_105809_update_existing_coupon_discounts.php +++ /dev/null @@ -1,36 +0,0 @@ -from('{{%commerce_coupons}}') - ->select(['discountId']) - ->groupBy('discountId'); - - $this->update('{{%commerce_discounts}}', ['requireCouponCode' => true], ['id' => $couponDiscountIds]); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240906_105809_update_existing_coupon_discounts cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240906_115901_add_orderable_to_product_types.php b/src/migrations/m240906_115901_add_orderable_to_product_types.php deleted file mode 100644 index 14360d26eb..0000000000 --- a/src/migrations/m240906_115901_add_orderable_to_product_types.php +++ /dev/null @@ -1,51 +0,0 @@ -db->columnExists('{{%commerce_producttypes}}', 'defaultPlacement')) { - $this->addColumn('{{%commerce_producttypes}}', 'defaultPlacement', $this->enum('defaultPlacement', [ - ProductType::DEFAULT_PLACEMENT_BEGINNING, - ProductType::DEFAULT_PLACEMENT_END, ] - )->defaultValue('end')->notNull()); - } - - if (!$this->db->columnExists('{{%commerce_producttypes}}', 'type')) { - $this->addColumn('{{%commerce_producttypes}}', 'type', $this->enum('type', [ - 'channel', - 'orderable', ] - )->defaultValue('channel')->notNull()); - } - - if (!$this->db->columnExists('{{%commerce_producttypes}}', 'structureId')) { - $this->addColumn('{{%commerce_producttypes}}', 'structureId', $this->integer()); - } - - $this->createIndex(null, '{{%commerce_producttypes}}', ['structureId'], false); - $this->addForeignKey(null, '{{%commerce_producttypes}}', ['structureId'], Table::STRUCTURES, ['id'], 'SET NULL', null); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240906_115901_add_orderable_to_product_types cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m240923_132625_remove_orphaned_variants_sites.php b/src/migrations/m240923_132625_remove_orphaned_variants_sites.php deleted file mode 100644 index d56046062f..0000000000 --- a/src/migrations/m240923_132625_remove_orphaned_variants_sites.php +++ /dev/null @@ -1,59 +0,0 @@ -select(['elementId', 'siteId']) - ->from('{{%elements_sites}}' . ' es') - ->innerJoin('{{%commerce_products}}' . ' p', '[[es.elementId]] = [[p.id]]') - ->collect(); - - // Group them by product ID - $siteIdsByProductId = $allProductsSites->groupBy('elementId')->map(fn($row) => collect($row)->pluck('siteId')->toArray() - ); - - // Find all existing combinations of variant and site IDs - $allVariantsSites = (new Query()) - ->select(['es.id', 'elementId', 'siteId', 'primaryOwnerId']) - ->from('{{%elements_sites}}' . ' es') - ->innerJoin('{{%commerce_variants}}' . ' v', '[[es.elementId]] = [[v.id]]') - ->collect(); - - // Find all variants that are not associated with any of their product's sites - $orphanedVariantsSites = array_values($allVariantsSites->filter(fn($row) => !in_array($row['siteId'], $siteIdsByProductId[$row['primaryOwnerId']]))->map(fn($row) => $row['id'])->toArray()); - - if (empty($orphanedVariantsSites)) { - return true; - } - - // Bulk delete the orphaned variants' site rows (if any) 1000 at a time - foreach (array_chunk($orphanedVariantsSites, 1000) as $chunk) { - $this->delete('{{%elements_sites}}', ['id' => $chunk]); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m240923_132625_remove_orphaned_variants_sites cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m241010_061430_rename_orderable_product_type_type.php b/src/migrations/m241010_061430_rename_orderable_product_type_type.php deleted file mode 100644 index e08dd4dc09..0000000000 --- a/src/migrations/m241010_061430_rename_orderable_product_type_type.php +++ /dev/null @@ -1,43 +0,0 @@ -db->createCommand('SELECT id, type FROM {{%commerce_producttypes}}')->queryAll(); - - if ($this->db->columnExists('{{%commerce_producttypes}}', 'type')) { - $this->dropColumn('{{%commerce_producttypes}}', 'type'); - } - - $this->addColumn('{{%commerce_producttypes}}', 'isStructure', $this->boolean()->notNull()->defaultValue(false)); - $this->addColumn('{{%commerce_producttypes}}', 'maxLevels', $this->smallInteger()->unsigned()); - - foreach ($productTypes as $productType) { - if ($productType['type'] == 'orderable') { - $this->update('{{%commerce_producttypes}}', ['isStructure' => true, 'maxLevels' => 1], ['id' => $productType['id']]); - } - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m241010_061430_rename_orderable_product_type_type cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m241017_072151_fix_temp_skus.php b/src/migrations/m241017_072151_fix_temp_skus.php deleted file mode 100644 index 24ce7036f5..0000000000 --- a/src/migrations/m241017_072151_fix_temp_skus.php +++ /dev/null @@ -1,42 +0,0 @@ -select(['id', 'sku']) - ->from('{{%commerce_purchasables}}') - ->where(['like', 'sku', '__temp_%', false]) - ->all(); - - // Need a unique one per purchasable - foreach ($purchasables as $purchasable) { - $newSku = Purchasable::tempSku(); - $this->update('{{%commerce_purchasables}}', ['sku' => $newSku], ['id' => $purchasable['id']]); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m241017_072151_fix_temp_skus cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m241022_075144_add_missing_variant_revision_records.php b/src/migrations/m241022_075144_add_missing_variant_revision_records.php deleted file mode 100644 index 1e003a6dc9..0000000000 --- a/src/migrations/m241022_075144_add_missing_variant_revision_records.php +++ /dev/null @@ -1,151 +0,0 @@ -select([ - 'e.id', - 'e.canonicalId', - 'e.revisionId', - 'es.siteId', - ]) - ->from('{{%elements}}' . ' e') - ->innerJoin('{{%elements_sites}}' . ' es', '[[e.id]] = [[es.elementId]]') - ->where(['type' => Variant::class]) - ->andWhere(['not', ['revisionId' => null]]) - ->collect(); - - $sitesStores = (new Query()) - ->select(['siteId', 'storeId']) - ->from('{{%commerce_site_stores}}') - ->collect(); - - /** @var Collection $variantsWithRevisions */ - $canonicalVariantIds = $variantsWithRevisions->pluck('canonicalId')->unique()->all(); - $revisionVariantElementIds = $variantsWithRevisions->pluck('id')->unique()->all(); - $nonCanonicalPurchasableRecords = []; - $nonCanonicalPurchasableStoreRecords = []; - - foreach (array_chunk($revisionVariantElementIds, 1000) as $chunk) { - $nonCanonicalPurchasableRecords += Purchasable::find()->where(['element.id' => $chunk])->indexBy('element.id')->all(); - $nonCanonicalPurchasableStoreRecords += PurchasableStore::find()->where(['purchasableId' => $chunk])->indexBy(fn(PurchasableStore $row) => $row['purchasableId'] . '-' . $row['storeId'])->all(); - } - - $canonicalVariantPurchasableRecords = []; - $canonicalVariantPurchasableStoreRecords = []; - - foreach (array_chunk($canonicalVariantIds, 1000) as $chunk) { - $canonicalVariantPurchasableRecords += Purchasable::find()->where(['element.id' => $chunk])->indexBy('element.id')->all(); - $canonicalVariantPurchasableStoreRecords += PurchasableStore::find()->where(['purchasableId' => $chunk])->indexBy(fn(PurchasableStore $row) => $row['purchasableId'] . '-' . $row['storeId'])->all(); - } - - $purchasableInserts = []; - $purchasableStoresInserts = []; - $date = Db::prepareDateForDb(new \DateTime()); - - foreach ($variantsWithRevisions as $v) { - $canonicalPurchasableRecord = $canonicalVariantPurchasableRecords[$v['canonicalId']] ?? null; - $nonCanonicalPurchasableRecord = $nonCanonicalPurchasableRecords[$v['id']] ?? null; - - // Skip if we can't find the canonical record or if a record exists for this variant ID - if (!$canonicalPurchasableRecord || $nonCanonicalPurchasableRecord) { - continue; - } - - // As we are looping over variants across sites we need to ensure we only insert a purchasable once - if (!($purchasableInserts[$v['id']] ?? null)) { - $purchasableInserts[$v['id']] = [ - 'id' => $v['id'], - 'description' => $canonicalPurchasableRecord['description'], - 'sku' => $canonicalPurchasableRecord['sku'], - 'width' => $canonicalPurchasableRecord['width'], - 'height' => $canonicalPurchasableRecord['height'], - 'length' => $canonicalPurchasableRecord['length'], - 'weight' => $canonicalPurchasableRecord['weight'], - 'dateCreated' => $date, - 'dateUpdated' => $date, - 'taxCategoryId' => $canonicalPurchasableRecord['taxCategoryId'], - 'uid' => StringHelper::UUID(), - ]; - } - - $storeId = $sitesStores->where('siteId', $v['siteId'])->pluck('storeId')->first(); - - $canonicalPurchasableStoreRecord = $canonicalVariantPurchasableStoreRecords[$v['canonicalId'] . '-' . $storeId] ?? null; - $nonCanonicalPurchaseStoreRecord = $nonCanonicalPurchasableStoreRecords[$v['id'] . '-' . $storeId] ?? null; - - // Skip if we can't find the canonical record or if a record exists for this variant ID and Store ID - if (!$canonicalPurchasableStoreRecord || $nonCanonicalPurchaseStoreRecord) { - continue; - } - - if (!($purchasableStoresInserts[$v['id'] . '-' . $storeId] ?? null)) { - $purchasableStoresInserts[$v['id'] . '-' . $storeId] = [ - 'purchasableId' => $v['id'], - 'storeId' => $storeId, - 'basePrice' => $canonicalPurchasableStoreRecord['basePrice'], - 'basePromotionalPrice' => $canonicalPurchasableStoreRecord['basePromotionalPrice'], - 'promotable' => $canonicalPurchasableStoreRecord['promotable'], - 'availableForPurchase' => $canonicalPurchasableStoreRecord['availableForPurchase'], - 'freeShipping' => $canonicalPurchasableStoreRecord['freeShipping'], - 'stock' => $canonicalPurchasableStoreRecord['stock'], - 'inventoryTracked' => $canonicalPurchasableStoreRecord['inventoryTracked'], - 'minQty' => $canonicalPurchasableStoreRecord['minQty'], - 'maxQty' => $canonicalPurchasableStoreRecord['maxQty'], - 'shippingCategoryId' => $canonicalPurchasableStoreRecord['shippingCategoryId'], - 'uid' => StringHelper::UUID(), - 'dateCreated' => $date, - 'dateUpdated' => $date, - ]; - } - } - - if (!empty($purchasableInserts)) { - foreach (array_chunk($purchasableInserts, 1000) as $purchasableInsertsChunk) { - Craft::$app->getDb()->createCommand() - ->batchInsert('{{%commerce_purchasables}}', array_keys($purchasableInsertsChunk[0]), $purchasableInsertsChunk) - ->execute(); - } - } - - if (!empty($purchasableStoresInserts)) { - foreach (array_chunk($purchasableStoresInserts, 1000) as $purchasableStoresInsertsChunk) { - Craft::$app->getDb()->createCommand() - ->batchInsert('{{%commerce_purchasables_stores}}', array_keys($purchasableStoresInsertsChunk[0]), $purchasableStoresInsertsChunk) - ->execute(); - } - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m241022_075144_add_missing_variant_revision_records cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m241128_174712_fix_maxLevels_structured_productTypes.php b/src/migrations/m241128_174712_fix_maxLevels_structured_productTypes.php deleted file mode 100644 index f07f69385d..0000000000 --- a/src/migrations/m241128_174712_fix_maxLevels_structured_productTypes.php +++ /dev/null @@ -1,43 +0,0 @@ -from(Table::PRODUCTTYPES) - ->where(['isStructure' => true]) - ->andWhere(['not', ['maxLevels' => null]]) - ->collect(); - - // Loop through and update the `maxLevels` column in the `structures` table - $structuredProductTypesWithMaxLevels->each(function($productType) { - $this->update(CraftTable::STRUCTURES, ['maxLevels' => $productType['maxLevels']], ['id' => $productType['structureId']]); - }); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m241128_174712_fix_maxLevels_structured_productTypes cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m241204_045158_enable_tax_rate.php b/src/migrations/m241204_045158_enable_tax_rate.php deleted file mode 100644 index d3fb160773..0000000000 --- a/src/migrations/m241204_045158_enable_tax_rate.php +++ /dev/null @@ -1,33 +0,0 @@ -db->columnExists('{{%commerce_taxrates}}', 'enabled')) { - $this->addColumn('{{%commerce_taxrates}}', 'enabled', $this->boolean()->notNull()->defaultValue(true)); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m241204_045158_enable_tax_rate cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m241204_091901_fix_store_environment_variables.php b/src/migrations/m241204_091901_fix_store_environment_variables.php deleted file mode 100644 index 279ac79e65..0000000000 --- a/src/migrations/m241204_091901_fix_store_environment_variables.php +++ /dev/null @@ -1,92 +0,0 @@ -from(Table::STORES) - ->all(); - - // Get the store settings for each store from the project config - $storeSettings = \Craft::$app->getProjectConfig()->get('commerce.stores'); - - - // Store properties to update - $storeProperties = [ - 'autoSetNewCartAddresses', - 'autoSetCartShippingMethodOption', - 'autoSetPaymentSource', - 'allowEmptyCartOnCheckout', - 'allowCheckoutWithoutPayment', - 'allowPartialPaymentOnCheckout', - 'requireShippingAddressAtCheckout', - 'requireBillingAddressAtCheckout', - 'requireShippingMethodSelectionAtCheckout', - 'useBillingAddressForTax', - 'validateOrganizationTaxIdAsVatId', - ]; - - // Update stores env var DB columns - foreach ($storeProperties as $storeProperty) { - $this->alterColumn(Table::STORES, $storeProperty, $this->string()->notNull()->defaultValue('false')); - } - - // Loop through each store and update values in the DB to match the PC values - foreach ($stores as $store) { - $storeSettingsForStore = $storeSettings[$store['uid']] ?? null; - - // If there isn't data in the PC for this store, skip it - if (!$storeSettingsForStore) { - continue; - } - - $updateData = []; - foreach ($storeProperties as $storeProperty) { - // If there isn't data in the PC for this store property, skip it - if (!isset($storeSettingsForStore[$storeProperty])) { - continue; - } - - // Parse the value from the PC - $envVarValue = App::parseBooleanEnv($storeSettingsForStore[$storeProperty]); - if ($envVarValue === null) { - continue; - } - - $updateData[$storeProperty] = $storeSettingsForStore[$storeProperty]; - } - - if (empty($updateData)) { - continue; - } - - $this->update(Table::STORES, $updateData, ['id' => $store['id']]); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m241204_091901_fix_store_environment_variables cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m241213_083338_update_promotional_price_in_line_items.php b/src/migrations/m241213_083338_update_promotional_price_in_line_items.php deleted file mode 100644 index 10d587e421..0000000000 --- a/src/migrations/m241213_083338_update_promotional_price_in_line_items.php +++ /dev/null @@ -1,48 +0,0 @@ -select('id') - ->from(Table::ORDERS) - ->where(['isCompleted' => true]); - - $lineItemsQuery = (new Query()) - ->select('id') - ->from(Table::LINEITEMS) - ->where(['orderId' => $ordersQuery]) - ->andWhere(['promotionalPrice' => null]) - ->andWhere(new Expression('[[salePrice]] < [[price]]')) - ->column(); - - foreach (array_chunk($lineItemsQuery, 1000) as $chunk) { - $this->update(Table::LINEITEMS, ['promotionalPrice' => new Expression('[[salePrice]]')], ['id' => $chunk]); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m241213_083338_update_promotional_price_in_line_items cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m241219_071723_add_inventory_backorder.php b/src/migrations/m241219_071723_add_inventory_backorder.php deleted file mode 100644 index ed36f697e9..0000000000 --- a/src/migrations/m241219_071723_add_inventory_backorder.php +++ /dev/null @@ -1,33 +0,0 @@ -db->columnExists(Table::PURCHASABLES_STORES, 'allowOutOfStockPurchases')) { - $this->addColumn(Table::PURCHASABLES_STORES, 'allowOutOfStockPurchases', $this->boolean()->after('inventoryTracked')->notNull()->defaultValue(false)); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m241219_071723_add_inventory_backorder cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m241220_082900_remove_inventory_for_non_inventory_purchasables.php b/src/migrations/m241220_082900_remove_inventory_for_non_inventory_purchasables.php deleted file mode 100644 index 8926ae77ca..0000000000 --- a/src/migrations/m241220_082900_remove_inventory_for_non_inventory_purchasables.php +++ /dev/null @@ -1,46 +0,0 @@ -select(['items.id AS id', 'elements.type AS type']) - ->from(['items' => Table::INVENTORYITEMS]) - ->leftJoin(['elements' => CraftTable::ELEMENTS], '[[items.purchasableId]] = [[elements.id]]') - ->all(); - - // Only remove the donation inventory items that shouldn't be there, can do others later. - foreach ($purchasables as $purchasable) { - if (is_subclass_of($purchasable['type'], Donation::class)) { - if (!$purchasable['type']::hasInventory()) { // should always be false, but just in case - $this->delete(Table::INVENTORYITEMS, ['id' => $purchasable['id']]); - } - } - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m241220_082900_remove_inventory_for_non_inventory_purchasables cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m250120_080035_move_to_tax_id_validators.php b/src/migrations/m250120_080035_move_to_tax_id_validators.php deleted file mode 100644 index 55366b03f5..0000000000 --- a/src/migrations/m250120_080035_move_to_tax_id_validators.php +++ /dev/null @@ -1,40 +0,0 @@ -addColumn('{{%commerce_taxrates}}', 'taxIdValidators', $this->text()->after('isVat')); - - $taxRates = (new \craft\db\Query()) - ->select(['id', 'isVat']) - ->from(['{{%commerce_taxrates}}']) - ->all(); - - foreach ($taxRates as $taxRate) { - $taxIdValidators = $taxRate['isVat'] ? ['craft\commerce\taxidvalidators\EuVatIdValidator'] : []; - $this->update('{{%commerce_taxrates}}', ['taxIdValidators' => json_encode($taxIdValidators)], ['id' => $taxRate['id']]); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m250120_080035_move_to_tax_id_validators cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m250128_083515_add_make_primary_addresses_to_orders.php b/src/migrations/m250128_083515_add_make_primary_addresses_to_orders.php deleted file mode 100644 index ec8403fa7a..0000000000 --- a/src/migrations/m250128_083515_add_make_primary_addresses_to_orders.php +++ /dev/null @@ -1,32 +0,0 @@ -addColumn(Table::ORDERS, 'makePrimaryShippingAddress', $this->boolean()->defaultValue(false)->after('saveShippingAddressOnOrderComplete')); - $this->addColumn(Table::ORDERS, 'makePrimaryBillingAddress', $this->boolean()->defaultValue(false)->after('saveBillingAddressOnOrderComplete')); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m250128_083515_add_make_primary_addresses_to_orders cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m250129_080909_fix_discount_conditions.php b/src/migrations/m250129_080909_fix_discount_conditions.php deleted file mode 100644 index ca43bf99a5..0000000000 --- a/src/migrations/m250129_080909_fix_discount_conditions.php +++ /dev/null @@ -1,55 +0,0 @@ -select(['id', 'orderCondition', 'storeId']) - ->from(Table::DISCOUNTS) - ->all(); - - foreach ($discounts as $discount) { - $discountId = $discount['id']; - $storeId = $discount['storeId']; - $orderConditionData = Json::decodeIfJson($discount['orderCondition']); - - if (!is_array($orderConditionData)) { - continue; - } - - if (!isset($orderConditionData['storeId'])) { - $orderConditionData['storeId'] = $storeId; - $orderConditionJson = Json::encode($orderConditionData); - $this->update(Table::DISCOUNTS, - ['orderCondition' => $orderConditionJson], - ['id' => $discountId] - ); - } - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m250129_080909_fix_discount_conditions cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m250210_125139_fix_cart_recalculation_modes.php b/src/migrations/m250210_125139_fix_cart_recalculation_modes.php deleted file mode 100644 index 666031d434..0000000000 --- a/src/migrations/m250210_125139_fix_cart_recalculation_modes.php +++ /dev/null @@ -1,35 +0,0 @@ -update(Table::ORDERS, ['recalculationMode' => Order::RECALCULATION_MODE_ALL], [ - 'recalculationMode' => Order::RECALCULATION_MODE_NONE, - 'isCompleted' => false, - ], [], false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m250210_125139_fix_cart_recalculation_modes cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m250301_120000_add_gateway_order_condition.php b/src/migrations/m250301_120000_add_gateway_order_condition.php deleted file mode 100644 index df4a9b22a7..0000000000 --- a/src/migrations/m250301_120000_add_gateway_order_condition.php +++ /dev/null @@ -1,64 +0,0 @@ -addColumn(Table::GATEWAYS, 'orderCondition', $this->text()); - - $projectConfig = \Craft::$app->getProjectConfig(); - - $projectConfig->muteEvents = true; - - $gateways = (new Query()) - ->select(['id', 'uid', 'isArchived']) - ->from(Table::GATEWAYS) - ->all(); - - foreach ($gateways as $gateway) { - $config = $projectConfig->get(Gateways::CONFIG_GATEWAY_KEY . '.' . $gateway['uid']); - - $orderCondition = [ - 'class' => 'craft\\commerce\\elements\\conditions\\orders\\GatewayOrderCondition', - 'conditionRules' => [], - ]; - - $this->update(Table::GATEWAYS, - ['orderCondition' => json_encode($orderCondition)], - ['id' => $gateway['id']] - ); - - if ($config && !$gateway['isArchived']) { - $config['orderCondition'] = $orderCondition; - $projectConfig->set(Gateways::CONFIG_GATEWAY_KEY . '.' . $gateway['uid'], $config); - } - } - - $projectConfig->muteEvents = false; - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m250301_120000_add_gateway_order_condition cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m250401_091214_add_shipping_method_customer_condition.php b/src/migrations/m250401_091214_add_shipping_method_customer_condition.php deleted file mode 100644 index a28354331b..0000000000 --- a/src/migrations/m250401_091214_add_shipping_method_customer_condition.php +++ /dev/null @@ -1,31 +0,0 @@ -addColumn(Table::SHIPPINGMETHODS, 'customerCondition', $this->text()); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m250401_091214_add_shipping_method_customer_condition cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m250403_134328_add_shipping_rule_customer_condition.php b/src/migrations/m250403_134328_add_shipping_rule_customer_condition.php deleted file mode 100644 index cbd37bc337..0000000000 --- a/src/migrations/m250403_134328_add_shipping_rule_customer_condition.php +++ /dev/null @@ -1,31 +0,0 @@ -addColumn(Table::SHIPPINGRULES, 'customerCondition', $this->text()); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m250403_134328_add_shipping_rule_customer_condition cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m250616_042356_fix_field_layout_id.php b/src/migrations/m250616_042356_fix_field_layout_id.php deleted file mode 100644 index e17a73c322..0000000000 --- a/src/migrations/m250616_042356_fix_field_layout_id.php +++ /dev/null @@ -1,97 +0,0 @@ -select([ - 'v.id as variantId', - 'v.primaryOwnerId as productId', - 'p.typeId as productTypeId', - ]) - ->from(['v' => Table::VARIANTS]) - ->innerJoin(['e' => CraftTable::ELEMENTS], '[[v.id]] = [[e.id]]') - ->innerJoin(['p' => Table::PRODUCTS], '[[v.primaryOwnerId]] = [[p.id]]') - ->where(['e.type' => Variant::class]) - ->andWhere(['e.fieldLayoutId' => null]) - ->all(); - - if (empty($variantsToFix)) { - return true; - } - - // Group variants by product type - $variantsByProductType = []; - foreach ($variantsToFix as $variant) { - $variantsByProductType[$variant['productTypeId']][] = $variant['variantId']; - } - - // Get valid field layout IDs for each product type - $productTypes = (new Query()) - ->select(['pt.id', 'pt.variantFieldLayoutId']) - ->from(['pt' => Table::PRODUCTTYPES]) - ->innerJoin(['fl' => CraftTable::FIELDLAYOUTS], '[[pt.variantFieldLayoutId]] = [[fl.id]]') // Ensure field layout exists - ->where(['pt.id' => array_keys($variantsByProductType)]) - ->andWhere(['not', ['pt.variantFieldLayoutId' => null]]) - ->indexBy('id') - ->all(); - - $db = Craft::$app->getDb(); - $transaction = $db->beginTransaction(); - - try { - // Update variants with the correct field layout ID - foreach ($variantsByProductType as $productTypeId => $variantIds) { - // Check if we have a valid field layout for this product type - if (!isset($productTypes[$productTypeId])) { - continue; - } - - $fieldLayoutId = $productTypes[$productTypeId]['variantFieldLayoutId']; - - // Update in batches - foreach (array_chunk($variantIds, 500) as $chunk) { - $db->createCommand() - ->update( - CraftTable::ELEMENTS, - ['fieldLayoutId' => $fieldLayoutId], - ['id' => $chunk] - ) - ->execute(); - } - } - - $transaction->commit(); - } catch (\Throwable $e) { - $transaction->rollBack(); - throw $e; - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m250616_042356_fix_field_layout_id cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m250617_105249_add_email_render_site_id.php b/src/migrations/m250617_105249_add_email_render_site_id.php deleted file mode 100644 index b6ea5f1756..0000000000 --- a/src/migrations/m250617_105249_add_email_render_site_id.php +++ /dev/null @@ -1,64 +0,0 @@ -db->columnExists(Table::EMAILS, 'renderSiteId')) { - return true; - } - - $this->addColumn(Table::EMAILS, 'renderSiteId', $this->integer()->after('language')); - - // Get the primary site ID - $primarySite = Craft::$app->getSites()->getPrimarySite(); - - // For all current emails set the `renderSiteId` to the primary site to keep the existing behavior - $this->db->createCommand()->update( - Table::EMAILS, - ['renderSiteId' => $primarySite->id], - ['renderSiteId' => null] - )->execute(); - - // Update the project config - $projectConfig = Craft::$app->getProjectConfig(); - - $emails = $projectConfig->get('commerce.emails') ?? []; - $muteEvents = $projectConfig->muteEvents; - $projectConfig->muteEvents = true; - - foreach ($emails as $emailUid => $email) { - $email['renderSite'] = $primarySite->uid; - $projectConfig->set("commerce.emails.$emailUid", $email); - } - - $projectConfig->muteEvents = $muteEvents; - - // Add foreign key - $this->addForeignKey(null, Table::EMAILS, ['renderSiteId'], CraftTable::SITES, ['id'], 'SET NULL', 'CASCADE'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m250617_105249_add_email_render_site_id cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m250701_054128_add_defaultVariant_idex_to_products.php b/src/migrations/m250701_054128_add_defaultVariant_idex_to_products.php deleted file mode 100644 index 56d5fea2dc..0000000000 --- a/src/migrations/m250701_054128_add_defaultVariant_idex_to_products.php +++ /dev/null @@ -1,53 +0,0 @@ -select(['id']) - ->from(\craft\db\Table::ELEMENTS) - ->where(['type' => Variant::class]); - - $this->update( - Table::PRODUCTS, - ['defaultVariantId' => null], - ['not', ['defaultVariantId' => $subQuery]], - [], - ); - - $this->addForeignKey( - null, - Table::PRODUCTS, - 'defaultVariantId', - \craft\db\Table::ELEMENTS, - 'id', - 'SET NULL', - null - ); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m250701_054128_add_defaultVariant_idex_to_products.php cannot be reverted.\n"; - - return true; - } -} diff --git a/src/migrations/m250721_130616_fix_gateway_order_condition_pc.php b/src/migrations/m250721_130616_fix_gateway_order_condition_pc.php deleted file mode 100644 index 7c872e9ab1..0000000000 --- a/src/migrations/m250721_130616_fix_gateway_order_condition_pc.php +++ /dev/null @@ -1,64 +0,0 @@ -getProjectConfig(); - - $projectConfig->muteEvents = true; - - // Fix gateways with missing order conditions - $gateways = (new Query()) - ->select(['id', 'uid', 'isArchived']) - ->from(Table::GATEWAYS) - ->where(['orderCondition' => null]) - ->all(); - - foreach ($gateways as $gateway) { - $config = $projectConfig->get(Gateways::CONFIG_GATEWAY_KEY . '.' . $gateway['uid']); - - $orderCondition = [ - 'class' => 'craft\\commerce\\elements\\conditions\\orders\\GatewayOrderCondition', - 'conditionRules' => [], - ]; - - $this->update(Table::GATEWAYS, - ['orderCondition' => json_encode($orderCondition)], - ['id' => $gateway['id']] - ); - - if ($config && !$gateway['isArchived']) { - $config['orderCondition'] = $orderCondition; - $projectConfig->set(Gateways::CONFIG_GATEWAY_KEY . '.' . $gateway['uid'], $config); - } - } - - $projectConfig->muteEvents = false; - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m250721_130616_fix_gateway_order_condition_pc cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m250731_020627_add_slug_options_to_product_types.php b/src/migrations/m250731_020627_add_slug_options_to_product_types.php deleted file mode 100644 index 63b7205d79..0000000000 --- a/src/migrations/m250731_020627_add_slug_options_to_product_types.php +++ /dev/null @@ -1,43 +0,0 @@ -db->columnExists(Table::PRODUCTTYPES, 'showSlugField')) { - $this->addColumn(Table::PRODUCTTYPES, 'showSlugField', $this->boolean()->notNull()->defaultValue(true)->after('productTitleTranslationKeyFormat')); - } - - // Add slugTranslationMethod column - if (!$this->db->columnExists(Table::PRODUCTTYPES, 'slugTranslationMethod')) { - $this->addColumn(Table::PRODUCTTYPES, 'slugTranslationMethod', $this->string()->notNull()->defaultValue('site')->after('showSlugField')); - } - - // Add slugTranslationKeyFormat column - if (!$this->db->columnExists(Table::PRODUCTTYPES, 'slugTranslationKeyFormat')) { - $this->addColumn(Table::PRODUCTTYPES, 'slugTranslationKeyFormat', $this->string()->after('slugTranslationMethod')); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - return true; - } -} diff --git a/src/migrations/m250815_120000_add_gateway_address_conditions.php b/src/migrations/m250815_120000_add_gateway_address_conditions.php deleted file mode 100644 index b9640400ed..0000000000 --- a/src/migrations/m250815_120000_add_gateway_address_conditions.php +++ /dev/null @@ -1,74 +0,0 @@ -addColumn(Table::GATEWAYS, 'billingAddressCondition', $this->text()); - $this->addColumn(Table::GATEWAYS, 'shippingAddressCondition', $this->text()); - - $projectConfig = \Craft::$app->getProjectConfig(); - - $projectConfig->muteEvents = true; - - $gateways = (new Query()) - ->select(['id', 'uid', 'isArchived']) - ->from(Table::GATEWAYS) - ->all(); - - foreach ($gateways as $gateway) { - $config = $projectConfig->get(Gateways::CONFIG_GATEWAY_KEY . '.' . $gateway['uid']); - - $billingAddressCondition = [ - 'class' => 'craft\\commerce\\elements\\conditions\\addresses\\GatewayAddressCondition', - 'conditionRules' => [], - ]; - - $shippingAddressCondition = [ - 'class' => 'craft\\commerce\\elements\\conditions\\addresses\\GatewayAddressCondition', - 'conditionRules' => [], - ]; - - $this->update(Table::GATEWAYS, - [ - 'billingAddressCondition' => json_encode($billingAddressCondition), - 'shippingAddressCondition' => json_encode($shippingAddressCondition), - ], - ['id' => $gateway['id']] - ); - - if ($config && !$gateway['isArchived']) { - $config['billingAddressCondition'] = $billingAddressCondition; - $config['shippingAddressCondition'] = $shippingAddressCondition; - $projectConfig->set(Gateways::CONFIG_GATEWAY_KEY . '.' . $gateway['uid'], $config); - } - } - - $projectConfig->muteEvents = false; - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m250815_120000_add_gateway_address_conditions cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m250919_111358_fix_methodId_shipping_rules_fk.php b/src/migrations/m250919_111358_fix_methodId_shipping_rules_fk.php deleted file mode 100644 index 4e50466800..0000000000 --- a/src/migrations/m250919_111358_fix_methodId_shipping_rules_fk.php +++ /dev/null @@ -1,33 +0,0 @@ -dropForeignKeyIfExists(Table::SHIPPINGRULES, ['methodId']); - - $this->addForeignKey(null, Table::SHIPPINGRULES, ['methodId'], Table::SHIPPINGMETHODS, ['id'], 'CASCADE'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m250919_111358_fix_methodId_shipping_rules_fk cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m251003_120720_add_preview_targets_to_product_types.php b/src/migrations/m251003_120720_add_preview_targets_to_product_types.php deleted file mode 100644 index 79b829de4d..0000000000 --- a/src/migrations/m251003_120720_add_preview_targets_to_product_types.php +++ /dev/null @@ -1,34 +0,0 @@ -db->columnExists(Table::PRODUCTTYPES, 'previewTargets')) { - $this->addColumn(Table::PRODUCTTYPES, 'previewTargets', $this->json()->after('propagationMethod')); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m251003_120720_add_preview_targets_to_product_types cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m251028_095831_add_order_date_first_paid_property.php b/src/migrations/m251028_095831_add_order_date_first_paid_property.php deleted file mode 100644 index 948cf347cf..0000000000 --- a/src/migrations/m251028_095831_add_order_date_first_paid_property.php +++ /dev/null @@ -1,31 +0,0 @@ -addColumn(Table::ORDERS, 'dateFirstPaid', $this->dateTime()->after('datePaid')); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m251028_095831_add_order_date_first_paid_property cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m251030_094827_add_link_expiry_to_pdfs.php b/src/migrations/m251030_094827_add_link_expiry_to_pdfs.php deleted file mode 100644 index a283826f4f..0000000000 --- a/src/migrations/m251030_094827_add_link_expiry_to_pdfs.php +++ /dev/null @@ -1,31 +0,0 @@ -addColumn(Table::PDFS, 'linkExpiry', $this->integer()->notNull()->defaultValue(86400)->after('language')); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m251030_094827_add_link_expiry_to_pdfs cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m251105_194014_add_icon_and_color_to_categories_and_methods.php b/src/migrations/m251105_194014_add_icon_and_color_to_categories_and_methods.php deleted file mode 100644 index 727b933f5f..0000000000 --- a/src/migrations/m251105_194014_add_icon_and_color_to_categories_and_methods.php +++ /dev/null @@ -1,53 +0,0 @@ -db->columnExists(Table::SHIPPINGMETHODS, 'icon')) { - $this->addColumn(Table::SHIPPINGMETHODS, 'icon', $this->string()->after('handle')); - } - if (!$this->db->columnExists(Table::SHIPPINGMETHODS, 'color')) { - $this->addColumn(Table::SHIPPINGMETHODS, 'color', $this->string()->after('icon')); - } - - // Add icon and color to shipping categories - if (!$this->db->columnExists(Table::SHIPPINGCATEGORIES, 'icon')) { - $this->addColumn(Table::SHIPPINGCATEGORIES, 'icon', $this->string()->after('handle')); - } - if (!$this->db->columnExists(Table::SHIPPINGCATEGORIES, 'color')) { - $this->addColumn(Table::SHIPPINGCATEGORIES, 'color', $this->string()->after('icon')); - } - - // Add icon and color to tax categories - if (!$this->db->columnExists(Table::TAXCATEGORIES, 'icon')) { - $this->addColumn(Table::TAXCATEGORIES, 'icon', $this->string()->after('handle')); - } - if (!$this->db->columnExists(Table::TAXCATEGORIES, 'color')) { - $this->addColumn(Table::TAXCATEGORIES, 'color', $this->string()->after('icon')); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m251105_194014_add_icon_and_color_to_categories_and_methods cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m251111_092942_ensure_catalog_pricing_indexes.php b/src/migrations/m251111_092942_ensure_catalog_pricing_indexes.php deleted file mode 100644 index 01b463eb96..0000000000 --- a/src/migrations/m251111_092942_ensure_catalog_pricing_indexes.php +++ /dev/null @@ -1,38 +0,0 @@ -createIndexIfMissing(Table::CATALOG_PRICING, 'catalogPricingRuleId', false); - $this->createIndexIfMissing(Table::CATALOG_PRICING, 'isPromotionalPrice', false); - $this->createIndexIfMissing(Table::CATALOG_PRICING, 'purchasableId', false); - $this->createIndexIfMissing(Table::CATALOG_PRICING, 'storeId', false); - $this->createIndexIfMissing(Table::CATALOG_PRICING, 'userId', false); - $this->createIndexIfMissing(Table::CATALOG_PRICING, ['purchasableId', 'storeId', 'isPromotionalPrice', 'price', 'catalogPricingRuleId', 'dateFrom', 'dateTo'], false); - $this->createIndexIfMissing(Table::CATALOG_PRICING, ['purchasableId', 'storeId', 'isPromotionalPrice', 'price'], false); - $this->createIndexIfMissing(Table::CATALOG_PRICING, ['purchasableId', 'storeId'], false); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m251111_092942_ensure_catalog_pricing_indexes cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m251112_120000_fix_null_gateway_order_condition.php b/src/migrations/m251112_120000_fix_null_gateway_order_condition.php deleted file mode 100644 index 38217422da..0000000000 --- a/src/migrations/m251112_120000_fix_null_gateway_order_condition.php +++ /dev/null @@ -1,65 +0,0 @@ -getProjectConfig(); - - $projectConfig->muteEvents = true; - - // Fix gateways with missing order conditions - $gateways = (new Query()) - ->select(['id', 'uid', 'isArchived']) - ->from(Table::GATEWAYS) - ->where(['orderCondition' => null]) - ->all(); - - if (!empty($gateways)) { - foreach ($gateways as $gateway) { - $config = $projectConfig->get(Gateways::CONFIG_GATEWAY_KEY . '.' . $gateway['uid']); - - $orderCondition = [ - 'class' => 'craft\\commerce\\elements\\conditions\\orders\\GatewayOrderCondition', - 'conditionRules' => [], - ]; - - $this->update(Table::GATEWAYS, - ['orderCondition' => json_encode($orderCondition)], - ['id' => $gateway['id']] - ); - - if ($config && !$gateway['isArchived']) { - $config['orderCondition'] = $orderCondition; - $projectConfig->set(Gateways::CONFIG_GATEWAY_KEY . '.' . $gateway['uid'], $config); - } - } - } - - $projectConfig->muteEvents = false; - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m251112_120000_fix_null_gateway_order_condition cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m260206_000000_add_ui_label_formats.php b/src/migrations/m260206_000000_add_ui_label_formats.php deleted file mode 100644 index 10cddc2415..0000000000 --- a/src/migrations/m260206_000000_add_ui_label_formats.php +++ /dev/null @@ -1,52 +0,0 @@ -db->columnExists(Table::PRODUCTTYPES, 'variantUiLabelFormat')) { - $this->addColumn( - Table::PRODUCTTYPES, - 'variantUiLabelFormat', - $this->string()->notNull()->defaultValue('{title}')->after('variantTitleTranslationKeyFormat') - ); - } - - if (!$this->db->columnExists(Table::PRODUCTTYPES, 'productUiLabelFormat')) { - $this->addColumn( - Table::PRODUCTTYPES, - 'productUiLabelFormat', - $this->string()->notNull()->defaultValue('{title}')->after('productTitleTranslationKeyFormat') - ); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - $this->dropColumn(Table::PRODUCTTYPES, 'variantUiLabelFormat'); - $this->dropColumn(Table::PRODUCTTYPES, 'productUiLabelFormat'); - - return true; - } -} diff --git a/src/migrations/m260226_120000_product_type_permissions.php b/src/migrations/m260226_120000_product_type_permissions.php deleted file mode 100644 index a701327eeb..0000000000 --- a/src/migrations/m260226_120000_product_type_permissions.php +++ /dev/null @@ -1,115 +0,0 @@ -select(['uid']) - ->from('{{%commerce_producttypes}}') - ->column($this->db); - - // Build the permission mapping - $map = []; // oldPermission => [newPermission, ...] - foreach ($productTypeUids as $uid) { - // commerce-editProductType → commerce-viewProductType + commerce-saveProductType - $map[strtolower("commerce-editProductType:$uid")] = [ - strtolower("commerce-viewProductType:$uid"), - strtolower("commerce-saveProductType:$uid"), - ]; - // commerce-createProducts → commerce-createProductType - $map[strtolower("commerce-createProducts:$uid")] = [ - strtolower("commerce-createProductType:$uid"), - ]; - // commerce-deleteProducts → commerce-deleteProductType - $map[strtolower("commerce-deleteProducts:$uid")] = [ - strtolower("commerce-deleteProductType:$uid"), - ]; - } - - // Migrate user permissions in the database - foreach ($map as $oldPermission => $newPermissions) { - // Find all users with the old permission - $userIds = (new Query()) - ->select(['upu.userId']) - ->from(['upu' => Table::USERPERMISSIONS_USERS]) - ->innerJoin(['up' => Table::USERPERMISSIONS], '[[up.id]] = [[upu.permissionId]]') - ->where(['up.name' => $oldPermission]) - ->column($this->db); - - $userIds = array_unique($userIds); - - if (!empty($userIds)) { - foreach ($newPermissions as $newPermission) { - // Delete the permission if it already exists - $this->delete(Table::USERPERMISSIONS, [ - 'name' => $newPermission, - ]); - - $this->insert(Table::USERPERMISSIONS, [ - 'name' => $newPermission, - ]); - $newPermissionId = $this->db->getLastInsertID(Table::USERPERMISSIONS); - - $insert = []; - foreach ($userIds as $userId) { - $insert[] = [$newPermissionId, $userId]; - } - - $this->batchInsert(Table::USERPERMISSIONS_USERS, ['permissionId', 'userId'], $insert); - } - } - } - - // Migrate project config for user groups - $projectConfig = Craft::$app->getProjectConfig(); - - foreach ($projectConfig->get('users.groups') ?? [] as $uid => $group) { - $groupPermissions = array_flip($group['permissions'] ?? []); - $save = false; - - foreach ($map as $oldPermission => $newPermissions) { - if (isset($groupPermissions[$oldPermission])) { - foreach ($newPermissions as $newPermission) { - $groupPermissions[$newPermission] = true; - } - $save = true; - } - } - - if ($save) { - $projectConfig->set("users.groups.$uid.permissions", array_keys($groupPermissions)); - } - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - // Permission migrations are not reversible - return true; - } -} diff --git a/src/migrations/m260327_000000_ensure_link_expiry_on_pdfs.php b/src/migrations/m260327_000000_ensure_link_expiry_on_pdfs.php deleted file mode 100644 index d2cafab9ad..0000000000 --- a/src/migrations/m260327_000000_ensure_link_expiry_on_pdfs.php +++ /dev/null @@ -1,33 +0,0 @@ -db->columnExists(Table::PDFS, 'linkExpiry')) { - $this->addColumn(Table::PDFS, 'linkExpiry', $this->integer()->notNull()->defaultValue(86400)->after('language')); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m260327_000000_ensure_link_expiry_on_pdfs cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m260407_000000_add_catalog_pricing_queue_table.php b/src/migrations/m260407_000000_add_catalog_pricing_queue_table.php deleted file mode 100644 index 0084068a2f..0000000000 --- a/src/migrations/m260407_000000_add_catalog_pricing_queue_table.php +++ /dev/null @@ -1,47 +0,0 @@ -db->tableExists(Table::CATALOG_PRICING_QUEUE)) { - $this->createTable(Table::CATALOG_PRICING_QUEUE, [ - 'id' => $this->primaryKey(), - 'storeId' => $this->integer(), - 'type' => $this->enum('type', [CatalogPricingQueue::TYPE_PURCHASABLE, CatalogPricingQueue::TYPE_RULE])->notNull(), - 'ids' => $this->mediumText(), - 'reserved' => $this->boolean()->notNull()->defaultValue(false), - 'dateCreated' => $this->dateTime()->notNull(), - 'dateUpdated' => $this->dateTime()->notNull(), - 'uid' => $this->uid(), - ]); - } - - $this->createIndexIfMissing(Table::CATALOG_PRICING_QUEUE, 'reserved', false); - $this->createIndexIfMissing(Table::CATALOG_PRICING_QUEUE, ['storeId', 'type', 'reserved'], false); - $this->addForeignKey(null, Table::CATALOG_PRICING_QUEUE, ['storeId'], Table::STORES, ['id'], 'CASCADE', 'CASCADE'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m260407_000000_add_catalog_pricing_queue_table cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m260505_071943_add_orders_customerDeleted_column.php b/src/migrations/m260505_071943_add_orders_customerDeleted_column.php deleted file mode 100644 index 21be29d4f3..0000000000 --- a/src/migrations/m260505_071943_add_orders_customerDeleted_column.php +++ /dev/null @@ -1,36 +0,0 @@ -getDb()->columnExists(Table::ORDERS, 'customerDeleted')) { - return true; - } - - $this->addColumn(Table::ORDERS, 'customerDeleted', $this->boolean()->notNull()->defaultValue(false)->after('customerId')); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m260505_071943_add_orders_customerDeleted_column cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m260506_000000_fix_fulfilled_check_constraint.php b/src/migrations/m260506_000000_fix_fulfilled_check_constraint.php deleted file mode 100644 index e10eda361a..0000000000 --- a/src/migrations/m260506_000000_fix_fulfilled_check_constraint.php +++ /dev/null @@ -1,52 +0,0 @@ -db->getIsPgsql()) { - $tableNameQuoted = $this->db->quoteTableName('{{%commerce_inventorytransactions}}'); - $typeColumnQuoted = $this->db->quoteColumnName('type'); - - // Old constraint: auto-named by PostgreSQL when the table was created as commerce_inventorymovements - $oldConstraint = $this->db->getSchema()->getRawTableName('{{%commerce_inventorymovements}}') . '_type_check'; - // New constraint: added by the previous alterColumn migration (m240228_060604) - $newConstraint = 'commerce_inventorytransactions_type_check'; - - foreach ([$oldConstraint, $newConstraint] as $constraint) { - $this->db->createCommand( - "ALTER TABLE $tableNameQuoted DROP CONSTRAINT IF EXISTS " . $this->db->quoteColumnName($constraint) - )->execute(); - } - - $this->db->createCommand( - "ALTER TABLE $tableNameQuoted ADD CONSTRAINT commerce_inventorytransactions_type_check CHECK ($typeColumnQuoted IN ('available', 'reserved', 'damaged', 'safety', 'qualityControl', 'committed', 'fulfilled', 'incoming'))" - )->execute(); - } - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m260506_000000_fix_fulfilled_check_constraint cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m260507_000000_subscriptions_nullable_userId.php b/src/migrations/m260507_000000_subscriptions_nullable_userId.php deleted file mode 100644 index 44efa7fa47..0000000000 --- a/src/migrations/m260507_000000_subscriptions_nullable_userId.php +++ /dev/null @@ -1,35 +0,0 @@ -dropForeignKeyIfExists(Table::SUBSCRIPTIONS, ['userId']); - $this->addForeignKey(null, Table::SUBSCRIPTIONS, ['userId'], CraftTable::USERS, ['id'], 'CASCADE'); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m260507_000000_subscriptions_nullable_userId cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m260615_000000_add_notice_type_to_order_notices.php b/src/migrations/m260615_000000_add_notice_type_to_order_notices.php deleted file mode 100644 index c1b7d5dac5..0000000000 --- a/src/migrations/m260615_000000_add_notice_type_to_order_notices.php +++ /dev/null @@ -1,32 +0,0 @@ -db->columnExists(Table::ORDERNOTICES, 'noticeType')) { - $this->addColumn(Table::ORDERNOTICES, 'noticeType', $this->string()->notNull()->defaultValue('customer')); - } - - return true; - } - - public function safeDown(): bool - { - if ($this->db->columnExists(Table::ORDERNOTICES, 'noticeType')) { - $this->dropColumn(Table::ORDERNOTICES, 'noticeType'); - } - - return true; - } -} diff --git a/src/migrations/m260616_000000_rename_allVariants_changedattributes.php b/src/migrations/m260616_000000_rename_allVariants_changedattributes.php deleted file mode 100644 index a441882ac9..0000000000 --- a/src/migrations/m260616_000000_rename_allVariants_changedattributes.php +++ /dev/null @@ -1,63 +0,0 @@ -allVariants (which no longer exists), throwing an UnknownPropertyException - // when opening a product with a provisional draft. - // This migration renames any lingering 'allVariants' entries to 'variants' for Product elements. - - $productSubquery = (new Query()) - ->select(['id']) - ->from([Table::ELEMENTS]) - ->where(['type' => 'craft\commerce\elements\Product']); - - // Insert 'variants' rows for Products that have 'allVariants' but no existing 'variants' entry - $select = (new Query()) - ->select(['ca.elementId', 'ca.siteId', new Expression("'variants'"), 'ca.dateUpdated', 'ca.propagated', 'ca.userId']) - ->from(['ca' => '{{%changedattributes}}']) - ->where(['ca.attribute' => 'allVariants', 'ca.elementId' => $productSubquery]) - ->andWhere('NOT EXISTS (SELECT 1 FROM {{%changedattributes}} [[ca2]] WHERE [[ca2.elementId]] = [[ca.elementId]] AND [[ca2.siteId]] = [[ca.siteId]] AND [[ca2.attribute]] = \'variants\')'); - - [$sql, $params] = $this->db->getQueryBuilder()->build($select); - $table = $this->db->quoteTableName('{{%changedattributes}}'); - $this->db->createCommand( - "INSERT INTO $table ([[elementId]], [[siteId]], [[attribute]], [[dateUpdated]], [[propagated]], [[userId]]) $sql", - $params - )->execute(); - - // Delete all 'allVariants' rows for Products - $this->delete('{{%changedattributes}}', [ - 'attribute' => 'allVariants', - 'elementId' => $productSubquery, - ]); - - return true; - } - - /** - * @inheritdoc - */ - public function safeDown(): bool - { - echo "m260616_000000_rename_allVariants_changedattributes cannot be reverted.\n"; - return false; - } -} diff --git a/src/models/CatalogPricing.php b/src/models/CatalogPricing.php deleted file mode 100644 index 335e8530fc..0000000000 --- a/src/models/CatalogPricing.php +++ /dev/null @@ -1,176 +0,0 @@ - - * @since 5.0.0 - */ -class CatalogPricing extends Model implements HasStoreInterface -{ - use StoreTrait; - - /** - * @var int|null - */ - public ?int $id = null; - - /** - * @var int|null - */ - public ?int $purchasableId = null; - - /** - * @var float|null - */ - public ?float $price = null; - - /** - * @var int|null - */ - public ?int $catalogPricingRuleId = null; - - /** - * @var \DateTime|null - */ - public ?\DateTime $dateFrom = null; - - /** - * @var \DateTime|null - */ - public ?\DateTime $dateTo = null; - - /** - * @var bool - */ - public bool $isPromotionalPrice = false; - - /** - * @var bool - */ - public bool $hasUpdatePending = false; - - /** - * @var string|null - */ - public ?string $uid = null; - - /** - * @var CatalogPricingRule|null - */ - private ?CatalogPricingRule $_catalogPricingRule = null; - - /** - * @var PurchasableInterface|null - */ - private ?PurchasableInterface $_purchasable = null; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [[ - 'catalogPricingRuleId', - 'dateFrom', - 'dateTo', - 'hasUpdatePending', - 'id', - 'isPromotionalPrice', - 'price', - 'purchasableId', - 'storeId', - 'uid', - ], 'safe']; - - return $rules; - } - - /** - * @throws InvalidConfigException - */ - public function behaviors(): array - { - $behaviors = parent::behaviors(); - - $behaviors['currencyAttributes'] = [ - 'class' => CurrencyAttributeBehavior::class, - 'currencyAttributes' => $this->currencyAttributes(), - ]; - - return $behaviors; - } - - /** - * @return array - */ - public function currencyAttributes(): array - { - return [ - 'price', - ]; - } - - /** - * @return PurchasableInterface|null - * @throws InvalidConfigException - * @throws SiteNotFoundException - */ - public function getPurchasable(): ?PurchasableInterface - { - if ($this->_purchasable !== null) { - return $this->_purchasable; - } - - if ($this->purchasableId === null || $this->storeId === null) { - return null; - } - - if (!$store = Plugin::getInstance()->getStores()->getStoreById($this->storeId)) { - throw new InvalidConfigException('Invalid store ID: ' . $this->storeId); - } - - // @TODO Resolve the correct site for the purchasable lookup rather than defaulting to the store's first site; catalog pricing is currently store-scoped but purchasables are site-aware - $site = $store->getSites()->first(); - - $this->_purchasable = Plugin::getInstance()->getPurchasables()->getPurchasableById($this->purchasableId, $site->id); - - return $this->_purchasable; - } - - /** - * @return CatalogPricingRule|null - * @throws InvalidConfigException - */ - public function getCatalogPricingRule(): ?CatalogPricingRule - { - if ($this->_catalogPricingRule !== null) { - return $this->_catalogPricingRule; - } - - if (!$this->catalogPricingRuleId) { - return null; - } - - $this->_catalogPricingRule = Plugin::getInstance()->getCatalogPricingRules()->getCatalogPricingRuleById($this->catalogPricingRuleId, $this->storeId); - - return $this->_catalogPricingRule; - } -} diff --git a/src/models/CatalogPricingRule.php b/src/models/CatalogPricingRule.php deleted file mode 100644 index 15c38f6feb..0000000000 --- a/src/models/CatalogPricingRule.php +++ /dev/null @@ -1,526 +0,0 @@ - - * @since 5.0.0 - */ -class CatalogPricingRule extends Model implements HasStoreInterface -{ - use StoreTrait; - - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string|null Name - */ - public ?string $name = null; - - /** - * @var string|null Description - */ - public ?string $description = null; - - /** - * @var DateTime|null Date From - */ - public ?DateTime $dateFrom = null; - - /** - * @var DateTime|null Date To - */ - public ?DateTime $dateTo = null; - - /** - * @var string How the sale should be applied - */ - public string $apply = PricingCatalogRuleRecord::APPLY_BY_PERCENT; - - /** - * @var float|null The amount field used by the apply option - */ - public ?float $applyAmount = null; - - /** - * @var string - */ - public string $applyPriceType = PricingCatalogRuleRecord::APPLY_PRICE_TYPE_PRICE; - - /** - * @var ElementConditionInterface|null - * @see getCustomerCondition() - * @see setCustomerCondition() - */ - public null|ElementConditionInterface $_customerCondition = null; - - /** - * @var ElementConditionInterface|null - * @see getProductCondition() - * @see setProductCondition() - */ - public null|ElementConditionInterface $_productCondition = null; - /** - * @var ElementConditionInterface|null - * @see getVariantCondition() - * @see setVariantCondition() - */ - public null|ElementConditionInterface $_variantCondition = null; - - /** - * @var ElementConditionInterface|null - * @see getPurchasableCondition() - * @see setPurchasableCondition() - */ - public null|ElementConditionInterface $_purchasableCondition = null; - - /** - * @var bool Enabled - */ - public bool $enabled = true; - - /** - * @var bool - */ - public bool $isPromotionalPrice = false; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateCreated = null; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateUpdated = null; - - /** - * @var int[]|null Product Ids - */ - private ?array $_purchasableIds = null; - - /** - * @var int[]|null - */ - private ?array $_userIds = null; - - /** - * @var array - * @todo Remove the unused $_metadata property in Commerce 6.0 - */ - private array $_metadata = []; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['apply'], 'in', 'range' => ['toPercent', 'toFlat', 'byPercent', 'byFlat']], - [['enabled'], 'boolean'], - [['name', 'apply'], 'required'], - [[ - 'applyAmount', - 'applyPriceType', - 'customerCondition', - 'dateUpdated', - 'dateCreated', - 'dateFrom', - 'dateTo', - 'description', - 'id', - 'isPromotionalPrice', - 'metadata', - 'productCondition', - 'purchasableCondition', - 'storeId', - 'variantCondition', - ], 'safe'], - ]; - } - - public function getCpEditUrl(): string - { - return $this->getStore()->getStoreSettingsUrl('pricing-rules/' . $this->id); - } - - /** - * @return array - */ - public function extraFields(): array - { - $fields = parent::extraFields(); - $fields[] = 'purchasableIds'; - - return $fields; - } - - /** - * @return string - */ - public function getApplyAmountAsPercent(): string - { - return Craft::$app->getFormatter()->asPercent(-($this->applyAmount ?? 0.0)); - } - - /** - * @return string - */ - public function getApplyAmountAsFlat(): string - { - return $this->applyAmount !== null ? (string)($this->applyAmount * -1) : '0'; - } - - /** - * @param string|array $metadata - * @return void - */ - public function setMetadata(string|array $metadata): void - { - $metadata = Json::decodeIfJson($metadata); - - if (!is_array($metadata)) { - $metadata = []; - } - - $this->_metadata = $metadata; - } - - /** - * @return array - */ - public function getMetadata(): array - { - return $this->_metadata; - } - - /** - * @return int[]|null - */ - public function getPurchasableIds(): ?array - { - if ($this->_purchasableIds === null) { - $siteIds = $this->getStore()->getSites()->map(fn(Site $site) => $site->id)->all(); - $productVariantIds = null; - - if (!empty($this->getProductCondition()->getConditionRules())) { - $productQuery = Product::find(); - $productQuery->siteId($siteIds); - /** @var CatalogPricingRuleProductCondition $productCondition */ - $productCondition = $this->getProductCondition(); - $productCondition->modifyQuery($productQuery); - - $productVariantIds = []; - if ($productIds = $productQuery->ids()) { - $productVariantIdsQuery = Variant::find() - ->siteId($siteIds) - ->productId($productIds); - - // If the rule is generating a promotional price, we need to make sure the purchasable is promotable - if ($this->isPromotionalPrice) { - $productVariantIdsQuery->andWhere(Db::parseBooleanParam('purchasables_stores.promotable', true)); - } - - $productVariantIds = $productVariantIdsQuery->ids(); - } - } - - // If there are product condition rules and they have returned no variant IDs that means there are no products that matched - // We can skip out early as the rest of the conditions will not be met - if ($productVariantIds === []) { - $this->_purchasableIds = []; - return $this->_purchasableIds; - } - - $this->_purchasableIds = $productVariantIds; - - $variantIds = $productVariantIds; - if (!empty($this->getVariantCondition()->getConditionRules())) { - $variantQuery = Variant::find(); - $variantQuery->siteId($siteIds); - /** @var CatalogPricingRuleVariantCondition $variantCondition */ - $variantCondition = $this->getVariantCondition(); - $variantCondition->modifyQuery($variantQuery); - - // If the rule is generating a promotional price, we need to make sure the purchasable is promotable - if ($this->isPromotionalPrice) { - $variantQuery->andWhere(Db::parseBooleanParam('purchasables_stores.promotable', true)); - } - - // If there are product condition rules we need to ensure the variant is in the list of product variants - if ($productVariantIds !== null) { - $variantQuery->andWhere(['commerce_variants.id' => $productVariantIds]); - } - - $variantIds = $variantQuery->ids(); - } - - // If there are variant condition rules and they have returned no variant IDs that means there are no variants that matched - // We can skip out early as the rest of the conditions will not be met - if ($variantIds === []) { - $this->_purchasableIds = []; - return $this->_purchasableIds; - } - - $this->_purchasableIds = $variantIds; - - if (!empty($this->getPurchasableCondition()->getConditionRules())) { - $purchasableQuery = Purchasable::find(); - - /** @var CatalogPricingRulePurchasableCondition $purchasableCondition */ - $purchasableCondition = $this->getPurchasableCondition(); - $purchasableCondition->modifyQuery($purchasableQuery); - - // If there are product/variant condition rules we need to ensure the purchasable is in the list of product variants - if ($variantIds !== null) { - $purchasableQuery->andWhere(['id' => $variantIds]); - } - - // We are unable to use `siteId()` on the purchasable query as it is only the subquery part that is used. - - // If the rule is generating a promotional price, we need to make sure the purchasable is promotable - if ($this->isPromotionalPrice) { - $purchasableQuery->andWhere(Db::parseBooleanParam('purchasables_stores.promotable', true)); - } - - // Do this adjustment to the query once (was previously using `Event::once` but this caused issues in some edge cases) - $purchasableQuery->on(ElementQuery::EVENT_AFTER_PREPARE, [$this, 'afterPreparePurchasableQuery'], ['siteIds' => $siteIds]); - $this->_purchasableIds = $purchasableQuery->ids(); - $purchasableQuery->off(ElementQuery::EVENT_AFTER_PREPARE, [$this, 'afterPreparePurchasableQuery']); - } - - $this->_purchasableIds = $this->_purchasableIds !== null ? array_unique($this->_purchasableIds) : null; - } - - return $this->_purchasableIds; - } - - /** - * @param CancelableEvent $event - * @return void - * @since 5.5.1 - */ - public function afterPreparePurchasableQuery(CancelableEvent $event): void - { - foreach ($event->sender->subQuery->where as &$value) { - if (is_array($value) && isset($value['elements_sites.siteId'])) { - $value['elements_sites.siteId'] = $event->data['siteIds']; - } - } - - $event->sender->subQuery->join[] = ['LEFT JOIN', ['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]']; - $event->sender->subQuery->join[] = ['LEFT JOIN', ['purchasables_stores' => Table::PURCHASABLES_STORES], '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[elements.id]]']; - } - - /** - * @return ElementConditionInterface - */ - public function getCustomerCondition(): ElementConditionInterface - { - $condition = $this->_customerCondition ?? new CatalogPricingRuleCustomerCondition(); - $condition->mainTag = 'div'; - $condition->name = 'customerCondition'; - - return $condition; - } - - /** - * @param ElementConditionInterface|string|array $condition - * @return void - * @throws InvalidConfigException - */ - public function setCustomerCondition(ElementConditionInterface|string|array $condition): void - { - if (is_string($condition)) { - $condition = Json::decodeIfJson($condition); - } - - if (!$condition instanceof ElementConditionInterface) { - $condition['class'] = CatalogPricingRuleCustomerCondition::class; - $condition = Craft::$app->getConditions()->createCondition($condition); - /** @var CatalogPricingRuleCustomerCondition $condition */ - } - $condition->forProjectConfig = false; - - $this->_customerCondition = $condition; - } - - /** - * @return ElementConditionInterface - */ - public function getPurchasableCondition(): ElementConditionInterface - { - $condition = $this->_purchasableCondition ?? new CatalogPricingRulePurchasableCondition(); - $condition->mainTag = 'div'; - $condition->name = 'purchasableCondition'; - - return $condition; - } - - /** - * @param ElementConditionInterface|string|array $condition - * @return void - * @throws InvalidConfigException - */ - public function setPurchasableCondition(ElementConditionInterface|string|array $condition): void - { - if (is_string($condition)) { - $condition = Json::decodeIfJson($condition); - } - - if (!$condition instanceof ElementConditionInterface) { - $condition['class'] = CatalogPricingRulePurchasableCondition::class; - $condition = Craft::$app->getConditions()->createCondition($condition); - /** @var CatalogPricingRulePurchasableCondition $condition */ - } - $condition->forProjectConfig = false; - - $this->_purchasableCondition = $condition; - } - - /** - * @return ElementConditionInterface - */ - public function getProductCondition(): ElementConditionInterface - { - $condition = $this->_productCondition ?? new CatalogPricingRuleProductCondition(); - $condition->mainTag = 'div'; - $condition->name = 'productCondition'; - $condition->elementType = Product::class; - - return $condition; - } - - /** - * @param ElementConditionInterface|string|array $condition - * @return void - * @throws InvalidConfigException - */ - public function setProductCondition(ElementConditionInterface|string|array $condition): void - { - if (is_string($condition)) { - $condition = Json::decodeIfJson($condition); - } - - if (!$condition instanceof ElementConditionInterface) { - $condition['class'] = CatalogPricingRuleProductCondition::class; - $condition = Craft::$app->getConditions()->createCondition($condition); - /** @var CatalogPricingRuleProductCondition $condition */ - } - $condition->forProjectConfig = false; - - $this->_productCondition = $condition; - } - - /** - * @return ElementConditionInterface - */ - public function getVariantCondition(): ElementConditionInterface - { - $condition = $this->_variantCondition ?? new CatalogPricingRuleVariantCondition(); - $condition->mainTag = 'div'; - $condition->name = 'variantCondition'; - $condition->elementType = Variant::class; - - return $condition; - } - - /** - * @param ElementConditionInterface|string|array $condition - * @return void - * @throws InvalidConfigException - */ - public function setVariantCondition(ElementConditionInterface|string|array $condition): void - { - if (is_string($condition)) { - $condition = Json::decodeIfJson($condition); - } - - if (!$condition instanceof ElementConditionInterface) { - $condition['class'] = CatalogPricingRuleVariantCondition::class; - $condition = Craft::$app->getConditions()->createCondition($condition); - /** @var CatalogPricingRuleVariantCondition $condition */ - } - $condition->forProjectConfig = false; - - $this->_variantCondition = $condition; - } - - /** - * @return int[]|null - */ - public function getUserIds(): ?array - { - if ($this->_userIds === null && !empty($this->getCustomerCondition()->getConditionRules())) { - $userQuery = User::find(); - $this->getCustomerCondition()->modifyQuery($userQuery); - $this->_userIds = $userQuery->ids(); - } - - return $this->_userIds; - } - - /** - * @param float $price - * @return float - */ - public function getRulePriceFromPrice(float $price): float - { - $price = match ($this->apply) { - PricingCatalogRuleRecord::APPLY_BY_PERCENT => $price * (1 + $this->applyAmount), - PricingCatalogRuleRecord::APPLY_BY_FLAT => $price + $this->applyAmount, - PricingCatalogRuleRecord::APPLY_TO_PERCENT => $price * -$this->applyAmount, - PricingCatalogRuleRecord::APPLY_TO_FLAT => -$this->applyAmount, - default => $price, - }; - - $price = (float)Plugin::getInstance()->getCurrencies()->getTeller($this->getStore()->getCurrency())->convertToString($price); - - return max($price, 0); - } -} diff --git a/src/models/Coupon.php b/src/models/Coupon.php deleted file mode 100644 index 2831dd4da4..0000000000 --- a/src/models/Coupon.php +++ /dev/null @@ -1,59 +0,0 @@ - - * @since 4.0 - */ -class Coupon extends Model -{ - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var int|null Discount ID - */ - public ?int $discountId = null; - - /** - * @var string|null The coupon code - */ - public ?string $code = null; - - /** - * @var int Number of times the coupon has been used - */ - public int $uses = 0; - - /** - * @var int|null Number of times the coupon has been used - */ - public ?int $maxUses = null; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['id', 'code', 'discountId', 'uses', 'maxUses'], 'safe']; - $rules[] = [['code'], 'required']; - $rules[] = [['code'], UniqueValidator::class, 'targetClass' => CouponRecord::class]; - - return $rules; - } -} diff --git a/src/models/Discount.php b/src/models/Discount.php deleted file mode 100644 index 1900cd86f9..0000000000 --- a/src/models/Discount.php +++ /dev/null @@ -1,739 +0,0 @@ - - * @since 2.0 - */ -class Discount extends Model implements HasStoreInterface -{ - use StoreTrait; - - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string Name of the discount - */ - public string $name = ''; - - /** - * @var string|null The description of this discount - */ - public ?string $description = null; - - /** - * @var string Format coupons should be generated with - * @since 4.0 - */ - public string $couponFormat = Coupons::DEFAULT_COUPON_FORMAT; - - /** - * @var ElementConditionInterface|null - * @see getOrderCondition() - * @see setOrderCondition() - */ - public null|ElementConditionInterface $_orderCondition = null; - - /** - * @var ElementConditionInterface|null - * @see getCustomerCondition() - * @see setCustomerCondition() - */ - public null|ElementConditionInterface $_customerCondition = null; - - /** - * @var ElementConditionInterface|null - * @see getShippingAddressCondition() - * @see setShippingAddressCondition() - */ - public null|ElementConditionInterface $_shippingAddressCondition = null; - - /** - * @var ElementConditionInterface|null - * @see getBillingAddressCondition() - * @see setBillingAddressCondition() - */ - public null|ElementConditionInterface $_billingAddressCondition = null; - - /** - * @var bool Requires a coupon code to be applied - * @since 5.2.0 - */ - public bool $requireCouponCode = false; - - /** - * @var int Per user coupon use limit - */ - public int $perUserLimit = 0; - - /** - * @var int Per email coupon use limit - */ - public int $perEmailLimit = 0; - - /** - * @var int Total use limit by users - * @since 3.0 - */ - public int $totalDiscountUseLimit = 0; - - /** - * @var int Total use counter; - * @since 3.0 - */ - public int $totalDiscountUses = 0; - - /** - * @var DateTime|null Date the discount is valid from - */ - public ?DateTime $dateFrom = null; - - /** - * @var DateTime|null Date the discount is valid to - */ - public ?DateTime $dateTo = null; - - /** - * @var float Total minimum spend on matching items - */ - public float $purchaseTotal = 0; - - /** - * @var string|null Condition that must match to match the order, null or empty string means match all - */ - public ?string $orderConditionFormula = null; - - /** - * @var int Total minimum qty of matching items - */ - public int $purchaseQty = 0; - - /** - * @var int Total maximum spend on matching items - */ - public int $maxPurchaseQty = 0; - - /** - * @var float Base amount of discount - */ - public float $baseDiscount = 0; - - /** - * @var float Amount of discount per item - */ - public float $perItemDiscount = 0.0; - - /** - * @var float Percentage of amount discount per item - */ - public float $percentDiscount = 0.0; - - /** - * @var string Whether the discount is off the original price, or the already discount price. - */ - public string $percentageOffSubject = DiscountRecord::TYPE_DISCOUNTED_SALEPRICE; - - /** - * @var bool Exclude the “On Promotion” Purchasables - */ - public bool $excludeOnPromotion = false; - - /** - * @var bool Matching products have free shipping. - */ - public bool $hasFreeShippingForMatchingItems = false; - - /** - * @var bool The whole order has free shipping. - */ - public bool $hasFreeShippingForOrder = false; - - /** - * @var bool Match all products - */ - public bool $allPurchasables = false; - - /** - * @var bool Match all product types - * - * @todo Rename $allCategories to $allEntries in Commerce 6.0 - */ - public bool $allCategories = false; - - /** - * @var string Type of relationship between Categories and Products - * - * @todo Rename $categoryRelationshipType to $entryRelationshipType in Commerce 6.0 - */ - public string $categoryRelationshipType = DiscountRecord::CATEGORY_RELATIONSHIP_TYPE_BOTH; - - /** - * @var bool Discount enabled? - */ - public bool $enabled = true; - - /** - * @var bool stopProcessing - */ - public bool $stopProcessing = false; - - /** - * @var int|null sortOrder - */ - public ?int $sortOrder = 999999; - - /** - * @var DateTime|null - */ - public ?DateTime $dateCreated = null; - - /** - * @var DateTime|null - */ - public ?DateTime $dateUpdated = null; - - /** - * @var bool Discount ignores sales - */ - public bool $ignorePromotions = true; - - /** - * @var string What the per item amount and per item percentage off amounts can apply to - */ - public string $appliedTo = DiscountRecord::APPLIED_TO_MATCHING_LINE_ITEMS; - - /** - * @var int[] Product Ids - */ - private array $_purchasableIds; - - /** - * @var int[] Product Type IDs - */ - private array $_categoryIds; - - /** - * @var Coupon[]|null - * @since 4.0 - */ - private ?array $_coupons = null; - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $fields = parent::extraFields(); - $fields[] = 'purchasableIds'; - $fields[] = 'categoryIds'; - $fields[] = 'percentDiscountAsPercent'; - - return $fields; - } - - public function getCpEditUrl(): string - { - return $this->getStore()->getStoreSettingsUrl('discounts/' . $this->id); - } - - /** - * @param bool $exclude - * @return void - * @since 5.0.0 - * @deprecated in 5.0.0. Use `$excludeOnPromotion` instead. - */ - public function setExcludeOnSale(bool $exclude): void - { - Craft::$app->getDeprecator()->log(__METHOD__, 'Discount::$excludeOnSale is deprecated. Use Discount::$excludeOnPromotion instead.'); - $this->excludeOnPromotion = $exclude; - } - - /** - * @return bool - * @since 5.0.0 - * @deprecated in 5.0.0. Use `$excludeOnPromotion` instead. - */ - public function getExcludeOnSale(): bool - { - Craft::$app->getDeprecator()->log(__METHOD__, 'Discount::$excludeOnSale is deprecated. Use Discount::$excludeOnPromotion instead.'); - return $this->excludeOnPromotion; - } - - /** - * @return ElementConditionInterface - */ - public function getOrderCondition(): ElementConditionInterface - { - /** @var DiscountOrderCondition $condition */ - $condition = $this->_orderCondition ?? new DiscountOrderCondition(); - $condition->mainTag = 'div'; - $condition->name = 'orderCondition'; - $condition->storeId = $this->storeId; - - return $condition; - } - - /** - * @return bool - * @since 4.3.0 - */ - public function hasOrderCondition(): bool - { - if ($this->_orderCondition === null) { - return false; - } - - return !empty($this->getOrderCondition()->getConditionRules()); - } - - /** - * @param ElementConditionInterface|string|array|null $condition - * @return void - * @throws InvalidConfigException - */ - public function setOrderCondition(ElementConditionInterface|string|array|null $condition): void - { - if (empty($condition)) { - $this->_orderCondition = null; - return; - } - - if (is_string($condition)) { - $condition = Json::decodeIfJson($condition); - } - - if (!$condition instanceof ElementConditionInterface) { - $condition['class'] = DiscountOrderCondition::class; - /** @var DiscountOrderCondition $condition */ - $condition = Craft::$app->getConditions()->createCondition($condition); - } - $condition->forProjectConfig = false; - - $this->_orderCondition = $condition; - } - - /** - * @return ElementConditionInterface - */ - public function getCustomerCondition(): ElementConditionInterface - { - $condition = $this->_customerCondition ?? new DiscountCustomerCondition(); - $condition->mainTag = 'div'; - $condition->name = 'customerCondition'; - - return $condition; - } - - /** - * @return bool - * @since 4.3.0 - */ - public function hasCustomerCondition(): bool - { - if ($this->_customerCondition === null) { - return false; - } - - return !empty($this->getCustomerCondition()->getConditionRules()); - } - - /** - * @param ElementConditionInterface|string|array|null $condition - * @return void - * @throws InvalidConfigException - */ - public function setCustomerCondition(ElementConditionInterface|string|array|null $condition): void - { - if (empty($condition)) { - $this->_customerCondition = null; - return; - } - - if (is_string($condition)) { - $condition = Json::decodeIfJson($condition); - } - - if (!$condition instanceof ElementConditionInterface) { - $condition['class'] = DiscountCustomerCondition::class; - /** @var DiscountCustomerCondition $condition */ - $condition = Craft::$app->getConditions()->createCondition($condition); - } - $condition->forProjectConfig = false; - - $this->_customerCondition = $condition; - } - - /** - * @return ElementConditionInterface - */ - public function getShippingAddressCondition(): ElementConditionInterface - { - $condition = $this->_shippingAddressCondition ?? new DiscountAddressCondition(); - $condition->mainTag = 'div'; - $condition->id = 'shippingAddressCondition'; - $condition->name = 'shippingAddressCondition'; - - return $condition; - } - - /** - * @return bool - * @since 4.3.0 - */ - public function hasShippingAddressCondition(): bool - { - if ($this->_shippingAddressCondition === null) { - return false; - } - - return !empty($this->getShippingAddressCondition()->getConditionRules()); - } - - /** - * @param ElementConditionInterface|string|array|null $condition - * @return void - * @throws InvalidConfigException - */ - public function setShippingAddressCondition(ElementConditionInterface|string|array|null $condition): void - { - if (empty($condition)) { - $this->_shippingAddressCondition = null; - return; - } - - if (is_string($condition)) { - $condition = Json::decodeIfJson($condition); - } - - if (!$condition instanceof ElementConditionInterface) { - $condition['class'] = DiscountAddressCondition::class; - /** @var DiscountAddressCondition $condition */ - $condition = Craft::$app->getConditions()->createCondition($condition); - } - $condition->forProjectConfig = false; - - $this->_shippingAddressCondition = $condition; - } - - /** - * @return ElementConditionInterface - */ - public function getBillingAddressCondition(): ElementConditionInterface - { - $condition = $this->_billingAddressCondition ?? new DiscountAddressCondition(); - $condition->mainTag = 'div'; - $condition->id = 'billingAddressCondition'; - $condition->name = 'billingAddressCondition'; - - return $condition; - } - - /** - * @return bool - * @since 4.3.0 - */ - public function hasBillingAddressCondition(): bool - { - if ($this->_billingAddressCondition === null) { - return false; - } - - return !empty($this->getBillingAddressCondition()->getConditionRules()); - } - - /** - * @param ElementConditionInterface|string|array|null $condition - * @return void - * @throws InvalidConfigException - */ - public function setBillingAddressCondition(ElementConditionInterface|string|array|null $condition): void - { - if (empty($condition)) { - $this->_billingAddressCondition = null; - return; - } - - if (is_string($condition)) { - $condition = Json::decodeIfJson($condition); - } - - if (!$condition instanceof ElementConditionInterface) { - $condition['class'] = DiscountAddressCondition::class; - /** @var DiscountAddressCondition $condition */ - $condition = Craft::$app->getConditions()->createCondition($condition); - } - $condition->forProjectConfig = false; - - $this->_billingAddressCondition = $condition; - } - - /** - * @return int[] - */ - public function getCategoryIds(): array - { - if (!isset($this->_categoryIds)) { - $this->_loadCategoryRelations(); - } - - return $this->_categoryIds; - } - - /** - * @return int[] - */ - public function getPurchasableIds(): array - { - if (!isset($this->_purchasableIds)) { - $this->_loadPurchasableRelations(); - } - - return $this->_purchasableIds; - } - - /** - * Sets the related product type ids - * - * @param int[] $categoryIds - */ - public function setCategoryIds(array $categoryIds): void - { - $this->_categoryIds = array_unique($categoryIds); - } - - /** - * Sets the related product ids - * - * @param int[] $purchasableIds - */ - public function setPurchasableIds(array $purchasableIds): void - { - $this->_purchasableIds = array_unique($purchasableIds); - } - - /** - * @param bool $value - * @return void - */ - public function setHasFreeShippingForMatchingItems(bool $value): void - { - $this->hasFreeShippingForMatchingItems = $value; - } - - /** - * @return bool - */ - public function getHasFreeShippingForMatchingItems(): bool - { - return $this->hasFreeShippingForMatchingItems; - } - - /** - * @return array - * @throws InvalidConfigException - */ - public function getCoupons(): array - { - if ($this->_coupons === null && $this->id) { - $this->_coupons = Plugin::getInstance()->getCoupons()->getCouponsByDiscountId($this->id); - } - - return $this->_coupons ?? []; - } - - /** - * @param array $coupons - */ - public function setCoupons(array $coupons): void - { - $this->_coupons = $coupons; - } - - public function getPercentDiscountAsPercent(): string - { - return Craft::$app->getFormatter()->asPercent(-($this->percentDiscount ?? 0.0)); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['name', 'couponFormat'], 'required'], - [ - [ - 'perUserLimit', - 'perEmailLimit', - 'totalDiscountUseLimit', - 'totalDiscountUses', - 'purchaseQty', - 'maxPurchaseQty', - 'baseDiscount', - 'perItemDiscount', - 'percentDiscount', - ], 'number', 'skipOnEmpty' => false, - ], - [['coupons'], CouponsValidator::class, 'skipOnEmpty' => true], - [['couponFormat'], 'string', 'length' => [1, 20]], - [ - ['categoryRelationshipType'], - 'in', 'range' => [ - DiscountRecord::CATEGORY_RELATIONSHIP_TYPE_SOURCE, - DiscountRecord::CATEGORY_RELATIONSHIP_TYPE_TARGET, - DiscountRecord::CATEGORY_RELATIONSHIP_TYPE_BOTH, - ], - ], - [ - ['appliedTo'], - 'in', - 'range' => [ - DiscountRecord::APPLIED_TO_MATCHING_LINE_ITEMS, - DiscountRecord::APPLIED_TO_ALL_LINE_ITEMS, - ], - ], - [ - 'hasFreeShippingForOrder', - function($attribute) { - if ($this->hasFreeShippingForMatchingItems && $this->hasFreeShippingForOrder) { - $this->addError($attribute, Craft::t('commerce', 'Free shipping can only be for whole order or matching items, not both.')); - } - }, - ], - [['orderConditionFormula'], 'string', 'length' => [1, 65000], 'skipOnEmpty' => true], - [ - 'orderConditionFormula', - function($attribute) { - if ($this->{$attribute}) { - $order = Order::find()->one(); - if (!$order) { - $order = new Order(); - } - - $fieldsAsArray = $order->getSerializedFieldValues(); - $orderAsArray = $order->toArray([], ['lineItems.snapshot', 'shippingAddress', 'billingAddress']); - $orderConditionParams = [ - 'order' => array_merge($orderAsArray, $fieldsAsArray), - ]; - - if (!Plugin::getInstance()->getFormulas()->validateConditionSyntax($this->{$attribute}, $orderConditionParams)) { - $this->addError($attribute, Craft::t('commerce', 'Invalid order condition syntax.')); - } - } - }, - ], - [[ - 'allCategories', - 'allPurchasables', - 'appliedTo', - 'baseDiscount', - 'baseDiscountType', - 'billingAddressCondition', - 'categoryIds', - 'categoryRelationshipType', - 'couponFormat', - 'customerCondition', - 'dateCreated', - 'dateFrom', - 'dateTo', - 'dateUpdated', - 'description', - 'enabled', - // @TODO Remove the legacy `excludeOnSale` field name in Commerce 6.0 (replaced by `excludeOnPromotion`) - 'excludeOnSale', - 'excludeOnPromotion', - 'hasFreeShippingForMatchingItems', - 'hasFreeShippingForOrder', - 'id', - 'ignoreSales', - 'maxPurchaseQty', - 'name', - 'orderCondition', - 'orderConditionFormula', - 'perEmailLimit', - 'perItemDiscount', - 'perUserLimit', - 'percentDiscount', - 'percentageOffSubject', - 'ignorePromotions', - 'purchasableIds', - 'purchaseQty', - 'purchaseTotal', - 'requireCouponCode', - 'shippingAddressCondition', - 'sortOrder', - 'stopProcessing', - 'storeId', - 'totalDiscountUseLimit', - 'totalDiscountUses', - ], 'safe'], - ]; - } - - /** - * Loads the related purchasable IDs into this discount - */ - private function _loadPurchasableRelations(): void - { - $purchasableIds = (new Query())->select(['dp.purchasableId']) - ->from(Table::DISCOUNTS . ' discounts') - ->leftJoin(Table::DISCOUNT_PURCHASABLES . ' dp', '[[dp.discountId]]=[[discounts.id]]') - ->where(['discounts.id' => $this->id]) - ->column(); - - $this->setPurchasableIds($purchasableIds); - } - - /** - * Loads the related category IDs into this discount - */ - private function _loadCategoryRelations(): void - { - $categoryIds = (new Query())->select(['dpt.categoryId']) - ->from(Table::DISCOUNTS . ' discounts') - ->leftJoin(Table::DISCOUNT_CATEGORIES . ' dpt', '[[dpt.discountId]]=[[discounts.id]]') - ->where(['discounts.id' => $this->id]) - ->column(); - - $this->setCategoryIds($categoryIds); - } -} diff --git a/src/models/Email.php b/src/models/Email.php deleted file mode 100644 index a13499a313..0000000000 --- a/src/models/Email.php +++ /dev/null @@ -1,420 +0,0 @@ - - * @since 2.0 - * - * @property-read string $pdfTemplatePath - * @property-read null|Pdf $pdf - * @property-read array $config - * @property ?string $bcc - * @property ?string $cc - * @property ?string $to - * @property string|null $senderAddress - */ -class Email extends Model implements HasStoreInterface -{ - use StoreTrait; - - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string|null Name - */ - public ?string $name = null; - - /** - * @var string|null Subject - */ - public ?string $subject = null; - - /** - * @var string Recipient Type - */ - public string $recipientType = EmailRecord::TYPE_CUSTOMER; - - /** - * @var string|null Reply to - */ - public ?string $replyTo = null; - - /** - * @var bool Is Enabled - */ - public bool $enabled = true; - - /** - * @var string|null Template path - */ - public ?string $templatePath = null; - - /** - * @var string|null Plain Text Template path - */ - public ?string $plainTextTemplatePath = null; - - /** - * @var int|null The PDF UID. - */ - public ?int $pdfId = null; - - /** - * @var string The language. - */ - public string $language = EmailRecord::LOCALE_ORDER_LANGUAGE; - - /** - * The site the email should be rendered in. Set to `null` to use the site the order was placed in. - * - * @var int|null - * @since 5.5.0 - */ - public ?int $renderSiteId = null; - - /** - * @var string|null - * @since 5.0.0 - * @see setSenderAddress() - * @see getSenderAddress() - */ - private ?string $_senderAddress = null; - - /** - * @var string|null - * @since 5.0.0 - * @see setSenderName() - * @see getSenderName() - */ - private ?string $_senderName = null; - - /** - * @var string|null - * @since 5.3.0 - * @see setBcc() - * @see getBcc() - */ - private ?string $_bcc = null; - - /** - * @var string|null - * @since 5.3.0 - * @see setBcc() - * @see getBcc() - */ - private ?string $_cc = null; - - /** - * @var string|null - * @since 5.3.0 - * @see setTo() - * @see getTo() - */ - private ?string $_to = null; - - /** - * @var string|null UID - */ - public ?string $uid = null; - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $fields = parent::extraFields(); - $fields[] = 'pdf'; - $fields[] = 'config'; - - return $fields; - } - - /** - * Determines the language this email is rendered in. - * - * @param Order|null $order - */ - public function getRenderLanguage(Order $order = null): string - { - $language = $this->language; - - if ($order == null && $language == EmailRecord::LOCALE_ORDER_LANGUAGE) { - throw new InvalidArgumentException('Can not get language for this email without providing an order'); - } - - if ($order && $language == EmailRecord::LOCALE_ORDER_LANGUAGE) { - $language = $order->orderLanguage; - } - - return $language; - } - - /** - * Determines the site this email is rendered in. - * - * @param Order|null $order - * @return Site - * @throws SiteNotFoundException - * @since 5.5.0 - */ - public function getRenderSite(Order $order = null): Site - { - $renderSiteId = $this->renderSiteId ?? $order?->orderSiteId; - - if ($renderSiteId !== null) { - return Craft::$app->getSites()->getSiteById($renderSiteId); - } - - return Craft::$app->getSites()->getPrimarySite(); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['subject', 'name', 'templatePath', 'language'], 'required'], - [['recipientType'], 'in', 'range' => [EmailRecord::TYPE_CUSTOMER, EmailRecord::TYPE_CUSTOM]], - [ - ['to'], - 'required', - 'when' => static fn($model) => $model->recipientType == EmailRecord::TYPE_CUSTOM, - ], - [ - [ - 'bcc', - 'cc', - 'enabled', - 'id', - 'language', - 'name', - 'pdfId', - 'plainTextTemplatePath', - 'recipientType', - 'renderSiteId', - 'replyTo', - 'senderAddress', - 'senderName', - 'storeId', - 'subject', - 'templatePath', - 'to', - 'uid', - ], - 'safe', - ], - ]; - } - - /** - * @throws InvalidConfigException - */ - public function getPdf(): ?Pdf - { - if (!$this->pdfId) { - return null; - } - return Plugin::getInstance()->getPdfs()->getPdfById($this->pdfId, $this->storeId); - } - - /** - * @param string|null $senderAddress - * @return void - * @since 5.0.0 - */ - public function setSenderAddress(?string $senderAddress): void - { - $this->_senderAddress = $senderAddress; - } - - /** - * @param bool $parse - * @return string|null Default email address Commerce system messages should be sent from. - * - * If `null` (default), Craft’s [MailSettings::$fromEmail](craft4:craft\models\MailSettings::$fromEmail) will be used. - * - * @since 5.0.0 - */ - public function getSenderAddress(bool $parse = true): ?string - { - if (!$parse) { - return $this->_senderAddress; - } - - if (!$senderAddress = App::parseEnv($this->_senderAddress)) { - $senderAddress = App::parseEnv(App::mailSettings()->fromEmail); - } - - return $senderAddress; - } - - /** - * @param string|null $bcc - * @return void - * @since 5.3.0 - */ - public function setBcc(?string $bcc): void - { - $this->_bcc = $bcc; - } - - /** - * @param bool $parse - * @return string|null Default bcc email address Commerce emails should be sent to. - * - * @since 5.3.0 - */ - public function getBcc(bool $parse = true): ?string - { - if (!$parse) { - return $this->_bcc; - } - - return App::parseEnv($this->_bcc); - } - - /** - * @param string|null $cc - * @return void - * @since 5.3.0 - */ - public function setCc(?string $cc): void - { - $this->_cc = $cc; - } - - /** - * @param bool $parse - * @return string|null Default cc email address Commerce emails should be sent to. - * - * @since 5.3.0 - */ - public function getCc(bool $parse = true): ?string - { - if (!$parse) { - return $this->_cc; - } - - return App::parseEnv($this->_cc); - } - - /** - * @param string|null $to - * @return void - * @since 5.3.0 - */ - public function setTo(?string $to): void - { - $this->_to = $to; - } - - /** - * @param bool $parse - * @return string|null Default to email address Commerce emails should be sent to. - * - * @since 5.3.0 - */ - public function getTo(bool $parse = true): ?string - { - if (!$parse) { - return $this->_to; - } - - return App::parseEnv($this->_to); - } - - /** - * @param string|null $senderName - * @return void - * @since 5.0.0 - */ - public function setSenderName(?string $senderName): void - { - $this->_senderName = $senderName; - } - - /** - * @param bool $parse - * @return string|null Placeholder value displayed for the sender name control panel settings field. - * - * If `null` (default), Craft’s [MailSettings::$fromName](craft4:craft\models\MailSettings::$fromName) will be used. - - * @since 5.0.0 - */ - public function getSenderName(bool $parse = true): ?string - { - if (!$parse) { - return $this->_senderName; - } - - if (!$senderName = App::parseEnv($this->_senderName)) { - $senderName = App::parseEnv(App::mailSettings()->fromName); - } - - return $senderName; - } - - /** - * Returns the field layout config for this email. - * - * @throws InvalidConfigException - * @since 3.2.0 - */ - public function getConfig(): array - { - return [ - 'bcc' => $this->getBcc(false) ?: null, - 'cc' => $this->getCc(false) ?: null, - 'senderAddress' => $this->getSenderAddress(false) ?: null, - 'senderName' => $this->getSenderName(false) ?: null, - 'enabled' => $this->enabled, - 'language' => $this->language, - 'name' => $this->name, - 'pdf' => $this->getPdf()?->uid, - 'plainTextTemplatePath' => $this->plainTextTemplatePath ?? null, - 'recipientType' => $this->recipientType, - 'renderSite' => $this->renderSiteId ? Craft::$app->getSites()->getSiteById($this->renderSiteId)?->uid ?? null : null, - 'replyTo' => $this->replyTo ?: null, - 'store' => $this->getStore()->uid, - 'subject' => $this->subject, - 'templatePath' => $this->templatePath ?: null, - 'to' => $this->getTo(false) ?: null, - ]; - } - - /** - * @return string - * @throws InvalidConfigException - * @since 5.0.0 - */ - public function getCpEditUrl(): string - { - return UrlHelper::cpUrl('commerce/settings/emails/' . $this->getStore()->handle . '/' . $this->id); - } -} diff --git a/src/models/InventoryFulfillmentLevel.php b/src/models/InventoryFulfillmentLevel.php deleted file mode 100644 index fb3f68ac5c..0000000000 --- a/src/models/InventoryFulfillmentLevel.php +++ /dev/null @@ -1,84 +0,0 @@ -getInventory()->getInventoryItemById($this->inventoryItemId); - } - - /** - * @return InventoryLocation - */ - public function getInventoryLocation(): InventoryLocation - { - return Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($this->inventoryLocationId); - } - - public function getOrder(): Order - { - return Order::find()->id($this->getLineItem()->order)->status(null)->one(); - } - - public function getLineItem(): LineItem - { - if (!$this->lineItemId) { - throw new InvalidConfigException('InventoryFulfillmentLevel is not associated with a line item'); - } - - return Plugin::getInstance()->getLineItems()->getLineItemById($this->lineItemId); - } - - /** - * @return Purchasable - */ - public function getPurchasable(null|string|int $siteId = null): Purchasable - { - return $this->getInventoryItem()->getPurchasable($siteId); - } -} diff --git a/src/models/InventoryItem.php b/src/models/InventoryItem.php deleted file mode 100644 index 592393b132..0000000000 --- a/src/models/InventoryItem.php +++ /dev/null @@ -1,89 +0,0 @@ -_purchasable !== null) { - return $this->_purchasable; - } - - /** @phpstan-ignore-next-line */ - $this->_purchasable = Craft::$app->getElements()->getElementById(elementId: $this->purchasableId, siteId: $siteId); - - /** @phpstan-ignore-next-line */ - return $this->_purchasable; - } - - public function getSku(): string - { - return $this->getPurchasable('*')->sku; - } - - protected function defineRules(): array - { - return array_merge(parent::defineRules(), [ - // unique based on purchasableId - [['purchasableId'], 'unique', 'targetClass' => InventoryItem::class, 'targetAttribute' => ['purchasableId']], - [['sku'], 'unique', 'targetClass' => InventoryItem::class, 'targetAttribute' => ['sku']], - ]); - } -} diff --git a/src/models/InventoryLevel.php b/src/models/InventoryLevel.php deleted file mode 100644 index 708ecdac22..0000000000 --- a/src/models/InventoryLevel.php +++ /dev/null @@ -1,137 +0,0 @@ -{$type->value . 'Total'}; - } - - /** - * @return string - */ - public function getCpEditUrl(): string - { - return UrlHelper::cpUrl('commerce/inventory/levels'); - } - - /** - * @return InventoryItem - */ - public function getInventoryItem(): InventoryItem - { - if ($this->_inventoryItem === null) { - $this->_inventoryItem = Plugin::getInstance()->getInventory()->getInventoryItemById($this->inventoryItemId); - } - return $this->_inventoryItem; - } - - /** - * @param InventoryItem $inventoryItem - * @return void - */ - public function setInventoryItem(InventoryItem $inventoryItem): void - { - $this->_inventoryItem = $inventoryItem; - $this->inventoryItemId = $inventoryItem->id; - } - - /** - * @return InventoryLocation - */ - public function getInventoryLocation(): InventoryLocation - { - return Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($this->inventoryLocationId); - } - - /** - * @return Purchasable - */ - public function getPurchasable(null|string|int $siteId = null): Purchasable - { - return $this->getInventoryItem()->getPurchasable($siteId); - } -} diff --git a/src/models/InventoryLocation.php b/src/models/InventoryLocation.php deleted file mode 100644 index 26197ca304..0000000000 --- a/src/models/InventoryLocation.php +++ /dev/null @@ -1,214 +0,0 @@ - - * @since 5.0.0 - */ -class InventoryLocation extends Model implements Chippable, CpEditable, Actionable -{ - /** - * @var ?int - */ - public ?int $id = null; - - /** - * @var string - */ - public string $name = ''; - - /** - * @var string - */ - public string $handle = ''; - - /** - * @var DateTime|null - */ - public DateTime|null $dateCreated = null; - - /** - * @var DateTime|null - */ - public DateTime|null $dateUpdated = null; - - /** - * @var ?int - */ - public ?int $addressId = null; - - /** - * @var ?Address - */ - private ?Address $_address = null; - - /** - * @inheritdoc - */ - public static function get(int|string $id): ?static - { - /** @phpstan-ignore-next-line */ - return Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($id); - } - - /** - * @inheritdoc - */ - public function getUiLabel(): string - { - return Craft::t('site',$this->name); - } - - /** - * @return Address - */ - public function getAddress(): Address - { - if (!isset($this->_address)) { - if ($id = $this->addressId) { - /** @var Address $address */ - $address = Craft::$app->getElements()->getElementById($id); - $this->_address = $address; - } else { - $this->_address = new Address(); - $this->_address->countryCode = 'US'; - } - } - - $this->_address->title = $this->name; - - return $this->_address; - } - - /** - * @param Address $address - * @return void - */ - public function setAddress(Address $address): void - { - $this->setAddressId($address->id); - $this->_address = $address; - } - - /** - * @return string - */ - public function getAddressLine(): string - { - return $this->addressId ? ($this->getAddress()->addressLine1 . ' ' . $this->getAddress()->getCountryCode()) : ''; - } - - /** - * @param $id - * @return void - */ - public function setAddressId($id) - { - $this->addressId = $id; - } - - /** - * @return int|null - */ - public function getAddressId() - { - return $this->addressId; - } - - /** - * @return string - */ - public function getCpEditUrl(): string - { - return UrlHelper::cpUrl('commerce/inventory-locations/' . $this->id); - } - - /** - * @return string - */ - public function getCpManageInventoryUrl(): string - { - return UrlHelper::cpUrl('commerce/inventory/levels/' . $this->handle); - } - - /** - * @inheritdoc - */ - public function defineRules(): array - { - $rules = parent::defineRules(); - - $rules[] = [['name', 'handle'], 'required']; - $rules[] = [ - ['name'], - UniqueValidator::class, - 'targetClass' => InventoryLocationRecord::class, - 'targetAttribute' => 'name', - 'message' => Craft::t('yii', '{attribute} "{value}" has already been taken.'), - ]; - - $rules[] = [ - ['handle'], - UniqueValidator::class, - 'targetClass' => InventoryLocationRecord::class, - 'targetAttribute' => 'handle', - 'message' => Craft::t('yii', '{attribute} "{value}" has already been taken.'), - ]; - - $rules[] = [ - ['handle'], - HandleValidator::class, - 'reservedWords' => ['id', 'dateCreated', 'dateUpdated', 'uid', 'title', 'create'], - ]; - - return $rules; - } - - /** - * @inheritdoc - */ - public function getId(): string|int|null - { - return $this->id; - } - - /** - * @inerhitdoc - */ - public function getActionMenuItems(): array - { - $canManage = Craft::$app->getUser()->getIdentity()?->can('commerce-manageInventoryLocations') ?? false; - if (!$canManage) { - return []; - } - - return [ - [ - 'label' => Craft::t('commerce', 'Edit'), - 'url' => $this->getCpEditUrl(), - 'icon' => 'edit', - ], - ]; - } -} diff --git a/src/models/InventoryTransaction.php b/src/models/InventoryTransaction.php deleted file mode 100644 index cd8e293418..0000000000 --- a/src/models/InventoryTransaction.php +++ /dev/null @@ -1,152 +0,0 @@ -getInventory()->getInventoryItemById($this->inventoryItemId); - } - - /** - * @return InventoryLocation - */ - public function getInventoryLocation(): InventoryLocation - { - return Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($this->inventoryLocationId); - } - - /** - * @return Purchasable - */ - public function getPurchasable(): Purchasable - { - return $this->getInventoryItem()->getPurchasable(); - } - - /** - * @return ?Order - */ - public function getOrder(): ?Order - { - if (!$this->getLineItem()) { - return null; - } - - /** @var ?Order $order */ - $order = Order::find()->id($this->getLineItem()->orderId)->status(null)->one(); - - return $order; - } - - /** - * @return ?LineItem - */ - public function getLineItem(): ?LineItem - { - if ($this->lineItemId === null) { - return null; - } - - return Plugin::getInstance()->getLineItems()->getLineItemById($this->lineItemId); - } - - - /** - * @return ?Transfer - */ -// public function getTransfer(): ?Transfer -// { -// if (!$this->transferId) { -// return null; -// } -// -// /** @var ?Transfer $transfer */ -// $transfer = Transfer::find()->id($this->transferId)->status(null)->one(); -// -// return $transfer; -// } - - /** - * @return ?User - */ - public function getUser(): ?User - { - if (!$this->userId) { - return null; - } - - /** @var ?User $user */ - $user = User::find()->id($this->userId)->status(null)->one(); - - return $user; - } -} diff --git a/src/models/LineItem.php b/src/models/LineItem.php deleted file mode 100755 index 4506cbd657..0000000000 --- a/src/models/LineItem.php +++ /dev/null @@ -1,1197 +0,0 @@ - - * @since 2.0 - */ -class LineItem extends Model implements HasStoreInterface -{ - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var LineItemType - * @since 5.1.0 - */ - public LineItemType $type = LineItemType::Purchasable; - - /** - * @var string|null Description - */ - private ?string $_description = null; - - /** - * @var float Price is the original price of the purchasable - */ - private float $_price = 0; - - /** - * @var float|null - * @since 5.0.0 - */ - private ?float $_promotionalPrice = null; - - /** - * @var float|null Sale price is the price the line item will be sold for. - */ - private ?float $_salePrice = null; - - /** - * @var float Weight - */ - public float $weight = 0; - - /** - * @var float Length - */ - public float $length = 0; - - /** - * @var float Height - */ - public float $height = 0; - - /** - * @var float Width - */ - public float $width = 0; - - /** - * @var int Quantity - */ - public int $qty; - - /** - * @var array|null Snapshot - */ - private ?array $_snapshot = null; - - /** - * @var string SKU - */ - private ?string $_sku = null; - - /** - * @var string Note - */ - public string $note = ''; - - /** - * @var string Private Note - */ - public string $privateNote = ''; - - /** - * @var int|null Purchasable ID - */ - public ?int $purchasableId = null; - - /** - * @var int|null Order ID - */ - public ?int $orderId = null; - - /** - * @var int|null Line Item Status ID - */ - public ?int $lineItemStatusId = null; - - /** - * @var int|null Tax category ID - */ - public ?int $taxCategoryId = null; - - /** - * @var int|null Shipping category ID - */ - public ?int $shippingCategoryId = null; - - /** - * @var DateTime|null - * @since 2.2 - */ - public ?DateTime $dateCreated = null; - - /** - * @var DateTime|null - * @since 3.2.0 - */ - public ?DateTime $dateUpdated = null; - - /** - * @var string|null UID - */ - public ?string $uid = null; - - /** - * @var PurchasableInterface|null Purchasable - */ - private ?PurchasableInterface $_purchasable = null; - - /** - * @var Order|null - */ - private ?Order $_order = null; - - /** - * @var LineItemStatus|null Line item status - */ - private ?LineItemStatus $_lineItemStatus = null; - - /** - * @var array - */ - private array $_options = []; - - /** - * @var bool|null - * @see setIsPromotable() - * @see getIsPromotable() - * @since 5.1.0 - */ - private ?bool $_isPromotable = null; - - /** - * @var bool|null - * @see setHasFreeShipping() - * @see getHasFreeShipping() - * @since 5.1.0 - */ - private ?bool $_hasFreeShipping = null; - - /** - * @var bool|null - * @see setIsTaxable() - * @see getIsTaxable() - * @since 5.1.0 - */ - private ?bool $_isTaxable = null; - - /** - * @var bool|null - * @see setIsShippable() - * @see getIsShippable() - * @since 5.1.0 - */ - private ?bool $_isShippable = null; - - /** - * @inheritDoc - */ - public function init(): void - { - $this->note = LitEmoji::shortcodeToUnicode($this->note); - $this->privateNote = LitEmoji::shortcodeToUnicode($this->privateNote); - - parent::init(); - } - - /** - * @inheritDoc - */ - public function behaviors(): array - { - $behaviors = parent::behaviors(); - - $behaviors['currencyAttributes'] = [ - 'class' => CurrencyAttributeBehavior::class, - // We don’t want to get the currency from the order now, as this will cause additional order queries - // as the order is not set on the line item at the time of bahaviors attaching. - // Let’s let \craft\commerce\behaviors\CurrencyAttributeBehavior::getDefaultCurrency look it up at - // runtime when the order is likely already eager loaded. - 'defaultCurrency' => null, - 'currencyAttributes' => $this->currencyAttributes(), - ]; - - return $behaviors; - } - - /** - * @inheritdoc - * @throws StoreNotFoundException - */ - public function getStore(): Store - { - if (!$this->getOrder()) { - throw new StoreNotFoundException('Cannot determine line item store without an order assigned to the line item.'); - } - - return $this->getOrder()->getStore(); - } - - /** - * @throws InvalidConfigException - */ - public function getOrder(): ?Order - { - if (!isset($this->_order) && isset($this->orderId) && $this->orderId) { - $this->_order = Plugin::getInstance()->getOrders()->getOrderById($this->orderId); - } - - return $this->_order; - } - - /** - * @param Order $order - * @return void - */ - public function setOrder(Order $order): void - { - $this->orderId = $order->id; - $this->_order = $order; - } - - /** - * @throws InvalidConfigException - */ - public function getLineItemStatus(): ?LineItemStatus - { - if (!isset($this->_lineItemStatus) && isset($this->lineItemStatusId)) { - $lineItemStatus = Plugin::getInstance()->getLineItemStatuses(); - $this->_lineItemStatus = $lineItemStatus->getLineItemStatusById($this->lineItemStatusId, $this->getOrder()?->getStore()->id); - } - - return $this->_lineItemStatus; - } - - /** - * @param LineItemStatus|null $status - * @since 3.2.2 - */ - public function setLineItemStatus(LineItemStatus $status = null): void - { - if ($status !== null) { - $this->_lineItemStatus = $status; - $this->lineItemStatusId = (int)$status->id; - } else { - $this->lineItemStatusId = null; - $this->_lineItemStatus = null; - } - } - - /** - * Returns the options for the line item. - */ - public function getOptions(): array - { - return $this->_options; - } - - /** - * Set the options array on the line item. - */ - public function setOptions(array|string $options): void - { - $options = Json::decodeIfJson($options); - - if (!is_array($options)) { - $options = []; - } - - $cleanEmojiValues = static function(&$options) use (&$cleanEmojiValues) { - foreach ($options as $key => $value) { - if (is_array($value)) { - $cleanEmojiValues($value); - } else { - if (is_string($value)) { - $options[$key] = LitEmoji::unicodeToShortcode($value); - } - } - } - - return $options; - }; - - // @TODO Normalize emoji handling in options to a consistent shape across DB drivers (currently only stripped when MB4 is unsupported); breaking change targeted for Commerce 6.0 #COM-46 - if (Craft::$app->getDb()->getSupportsMb4()) { - $this->_options = $options; - } else { - $this->_options = $cleanEmojiValues($options); - } - } - - /** - * Returns the snapshot for the line item. - * - * @return array - * @since 5.0.0 - */ - public function getSnapshot(): array - { - return $this->_snapshot ?? []; - } - - /** - * Set the snapshot array on the line item. - * - * @param array|string $snapshot - * @return void - * @since 5.0.0 - */ - public function setSnapshot(array|string $snapshot): void - { - $snapshot = Json::decodeIfJson($snapshot); - - if (!is_array($snapshot)) { - $snapshot = []; - } - - $this->_snapshot = $snapshot; - } - - /** - * @return string - */ - public function getDescription(): string - { - if (!$this->_description) { - $snapshot = $this->getSnapshot(); - $this->_description = $snapshot['description'] ?? ''; - } - - return $this->_description; - } - - /** - * @param ?string $description - * @return void - */ - public function setDescription(?string $description): void - { - $this->_description = (string)$description; - } - - /** - * @return string - */ - public function getSku(): string - { - if ($this->_sku === null) { - $snapshot = $this->getSnapshot(); - $this->_sku = $snapshot['sku'] ?? ''; - } - - return $this->_sku ?? ''; - } - - /** - * @param ?string $sku - * @return void - */ - public function setSku(?string $sku): void - { - $this->_sku = (string)$sku; - } - - /** - * Returns a unique hash of the line item options - */ - public function getOptionsSignature(): string - { - $orderId = $this->getOrder()?->isCompleted ? $this->id : null; - - return LineItemHelper::generateOptionsSignature($this->_options, $orderId); - } - - /** - * @since 3.1.1 - */ - public function getPrice(): float - { - return CurrencyHelper::round($this->_price); - } - - /** - * @since 3.1.1 - */ - public function setPrice(float|int $price): void - { - $this->_price = $price; - // clear sale price cache - $this->_salePrice = null; - } - - /** - * @return float|null - * @since 5.0.0 - */ - public function getPromotionalPrice(): ?float - { - if ($this->_promotionalPrice === null) { - return null; - } - - return CurrencyHelper::round($this->_promotionalPrice); - } - - /** - * @param float|int|null $price - * @return void - * @since 5.0.0 - */ - public function setPromotionalPrice(float|int|null $price): void - { - $this->_promotionalPrice = $price; - // clear sale price cache - $this->_salePrice = null; - } - - /** - * @return float Sale Price - */ - public function getSalePrice(): float - { - if ($this->_salePrice === null) { - $this->_salePrice = $this->getOnPromotion() ? $this->getPromotionalPrice() : $this->getPrice(); - } - - return $this->_salePrice; - } - - /** - * @return float - * @throws DeprecationException - * @deprecated in 5.0.0. Use `getPromotionalAmount()` instead.) - */ - public function getSaleAmount(): float - { - Craft::$app->getDeprecator()->log(__METHOD__, 'LineItem `getSaleAmount()` method has been deprecated. Use `getPromotionalAmount()` instead.'); - return $this->getPromotionalAmount(); - } - - /** - * @return float - * @since 5.0.0 - */ - public function getPromotionalAmount(): float - { - if ($this->getPromotionalPrice() === null) { - return 0; - } - - return Currency::round($this->getPrice() - $this->getPromotionalPrice()); - } - - /** - * @inerhitdoc - */ - protected function defineRules(): array - { - $rules = [ - [ - [ - 'optionsSignature', - 'price', - 'promotionalAmount', - 'weight', - 'length', - 'height', - 'width', - 'qty', - 'taxCategoryId', - 'type', - 'shippingCategoryId', - ], 'required', - ], - [['snapshot'], 'required', 'when' => fn() => $this->type === LineItemType::Purchasable], - [['qty'], 'integer', 'min' => 1], - [['shippingCategoryId', 'taxCategoryId'], 'integer'], - [['price'], 'number', 'min' => 0], - [['promotionalPrice'], 'number', 'min' => 0, 'skipOnEmpty' => true], - [['orderId', 'purchasableId', 'hasFreeShipping', 'isPromotable', 'isShippable', 'isTaxable', 'type'], 'safe'], - ]; - - if ($this->type === LineItemType::Purchasable && $this->purchasableId) { - $order = $this->getOrder(); - /** @var PurchasableInterface|null $purchasable */ - $purchasable = Plugin::getInstance()->getPurchasables()->getPurchasableById($this->purchasableId, $order?->orderSiteId, $order?->getCustomer()?->id); - if ($purchasable && !empty($purchasableRules = $purchasable->getLineItemRules($this))) { - foreach ($purchasableRules as $rule) { - $rules[] = $this->_normalizePurchasableRule($rule, $purchasable); - } - } - } - - // @TODO Add a validation rule preventing qty from being reduced below the total fulfilled quantity across inventory locations when the order is complete - - return $rules; - } - - /** - * @return int - * @throws DeprecationException - * @throws InvalidConfigException - * @since 5.0.0 - */ - public function getFulfilledTotalQuantity(): int - { - if ($order = $this->getOrder()) { - return Plugin::getInstance()->getInventory()->getInventoryFulfillmentLevels($order) - ->filter(fn($fulfillment) => $fulfillment->getLineItem()->id === $this->id) - ->sum('fulfilledQuantity'); - } - - return 0; - } - - /** - * Normalizes a purchasable’s validation rule. - * - * @param PurchasableInterface $purchasable - * @return mixed - */ - private function _normalizePurchasableRule(mixed $rule, PurchasableInterface $purchasable): mixed - { - if (isset($rule[1]) && $rule[1] instanceof Closure) { - $method = $rule[1]; - $method = $method->bindTo($purchasable); - $rule[1] = static function($attribute, $params, $validator, $current) use ($method) { - $method($attribute, $params, $validator, $current); - }; - } - - return $rule; - } - - /** - * @inheritdoc - */ - public function attributes(): array - { - $names = parent::attributes(); - ArrayHelper::removeValue($names, 'snapshot'); - - $names[] = 'type'; - $names[] = 'adjustments'; - $names[] = 'description'; - $names[] = 'hasFreeShipping'; - $names[] = 'isPromotable'; - $names[] = 'isShippable'; - $names[] = 'isTaxable'; - $names[] = 'options'; - $names[] = 'optionsSignature'; - $names[] = 'onPromotion'; - $names[] = 'price'; - $names[] = 'promotionalPrice'; - $names[] = 'salePrice'; - $names[] = 'sku'; - $names[] = 'total'; - - return $names; - } - - /** - * @inheritDoc - */ - public function fields(): array - { - $fields = parent::fields(); // get the currency and date fields formatted - $fields['subtotal'] = 'subtotal'; - - return $fields; - } - - /** - * @inheritdoc - */ - public function extraFields(): array - { - return array_values(array_filter([ - 'lineItemStatus', - 'order', - $this->type === LineItemType::Purchasable ? 'purchasable' : null, - 'shippingCategory', - 'snapshot', - 'taxCategory', - 'fulfilledTotalQuantity', - ], fn($value) => $value !== null)); - } - - /** - * The attributes on the order that should be made available as formatted currency. - */ - public function currencyAttributes(): array - { - $attributes = []; - $attributes[] = 'price'; - $attributes[] = 'promotionalPrice'; - $attributes[] = 'promotionalAmount'; - $attributes[] = 'salePrice'; - $attributes[] = 'subtotal'; - $attributes[] = 'total'; - $attributes[] = 'discount'; - $attributes[] = 'shippingCost'; - $attributes[] = 'tax'; - $attributes[] = 'taxIncluded'; - $attributes[] = 'adjustmentsTotal'; - - return $attributes; - } - - public function getSubtotal(): float - { - // Even though we validate salePrice as numeric, we still need to - // stop any exceptions from occurring when displaying subtotal on an order/lineitems with errors. - if (!is_numeric($this->salePrice)) { - $salePrice = 0; - } else { - $salePrice = $this->salePrice; - } - - return CurrencyHelper::round($this->qty * $salePrice); - } - - /** - * Returns the Purchasable’s sale price multiplied by the quantity of the line item, plus any adjustment belonging to this lineitem. - * - * @throws InvalidConfigException - */ - public function getTotal(): float - { - return (float)$this->order->getTeller()->add($this->getSubtotal(), $this->getAdjustmentsTotal()); - } - - /** - * @param string $taxable - * @return float - * @throws InvalidConfigException - */ - public function getTaxableSubtotal(string $taxable): float - { - return match ($taxable) { - TaxRateRecord::TAXABLE_SHIPPING => $this->getShippingCost(), - TaxRateRecord::TAXABLE_PRICE_SHIPPING => (float)$this->order->getTeller()->sum($this->getSubtotal(), $this->getDiscount() , $this->getShippingCost()), - default => (float)$this->order->getTeller()->add($this->getSubtotal() , $this->getDiscount()), // TaxRateRecord::TAXABLE_PRICE is default - }; - } - - /** - * @return bool - * @throws InvalidConfigException - * @throws SiteNotFoundException - * @throws Exception - * @since 5.1.0 - */ - public function refresh(): bool - { - if ($this->type === LineItemType::Custom) { - return true; - } - - return $this->_refreshFromPurchasable(); - } - - /** - * @return bool False when no related purchasable exists - * @throws DeprecationException - * @throws Exception - * @throws InvalidConfigException - * @throws SiteNotFoundException - * @deprecated in 5.1.0. Use `refresh()` instead. - */ - public function refreshFromPurchasable(): bool - { - Craft::$app->getDeprecator()->log(__METHOD__, '`LineItem::refreshFromPurchasable()` has been deprecated. Use `LineItem::refresh()` instead.'); - - if ($this->type === LineItemType::Custom) { - Craft::warning('Cannot refresh a custom line item from a purchasable', 'commerce'); - return true; - } - - return $this->_refreshFromPurchasable(); - } - - /** - * @return bool False when no related purchasable exists - * @throws Exception - * @throws InvalidConfigException - * @throws SiteNotFoundException - */ - private function _refreshFromPurchasable(): bool - { - if ($this->type === LineItemType::Custom) { - throw new Exception('Cannot refresh a custom line item from a purchasable'); - } - - if ($this->qty <= 0 && $this->id) { - return false; - } - - /* @var $purchasable Purchasable */ - $purchasable = $this->getPurchasable(); - if (!$purchasable || !Plugin::getInstance()->getPurchasables()->isPurchasableAvailable($purchasable, $this->getOrder())) { - return false; - } - - $this->_populateFromPurchasable($purchasable); - - return true; - } - - /** - * @param bool|null $hasFreeShipping - * @return void - * @since 5.1.0 - */ - public function setHasFreeShipping(?bool $hasFreeShipping): void - { - $this->_hasFreeShipping = $hasFreeShipping; - } - - /** - * @return bool - * @throws InvalidConfigException - * @throws SiteNotFoundException - * @since 5.1.0 - */ - public function getHasFreeShipping(): bool - { - // For purchasable line item types try and get the live data - if ($this->type === LineItemType::Purchasable && $this->getPurchasable()) { - return $this->getPurchasable()->hasFreeShipping(); - } - - return $this->_hasFreeShipping ?? false; - } - - /** - * @return PurchasableInterface|null - * @throws InvalidConfigException - * @throws SiteNotFoundException - */ - public function getPurchasable(): ?PurchasableInterface - { - if ($this->type === LineItemType::Custom) { - throw new InvalidConfigException('Cannot get a purchasable for a custom line item'); - } - - if (!isset($this->_purchasable) && isset($this->purchasableId)) { - $order = $this->getOrder(); - /** @var PurchasableInterface|null $purchasable */ - $purchasable = Plugin::getInstance()->getPurchasables()->getPurchasableById($this->purchasableId, $order?->orderSiteId, $order?->getCustomer()?->id); - - // If we are still using sales we need to make sure that the promotional price is set. - if (!Plugin::getInstance()->getCatalogPricingRules()->canUseCatalogPricingRules()) { - if ($purchasable instanceof Purchasable) { - $purchasable->loadSales($this->getOrder()); - } - } - - $this->_purchasable = $purchasable; - } - - return $this->_purchasable; - } - - /** - * @param PurchasableInterface $purchasable - * @return void - * @throws InvalidConfigException - */ - public function setPurchasable(PurchasableInterface $purchasable): void - { - $this->purchasableId = $purchasable->getId(); - $this->_purchasable = $purchasable; - $this->type = LineItemType::Purchasable; - } - - /** - * @param mixed|null $data - * @return void - * @throws InvalidConfigException - * @since 5.1.0 - */ - public function populate(mixed $data = null): void - { - if ($this->type === LineItemType::Custom) { - return; - } - - if ($data) { - $this->_populateFromPurchasable($data); - } - } - - /** - * @param PurchasableInterface $purchasable - * @return void - * @throws InvalidConfigException - * @deprecated in 5.0.0. Use `populate()` instead. - */ - public function populateFromPurchasable(PurchasableInterface $purchasable): void - { - Craft::$app->getDeprecator()->log(__METHOD__, '`LineItem::populateFromPurchasable()` has been deprecated. Use `LineItem::populate()` instead.'); - - if ($this->type === LineItemType::Custom) { - // @TODO Throw an exception instead of logging a warning when populating a custom line item from a purchasable, in Commerce 6.0 - Craft::warning('Cannot populate a custom line item from a purchasable', 'commerce'); - return; - } - - $this->_populateFromPurchasable($purchasable); - } - - /** - * @param PurchasableInterface $purchasable - * @throws Exception - * @throws InvalidConfigException - */ - private function _populateFromPurchasable(PurchasableInterface $purchasable): void - { - if ($this->type === LineItemType::Custom) { - throw new Exception('Cannot populate a custom line item from a purchasable'); - } - - // Set all things from the purchasable interface that are applicable to the line item. - $this->purchasableId = $purchasable->getId(); - $this->setPrice($purchasable->getPrice()); - $this->setPromotionalPrice($purchasable->getPromotionalPrice()); - $this->taxCategoryId = $purchasable->getTaxCategory()->id; - $this->shippingCategoryId = $purchasable->getShippingCategory()->id; - $this->setSku($purchasable->getSku()); - $this->setDescription($purchasable->getDescription()); - - // Check to see if there is a discount applied that ignores promotions for this line item - $ignorePromotions = false; - foreach (Plugin::getInstance()->getDiscounts()->getAllActiveDiscounts($this->getOrder()) as $discount) { - if (Plugin::getInstance()->getDiscounts()->matchLineItem($this, $discount, true)) { - // Break if matched discount is set to ignore promotions. - $ignorePromotions = $discount->ignorePromotions; - if ($ignorePromotions) { - break; - } - - // Break if matched discount is set to not apply any subsequent discounts. - if ($discount->stopProcessing) { - break; - } - } - } - - // One of the matching discounts has ignored promotions, so we want to remove any promotional price. - if ($ignorePromotions) { - $this->setPromotionalPrice(null); - } - - $snapshot = [ - // @TODO Move these common snapshot fields (price, sku, description, purchasableId, cpEditUrl, options) into the base purchasable's getSnapshot() in Commerce 6.0 - 'price' => $purchasable->getPrice(), - 'sku' => $purchasable->getSku(), - 'description' => $purchasable->getDescription(), - 'purchasableId' => $purchasable->getId(), - 'cpEditUrl' => '#', - 'options' => $this->getOptions(), - // Only add sales information to the snapshot if we are not ignoring promotions and they are still using the sales system. - 'sales' => $ignorePromotions || Plugin::getInstance()->getCatalogPricingRules()->canUseCatalogPricingRules() ? [] : Plugin::getInstance()->getSales()->getSalesForPurchasable($purchasable, $this->order), - ]; - - // Add our purchasable data to the snapshot, save our sales. - $purchasableSnapshot = $purchasable->getSnapshot(); - $this->setSnapshot(array_merge($purchasableSnapshot, $snapshot)); - - $purchasable->populateLineItem($this); - - $lineItemsService = Plugin::getInstance()->getLineItems(); - - if ($lineItemsService->hasEventHandlers($lineItemsService::EVENT_POPULATE_LINE_ITEM)) { - $lineItemsService->trigger($lineItemsService::EVENT_POPULATE_LINE_ITEM, new LineItemEvent([ - 'lineItem' => $this, - 'isNew' => !$this->id, - ])); - } - } - - /** - * @param bool|null $isPromotable - * @return void - * @since 5.1.0 - */ - public function setIsPromotable(?bool $isPromotable): void - { - $this->_isPromotable = $isPromotable; - } - - /** - * @return bool - * @throws InvalidConfigException - * @throws SiteNotFoundException - * @since 5.1.0 - */ - public function getIsPromotable(): bool - { - // For purchasable line item types try and get the live data - if ($this->type === LineItemType::Purchasable && $this->getPurchasable()) { - return $this->getPurchasable()->getIsPromotable(); - } - - return $this->_isPromotable ?? false; - } - - /** - * @return bool - * @since 5.0.0 - */ - public function getOnPromotion(): bool - { - return $this->getPromotionalAmount() > 0; - } - - /** - * @return bool - * @throws DeprecationException - * @deprecated in 5.0.0. Use `getOnPromotion()` instead. - */ - public function getOnSale(): bool - { - Craft::$app->getDeprecator()->log(__METHOD__, 'LineItem `' . __METHOD__ . '()` method has been deprecated. Use `getOnPromotion()` instead.'); - return $this->getOnPromotion(); - } - - /** - * @throws InvalidConfigException - */ - public function getTaxCategory(): TaxCategory - { - // Category may have been archived - $categories = Plugin::getInstance()->getTaxCategories()->getAllTaxCategories(true); - return ArrayHelper::firstWhere($categories, 'id', $this->taxCategoryId); - } - - /** - * @return ShippingCategory - * @throws InvalidConfigException - * @throws StoreNotFoundException - */ - public function getShippingCategory(): ShippingCategory - { - if (!isset($this->shippingCategoryId)) { - throw new InvalidConfigException('Line Item is missing its shipping category ID'); - } - - // Category may have been archived - $categories = Plugin::getInstance()->getShippingCategories()->getAllShippingCategories(withTrashed: true); - return ArrayHelper::firstWhere($categories, 'id', $this->shippingCategoryId); - } - - /** - * @return OrderAdjustment[] - * @throws InvalidConfigException - */ - public function getAdjustments(): array - { - $lineItemAdjustments = []; - - $adjustments = $this->getOrder()->getAdjustments(); - - foreach ($adjustments as $adjustment) { - // Since the line item may not yet be saved and won't have an ID, we need to check the adjuster references this as it's line item. - if (($adjustment->lineItemId && $adjustment->lineItemId == $this->id) || (!$adjustment->lineItemId && $adjustment->getLineItem() === $this)) { - $lineItemAdjustments[] = $adjustment; - } - } - - return $lineItemAdjustments; - } - - /** - * @throws InvalidConfigException - */ - public function getAdjustmentsTotal(bool $included = false): float - { - $amount = 0; - $teller = $this->_getTeller(); - foreach ($this->getAdjustments() as $adjustment) { - if ($adjustment->included == $included) { - $amount = (float)$teller->add($amount, $adjustment->amount); - } - } - - return $amount; - } - - /** - * @throws InvalidConfigException - */ - private function _getAdjustmentsTotalByType(string $type, bool $included = false): float|int - { - $amount = 0; - $teller = $this->_getTeller(); - foreach ($this->getAdjustments() as $adjustment) { - if ($adjustment->included == $included && $adjustment->type === $type) { - $amount = (float)$teller->add($amount, $adjustment->amount); - } - } - - return $amount; - } - - /** - * @param bool|null $isTaxable - * @return void - * @since 5.1.0 - */ - public function setIsTaxable(?bool $isTaxable): void - { - $this->_isTaxable = $isTaxable; - } - - /** - * @since 3.3.4 - */ - public function getIsTaxable(): bool - { - if ($this->type === LineItemType::Custom) { - return $this->_isTaxable ?? false; - } - - if (!$this->getPurchasable()) { - return $this->_isTaxable ?? true; // we have a default tax category so assume so. - } - - return $this->getPurchasable()->getIsTaxable(); - } - - /** - * @param bool|null $isShippable - * @return void - * @since 5.1.0 - */ - public function setIsShippable(?bool $isShippable): void - { - $this->_isShippable = $isShippable; - } - - /** - * @since 3.4 - */ - public function getIsShippable(): bool - { - if ($this->type === LineItemType::Custom) { - return $this->_isShippable ?? false; - } - - if (!$this->getPurchasable()) { - return $this->_isShippable ?? true; // we have a default shipping category so assume so. - } - - return Plugin::getInstance()->getPurchasables()->isPurchasableShippable($this->getPurchasable(), $this->getOrder()); - } - - /** - * @throws InvalidConfigException - */ - public function getTax(): float - { - return $this->_getAdjustmentsTotalByType('tax'); - } - - /** - * @throws InvalidConfigException - */ - public function getTaxIncluded(): float - { - return $this->_getAdjustmentsTotalByType('tax', true); - } - - /** - * @throws InvalidConfigException - */ - public function getShippingCost(): float - { - return $this->_getAdjustmentsTotalByType('shipping'); - } - - /** - * @throws InvalidConfigException - */ - public function getDiscount(): float - { - return $this->_getAdjustmentsTotalByType('discount'); - } - - /** - * @return Teller - * @throws InvalidConfigException - */ - private function _getTeller(): Teller - { - if (!$order = $this->getOrder()) { - throw new InvalidConfigException('Line Item requires an order to calculate costs.'); - } - - return $order->getTeller(); - } -} diff --git a/src/models/LineItemStatus.php b/src/models/LineItemStatus.php deleted file mode 100644 index 24beef919d..0000000000 --- a/src/models/LineItemStatus.php +++ /dev/null @@ -1,198 +0,0 @@ - - * @since 2.0 - */ -class LineItemStatus extends Model implements HasStoreInterface, Chippable -{ - use StoreTrait; - - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string|null Name - */ - public ?string $name = null; - - /** - * @var string|null Handle - */ - public ?string $handle = null; - - /** - * @var string Color - */ - public string $color = 'green'; - - /** - * @var int|null Sort order - */ - public ?int $sortOrder = null; - - /** - * @var bool Default status - */ - public bool $default = false; - - /** - * @var bool Whether the order status is archived. - */ - public bool $isArchived = false; - - /** - * @var DateTime|null Archived Date - */ - public ?DateTime $dateArchived = null; - - /** - * @var string|null UID - */ - public ?string $uid = null; - - /** - * @return string - */ - public function __toString() - { - return $this->getUiLabel(); - } - - public function getUiLabel(): string - { - return Craft::t('site', $this->name ?? ''); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['name', 'handle'], 'required'], - [['handle'], - UniqueValidator::class, - 'targetClass' => LineItemStatusRecord::class, - 'targetAttribute' => ['handle', 'storeId'], - 'filter' => ['isArchived' => false], - 'message' => '{attribute} "{value}" has already been taken.', - ], - [ - ['handle'], - HandleValidator::class, - 'reservedWords' => ['id', 'dateCreated', 'dateUpdated', 'uid', 'title', 'create'], - ], - [[ - 'id', - 'storeId', - 'name', - 'handle', - 'color', - 'sortOrder', - 'default', - 'isArchived', - 'dateArchived', - 'uid', - ], 'safe'], - ]; - } - - /** - * @inerhitdoc - */ - public function extraFields(): array - { - $fields = parent::extraFields(); - $fields[] = 'labelHtml'; - $fields[] = 'uiLabel'; - - return $fields; - } - - public function getCpEditUrl(): string - { - return UrlHelper::cpUrl('commerce/settings/lineitemstatuses/' . $this->getStore()->handle . '/' . $this->id); - } - - /** - * @return string - */ - public function getLabelHtml(): string - { - return Cp::statusLabelHtml([ - 'label' => Html::encode($this->getUiLabel()), - 'color' => Html::encode($this->color), - ]); - } - - /** - * Returns the config for this status. - * - * @since 3.2.2 - */ - public function getConfig(): array - { - return [ - 'store' => $this->getStore()->uid, - 'name' => $this->name, - 'handle' => $this->handle, - 'color' => $this->color, - 'sortOrder' => $this->sortOrder ?: 9999, - 'default' => $this->default, - ]; - } - - /** - * @inheritdoc - */ - public static function get(int|string $id): ?static - { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $storeId = $site?->getStore()->id ?? null; - - /** @phpstan-ignore-next-line */ - return Plugin::getInstance()->getLineItemStatuses()->getLineItemStatusById($id, $storeId); - } - - /** - * @inheritdoc - */ - public function getId(): string|int|null - { - return $this->id; - } -} diff --git a/src/models/OrderAdjustment.php b/src/models/OrderAdjustment.php deleted file mode 100644 index 61292b1f45..0000000000 --- a/src/models/OrderAdjustment.php +++ /dev/null @@ -1,207 +0,0 @@ - - * @since 2.0 - */ -class OrderAdjustment extends Model -{ - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string Name - */ - public string $name; - - /** - * @var string|null Description - */ - public ?string $description = null; - - /** - * @var string Type - */ - public string $type; - - /** - * @var float Amount - */ - public float $amount; - - /** - * @var bool Included - */ - public bool $included = false; - - /** - * @var mixed Adjuster options - */ - private mixed $_sourceSnapshot = []; - - /** - * @var int|null Order ID - */ - public ?int $orderId = null; - - /** - * @var int|null Line item ID this adjustment belongs to - */ - public ?int $lineItemId = null; - - /** - * @var bool Whether the adjustment is based of estimated data - */ - public bool $isEstimated = false; - - /** - * @var LineItem|null The line item this adjustment belongs to - */ - private ?LineItem $_lineItem = null; - - /** - * @var Order|null The order this adjustment belongs to - */ - private ?Order $_order = null; - - - public function behaviors(): array - { - $behaviors = parent::behaviors(); - - $behaviors['currencyAttributes'] = [ - 'class' => CurrencyAttributeBehavior::class, - 'defaultCurrency' => $this->getCurrency(), - 'currencyAttributes' => $this->currencyAttributes(), - ]; - - return $behaviors; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['type', 'amount', 'sourceSnapshot', 'orderId'], 'required'], - [['amount'], 'number'], - [['orderId'], 'integer'], - [['lineItemId'], 'integer'], - ]; - } - - /** - * @inheritdoc - */ - public function attributes(): array - { - $attributes = parent::attributes(); - $attributes[] = 'sourceSnapshot'; - - return $attributes; - } - - /** - * The attributes on the order that should be made available as formatted currency. - */ - public function currencyAttributes(): array - { - $attributes = []; - $attributes[] = 'amount'; - return $attributes; - } - - /** - * @return ?string - * @throws InvalidConfigException - */ - protected function getCurrency(): ?string - { - return $this->getOrder()?->currency; - } - - /** - * Gets the options for the line item. - */ - public function getSourceSnapshot(): array - { - return $this->_sourceSnapshot; - } - - /** - * Set the options array on the line item. - */ - public function setSourceSnapshot(array|string $snapshot): void - { - if (is_string($snapshot)) { - $snapshot = Json::decode($snapshot); - } - - if (!is_array($snapshot)) { - throw new InvalidArgumentException('Adjustment source snapshot must be an array.'); - } - - $this->_sourceSnapshot = $snapshot; - } - - /** - * @throws InvalidConfigException - */ - public function getLineItem(): ?LineItem - { - if ($this->_lineItem === null && isset($this->lineItemId) && $this->lineItemId) { - $this->_lineItem = Plugin::getInstance()->getLineItems()->getLineItemById($this->lineItemId); - } - - return $this->_lineItem; - } - - public function setLineItem(LineItem $lineItem): void - { - $this->_lineItem = $lineItem; - } - - /** - * @throws InvalidConfigException - */ - public function getOrder(): ?Order - { - if (!isset($this->_order) && isset($this->orderId) && $this->orderId) { - $this->_order = Plugin::getInstance()->getOrders()->getOrderById($this->orderId); - } - - return $this->_order; - } - - public function setOrder(Order $order): void - { - $this->_order = $order; - $this->orderId = $order->id; - } -} diff --git a/src/models/OrderHistory.php b/src/models/OrderHistory.php deleted file mode 100644 index 1b793bcd4f..0000000000 --- a/src/models/OrderHistory.php +++ /dev/null @@ -1,133 +0,0 @@ - - * @since 2.0 - */ -class OrderHistory extends Model -{ - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string|null Message - */ - public ?string $message = null; - - /** - * @var int Order ID - */ - public int $orderId; - - /** - * @var int|null Previous Status ID - */ - public ?int $prevStatusId = null; - - /** - * @var int|null New status ID - */ - public ?int $newStatusId = null; - - /** - * @var int|null User ID - */ - public ?int $userId = null; - - /** - * @var string|null User name or email - */ - public ?string $userName = ''; - - /** - * @var Datetime|null - */ - public ?DateTime $dateCreated = null; - - /** - * @var Order|null - */ - private ?Order $_order = null; - - /** - * @throws InvalidConfigException - */ - public function getOrder(): ?Order - { - if ($this->_order === null) { - $this->_order = Plugin::getInstance()->getOrders()->getOrderById($this->orderId); - } - - return $this->_order; - } - - public function setOrder(Order $order): void - { - $this->_order = $order; - $this->orderId = $order->id; - } - - /** - * @throws InvalidConfigException - */ - public function getPrevStatus(): ?OrderStatus - { - $orderStatuses = Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses($this->getOrder()?->storeId); - return ArrayHelper::firstWhere($orderStatuses, 'id', $this->prevStatusId); - } - - /** - * @throws InvalidConfigException - */ - public function getNewStatus(): ?OrderStatus - { - $orderStatuses = Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses($this->getOrder()?->storeId); - return ArrayHelper::firstWhere($orderStatuses, 'id', $this->newStatusId); - } - - /** - * @return User|null - */ - public function getUser(): ?User - { - if ($this->userId === null) { - return null; - } - - return Craft::$app->getUsers()->getUserById($this->userId); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['orderId', 'userId'], 'required'], - ]; - } -} diff --git a/src/models/OrderNotice.php b/src/models/OrderNotice.php deleted file mode 100644 index caef89c43d..0000000000 --- a/src/models/OrderNotice.php +++ /dev/null @@ -1,112 +0,0 @@ - - * @since 3.3 - */ -class OrderNotice extends Model -{ - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string Type - */ - public string $type; - - /** - * @var string Attribute - */ - public string $attribute; - - /** - * @var string Message - */ - public string $message; - - /** - * @var int|null Order ID - */ - public ?int $orderId = null; - - /** - * @var OrderNoticeType Whether this notice is for customers or admins only. - * @since 5.7.0 - */ - private OrderNoticeType $_noticeType = OrderNoticeType::Customer; - - /** - * @var Order|null The order this notice belongs to - */ - private ?Order $_order = null; - - /** - * @return string - */ - public function __toString() - { - return $this->message ?: ''; - } - - public function getNoticeType(): OrderNoticeType - { - return $this->_noticeType; - } - - public function setNoticeType(string|OrderNoticeType $noticeType): void - { - $this->_noticeType = $noticeType instanceof OrderNoticeType - ? $noticeType - : OrderNoticeType::from($noticeType); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['id', 'noticeType'], 'safe'], - [['type', 'message', 'attribute', 'orderId'], 'required'], - [['orderId'], 'integer'], - ]; - } - - public function setOrder(Order $order): void - { - $this->_order = $order; - $this->orderId = $order->id; - } - - /** - * @throws InvalidConfigException - */ - public function getOrder(): ?Order - { - if (!isset($this->_order) && $this->orderId) { - $this->_order = Plugin::getInstance()->getOrders()->getOrderById($this->orderId); - } - - return $this->_order; - } -} diff --git a/src/models/OrderStatus.php b/src/models/OrderStatus.php deleted file mode 100644 index b3099c50ec..0000000000 --- a/src/models/OrderStatus.php +++ /dev/null @@ -1,247 +0,0 @@ - - * @since 2.0 - */ -class OrderStatus extends Model implements HasStoreInterface, Chippable -{ - use SoftDeleteTrait { - SoftDeleteTrait::behaviors as softDeleteBehaviors; - } - use StoreTrait; - - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string|null Name - */ - public ?string $name = null; - - /** - * @var string|null Handle - */ - public ?string $handle = null; - - /** - * @var string Color - */ - public string $color = 'green'; - - /** - * @var string|null Description - */ - public ?string $description = null; - - /** - * @var int|null Sort order - */ - public ?int $sortOrder = null; - - /** - * @var bool Default status - */ - public bool $default = false; - - /** - * @var DateTime|null Date deleted - */ - public ?DateTime $dateDeleted = null; - - /** - * @var string|null UID - */ - public ?string $uid = null; - - public function behaviors(): array - { - return $this->softDeleteBehaviors(); - } - - /** - * @return string - */ - public function __toString() - { - return $this->getUiLabel(); - } - - /** - * @since 2.2 - * @deprecated in 5.6. Use [[getUiLabel()]] instead. - */ - public function getDisplayName(): string - { - return $this->getUiLabel(); - } - - /** - * @inheritdoc - */ - public function getUiLabel(): string - { - if ($this->dateDeleted !== null) { - return Craft::t('commerce', '{name} (Trashed)', ['name' => Craft::t('site', $this->name)]); - } - - return Craft::t('site', $this->name ?? ''); - } - - - protected function defineRules(): array - { - return [ - [['name', 'handle'], 'required'], - [['handle'], - UniqueValidator::class, - 'targetClass' => OrderStatusRecord::class, - 'targetAttribute' => ['handle', 'storeId'], - 'message' => '{attribute} "{value}" has already been taken.', - ], - [ - ['handle'], - HandleValidator::class, - 'reservedWords' => ['id', 'dateCreated', 'dateUpdated', 'uid', 'title', 'create'], - ], - [['id', 'color', 'description', 'default', 'sortOrder', 'dateDeleted', 'uid', 'storeId'], 'safe'], - ]; - } - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $fields = parent::extraFields(); - $fields[] = 'emails'; - $fields[] = 'emailIds'; - $fields[] = 'labelHtml'; - $fields[] = 'uiLabel'; - - return $fields; - } - - public function getCpEditUrl(): string - { - return UrlHelper::cpUrl('commerce/settings/orderstatuses/' . $this->getStore()->handle . '/' . $this->id); - } - - /** - * @throws InvalidConfigException - */ - public function getEmailIds(): array - { - return array_column($this->getEmails(), 'id'); - } - - /** - * @return Email[] - * @throws InvalidConfigException - */ - public function getEmails(): array - { - return $this->id ? Plugin::getInstance()->getEmails()->getAllEmailsByOrderStatusId($this->id) : []; - } - - public function getLabelHtml(): string - { - return Cp::statusLabelHtml([ - 'color' => Html::encode($this->color), - 'label' => Html::encode($this->getUiLabel()), - ]); - } - - /** - * @since 2.2 - */ - public function canDelete(): bool - { - /** @var OrderQuery $orderQuery */ - $orderQuery = Order::find()->trashed(null); - return !$orderQuery->orderStatus($this)->one() && !$this->default; - } - - /** - * Returns the config for this status. - * - * @since 5.0.3 - */ - public function getConfig(?array $emailIds = null): array - { - if ($emailIds === null) { - $emailIds = $this->getEmailIds(); - } - - $emails = !empty($emailIds) ? Db::uidsByIds(Table::EMAILS, $emailIds) : []; - return [ - 'name' => $this->name, - 'handle' => $this->handle, - 'color' => $this->color, - 'description' => $this->description, - 'sortOrder' => $this->sortOrder ?? 99, - 'default' => $this->default, - 'emails' => !empty($emails) ? array_combine($emails, $emails) : [], - 'store' => $this->getStore()->uid, - ]; - } - - /** - * @inheritdoc - */ - public static function get(int|string $id): ?static - { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $storeId = $site?->getStore()->id ?? null; - - /** @phpstan-ignore-next-line */ - return Plugin::getInstance()->getOrderStatuses()->getOrderStatusById($id, $storeId); - } - - /** - * @inheritdoc - */ - public function getId(): string|int|null - { - return $this->id; - } -} diff --git a/src/models/PaymentCurrency.php b/src/models/PaymentCurrency.php deleted file mode 100644 index a4e828d120..0000000000 --- a/src/models/PaymentCurrency.php +++ /dev/null @@ -1,203 +0,0 @@ - - * @since 2.0 - */ -class PaymentCurrency extends Model -{ - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var int|null Store ID - */ - public ?int $storeId = null; - - /** - * @var string|null ISO code - */ - public ?string $iso = null; - - /** - * @var float Exchange rate vs primary currency - */ - public float $rate = 1; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateCreated = null; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateUpdated = null; - - public function __toString(): string - { - return (string)$this->iso; - } - - /** - * @return Currency - */ - public function getCurrency(): Currency - { - return new Currency($this->iso); - } - - /** - * @return string - * @throws InvalidConfigException - */ - public function getCpEditUrl(): string - { - if ($this->storeId === null) { - return ''; - } - - $store = Plugin::getInstance()->getStores()->getStoreById($this->storeId); - if ($store === null) { - throw new InvalidConfigException('Invalid store ID: ' . $this->storeId); - } - - return UrlHelper::cpUrl(sprintf('commerce/store-management/%s/payment-currencies/%s', $store->handle, $this->id)); - } - - /** - * @inheritdoc - */ - public function attributes(): array - { - $names = parent::attributes(); - $names[] = 'minorUnit'; - $names[] = 'alphabeticCode'; - $names[] = 'currency'; - $names[] = 'numericCode'; - $names[] = 'entity'; - return $names; - } - - public function safeAttributes() - { - $names = parent::safeAttributes(); - return array_unique(array_merge(['id', 'storeId', 'iso', 'rate', 'dateCreated', 'dateUpdated'], $names)); - } - - /** - * @return string|null - */ - public function getAlphabeticCode(): ?string - { - return $this->iso; - } - - /** - * @return int|null - * @throws InvalidConfigException - */ - public function getNumericCode(): ?int - { - return Plugin::getInstance()->getCurrencies()->numericCodeFor($this->iso); - } - - public function getEntity(): ?string - { - // @TODO Implement getEntity() to return the country/region entity name from \craft\commerce\services\Currencies::$_isoCurrencies instead of an empty string - return ''; - } - - /** - * @return int|null - * @throws InvalidConfigException - * @deprecated Use getSubUnit() instead. - */ - public function getMinorUnit(): ?int - { - return $this->getSubUnit(); - } - - /** - * @return int|null - * @throws InvalidConfigException - */ - public function getSubUnit(): ?int - { - return Plugin::getInstance()->getCurrencies()->getSubunitFor($this->iso); - } - - /** - * Returns alias of getCurrency() - */ - public function getName(): ?string - { - return $this->iso; - } - - /** - * @return Store - * @throws InvalidConfigException - */ - public function getStore() - { - return Plugin::getInstance()->getStores()->getStoreById($this->storeId); - } - - /** - * @return bool - * @throws InvalidConfigException - */ - public function getPrimary(): bool - { - return $this->getCode() === $this->getStore()->getCurrency()->getCode(); - } - - /** - * @return string|null - */ - public function getCode() - { - return $this->iso; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['iso', 'rate'], 'required'], - [['iso'], UniqueValidator::class, 'targetClass' => PaymentCurrencyRecord::class, 'targetAttribute' => ['iso', 'storeId'], 'message' => '{attribute} "{value}" has already been taken.'], - ]; - } -} diff --git a/src/models/PaymentSource.php b/src/models/PaymentSource.php deleted file mode 100644 index 77d4db84c8..0000000000 --- a/src/models/PaymentSource.php +++ /dev/null @@ -1,142 +0,0 @@ - - * @since 2.0 - */ -class PaymentSource extends Model -{ - /** - * @var int|null Payment source ID - */ - public ?int $id = null; - - /** - * @var int The customer element ID - */ - public int $customerId; - - /** - * @var int The gateway ID. - */ - public int $gatewayId; - - /** - * @var string Token - */ - public string $token; - - /** - * @var string Description - */ - public string $description; - - /** - * @var string Response data - */ - public string $response; - - /** - * @var User|null $_user - */ - private ?User $_customer = null; - - /** - * @var GatewayInterface|null $_gateway - */ - private ?GatewayInterface $_gateway = null; - - - /** - * Returns the payment source token. - * - * @return string - */ - public function __toString() - { - return $this->token; - } - - /** - * Returns the user element associated with this payment source. - * - * @return User|null - */ - public function getCustomer(): ?User - { - if (!isset($this->_customer)) { - $this->_customer = Craft::$app->getUsers()->getUserById($this->customerId); - } - - return $this->_customer; - } - - /** - * @return bool - * @since 4.2 - */ - public function getIsPrimary(): bool - { - /** @var User|CustomerBehavior|null $customer */ - $customer = $this->getCustomer(); - return $customer && $customer->primaryPaymentSourceId === $this->id; - } - - /** - * @deprecated in 4.0.0. Use [[getCustomer()]] instead. - */ - public function getUser(): ?User - { - Craft::$app->getDeprecator()->log('PaymentSource::getUser()', 'The `PaymentSource::getUser()` is deprecated, use the `PaymentSource::getCustomer()` instead.'); - return $this->getCustomer(); - } - - /** - * Returns the gateway associated with this payment source. - * - * @return GatewayInterface|null - * @throws InvalidConfigException - */ - public function getGateway(): ?GatewayInterface - { - if ($this->_gateway === null && $this->gatewayId) { - $this->_gateway = Commerce::getInstance()->getGateways()->getGatewayById($this->gatewayId); - } - - return $this->_gateway; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['token'], UniqueValidator::class, 'targetAttribute' => ['gatewayId', 'token'], 'targetClass' => PaymentSourceRecord::class], - [['gatewayId', 'customerId', 'token', 'description'], 'required'], - [['id', 'response'], 'safe'], - ]; - } -} diff --git a/src/models/Pdf.php b/src/models/Pdf.php deleted file mode 100644 index d1180482c3..0000000000 --- a/src/models/Pdf.php +++ /dev/null @@ -1,234 +0,0 @@ - - * @since 3.2 - * - * @property-read array $config - */ -class Pdf extends Model implements HasStoreInterface -{ - use StoreTrait; - - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string|null Name - */ - public ?string $name = null; - - /** - * @var string|null Handle - */ - public ?string $handle = null; - - /** - * @var string|null Subject - */ - public ?string $description = null; - - /** - * @var bool Is Enabled - */ - public bool $enabled = true; - - /** - * @var bool Is default PDF for order - */ - public bool $isDefault = false; - - /** - * @var string Template path - */ - public string $templatePath = ''; - - /** - * @var string|null Filename format - */ - public ?string $fileNameFormat = null; - - /** - * @var int|null Sort order - */ - public ?int $sortOrder = null; - - /** - * @var string The orientation of the paper to use for generated order PDF files. - * - * Options are `'portrait'` and `'landscape'`. - * - * @since 5.0.0 - */ - public string $paperOrientation = PdfRecord::PAPER_ORIENTATION_PORTRAIT; - - /** - * @var string The size of the paper to use for generated order PDFs. - * - * The full list of supported paper sizes can be found [in the dompdf library](https://github.com/dompdf/dompdf/blob/master/src/Adapter/CPDF.php#L45). - * - * @since 5.0.0 - */ - public string $paperSize = 'letter'; - - /** - * @var string|null UID - */ - public ?string $uid = null; - - /** - * @var string locale language - */ - public string $language = PdfRecord::LOCALE_ORDER_LANGUAGE; - - - /** - * @return string - * @throws InvalidConfigException - * @since 5.0.0 - */ - public function getCpEditUrl(): string - { - return UrlHelper::cpUrl('commerce/settings/pdfs/' . $this->getStore()->handle . '/' . $this->id); - } - - /** - * @var int How long (in seconds) a PDF download link should remain valid before expiring - * @since 4.10 - */ - public int $linkExpiry = 86400; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['name', 'handle', 'templatePath', 'language'], 'required'], - [['handle'], - UniqueValidator::class, - 'targetClass' => PdfRecord::class, - 'targetAttribute' => ['handle', 'storeId'], - 'message' => '{attribute} "{value}" has already been taken.', - ], - [['paperOrientation'], 'in', 'range' => [PdfRecord::PAPER_ORIENTATION_PORTRAIT, PdfRecord::PAPER_ORIENTATION_LANDSCAPE]], - [['paperSize'], 'in', 'range' => array_keys(CPDF::$PAPER_SIZES)], - [[ - 'description', - 'enabled', - 'fileNameFormat', - 'handle', - 'id', - 'isDefault', - 'language', - 'linkExpiry', - 'name', - 'paperOrientation', - 'paperSize', - 'sortOrder', - 'storeId', - 'templatePath', - 'uid', - ], 'safe'], - ]; - } - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $fields = parent::extraFields(); - $fields[] = 'config'; - - return $fields; - } - - /** - * Determines the language this PDF - * - * @param Order|null $order - */ - public function getRenderLanguage(Order $order = null): string - { - $language = $this->language; - - if ($order == null && $language == PdfRecord::LOCALE_ORDER_LANGUAGE) { - throw new InvalidArgumentException('Can not get language for this PDF without providing an order'); - } - - if ($order && $language == PdfRecord::LOCALE_ORDER_LANGUAGE) { - $language = $order->orderLanguage; - } - - return $language; - } - - /** - * Returns the field layout config for this email. - * - * @since 3.2.0 - */ - public function getConfig(): array - { - return [ - 'description' => $this->description, - 'enabled' => $this->enabled, - 'fileNameFormat' => $this->fileNameFormat ?? '', - 'handle' => $this->handle, - 'isDefault' => $this->isDefault, - 'language' => $this->language, - 'name' => $this->name, - 'paperOrientation' => $this->paperOrientation, - 'paperSize' => $this->paperSize, - 'sortOrder' => $this->sortOrder ?: 9999, - 'store' => $this->getStore()->uid, - 'templatePath' => $this->templatePath, - 'linkExpiry' => $this->linkExpiry, - ]; - } - - /** - * @return string[] - * @since 5.0.0 - */ - public static function getPaperOrientationOptions(): array - { - return [ - PdfRecord::PAPER_ORIENTATION_PORTRAIT => Craft::t('commerce', 'Portrait'), - PdfRecord::PAPER_ORIENTATION_LANDSCAPE => Craft::t('commerce', 'Landscape'), - ]; - } - - /** - * @return array - * @since 5.0.0 - */ - public static function getPaperSizeOptions(): array - { - return collect(CPDF::$PAPER_SIZES)->mapWithKeys(fn($value, $key) => [$key => $key])->all(); - } -} diff --git a/src/models/ProductType.php b/src/models/ProductType.php deleted file mode 100644 index 5d34392b43..0000000000 --- a/src/models/ProductType.php +++ /dev/null @@ -1,750 +0,0 @@ - - * @since 2.0 - */ -class ProductType extends Model implements FieldLayoutProviderInterface -{ - /** @since 5.2.0 */ - public const DEFAULT_PLACEMENT_BEGINNING = 'beginning'; - /** @since 5.2.0 */ - public const DEFAULT_PLACEMENT_END = 'end'; - - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string|null Name - */ - public ?string $name = null; - - /** - * @var string|null Handle - */ - public ?string $handle = null; - - /** - * @var bool Whether versioning should be enabled for this product type. - * @since 5.0.0 - */ - public bool $enableVersioning = false; - - /** - * @var bool Has dimension - */ - public bool $hasDimensions = false; - - /** - * @var int|null Maximum number of variants - */ - public ?int $maxVariants = null; - - /** - * @var bool Has variant title field - */ - public bool $hasVariantTitleField = true; - - /** - * @var string Variant title format - */ - public string $variantTitleFormat = '{product.title}'; - - /** - * @var string Variant UI label format - * @since 5.6.0 - */ - public string $variantUiLabelFormat = '{title}'; - - /** - * @var string Variant title translation method - * @phpstan-var Field::TRANSLATION_METHOD_NONE|Field::TRANSLATION_METHOD_SITE|Field::TRANSLATION_METHOD_SITE_GROUP|Field::TRANSLATION_METHOD_LANGUAGE|Field::TRANSLATION_METHOD_CUSTOM - * @since 5.1.0 - */ - public string $variantTitleTranslationMethod = Field::TRANSLATION_METHOD_SITE; - - /** - * @var string|null Variant title translation key format - * @since 5.1.0 - */ - public ?string $variantTitleTranslationKeyFormat = null; - - /** - * @var bool Has product title field? - */ - public bool $hasProductTitleField = true; - - /** - * @var string Product title format - */ - public string $productTitleFormat = ''; - - /** - * @var string Product UI label format - * @since 5.6.0 - */ - public string $productUiLabelFormat = '{title}'; - - /** - * @var string Product title translation method - * @phpstan-var Field::TRANSLATION_METHOD_NONE|Field::TRANSLATION_METHOD_SITE|Field::TRANSLATION_METHOD_SITE_GROUP|Field::TRANSLATION_METHOD_LANGUAGE|Field::TRANSLATION_METHOD_CUSTOM - * @since 5.1.0 - */ - public string $productTitleTranslationMethod = Field::TRANSLATION_METHOD_SITE; - - /** - * @var string|null Product title translation key format - * @since 5.1.0 - */ - public ?string $productTitleTranslationKeyFormat = null; - - /** - * @var bool Whether to show the Slug field - * @since 5.5.0 - */ - public bool $showSlugField = true; - - /** - * @var string Slug translation method - * @phpstan-var Field::TRANSLATION_METHOD_NONE|Field::TRANSLATION_METHOD_SITE|Field::TRANSLATION_METHOD_SITE_GROUP|Field::TRANSLATION_METHOD_LANGUAGE|Field::TRANSLATION_METHOD_CUSTOM - * @since 5.5.0 - */ - public string $slugTranslationMethod = Field::TRANSLATION_METHOD_SITE; - - /** - * @var string|null Slug translation key format - * @since 5.5.0 - */ - public ?string $slugTranslationKeyFormat = null; - - /** - * @var string|null SKU format - */ - public ?string $skuFormat = null; - - /** - * @var string Description format - */ - public string $descriptionFormat = '{product.title} - {title}'; - - /** - * @var string|null Template - */ - public ?string $template = null; - - /** - * @var bool Is this a structure product type - * @since 5.2.0 - */ - public bool $isStructure = false; - - /** - * @var ?int max levels of structure - * @since 5.2.0 - */ - public ?int $maxLevels = null; - - /** - * @var string Default placement - * @phpstan-var self::DEFAULT_PLACEMENT_BEGINNING|self::DEFAULT_PLACEMENT_END - * @since 5.2.0 - */ - public string $defaultPlacement = self::DEFAULT_PLACEMENT_END; - - /** - * @var int|null Structure ID - * @since 5.2.0 - */ - public ?int $structureId = null; - - /** - * @var int|null Field layout ID - */ - public ?int $fieldLayoutId = null; - - /** - * @var int|null Variant layout ID - */ - public ?int $variantFieldLayoutId = null; - - /** - * @var string|null UID - */ - public ?string $uid = null; - - /** - * @var array|null Preview targets - * @since 5.5.0 - */ - public ?array $previewTargets = null; - - /** - * @var TaxCategory[]|null - */ - private ?array $_taxCategories = null; - - /** - * @var ShippingCategory[]|null - */ - private ?array $_shippingCategories = null; - - /** - * @var ProductTypeSite[]|null - */ - private ?array $_siteSettings = null; - - /** - * @var PropagationMethod Propagation method - * - * This will be set to one of the following: - * - * - [[PropagationMethod::None]] – Only save products in the site they were created in - * - [[PropagationMethod::SiteGroup]] – Save products to other sites in the same site group - * - [[PropagationMethod::Language]] – Save products to other sites with the same language - * - [[PropagationMethod::Custom]] – Save products to other sites based on a custom [[$propagationKeyFormat|propagation key format]] - * - [[PropagationMethod::All]] – Save products to all sites supported by the owner element - * - * @since 5.1.0 - */ - public PropagationMethod $propagationMethod = PropagationMethod::All; - - /** - * @inheritdoc - */ - public function init(): void - { - parent::init(); - - if (!isset($this->previewTargets)) { - $this->previewTargets = [ - [ - 'label' => Craft::t('app', 'Primary {type} page', [ - 'type' => Product::lowerDisplayName(), - ]), - 'urlFormat' => '{url}', - ], - ]; - } - - if ($this->productTitleTranslationKeyFormat === '') { - $this->productTitleTranslationKeyFormat = null; - } - - if ($this->variantTitleTranslationKeyFormat === '') { - $this->variantTitleTranslationKeyFormat = null; - } - - if ($this->slugTranslationKeyFormat === '') { - $this->slugTranslationKeyFormat = null; - } - } - - /** - * @return null|string - */ - public function __toString() - { - return (string)$this->handle; - } - - /** - * @inerhitdoc - */ - public function getHandle(): ?string - { - return $this->handle; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['id', 'fieldLayoutId', 'variantFieldLayoutId', 'structureId'], 'number', 'integerOnly' => true], - [['name', 'handle'], 'required'], - [ - ['variantTitleFormat'], - 'required', - 'when' => static fn($model) => - /** @var static $model */ - !$model->hasVariantTitleField, - ], - [ - ['productTitleFormat'], - 'required', - 'when' => static fn($model) => - /** @var static $model */ - !$model->hasProductTitleField, - ], - [['name', 'handle', 'descriptionFormat'], 'string', 'max' => 255], - [['handle'], UniqueValidator::class, 'targetClass' => ProductTypeRecord::class, 'targetAttribute' => ['handle'], 'message' => 'Not Unique'], - [['handle'], HandleValidator::class, 'reservedWords' => ['id', 'dateCreated', 'dateUpdated', 'uid', 'title']], - [['maxVariants'], 'integer', 'min' => 1], - ['fieldLayout', 'validateFieldLayout'], - ['variantFieldLayout', 'validateVariantFieldLayout'], - ['siteSettings', 'required', 'message' => Craft::t('commerce','At least one site must be enabled for the product type.')], - [['isStructure', 'defaultPlacement', 'maxLevels', 'structureId', 'productUiLabelFormat', 'variantUiLabelFormat'], 'safe'], - [['previewTargets'], 'validatePreviewTargets'], - ]; - } - - public function getCpEditUrl(): string - { - return UrlHelper::cpUrl('commerce/settings/producttypes/' . $this->id); - } - - public function getCpEditVariantUrl(): string - { - return UrlHelper::cpUrl('commerce/settings/producttypes/' . $this->id . '/variant'); - } - - /** - * Returns the site IDs that are enabled for the product type. - * - * @return int[] - * @since 5.1.0 - */ - public function getSiteIds(): array - { - return array_keys($this->getSiteSettings()); - } - - /** - * Returns the product type's site-specific settings. - * - * @return ProductTypeSite[] - * @throws InvalidConfigException - */ - public function getSiteSettings(): array - { - if (isset($this->_siteSettings)) { - return $this->_siteSettings; - } - - if (!$this->id) { - return []; - } - - $this->setSiteSettings(ArrayHelper::index(Plugin::getInstance()->getProductTypes()->getProductTypeSites($this->id), 'siteId')); - - return $this->_siteSettings; - } - - /** - * Sets the product type's site-specific settings. - * - * @param ProductTypeSite[] $siteSettings - */ - public function setSiteSettings(array $siteSettings): void - { - $this->_siteSettings = $siteSettings; - - foreach ($this->_siteSettings as $settings) { - $settings->setProductType($this); - } - } - - /** - * @return ShippingCategory[] - * @throws InvalidConfigException - */ - public function getShippingCategories(): array - { - if ($this->_shippingCategories === null && $this->id) { - $this->_shippingCategories = Plugin::getInstance()->getShippingCategories()->getShippingCategoriesByProductTypeId($this->id); - } - - return $this->_shippingCategories ?? []; - } - - /** - * @param int[]|ShippingCategory[] $shippingCategories - * @throws InvalidConfigException - */ - public function setShippingCategories(array $shippingCategories): void - { - $categories = []; - foreach ($shippingCategories as $category) { - if (is_numeric($category)) { - if ($category = Plugin::getInstance()->getShippingCategories()->getShippingCategoryById($category)) { - $categories[$category->id] = $category; - } - } elseif ($category instanceof ShippingCategory) { - // Make sure it exists - if ($category = Plugin::getInstance()->getShippingCategories()->getShippingCategoryById($category->id)) { - $categories[$category->id] = $category; - } - } - } - - $this->_shippingCategories = $categories; - } - - /** - * @return TaxCategory[] - * @throws InvalidConfigException - */ - public function getTaxCategories(): array - { - if ($this->_taxCategories === null && $this->id) { - $this->_taxCategories = Plugin::getInstance()->getTaxCategories()->getTaxCategoriesByProductTypeId($this->id); - } - - return $this->_taxCategories ?? []; - } - - /** - * @param int[]|TaxCategory[] $taxCategories - * @throws InvalidConfigException - */ - public function setTaxCategories(array $taxCategories): void - { - $categories = []; - foreach ($taxCategories as $category) { - if (is_numeric($category)) { - if ($category = Plugin::getInstance()->getTaxCategories()->getTaxCategoryById($category)) { - $categories[$category->id] = $category; - } - } else { - if ($category instanceof TaxCategory) { - // Make sure it exists. - if ($category = Plugin::getInstance()->getTaxCategories()->getTaxCategoryById($category->id)) { - $categories[$category->id] = $category; - } - } - } - } - - $this->_taxCategories = $categories; - } - - /** - * @inheritdoc - */ - public function getFieldLayout(): FieldLayout - { - return $this->getProductFieldLayout(); - } - - /** - * @throws InvalidConfigException - */ - public function getProductFieldLayout(): FieldLayout - { - /** @var FieldLayoutBehavior $behavior */ - $behavior = $this->getBehavior('productFieldLayout'); - $fieldLayout = $behavior->getFieldLayout(); - - // If this product type has variants, make sure the Variants field is in the layout somewhere - if (!$fieldLayout->isFieldIncluded('variants')) { - $layoutTabs = $fieldLayout->getTabs(); - $variantTabName = Craft::t('commerce', 'Variants'); - if (ArrayHelper::contains($layoutTabs, 'name', $variantTabName)) { - $variantTabName .= ' ' . StringHelper::randomString(10); - } - - $contentTab = new FieldLayoutTab(); - $contentTab->setLayout($fieldLayout); - $contentTab->name = $variantTabName; - $contentTab->setElements([ - ['type' => VariantsField::class], - ]); - - $layoutTabs[] = $contentTab; - $fieldLayout->setTabs($layoutTabs); - } - - return $fieldLayout; - } - - /** - * Validate the field layout to make sure no fields with reserved words are used. - * - * @since 3.4 - */ - public function validateFieldLayout(): void - { - $fieldLayout = $this->getFieldLayout(); - - $fieldLayout->reservedFieldHandles = [ - 'cheapestVariant', - 'defaultVariant', - 'variants', - ]; - - if (!$fieldLayout->validate()) { - $this->addModelErrors($fieldLayout, 'fieldLayout'); - } - } - - /** - * Validate the variant field layout to make sure no fields with reserved words are used. - * - * @since 3.4 - */ - public function validateVariantFieldLayout(): void - { - $variantFieldLayout = $this->getVariantFieldLayout(); - - $variantFieldLayout->reservedFieldHandles = [ - 'availableForPurchase', - 'description', - 'freeShipping', - 'hasUnlimitedStock', - 'height', - 'length', - 'maxQty', - 'minQty', - 'price', - 'product', - 'promotable', - 'promotionalPrice', - 'sku', - 'stock', - 'weight', - 'width', - ]; - - if (!$variantFieldLayout->validate()) { - $this->addModelErrors($variantFieldLayout, 'variantFieldLayout'); - } - } - - /** - * Validates the preview targets. - * - * @since 5.5.0 - */ - public function validatePreviewTargets(): void - { - $hasErrors = false; - - foreach ($this->previewTargets as &$target) { - $target['label'] = trim($target['label']); - $target['urlFormat'] = trim($target['urlFormat']); - - if ($target['label'] === '') { - $target['label'] = ['value' => $target['label'], 'hasErrors' => true]; - $hasErrors = true; - } - } - unset($target); - - if ($hasErrors) { - $this->addError('previewTargets', Craft::t('app', 'All targets must have a label.')); - } - } - - /** - * @throws InvalidConfigException - */ - public function getVariantFieldLayout(): FieldLayout - { - /** @var FieldLayoutBehavior $behavior */ - $behavior = $this->getBehavior('variantFieldLayout'); - return $behavior->getFieldLayout(); - } - - /** - * @return string - * @deprecated 4.0.0 - */ - public function getTitleFormat(): string - { - Craft::$app->getDeprecator()->log('craft\commerce\models\ProductType::titleFormat', 'Getting `ProductType::titleFormat` has been deprecate. Use `ProductType::variantTitleFormat` instead.'); - return $this->variantTitleFormat; - } - - /** - * @param string $titleFormat - * @return void - * @throws DeprecationException - * @deprecated 4.0.0 - */ - public function setTitleFormat(string $titleFormat): void - { - Craft::$app->getDeprecator()->log('craft\commerce\models\ProductType::titleFormat', 'Setting `ProductType::titleFormat` has been deprecate. Use `ProductType::variantTitleFormat` instead.'); - $this->variantTitleFormat = $titleFormat; - } - - /** - * @return bool - * @deprecated 5.0.0 - */ - public function getHasVariants(): bool - { - Craft::$app->getDeprecator()->log('craft\commerce\models\ProductType::hasVariants', 'Use `ProductType::maxVariants > 1` instead.'); - return $this->maxVariants > 1; - } - - /** - * @inheritdoc - */ - protected function defineBehaviors(): array - { - $behaviors['productFieldLayout'] = [ - 'class' => FieldLayoutBehavior::class, - 'elementType' => Product::class, - 'idAttribute' => 'fieldLayoutId', - ]; - - $behaviors['variantFieldLayout'] = [ - 'class' => FieldLayoutBehavior::class, - 'elementType' => Variant::class, - 'idAttribute' => 'variantFieldLayoutId', - ]; - - return $behaviors; - } - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $fields = parent::extraFields(); - $fields[] = 'taxCategories'; - $fields[] = 'shippingCategories'; - $fields[] = 'siteSettings'; - - return $fields; - } - - /** - * Returns the product types’s config. - * - * @return array - * @since 5.2.0 - */ - public function getConfig(): array - { - $config = [ - 'name' => $this->name, - 'handle' => $this->handle, - 'enableVersioning' => $this->enableVersioning, - 'hasDimensions' => $this->hasDimensions, - 'maxVariants' => $this->maxVariants, - - // Variant title field - 'hasVariantTitleField' => $this->hasVariantTitleField, - 'variantTitleFormat' => $this->variantTitleFormat, - 'variantTitleTranslationMethod' => $this->variantTitleTranslationMethod, - 'variantTitleTranslationKeyFormat' => $this->variantTitleTranslationKeyFormat, - 'variantUiLabelFormat' => $this->variantUiLabelFormat, - - // Product title field - 'hasProductTitleField' => $this->hasProductTitleField, - 'productTitleFormat' => $this->productTitleFormat, - 'productTitleTranslationMethod' => $this->productTitleTranslationMethod, - 'productTitleTranslationKeyFormat' => $this->productTitleTranslationKeyFormat, - 'productUiLabelFormat' => $this->productUiLabelFormat, - - // Slug field - 'showSlugField' => $this->showSlugField, - 'slugTranslationMethod' => $this->slugTranslationMethod, - 'slugTranslationKeyFormat' => $this->slugTranslationKeyFormat, - - 'propagationMethod' => $this->propagationMethod->value, - - 'skuFormat' => $this->skuFormat, - 'descriptionFormat' => $this->descriptionFormat, - 'siteSettings' => [], - - 'isStructure' => $this->isStructure, - 'maxLevels' => $this->maxLevels, - 'defaultPlacement' => $this->defaultPlacement, - ]; - - if (!empty($this->previewTargets)) { - $config['previewTargets'] = ProjectConfigHelper::packAssociativeArray(array_values($this->previewTargets)); - } - - if ($this->isStructure) { - $config['structure'] = [ - 'uid' => $this->structureId ? Db::uidById(Table::STRUCTURES, $this->structureId) : StringHelper::UUID(), - ]; - } - - $generateLayoutConfig = function(FieldLayout $fieldLayout): array { - $fieldLayoutConfig = $fieldLayout->getConfig(); - - if ($fieldLayoutConfig) { - if (empty($fieldLayout->id)) { - $layoutUid = StringHelper::UUID(); - $fieldLayout->uid = $layoutUid; - } else { - $layoutUid = Db::uidById(CraftTable::FIELDLAYOUTS, $fieldLayout->id); - } - - return [$layoutUid => $fieldLayoutConfig]; - } - - return []; - }; - - $config['productFieldLayouts'] = $generateLayoutConfig($this->getFieldLayout()); - $config['variantFieldLayouts'] = $generateLayoutConfig($this->getVariantFieldLayout()); - - // Get the site settings - $allSiteSettings = $this->getSiteSettings(); - - foreach ($allSiteSettings as $siteId => $settings) { - $siteUid = Db::uidById(CraftTable::SITES, $siteId); - $config['siteSettings'][$siteUid] = [ - 'hasUrls' => $settings['hasUrls'], - 'enabledByDefault' => $settings['enabledByDefault'], - 'uriFormat' => $settings['uriFormat'], - 'template' => $settings['template'], - ]; - } - - return $config; - } -} diff --git a/src/models/ProductTypeSite.php b/src/models/ProductTypeSite.php deleted file mode 100644 index 04e361cfb1..0000000000 --- a/src/models/ProductTypeSite.php +++ /dev/null @@ -1,141 +0,0 @@ - - * @since 2.0 - */ -class ProductTypeSite extends Model -{ - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var int Product type ID - */ - public int $productTypeId; - - /** - * @var int Site ID - */ - public int $siteId; - - /** - * @var bool Has Urls - */ - public bool $hasUrls = false; - - /** - * @var string|null URL Format - */ - public ?string $uriFormat = null; - - /** - * @var string|null Template Path - */ - public ?string $template = null; - - /** - * @var bool Enabled by default - * @since 5.1.0 - */ - public bool $enabledByDefault = true; - - /** - * @var ProductType|null - */ - private ?ProductType $_productType = null; - - /** - * @var Site|null - */ - private ?Site $_site = null; - - /** - * @var bool - */ - public bool $uriFormatIsRequired = true; - - - /** - * Returns the Product Type. - * - * @throws InvalidConfigException if [[productTypeId]] is missing or invalid - */ - public function getProductType(): ProductType - { - if ($this->_productType !== null) { - return $this->_productType; - } - - if (!$this->productTypeId) { - throw new InvalidConfigException('Product type site is missing its product type ID'); - } - - if (($this->_productType = Plugin::getInstance()->getProductTypes()->getProductTypeById($this->productTypeId)) === null) { - throw new InvalidConfigException('Invalid product type ID: ' . $this->productTypeId); - } - - return $this->_productType; - } - - /** - * Sets the Product Type. - */ - public function setProductType(ProductType $productType): void - { - $this->_productType = $productType; - } - - /** - * @throws InvalidConfigException if [[siteId]] is missing or invalid - */ - public function getSite(): Site - { - if ($this->_site !== null) { - return $this->_site; - } - - if (!$this->siteId) { - throw new InvalidConfigException('Product type site is missing its site ID'); - } - - if (($this->_site = Craft::$app->getSites()->getSiteById($this->siteId)) === null) { - throw new InvalidConfigException('Invalid site ID: ' . $this->siteId); - } - - return $this->_site; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = []; - - if ($this->uriFormatIsRequired) { - $rules[] = ['uriFormat', 'required']; - } - - return $rules; - } -} diff --git a/src/models/PurchasableStore.php b/src/models/PurchasableStore.php deleted file mode 100644 index 4fa267b968..0000000000 --- a/src/models/PurchasableStore.php +++ /dev/null @@ -1,105 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasableStore extends Model -{ - /** - * @var int|null - */ - public ?int $id = null; - - /** - * @var int|null - */ - public ?int $purchasableId = null; - - /** - * @var int|null - */ - public ?int $storeId = null; - - /** - * @var float|null - */ - public ?float $basePrice = null; - - /** - * @var float|null - */ - public ?float $basePromotionalPrice = null; - - /** - * @var int|null - */ - public ?int $stock = null; - - /** - * @var bool - */ - public bool $hasUnlimitedStock = false; - - /** - * @var int|null - */ - public ?int $minQty = null; - - /** - * @var int|null - */ - public ?int $maxQty = null; - - /** - * @var bool - */ - public bool $promotable = false; - - /** - * @var bool - */ - public bool $availableForPurchase = false; - - /** - * @var bool - * @since 5.3.0 - */ - public bool $allowOutOfStockPurchases = false; - - /** - * @var bool - */ - public bool $freeShipping = false; - - /** - * @var int|null - */ - public ?int $shippingCategoryId = null; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['purchasableId', 'storeId'], 'required']; - $rules[] = [['purchasableId', 'storeId', 'stock', 'minQty', 'maxQty'], 'integer']; - $rules[] = [['basePrice', 'basePromotionalPrice'], 'number']; - $rules[] = [['hasUnlimitedStock', 'promotable', 'availableForPurchase', 'freeShipping', 'allowOutOfStockPurchases'], 'boolean']; - $rules[] = [['shippingCategoryId'], 'safe']; - - return $rules; - } -} diff --git a/src/models/Sale.php b/src/models/Sale.php deleted file mode 100644 index ff41e2c444..0000000000 --- a/src/models/Sale.php +++ /dev/null @@ -1,268 +0,0 @@ - - * @since 2.0 - */ -class Sale extends Model -{ - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string|null Name - */ - public ?string $name = null; - - /** - * @var string|null Description - */ - public ?string $description = null; - - /** - * @var DateTime|null Date From - */ - public ?DateTime $dateFrom = null; - - /** - * @var DateTime|null Date To - */ - public ?DateTime $dateTo = null; - - /** - * @var string How the sale should be applied - */ - public string $apply = SaleRecord::APPLY_BY_PERCENT; - - /** - * @var float|null The amount field used by the apply option - */ - public ?float $applyAmount = null; - - /** - * @var bool ignore the previous sales that affect the purchasable - */ - public bool $ignorePrevious = false; - - /** - * @var bool should the sales system stop processing other sales after this one - */ - public bool $stopProcessing = false; - - /** - * @var bool Match all groups - */ - public bool $allGroups = false; - - /** - * @var bool Match all purchasables - */ - public bool $allPurchasables = false; - - /** - * @var bool Match all categories - */ - public bool $allCategories = false; - - /** - * @var string Type of relationship between Categories and Products - */ - public string $categoryRelationshipType = SaleRecord::CATEGORY_RELATIONSHIP_TYPE_BOTH; - - /** - * @var bool Enabled - */ - public bool $enabled = true; - - /** - * @var int|null The order index of the application of the sale - */ - public ?int $sortOrder = null; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateCreated = null; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateUpdated = null; - - /** - * @var int[]|null Product Ids - */ - private ?array $_purchasableIds = null; - - /** - * @var int[]|null Product Type IDs - */ - private ?array $_categoryIds = null; - - /** - * @var int[]|null Group IDs - */ - private ?array $_userGroupIds = null; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['apply'], 'in', 'range' => ['toPercent', 'toFlat', 'byPercent', 'byFlat']], - [ - ['categoryRelationshipType'], - 'in', - 'range' => [ - SaleRecord::CATEGORY_RELATIONSHIP_TYPE_SOURCE, - SaleRecord::CATEGORY_RELATIONSHIP_TYPE_TARGET, - SaleRecord::CATEGORY_RELATIONSHIP_TYPE_BOTH, - ], - ], - [['enabled'], 'boolean'], - [['name', 'apply', 'allGroups', 'allPurchasables', 'allCategories'], 'required'], - ]; - } - - public function getCpEditUrl(): string - { - // Sales cannot exist with multiple stores so we can just use the primary store - $store = Plugin::getInstance()->getStores()->getPrimaryStore(); - return $store->getStoreSettingsUrl('sales/' . $this->id); - } - - /** - * @return array - */ - public function extraFields(): array - { - $fields = parent::extraFields(); - $fields[] = 'purchasableIds'; - - return $fields; - } - - public function getApplyAmountAsPercent(): string - { - return Craft::$app->getFormatter()->asPercent(-($this->applyAmount ?? 0.0)); - } - - public function getApplyAmountAsFlat(): string - { - return $this->applyAmount !== null ? (string)($this->applyAmount * -1) : '0'; - } - - public function getCategoryIds(): array - { - if (!isset($this->_categoryIds)) { - $categoryIds = []; - if ($this->id) { - $categoryIds = (new Query())->select( - 'spt.categoryId') - ->from(Table::SALES . ' sales') - ->leftJoin(Table::SALE_CATEGORIES . ' spt', '[[spt.saleId]]=[[sales.id]]') - ->where(['sales.id' => $this->id]) - ->column(); - - $categoryIds = array_filter($categoryIds); - } - - $this->_categoryIds = $categoryIds; - } - - return $this->_categoryIds; - } - - public function getPurchasableIds(): array - { - if (!isset($this->_purchasableIds)) { - $purchasableIds = []; - if ($this->id) { - $purchasableIds = (new Query())->select( - '[[sp.purchasableId]]') - ->from(Table::SALES . ' sales') - ->leftJoin(Table::SALE_PURCHASABLES . ' sp', '[[sp.saleId]]=[[sales.id]]') - ->where(['sales.id' => $this->id]) - ->column(); - - $purchasableIds = array_filter($purchasableIds); - } - - $this->_purchasableIds = $purchasableIds; - } - - return $this->_purchasableIds; - } - - public function getUserGroupIds(): array - { - if (!isset($this->_userGroupIds)) { - $userGroupIds = []; - if ($this->id) { - $userGroupIds = (new Query())->select( - 'sug.userGroupId') - ->from(Table::SALES . ' sales') - ->leftJoin(Table::SALE_USERGROUPS . ' sug', '[[sug.saleId]]=[[sales.id]]') - ->where(['sales.id' => $this->id]) - ->column(); - $userGroupIds = array_filter($userGroupIds); - } - - $this->_userGroupIds = $userGroupIds; - } - - return $this->_userGroupIds; - } - - /** - * Sets the related category ids - */ - public function setCategoryIds(array $ids): void - { - $this->_categoryIds = array_unique($ids); - } - - /** - * Sets the related purchasable ids - */ - public function setPurchasableIds(array $purchasableIds): void - { - $this->_purchasableIds = array_unique($purchasableIds); - } - - /** - * Sets the related user group ids - */ - public function setUserGroupIds(array $userGroupIds): void - { - $this->_userGroupIds = array_unique($userGroupIds); - } -} diff --git a/src/models/Settings.php b/src/models/Settings.php deleted file mode 100644 index 9708dd8f80..0000000000 --- a/src/models/Settings.php +++ /dev/null @@ -1,352 +0,0 @@ - - * @since 2.0 - */ -class Settings extends Model -{ - public const VIEW_URI_ORDERS = 'commerce/orders'; - public const VIEW_URI_PRODUCTS = 'commerce/products'; - /** - * @since 5.0.0. - */ - public const VIEW_URI_INVENTORY = 'commerce/inventory'; - - /** - * @since 5.0.0. - */ - public const VIEW_URI_STORE_MANAGEMENT = 'commerce/store-management'; - - /** - * @deprecated in 5.0.0. - */ - public const VIEW_URI_CUSTOMERS = 'commerce/customers'; - - /** - * @deprecated in 5.0.0. - */ - public const VIEW_URI_PROMOTIONS = 'commerce/promotions'; - - /** - * @deprecated in 5.0.0. - */ - public const VIEW_URI_SHIPPING = 'commerce/shipping/shippingmethods'; - - /** - * @deprecated in 5.0.0. - */ - public const VIEW_URI_TAX = 'commerce/tax/taxrates'; - public const VIEW_URI_SUBSCRIPTIONS = 'commerce/subscriptions'; - - /** - * @var mixed How long a cart should go without being updated before it’s considered inactive. - * - * See [craft\helpers\ConfigHelper::durationInSeconds()](craft5:craft\helpers\ConfigHelper::durationInSeconds()) for a list of supported value types. - * - * @group Cart - * @since 2.2 - * @defaultAlt 1 hour - */ - public mixed $activeCartDuration = 3600; - - /** - * @var string Key to be used when returning cart information in a response. - * @group Cart - */ - public string $cartVariable = 'cart'; - - /** - * @var string Commerce’s default control panel view. (Defaults to order index.) - * @group System - * @since 2.2 - */ - public string $defaultView = 'commerce/orders'; - - /** - * @var string Unit type for dimension measurements. - * - * Options: - * - * - `'mm'` - * - `'cm'` - * - `'m'` - * - `'ft'` - * - `'in'` - * - * @group Units - */ - public string $dimensionUnits = 'mm'; - - /** - * @var string The path to the template that should be used to perform POST requests to offsite payment gateways. - * - * The template must contain a form that posts to the URL supplied by the `actionUrl` variable and outputs all hidden inputs with - * the `inputs` variable. - * - * ```twig - * - * - * - * - * Redirecting... - * - * - *
- *

Redirecting to payment page...

- *

- * {{ inputs|raw }} - * - *

- *
- * - * - * ``` - * - * ::: tip - * Since this template is simply used for redirecting, it only appears for a few seconds, so we suggest making it load fast with minimal - * images and inline styles to reduce HTTP requests. - * ::: - * - * If empty (default), each gateway will decide how to handle after-payment redirects. - * - * @group Payments - */ - public string $gatewayPostRedirectTemplate = ''; - - /** - * @var string|null Default URL to be loaded after using the [load cart controller action](https://craftcms.com/docs/commerce/5.x/system/orders-carts.html#loading-a-cart). - * - * If `null` (default), Craft’s default [`siteUrl`](config5:siteUrl) will be used. - * - * @group Cart - * @since 3.1 - */ - public ?string $loadCartRedirectUrl = null; - - /** - * @var int How long (in seconds) a cart recovery link should remain valid before expiring. - * Default is 604800 (7 days). - * - * @group Cart - * @since 5.7.0 - */ - public int $loadCartUrlExpiry = 604800; - - /** - * @var array|null ISO codes for supported payment currencies. - * - * See [Payment Currencies](https://craftcms.com/docs/commerce/5.x/system/payment-currencies.html). - * - * @group Payments - */ - public ?array $paymentCurrency = null; - - /** - * @var bool Whether to allow non-local images in generated order PDFs. - * @group Orders - */ - public bool $pdfAllowRemoteImages = false; - - /** - * @var bool Whether inactive carts should automatically be deleted from the database during garbage collection. - * - * ::: tip - * You can control how long a cart should go without being updated before it gets deleted [`purgeInactiveCartsDuration`](#purgeinactivecartsduration) setting. - * ::: - * - * @group Cart - */ - public bool $purgeInactiveCarts = true; - - /** - * @var mixed Default length of time before inactive carts are purged. (Defaults to 90 days.) - * - * See [craft\helpers\ConfigHelper::durationInSeconds()](craft5:craft\helpers\ConfigHelper::durationInSeconds()) for a list of supported value types. - * - * @group Cart - * @defaultAlt 90 days - */ - public mixed $purgeInactiveCartsDuration = 7776000; - - /** - * @var string URL for a user to resolve billing issues with their subscription. - * - * ::: tip - * The example templates include [a template for this page](https://github.com/craftcms/commerce/tree/5.x/example-templates/dist/shop/plans/update-billing-details.twig). - * ::: - * - * @group Orders - */ - public string $updateBillingDetailsUrl = ''; - - /** - * @var bool Whether the search index for a cart should be updated when saving the cart via `commerce/cart/*` controller actions. - * - * May be set to `false` to reduce performance impact on high-traffic sites. - * - * ::: warning - * Setting this to `false` will result in fewer index update queue jobs, but you’ll need to manually re-index orders to ensure up-to-date cart search results in the control panel. - * ::: - * - * @group Cart - * @since 3.1.5 - */ - public bool $updateCartSearchIndexes = true; - - /** - * @var string Units to be used for weight measurements. - * - * Options: - * - * - `'g'` - * - `'kg'` - * - `'lb'` - * - * @group Units - */ - public string $weightUnits = 'g'; - - /** - * @var bool Whether to validate custom fields when a cart is updated. - * - * Set to `true` to allow custom content fields to return validation errors when a cart is updated. - * - * @group Cart - * @since 3.0.12 - */ - public bool $validateCartCustomFieldsOnSubmission = false; - - /** - * @inheritDoc - */ - public function setAttributes($values, $safeOnly = true): void - { - unset( - $values['orderPdfFilenameFormat'], - $values['orderPdfPath'], - $values['emailSenderAddress'], - $values['emailSenderAddressPlaceholder'], - $values['emailSenderName'], - $values['emailSenderNamePlaceholder'], - $values['autoSetNewCartAddresses'], - $values['autoSetCartShippingMethodOption'], - $values['autoSetPaymentSource'], - $values['allowEmptyCartOnCheckout'], - $values['allowCheckoutWithoutPayment'], - $values['allowPartialPaymentOnCheckout'], - $values['orderReferenceFormat'], - $values['requireShippingAddressAtCheckout'], - $values['requireBillingAddressAtCheckout'], - $values['requireShippingMethodSelectionAtCheckout'], - $values['useBillingAddressForTax'], - $values['freeOrderPaymentStrategy'], - $values['minimumTotalPriceStrategy'], - $values['showEditUserCommerceTab'], - ); - parent::setAttributes($values, $safeOnly); - } - - /** - * Returns a key-value array of weight unit options and labels. - */ - public function getWeightUnitsOptions(): array - { - return [ - 'g' => Craft::t('commerce', 'Grams (g)'), - 'kg' => Craft::t('commerce', 'Kilograms (kg)'), - 'lb' => Craft::t('commerce', 'Pounds (lb)'), - ]; - } - - /** - * Returns a key-value array of dimension unit options and labels. - */ - public function getDimensionUnits(): array - { - return [ - 'mm' => Craft::t('commerce', 'Millimeters (mm)'), - 'cm' => Craft::t('commerce', 'Centimeters (cm)'), - 'm' => Craft::t('commerce', 'Meters (m)'), - 'ft' => Craft::t('commerce', 'Feet (ft)'), - 'in' => Craft::t('commerce', 'Inches (in)'), - ]; - } - - /** - * Returns the ISO payment currency for a given site, or the default site if no handle is provided. - * - * @param string|null $siteHandle - * @return string|null - * @throws CurrencyException - * @throws InvalidConfigException if the currency in the config file is not set up - * @throws SiteNotFoundException - */ - public function getPaymentCurrency(string $siteHandle = null): ?string - { - /** @var Site|StoreBehavior|null $site */ - $site = $siteHandle ? Craft::$app->getSites()->getSiteByHandle($siteHandle) : Craft::$app->getSites()->getPrimarySite(); - if (!$site) { - throw new InvalidConfigException("Invalid site: $siteHandle"); - } - - $paymentCurrency = ConfigHelper::localizedValue($this->paymentCurrency, $siteHandle); - $allPaymentCurrencies = Plugin::getInstance()->getPaymentCurrencies()->getAllPaymentCurrencies($site->getStore()->id); - - if ($paymentCurrency && !$allPaymentCurrencies->contains('iso', '==', $paymentCurrency)) { - throw new InvalidConfigException("Invalid payment currency: $paymentCurrency"); - } - - return $paymentCurrency; - } - - /** - * Returns a key-value array of default control panel view options and labels. - * - * @since 2.2 - */ - public function getDefaultViewOptions(): array - { - return [ - self::VIEW_URI_ORDERS => Craft::t('commerce', 'Orders'), - self::VIEW_URI_PRODUCTS => Craft::t('commerce', 'Products'), - self::VIEW_URI_INVENTORY => Craft::t('commerce', 'Inventory'), - self::VIEW_URI_STORE_MANAGEMENT => Craft::t('commerce', 'Store Management'), - self::VIEW_URI_SUBSCRIPTIONS => Craft::t('commerce', 'Subscriptions'), - ]; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['weightUnits', 'dimensionUnits'], 'required'], - ]; - } -} diff --git a/src/models/ShippingAddressZone.php b/src/models/ShippingAddressZone.php deleted file mode 100644 index 97692a973c..0000000000 --- a/src/models/ShippingAddressZone.php +++ /dev/null @@ -1,79 +0,0 @@ - - * @since 2.0 - * - * @property-read string $cpEditUrl - */ -class ShippingAddressZone extends Zone implements Chippable -{ - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['name'], UniqueValidator::class, 'targetClass' => ShippingZone::class, 'targetAttribute' => ['name', 'storeId']]; - - return $rules; - } - - /** - * @return string - * @throws InvalidConfigException - */ - public function getCpEditUrl(): string - { - return UrlHelper::cpUrl('commerce/store-management/' . $this->getStore()->handle . '/shippingzones/' . $this->id); - } - - /** - * @inheritdoc - */ - public static function get(int|string $id): ?static - { - foreach (Plugin::getInstance()->getStores()->getAllStores() as $store) { - $zone = Plugin::getInstance()->getShippingZones()->getShippingZoneById((int)$id, $store->id); - if ($zone !== null) { - /** @phpstan-ignore-next-line */ - return $zone; - } - } - return null; - } - - /** - * @inheritdoc - */ - public function getUiLabel(): string - { - return Craft::t('site', $this->name); - } - - /** - * @inheritdoc - */ - public function getId(): ?int - { - return $this->id; - } -} diff --git a/src/models/ShippingCategory.php b/src/models/ShippingCategory.php deleted file mode 100644 index a1413d0594..0000000000 --- a/src/models/ShippingCategory.php +++ /dev/null @@ -1,231 +0,0 @@ - - * @since 2.0 - */ -class ShippingCategory extends Model implements HasStoreInterface, Chippable, Colorable, Iconic -{ - use StoreTrait; - - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string|null Name - */ - public ?string $name = null; - - /** - * @var string|null Handle - */ - public ?string $handle = null; - - /** - * @var string|null Icon - */ - public ?string $icon = null; - - /** - * @var string|null Color - */ - public ?string $color = null; - - /** - * @var string|null Description - */ - public ?string $description = null; - - /** - * @var bool Default - */ - public bool $default = false; - - /** - * @var ProductType[]|null - */ - private ?array $_productTypes = null; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateCreated = null; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateUpdated = null; - - /** - * @var DateTime|null Date deleted - * @since 4.2.0.1 - */ - public ?DateTime $dateDeleted = null; - - /** - * Returns the name of this shipping category. - * - * @return string - */ - public function __toString() - { - return (string)$this->name; - } - - public function getCpEditUrl(): string - { - return $this->getStore()->getStoreSettingsUrl('shippingcategories/' . $this->id); - } - - /** - * @inheritdoc - */ - public static function get(int|string $id): ?static - { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $storeId = $site?->getStore()->id ?? null; - - /** @phpstan-ignore-next-line */ - return Plugin::getInstance()->getShippingCategories()->getShippingCategoryById($id, $storeId); - } - - /** - * @inheritdoc - */ - public function getUiLabel(): string - { - return Craft::t('site', $this->name); - } - - /** - * @inheritdoc - */ - public function getId(): ?int - { - return $this->id; - } - - /** - * @inheritdoc - */ - public function getIcon(): ?string - { - return $this->icon; - } - - /** - * @inheritdoc - */ - public function getColor(): ?Color - { - return $this->color ? Color::tryFrom($this->color) : null; - } - - /** - * @param ProductType[] $productTypes - */ - public function setProductTypes(array $productTypes): void - { - $this->_productTypes = $productTypes; - } - - /** - * @return ProductType[] - * @throws InvalidConfigException - */ - public function getProductTypes(): array - { - if (!isset($this->_productTypes) && $this->id) { - $this->_productTypes = Plugin::getInstance()->getProductTypes()->getProductTypesByShippingCategoryId($this->id); - } - - return $this->_productTypes ?? []; - } - - /** - * Helper method to just get the product type IDs - * - * @return int[] - * @throws InvalidConfigException - */ - public function getProductTypeIds(): array - { - return ArrayHelper::getColumn($this->getProductTypes(), 'id', false); - } - - protected function defineRules(): array - { - return [ - [['name', 'handle'], 'required'], - [['handle'], - UniqueValidator::class, - 'targetClass' => ShippingCategoryRecord::class, - 'targetAttribute' => ['handle', 'storeId'], - 'message' => '{attribute} "{value}" has already been taken.', - ], - [['handle'], HandleValidator::class], - [[ - 'dateCreated', - 'dateDeleted', - 'dateUpdated', - 'default', - 'description', - 'handle', - 'icon', - 'color', - 'id', - 'name', - 'storeId', - ], 'safe'], - ]; - } - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $fields = parent::extraFields(); - $fields[] = 'productTypes'; - $fields[] = 'productTypeIds'; - $fields[] = 'uiLabel'; - - return $fields; - } -} diff --git a/src/models/ShippingMethod.php b/src/models/ShippingMethod.php deleted file mode 100644 index 64d7996cb6..0000000000 --- a/src/models/ShippingMethod.php +++ /dev/null @@ -1,191 +0,0 @@ - - * @since 2.0 - */ -class ShippingMethod extends BaseShippingMethod implements Chippable, Colorable, Iconic, Statusable -{ - public function behaviors(): array - { - $behaviors = parent::behaviors(); - - $behaviors['typecast'] = [ - 'class' => AttributeTypecastBehavior::class, - 'attributeTypes' => [ - 'id' => AttributeTypecastBehavior::TYPE_INTEGER, - 'name' => AttributeTypecastBehavior::TYPE_STRING, - 'handle' => AttributeTypecastBehavior::TYPE_STRING, - 'enabled' => AttributeTypecastBehavior::TYPE_BOOLEAN, - ], - ]; - - return $behaviors; - } - - /** - * @inheritdoc - */ - public function getType(): string - { - return Craft::t('commerce', 'Custom'); - } - - /** - * @inheritdoc - */ - public function getId(): ?int - { - return $this->id; - } - - /** - * @inheritdoc - */ - public function getName(): string - { - return (string)$this->name; - } - - /** - * @inheritdoc - */ - public function getHandle(): string - { - return (string)$this->handle; - } - - /** - * @inheritdoc - */ - public function getShippingRules(): Collection - { - if ($this->id === null) { - return collect(); - } - - return Plugin::getInstance()->getShippingRules()->getAllShippingRulesByShippingMethodId($this->id); - } - - /** - * @inheritdoc - */ - public function getIsEnabled(): bool - { - return $this->enabled; - } - - /** - * @inheritdoc - */ - public function getCpEditUrl(): string - { - return $this->getStore()->getStoreSettingsUrl('shippingmethods/' . $this->id); - } - - /** - * @inheritdoc - */ - public static function get(int|string $id): ?static - { - /** @phpstan-ignore-next-line */ - return Plugin::getInstance()->getShippingMethods()->getShippingMethodById($id); - } - - /** - * @inheritdoc - */ - public function getUiLabel(): string - { - return Craft::t('site', $this->name); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['name', 'handle'], 'required']; - $rules[] = [['name'], UniqueValidator::class, 'targetClass' => ShippingMethodRecord::class, 'targetAttribute' => ['name', 'storeId']]; - $rules[] = [['handle'], UniqueValidator::class, - 'targetClass' => ShippingMethodRecord::class, - 'targetAttribute' => ['handle', 'storeId'], - 'message' => '{attribute} "{value}" has already been taken.', - ]; - - return $rules; - } - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $fields = parent::extraFields(); - $fields[] = 'shippingRules'; - - return $fields; - } - - /** - * @inheritdoc - */ - public function getIcon(): ?string - { - return $this->icon; - } - - /** - * @inheritdoc - */ - public function getColor(): ?Color - { - return $this->color ? Color::tryFrom($this->color) : null; - } - - /** - * @inheritdoc - */ - public static function statuses(): array - { - return [ - 'enabled' => ['label' => Craft::t('commerce', 'Enabled'), 'color' => 'green'], - 'disabled' => ['label' => Craft::t('commerce', 'Disabled'), 'color' => 'red'], - ]; - } - - /** - * @inheritdoc - */ - public function getStatus(): ?string - { - return $this->enabled ? 'enabled' : 'disabled'; - } -} diff --git a/src/models/ShippingMethodOption.php b/src/models/ShippingMethodOption.php deleted file mode 100644 index 57db9d9d88..0000000000 --- a/src/models/ShippingMethodOption.php +++ /dev/null @@ -1,94 +0,0 @@ - - * @since 3.1 - */ -class ShippingMethodOption extends ShippingMethod -{ - /** - * @var Order - */ - private Order $_order; - - /** - * @var float Price of the shipping method option - */ - public float $price; - - /** - * @var boolean - */ - public bool $matchesOrder; - - /** - * @var ?ShippingMethodInterface - * @since 4.3.1 - */ - public ?ShippingMethodInterface $shippingMethod = null; - - /** - * @throws InvalidConfigException - */ - public function behaviors(): array - { - $behaviors = parent::behaviors(); - - $behaviors['currencyAttributes'] = [ - 'class' => CurrencyAttributeBehavior::class, - 'currencyAttributes' => $this->currencyAttributes(), - ]; - - return $behaviors; - } - - /** - * The attributes on the order that should be made available as formatted currency. - */ - public function currencyAttributes(): array - { - $attributes = []; - $attributes[] = 'price'; - return $attributes; - } - - protected function getCurrency(): string - { - if (!isset($this->_order->currency)) { - throw new InvalidConfigException('Order doesn’t have a currency.'); - } - - return $this->_order->currency; - } - - public function getPrice(): float - { - return $this->price; - } - - /** - * @since 3.1.10 - */ - public function setOrder(Order $order): void - { - $this->_order = $order; - } -} diff --git a/src/models/ShippingRule.php b/src/models/ShippingRule.php deleted file mode 100644 index 059b6ab4f7..0000000000 --- a/src/models/ShippingRule.php +++ /dev/null @@ -1,518 +0,0 @@ - - * @since 2.0 - */ -class ShippingRule extends Model implements ShippingRuleInterface, HasStoreInterface -{ - use StoreTrait; - - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string|null Name - */ - public ?string $name = null; - - /** - * @var string|null Description - */ - public ?string $description = null; - - /** - * @var int|null Shipping method ID - */ - public ?int $methodId = null; - - /** - * @var int Priority - */ - public int $priority = 0; - - /** - * @var bool Enabled - */ - public bool $enabled = true; - - /** - * @var string|null Order Condition Formula - */ - public ?string $orderConditionFormula = ''; - - /** - * @var float Base rate - */ - public float $baseRate = 0; - - /** - * @var float Per item rate - */ - public float $perItemRate = 0; - - /** - * @var float Percentage rate - */ - public float $percentageRate = 0; - - /** - * @var float Weight rate - */ - public float $weightRate = 0; - - /** - * @var float Minimum Rate - */ - public float $minRate = 0; - - /** - * @var float Maximum rate - */ - public float $maxRate = 0; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateCreated = null; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateUpdated = null; - - /** - * @var ShippingRuleCategory[]|null - */ - private ?array $_shippingRuleCategories = null; - - /** - * @var string|array|ShippingRuleOrderCondition|null - * @see setOrderCondition() - * @see getOrderCondition() - * @since 5.0.0 - */ - private ShippingRuleOrderCondition|string|array|null $_orderCondition = null; - - /** - * @var string|array|ShippingRuleCustomerCondition|null - * @see setCustomerCondition() - * @see getCustomerCondition() - * @since 5.4.0 - */ - private ShippingRuleCustomerCondition|string|array|null $_customerCondition = null; - - /** - * @throws InvalidConfigException - */ - private function _getUniqueCategoryIdsInOrder(Order $order): array - { - $orderShippingCategories = []; - foreach ($order->getLineItems() as $lineItem) { - // Don't look at the shipping category of non-shippable products. - if (!$lineItem->getIsShippable()) { - continue; - } - - $orderShippingCategories[] = $lineItem->shippingCategoryId; - } - - return array_unique($orderShippingCategories); - } - - /** - * @param $shippingRuleCategories - * @return array - */ - private function _getRequiredAndDisallowedCategoriesFromRule($shippingRuleCategories): array - { - $disallowedCategories = []; - $requiredCategories = []; - foreach ($shippingRuleCategories as $ruleCategory) { - if ($ruleCategory->condition === ShippingRuleCategoryRecord::CONDITION_DISALLOW) { - $disallowedCategories[] = $ruleCategory->shippingCategoryId; - } - - if ($ruleCategory->condition === ShippingRuleCategoryRecord::CONDITION_REQUIRE) { - $requiredCategories[] = $ruleCategory->shippingCategoryId; - } - } - return [$disallowedCategories, $requiredCategories]; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [ - [ - 'name', - 'methodId', - 'priority', - 'enabled', - 'baseRate', - 'perItemRate', - 'weightRate', - 'percentageRate', - 'minRate', - 'maxRate', - ], - 'required', - ], - [['perItemRate', 'weightRate', 'percentageRate'], 'number'], - [['shippingRuleCategories'], 'validateShippingRuleCategories', 'skipOnEmpty' => true], - [['orderConditionFormula'], 'string', 'length' => [1, 65000], 'skipOnEmpty' => true], - [ - 'orderConditionFormula', - function($attribute) { - if ($this->{$attribute}) { - $order = Order::find()->one(); - if (!$order) { - $order = new Order(); - } - $orderAsArray = Plugin::getInstance()->getShippingMethods()->getSerializedOrderForMatchingRules($order); - $orderConditionParams = [ - 'order' => $orderAsArray, - ]; - if (!Plugin::getInstance()->getFormulas()->validateConditionSyntax($this->{$attribute}, $orderConditionParams)) { - $this->addError($attribute, Craft::t('commerce', 'Invalid order condition syntax.')); - } - } - }, - ], - [['id', 'customerCondition', 'orderCondition', 'description', 'storeId'], 'safe'], - ]; - } - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $fields = parent::extraFields(); - $fields[] = 'shippingRuleCategories'; - - return $fields; - } - - /** - * @inheritdoc - */ - public function getIsEnabled(): bool - { - return $this->enabled; - } - - /** - * @param ShippingRuleOrderCondition|string|array|null $condition - * @return void - * @throws InvalidConfigException - * @since 5.0.0 - */ - public function setOrderCondition(ShippingRuleOrderCondition|string|array|null $condition): void - { - if (empty($condition)) { - $this->_orderCondition = null; - return; - } - - $this->_orderCondition = $condition; - } - - /** - * @return ShippingRuleOrderCondition - * @since 5.0.0 - */ - public function getOrderCondition(): ShippingRuleOrderCondition - { - if ($this->_orderCondition instanceof ShippingRuleOrderCondition) { - return $this->_orderCondition; - } - - $condition = $this->_orderCondition ?? []; - if (is_string($condition)) { - $condition = Json::decodeIfJson($condition); - } - - $condition['class'] = ShippingRuleOrderCondition::class; - $condition = Craft::$app->getConditions()->createCondition($condition); - /** @var ShippingRuleOrderCondition $condition */ - $condition->forProjectConfig = false; - $condition->mainTag = 'div'; - $condition->name = 'orderCondition'; - $condition->storeId = $this->storeId; - - $this->_orderCondition = $condition; - - return $this->_orderCondition; - } - - /** - * @param ShippingRuleCustomerCondition|string|array|null $condition - * @return void - * @throws InvalidConfigException - * @since 5.4.0 - */ - public function setCustomerCondition(ShippingRuleCustomerCondition|string|array|null $condition): void - { - if (empty($condition)) { - $this->_customerCondition = null; - return; - } - - $this->_customerCondition = $condition; - } - - /** - * @return ShippingRuleCustomerCondition - * @throws InvalidConfigException - * @since 5.4.0 - */ - public function getCustomerCondition(): ShippingRuleCustomerCondition - { - if ($this->_customerCondition instanceof ShippingRuleCustomerCondition) { - return $this->_customerCondition; - } - - $condition = $this->_customerCondition ?? []; - if (is_string($condition)) { - $condition = Json::decodeIfJson($condition); - } - - $condition['class'] = ShippingRuleCustomerCondition::class; - $condition = Craft::$app->getConditions()->createCondition($condition); - /** @var ShippingRuleCustomerCondition $condition */ - $condition->forProjectConfig = false; - $condition->mainTag = 'div'; - $condition->name = 'customerCondition'; - $this->_customerCondition = $condition; - - return $this->_customerCondition; - } - - /** - * @inheritdoc - */ - public function matchOrder(Order $order): bool - { - if (!$this->enabled) { - return false; - } - - $lineItems = $order->getLineItems(); - - $nonShippableItems = []; - foreach ($lineItems as $item) { - if ($item->getIsShippable()) { - continue; - } - - $nonShippableItems[$item->id] = $item->id; - } - - $wholeOrderNonShippable = count($nonShippableItems) > 0 && count($lineItems) == count($nonShippableItems); - - if ($wholeOrderNonShippable) { - return false; - } - - $shippingRuleCategories = $this->getShippingRuleCategories(); - $orderShippingCategories = $this->_getUniqueCategoryIdsInOrder($order); - [$disallowedCategories, $requiredCategories] = $this->_getRequiredAndDisallowedCategoriesFromRule($shippingRuleCategories); - - // Does the order have any disallowed categories in the cart? - $result = array_intersect($orderShippingCategories, $disallowedCategories); - if (!empty($result)) { - return false; - } - - // Does the order have all required categories in the cart? - $result = !array_diff($requiredCategories, $orderShippingCategories); - if (!$result) { - return false; - } - - // Order condition builder match - if (!$this->getOrderCondition()->matchElement($order)) { - return false; - } - - $customer = $order->getCustomer(); - // If there is no customer on the order and there are customer conditions, we can't match. - if (!$customer && !empty($this->getCustomerCondition()->getConditionRules())) { - return false; - } - - // Match the method's customer condition. - if ($customer && !$this->getCustomerCondition()->matchElement($customer)) { - return false; - } - - // Evaluate the Twig formula last — it's the most expensive check. - if ($this->orderConditionFormula) { - $orderAsArray = Plugin::getInstance()->getShippingMethods()->getSerializedOrderForMatchingRules($order); - $orderConditionParams = [ - 'order' => $orderAsArray, - ]; - if (!Plugin::getInstance()->getFormulas()->evaluateCondition($this->orderConditionFormula, $orderConditionParams, 'Evaluate Shipping Rule Order Condition Formula')) { - return false; - } - } - - // all rules match - return true; - } - - /** - * @return ShippingRuleCategory[] - * @throws InvalidConfigException - */ - public function getShippingRuleCategories(): array - { - if ($this->_shippingRuleCategories === null && $this->id) { - $this->_shippingRuleCategories = Plugin::getInstance()->getShippingRuleCategories()->getShippingRuleCategoriesByRuleId($this->id); - } - - return $this->_shippingRuleCategories ?? []; - } - - /** - * @param ShippingRuleCategory[] $models - */ - public function setShippingRuleCategories(array $models): void - { - $this->_shippingRuleCategories = $models; - } - - /** - * @inheritdoc - */ - public function getOptions(): array - { - return $this->getAttributes(); - } - - /** - * @inheritdoc - */ - public function getPercentageRate(?int $shippingCategoryId = null): float - { - return $this->_getRate('percentageRate', $shippingCategoryId); - } - - /** - * @inheritdoc - */ - public function getPerItemRate(?int $shippingCategoryId = null): float - { - return $this->_getRate('perItemRate', $shippingCategoryId); - } - - /** - * @inheritdoc - */ - public function getWeightRate(?int $shippingCategoryId = null): float - { - return $this->_getRate('weightRate', $shippingCategoryId); - } - - /** - * @inheritdoc - */ - public function getBaseRate(): float - { - return (float)$this->baseRate; - } - - /**@inheritdoc - */ - public function getMaxRate(): float - { - return (float)$this->maxRate; - } - - /** - * @inheritdoc - */ - public function getMinRate(): float - { - return (float)$this->minRate; - } - - /** - * @inheritdoc - */ - public function getDescription(): string - { - return $this->description ?? ''; - } - - /** - * @since 3.2.7 - */ - public function validateShippingRuleCategories(string $attribute): void - { - $ruleCategories = $this->$attribute; - - if (!empty($ruleCategories)) { - foreach ($ruleCategories as $key => $ruleCategory) { - if (!$ruleCategory->validate()) { - $this->addModelErrors($ruleCategory, $attribute . '.' . $key); - } - } - } - } - - /** - * @param $attribute - * @param int|null $shippingCategoryId - * @return mixed - * @throws InvalidConfigException - */ - private function _getRate($attribute, ?int $shippingCategoryId = null): mixed - { - if (!$shippingCategoryId) { - return $this->$attribute; - } - - foreach ($this->getShippingRuleCategories() as $ruleCategory) { - if ($shippingCategoryId === $ruleCategory->shippingCategoryId && $ruleCategory->$attribute !== null) { - return $ruleCategory->$attribute; - } - } - - return $this->$attribute; - } -} diff --git a/src/models/ShippingRuleCategory.php b/src/models/ShippingRuleCategory.php deleted file mode 100644 index cb0c756da9..0000000000 --- a/src/models/ShippingRuleCategory.php +++ /dev/null @@ -1,96 +0,0 @@ - - * @since 2.0 - */ -class ShippingRuleCategory extends Model -{ - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var int Shipping rule ID - */ - public int $shippingRuleId; - - /** - * @var int Shipping category ID - */ - public int $shippingCategoryId; - - /** - * @var float|null Per item rate - */ - public ?float $perItemRate = null; - - /** - * @var float|null Weight rate - */ - public ?float $weightRate = null; - - /** - * @var float|null Percentage rate - */ - public ?float $percentageRate = null; - - /** - * @var string Condition - */ - public string $condition; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['condition'], 'in', 'range' => ['allow', 'disallow', 'require']], - [['perItemRate', 'weightRate', 'percentageRate'], 'number', 'skipOnEmpty' => true], - [ - [ - 'shippingRuleId', - 'shippingCategoryId', - 'condition', - 'perItemRate', - 'weightRate', - 'percentageRate', - ], - 'safe', - ], - ]; - } - - /** - * @throws InvalidConfigException - */ - public function getRule(): ShippingRule - { - return Plugin::getInstance()->getShippingRules()->getShippingRuleById($this->shippingRuleId); - } - - /** - * @throws InvalidConfigException - */ - public function getCategory(): ShippingCategory - { - return Plugin::getInstance()->getShippingCategories()->getShippingCategoryById($this->shippingCategoryId); - } -} diff --git a/src/models/SiteStore.php b/src/models/SiteStore.php deleted file mode 100644 index 7773d445bf..0000000000 --- a/src/models/SiteStore.php +++ /dev/null @@ -1,78 +0,0 @@ - - * @since 5.0 - */ -class SiteStore extends Model implements HasStoreInterface -{ - use StoreTrait; - - /** - * @var int Site ID - */ - public int $siteId; - - /** - * @var string|null Store UID - */ - public ?string $uid = null; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['storeId', 'siteId'], 'required']; - $rules[] = [['storeId', 'siteId'], 'safe']; - - return $rules; - } - - /** - * @return Site|null - */ - public function getSite() - { - return Craft::$app->getSites()->getSiteById($this->siteId); - } - - /** - * @return string|null - */ - public function getStoreUid() - { - if ($this->storeId && $uid = Db::uidById('{{%commerce_stores}}', $this->storeId)) { - return $uid; - } - - return null; - } - - /** - * Returns the project config data for this store. - */ - public function getConfig(): array - { - return [ - 'store' => $this->getStoreUid(), - ]; - } -} diff --git a/src/models/Store.php b/src/models/Store.php deleted file mode 100644 index 468eca6112..0000000000 --- a/src/models/Store.php +++ /dev/null @@ -1,777 +0,0 @@ - - * @since 4.0 - * - * @property-read StoreSettings|null $settings - * @property-write string $name - * @property-read array $config - */ -class Store extends Model -{ - public const MINIMUM_TOTAL_PRICE_STRATEGY_DEFAULT = 'default'; - public const MINIMUM_TOTAL_PRICE_STRATEGY_ZERO = 'zero'; - public const MINIMUM_TOTAL_PRICE_STRATEGY_SHIPPING = 'shipping'; - - public const FREE_ORDER_PAYMENT_STRATEGY_COMPLETE = 'complete'; - public const FREE_ORDER_PAYMENT_STRATEGY_PROCESS = 'process'; - - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string|null - */ - private ?string $_name = null; - - /** - * @var string|null Handle - */ - public ?string $handle = null; - - /** - * @var bool Primary store? - */ - public bool $primary = false; - - /** - * @var int Sort order - */ - public int $sortOrder = 99; - - private ?string $_currency = 'USD'; - - /** - * @var bool - * @see setAutoSetNewCartAddresses() - * @see getAutoSetNewCartAddresses() - */ - private bool|string $_autoSetNewCartAddresses = false; - - /** - * @var bool - * @see setAutoSetCartShippingMethodOption() - * @see getAutoSetCartShippingMethodOption() - */ - private bool|string $_autoSetCartShippingMethodOption = false; - - /** - * @var bool - * @see setAutoSetPaymentSource() - * @see getAutoSetPaymentSource() - */ - private bool|string $_autoSetPaymentSource = false; - - /** - * @var bool - * @see setAllowEmptyCartOnCheckout() - * @see getAllowEmptyCartOnCheckout() - */ - private bool|string $_allowEmptyCartOnCheckout = false; - - /** - * @inheritdoc - */ - public function attributes(): array - { - $names = parent::attributes(); - $names[] = 'name'; - $names[] = 'settings'; - return $names; - } - - /** - * @var bool - * @see setAllowCheckoutWithoutPayment() - * @see getAllowCheckoutWithoutPayment() - */ - private bool|string $_allowCheckoutWithoutPayment = false; - - /** - * @var bool - * @see setAllowPartialPaymentOnCheckout() - * @see getAllowPartialPaymentOnCheckout() - */ - private bool|string $_allowPartialPaymentOnCheckout = false; - - /** - * @var bool - * @see setRequireShippingAddressAtCheckout() - * @see getRequireShippingAddressAtCheckout() - */ - private bool|string $_requireShippingAddressAtCheckout = false; - - /** - * @var bool - * @see setRequireBillingAddressAtCheckout() - * @see getRequireBillingAddressAtCheckout() - */ - private bool|string $_requireBillingAddressAtCheckout = false; - - /** - * @var bool - * @see setRequireShippingMethodSelectionAtCheckout() - * @see getRequireShippingMethodSelectionAtCheckout() - */ - private bool|string $_requireShippingMethodSelectionAtCheckout = false; - - /** - * @var bool - * @see setUseBillingAddressForTax() - * @see getUseBillingAddressForTax() - */ - private bool|string $_useBillingAddressForTax = false; - - /** - * @var bool - * @see setValidateOrganizationTaxIdAsVatId() - * @see getValidateOrganizationTaxIdAsVatId() - */ - private bool|string $_validateOrganizationTaxIdAsVatId = false; - - /** - * @var string - * @see setOrderReferenceFormat() - * @see getOrderReferenceFormat() - */ - private string $_orderReferenceFormat = '{{number[:7]}}'; - - /** - * @var string - * @see setFreeOrderPaymentStrategy() - * @see getFreeOrderPaymentStrategy() - */ - private string $_freeOrderPaymentStrategy = 'complete'; - - /** - * @var string - * @see setMinimumTotalPriceStrategy() - * @see getMinimumTotalPriceStrategy() - */ - private string $_minimumTotalPriceStrategy = 'default'; - - /** - * @var string|null Store UID - */ - public ?string $uid = null; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['handle'], UniqueValidator::class, 'targetClass' => StoreRecord::class, 'targetAttribute' => ['handle']]; - $rules[] = [['name', 'handle'], 'required']; - $rules[] = [ - ['currency'], - // Only allow changing of currency if the store has no orders - function($attribute) { - $isCurrencyChanging = \craft\commerce\records\Store::findOne(['id' => $this->id, 'currency' => $this->$attribute]) === null; - - if (!$isCurrencyChanging) { - return; - } - - $hasOrders = Order::find() - ->trashed(null) - ->storeId($this->id) - ->exists(); - - if ($hasOrders) { - $this->addError($attribute, Craft::t('commerce', 'The primary currency cannot be changed after orders are placed.')); - } - }, - 'when' => fn() => $this->id, - ]; - $rules[] = [[ - 'allowCheckoutWithoutPayment', - 'allowEmptyCartOnCheckout', - 'allowPartialPaymentOnCheckout', - 'autoSetCartShippingMethodOption', - 'autoSetNewCartAddresses', - 'autoSetPaymentSource', - 'freeOrderPaymentStrategy', - 'id', - 'orderReferenceFormat', - 'primary', - 'requireBillingAddressAtCheckout', - 'requireShippingAddressAtCheckout', - 'requireShippingMethodSelectionAtCheckout', - 'sortOrder', - 'uid', - 'useBillingAddressForTax', - 'validateOrganizationTaxIdAsVatId', - ], 'safe']; - - return $rules; - } - - /** - * Returns the store’s name. - * - * @param bool $parse Whether to parse the name for an environment variable - * @return string - */ - public function getName(bool $parse = true): string - { - return ($parse ? App::parseEnv($this->_name) : $this->_name) ?? ''; - } - - /** - * Sets the store’s name. - * - * @param string $name - */ - public function setName(string $name): void - { - $this->_name = $name; - } - - /** - * @inheritdoc - */ - protected function defineBehaviors(): array - { - return [ - 'parser' => [ - 'class' => EnvAttributeParserBehavior::class, - 'attributes' => [ - 'name' => fn() => $this->getName(false), - ], - ], - ]; - } - - /** - * Gets the CP url to these stores settings - * - * @param string|null $path - * @return string - */ - public function getStoreSettingsUrl(?string $path = null): string - { - $path = $path ? '/' . $path : ''; - return UrlHelper::cpUrl('commerce/store-management/' . $this->handle . $path); - } - - /** - * @return StoreSettings - */ - public function getSettings(): StoreSettings - { - return Plugin::getInstance()->getStoreSettings()->getStoreSettingsById($this->id); - } - - /** - * Returns the sites that are related to this store. - * - * @return Collection - * @throws InvalidConfigException - */ - public function getSites(): Collection - { - return Plugin::getInstance()->getStores()->getAllSitesForStore($this); - } - - /** - * Returns the names of the sites related to this store - * - * @return Collection - * @throws InvalidConfigException - */ - public function getSiteNames(): Collection - { - return collect($this->getSites())->map(fn(Site $site) => $site->getName()); - } - - /** - * @inheritdoc - */ - public function attributeLabels(): array - { - return [ - 'name' => Craft::t('commerce', 'Name'), - 'commerce' => Craft::t('commerce', 'Handle'), - 'primary' => Craft::t('commerce', 'Primary'), - ]; - } - - /** - * Returns the project config data for this store. - */ - public function getConfig(): array - { - return [ - 'allowCheckoutWithoutPayment' => $this->getAllowCheckoutWithoutPayment(false), - 'allowEmptyCartOnCheckout' => $this->getAllowEmptyCartOnCheckout(false), - 'allowPartialPaymentOnCheckout' => $this->getAllowPartialPaymentOnCheckout(false), - 'autoSetCartShippingMethodOption' => $this->getAutoSetCartShippingMethodOption(false), - 'autoSetNewCartAddresses' => $this->getAutoSetNewCartAddresses(false), - 'autoSetPaymentSource' => $this->getAutoSetPaymentSource(false), - 'freeOrderPaymentStrategy' => $this->getFreeOrderPaymentStrategy(false), - 'handle' => $this->handle, - 'minimumTotalPriceStrategy' => $this->getMinimumTotalPriceStrategy(false), - 'name' => $this->_name, - 'orderReferenceFormat' => $this->getOrderReferenceFormat(false), - 'primary' => $this->primary, - 'requireBillingAddressAtCheckout' => $this->getRequireBillingAddressAtCheckout(false), - 'requireShippingAddressAtCheckout' => $this->getRequireShippingAddressAtCheckout(false), - 'requireShippingMethodSelectionAtCheckout' => $this->getRequireShippingMethodSelectionAtCheckout(false), - 'sortOrder' => $this->sortOrder, - 'useBillingAddressForTax' => $this->getUseBillingAddressForTax(false), - 'validateOrganizationTaxIdAsVatId' => $this->getValidateOrganizationTaxIdAsVatId(false), - 'currency' => $this->getCurrency()->getCode(), - ]; - } - - /** - * Returns a key-value array of `freeOrderPaymentStrategy` options and labels. - */ - public function getFreeOrderPaymentStrategyOptions(): array - { - return [ - self::FREE_ORDER_PAYMENT_STRATEGY_COMPLETE => Craft::t('commerce', 'Free orders complete immediately'), - self::FREE_ORDER_PAYMENT_STRATEGY_PROCESS => Craft::t('commerce', 'Free orders are processed by the payment gateway'), - ]; - } - - /** - * Returns a key-value array of `minimumTotalPriceStrategy` options and labels. - */ - public function getMinimumTotalPriceStrategyOptions(): array - { - return [ - self::MINIMUM_TOTAL_PRICE_STRATEGY_DEFAULT => Craft::t('commerce', 'Default - Allow the price to be negative if discounts are greater than the order value.'), - self::MINIMUM_TOTAL_PRICE_STRATEGY_ZERO => Craft::t('commerce', 'Zero - Minimum price is zero if discounts are greater than the order value.'), - self::MINIMUM_TOTAL_PRICE_STRATEGY_SHIPPING => Craft::t('commerce', 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.'), - ]; - } - - /** - * @param bool|string $autoSetNewCartAddresses - * @return void - */ - public function setAutoSetNewCartAddresses(bool|string $autoSetNewCartAddresses): void - { - $this->_autoSetNewCartAddresses = $autoSetNewCartAddresses; - } - - /** - * Whether the user’s primary shipping and billing addresses should be set automatically on new carts. - * - * @param bool $parse - * @return bool|string - */ - public function getAutoSetNewCartAddresses(bool $parse = true): bool|string - { - return $parse ? (App::parseBooleanEnv($this->_autoSetNewCartAddresses) ?? false) : $this->_autoSetNewCartAddresses; - } - - /** - * @param bool|string $autoSetCartShippingMethodOption - * @return void - */ - public function setAutoSetCartShippingMethodOption(bool|string $autoSetCartShippingMethodOption): void - { - $this->_autoSetCartShippingMethodOption = $autoSetCartShippingMethodOption; - } - - /** - * Whether the first available shipping method option should be set automatically on carts. - * - * @param bool $parse - * @return bool|string - */ - public function getAutoSetCartShippingMethodOption(bool $parse = true): bool|string - { - return $parse ? (App::parseBooleanEnv($this->_autoSetCartShippingMethodOption) ?? false) : $this->_autoSetCartShippingMethodOption; - } - - /** - * @param bool|string $autoSetPaymentSource - * @return void - */ - public function setAutoSetPaymentSource(bool|string $autoSetPaymentSource): void - { - $this->_autoSetPaymentSource = $autoSetPaymentSource; - } - - /** - * Whether the user’s primary payment source should be set automatically on new carts. - * - * @param bool $parse - * @return bool|string - */ - public function getAutoSetPaymentSource(bool $parse = true): bool|string - { - return $parse ? (App::parseBooleanEnv($this->_autoSetPaymentSource) ?? false) : $this->_autoSetPaymentSource; - } - - /** - * @param bool|string $allowEmptyCartOnCheckout - * @return void - */ - public function setAllowEmptyCartOnCheckout(bool|string $allowEmptyCartOnCheckout): void - { - $this->_allowEmptyCartOnCheckout = $allowEmptyCartOnCheckout; - } - - /** - * Whether carts are allowed to be empty on checkout. - * - * @param bool $parse - * @return bool|string - */ - public function getAllowEmptyCartOnCheckout(bool $parse = true): bool|string - { - return $parse ? (App::parseBooleanEnv($this->_allowEmptyCartOnCheckout) ?? false) : $this->_allowEmptyCartOnCheckout; - } - - /** - * @param bool|string $allowCheckoutWithoutPayment - * @return void - */ - public function setAllowCheckoutWithoutPayment(bool|string $allowCheckoutWithoutPayment): void - { - $this->_allowCheckoutWithoutPayment = $allowCheckoutWithoutPayment; - } - - /** - * Whether carts are can be marked as completed without a payment. - * - * @param bool $parse - * @return bool|string - */ - public function getAllowCheckoutWithoutPayment(bool $parse = true): bool|string - { - return $parse ? (App::parseBooleanEnv($this->_allowCheckoutWithoutPayment) ?? false) : $this->_allowCheckoutWithoutPayment; - } - - /** - * @param bool|string $allowPartialPaymentOnCheckout - * @return void - */ - public function setAllowPartialPaymentOnCheckout(bool|string $allowPartialPaymentOnCheckout): void - { - $this->_allowPartialPaymentOnCheckout = $allowPartialPaymentOnCheckout; - } - - /** - * Whether [partial payment](https://craftcms.com/docs/commerce/5.x/system/development/making-payments.html#checkout-with-partial-payment) can be made from the front end when the gateway allows them. - * - * The `false` default does not allow partial payments on the front end. - * - * @param bool $parse - * @return bool|string - */ - public function getAllowPartialPaymentOnCheckout(bool $parse = true): bool|string - { - return $parse ? (App::parseBooleanEnv($this->_allowPartialPaymentOnCheckout) ?? false) : $this->_allowPartialPaymentOnCheckout; - } - - /** - * @param bool|string $requireShippingAddressAtCheckout - * @return void - */ - public function setRequireShippingAddressAtCheckout(bool|string $requireShippingAddressAtCheckout): void - { - $this->_requireShippingAddressAtCheckout = $requireShippingAddressAtCheckout; - } - - /** - * @param bool $parse - * @return bool|string - */ - public function getRequireShippingAddressAtCheckout(bool $parse = true): bool|string - { - return $parse ? (App::parseBooleanEnv($this->_requireShippingAddressAtCheckout) ?? false) : $this->_requireShippingAddressAtCheckout; - } - - /** - * @param bool|string $requireBillingAddressAtCheckout - * @return void - */ - public function setRequireBillingAddressAtCheckout(bool|string $requireBillingAddressAtCheckout): void - { - $this->_requireBillingAddressAtCheckout = $requireBillingAddressAtCheckout; - } - - /** - * Whether a billing address is required before making payment on an order. - * - * @param bool $parse - * @return bool|string - */ - public function getRequireBillingAddressAtCheckout(bool $parse = true): bool|string - { - return $parse ? (App::parseBooleanEnv($this->_requireBillingAddressAtCheckout) ?? false) : $this->_requireBillingAddressAtCheckout; - } - - /** - * @param bool|string $requireShippingMethodSelectionAtCheckout - * @return void - */ - public function setRequireShippingMethodSelectionAtCheckout(bool|string $requireShippingMethodSelectionAtCheckout): void - { - $this->_requireShippingMethodSelectionAtCheckout = $requireShippingMethodSelectionAtCheckout; - } - - /** - * Whether shipping method selection is required before making payment on an order. - * - * @param bool $parse - * @return bool|string - */ - public function getRequireShippingMethodSelectionAtCheckout(bool $parse = true): bool|string - { - return $parse ? (App::parseBooleanEnv($this->_requireShippingMethodSelectionAtCheckout) ?? false) : $this->_requireShippingMethodSelectionAtCheckout; - } - - /** - * @param bool|string $useBillingAddressForTax - * @return void - */ - public function setUseBillingAddressForTax(bool|string $useBillingAddressForTax): void - { - $this->_useBillingAddressForTax = $useBillingAddressForTax; - } - - /** - * Whether taxes should be calculated based on the billing address instead of the shipping address. - * - * @param bool $parse - * @return bool|string - */ - public function getUseBillingAddressForTax(bool $parse = true): bool|string - { - return $parse ? (App::parseBooleanEnv($this->_useBillingAddressForTax) ?? false) : $this->_useBillingAddressForTax; - } - - /** - * @param bool|string $validateOrganizationTaxIdAsVatId - * @return void - */ - public function setValidateOrganizationTaxIdAsVatId(bool|string $validateOrganizationTaxIdAsVatId): void - { - $this->_validateOrganizationTaxIdAsVatId = $validateOrganizationTaxIdAsVatId; - } - - /** - * @param bool $parse - * @return bool|string Whether to enable validation requiring the `organizationTaxId` to be a valid VAT ID. - * - * When set to `false`, no validation is applied to `organizationTaxId`. - * - * When set to `true`, `organizationTaxId` must contain a valid VAT ID. - * - * ::: tip - * This setting strictly toggles input validation and has no impact on tax configuration or behavior elsewhere in the system. - * ::: - */ - public function getValidateOrganizationTaxIdAsVatId(bool $parse = true): bool|string - { - return $parse ? (App::parseBooleanEnv($this->_validateOrganizationTaxIdAsVatId) ?? false) : $this->_validateOrganizationTaxIdAsVatId; - } - - /** - * @param string|null $orderReferenceFormat - * @return void - */ - public function setOrderReferenceFormat(?string $orderReferenceFormat): void - { - if (!$orderReferenceFormat) { - return; - } - - $this->_orderReferenceFormat = $orderReferenceFormat; - } - - /** - * Human-friendly reference number format for orders. Result must be unique. - * - * See [Order Numbers](https://craftcms.com/docs/commerce/5.x/system/orders-carts.html#order-numbers). - * - * @param bool $parse - * @return string - */ - public function getOrderReferenceFormat(bool $parse = true): string - { - return $parse ? App::parseEnv($this->_orderReferenceFormat) : $this->_orderReferenceFormat; - } - - /** - * @param string $freeOrderPaymentStrategy - * @return void - */ - public function setFreeOrderPaymentStrategy(string $freeOrderPaymentStrategy): void - { - $this->_freeOrderPaymentStrategy = $freeOrderPaymentStrategy; - } - - /** - * How Commerce should handle free orders. - * - * The default `'complete'` setting automatically completes zero-balance orders without forwarding them to the payment gateway. - * - * The `'process'` setting forwards zero-balance orders to the payment gateway for processing. This can be useful if the customer’s balance - * needs to be updated or otherwise adjusted by the payment gateway. - * - * @param bool $parse - * @return string - */ - public function getFreeOrderPaymentStrategy(bool $parse = true): string - { - return $parse ? App::parseEnv($this->_freeOrderPaymentStrategy) : $this->_freeOrderPaymentStrategy; - } - - /** - * @param string $minimumTotalPriceStrategy - * @return void - */ - public function setMinimumTotalPriceStrategy(string $minimumTotalPriceStrategy): void - { - $this->_minimumTotalPriceStrategy = $minimumTotalPriceStrategy; - } - - /** - * How Commerce should handle minimum total price for an order. - * - * Options: - * - * - `'default'` [rounds](commerce4:\craft\commerce\helpers\Currency::round()) the sum of the item subtotal and adjustments. - * - `'zero'` returns `0` if the result from `'default'` would’ve been negative; minimum order total is `0`. - * - `'shipping'` returns the total shipping cost if the `'default'` result would’ve been negative; minimum order total equals shipping amount. - * - * @param bool $parse - * @return string - */ - public function getMinimumTotalPriceStrategy(bool $parse = true): string - { - return $parse ? App::parseEnv($this->_minimumTotalPriceStrategy) : $this->_minimumTotalPriceStrategy; - } - - /** - * @return void - * @throws DeprecationException - * @throws InvalidConfigException - * @deprecated in 5.0.0. Use [[Store::getSettings()->setCountries()]] instead. - */ - public function setCountries(mixed $countries): void - { - Craft::$app->getDeprecator()->log(__METHOD__, 'Store::setCountries() is deprecated. Use Store::getSettings()->setCountries() instead.'); - $this->getSettings()->setCountries($countries); - } - - /** - * @return string[] $countries - * @deprecated in 5.0.0. Use [[Store::getSettings()->getCountries()]] instead. - */ - public function getCountries(): array - { - Craft::$app->getDeprecator()->log(__METHOD__, 'Store::getCountries() is deprecated. Use Store::getSettings()->getCountries() instead.'); - return $this->getSettings()->getCountries(); - } - - /** - * @return array - * @throws DeprecationException - * @deprecated in 5.0.0. Use [[Store::getSettings()->getCountriesList()]] instead. - */ - public function getCountriesList(): array - { - Craft::$app->getDeprecator()->log(__METHOD__, 'Store::getCountriesList() has been deprecated. Use Store::getSettings()->getCountriesList() instead.'); - return $this->getSettings()->getCountriesList(); - } - - /** - * @return array - * @throws DeprecationException - * @deprecated in 5.0.0. Use [[Store::getSettings()->getAdministrativeAreasListByCountryCode()]] instead. - */ - public function getAdministrativeAreasListByCountryCode(): array - { - Craft::$app->getDeprecator()->log(__METHOD__, 'Store::getAdministrativeAreasListByCountryCode() has been deprecated. Use Store::getSettings()->getAdministrativeAreasListByCountryCode() instead.'); - return $this->getSettings()->getAdministrativeAreasListByCountryCode(); - } - - /** - * @return ZoneAddressCondition - * @deprecated in 5.0.0. Use [[Store::getSettings()->getMarketAddressCondition()]] instead. - */ - public function getMarketAddressCondition(): ZoneAddressCondition - { - Craft::$app->getDeprecator()->log(__METHOD__, 'Store::getMarketAddressCondition() has been deprecated. Use Store::getSettings()->getMarketAddressCondition() instead.'); - return $this->getSettings()->getMarketAddressCondition(); - } - - /** - * @return MoneyCurrency|null - */ - public function getCurrency(): ?MoneyCurrency - { - return $this->_currency ? (new MoneyCurrency($this->_currency)) : null; - } - - /** - * @param string|MoneyCurrency $currency - * @return void - */ - public function setCurrency(string|MoneyCurrency $currency): void - { - if ($currency instanceof MoneyCurrency) { - $currency = $currency->getCode(); - } - - $this->_currency = $currency; - } - - /** - * Returns the inventory locations related to this store. - * - * @return Collection - * @throws InvalidConfigException - * @throws \craft\errors\DeprecationException - */ - public function getInventoryLocations(): Collection - { - return Plugin::getInstance()->getInventoryLocations()->getInventoryLocations($this->id); - } - - /** - * @return array - * @throws InvalidConfigException - */ - public function getInventoryLocationsOptions(): array - { - return Plugin::getInstance()->getInventoryLocations()->getInventoryLocations($this->id)->map(fn($location) => ['value' => $location->id, 'label' => $location->getUiLabel()])->toArray(); - } -} diff --git a/src/models/StoreSettings.php b/src/models/StoreSettings.php deleted file mode 100644 index 40dc31ffb2..0000000000 --- a/src/models/StoreSettings.php +++ /dev/null @@ -1,252 +0,0 @@ - - * @since 5.0 - */ -class StoreSettings extends Model -{ - /** - * @var int - */ - public int $id; - - /** - * @var int|null - */ - private ?int $_locationAddressId = null; - - /** - * @var Address|null - */ - private ?Address $_locationAddress = null; - - /** - * @var array - */ - private array $_countries = []; - /** - * @var ?ZoneAddressCondition - */ - private ?ZoneAddressCondition $_marketAddressCondition = null; - - /** - * @inheritdoc - */ - public function attributes(): array - { - $names = parent::attributes(); - $names[] = 'locationAddressId'; - $names[] = 'countries'; - $names[] = 'marketAddressCondition'; - return $names; - } - - /** - * @inheritdoc - */ - public function safeAttributes(): array - { - return [ - 'id', - 'locationAddressId', - 'countries', - 'marketAddressCondition', - ]; - } - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $names = parent::extraFields(); - $names[] = 'locationAddress'; - - return $names; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - return $rules; - } - - /** - * Sets the store location address ID. - * - * @param null|int|int[] $locationAddressId - */ - public function setLocationAddressId(array|int|null $locationAddressId): void - { - if ($locationAddressId === null) { - $this->_locationAddressId = $this->getLocationAddress()->id; - } - - if (is_array($locationAddressId)) { - $this->_locationAddressId = ArrayHelper::firstValue($locationAddressId) ?: null; - } else { - $this->_locationAddressId = $locationAddressId; - } - } - - /** - * Returns the store location address ID. - * - * @return int|null - */ - public function getLocationAddressId(): ?int - { - return $this->_locationAddressId; - } - - /** - * @return ?Address - */ - public function getLocationAddress(): ?Address - { - if (!isset($this->_locationAddress)) { - if ($this->_locationAddressId && $location = Address::findOne($this->_locationAddressId)) { - $this->_locationAddress = $location; - } else { - $storeLocationAddress = new Address(); - $storeLocationAddress->title = 'Store'; - $storeLocationAddress->countryCode = 'US'; - if (Craft::$app->getElements()->saveElement($storeLocationAddress, false)) { - $this->setLocationAddress($storeLocationAddress); - StoreSettingsRecord::updateAll(['locationAddressId' => $this->_locationAddressId], ['id' => $this->id]); - } else { - throw new \Exception('Could not save store location address'); - } - } - - return $this->_locationAddress; - } - - return $this->_locationAddress; - } - - /** - * Sets the store's location address. - * - * @param Address|null $locationAddress - */ - public function setLocationAddress(?Address $locationAddress = null): void - { - $this->_locationAddress = $locationAddress; - $this->setLocationAddressId($locationAddress?->id); - } - - /** - * @return string[] $countries - */ - public function getCountries(): array - { - return $this->_countries ?? []; - } - - /** - * @return void - * @throws InvalidConfigException - */ - public function setCountries(mixed $countries): void - { - $countries ??= []; - $countries = Json::decodeIfJson($countries); - - if (!is_array($countries)) { - throw new InvalidConfigException('Countries must be an array.'); - } - - $this->_countries = $countries; - } - - /** - * @return array - */ - public function getCountriesList(): array - { - $all = Craft::$app->getAddresses()->getCountryRepository()->getList(Craft::$app->language); - return array_filter($all, fn($fieldHandle) => in_array($fieldHandle, $this->getCountries(), true), ARRAY_FILTER_USE_KEY); - } - - /** - * @return array - */ - public function getAdministrativeAreasListByCountryCode(): array - { - if (empty($this->_countries)) { - return []; - } - - $administrativeAreas = []; - foreach ($this->_countries as $countryCode) { - $administrativeAreas[$countryCode] = Craft::$app->getAddresses()->getSubdivisionRepository()->getList([$countryCode]); - } - - return $administrativeAreas; - } - - /** - * @return ZoneAddressCondition - */ - public function getMarketAddressCondition(): ZoneAddressCondition - { - /** @var ZoneAddressCondition $condition */ - $condition = $this->_marketAddressCondition ?? Craft::$app->getConditions()->createCondition(ZoneAddressCondition::class); - return $condition; - } - - /** - * @param ZoneAddressCondition|string|array|null $condition - * @return void - */ - public function setMarketAddressCondition(ZoneAddressCondition|string|array|null $condition): void - { - if (is_string($condition)) { - $condition = Json::decodeIfJson($condition); - $condition = Craft::$app->getConditions()->createCondition($condition); - } - - if (is_array($condition)) { - $condition = Craft::$app->getConditions()->createCondition($condition); - } - - if ($condition === null) { - $condition = Craft::$app->getConditions()->createCondition(ZoneAddressCondition::class); - } - - $condition->forProjectConfig = false; - - /** @var ZoneAddressCondition $condition */ - $this->_marketAddressCondition = $condition; - } -} diff --git a/src/models/TaxAddressZone.php b/src/models/TaxAddressZone.php deleted file mode 100644 index e1d9d252e2..0000000000 --- a/src/models/TaxAddressZone.php +++ /dev/null @@ -1,83 +0,0 @@ - - * @since 2.0 - * - * @property-read string $cpEditUrl - */ -class TaxAddressZone extends Zone implements Chippable -{ - /** - * @var bool Default - */ - public bool $default = false; - - /** - * @inheritdoc - */ - public static function get(int|string $id): ?static - { - foreach (Plugin::getInstance()->getStores()->getAllStores() as $store) { - $zone = Plugin::getInstance()->getTaxZones()->getTaxZoneById((int)$id, $store->id); - if ($zone !== null) { - /** @phpstan-ignore-next-line */ - return $zone; - } - } - return null; - } - - /** - * @return string - * @throws InvalidConfigException - */ - public function getCpEditUrl(): string - { - return $this->getStore()->getStoreSettingsUrl('taxzones/' . $this->id); - } - - /** - * @inheritdoc - */ - public function getUiLabel(): string - { - return \Craft::t('site', $this->name); - } - - /** - * @inheritdoc - */ - public function getId(): ?int - { - return $this->id; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['name'], UniqueValidator::class, 'targetClass' => TaxZone::class, 'targetAttribute' => ['name', 'storeId']]; - $rules[] = [['default'], 'safe']; - - return $rules; - } -} diff --git a/src/models/TaxCategory.php b/src/models/TaxCategory.php deleted file mode 100644 index 7eea393682..0000000000 --- a/src/models/TaxCategory.php +++ /dev/null @@ -1,244 +0,0 @@ - - * @since 2.0 - */ -class TaxCategory extends Model implements Chippable, Colorable, Iconic -{ - /** - * @var int|null ID; - */ - public ?int $id = null; - - /** - * @var string|null Name - */ - public ?string $name = null; - - /** - * @var string|null Handle - */ - public ?string $handle = null; - - /** - * @var string|null Icon - */ - public ?string $icon = null; - - /** - * @var string|null Color - */ - public ?string $color = null; - - /** - * @var string|null Description - */ - public ?string $description = null; - - /** - * @var bool Default - */ - public bool $default = false; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateCreated = null; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateUpdated = null; - - /** - * @var DateTime|null Date deleted - * @since 4.2.0.1 - */ - public ?DateTime $dateDeleted = null; - - /** - * @var array|null Product Types - */ - private ?array $_productTypes = null; - - - /** - * Returns the name of this tax category. - * - * @return string - */ - public function __toString() - { - return (string)$this->name; - } - - /** - * @inheritdoc - */ - public static function get(int|string $id): ?static - { - /** @phpstan-ignore-next-line */ - return Plugin::getInstance()->getTaxCategories()->getTaxCategoryById($id); - } - - /** - * @inheritdoc - */ - public function getUiLabel(): string - { - return Craft::t('site', $this->name); - } - - /** - * @inheritdoc - */ - public function getId(): ?int - { - return $this->id; - } - - /** - * @inheritdoc - */ - public function getIcon(): ?string - { - return $this->icon; - } - - /** - * @inheritdoc - */ - public function getColor(): ?Color - { - return $this->color ? Color::tryFrom($this->color) : null; - } - - /** - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - * @throws StoreNotFoundException - */ - public function getTaxRates(?int $storeId = null): Collection - { - return Plugin::getInstance()->getTaxRates()->getAllTaxRates($storeId)->where('taxCategoryId', $this->id); - } - - /** - * @param int|null $storeId - * @return string - * @throws InvalidConfigException - */ - public function getCpEditUrl(?int $storeId = null): string - { - if ($storeId === null || !$store = Plugin::getInstance()->getStores()->getStoreById($storeId)) { - $store = Plugin::getInstance()->getStores()->getPrimaryStore(); - } - - return $store->getStoreSettingsUrl('taxcategories/' . $this->id); - } - - /** - * @param ProductType[] $productTypes - */ - public function setProductTypes(array $productTypes): void - { - $this->_productTypes = $productTypes; - } - - /** - * @return ProductType[] - * @throws InvalidConfigException - */ - public function getProductTypes(): array - { - if ($this->_productTypes === null && $this->id) { - $this->_productTypes = Plugin::getInstance()->getProductTypes()->getProductTypesByTaxCategoryId($this->id); - } - - return $this->_productTypes ?? []; - } - - /** - * Helper method to just get the product type IDs - * - * @return int[] - * @throws InvalidConfigException - */ - public function getProductTypeIds(): array - { - return ArrayHelper::getColumn($this->getProductTypes(), 'id'); - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $engine = Plugin::getInstance()->getTaxes()->getEngine(); - $isStandardTaxEngine = $engine instanceof Tax; - return [ - [['handle'], 'required'], - [['handle'], UniqueValidator::class, 'targetClass' => TaxCategoryRecord::class], - [['handle'], HandleValidator::class, 'when' => fn($model) => $isStandardTaxEngine], - [[ - 'id', - 'name', - 'handle', - 'icon', - 'color', - 'description', - 'default', - 'dateCreated', - 'dateUpdated', - 'dateDeleted', - ], 'safe'], - ]; - } - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $fields = parent::extraFields(); - $fields[] = 'productTypes'; - $fields[] = 'productTypeIds'; - $fields[] = 'taxRates'; - - return $fields; - } -} diff --git a/src/models/TaxRate.php b/src/models/TaxRate.php deleted file mode 100644 index c2266dc0fe..0000000000 --- a/src/models/TaxRate.php +++ /dev/null @@ -1,327 +0,0 @@ - - * @since 2.0 - */ -class TaxRate extends Model implements HasStoreInterface, Chippable -{ - use StoreTrait; - - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var string|null Human-friendly name for the tax rate - */ - public ?string $name = null; - - /** - * @var string|null Optional code used for internal reference - * @since 2.2 - */ - public ?string $code = null; - - /** - * @var float Rate percentage applied to the taxable subject - */ - public float $rate = .00; - - /** - * @var bool Whether the tax amount should be included in the subject price - */ - public bool $include = false; - - /** - * @var bool Whether the included tax amount should be removed from disqualified subject prices - * @since 3.4 - */ - public bool $removeIncluded = false; - - /** - * @var bool Whether an included VAT ID tax amount should be removed from VAT-disqualified subject prices - * @since 3.4 - */ - public bool $removeVatIncluded = false; - - /** - * @var string The subject to which `$rate` should be applied. Options: - * - `price` – line item price - * - `shipping` – line item shipping cost - * - `price_shipping` – line item price and shipping cost - * - `order_total_shipping` – order total shipping cost - * - `order_total_price` – order total taxable price (line item subtotal + total discounts + - * total shipping) - */ - public string $taxable = 'price'; - - /** - * @var int|null Tax category ID - */ - public ?int $taxCategoryId = null; - - /** - * @var int|null Tax zone ID - */ - public ?int $taxZoneId = null; - - /** - * @var array Tax ID Validators - */ - public array $taxIdValidators = []; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateCreated = null; - - /** - * @var DateTime|null - * @since 3.4 - */ - public ?DateTime $dateUpdated = null; - - /** - * @var bool Whether the tax rate is enabled - */ - public bool $enabled = true; - - /** - * @var TaxCategory|null - */ - private ?TaxCategory $_taxCategory = null; - - /** - * @var TaxAddressZone|null - */ - private ?TaxAddressZone $_taxZone = null; - - /** - * @inheritdoc - */ - public function attributes(): array - { - $names = parent::attributes(); - $names[] = 'isVat'; // @TODO Remove the deprecated `isVat` attribute in Commerce 6.0 - return $names; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['name'], 'required']; - $rules[] = [ - ['taxCategoryId'], - 'required', - 'when' => fn($model): bool => !in_array($model->taxable, TaxRateRecord::ORDER_TAXABALES, true), - ]; - $rules[] = [[ - 'code', - 'id', - 'include', - 'isVat', - 'name', - 'rate', - 'taxIdValidators', - 'removeIncluded', - 'removeVatIncluded', - 'storeId', - 'taxable', - 'taxCategoryId', - 'taxZoneId', - 'enabled', - ], 'safe']; - - return $rules; - } - - /** - * @inheritdoc - */ - public function extraFields(): array - { - $fields = parent::extraFields(); - $fields[] = 'taxCategory'; - $fields[] = 'taxZone'; - $fields[] = 'rateAsPercent'; - $fields[] = 'isEverywhere'; - - return $fields; - } - - /** - * @inheritdoc - */ - public static function get(int|string $id): ?static - { - /** @phpstan-ignore-next-line */ - return Plugin::getInstance()->getTaxRates()->getTaxRateById($id); - } - - /** - * @inheritdoc - */ - public function getUiLabel(): string - { - return Craft::t('site', $this->name); - } - - /** - * @inheritdoc - */ - public function getId(): ?int - { - return $this->id; - } - - /** - * Returns the tax rate's control panel edit page URL. - * - * @return string - * @throws InvalidConfigException - */ - public function getCpEditUrl(): string - { - return $this->getStore()->getStoreSettingsUrl('taxrates/' . $this->id); - } - - /** - * Returns `$rate` formatted as a percentage. - * - * @return string - */ - public function getRateAsPercent(): string - { - return Craft::$app->getFormatter()->asPercent($this->rate); - } - - /** - * Returns the designated Tax Zone for the rate, or `null` if none has been designated. - * - * @return TaxAddressZone|null - * @throws InvalidConfigException - */ - public function getTaxZone(): ?TaxAddressZone - { - if ($this->_taxZone === null && $this->taxZoneId) { - $this->_taxZone = Plugin::getInstance()->getTaxZones()->getTaxZoneById($this->taxZoneId, $this->storeId); - } - - return $this->_taxZone; - } - - /** - * Returns the designated Tax Category for the rate, or `null` if none has been designated. - * - * @return TaxCategory|null - * @throws InvalidConfigException - */ - public function getTaxCategory(): ?TaxCategory - { - if (!isset($this->_taxCategory) && $this->taxCategoryId) { - $this->_taxCategory = Plugin::getInstance()->getTaxCategories()->getTaxCategoryById($this->taxCategoryId); - } - - return $this->_taxCategory; - } - - /** - * Returns `true` is this tax rate isn’t limited by zone. - * - * @return bool Whether this tax rate applies to any zone - * @throws InvalidConfigException - */ - public function getIsEverywhere(): bool - { - return !$this->getTaxZone(); - } - - /** - * @return bool - * @deprecated in 5.3.0 - */ - public function getIsVat(): bool - { - // Don't throw deprecation log as `isVat` is still set as an attribute so will be called when the model is serialized. - return $this->hasTaxIdValidators(); - } - - /** - * @param bool $isVat - * @throws DeprecationException - * @deprecated in 5.3.0 - */ - public function setIsVat(bool $isVat): void - { - Craft::$app->getDeprecator()->log(__METHOD__, 'TaxRate::setIsVat() is deprecated.'); - } - - /** - * @return bool - * @since 5.3.0 - */ - public function hasTaxIdValidators(): bool - { - return count($this->taxIdValidators) > 0; - } - - /** - * @param string $className - * @return bool - * @since 5.3.0 - */ - public function hasTaxIdValidator(string $className): bool - { - return in_array($className, $this->taxIdValidators, true); - } - - /** - * @return TaxIdValidatorInterface[] - * @throws InvalidConfigException - * @since 5.3.0 - */ - public function getSelectedEnabledTaxIdValidators(): array - { - $selectedValidators = $this->taxIdValidators; - $validators = Plugin::getInstance()->getTaxes()->getEnabledTaxIdValidators(); - $activeValidators = []; - foreach ($validators as $validator) { - if (in_array($validator::class, $selectedValidators)) { - $activeValidators[] = $validator; - } - } - return $activeValidators; - } -} diff --git a/src/models/Transaction.php b/src/models/Transaction.php deleted file mode 100644 index 47915508d4..0000000000 --- a/src/models/Transaction.php +++ /dev/null @@ -1,355 +0,0 @@ - - * @since 2.0 - */ -class Transaction extends Model -{ - /** - * @var int|null ID - */ - public ?int $id = null; - - /** - * @var int|null Order ID - */ - public ?int $orderId = null; - - /** - * @var int|null Parent transaction ID - */ - public ?int $parentId = null; - - /** - * This is the user who made the transaction. It could be the customer if logged in, or a store administrator. - * - * @var int|null User ID - */ - public ?int $userId = null; - - /** - * @var string|null Hash - */ - public ?string $hash = null; - - /** - * @var int|null Gateway ID - */ - public ?int $gatewayId = null; - - /** - * @var string|null Currency - */ - public ?string $currency = null; - - /** - * The payment amount in the payment currency. - * Multiplying this by the `paymentRate`, give you the `amount`. - * - * @var float Payment Amount - */ - public float $paymentAmount; - - /** - * @var string|null Payment currency - */ - public ?string $paymentCurrency = null; - - /** - * @var float Payment Rate - */ - public float $paymentRate; - - /** - * @var string|null Transaction Type - */ - public ?string $type = null; - - /** - * The amount in the currency (which is the currency of the order) - * - * @var float Amount - */ - public float $amount; - - /** - * @var string|null Status - */ - public ?string $status = null; - - /** - * @var string|null reference - */ - public ?string $reference = null; - - /** - * @var string|null Code - */ - public ?string $code = null; - - /** - * @var string|null Message - */ - public ?string $message = null; - - /** - * @var string Note - */ - public string $note = ''; - - /** - * @var mixed Response - */ - public mixed $response = null; - - /** - * @var DateTime|null The date that the transaction was created - */ - public ?DateTIme $dateCreated = null; - - /** - * @var DateTime|null The date that the transaction was last updated - */ - public ?DateTIme $dateUpdated = null; - - /** - * @var Gateway|null - */ - private ?Gateway $_gateway = null; - - /** - * @var Transaction|null - */ - private ?Transaction $_parentTransaction = null; - - /** - * @var Order|null - */ - private ?Order $_order = null; - - /** - * @var Transaction[]|null - */ - private ?array $_children = null; - - - /** - * @inheritdoc - */ - public function __construct($attributes = []) - { - // generate unique hash - $this->hash = md5(uniqid((string)mt_rand(), true)); - - parent::__construct($attributes); - } - - /** - * @inheritdoc - */ - public function init(): void - { - $primaryCurrency = Plugin::getInstance()->getPaymentCurrencies()->getPrimaryPaymentCurrencyIso(); - - if (!isset($this->currency)) { - $this->currency = $primaryCurrency; - } - - if (!isset($this->paymentCurrency)) { - $this->paymentCurrency = $primaryCurrency; - } - - parent::init(); - } - - public function behaviors(): array - { - $behaviors = parent::behaviors(); - - $behaviors['currencyAttributes'] = [ - 'class' => CurrencyAttributeBehavior::class, - 'defaultCurrency' => $this->currency, - 'currencyAttributes' => $this->currencyAttributes(), - 'attributeCurrencyMap' => [ - 'paymentAmount' => $this->paymentCurrency, - ], - ]; - - return $behaviors; - } - - /** - * @return array - */ - public function currencyAttributes(): array - { - return [ - 'amount', - 'paymentAmount', - 'refundableAmount', - ]; - } - - /** - * @inheritdoc - */ - public function attributes(): array - { - $names = parent::attributes(); - ArrayHelper::removeValue($names, 'response'); - return $names; - } - - /** - * @inheritDoc - */ - public function extraFields(): array - { - return [ - 'response', - ]; - } - - /** - * @throws InvalidConfigException - */ - public function canCapture(): bool - { - return Plugin::getInstance()->getTransactions()->canCaptureTransaction($this); - } - - /** - * @throws InvalidConfigException - */ - public function canRefund(): bool - { - return Plugin::getInstance()->getTransactions()->canRefundTransaction($this); - } - - /** - * @throws InvalidConfigException - */ - public function getRefundableAmount(): float - { - return Plugin::getInstance()->getTransactions()->refundableAmountForTransaction($this); - } - - /** - * @throws InvalidConfigException - */ - public function getParent(): ?Transaction - { - if (null === $this->_parentTransaction && $this->parentId) { - $this->_parentTransaction = Plugin::getInstance()->getTransactions()->getTransactionById($this->parentId); - } - - return $this->_parentTransaction; - } - - /** - * @throws InvalidConfigException - */ - public function getOrder(): ?Order - { - if (!isset($this->_order) && $this->orderId) { - $this->_order = Plugin::getInstance()->getOrders()->getOrderById($this->orderId); - } - - return $this->_order; - } - - public function setOrder(Order $order): void - { - $this->_order = $order; - $this->orderId = $order->id; - } - - /** - * @throws InvalidConfigException - */ - public function getGateway(): ?Gateway - { - if (!isset($this->_gateway) && $this->gatewayId) { - $this->_gateway = Plugin::getInstance()->getGateways()->getGatewayById($this->gatewayId); - } - - return $this->_gateway; - } - - public function setGateway(Gateway $gateway): void - { - $this->_gateway = $gateway; - } - - /** - * Returns child transactions. - * - * @return Transaction[] - * @throws InvalidConfigException - */ - public function getChildTransactions(): array - { - if (!isset($this->_children) && $this->id) { - $this->_children = Plugin::getInstance()->getTransactions()->getChildrenByTransactionId($this->id); - } - - return $this->_children ?? []; - } - - /** - * Adds a child transaction. - */ - public function addChildTransaction(Transaction $transaction): void - { - if (null === $this->_children) { - $this->_children = []; - } - - $this->_children[] = $transaction; - } - - /** - * Sets child transactions. - */ - public function setChildTransactions(array $transactions): void - { - $this->_children = $transactions; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['type', 'status', 'orderId'], 'required'], - ]; - } -} diff --git a/src/models/TransferDetail.php b/src/models/TransferDetail.php deleted file mode 100644 index 031ba7099d..0000000000 --- a/src/models/TransferDetail.php +++ /dev/null @@ -1,87 +0,0 @@ -transferId) { - $this->_transfer = Transfer::findOne($this->transferId); - } - - if ($this->inventoryItemId) { - $inventoryItem = Plugin::getInstance()->getInventory()->getInventoryItemById($this->inventoryItemId); - $this->inventoryItemDescription = $inventoryItem->getSku(); - } - } - - public function getReceived(): int - { - return $this->quantityAccepted + $this->quantityRejected; - } - - /** - * @return ?InventoryItem - */ - public function getInventoryItem(): ?InventoryItem - { - if ($this->inventoryItemId === null) { - return null; - } - - return Plugin::getInstance()->getInventory()->getInventoryItemById($this->inventoryItemId); - } - - /** - * @return Transfer - */ - public function getTransfer(): Transfer - { - return $this->_transfer; - } - - /** - * @return void - */ - public function setTransfer(Transfer $transfer): void - { - $this->transferId = $transfer->id; - $this->_transfer = $transfer; - } - - public function defineRules(): array - { - return [ - [['quantity'], 'number', 'integerOnly' => true, 'min' => 1, 'max' => 99999, 'when' => fn() => $this->getTransfer()->transferStatus === TransferStatusType::DRAFT], - ]; - } -} diff --git a/src/models/inventory/DeactivateInventoryLocation.php b/src/models/inventory/DeactivateInventoryLocation.php deleted file mode 100644 index 1c0b7cf8d7..0000000000 --- a/src/models/inventory/DeactivateInventoryLocation.php +++ /dev/null @@ -1,102 +0,0 @@ -select(['id']) - ->from([Table::INVENTORYLOCATIONS]) - ->where(['id' => $this->inventoryLocation->id, 'dateDeleted' => null]) - ->exists(); - - if (!$exists) { - $this->addError($attribute, \Craft::t('commerce','Inventory location is already deactivated.')); - } - }, - ]; - - $rules[] = [ - ['inventoryLocation'], - function($attribute, $params, $validator) { - // Look through all the stores and see if they only have 1 location and it's the one we are deactivating - $stores = Plugin::getInstance()->getStores()->getAllStores(); - foreach ($stores as $store) { - $locations = $store->getInventoryLocations(); - if ($locations->count() == 1 && $locations->contains('id', $this->inventoryLocation->id)) { - $this->addError($attribute, \Craft::t('commerce','This is the last location for the {store} store.', ['store' => $store->getName()])); - } - } - }, - ]; - - $rules[] = [ - ['inventoryLocation'], - function($attribute, $params, $validator) { - if ($this->hasOutStandingCommittedStock()) { - $this->addError($attribute, \Craft::t('commerce','Inventory location has committed stock, the order(s) must first be fulfilled.')); - } - }, - ]; - - $rules[] = [ - ['inventoryLocation'], - function($attribute, $params, $validator) { - if ($this->hasOutStandingIncomingStock()) { - $this->addError($attribute, \Craft::t('commerce','Inventory location has incoming stock, the transfer(s) must first be completed.')); - } - }, - ]; - - return $rules; - } - - public function hasOutStandingCommittedStock(): bool - { - $committedTotal = Plugin::getInstance()->getInventory()->getInventoryLocationLevels($this->inventoryLocation) - ->sum('committedTotal'); - - return $committedTotal > 0; - } - - public function hasOutStandingIncomingStock(): bool - { - $incomingTotal = Plugin::getInstance()->getInventory()->getInventoryLocationLevels($this->inventoryLocation) - ->sum('incomingTotal'); - - return $incomingTotal > 0; - } -} diff --git a/src/models/inventory/InventoryCommittedMovement.php b/src/models/inventory/InventoryCommittedMovement.php deleted file mode 100644 index 16e76b3989..0000000000 --- a/src/models/inventory/InventoryCommittedMovement.php +++ /dev/null @@ -1,42 +0,0 @@ -fromInventoryTransactionType !== InventoryTransactionType::AVAILABLE && $this->toInventoryTransactionType !== InventoryTransactionType::COMMITTED) { - $validator->addError($this, $attribute, 'Invalid committed transaction types'); - } - }, - ]; - - $rules[] = [ - ['fromInventoryLocation', 'toInventoryLocation'], - function($attribute, $params, $validator) { - if ($this->fromInventoryLocation->id !== $this->toInventoryLocation->id) { - $validator->addError($this, $attribute, 'The from and to inventory locations must be the same.'); - } - }, - ]; - - return $rules; - } -} diff --git a/src/models/inventory/InventoryFulfillMovement.php b/src/models/inventory/InventoryFulfillMovement.php deleted file mode 100644 index db79fc5272..0000000000 --- a/src/models/inventory/InventoryFulfillMovement.php +++ /dev/null @@ -1,42 +0,0 @@ -fromInventoryTransactionType !== InventoryTransactionType::COMMITTED && $this->toInventoryTransactionType !== InventoryTransactionType::FULFILLED) { - $validator->addError($this, $attribute, 'Invalid Restock transaction type'); - } - }, - ]; - - $rules[] = [ - ['fromInventoryLocation', 'toInventoryLocation'], - function($attribute, $params, $validator) { - if ($this->fromInventoryLocation->id !== $this->toInventoryLocation->id) { - $validator->addError($this, $attribute, 'The from and to inventory locations must be the same.'); - } - }, - ]; - - return $rules; - } -} diff --git a/src/models/inventory/InventoryLocationDeactivatedMovement.php b/src/models/inventory/InventoryLocationDeactivatedMovement.php deleted file mode 100644 index bd7234c3c0..0000000000 --- a/src/models/inventory/InventoryLocationDeactivatedMovement.php +++ /dev/null @@ -1,53 +0,0 @@ -fromInventoryLocation->id === $this->toInventoryLocation->id) { - $validator->addError($this, $attribute, \Craft::t('commerce','The from and to inventory locations must be different.')); - } - }, - ]; - - $rules[] = [ - ['fromInventoryTransactionType'], - function($attribute, $params, $validator) { - if (!in_array($this->fromInventoryTransactionType, InventoryTransactionType::allowedManualMoveTransactionTypes(), true)) { - $validator->addError($this, $attribute, 'Can not move between these inventory types.'); - } - - if (!in_array($this->toInventoryTransactionType, InventoryTransactionType::allowedManualMoveTransactionTypes(), true)) { - $validator->addError($this, $attribute, 'Can not move between these inventory types.'); - } - }, - ]; - - return $rules; - } -} diff --git a/src/models/inventory/InventoryManualMovement.php b/src/models/inventory/InventoryManualMovement.php deleted file mode 100644 index 7d0ed0a7ec..0000000000 --- a/src/models/inventory/InventoryManualMovement.php +++ /dev/null @@ -1,121 +0,0 @@ -{$attribute}->canBeNegative() && $this->fromLocationAfterQuantity() < 0) { - $validator->addError($this, $attribute, 'The {inventoryLocation} inventory location’s {type} stock would drop below zero.', - [ - 'inventoryLocation' => $this->fromInventoryLocation->getUiLabel(), - 'type' => $this->{$attribute}->typeAsLabel(), - ] - ); - } - }, - ]; - - $rules[] = [ - ['toInventoryTransactionType'], - function($attribute, $params, $validator) { - if (!$this->{$attribute}->canBeNegative() && $this->toLocationAfterQuantity() < 0) { - $validator->addError($this, $attribute, 'The {inventoryLocation} inventory location stock of {type} would drop below zero.', - [ - 'inventoryLocation' => $this->toInventoryLocation->getUiLabel(), - 'type' => $this->{$attribute}->typeAsLabel(), - ] - ); - } - }, - ]; - - $rules[] = [ - ['fromInventoryLocation', 'toInventoryLocation'], - function($attribute, $params, $validator) { - if ($this->fromInventoryLocation->id !== $this->toInventoryLocation->id) { - $validator->addError($this, $attribute, 'The from and to inventory locations must be the same.'); - } - }, - ]; - - $rules[] = [ - ['toInventoryTransactionType'], - function($attribute, $params, $validator) { - if ($this->isManualMovement() && - ( - !in_array($this->fromInventoryTransactionType, InventoryTransactionType::allowedManualMoveTransactionTypes()) || - !in_array($this->toInventoryTransactionType, InventoryTransactionType::allowedManualMoveTransactionTypes()) - ) - ) { - $validator->addError($this, $attribute, \Craft::t('commerce','Can not move between these inventory types.')); - } - }, - ]; - - return $rules; - } - - /** - * @return int - */ - public function fromLocationAfterQuantity(): int - { - return (new Query()) - ->select(['quantity' => new \yii\db\Expression('COALESCE(SUM(quantity), 0) - :quantity')]) - ->from(Table::INVENTORYTRANSACTIONS) - ->where([ - 'type' => $this->fromInventoryTransactionType->value, - 'inventoryItemId' => $this->inventoryItemId, - 'inventoryLocationId' => $this->fromInventoryLocation->id, - ]) - ->params([':quantity' => $this->quantity]) - ->scalar(); - } - - /** - * Determines if this is a manual movement between available and unavailable inventory. - * - * @return bool - */ - public function isManualMovement(): bool - { - return ( - $this->lineItemId === null && $this->transferId === null - ); - } - - /** - * @return int - */ - public function toLocationAfterQuantity(): int - { - return (new Query()) - ->select(['quantity' => new \yii\db\Expression('COALESCE(SUM(quantity), 0) + :quantity')]) - ->from(Table::INVENTORYTRANSACTIONS) - ->where([ - 'type' => $this->toInventoryTransactionType->value, - 'inventoryItemId' => $this->inventoryItemId, - 'inventoryLocationId' => $this->toInventoryLocation->id, - ]) - ->params([':quantity' => $this->quantity]) - ->scalar(); - } -} diff --git a/src/models/inventory/InventoryRestockMovement.php b/src/models/inventory/InventoryRestockMovement.php deleted file mode 100644 index 1258dd3380..0000000000 --- a/src/models/inventory/InventoryRestockMovement.php +++ /dev/null @@ -1,42 +0,0 @@ -fromInventoryTransactionType !== InventoryTransactionType::COMMITTED || $this->toInventoryTransactionType !== InventoryTransactionType::AVAILABLE) { - $validator->addError($this, $attribute, 'Invalid Restock transaction type'); - } - }, - ]; - - $rules[] = [ - ['fromInventoryLocation', 'toInventoryLocation'], - function($attribute, $params, $validator) { - if ($this->fromInventoryLocation->id !== $this->toInventoryLocation->id) { - $validator->addError($this, $attribute, 'The from and to inventory locations must be the same.'); - } - }, - ]; - - return $rules; - } -} diff --git a/src/models/inventory/InventoryTransferMovement.php b/src/models/inventory/InventoryTransferMovement.php deleted file mode 100644 index cbe7d8506d..0000000000 --- a/src/models/inventory/InventoryTransferMovement.php +++ /dev/null @@ -1,12 +0,0 @@ - [...InventoryTransactionType::allowedManualAdjustmentTypes(), 'onHand']], - [['updateAction'], 'in', 'range' => InventoryUpdateQuantityType::values()], - ]); - } -} diff --git a/src/models/inventory/UpdateInventoryLevelInTransfer.php b/src/models/inventory/UpdateInventoryLevelInTransfer.php deleted file mode 100644 index e3e5b5c6ed..0000000000 --- a/src/models/inventory/UpdateInventoryLevelInTransfer.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @since 2.0 - */ -abstract class BasePaymentForm extends Model -{ - public bool $savePaymentSource = false; - - /** - * Populate the payment form from a payment form. - * - * @param PaymentSource $paymentSource the source to ue - * @throws NotSupportedException if not supported by current gateway. - */ - public function populateFromPaymentSource(PaymentSource $paymentSource): void - { - throw new NotSupportedException(); - } -} diff --git a/src/models/payments/CreditCardPaymentForm.php b/src/models/payments/CreditCardPaymentForm.php deleted file mode 100644 index 77ffd4f95d..0000000000 --- a/src/models/payments/CreditCardPaymentForm.php +++ /dev/null @@ -1,119 +0,0 @@ - - * @since 2.0 - */ -class CreditCardPaymentForm extends BasePaymentForm -{ - /** - * @var string|null First name - */ - public ?string $firstName = null; - - /** - * @var string|null Last name - */ - public ?string $lastName = null; - - /** - * @var string|null Card number - */ - public ?string $number = null; - - /** - * @var string|null Expiry month - */ - public ?string $month = null; - - /** - * @var string|null Expiry year - */ - public ?string $year = null; - - /** - * @var string|null CVV number - */ - public ?string $cvv = null; - - /** - * @var string|null Token - */ - public ?string $token = null; - - /** - * @var string|null Expiry date - */ - public ?string $expiry = null; - - /** - * @var bool - */ - public bool $threeDSecure = false; - - /** - * @inheritdoc - */ - public function setAttributes($values, $safeOnly = true): void - { - parent::setAttributes($values, $safeOnly); - - $this->number = preg_replace('/\D/', '', $values['number'] ?? ''); - - if (isset($values['expiry'])) { - $expiry = explode('/', $values['expiry']); - - if (isset($expiry[0])) { - $this->month = trim($expiry[0]); - } - - if (isset($expiry[1])) { - $this->year = trim($expiry[1]); - } - } - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['firstName', 'lastName', 'month', 'year', 'cvv', 'number'], 'required'], - [['month'], 'integer', 'integerOnly' => true, 'min' => 1, 'max' => 12], - [['year'], 'integer', 'integerOnly' => true, 'min' => date('Y'), 'max' => (int)date('Y') + 12], - [['cvv'], 'integer', 'integerOnly' => true], - [['cvv'], 'string', 'length' => [3, 4]], - [['number'], 'integer', 'integerOnly' => true], - [['number'], 'string', 'max' => 19], - [['number'], 'creditCardLuhn'], - ]; - } - - /** - * @param string $attribute - */ - public function creditCardLuhn(string $attribute): void - { - $str = ''; - foreach (array_reverse(str_split($this->$attribute)) as $i => $c) { - /** @var int $c */ - $str .= ($i % 2) ? $c * 2 : $c; - } - - if (array_sum(str_split($str)) % 10 !== 0) { - $this->addError($attribute, Craft::t('commerce', 'Not a valid credit card number.')); - } - } -} diff --git a/src/models/payments/DummyPaymentForm.php b/src/models/payments/DummyPaymentForm.php deleted file mode 100644 index ce24c9b4c6..0000000000 --- a/src/models/payments/DummyPaymentForm.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @since 2.0 - */ -class DummyPaymentForm extends CreditCardPaymentForm -{ - public function populateFromPaymentSource(PaymentSource $paymentSource): void - { - $this->token = (string)$paymentSource->id; - } - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - if ($this->token) { - return []; //No validation of form if using a token - } - - return parent::defineRules(); - } -} diff --git a/src/models/payments/OffsitePaymentForm.php b/src/models/payments/OffsitePaymentForm.php deleted file mode 100644 index 3fd90fb63a..0000000000 --- a/src/models/payments/OffsitePaymentForm.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @since 2.0 - */ -class OffsitePaymentForm extends BasePaymentForm -{ -} diff --git a/src/models/responses/Dummy.php b/src/models/responses/Dummy.php deleted file mode 100644 index f3109d606e..0000000000 --- a/src/models/responses/Dummy.php +++ /dev/null @@ -1,133 +0,0 @@ - - * @since 2.0 - */ -class Dummy implements RequestResponseInterface -{ - /** - * @var bool - */ - private bool $_success = true; - - public function __construct(?CreditCardPaymentForm $form = null) - { - if ($form === null) { - $this->_success = false; - return; - } - - // Token populated? This is a "payment source" so no need to fail anything - if ($form->token) { - return; - } - - $number = (string)$form->number; - $isValid = ((int)substr($number, -1) % 2 === 0); - - if (!$isValid) { - $this->_success = false; - } - } - - /** - * @inheritdoc - */ - public function isSuccessful(): bool - { - return $this->_success; - } - - /** - * @inheritdoc - */ - public function isRedirect(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function getRedirectMethod(): string - { - return ''; - } - - /** - * @inheritdoc - */ - public function getRedirectData(): array - { - return []; - } - - /** - * @inheritdoc - */ - public function getRedirectUrl(): string - { - return ''; - } - - /** - * @inheritdoc - */ - public function getTransactionReference(): string - { - return date('Y-m-d-H-i-s'); - } - - /** - * @inheritdoc - */ - public function getCode(): string - { - return $this->_success ? '' : 'payment.failed'; - } - - /** - * @inheritdoc - */ - public function getMessage(): string - { - return $this->_success ? '' : Craft::t('commerce', 'Dummy gateway payment failed.'); - } - - /** - * @inheritdoc - */ - public function redirect(): void - { - } - - /** - * @inheritdoc - */ - public function getData(): mixed - { - return ''; - } - - /** - * @inheritdoc - */ - public function isProcessing(): bool - { - return false; - } -} diff --git a/src/models/responses/DummySubscriptionResponse.php b/src/models/responses/DummySubscriptionResponse.php deleted file mode 100644 index 26cef472c2..0000000000 --- a/src/models/responses/DummySubscriptionResponse.php +++ /dev/null @@ -1,98 +0,0 @@ - - * @since 2.0 - */ -class DummySubscriptionResponse implements SubscriptionResponseInterface -{ - /** - * @var bool Whether this subscription is canceled - */ - private bool $_isCanceled = false; - - /** - * @var int Amount of trial days - */ - private int $_trialDays = 0; - - public function setIsCanceled(bool $isCanceled): void - { - $this->_isCanceled = $isCanceled; - } - - public function setTrialDays(int $trialDays): void - { - $this->_trialDays = $trialDays; - } - - /** - * @inheritdoc - */ - public function getData(): mixed - { - return ['dummyData' => StringHelper::randomString()]; - } - - /** - * @inheritdoc - */ - public function getReference(): string - { - return StringHelper::randomString(); - } - - /** - * @inheritdoc - */ - public function getTrialDays(): int - { - return $this->_trialDays; - } - - /** - * @inheritdoc - */ - public function getNextPaymentDate(): DateTime - { - return (new DateTime())->add(new DateInterval('P1Y')); - } - - /** - * @inheritdoc - */ - public function isCanceled(): bool - { - return $this->_isCanceled; - } - - /** - * @inheritdoc - */ - public function isScheduledForCancellation(): bool - { - return $this->_isCanceled; - } - - /** - * @inheritdoc - */ - public function isInactive(): bool - { - return false; - } -} diff --git a/src/models/responses/Manual.php b/src/models/responses/Manual.php deleted file mode 100644 index 92018e001c..0000000000 --- a/src/models/responses/Manual.php +++ /dev/null @@ -1,106 +0,0 @@ - - * @since 2.0 - */ -class Manual implements RequestResponseInterface -{ - /** - * @inheritdoc - */ - public function isSuccessful(): bool - { - return true; - } - - /** - * @inheritdoc - */ - public function isRedirect(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function getRedirectMethod(): string - { - return ''; - } - - /** - * @inheritdoc - */ - public function getRedirectData(): array - { - return []; - } - - /** - * @inheritdoc - */ - public function getRedirectUrl(): string - { - return ''; - } - - /** - * @inheritdoc - */ - public function getTransactionReference(): string - { - return date('Y-m-d-H-i-s'); - } - - /** - * @inheritdoc - */ - public function getCode(): string - { - return ''; - } - - /** - * @inheritdoc - */ - public function getMessage(): string - { - return ''; - } - - /** - * @inheritdoc - */ - public function redirect(): void - { - } - - /** - * @inheritdoc - */ - public function getData(): mixed - { - return ''; - } - - /** - * @inheritdoc - */ - public function isProcessing(): bool - { - return false; - } -} diff --git a/src/models/subscriptions/CancelSubscriptionForm.php b/src/models/subscriptions/CancelSubscriptionForm.php deleted file mode 100644 index a823f82380..0000000000 --- a/src/models/subscriptions/CancelSubscriptionForm.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class CancelSubscriptionForm extends Model -{ -} diff --git a/src/models/subscriptions/DummyPlan.php b/src/models/subscriptions/DummyPlan.php deleted file mode 100644 index 7af55354d8..0000000000 --- a/src/models/subscriptions/DummyPlan.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @since 2.0 - */ -class DummyPlan extends Plan -{ - /** - * @inheritdoc - * @todo Fix typo: rename $currentPlant parameter to $currentPlan in Commerce 6.0 - */ - public function canSwitchFrom(PlanInterface $currentPlant): bool - { - return true; - } -} diff --git a/src/models/subscriptions/SubscriptionForm.php b/src/models/subscriptions/SubscriptionForm.php deleted file mode 100644 index 51e8a73461..0000000000 --- a/src/models/subscriptions/SubscriptionForm.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @since 2.0 - */ -class SubscriptionForm extends Model -{ - /** - * Trial days for the subscription. - * - * @var int - */ - public int $trialDays = 0; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - return [ - [['trialDays'], 'integer', 'integerOnly' => true, 'min' => 0], - ]; - } -} diff --git a/src/models/subscriptions/SubscriptionPayment.php b/src/models/subscriptions/SubscriptionPayment.php deleted file mode 100644 index c2380c6473..0000000000 --- a/src/models/subscriptions/SubscriptionPayment.php +++ /dev/null @@ -1,51 +0,0 @@ - - * @since 2.0 - */ -class SubscriptionPayment extends Model -{ - /** - * @var float payment amount - */ - public float $paymentAmount; - - /** - * @var Currency payment currency - */ - public Currency $paymentCurrency; - - /** - * @var DateTime time of payment in UTC - */ - public DateTime $paymentDate; - - /** - * @var string the payment reference on gateway - */ - public string $paymentReference; - - /** - * @var bool whether payment has been collected - */ - public bool $paid = false; - - /** - * @var string the gateway response text - */ - public string $response; -} diff --git a/src/models/subscriptions/SwitchPlansForm.php b/src/models/subscriptions/SwitchPlansForm.php deleted file mode 100644 index 6ea0af5a60..0000000000 --- a/src/models/subscriptions/SwitchPlansForm.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @since 2.0 - */ -class SwitchPlansForm extends Model -{ -} diff --git a/src/plugin/Routes.php b/src/plugin/Routes.php deleted file mode 100644 index 7361d6bd6a..0000000000 --- a/src/plugin/Routes.php +++ /dev/null @@ -1,179 +0,0 @@ - - * @since 2.0 - */ -trait Routes -{ - /** - * @since 3.1.10 - */ - private function _registerSiteRoutes(): void - { - Event::on(UrlManager::class, UrlManager::EVENT_REGISTER_SITE_URL_RULES, function(RegisterUrlRulesEvent $event) { - $event->rules['commerce/webhooks/process-webhook/gateway/'] = 'commerce/webhooks/process-webhook'; - }); - } - - /** - * @since 2.0 - */ - private function _registerCpRoutes(): void - { - Event::on(UrlManager::class, UrlManager::EVENT_REGISTER_CP_URL_RULES, function(RegisterUrlRulesEvent $event) { - $event->rules['commerce'] = ['template' => 'commerce/index']; - - // User edit screen - $event->rules['myaccount/commerce'] = 'commerce/users/index'; - $event->rules['users//commerce'] = 'commerce/users/index'; - - // Products / Variants - $event->rules['commerce/products'] = 'commerce/products/product-index'; - $event->rules['commerce/variants'] = 'commerce/variants/index'; - $event->rules['commerce/products/'] = 'commerce/products/product-index'; - $event->rules['commerce/variants/'] = 'commerce/variants/index'; - $event->rules['commerce/variants/'] = 'elements/edit'; - $event->rules['commerce/products//new'] = 'commerce/products/create'; - $event->rules['commerce/products//'] = 'elements/edit'; - - $event->rules['commerce/subscriptions'] = 'commerce/subscriptions/index'; - $event->rules['commerce/subscriptions/'] = 'commerce/subscriptions/index'; - $event->rules['commerce/subscriptions/'] = 'commerce/subscriptions/edit'; - - // Subscription plans - $event->rules['commerce/subscription-plans'] = 'commerce/plans/plan-index'; - $event->rules['commerce/subscription-plans/'] = 'commerce/plans/edit-plan'; - $event->rules['commerce/subscription-plans/new'] = 'commerce/plans/edit-plan'; - - // Product Types - $event->rules['commerce/settings/producttypes'] = 'commerce/product-types/product-type-index'; - $event->rules['commerce/settings/producttypes/'] = 'commerce/product-types/edit-product-type'; - $event->rules['commerce/settings/producttypes/new'] = 'commerce/product-types/edit-product-type'; - - // Orders - $event->rules['commerce/orders'] = 'commerce/orders/order-index'; - $event->rules['commerce/orders/'] = 'commerce/orders/edit-order'; - - $event->rules['commerce/orders//create'] = 'commerce/orders/create'; - - $event->rules['commerce/orders/'] = 'commerce/orders/order-index'; - - // Settings - - $event->rules['commerce/settings/stores'] = 'commerce/stores/stores-index'; - $event->rules['commerce/settings/stores/new'] = 'commerce/stores/edit-store'; - $event->rules['commerce/settings/stores/'] = 'commerce/stores/edit-store'; - - $event->rules['commerce/settings/sites'] = 'commerce/stores/edit-site-stores'; - - $event->rules['commerce/settings/general'] = 'commerce/settings/edit'; - - $event->rules['commerce/settings/ordersettings'] = 'commerce/order-settings/edit'; - - $event->rules['commerce/settings/transfers'] = 'commerce/settings/edit-transfer-settings'; - - $event->rules['commerce/settings/subscriptions'] = 'commerce/settings/edit-subscription-settings'; - - $event->rules['commerce/settings/gateways'] = 'commerce/gateways/index'; - $event->rules['commerce/settings/gateways/new'] = 'commerce/gateways/edit'; - $event->rules['commerce/settings/gateways/'] = 'commerce/gateways/edit'; - - $event->rules['commerce/settings/emails'] = 'commerce/emails/index'; - $event->rules['commerce/settings/emails//new'] = 'commerce/emails/edit'; - $event->rules['commerce/settings/emails//'] = 'commerce/emails/edit'; - - $event->rules['commerce/settings/pdfs'] = 'commerce/pdfs/index'; - $event->rules['commerce/settings/pdfs//new'] = 'commerce/pdfs/edit'; - $event->rules['commerce/settings/pdfs//'] = 'commerce/pdfs/edit'; - - $event->rules['commerce/settings/orderstatuses'] = 'commerce/order-statuses/index'; - $event->rules['commerce/settings/orderstatuses//new'] = 'commerce/order-statuses/edit'; - $event->rules['commerce/settings/orderstatuses//'] = 'commerce/order-statuses/edit'; - - $event->rules['commerce/settings/lineitemstatuses'] = 'commerce/line-item-statuses/index'; - $event->rules['commerce/settings/lineitemstatuses//new'] = 'commerce/line-item-statuses/edit'; - $event->rules['commerce/settings/lineitemstatuses//'] = 'commerce/line-item-statuses/edit'; - - // Store Settings - $event->rules['commerce/store-management'] = 'commerce/store-management/index'; // Redirects to the first store - $event->rules['commerce/store-management/'] = 'commerce/store-management/edit'; - - $event->rules['commerce/store-management//payment-currencies'] = 'commerce/payment-currencies/index'; - $event->rules['commerce/store-management//payment-currencies/new'] = 'commerce/payment-currencies/edit'; - $event->rules['commerce/store-management//payment-currencies/'] = 'commerce/payment-currencies/edit'; - - // Shipping - $event->rules['commerce/store-management//shippingzones'] = 'commerce/shipping-zones/index'; - $event->rules['commerce/store-management//shippingzones/new'] = 'commerce/shipping-zones/edit'; - $event->rules['commerce/store-management//shippingzones/'] = 'commerce/shipping-zones/edit'; - - $event->rules['commerce/store-management//shippingcategories'] = 'commerce/shipping-categories/index'; - $event->rules['commerce/store-management//shippingcategories/new'] = 'commerce/shipping-categories/edit'; - $event->rules['commerce/store-management//shippingcategories/'] = 'commerce/shipping-categories/edit'; - - $event->rules['commerce/store-management//shippingmethods'] = 'commerce/shipping-methods/index'; - $event->rules['commerce/store-management//shippingmethods/new'] = 'commerce/shipping-methods/edit'; - $event->rules['commerce/store-management//shippingmethods/'] = 'commerce/shipping-methods/edit'; - $event->rules['commerce/store-management//shippingmethods//shippingrules/new'] = 'commerce/shipping-rules/edit'; - $event->rules['commerce/store-management//shippingmethods//shippingrules/'] = 'commerce/shipping-rules/edit'; - - // Taxes - $event->rules['commerce/store-management//taxcategories'] = 'commerce/tax-categories/index'; - $event->rules['commerce/store-management//taxcategories/new'] = 'commerce/tax-categories/edit'; - $event->rules['commerce/store-management//taxcategories/'] = 'commerce/tax-categories/edit'; - - $event->rules['commerce/store-management//taxzones'] = 'commerce/tax-zones/index'; - $event->rules['commerce/store-management//taxzones/new'] = 'commerce/tax-zones/edit'; - $event->rules['commerce/store-management//taxzones/'] = 'commerce/tax-zones/edit'; - $event->rules['commerce/store-management//taxrates'] = 'commerce/tax-rates/index'; - $event->rules['commerce/store-management//taxrates/new'] = 'commerce/tax-rates/edit'; - $event->rules['commerce/store-management//taxrates/'] = 'commerce/tax-rates/edit'; - - // Sales - $event->rules['commerce/store-management//sales'] = 'commerce/sales/index'; - $event->rules['commerce/store-management//sales/new'] = 'commerce/sales/edit'; - $event->rules['commerce/store-management//sales/'] = 'commerce/sales/edit'; - - // Discounts - $event->rules['commerce/store-management//discounts'] = 'commerce/discounts/index'; - $event->rules['commerce/store-management//discounts/new'] = 'commerce/discounts/edit'; - $event->rules['commerce/store-management//discounts/'] = 'commerce/discounts/edit'; - - // Pricing - $event->rules['commerce/store-management//pricing-rules'] = 'commerce/catalog-pricing-rules/index'; - $event->rules['commerce/store-management//pricing-rules/new'] = 'commerce/catalog-pricing-rules/edit'; - $event->rules['commerce/store-management//pricing-rules/'] = 'commerce/catalog-pricing-rules/edit'; - - // Inventory - $event->rules['commerce/inventory'] = 'commerce/inventory/edit-location-levels'; // redirect to the first location - $event->rules['commerce/inventory/levels'] = 'commerce/inventory/edit-location-levels'; // redirect to the first location - - $event->rules['commerce/inventory/item/'] = 'commerce/inventory/item-edit'; - $event->rules['commerce/inventory/levels/'] = 'commerce/inventory/edit-location-levels'; - - $event->rules['commerce/inventory-locations'] = 'commerce/inventory-locations/index'; - $event->rules['commerce/inventory-locations/new'] = 'commerce/inventory-locations/edit'; - $event->rules['commerce/inventory-locations/'] = 'commerce/inventory-locations/edit'; - - $event->rules['commerce/inventory/transfers'] = 'commerce/transfers/index'; - $event->rules['commerce/inventory/transfers/'] = 'elements/edit'; - - // Donations - $event->rules['commerce/donations'] = 'commerce/donations/edit'; - }); - } -} diff --git a/src/plugin/Services.php b/src/plugin/Services.php deleted file mode 100644 index 2c350cfbf2..0000000000 --- a/src/plugin/Services.php +++ /dev/null @@ -1,607 +0,0 @@ - - * @since 2.0 - */ -trait Services -{ - /** - * Returns the cart service - * - * @return Carts The cart service - * @throws InvalidConfigException - */ - public function getCarts(): Carts - { - return $this->get('carts'); - } - - /** - * Returns the coupons service - * - * @return Coupons The countries service - * @throws InvalidConfigException - */ - public function getCoupons(): Coupons - { - return $this->get('coupons'); - } - - /** - * Returns the currencies service - * - * @return Currencies The currencies service - * @throws InvalidConfigException - */ - public function getCurrencies(): Currencies - { - return $this->get('currencies'); - } - - /** - * Returns the customers service - * - * @return Customers The customers service - * @throws InvalidConfigException - */ - public function getCustomers(): Customers - { - return $this->get('customers'); - } - - /** - * Returns the discounts service - * - * @return Discounts The discounts service - * @throws InvalidConfigException - */ - public function getDiscounts(): Discounts - { - return $this->get('discounts'); - } - - /** - * Returns the emails service - * - * @return Emails The emails service - * @throws InvalidConfigException - */ - public function getEmails(): Emails - { - return $this->get('emails'); - } - - /** - * Returns the formulas service - * - * @return Formulas the formulas service - * @throws InvalidConfigException - * @since 2.2 - */ - public function getFormulas(): Formulas - { - return $this->get('formulas'); - } - - /** - * Returns the gateways service - * - * @return Gateways The gateways service - * @throws InvalidConfigException - */ - public function getGateways(): Gateways - { - return $this->get('gateways'); - } - - /** - * Returns the inventory service - * - * @return Inventory The inventory service - * @throws InvalidConfigException - */ - public function getInventory(): Inventory - { - return $this->get('inventory'); - } - - /** - * Returns the inventory locations service - * - * @return InventoryLocations The inventory locations service - * @throws InvalidConfigException - */ - public function getInventoryLocations(): InventoryLocations - { - return $this->get('inventoryLocations'); - } - - /** - * Returns the lineItems service - * - * @return LineItems The lineItems service - * @throws InvalidConfigException - */ - public function getLineItems(): LineItems - { - return $this->get('lineItems'); - } - - /** - * Returns the lineItems statuses service - * - * @return LineItemStatuses The lineItems service - * @throws InvalidConfigException - */ - public function getLineItemStatuses(): LineItemStatuses - { - return $this->get('lineItemStatuses'); - } - - /** - * Returns the orderAdjustments service - * - * @return OrderAdjustments The orderAdjustments service - * @throws InvalidConfigException - */ - public function getOrderAdjustments(): OrderAdjustments - { - return $this->get('orderAdjustments'); - } - - /** - * Returns the orderHistories service - * - * @return OrderHistories The orderHistories service - * @throws InvalidConfigException - */ - public function getOrderHistories(): OrderHistories - { - return $this->get('orderHistories'); - } - - /** - * Returns the orders service - * - * @return Orders The orders service - * @throws InvalidConfigException - */ - public function getOrders(): Orders - { - return $this->get('orders'); - } - - /** - * Returns the OrderNotices service - * - * @return OrderNotices The OrderNotices service - * @throws InvalidConfigException - */ - public function getOrderNotices(): OrderNotices - { - return $this->get('orderNotices'); - } - - /** - * Returns the OrderStatuses service - * - * @return OrderStatuses The OrderStatuses service - * @throws InvalidConfigException - */ - public function getOrderStatuses(): OrderStatuses - { - return $this->get('orderStatuses'); - } - - /** - * Returns the paymentCurrencies service - * - * @return PaymentCurrencies The paymentCurrencies service - * @throws InvalidConfigException - */ - public function getPaymentCurrencies(): PaymentCurrencies - { - return $this->get('paymentCurrencies'); - } - - /** - * Returns the payments service - * - * @return Payments The payments service - * @throws InvalidConfigException - */ - public function getPayments(): Payments - { - return $this->get('payments'); - } - - /** - * Returns the payment sources service - * - * @return PaymentSources The payment sources service - * @throws InvalidConfigException - */ - public function getPaymentSources(): PaymentSources - { - return $this->get('paymentSources'); - } - - /** - * Returns the PDFs service - * - * @return Pdfs The PDFs service - * @throws InvalidConfigException - */ - public function getPdfs(): Pdfs - { - return $this->get('pdfs'); - } - - /** - * Returns the payment sources service - * - * @return Plans The subscription plans service - * @throws InvalidConfigException - */ - public function getPlans(): Plans - { - return $this->get('plans'); - } - - /** - * Returns the catalog pricing service - * - * @return CatalogPricing - * @throws InvalidConfigException - */ - public function getCatalogPricing(): CatalogPricing - { - return $this->get('catalogPricing'); - } - - /** - * Returns the catalog pricing rules service - * - * @return CatalogPricingRules - * @throws InvalidConfigException - */ - public function getCatalogPricingRules(): CatalogPricingRules - { - return $this->get('catalogPricingRules'); - } - - /** - * Returns the products service - * - * @return Products The products service - * @throws InvalidConfigException - */ - public function getProducts(): Products - { - return $this->get('products'); - } - - /** - * Returns the productTypes service - * - * @return ProductTypes The productTypes service - * @throws InvalidConfigException - */ - public function getProductTypes(): ProductTypes - { - return $this->get('productTypes'); - } - - /** - * Returns the purchasables service - * - * @return Purchasables The purchasables service - * @throws InvalidConfigException - */ - public function getPurchasables(): Purchasables - { - return $this->get('purchasables'); - } - - /** - * Returns the sales service - * - * @return Sales The sales service - * @throws InvalidConfigException - */ - public function getSales(): Sales - { - return $this->get('sales'); - } - - /** - * Returns the shippingMethods service - * - * @return ShippingMethods The shippingMethods service - * @throws InvalidConfigException - */ - public function getShippingMethods(): ShippingMethods - { - return $this->get('shippingMethods'); - } - - /** - * Returns the shippingRules service - * - * @return ShippingRules The shippingRules service - * @throws InvalidConfigException - */ - public function getShippingRules(): ShippingRules - { - return $this->get('shippingRules'); - } - - /** - * Returns the shippingRules service - * - * @return ShippingRuleCategories The shippingRuleCategories service - * @throws InvalidConfigException - */ - public function getShippingRuleCategories(): ShippingRuleCategories - { - return $this->get('shippingRuleCategories'); - } - - /** - * Returns the shippingCategories service - * - * @return ShippingCategories The shippingCategories service - * @throws InvalidConfigException - */ - public function getShippingCategories(): ShippingCategories - { - return $this->get('shippingCategories'); - } - - /** - * Returns the shippingZones service - * - * @return ShippingZones The shippingZones service - * @throws InvalidConfigException - */ - public function getShippingZones(): ShippingZones - { - return $this->get('shippingZones'); - } - - /** - * Returns the store service - * - * @return StoreSettings The store service - * @throws InvalidConfigException - */ - public function getStoreSettings(): StoreSettings - { - return $this->get('storeSettings'); - } - - /** - * Returns the stores service - * - * @return Stores The stores service - * @throws InvalidConfigException - */ - public function getStores(): Stores - { - return $this->get('stores'); - } - - /** - * Returns the stores service - * - * @return Store The store service - * @throws InvalidConfigException - */ - public function getStore(): Store - { - return $this->get('store'); - } - - /** - * Returns the subscriptions service - * - * @return Subscriptions The subscriptions service - * @throws InvalidConfigException - */ - public function getSubscriptions(): Subscriptions - { - return $this->get('subscriptions'); - } - - /** - * Returns the taxes service - * - * @return Taxes The taxes service - * @throws InvalidConfigException - */ - public function getTaxes(): Taxes - { - return $this->get('taxes'); - } - - /** - * Returns the taxCategories service - * - * @return TaxCategories The taxCategories service - * @throws InvalidConfigException - */ - public function getTaxCategories(): TaxCategories - { - return $this->get('taxCategories'); - } - - /** - * Returns the taxRates service - * - * @return TaxRates The taxRates service - * @throws InvalidConfigException - */ - public function getTaxRates(): TaxRates - { - return $this->get('taxRates'); - } - - /** - * Returns the taxZones service - * - * @return TaxZones The taxZones service - * @throws InvalidConfigException - */ - public function getTaxZones(): TaxZones - { - return $this->get('taxZones'); - } - - /** - * Returns the transactions service - * - * @return Transactions The transactions service - * @throws InvalidConfigException - */ - public function getTransactions(): Transactions - { - return $this->get('transactions'); - } - - /** - * Returns the transfers service - * - * @return Transfers The transfers service - * @throws InvalidConfigException - */ - public function getTransfers(): Transfers - { - return $this->get('transfers'); - } - - /** - * Returns the variants service - * - * @return Variants The variants service - * @throws InvalidConfigException - */ - public function getVariants(): Variants - { - return $this->get('variants'); - } - - /** - * Returns the VAT service - * - * @return Vat The VAT service - * @throws InvalidConfigException - */ - public function getVat(): Vat - { - return $this->get('vat'); - } - - /** - * Returns the webhooks service - * - * @return Webhooks The variants service - * @throws InvalidConfigException - * @since 3.1.9 - */ - public function getWebhooks(): Webhooks - { - return $this->get('webhooks'); - } -} diff --git a/src/plugin/Variables.php b/src/plugin/Variables.php deleted file mode 100644 index a993ebf7a4..0000000000 --- a/src/plugin/Variables.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 2.0 - */ -trait Variables -{ - /** - * Returns the donation purchasable - * - * @return Donation|null The donation purchasable - */ - public function getDonation(): ?Donation - { - return Donation::find()->status(null)->one(); - } -} diff --git a/src/queue/jobs/CatalogPricing.php b/src/queue/jobs/CatalogPricing.php deleted file mode 100644 index dff7406709..0000000000 --- a/src/queue/jobs/CatalogPricing.php +++ /dev/null @@ -1,90 +0,0 @@ -getCatalogPricing(); - $isConsolidatedJob = $this->storeId === null && $this->purchasableIds === null && $this->catalogPricingRuleIds === null; - $catalogPricingRules = null; - $reservedRowId = null; - - // @TODO: remove these properties and behaviour at next breaking change - $storeId = $this->storeId; - $purchasableIds = $this->purchasableIds; - $catalogPricingRuleIds = $this->catalogPricingRuleIds; - - if ($isConsolidatedJob) { - // New method of processing catalog pricing via queue table: reserve a row and process based on its type and IDs - $reservedRecord = $catalogPricingService->reserveCatalogPricingQueueRow(); - - if (!$reservedRecord) { - return; - } - - $reservedRowId = $reservedRecord->id; - $storeId = $reservedRecord->storeId; - - if ($reservedRecord->type === CatalogPricingQueueRecord::TYPE_PURCHASABLE) { - // Specific purchasable IDs: regenerate against all applicable rules - $purchasableIds = $reservedRecord->getIds(); - } elseif ($reservedRecord->type === CatalogPricingQueueRecord::TYPE_RULE) { - $catalogPricingRuleIds = $reservedRecord->getIds(); - } else { - throw new \UnexpectedValueException("Unrecognized catalog pricing queue row type: {$reservedRecord->type}"); - } - } - - if (!empty($catalogPricingRuleIds)) { - $catalogPricingRules = Plugin::getInstance()->getCatalogPricingRules() - ->getAllCatalogPricingRules($storeId) - ->whereIn('id', $catalogPricingRuleIds) - ->all(); - } - - try { - $catalogPricingService->generateCatalogPrices($purchasableIds, $catalogPricingRules, queue: $queue); - - if ($reservedRowId) { - $catalogPricingService->deleteCatalogPricingQueueRowById($reservedRowId); - } - } catch (\Throwable $e) { - if ($reservedRowId) { - $catalogPricingService->releaseCatalogPricingQueueRowById($reservedRowId); - } - - throw $e; - } - } - - protected function defaultDescription(): ?string - { - return 'Generating catalog pricing.'; - } -} diff --git a/src/queue/jobs/ResaveProductVariants.php b/src/queue/jobs/ResaveProductVariants.php deleted file mode 100644 index 28bd63c636..0000000000 --- a/src/queue/jobs/ResaveProductVariants.php +++ /dev/null @@ -1,68 +0,0 @@ - - * @since 5.5.0 - */ -class ResaveProductVariants extends BaseJob -{ - /** - * @var int The product ID whose variants should be resaved - */ - public int $productId; - - /** - * @inheritdoc - */ - public function execute($queue): void - { - $product = Product::find() - ->id($this->productId) - ->one(); - - if (!$product) { - return; - } - - $variants = Variant::find() - ->productId($this->productId) - ->status(null) - ->all(); - - $total = count($variants); - - foreach ($variants as $i => $variant) { - $this->setProgress($queue, $i / $total); - \Craft::$app->getElements()->saveElement($variant); - } - } - - /** - * @inheritdoc - */ - protected function defaultDescription(): ?string - { - $product = Product::find() - ->id($this->productId) - ->one(); - - if ($product) { - return 'Resaving variants for product: ' . $product->title; - } - - return 'Resaving product variants'; - } -} diff --git a/src/queue/jobs/SendEmail.php b/src/queue/jobs/SendEmail.php deleted file mode 100644 index 1ed9deee25..0000000000 --- a/src/queue/jobs/SendEmail.php +++ /dev/null @@ -1,101 +0,0 @@ -setProgress($queue, 0.2); - - $order = $this->_getOrder(); - - if (!$order) { - throw new InvalidConfigException('Invalid order ID: ' . $this->orderId); - } - - $email = Plugin::getInstance()->getEmails()->getEmailById($this->commerceEmailId, $order->getStore()->id); - if (!$email) { - throw new InvalidConfigException('Invalid email ID: ' . $this->commerceEmailId); - } - - $orderHistory = Plugin::getInstance()->getOrderHistories()->getOrderHistoryById($this->orderHistoryId); - $this->setProgress($queue, 0.5); - - $error = ''; - if (!Plugin::getInstance()->getEmails()->sendEmail($email, $order, $orderHistory, $this->orderData, $error)) { - throw new EmailException($error); - } - - $this->setProgress($queue, 1); - } - - /** - * @return Order|null - */ - private function _getOrder(): ?Order - { - return Order::find()->id($this->orderId)->one(); - } - - /** - * @return int - * @inheritDoc - */ - public function getTtr(): int - { - return 60; - } - - /** - * @inheritDoc - */ - public function canRetry($attempt, $error): bool - { - return $attempt < 5; - } - - /** - * @inheritDoc - * - */ - protected function defaultDescription(): ?string - { - return 'Sending email for order ' . $this->_getOrder()?->reference; - } -} diff --git a/src/records/CatalogPricing.php b/src/records/CatalogPricing.php deleted file mode 100644 index 3a87d1cf3a..0000000000 --- a/src/records/CatalogPricing.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @since 5.0.0 - */ -class CatalogPricing extends ActiveRecord -{ - use StoreRecordTrait; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::CATALOG_PRICING; - } -} diff --git a/src/records/CatalogPricingQueue.php b/src/records/CatalogPricingQueue.php deleted file mode 100644 index c73226f842..0000000000 --- a/src/records/CatalogPricingQueue.php +++ /dev/null @@ -1,84 +0,0 @@ - - * @since 5.7.0 - */ -class CatalogPricingQueue extends ActiveRecord -{ - /** - * Row type for purchasable-ID-based catalog pricing work. - */ - public const TYPE_PURCHASABLE = 'purchasable'; - - /** - * Row type for rule-ID-based (or full-regeneration) catalog pricing work. - */ - public const TYPE_RULE = 'rule'; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::CATALOG_PRICING_QUEUE; - } - - /** - * Returns the decoded IDs array from the JSON column value. - * - * @return array|null - */ - public function getIds(): ?array - { - $raw = $this->getAttribute('ids'); - - if ($raw === null || $raw === '') { - return null; - } - - $decoded = Json::decodeIfJson($raw); - - return is_array($decoded) ? $decoded : null; - } - - /** - * Encodes the IDs array to JSON and stores it in the column. - * - * @param array|null $ids - */ - public function setIds(?array $ids): void - { - $this->setAttribute('ids', $ids !== null ? Json::encode($ids) : null); - } - - /** - * @return ActiveQueryInterface - */ - public function getStore(): ActiveQueryInterface - { - return $this->hasOne(Store::class, ['id' => 'storeId']); - } -} diff --git a/src/records/CatalogPricingRule.php b/src/records/CatalogPricingRule.php deleted file mode 100644 index c835a3eaaa..0000000000 --- a/src/records/CatalogPricingRule.php +++ /dev/null @@ -1,67 +0,0 @@ - - * @since 5.0.0 - */ -class CatalogPricingRule extends ActiveRecord -{ - use StoreRecordTrait; - - public const APPLY_BY_PERCENT = 'byPercent'; - public const APPLY_BY_FLAT = 'byFlat'; - public const APPLY_TO_PERCENT = 'toPercent'; - public const APPLY_TO_FLAT = 'toFlat'; - public const APPLY_PRICE_TYPE_PRICE = 'price'; - public const APPLY_PRICE_TYPE_PROMOTIONAL_PRICE = 'promotionalPrice'; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::CATALOG_PRICING_RULES; - } - - /** - * @throws InvalidConfigException - */ - public function getUsers(): ActiveQueryInterface - { - return $this->hasMany(User::class, ['id' => 'userId'])->viaTable(Table::CATALOG_PRICING_RULES_USERS, ['catalogPricingRuleId' => 'id']); - } -} diff --git a/src/records/CatalogPricingRuleUser.php b/src/records/CatalogPricingRuleUser.php deleted file mode 100644 index 23b1cff3b8..0000000000 --- a/src/records/CatalogPricingRuleUser.php +++ /dev/null @@ -1,61 +0,0 @@ - - * @since 5.0.0 - */ -class CatalogPricingRuleUser extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::CATALOG_PRICING_RULES_USERS; - } - - /** - * @inheritdoc - */ - public function rules(): array - { - return [ - [['catalogPricingRuleId', 'userId'], 'unique', 'targetAttribute' => ['catalogPricingRuleId', 'userId']], - ]; - } - - /** - * @return ActiveQueryInterface - */ - public function getCatalogPricingRule(): ActiveQueryInterface - { - return $this->hasOne(CatalogPricingRule::class, ['id' => 'catalogPricingRuleId']); - } - - /** - * @noinspection PhpUnused - */ - public function getUser(): ActiveQueryInterface - { - return $this->hasOne(User::class, ['id' => 'userId']); - } -} diff --git a/src/records/Coupon.php b/src/records/Coupon.php deleted file mode 100644 index c697b7316c..0000000000 --- a/src/records/Coupon.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @since 4.0 - */ -class Coupon extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::COUPONS; - } -} diff --git a/src/records/Customer.php b/src/records/Customer.php deleted file mode 100644 index 24784b37fd..0000000000 --- a/src/records/Customer.php +++ /dev/null @@ -1,70 +0,0 @@ - - * @since 4.0 - */ -class Customer extends ActiveRecord -{ - /** - * @inheritdoc - */ - public function rules(): array - { - return [ - [ - [ - 'customerId', - 'primaryBillingAddressId', - 'primaryShippingAddressId', - 'primaryPaymentSourceId', - ], 'safe', - ], - ]; - } - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::CUSTOMERS; - } - - public function getPrimaryBillingAddress(): ActiveQueryInterface - { - return $this->hasOne(Element::class, ['id' => 'primaryBillingAddressId']); - } - - public function getPrimaryShippingAddress(): ActiveQueryInterface - { - return $this->hasOne(Element::class, ['id' => 'primaryShippingAddressId']); - } - - public function getPrimaryPaymentSource(): ActiveQueryInterface - { - return $this->hasOne(PaymentSource::class, ['id' => 'primaryPaymentSourceId']); - } -} diff --git a/src/records/CustomerDiscountUse.php b/src/records/CustomerDiscountUse.php deleted file mode 100644 index daf03c286d..0000000000 --- a/src/records/CustomerDiscountUse.php +++ /dev/null @@ -1,49 +0,0 @@ - - * @since 2.0 - */ -class CustomerDiscountUse extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::CUSTOMER_DISCOUNTUSES; - } - - public function getDiscount(): ActiveQueryInterface - { - return $this->hasOne(Discount::class, ['id', 'discountId']); - } - - /** - * @return ActiveQueryInterface - */ - public function getCustomer(): ActiveQueryInterface - { - return $this->hasOne(Element::class, ['id', 'customerId']); - } -} diff --git a/src/records/Discount.php b/src/records/Discount.php deleted file mode 100644 index a6913b3928..0000000000 --- a/src/records/Discount.php +++ /dev/null @@ -1,112 +0,0 @@ - - * @since 2.0 - */ -class Discount extends ActiveRecord -{ - use StoreRecordTrait; - - public const TYPE_ORIGINAL_SALEPRICE = 'original'; - public const TYPE_DISCOUNTED_SALEPRICE = 'discounted'; - - public const CATEGORY_RELATIONSHIP_TYPE_SOURCE = 'sourceElement'; - public const CATEGORY_RELATIONSHIP_TYPE_TARGET = 'targetElement'; - public const CATEGORY_RELATIONSHIP_TYPE_BOTH = 'element'; - - public const APPLIED_TO_MATCHING_LINE_ITEMS = 'matchingLineItems'; - public const APPLIED_TO_ALL_LINE_ITEMS = 'allLineItems'; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::DISCOUNTS; - } - - /** - * @noinspection PhpUnused - */ - public function getDiscountPurchasables(): ActiveQueryInterface - { - return $this->hasMany(DiscountPurchasable::class, ['discountId' => 'id']); - } - - public function getDiscountCategories(): ActiveQueryInterface - { - return $this->hasMany(DiscountCategory::class, ['discountId' => 'id']); - } - - public function getGroups(): ActiveQueryInterface - { - return $this->hasMany(UserGroup::class, ['id' => 'discountId'])->via('discountUserGroups'); - } - - public function getPurchasables(): ActiveQueryInterface - { - return $this->hasMany(Purchasable::class, ['id' => 'discountId'])->via('discountPurchasables'); - } - - public function getCategories(): ActiveQueryInterface - { - return $this->hasMany(Category::class, ['id' => 'discountId'])->via('discountCategories'); - } -} diff --git a/src/records/DiscountCategory.php b/src/records/DiscountCategory.php deleted file mode 100644 index dcc2ff23d5..0000000000 --- a/src/records/DiscountCategory.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @since 2.0 - */ -class DiscountCategory extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::DISCOUNT_CATEGORIES; - } - - public function getDiscount(): ActiveQueryInterface - { - return $this->hasOne(Discount::class, ['id' => 'discountId']); - } - - public function getCategory(): ActiveQueryInterface - { - return $this->hasOne(Category::class, ['id' => 'categoryId']); - } -} diff --git a/src/records/DiscountPurchasable.php b/src/records/DiscountPurchasable.php deleted file mode 100644 index 7856dbe1c5..0000000000 --- a/src/records/DiscountPurchasable.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @since 2.0 - */ -class DiscountPurchasable extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::DISCOUNT_PURCHASABLES; - } - - public function getDiscount(): ActiveQueryInterface - { - return $this->hasOne(Discount::class, ['id' => 'discountId']); - } - - public function getPurchasable(): ActiveQueryInterface - { - return $this->hasOne(Purchasable::class, ['id' => 'purchasableId']); - } -} diff --git a/src/records/Donation.php b/src/records/Donation.php deleted file mode 100644 index 8ec72a1e25..0000000000 --- a/src/records/Donation.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @since 2.0 - */ -class Donation extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::DONATIONS; - } - - public function getElement(): ActiveQueryInterface - { - return $this->hasOne(Element::class, ['id', 'id']); - } -} diff --git a/src/records/Email.php b/src/records/Email.php deleted file mode 100644 index 0eca86e947..0000000000 --- a/src/records/Email.php +++ /dev/null @@ -1,53 +0,0 @@ - - * @since 2.0 - */ -class Email extends ActiveRecord -{ - use StoreRecordTrait; - - public const LOCALE_ORDER_LANGUAGE = 'orderLanguage'; - - public const TYPE_CUSTOMER = 'customer'; - public const TYPE_CUSTOM = 'custom'; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::EMAILS; - } -} diff --git a/src/records/EmailDiscountUse.php b/src/records/EmailDiscountUse.php deleted file mode 100644 index 0416392111..0000000000 --- a/src/records/EmailDiscountUse.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @since 2.0 - */ -class EmailDiscountUse extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::EMAIL_DISCOUNTUSES; - } - - public function getDiscount(): ActiveQueryInterface - { - return $this->hasOne(Discount::class, ['id', 'discountId']); - } -} diff --git a/src/records/Gateway.php b/src/records/Gateway.php deleted file mode 100644 index bfc269160d..0000000000 --- a/src/records/Gateway.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @since 2.0 - */ -class Gateway extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::GATEWAYS; - } -} diff --git a/src/records/InventoryItem.php b/src/records/InventoryItem.php deleted file mode 100644 index fc47048c47..0000000000 --- a/src/records/InventoryItem.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @since 5.0.0 - */ -class InventoryItem extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::INVENTORYITEMS; - } - - public function getPurchasable(): ActiveQueryInterface - { - return $this->hasOne(Purchasable::class, ['id' => 'purchasableId']); - } -} diff --git a/src/records/InventoryLocation.php b/src/records/InventoryLocation.php deleted file mode 100644 index 92b3778e09..0000000000 --- a/src/records/InventoryLocation.php +++ /dev/null @@ -1,36 +0,0 @@ - ['handle']], - ]; - } -} diff --git a/src/records/LineItem.php b/src/records/LineItem.php deleted file mode 100644 index b0f1b098ba..0000000000 --- a/src/records/LineItem.php +++ /dev/null @@ -1,89 +0,0 @@ - - * @since 2.0 - */ -class LineItem extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::LINEITEMS; - } - - public function getOrder(): ActiveQueryInterface - { - return $this->hasOne(Order::class, ['id' => 'orderId']); - } - - public function getPurchasable(): ActiveQueryInterface - { - return $this->hasOne(Element::class, ['id' => 'purchasableId']); - } - - public function getTaxCategory(): ActiveQueryInterface - { - return $this->hasOne(TaxCategory::class, ['id' => 'taxCategoryId']); - } - - public function getShippingCategory(): ActiveQueryInterface - { - return $this->hasOne(ShippingCategory::class, ['id' => 'shippingCategoryId']); - } - - public function getLineItemStatus(): ActiveQueryInterface - { - return $this->hasOne(LineItemStatus::class, ['id' => 'lineItemStatusId']); - } -} diff --git a/src/records/LineItemStatus.php b/src/records/LineItemStatus.php deleted file mode 100644 index 3f8188d5ac..0000000000 --- a/src/records/LineItemStatus.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @since 2.0 - */ -class LineItemStatus extends ActiveRecord -{ - use StoreRecordTrait; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::LINEITEMSTATUSES; - } -} diff --git a/src/records/Order.php b/src/records/Order.php deleted file mode 100644 index 3927c40a61..0000000000 --- a/src/records/Order.php +++ /dev/null @@ -1,148 +0,0 @@ - - * @since 2.0 - */ -class Order extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::ORDERS; - } - - public function getLineItems(): ActiveQueryInterface - { - return $this->hasMany(LineItem::class, ['orderId' => 'id']); - } - - public function getTransactions(): ActiveQueryInterface - { - return $this->hasMany(Transaction::class, ['orderId' => 'id']); - } - - public function getHistories(): ActiveQueryInterface - { - return $this->hasMany(OrderHistory::class, ['orderId' => 'id']); - } - - public function getBillingAddress(): ActiveQueryInterface - { - return $this->hasOne(Element::class, ['id' => 'billingAddressId']); - } - - public function getShippingAddress(): ActiveQueryInterface - { - return $this->hasOne(Element::class, ['id' => 'shippingAddressId']); - } - - public function getDiscount(): ActiveQueryInterface - { - return $this->hasOne(Discount::class, ['code' => 'couponCode']); - } - - public function getGateway(): ActiveQueryInterface - { - return $this->hasOne(Gateway::class, ['id' => 'gatewayId']); - } - - public function getPaymentSource(): ActiveQueryInterface - { - return $this->hasOne(PaymentSource::class, ['id' => 'paymentSourceId']); - } - - public function getCustomer(): ActiveQueryInterface - { - return $this->hasOne(User::class, ['id' => 'customerId']); - } - - public function getElement(): ActiveQueryInterface - { - return $this->hasOne(Element::class, ['id' => 'id']); - } - - public function getOrderStatus(): ActiveQueryInterface - { - return $this->hasOne(OrderStatus::class, ['id' => 'orderStatusId']); - } -} diff --git a/src/records/OrderAdjustment.php b/src/records/OrderAdjustment.php deleted file mode 100644 index da0c165813..0000000000 --- a/src/records/OrderAdjustment.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @since 2.0 - */ -class OrderAdjustment extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::ORDERADJUSTMENTS; - } - - public function getOrder(): ActiveQueryInterface - { - return $this->hasOne(Order::class, ['id' => 'orderId']); - } -} diff --git a/src/records/OrderHistory.php b/src/records/OrderHistory.php deleted file mode 100644 index 6ff64e1f2c..0000000000 --- a/src/records/OrderHistory.php +++ /dev/null @@ -1,72 +0,0 @@ - - * @since 2.0 - */ -class OrderHistory extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::ORDERHISTORIES; - } - - public function getOrder(): ActiveQueryInterface - { - return $this->hasOne(Order::class, ['id' => 'orderId']); - } - - /** - * @noinspection PhpUnused - */ - public function getPrevStatus(): ActiveQueryInterface - { - return $this->hasOne(OrderStatus::class, ['id' => 'prevStatusId']); - } - - /** - * @noinspection PhpUnused - */ - public function getNewStatus(): ActiveQueryInterface - { - return $this->hasOne(OrderStatus::class, ['id' => 'newStatusId']); - } - - /** - * @return ActiveQueryInterface - */ - public function getUser(): ActiveQueryInterface - { - return $this->hasOne(User::class, ['id' => 'userId']); - } -} diff --git a/src/records/OrderNotice.php b/src/records/OrderNotice.php deleted file mode 100644 index 2474956b38..0000000000 --- a/src/records/OrderNotice.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @since 3.3 - */ -class OrderNotice extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::ORDERNOTICES; - } - - public function getOrder(): ActiveQueryInterface - { - return $this->hasOne(Order::class, ['id' => 'orderId']); - } -} diff --git a/src/records/OrderStatus.php b/src/records/OrderStatus.php deleted file mode 100644 index e8567c4928..0000000000 --- a/src/records/OrderStatus.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @since 2.0 - */ -class OrderStatus extends ActiveRecord -{ - use SoftDeleteTrait; - use StoreRecordTrait; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::ORDERSTATUSES; - } - - /** - * @throws InvalidConfigException - */ - public function getEmails(): ActiveQueryInterface - { - return $this->hasMany(Email::class, ['id' => 'emailId'])->viaTable(Table::ORDERSTATUS_EMAILS, ['orderStatusId' => 'id']); - } -} diff --git a/src/records/OrderStatusEmail.php b/src/records/OrderStatusEmail.php deleted file mode 100644 index 7f68eb3381..0000000000 --- a/src/records/OrderStatusEmail.php +++ /dev/null @@ -1,43 +0,0 @@ - - * @since 2.0 - */ -class OrderStatusEmail extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::ORDERSTATUS_EMAILS; - } - - public function getOrderStatus(): ActiveQueryInterface - { - return $this->hasOne(OrderStatus::class, ['id' => 'orderStatusId']); - } - - public function getEmail(): ActiveQueryInterface - { - return $this->hasOne(Email::class, ['id' => 'emailId']); - } -} diff --git a/src/records/PaymentCurrency.php b/src/records/PaymentCurrency.php deleted file mode 100644 index 8e9a960cf1..0000000000 --- a/src/records/PaymentCurrency.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @since 2.0 - */ -class PaymentCurrency extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::PAYMENTCURRENCIES; - } -} diff --git a/src/records/PaymentSource.php b/src/records/PaymentSource.php deleted file mode 100644 index 868d3f6c14..0000000000 --- a/src/records/PaymentSource.php +++ /dev/null @@ -1,59 +0,0 @@ - - * @since 2.0 - */ -class PaymentSource extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::PAYMENTSOURCES; - } - - /** - * Return the payment source's gateway - * - * @return ActiveQueryInterface The relational query object. - */ - public function getGateway(): ActiveQueryInterface - { - return $this->hasOne(Gateway::class, ['id' => 'gatewayId']); - } - - - /** - * Return the payment source's owner customer/user. - * - * @return ActiveQueryInterface The relational query object. - */ - public function getUser(): ActiveQueryInterface - { - return $this->hasOne(Element::class, ['id' => 'customerId']); - } -} diff --git a/src/records/Pdf.php b/src/records/Pdf.php deleted file mode 100644 index 551fef69dd..0000000000 --- a/src/records/Pdf.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @since 3.2 - */ -class Pdf extends ActiveRecord -{ - use StoreRecordTrait; - - public const LOCALE_ORDER_LANGUAGE = 'orderLanguage'; - - /** - * @since 5.0.0 - */ - public const PAPER_ORIENTATION_PORTRAIT = 'portrait'; - - /** - * @since 5.0.0 - */ - public const PAPER_ORIENTATION_LANDSCAPE = 'landscape'; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::PDFS; - } -} diff --git a/src/records/Plan.php b/src/records/Plan.php deleted file mode 100644 index eaa69caaff..0000000000 --- a/src/records/Plan.php +++ /dev/null @@ -1,51 +0,0 @@ - - * @since 2.0 - */ -class Plan extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::PLANS; - } - - /** - * Return the subscription plan's gateway - * - * @return ActiveQueryInterface The relational query object. - */ - public function getGateway(): ActiveQueryInterface - { - return $this->hasOne(Gateway::class, ['gatewayId' => 'id']); - } -} diff --git a/src/records/Product.php b/src/records/Product.php deleted file mode 100644 index e4a2b26cfa..0000000000 --- a/src/records/Product.php +++ /dev/null @@ -1,60 +0,0 @@ - - * @since 2.0 - */ -class Product extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::PRODUCTS; - } - - public function getVariants(): ActiveQueryInterface - { - return $this->hasMany(Variant::class, ['productId' => 'id']); - } - - public function getElement(): ActiveQueryInterface - { - return $this->hasOne(Element::class, ['id' => 'id']); - } - - public function getType(): ActiveQueryInterface - { - return $this->hasOne(ProductType::class, ['id' => 'productTypeId']); - } -} diff --git a/src/records/ProductType.php b/src/records/ProductType.php deleted file mode 100644 index bbaa63efb3..0000000000 --- a/src/records/ProductType.php +++ /dev/null @@ -1,100 +0,0 @@ - - * @since 2.0 - */ -class ProductType extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::PRODUCTTYPES; - } - - public function getProductTypesShippingCategories(): ActiveQueryInterface - { - return $this->hasMany(ProductTypeShippingCategory::class, ['productTypeId' => 'id']); - } - - public function getShippingCategories(): ActiveQueryInterface - { - return $this->hasMany(ShippingCategory::class, ['id' => 'shippingCategoryId']) - ->via('productTypesShippingCategories'); - } - - public function getProductTypesTaxCategories(): ActiveQueryInterface - { - return $this->hasMany(ProductTypeTaxCategory::class, ['productTypeId' => 'id']); - } - - public function getTaxCategories(): ActiveQueryInterface - { - return $this->hasMany(TaxCategory::class, ['id' => 'taxCategoryId']) - ->via('productTypesTaxCategories'); - } - - public function getFieldLayout(): ActiveQueryInterface - { - return $this->hasOne(FieldLayout::class, ['id' => 'fieldLayoutId']); - } - - /** - * @noinspection PhpUnused - */ - public function getVariantFieldLayout(): ActiveQueryInterface - { - return $this->hasOne(FieldLayout::class, ['id' => 'variantFieldLayoutId']); - } -} diff --git a/src/records/ProductTypeShippingCategory.php b/src/records/ProductTypeShippingCategory.php deleted file mode 100644 index 4633dfd01d..0000000000 --- a/src/records/ProductTypeShippingCategory.php +++ /dev/null @@ -1,43 +0,0 @@ - - * @since 2.0 - */ -class ProductTypeShippingCategory extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::PRODUCTTYPES_SHIPPINGCATEGORIES; - } - - public function getProductType(): ActiveQueryInterface - { - return $this->hasOne(ProductType::class, ['id', 'productTypeId']); - } - - public function getShippingCategory(): ActiveQueryInterface - { - return $this->hasOne(ShippingCategory::class, ['id', 'shippingCategoryId']); - } -} diff --git a/src/records/ProductTypeSite.php b/src/records/ProductTypeSite.php deleted file mode 100644 index 08191f52d0..0000000000 --- a/src/records/ProductTypeSite.php +++ /dev/null @@ -1,49 +0,0 @@ - - * @since 2.0 - */ -class ProductTypeSite extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::PRODUCTTYPES_SITES; - } - - public function getProductType(): ActiveQueryInterface - { - return $this->hasOne(ProductType::class, ['id', 'productTypeId']); - } - - public function getSite(): ActiveQueryInterface - { - return $this->hasOne(Site::class, ['id', 'siteId']); - } -} diff --git a/src/records/ProductTypeTaxCategory.php b/src/records/ProductTypeTaxCategory.php deleted file mode 100644 index 14d752072a..0000000000 --- a/src/records/ProductTypeTaxCategory.php +++ /dev/null @@ -1,43 +0,0 @@ - - * @since 2.0 - */ -class ProductTypeTaxCategory extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::PRODUCTTYPES_TAXCATEGORIES; - } - - public function getProductType(): ActiveQueryInterface - { - return $this->hasOne(ProductType::class, ['id', 'productTypeId']); - } - - public function getTaxCategory(): ActiveQueryInterface - { - return $this->hasOne(TaxCategory::class, ['id', 'taxCategoryId']); - } -} diff --git a/src/records/Purchasable.php b/src/records/Purchasable.php deleted file mode 100644 index 88de1ff67e..0000000000 --- a/src/records/Purchasable.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @since 2.0 - */ -class Purchasable extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::PURCHASABLES; - } - - public static function find(): \craft\db\ActiveQuery - { - return parent::find() - ->innerJoinWith(['element element']) - ->where(['element.dateDeleted' => null]); - } - - public static function findWithTrashed(): ActiveQuery - { - return static::find()->where([]); - } - - public static function findTrashed(): ActiveQuery - { - return static::find()->where(['not', ['element.dateDeleted' => null]]); - } - - public function getElement(): ActiveQueryInterface - { - return $this->hasOne(Element::class, ['id' => 'id']); - } -} diff --git a/src/records/PurchasableStore.php b/src/records/PurchasableStore.php deleted file mode 100644 index 696d70fa25..0000000000 --- a/src/records/PurchasableStore.php +++ /dev/null @@ -1,54 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasableStore extends ActiveRecord -{ - use StoreRecordTrait; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::PURCHASABLES_STORES; - } - - /** - * @return ActiveQueryInterface - */ - public function getPurchasable(): ActiveQueryInterface - { - return $this->hasOne(Purchasable::class, ['id' => 'purchasableId']); - } -} diff --git a/src/records/Sale.php b/src/records/Sale.php deleted file mode 100644 index de9405a0f6..0000000000 --- a/src/records/Sale.php +++ /dev/null @@ -1,83 +0,0 @@ - - * @since 2.0 - */ -class Sale extends ActiveRecord -{ - public const APPLY_BY_PERCENT = 'byPercent'; - public const APPLY_BY_FLAT = 'byFlat'; - public const APPLY_TO_PERCENT = 'toPercent'; - public const APPLY_TO_FLAT = 'toFlat'; - - public const CATEGORY_RELATIONSHIP_TYPE_SOURCE = 'sourceElement'; - public const CATEGORY_RELATIONSHIP_TYPE_TARGET = 'targetElement'; - public const CATEGORY_RELATIONSHIP_TYPE_BOTH = 'element'; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::SALES; - } - - /** - * @throws InvalidConfigException - */ - public function getGroups(): ActiveQueryInterface - { - return $this->hasMany(UserGroup::class, ['id' => 'userGroupId'])->viaTable(Table::SALE_USERGROUPS, ['saleId' => 'id']); - } - - /** - * @throws InvalidConfigException - */ - public function getPurchasables(): ActiveQueryInterface - { - return $this->hasMany(Purchasable::class, ['id' => 'purchasableId'])->viaTable(Table::SALE_PURCHASABLES, ['saleId' => 'id']); - } - - /** - * @throws InvalidConfigException - */ - public function getCategories(): ActiveQueryInterface - { - return $this->hasMany(Category::class, ['id' => 'categoryId'])->viaTable(Table::SALE_CATEGORIES, ['saleId' => 'id']); - } -} diff --git a/src/records/SaleCategory.php b/src/records/SaleCategory.php deleted file mode 100644 index e514c520be..0000000000 --- a/src/records/SaleCategory.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @since 2.0 - */ -class SaleCategory extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::SALE_CATEGORIES; - } - - /** - * @inheritdoc - */ - public function rules(): array - { - return [ - [['saleId', 'categoryId'], 'unique', 'targetAttribute' => ['saleId', 'categoryId']], - ]; - } - - public function getSale(): ActiveQueryInterface - { - return $this->hasOne(Sale::class, ['saleId' => 'id']); - } - - public function getCategory(): ActiveQueryInterface - { - return $this->hasOne(Category::class, ['saleId' => 'id']); - } -} diff --git a/src/records/SalePurchasable.php b/src/records/SalePurchasable.php deleted file mode 100644 index dedc194183..0000000000 --- a/src/records/SalePurchasable.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @since 2.0 - */ -class SalePurchasable extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::SALE_PURCHASABLES; - } - - /** - * @inheritdoc - */ - public function rules(): array - { - return [ - [['saleId', 'purchasableId'], 'unique', 'targetAttribute' => ['saleId', 'purchasableId']], - ]; - } - - public function getSale(): ActiveQueryInterface - { - return $this->hasOne(Sale::class, ['saleId' => 'id']); - } - - public function getPurchasable(): ActiveQueryInterface - { - return $this->hasOne(Purchasable::class, ['saleId' => 'id']); - } -} diff --git a/src/records/SaleUserGroup.php b/src/records/SaleUserGroup.php deleted file mode 100644 index 8cde2b3239..0000000000 --- a/src/records/SaleUserGroup.php +++ /dev/null @@ -1,58 +0,0 @@ - - * @since 2.0 - */ -class SaleUserGroup extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::SALE_USERGROUPS; - } - - /** - * @inheritdoc - */ - public function rules(): array - { - return [ - [['saleId', 'userGroupId'], 'unique', 'targetAttribute' => ['saleId', 'userGroupId']], - ]; - } - - public function getSale(): ActiveQueryInterface - { - return $this->hasOne(Sale::class, ['saleId' => 'id']); - } - - /** - * @noinspection PhpUnused - */ - public function getUserGroup(): ActiveQueryInterface - { - return $this->hasOne(UserGroup::class, ['saleId' => 'id']); - } -} diff --git a/src/records/ShippingCategory.php b/src/records/ShippingCategory.php deleted file mode 100644 index 15bd3bb7e6..0000000000 --- a/src/records/ShippingCategory.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @since 2.0 - */ -class ShippingCategory extends ActiveRecord -{ - use SoftDeleteTrait; - use StoreRecordTrait; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::SHIPPINGCATEGORIES; - } -} diff --git a/src/records/ShippingMethod.php b/src/records/ShippingMethod.php deleted file mode 100644 index 43f284bfd3..0000000000 --- a/src/records/ShippingMethod.php +++ /dev/null @@ -1,47 +0,0 @@ - - * @since 2.0 - */ -class ShippingMethod extends ActiveRecord -{ - use StoreRecordTrait; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::SHIPPINGMETHODS; - } - - public function getRules(): ActiveQueryInterface - { - return $this->hasMany(ShippingRule::class, ['shippingMethodId' => 'id']); - } -} diff --git a/src/records/ShippingRule.php b/src/records/ShippingRule.php deleted file mode 100644 index 8e9cce2df3..0000000000 --- a/src/records/ShippingRule.php +++ /dev/null @@ -1,60 +0,0 @@ - - * @since 2.0 - */ -class ShippingRule extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::SHIPPINGRULES; - } - - /** - * @inheritdoc - */ - public function rules(): array - { - return [ - [['name'], 'required'], - ]; - } - - public function getMethod(): ActiveQueryInterface - { - return $this->hasOne(ShippingZone::class, ['id' => 'shippingMethodId']); - } -} diff --git a/src/records/ShippingRuleCategory.php b/src/records/ShippingRuleCategory.php deleted file mode 100644 index a8e765a354..0000000000 --- a/src/records/ShippingRuleCategory.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @since 2.0 - */ -class ShippingRuleCategory extends ActiveRecord -{ - public const CONDITION_ALLOW = 'allow'; - public const CONDITION_DISALLOW = 'disallow'; - public const CONDITION_REQUIRE = 'require'; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::SHIPPINGRULE_CATEGORIES; - } - - /** - * @noinspection PhpUnused - */ - public function getShippingRule(): ActiveQueryInterface - { - return $this->hasOne(ShippingRule::class, ['id' => 'shippingRuleId']); - } - - public function getShippingCategory(): ActiveQueryInterface - { - return $this->hasOne(ShippingCategory::class, ['id' => 'shippingCategoryId']); - } -} diff --git a/src/records/ShippingZone.php b/src/records/ShippingZone.php deleted file mode 100644 index 2b9d8b3c0e..0000000000 --- a/src/records/ShippingZone.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @since 2.0 - */ -class ShippingZone extends ActiveRecord -{ - use StoreRecordTrait; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::SHIPPINGZONES; - } -} diff --git a/src/records/SiteStore.php b/src/records/SiteStore.php deleted file mode 100644 index 8243d1fee0..0000000000 --- a/src/records/SiteStore.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @since 4.0 - */ -class SiteStore extends ActiveRecord -{ - use StoreRecordTrait; - - /** - * @inheritDoc - */ - public static function primaryKey(): array - { - return ['siteId']; - } - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::SITESTORES; - } -} diff --git a/src/records/Store.php b/src/records/Store.php deleted file mode 100644 index eeb2735eee..0000000000 --- a/src/records/Store.php +++ /dev/null @@ -1,48 +0,0 @@ - - * @since 4.0 - */ -class Store extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::STORES; - } -} diff --git a/src/records/StoreSettings.php b/src/records/StoreSettings.php deleted file mode 100644 index 5f24dc8ffc..0000000000 --- a/src/records/StoreSettings.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @since 4.0 - */ -class StoreSettings extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::STORESETTINGS; - } - - /** - * Returns the store's location - * - * @return ActiveQueryInterface The relational query object. - */ - public function getStoreLocation(): ActiveQueryInterface - { - return $this->hasOne(Address::class, ['id' => 'locationAddressId']); - } -} diff --git a/src/records/Subscription.php b/src/records/Subscription.php deleted file mode 100644 index b89dc1f440..0000000000 --- a/src/records/Subscription.php +++ /dev/null @@ -1,81 +0,0 @@ - - * @since 2.0 - */ -class Subscription extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::SUBSCRIPTIONS; - } - - /** - * Return the subscription's gateway - * - * @return ActiveQueryInterface The relational query object. - */ - public function getGateway(): ActiveQueryInterface - { - return $this->hasOne(Gateway::class, ['gatewayId' => 'id']); - } - - /** - * Return the subscription's user - * - * @return ActiveQueryInterface The relational query object. - */ - public function getUser(): ActiveQueryInterface - { - return $this->hasOne(User::class, ['userId' => 'id']); - } - - /** - * Return the subscription's plan - * - * @return ActiveQueryInterface The relational query object. - */ - public function getPlan(): ActiveQueryInterface - { - return $this->hasOne(Plan::class, ['planId' => 'id']); - } -} diff --git a/src/records/TaxCategory.php b/src/records/TaxCategory.php deleted file mode 100644 index 5ed1d3c214..0000000000 --- a/src/records/TaxCategory.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @since 2.0 - */ -class TaxCategory extends ActiveRecord -{ - use SoftDeleteTrait; - - public static function tableName(): string - { - return Table::TAXCATEGORIES; - } - - /** - * @inheritdoc - */ - public function rules(): array - { - return [ - [['handle'], 'required'], - ]; - } -} diff --git a/src/records/TaxRate.php b/src/records/TaxRate.php deleted file mode 100644 index 43526cea1b..0000000000 --- a/src/records/TaxRate.php +++ /dev/null @@ -1,100 +0,0 @@ - - * @since 2.0 - */ -class TaxRate extends ActiveRecord -{ - use StoreRecordTrait; - - /** - * @var string Tax subject is line item price. - */ - public const TAXABLE_PURCHASABLE = 'purchasable'; - - /** - * @var string Tax subject is line item price. - */ - public const TAXABLE_PRICE = 'price'; - - /** - * @var string Tax subject is line item shipping cost. - */ - public const TAXABLE_SHIPPING = 'shipping'; - - /** - * @var string Tax subject is line item price and shipping cost. - */ - public const TAXABLE_PRICE_SHIPPING = 'price_shipping'; - - /** - * @var string Tax subject is order total shipping cost. - */ - public const TAXABLE_ORDER_TOTAL_SHIPPING = 'order_total_shipping'; - - /** - * @var string Tax subject is order total price. - */ - public const TAXABLE_ORDER_TOTAL_PRICE = 'order_total_price'; - - /** - * @var array Order-specific tax subject options. - */ - public const ORDER_TAXABALES = [ - self::TAXABLE_ORDER_TOTAL_PRICE, - self::TAXABLE_ORDER_TOTAL_SHIPPING, - ]; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::TAXRATES; - } - - /** - * @noinspection PhpUnused - */ - public function getTaxZone(): ActiveQueryInterface - { - return $this->hasOne(TaxZone::class, ['id' => 'taxZoneId']); - } - - public function getTaxCategory(): ActiveQueryInterface - { - return $this->hasOne(TaxCategory::class, ['id' => 'taxCategoryId']); - } -} diff --git a/src/records/TaxZone.php b/src/records/TaxZone.php deleted file mode 100644 index e8acde36a5..0000000000 --- a/src/records/TaxZone.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @since 2.0 - */ -class TaxZone extends ActiveRecord -{ - use StoreRecordTrait; - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::TAXZONES; - } -} diff --git a/src/records/Transaction.php b/src/records/Transaction.php deleted file mode 100644 index d35e45d761..0000000000 --- a/src/records/Transaction.php +++ /dev/null @@ -1,89 +0,0 @@ - - * @since 2.0 - */ -class Transaction extends ActiveRecord -{ - public const TYPE_AUTHORIZE = 'authorize'; - public const TYPE_CAPTURE = 'capture'; - public const TYPE_PURCHASE = 'purchase'; - public const TYPE_REFUND = 'refund'; - public const STATUS_PENDING = 'pending'; - public const STATUS_REDIRECT = 'redirect'; - public const STATUS_PROCESSING = 'processing'; - public const STATUS_SUCCESS = 'success'; - public const STATUS_FAILED = 'failed'; - - - /** - * @var int $total - */ - public int $total = 0; - - - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::TRANSACTIONS; - } - - public function getParent(): ActiveQueryInterface - { - return $this->hasOne(self::class, ['id' => 'parentId']); - } - - public function getGateway(): ActiveQueryInterface - { - return $this->hasOne(Gateway::class, ['id' => 'gatewayId']); - } - - public function getOrder(): ActiveQueryInterface - { - return $this->hasOne(Order::class, ['id' => 'orderId']); - } - - public function getUser(): ActiveQueryInterface - { - return $this->hasOne(User::class, ['id' => 'userId']); - } -} diff --git a/src/records/Transfer.php b/src/records/Transfer.php deleted file mode 100644 index 59e87c2d39..0000000000 --- a/src/records/Transfer.php +++ /dev/null @@ -1,21 +0,0 @@ - - * @since 2.0 - */ -class Variant extends ActiveRecord -{ - /** - * @inheritdoc - */ - public static function tableName(): string - { - return Table::VARIANTS; - } - - public function getProduct(): ActiveQueryInterface - { - return $this->hasOne(Product::class, ['id', 'productId']); - } - - public function getElement(): ActiveQueryInterface - { - return $this->hasOne(Element::class, ['id', 'id']); - } -} diff --git a/src/services/Carts.php b/src/services/Carts.php deleted file mode 100644 index e5a0e7c21c..0000000000 --- a/src/services/Carts.php +++ /dev/null @@ -1,673 +0,0 @@ - - * @since 2.0 - */ -class Carts extends Component -{ - /** - * @event CartPurgeEvent The event that is triggered before the carts are purged. - * - * This example modifies the query to only purge carts with a total price of 0. - * You can also set the `isValid` property to `false` to prevent the carts from being purged. - * - * ```php - * use craft\commerce\events\CartPurgeEvent; - * use craft\commerce\services\Carts; - * use yii\base\Event; - * - * Event::on( - * Carts::class, - * Carts::EVENT_BEFORE_PURGE_INACTIVE_CARTS, - * function(CartPurgeEvent $event) { - * $event->inactiveCartsQuery = $event->inactiveCartsQuery->andWhere(['totalPrice' => 0]); - * } - * ); - * ``` - */ - public const EVENT_BEFORE_PURGE_INACTIVE_CARTS = 'beforePurgeInactiveCarts'; - - /** - * @var array The configuration of the cart cookie. - * @since 4.0.0 - * @see setSessionCartNumber() - */ - public array $cartCookie = []; - - /** - * @var int The expiration duration of the cart cookie, in seconds. (Defaults to one year.) - * @since 4.0.0 - * @see setSessionCartNumber() - */ - public int $cartCookieDuration = 31536000; - - /** - * @var Order|null - */ - private ?Order $_cart = null; - - /** - * @var string|null The current cart number - */ - private string|false|null $_cartNumber = null; - - /** - * Useful for debugging how many times the cart is being requested during a request. - * - * @var int The number of times the cart was requested. - */ - private int $_getCartCount = 0; - - /** - * Initializes the cart service - * - * @return void - * @throws MissingComponentException - */ - public function init() - { - parent::init(); - - $currentStore = Plugin::getInstance()->getStores()->getCurrentStore(); - - // Complete the cart cookie config - if (!isset($this->cartCookie['name'])) { - $this->cartCookie['name'] = md5(sprintf('Craft.%s.%s.%s', self::class, Craft::$app->id, $currentStore->handle)) . '_commerce_cart'; - } - - $request = Craft::$app->getRequest(); - if (!$request->getIsConsoleRequest()) { - $this->cartCookie = Craft::cookieConfig($this->cartCookie); - - $session = Craft::$app->getSession(); - - // Also check pre Commerce 4.0 for a cart number in the session just in case. - if (($session->getHasSessionId() || $session->getIsActive()) && $session->has('commerce_cart')) { - $this->setSessionCartNumber($session->get('commerce_cart')); - $session->remove('commerce_cart'); - } - } - } - - /** - * Get the current cart for this session. - * - * @param bool $forceSave Force the cart. - * @throws ElementNotFoundException - * @throws Exception - * @throws Throwable - */ - public function getCart(bool $forceSave = false): Order - { - $this->loadCookie(); // @TODO Audit other public runtime entry points (e.g. forgetCart, restorePreviousCartForCurrentUser) to see if they also need loadCookie() called first - - $this->_getCartCount++; //useful when debugging - $currentUser = Craft::$app->getUser()->getIdentity(); - - // If there is no cart set for this request, and we can't get a cart from session, create one. - if (!isset($this->_cart) && !$this->_cart = $this->_getCart()) { - $cartAttributes = [ - 'number' => $this->getSessionCartNumber(), - 'orderSiteId' => Craft::$app->getSites()->getCurrentSite()->id, - 'storeId' => Plugin::getInstance()->getStores()->getCurrentStore()->id, - ]; - - if ($currentUser) { - $cartAttributes['customer'] = $currentUser; // Will ensure the email is also set - } - - $this->_cart = Craft::createObject([ - 'class' => Order::class, - 'attributes' => $cartAttributes, - ]); - } elseif ($this->_cart->orderSiteId != Craft::$app->getSites()->getCurrentSite()->id) { - $this->_cart->orderSiteId = Craft::$app->getSites()->getCurrentSite()->id; - $forceSave = true; - } - - // Just in case the cart go put into a non all recalculation mode - if ($this->_cart->getRecalculationMode() !== Order::RECALCULATION_MODE_ALL) { - $this->_cart->setRecalculationMode(Order::RECALCULATION_MODE_ALL); - $forceSave = true; - } - - $autoSetAddresses = false; - // We only want to call autoSetAddresses() if we have a authed cart customer - if ($currentUser && $currentUser->id == $this->_cart->customerId) { - $autoSetAddresses = $this->_cart->autoSetAddresses(); - } - $autoSetShippingMethod = $this->_cart->autoSetShippingMethod(); - $autoSetPaymentSource = $this->_cart->autoSetPaymentSource(); - if ($autoSetAddresses || $autoSetShippingMethod || $autoSetPaymentSource) { - $forceSave = true; - } - - // Ensure the session knows what the current cart is. - $this->setSessionCartNumber($this->_cart->number); - - // Track the things that might change on this cart - $originalIp = $this->_cart->lastIp; - $originalOrderLanguage = $this->_cart->orderLanguage; - $originalSiteId = $this->_cart->orderSiteId; - $originalPaymentCurrency = $this->_cart->paymentCurrency; - $originalUserId = $this->_cart->getCustomerId(); - - // These values should always be kept up to date when a cart is retrieved from session. - $this->_cart->lastIp = Craft::$app->getRequest()->getUserIP(); - $this->_cart->orderLanguage = Craft::$app->language; - $this->_cart->orderSiteId = Craft::$app->getSites()->getHasCurrentSite() ? Craft::$app->getSites()->getCurrentSite()->id : Craft::$app->getSites()->getPrimarySite()->id; - $this->_cart->paymentCurrency = $this->_getCartPaymentCurrencyIso(); - $this->_cart->origin = Order::ORIGIN_WEB; - - // Switch the cart customer if needed - if ($currentUser && ($this->_cart->getCustomer() === null || ($currentUser->email && $currentUser->email !== $this->_cart->getEmail()))) { - $this->_cart->setCustomer($currentUser); - } - - $hasIpChanged = $originalIp != $this->_cart->lastIp; - $hasOrderLanguageChanged = $originalOrderLanguage != $this->_cart->orderLanguage; - $hasOrderSiteIdChanged = $originalSiteId != $this->_cart->orderSiteId; - $hasPaymentCurrencyChanged = $originalPaymentCurrency != $this->_cart->paymentCurrency; - $hasUserChanged = $originalUserId != $this->_cart->getCustomerId(); - - $hasSomethingChangedOnCart = ($hasIpChanged || $hasOrderLanguageChanged || $hasUserChanged || $hasPaymentCurrencyChanged || $hasOrderSiteIdChanged); - - // If the cart has already been saved (has an ID), then only save if something else changed. - if (($this->_cart->id && $hasSomethingChangedOnCart) || $forceSave) { - Craft::$app->getElements()->saveElement($this->_cart, false); - } - - return $this->_cart; - } - - /** - * Returns the existing cart for this session without creating one, setting cookies, or touching the session. - * Returns null if no cart cookie is present or no matching cart exists. - * - * @since 5.7.0 - */ - public function peekCart(): ?Order - { - if (isset($this->_cart)) { - return $this->_cart; - } - - if ($this->_cartNumber === false) { - return null; - } - - if (!$this->_cartNumber) { - $cookieNumber = Craft::$app->getRequest()->getCookies()->getValue($this->cartCookie['name'], false); - if (!$cookieNumber) { - return null; - } - $this->_cartNumber = $cookieNumber; - } - - /** @var Order|null $cart */ - $cart = Order::find() - ->number($this->_cartNumber) - ->storeId(Plugin::getInstance()->getStores()->getCurrentStore()->id) - ->isCompleted(false) - ->trashed(false) - ->one(); - - if (!$cart) { - return null; - } - - // Don't return a cart that belongs to a credentialed user who isn't currently logged in - // as that user, unless this session has been authorized to use it (e.g. loaded via a valid - // load-cart token). Mirrors the privacy check in _getCart(), but without forgetting the cart - // (which would set a Set-Cookie header and defeat the purpose of this method). - $cartCustomer = $cart->getCustomer(); - if ($cartCustomer && $cartCustomer->getIsCredentialed()) { - $authorizedForCredentialedCart = Craft::$app->getSession()->get('commerce:anonymousCartWithCredentialedCustomer:' . $cart->number, false); - if (!$authorizedForCredentialedCart) { - $currentUser = Craft::$app->getUser()->getIdentity(); - if (!$currentUser || $currentUser->id != $cartCustomer->id) { - return null; - } - } - } - - $this->_cart = $cart; - return $this->_cart; - } - - /** - * Get the current cart for this session. - */ - private function _getCart(): ?Order - { - $number = $this->getSessionCartNumber(); - /** @var Order|null $cart */ - $cart = Order::find() - ->withLineItems() - ->withAdjustments() - ->number($number) - ->storeId(Plugin::getInstance()->getStores()->getCurrentStore()->id) - ->trashed(null) - ->status(null) - ->one(); - - // If the cart is already completed or trashed, forget the cart and start again. - if ($cart && ($cart->isCompleted || $cart->trashed)) { - $this->forgetCart(); - return null; - } - - $currentUser = Craft::$app->getUser()->getIdentity(); - - $cartCustomer = $cart?->getCustomer(); - - // Is this session authorized to use a cart that belongs to a credentialed user? This is the - // case when an anonymous user submitted the credentialed user's email to the cart (see - // CartController::actionUpdate()), or when the cart was loaded via a valid load-cart token - // (see CartController::actionLoadCart()). - $authorizedForCredentialedCart = $cart && Craft::$app->getSession()->get('commerce:anonymousCartWithCredentialedCustomer:' . $cart->number, false); - - if ($cart && $cartCustomer && $cartCustomer->getIsCredentialed() && - !$authorizedForCredentialedCart && - ( - // Forget cart if they are not logged-in. - !$currentUser - || - // Forget cart if the logged-in user is not the same as the cart customer. - $currentUser->id != $cartCustomer->id - ) - ) { - $this->forgetCart(); - return null; - } - - return $cart; - } - - /** - * Forgets the cart in the current session. - */ - public function forgetCart(): void - { - $this->_cart = null; - // Force a new cart number to be generated when next requested. - $this->_cartNumber = false; - if (!Craft::$app->getRequest()->getIsConsoleRequest()) { - $cookie = Craft::createObject(array_merge($this->cartCookie, [ - 'class' => Cookie::class, - ])); - - Craft::$app->getResponse()->getCookies()->remove($cookie, true); - } - } - - /** - * Generates a new random cart number and returns it. - * - * @since 2.0 - */ - public function generateCartNumber(): string - { - return bin2hex(random_bytes(16)); - } - - /** - * Calculates the date of the active cart duration edge. - * - * @throws \Exception - * @since 2.2 - */ - public function getActiveCartEdgeDuration(): string - { - $edge = new DateTime(); - $activeCartDuration = ConfigHelper::durationInSeconds(Plugin::getInstance()->getSettings()->activeCartDuration); - $interval = DateTimeHelper::secondsToInterval($activeCartDuration); - $edge->sub($interval); - return $edge->format(DateTime::ATOM); - } - - /** - * @since 3.1 - * @deprecated in 4.0.0. The cookie name is available via [[$cartCookie]] `['name']`. - */ - public function getCartName(): string - { - return $this->cartCookie['name']; - } - - /** - * Returns whether there is a cart number in the session. - * - * @throws MissingComponentException - * @since 2.1.11 - */ - public function getHasSessionCartNumber(): bool - { - if ($this->_cartNumber === false) { - return false; - } - - if ($this->_cartNumber === null) { - $request = Craft::$app->getRequest(); - $requestCookies = $request->getCookies(); - - return $requestCookies->getValue($this->cartCookie['name'], false) !== false; - } - - return true; - } - - /** - * Get the session cart number or generates one if none exists. - * - */ - protected function getSessionCartNumber(): string - { - if (!Craft::$app->getRequest()->getIsConsoleRequest()) { - $request = Craft::$app->getRequest(); - $requestCookies = $request->getCookies(); - - // Only try to retrieve the cart number from the cookie if `_cartNumber` is `null`. - if ($this->_cartNumber === null && $cookieNumber = $requestCookies->getValue($this->cartCookie['name'])) { - $this->_cartNumber = $cookieNumber; - } - } - - // A `null` or `false` value means we need to generate a new cart number. - if ($this->_cartNumber === null || $this->_cartNumber === false) { - $this->_cartNumber = $this->generateCartNumber(); - } - - /// Just in case the current cart is not the one in session, clear the cached cart. - if ($this->_cart && $this->_cart->number !== $this->_cartNumber) { - $this->_cart = null; - } - - return $this->_cartNumber; - } - - /** - * Set the session cart number. - */ - public function setSessionCartNumber(string $cartNumber): void - { - if (!Craft::$app->getRequest()->getIsConsoleRequest()) { - $this->_cartNumber = $cartNumber; - $cookie = Craft::createObject(array_merge($this->cartCookie, [ - 'class' => Cookie::class, - 'value' => $cartNumber, - 'expire' => time() + $this->cartCookieDuration, - ])); - Craft::$app->getResponse()->getCookies()->add($cookie); - } - } - - /** - * Returns a URL to load a cart with a secure token. - * - * @param Order $cart The cart to generate the load URL for - * @return string The URL with secure token - * @since 5.7.0 - */ - public function getLoadCartUrl(Order $cart): string - { - $linkExpiry = Plugin::getInstance()->getSettings()->loadCartUrlExpiry; - $expiryDate = DateTimeHelper::currentUTCDateTime()->add(DateTimeHelper::secondsToInterval($linkExpiry)); - - $token = Craft::$app->getTokens()->createToken([ - 'commerce/cart/load-cart', - ['cartNumber' => $cart->number], - ], expiryDate: $expiryDate); - - $request = Craft::$app->getRequest(); - $isCpRequest = $request->getIsCpRequest(); - - if ($isCpRequest) { - $request->setIsCpRequest(false); - } - - try { - return UrlHelper::actionUrl('commerce/cart/load-cart', [ - 'number' => $cart->number, - 'code' => $token, - ]); - } finally { - if ($isCpRequest) { - $request->setIsCpRequest($isCpRequest); - } - } - } - - /** - * Restores previous cart for the current user if their current cart is empty. - * Ideally this is only used when a user logs in. - * - * @throws ElementNotFoundException - * @throws Exception - * @throws MissingComponentException - * @throws Throwable - */ - public function restorePreviousCartForCurrentUser(): void - { - $currentUser = Craft::$app->getUser()->getIdentity(); - $currentStoreId = Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if (!$currentUser) { - return; - } - - // If the current cart is empty see if the logged-in user has a previous cart - // Get any cart that is not empty, is not trashed or complete, and belongings to the user - /** @var Order|null $previousCartsWithLineItems */ - $previousCartsWithLineItems = Order::find() - ->customer($currentUser) - ->isCompleted(false) - ->hasLineItems() - ->trashed(false) - ->storeId($currentStoreId) - ->one(); - - /** @var Order|null $anyPreviousCart */ - $anyPreviousCart = Order::find() - ->customer($currentUser) - ->isCompleted(false) - ->trashed(false) - ->storeId($currentStoreId) - ->one(); - - /** @var Order|null $currentCartInSession */ - $currentCartInSession = Order::find() - ->number($this->getSessionCartNumber()) - ->isCompleted(false) - ->hasLineItems() - ->trashed(false) - ->storeId($currentStoreId) - ->one(); - - /** - * Cart restoring preference order: - * 1. Give the cart in session to the current customer if they are logging in and there are items in the cart - * 2. Restore a previous cart belonging to the customer that has line items - * 3. Restore any other previous cart for the customer - */ - if ($currentCartInSession) { - // Give the cart to the current customer if they are logging in and there are items in the cart - // Call get cart as this will switch the user and save it if needed - $this->getCart(); - } elseif ($previousCartsWithLineItems) { - // Restore previous cart that has line items - $this->_cart = $previousCartsWithLineItems; - $this->setSessionCartNumber($previousCartsWithLineItems->number); - } elseif ($anyPreviousCart) { - // Finally try to restore any other previous cart for the customer - $this->_cart = $anyPreviousCart; - $this->setSessionCartNumber($anyPreviousCart->number); - } - } - - /** - * Removes all carts that are incomplete and older than the config setting. - * - * @return int The number of carts purged from the database - * @throws \Exception - * @throws Throwable - */ - public function purgeIncompleteCarts(): int - { - if (!Plugin::getInstance()->getSettings()->purgeInactiveCarts) { - return 0; - }; - - $configInterval = ConfigHelper::durationInSeconds(Plugin::getInstance()->getSettings()->purgeInactiveCartsDuration); - $edge = new DateTime(); - $interval = DateTimeHelper::secondsToInterval($configInterval); - $edge->sub($interval); - - $cartIdsQuery = (new Query()) - ->select(['orders.id']) - ->where(['not', ['isCompleted' => true]]) - ->andWhere('[[orders.dateUpdated]] <= :edge', ['edge' => Db::prepareDateForDb($edge)]) - ->from(['orders' => Table::ORDERS]); - - $event = new CartPurgeEvent([ - 'inactiveCartsQuery' => $cartIdsQuery, - ]); - - if ($this->hasEventHandlers(self::EVENT_BEFORE_PURGE_INACTIVE_CARTS)) { - $this->trigger(self::EVENT_BEFORE_PURGE_INACTIVE_CARTS, $event); - } - - if (!$event->isValid) { - return 0; - } - - // The searchindex table is probably MyISAM, though - Craft::$app->getDb()->createCommand() - ->delete('{{%searchindex}}', ['elementId' => $event->inactiveCartsQuery]) - ->execute(); - - // Taken from craft\services\Elements::deleteElement(); Using the method directly - // takes too many resources since it retrieves the order before deleting it. - // Delete the elements table rows, which will cascade across all other InnoDB tables - Craft::$app->getDb()->createCommand() - ->delete('{{%elements}}', ['id' => $event->inactiveCartsQuery]) - ->execute(); - - return $cartIdsQuery->count(); - } - - /** - * @return void - * @throws SiteNotFoundException - * @throws InvalidConfigException - */ - protected function loadCookie(): void - { - $currentStore = Plugin::getInstance()->getStores()->getCurrentStore(); - - // Complete the cart cookie config - if (!isset($this->cartCookie['name'])) { - $this->cartCookie['name'] = md5(sprintf('Craft.%s.%s.%s', self::class, Craft::$app->id, $currentStore->handle)) . '_commerce_cart'; - } - - // Don't restore from cookie if the cart was explicitly forgotten this request. - if ($this->_cartNumber === false) { - return; - } - - $request = Craft::$app->getRequest(); - if (!$request->getIsConsoleRequest()) { - $this->cartCookie = Craft::cookieConfig($this->cartCookie); - - $requestCookies = $request->getCookies(); - - // If we have a cart cookie, assign it to the cart number. - if ($requestCookies->has($this->cartCookie['name'])) { - $this->setSessionCartNumber($requestCookies->getValue($this->cartCookie['name'])); - } - } - } - - /** - * Gets the current payment currency ISO code - * @todo in Commerce 6.0, replace the COMMERCE_PAYMENT_CURRENCY constant with a proper per-store config setting and surface validation errors instead of throwing InvalidConfigException - */ - private function _getCartPaymentCurrencyIso(): string - { - if ($this->_cart) { - // Is the payment currency locked to the constant - if (defined('COMMERCE_PAYMENT_CURRENCY')) { - $paymentCurrencies = Plugin::getInstance()->getPaymentCurrencies()->getAllPaymentCurrencies($this->_cart->storeId); - // if not in array - if (!$paymentCurrencies->contains('iso', '==', COMMERCE_PAYMENT_CURRENCY)) { - throw new InvalidConfigException('The COMMERCE_PAYMENT_CURRENCY constant is not set to a valid payment currency.'); - } - - $this->_cart->paymentCurrency = COMMERCE_PAYMENT_CURRENCY; - } - - return $this->_cart->paymentCurrency; - } - - return Plugin::getInstance()->getPaymentCurrencies()->getPrimaryPaymentCurrencyIso(); - } - - /** - * @param ModelEvent $event - * @return void - * @throws MissingComponentException - * @throws Throwable - */ - public function afterSaveUserHandler(ModelEvent $event): void - { - $segments = Craft::$app->getRequest()->getActionSegments(); - $userSaveSegments = ['users', 'save-user']; - $isUserSaveAction = $segments == $userSaveSegments; - - // we have a cart number, currently anon, and the current action being executed is user save - if (!Craft::$app->getUser()->getIdentity() && - !Craft::$app->getRequest()->getIsCpRequest() && - $isUserSaveAction - ) { - $currentCartNumber = $this->getSessionCartNumber(); - // Set the session flag to preserve the cart for this user - Craft::$app->getSession()->set('commerce:anonymousCartWithCredentialedCustomer:' . $currentCartNumber, true); - } - } -} diff --git a/src/services/CatalogPricing.php b/src/services/CatalogPricing.php deleted file mode 100755 index 99c6f60d72..0000000000 --- a/src/services/CatalogPricing.php +++ /dev/null @@ -1,786 +0,0 @@ - - * @since 5.0.0 - */ -class CatalogPricing extends Component -{ - /** - * @var array|null - */ - private ?array $_allCatalogPrices = null; - - /** - * @param Queue|QueueInterface|null $queue - * @param float $progress - * @param string|null $label - * @return void - */ - private function setQueueProgress(Queue|QueueInterface|null $queue, float $progress, ?string $label = null): void - { - if ($queue instanceof QueueInterface) { - $queue->setProgress((int)$progress, $label); - } - } - - /** - * @param array|null $purchasableIds - * @param CatalogPricingRule[]|null $catalogPricingRules - * @param bool $showConsoleOutput - * @param Queue|QueueInterface|null $queue - * @return void - * @throws Exception - * @throws InvalidConfigException - */ - public function generateCatalogPrices(?array $purchasableIds = null, ?array $catalogPricingRules = null, bool $showConsoleOutput = false, Queue|QueueInterface $queue = null): void - { - $chunkSize = 1000; - $this->setQueueProgress($queue, 10, 'Retrieving purchasables'); - - $isAllPurchasables = $purchasableIds === null; - if ($isAllPurchasables) { - $purchasableIds = (new Query()) - ->select(['purchasables.id']) - ->from(Table::PURCHASABLES . ' purchasables') - ->innerJoin(\craft\db\Table::ELEMENTS . ' e', '[[e.id]] = [[purchasables.id]]') - // Make sure we aren't putting and draft or revision purchasables in the catalog pricing table - ->where(['e.revisionId' => null]) - ->andWhere(['e.draftId' => null]) - ->column(); - } else { - // If purchasable IDs have been passed in remove all IDs that are revisions or drafts - $allowedPurchasableIds = []; - // Chunk through the IDs to avoid hitting the int limit in the where clause - foreach (array_chunk($purchasableIds, 2000) as $purchasableIdsChunk) { - $allowedPurchasableIds = array_merge($allowedPurchasableIds, (new Query()) - ->select(['purchasables.id']) - ->from(Table::PURCHASABLES . ' purchasables') - ->innerJoin(\craft\db\Table::ELEMENTS . ' e', '[[e.id]] = [[purchasables.id]]') - ->where(['e.revisionId' => null]) - ->andWhere(['e.draftId' => null]) - ->andWhere(['purchasables.id' => $purchasableIdsChunk]) - ->column()); - } - - $purchasableIds = $allowedPurchasableIds; - } - - if (empty($purchasableIds)) { - return; - } - - // Rules with user ID records - $cprWithUserIds = (new Query()) - ->select(['catalogPricingRuleId']) - ->from(Table::CATALOG_PRICING_RULES_USERS) - ->groupBy('catalogPricingRuleId') - ->column(); - - // @TODO Consider marking catalog prices for the affected purchasables as pending here so consumers can detect a stale state while regeneration is in progress - - $cprStartTime = microtime(true); - if ($showConsoleOutput) { - Console::stdout(PHP_EOL . 'Generating price data from catalog pricing rules... '); - } - - $this->setQueueProgress($queue, 20, 'Generating catalog pricing data'); - $catalogPricing = []; - foreach (Plugin::getInstance()->getStores()->getAllStores() as $store) { - $priceByPurchasableId = (new Query()) - ->select(['purchasableId', 'basePrice', 'basePromotionalPrice']) - ->from([Table::PURCHASABLES_STORES]) - ->where(['storeId' => $store->id]) - ->indexBy('purchasableId') - ->all(); - - $runCatalogPricingRules = $catalogPricingRules ?? Plugin::getInstance()->getCatalogPricingRules()->getAllActiveCatalogPricingRules($store->id)->all(); - - foreach ($runCatalogPricingRules as $catalogPricingRule) { - // Skip rule processing if it isn't for this store. - // This is in case incompatible rules were passed in. - if ($catalogPricingRule->storeId !== $store->id) { - continue; - } - - // Skip rule if the rule is not enabled - if (!$catalogPricingRule->enabled) { - continue; - } - - // Skip if the rule has user conditions but didn't generate any applicable users - if (!empty($catalogPricingRule->getCustomerCondition()->getConditionRules()) && !in_array($catalogPricingRule->id, $cprWithUserIds, true)) { - continue; - } - - // If `getPurchasableIds()` is `null` this means all purchasables - if ($catalogPricingRule->getPurchasableIds() === null) { - $applyPurchasableIds = $purchasableIds; - } else { - $applyPurchasableIds = $isAllPurchasables ? $catalogPricingRule->getPurchasableIds() : array_intersect($catalogPricingRule->getPurchasableIds(), $purchasableIds); - } - - if (empty($applyPurchasableIds)) { - continue; - } - - foreach ($applyPurchasableIds as $purchasableId) { - if (!isset($priceByPurchasableId[$purchasableId])) { - continue; - } - - $catalogPrice = Plugin::getInstance()->getCatalogPricingRules()->generateRulePriceFromPrice($priceByPurchasableId[$purchasableId]['basePrice'], $priceByPurchasableId[$purchasableId]['basePromotionalPrice'], $catalogPricingRule); - - if ($catalogPrice === null) { - continue; - } - - $catalogPricing[] = [ - $purchasableId, // purchasableId - $catalogPrice, // price - $store->id, // storeId - $catalogPricingRule->isPromotionalPrice, // isPromotionalPrice - $catalogPricingRule->id, // catalogPricingRuleId - $catalogPricingRule->dateFrom ? Db::prepareDateForDb($catalogPricingRule->dateFrom) : null, // dateFrom - $catalogPricingRule->dateTo ? Db::prepareDateForDb($catalogPricingRule->dateTo) : null, // dateTo - false, // hasUpdatePending - ]; - } - } - } - - $cprExecutionLength = microtime(true) - $cprStartTime; - if ($showConsoleOutput) { - Console::stdout('done!'); - Console::stdout(PHP_EOL . 'Created ' . count($catalogPricing) . ' rule price data in ' . round($cprExecutionLength, 2) . ' seconds' . PHP_EOL); - } - - $this->setQueueProgress($queue, 40, 'Clearing existing catalog prices'); - $transaction = Craft::$app->getDb()->beginTransaction(); - // Truncate the catalog pricing table - if (!$isAllPurchasables || !empty($catalogPricingRules)) { - // If purchasable IDs are passed in or catalog pricing rules are passed in - // only delete the rows for those purchasable IDs and catalog pricing rules - foreach (array_chunk($purchasableIds, 1000) as $purchasableIdsChunk) { - $where = ['purchasableId' => $purchasableIdsChunk]; - - // If passing catalog pricing rules only delete the rows for those rules - if (!empty($catalogPricingRules)) { - $where['catalogPricingRuleId'] = ArrayHelper::getColumn($catalogPricingRules, 'id'); - } - - Craft::$app->getDb()->createCommand() - ->delete(Table::CATALOG_PRICING, $where) - ->execute(); - } - } else { - Craft::$app->getDb()->createCommand()->truncateTable(Table::CATALOG_PRICING)->execute(); - } - - // If there are no specific catalog pricing rules passed in then copy the base prices into the catalog pricing table - if (empty($catalogPricingRules)) { - $this->setQueueProgress($queue, 60, 'Copying base prices to catalog pricing'); - $total = count($purchasableIds); - $baseStateTime = microtime(true); - $count = 1; - // Copy base prices into the catalog pricing table with a query for speed - // Batch through the purchasable IDs as we don't know what is passed in and don't want to hit the int limit in the where clause - foreach (array_chunk($purchasableIds, $chunkSize) as $purchasableIdsChunk) { - $fromCount = Craft::$app->getFormatter()->asDecimal($count, 0); - $toCount = ($count + ($chunkSize - 1)) > count($purchasableIds) ? $total : Craft::$app->getFormatter()->asDecimal($count + count($purchasableIdsChunk) - 1, 0); - if ($showConsoleOutput) { - Console::stdout(PHP_EOL . sprintf('Generating base prices rows for purchasables %s to %s of %s... ', $fromCount, $toCount, $total)); - } - - $uuidFunction = Craft::$app->getDb()->getIsPgsql() ? 'gen_random_uuid()' : 'UUID()'; - - $schema = Craft::$app->getDb()->getSchema(); - $catalogPricingTable = $schema->getRawTableName(Table::CATALOG_PRICING); - $commercePurchasablesStoresTable = $schema->getRawTableName(Table::PURCHASABLES_STORES); - - $insert = Craft::$app->getDb()->createCommand()->setSql(' - INSERT INTO [[' . $catalogPricingTable . ']] ([[price]], [[purchasableId]], [[storeId]], [[uid]], [[dateCreated]], [[dateUpdated]]) - SELECT [[basePrice]], [[purchasableId]], [[storeId]], ' . $uuidFunction . ', NOW(), NOW() FROM [[' . $commercePurchasablesStoresTable . ']] - WHERE [[purchasableId]] IN (' . implode(',', $purchasableIdsChunk) . ') - '); - $insert->execute(); - - $insert = Craft::$app->getDb()->createCommand()->setSql(' - INSERT INTO [[' . $catalogPricingTable . ']] ([[price]], [[purchasableId]], [[storeId]], [[isPromotionalPrice]], [[uid]], [[dateCreated]], [[dateUpdated]]) - SELECT [[basePromotionalPrice]], [[purchasableId]], [[storeId]], true, ' . $uuidFunction . ', NOW(), NOW() FROM [[' . $commercePurchasablesStoresTable . ']] - WHERE (NOT ([[basePromotionalPrice]] is null)) AND [[purchasableId]] IN (' . implode(',', $purchasableIdsChunk) . ') - '); - $insert->execute(); - - if ($showConsoleOutput) { - Console::stdout('done!'); - } - $count += $chunkSize; - } - $baseExecutionLength = microtime(true) - $baseStateTime; - if ($showConsoleOutput) { - Console::stdout(PHP_EOL . 'Generated ' . $total . ' base prices in ' . round($baseExecutionLength, 2) . ' seconds' . PHP_EOL); - } - } - - $this->setQueueProgress($queue, 80, 'Inserting catalog pricing'); - // Batch through `$catalogPricing` and insert into the catalog pricing table - if (!empty($catalogPricing)) { - $count = 1; - $startTime = microtime(true); - $total = Craft::$app->getFormatter()->asDecimal(count($catalogPricing), 0); - foreach (array_chunk($catalogPricing, $chunkSize) as $catalogPricingChunk) { - $fromCount = Craft::$app->getFormatter()->asDecimal($count, 0); - $toCount = ($count + ($chunkSize - 1)) > count($catalogPricing) ? $total : Craft::$app->getFormatter()->asDecimal($count + count($catalogPricingChunk) - 1, 0); - if ($showConsoleOutput) { - Console::stdout(PHP_EOL . sprintf('Inserting catalog pricing rule prices rows %s to %s of %s... ', $fromCount, $toCount, $total)); - } - Craft::$app->getDb()->createCommand()->batchInsert(Table::CATALOG_PRICING, [ - 'purchasableId', - 'price', - 'storeId', - 'isPromotionalPrice', - 'catalogPricingRuleId', - 'dateFrom', - 'dateTo', - 'hasUpdatePending', - ], $catalogPricingChunk)->execute(); - $count += $chunkSize; - if ($showConsoleOutput) { - Console::stdout('done!'); - } - } - - $executionLength = microtime(true) - $startTime; - if ($showConsoleOutput) { - Console::stdout(PHP_EOL . 'Generated ' . $total . ' prices in ' . round($executionLength, 2) . ' seconds' . PHP_EOL); - } - } - - $transaction->commit(); - $this->setQueueProgress($queue, 100); - } - - /** - * Return the catalog price for a purchasable. - * - * @param int $purchasableId - * @param int|null $storeId - * @param int|null $userId - * @param bool $isPromotionalPrice - * @return float|null - * @throws InvalidConfigException - */ - public function getCatalogPrice(int $purchasableId, ?int $storeId = null, ?int $userId = null, bool $isPromotionalPrice = false): ?float - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - $userKey = $userId ?? 'all'; - $promoKey = $isPromotionalPrice ? 'promo' : 'standard'; - $key = 'catalog-price-' . implode('-', [$storeId, $userKey, $promoKey]); - - if ($this->_allCatalogPrices === null || !isset($this->_allCatalogPrices[$key])) { - $query = $this->createCatalogPricesQuery($userId, $storeId) - ->addSelect(['purchasableId']) - ->indexBy('purchasableId') - ->collect(); - - $this->_allCatalogPrices[$key] = $query->pluck($isPromotionalPrice ? 'promotionalPrice' : 'price', 'purchasableId'); - } - - return $this->_allCatalogPrices[$key][$purchasableId] ?? null; - } - - /** - * @param int $purchasableId - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - * @throws SiteNotFoundException - */ - public function getCatalogPricesByPurchasableId(int $purchasableId, ?int $storeId = null): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - $allPriceRows = $this->createCatalogPricesQuery(storeId: $storeId, allPrices: true) - // Override select to prevent `min`/grouping - ->select([ - 'id', 'price', 'purchasableId', 'storeId', 'isPromotionalPrice', 'catalogPricingRuleId', 'dateFrom', 'dateTo', 'uid', - ]) - ->andWhere(['purchasableId' => $purchasableId]) - ->andWhere(['not', ['catalogPricingRuleId' => null]]) - ->all(); - - $allPrices = []; - foreach ($allPriceRows as $catalogPrice) { - $allPrices[] = Craft::createObject(['class' => CatalogPricingModel::class, 'attributes' => $catalogPrice]); - } - - return collect($allPrices); - } - - /** - * @param int $storeId - * @param CatalogPricingCondition|null $conditionBuilder - * @param string|null $searchText - * @param int|null $limit - * @param int|null $offset - * @param bool $includeBasePrices - * @return Collection - * @throws InvalidConfigException - */ - public function getCatalogPrices(int $storeId, ?CatalogPricingCondition $conditionBuilder = null, bool $includeBasePrices = true, ?string $searchText = null, ?int $limit = null, ?int $offset = null): Collection - { - $query = $this->_createCatalogPricesQuery($storeId, $conditionBuilder, $includeBasePrices, $searchText, $limit, $offset) - ->select([ - 'price', 'purchasableId', 'storeId', 'isPromotionalPrice', 'catalogPricingRuleId', 'dateFrom', 'dateTo', 'cp.uid', - ]); - - $query->orderBy('purchasableId ASC, catalogPricingRuleId ASC'); - $results = $query->all(); - - $catalogPrices = []; - foreach ($results as $result) { - $catalogPrices[] = Craft::createObject([ - 'class' => CatalogPricingModel::class, - 'attributes' => $result, - ]); - } - - return collect($catalogPrices); - } - - public function getCatalogPricesPageInfo(int $storeId, ?CatalogPricingCondition $conditionBuilder = null, bool $includeBasePrices = true, ?string $searchText = null, int $limit = 100, int $offset = 0) - { - $results = $this->_createCatalogPricesQuery($storeId, $conditionBuilder, $includeBasePrices, $searchText) - ->select(['purchasableId']) - ->groupBy(['purchasableId']) - ->all(); - - $total = count($results); - - return [ - 'first' => $offset + 1, - 'last' => $offset + $limit, - 'total' => $total, - 'prevUrl' => null, - 'nextUrl' => null, - ]; - } - - /** - * @param int|array|null $catalogPricingRuleId - * @param int|array|null $purchasableId - * @param int|array|null $storeId - * @return void - * @throws Exception - */ - public function markPricesAsUpdatePending(int|array|null $catalogPricingRuleId = null, int|array|null $purchasableId = null, int|array|null $storeId = null): void - { - $conditions = []; - - if ($catalogPricingRuleId !== null) { - $conditions['catalogPricingRuleId'] = $catalogPricingRuleId; - } - - if ($purchasableId !== null) { - $conditions['purchasableId'] = $purchasableId; - } - - if ($storeId !== null) { - $conditions['storeId'] = $storeId; - } - - Craft::$app->getDb()->createCommand() - ->update(Table::CATALOG_PRICING, ['hasUpdatePending' => true], $conditions) - ->execute(); - } - - /** - * @param int $storeId - * @param CatalogPricingCondition|null $conditionBuilder - * @param string|null $searchText - * @param bool $includeBasePrices - * @param int|null $limit - * @param int|null $offset - * @return Query - * @throws InvalidConfigException - */ - private function _createCatalogPricesQuery(int $storeId, ?CatalogPricingCondition $conditionBuilder = null, bool $includeBasePrices = true, ?string $searchText = null, ?int $limit = null, ?int $offset = null): Query - { - $query = Plugin::getInstance()->getCatalogPricing()->createCatalogPricesQuery(storeId: $storeId, allPrices: true, condition: $conditionBuilder); - - if ($includeBasePrices === false) { - $query->andWhere(['not', ['catalogPricingRuleId' => null]]); - } - - $subQuery = (new Query()) - ->from(Table::PURCHASABLES) - ->select(['id']); - - if ($limit) { - $subQuery->limit($limit); - } - - if ($offset) { - $subQuery->offset($offset); - } - - if ($searchText) { - $likeOperator = Craft::$app->getDb()->getIsPgsql() ? 'ilike' : 'like'; - $subQuery->andWhere([$likeOperator, 'purchasables.description', $searchText]); - } - - $query->innerJoin(['purchasables' => $subQuery], '[[purchasables.id]] = [[cp.purchasableId]]'); - - // If there is a condition builder, modify the query - $conditionBuilder?->modifyQuery($query); - - return $query; - } - - /** - * @param ModelEvent $event - * @return void - * @throws InvalidConfigException - * @since 5.0.0 - * @deprecated in 5.5.0 - */ - public function afterSavePurchasableHandler(ModelEvent $event): void - { - $purchasable = $event->sender; - if (!$purchasable instanceof Purchasable || $purchasable->propagating || $purchasable->getIsDraft() || $purchasable->getIsRevision()) { - return; - } - - $this->createCatalogPricingJob(['purchasableIds' => [$purchasable->id], 'storeId' => $purchasable->storeId]); - } - - /** - * @param array $config - * @param int $priority - * @return void - * @throws InvalidConfigException - */ - public function createCatalogPricingJob(array $config = [], int $priority = 100): void - { - $catalogPricingRuleIds = $this->_normalizeIds($config['catalogPricingRuleIds'] ?? null); - $purchasableIds = $this->_normalizeIds($config['purchasableIds'] ?? null); - - if ($catalogPricingRuleIds === [] && $purchasableIds === []) { - return; - } - - $storeId = $config['storeId'] ?? null; - $this->markPricesAsUpdatePending($catalogPricingRuleIds, $purchasableIds, $storeId); - - // Queue purchasable-based and rule-based work into separate rows so they are never cross-contaminated. - // Catalog pricing rules determine which purchasables are relevant, so the two must be processed independently. - - if (!empty($purchasableIds) || ($purchasableIds === null && empty($catalogPricingRuleIds))) { - // Specific purchasable IDs: these will be regenerated against all applicable rules - $this->_queueCatalogPricingIds($storeId, CatalogPricingQueueRecord::TYPE_PURCHASABLE, $purchasableIds); - } - - if (!empty($catalogPricingRuleIds)) { - $this->_queueCatalogPricingIds($storeId, CatalogPricingQueueRecord::TYPE_RULE, $catalogPricingRuleIds); - } - - QueueHelper::push(Craft::createObject(CatalogPricingJob::class), $priority); - } - - /** - * @return bool - */ - public function areCatalogPricingJobsRunning(): bool - { - return (new Query()) - ->from(Table::CATALOG_PRICING_QUEUE) - ->exists(); - } - - /** - * Reserves one pending queue row for processing. - * - * @return CatalogPricingQueueRecord|null - * @since 5.7.0 - */ - public function reserveCatalogPricingQueueRow(): ?CatalogPricingQueueRecord - { - $mutex = Craft::$app->getMutex(); - - // Use the same lock as the write methods so that reservation and inserts/merges are fully serialised. - // Non-blocking: if a write operation is currently holding the lock, return null and let the next - // queue job execution pick up the row instead. - if (!$mutex->acquire('catalogpricingqueue', 0)) { - return null; - } - - try { - $pendingId = (new Query()) - ->select(['id']) - ->from(Table::CATALOG_PRICING_QUEUE) - ->where(['reserved' => false]) - ->orderBy(['id' => SORT_ASC]) - ->scalar(); - - if (!$pendingId) { - return null; - } - - /** @var CatalogPricingQueueRecord|null $record */ - $record = CatalogPricingQueueRecord::findOne(['id' => (int)$pendingId, 'reserved' => false]); - - if (!$record) { - return null; - } - - $record->reserved = true; - $record->save(false); - - return $record; - } finally { - $mutex->release('catalogpricingqueue'); - } - } - - /** - * @param int $id - * @return void - * @throws Exception - * @since 5.7.0 - */ - public function releaseCatalogPricingQueueRowById(int $id): void - { - $record = CatalogPricingQueueRecord::findOne($id); - if ($record) { - $record->reserved = false; - $record->save(false); - } - } - - /** - * @param int $id - * @return void - * @since 5.7.0 - */ - public function deleteCatalogPricingQueueRowById(int $id): void - { - CatalogPricingQueueRecord::deleteAll(['id' => $id]); - } - - /** - * Queues catalog pricing regeneration IDs by row type, merging into any existing unreserved row - * for the same store and type. - * - * @param int|null $storeId - * @param string $type - * @param array|null $ids - * @return void - * @throws Exception - * @throws \RuntimeException if the queue mutex cannot be acquired - */ - private function _queueCatalogPricingIds(?int $storeId, string $type, ?array $ids): void - { - $mutex = Craft::$app->getMutex(); - - if (!$mutex->acquire('catalogpricingqueue', 5)) { - throw new \RuntimeException('Unable to acquire the catalog pricing queue mutex.'); - } - - try { - // Merge into an existing unreserved row for the same store and type. - // _mergeIdSets keeps null when either side is null (broader scope wins). - /** @var CatalogPricingQueueRecord|null $pendingRecord */ - $pendingRecord = CatalogPricingQueueRecord::findOne([ - 'storeId' => $storeId, - 'type' => $type, - 'reserved' => false, - ]); - - if ($pendingRecord) { - // Merge IDs, preserving null to represent the broader "all IDs" scope. - $pendingIds = $pendingRecord->getIds(); - $ids = ($pendingIds === null || $ids === null) - ? null - : $this->_normalizeIds(array_merge($pendingIds, $ids)); - - $pendingRecord->setIds($ids); - $pendingRecord->save(false); - - return; - } - - $record = new CatalogPricingQueueRecord(); - $record->storeId = $storeId; - $record->type = $type; - $record->setIds($ids); - $record->reserved = false; - $record->save(false); - } finally { - $mutex->release('catalogpricingqueue'); - } - } - - /** - * @param array|null $ids - * @return array|null - * @since 5.7.0 - */ - private function _normalizeIds(?array $ids): ?array - { - if ($ids === null) { - return null; - } - - $ids = array_map(fn(mixed $id) => (int)$id, $ids); - $ids = array_values(array_unique(array_filter($ids, fn(int $id) => $id > 0))); - sort($ids, SORT_NUMERIC); - - return $ids; - } - - - /** - * Creates query for catalog pricing. - * - * @param int|null $userId - * @param int|string|null $storeId - * @param bool|null $isPromotionalPrice - * @param bool $allPrices - * @param CatalogPricingCondition|null $condition - * @return Query - * @throws InvalidConfigException - * @throws DeprecationException - * @deprecated in 5.1.0. Use `createCatalogPricesQuery()` instead. - */ - public function createCatalogPricingQuery(?int $userId = null, int|string|null $storeId = null, ?bool $isPromotionalPrice = null, bool $allPrices = false, ?CatalogPricingCondition $condition = null): Query - { - Craft::$app->getDeprecator()->log(__METHOD__, 'CatalogPricing `' . __METHOD__ . '()` method has been deprecated. Use `createCatalogPricesQuery()` instead.'); - $query = (new Query()) - ->select([new Expression('MIN(price) as price')]) - ->from([Table::CATALOG_PRICING . ' cp']); - - // Use condition builder to tweak the query for reusability - $condition ??= Craft::$app->getConditions()->createCondition([ - 'class' => CatalogPricingCondition::class, - 'allPrices' => $allPrices, - ]); - - if ($userId) { - $condition->addConditionRule(Craft::$app->getConditions()->createConditionRule([ - 'class' => CatalogPricingCustomerConditionRule::class, - 'customerId' => $userId, - ])); - } - - $condition->modifyQuery($query); - - $query - ->andWhere(['or', ['dateFrom' => null], ['<=', 'dateFrom', Db::prepareDateForDb(new DateTime())]]) - ->andWhere(['or', ['dateTo' => null], ['>=', 'dateTo', Db::prepareDateForDb(new DateTime())]]) - ->orderBy(['purchasableId' => SORT_ASC, 'price' => SORT_ASC]); - - // If we're not getting all prices, we need to group by purchasableId and storeId - if (!$allPrices) { - $query->groupBy(['purchasableId', 'storeId']); - } - - if ($storeId) { - $query->andWhere(['storeId' => $storeId]); - } - - if ($isPromotionalPrice !== null) { - $query->andWhere(['isPromotionalPrice' => $isPromotionalPrice]); - } - - return $query; - } - - /** - * Returns rows of purchasable prices. - * - * @param int|null $userId - * @param int|string|null $storeId - * @param bool $allPrices - * @param CatalogPricingCondition|null $condition - * @return Query - * @throws InvalidConfigException - * @since 5.1.0 - */ - public function createCatalogPricesQuery(?int $userId = null, int|string|null $storeId = null, bool $allPrices = false, ?CatalogPricingCondition $condition = null): Query - { - $query = (new Query()) - ->select([ - new Expression('MIN(CASE WHEN [[isPromotionalPrice]] = FALSE THEN [[price]] END) AS [[price]]'), - new Expression('MIN(CASE WHEN [[isPromotionalPrice]] = TRUE THEN [[price]] END) AS [[promotionalPrice]]'), - new Expression('MIN([[price]]) AS [[salePrice]]'), - ]) - ->from([Table::CATALOG_PRICING . ' cp']); - - // Use condition builder to tweak the query for reusability - $condition ??= Craft::$app->getConditions()->createCondition([ - 'class' => CatalogPricingCondition::class, - 'allPrices' => $allPrices, - ]); - - if ($userId) { - $condition->addConditionRule(Craft::$app->getConditions()->createConditionRule([ - 'class' => CatalogPricingCustomerConditionRule::class, - 'customerId' => $userId, - ])); - } - - $condition->modifyQuery($query); - - $query - ->andWhere(['or', ['dateFrom' => null], ['<=', 'dateFrom', Db::prepareDateForDb(new DateTime())]]) - ->andWhere(['or', ['dateTo' => null], ['>=', 'dateTo', Db::prepareDateForDb(new DateTime())]]); - - // If we're not getting all prices, we need to group by purchasableId and storeId - if (!$allPrices) { - $query->groupBy(['purchasableId', 'storeId']); - } - - if ($storeId) { - $query->andWhere(['storeId' => $storeId]); - } - - return $query; - } -} diff --git a/src/services/CatalogPricingRules.php b/src/services/CatalogPricingRules.php deleted file mode 100644 index 03da1a10f3..0000000000 --- a/src/services/CatalogPricingRules.php +++ /dev/null @@ -1,398 +0,0 @@ - - * @since 5.0.0 - */ -class CatalogPricingRules extends Component -{ - /** - * @var bool|null - */ - private ?bool $_hasCatalogPricingRules = null; - - /** - * @return bool - * @throws InvalidConfigException - */ - public function hasCatalogPricingRules(): bool - { - if (!$this->canUseCatalogPricingRules()) { - return false; - } - - if ($this->_hasCatalogPricingRules === null) { - $this->_hasCatalogPricingRules = $this->_createCatalogPricingRuleQuery()->exists(); - } - - return (bool)$this->_hasCatalogPricingRules; - } - - /** - * @var Collection[]|null - */ - private ?array $_allCatalogPricingRules = null; - - /** - * @return bool - * @throws InvalidConfigException - */ - public function canUseCatalogPricingRules(): bool - { - if (!empty(Plugin::getInstance()->getSales()->getAllSales())) { - return false; - } - - return true; - } - - /** - * Get a catalog pricing rule by its ID. - * - * @param int $id - * @param int|null $storeId - * @return CatalogPricingRule|null - * @throws InvalidConfigException - * @throws SiteNotFoundException - */ - public function getCatalogPricingRuleById(int $id, ?int $storeId = null): ?CatalogPricingRule - { - return $this->getAllCatalogPricingRules($storeId)->firstWhere('id', $id); - } - - /** - * Get all catalog pricing rules. - * - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - * @throws SiteNotFoundException - */ - public function getAllCatalogPricingRules(?int $storeId = null): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if ($this->_allCatalogPricingRules === null || !isset($this->_allCatalogPricingRules[$storeId])) { - $query = $this->_createCatalogPricingRuleQuery() - ->where(['storeId' => $storeId]); - - $results = $query->all(); - - if ($this->_allCatalogPricingRules === null) { - $this->_allCatalogPricingRules = []; - } - - $models = $this->_createCatalogPricingRuleModels($results); - $this->_allCatalogPricingRules[$storeId] = collect($models); - } - - return $this->_allCatalogPricingRules[$storeId]; - } - - /** - * @param int $purchasableId - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - * @throws SiteNotFoundException - */ - public function getAllCatalogPricingRulesByPurchasableId(int $purchasableId, ?int $storeId = null): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - // @TODO Benchmark this lookup under load and add per-purchasable memoization if it becomes a hot path - $catalogPricingRules = $this->_createCatalogPricingRuleQuery() - ->andWhere(['id' => (new Query()) - ->select(['catalogPricingRuleId']) - ->from([Table::CATALOG_PRICING]) - ->where(['purchasableId' => $purchasableId]), - ]) - ->andWhere(['storeId' => $storeId]) - ->all(); - - return $this->_createCatalogPricingRuleModels($catalogPricingRules); - } - - /** - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - */ - public function getAllEnabledCatalogPricingRules(?int $storeId = null): Collection - { - return $this->getAllCatalogPricingRules($storeId)->where(fn(CatalogPricingRule $pcr) => $pcr->enabled); - } - - /** - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - */ - public function getAllActiveCatalogPricingRules(?int $storeId = null): Collection - { - return $this->getAllEnabledCatalogPricingRules($storeId)->where(fn(CatalogPricingRule $pcr) => - // If there are no dates or rule is currently in the date range add it to the active list - ($pcr->dateFrom === null || $pcr->dateFrom->getTimestamp() <= time()) && ($pcr->dateTo === null || $pcr->dateTo->getTimestamp() >= time())); - } - - /** - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - */ - public function getAllCatalogPricingRulesWithUserConditions(?int $storeId = null): Collection - { - return $this->getAllCatalogPricingRules($storeId)->where(fn(CatalogPricingRule $pcr) => !empty($pcr->getCustomerCondition()->getConditionRules())); - } - - /** - * @param float|null $basePrice - * @param float|null $basePromotionalPrice - * @param CatalogPricingRule $catalogPricingRule - * @return float|null - */ - public function generateRulePriceFromPrice(?float $basePrice, ?float $basePromotionalPrice, CatalogPricingRule $catalogPricingRule): ?float - { - $price = null; - - // A third option may be required for catalog pricing rules that allow store admins to select `salePrice`. - // So that just want to create a catalog price from the `price` or the `promotionalPrice` if there is one. - if ($catalogPricingRule->applyPriceType === CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE) { - $price = $basePrice; - } elseif ($catalogPricingRule->applyPriceType === CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PROMOTIONAL_PRICE) { - // Skip if there is no promotional price - if ($basePromotionalPrice === null) { - return null; - } - $price = $basePromotionalPrice; - } - - if ($price === null) { - return null; - } - - return $catalogPricingRule->getRulePriceFromPrice($price); - } - - /** - * @param ModelEvent $event - * @return void - * @throws InvalidConfigException - * @throws \yii\db\Exception - */ - public function afterSaveUserHandler(ModelEvent|UserGroupsAssignEvent $event): void - { - $stores = Plugin::getInstance()->getStores()->getAllStores(); - - foreach ($stores as $store) { - $rules = $this->getAllCatalogPricingRulesWithUserConditions($store->id); - if ($rules->isEmpty()) { - continue; - } - - /** @var User $user */ - $user = $event instanceof ModelEvent ? $event->sender : Craft::$app->getUsers()->getUserById($event->userId); - $rules->each(function(CatalogPricingRule $rule) use ($user) { - $customerCondition = $rule->getCustomerCondition(); - if ($customerCondition->matchElement($user)) { - if (!CatalogPricingRuleUser::find()->where(['userId' => $user->id, 'catalogPricingRuleId' => $rule->id])->exists()) { - Craft::$app->getDb()->createCommand() - ->insert(Table::CATALOG_PRICING_RULES_USERS, ['userId' => $user->id, 'catalogPricingRuleId' => $rule->id]) - ->execute(); - } - } else { - CatalogPricingRuleUser::deleteAll(['userId' => $user->id, 'catalogPricingRuleId' => $rule->id]); - } - }); - } - } - - /** - * Save a Catalog Pricing Rule. - * - * @param bool $runValidation should we validate this before saving. - * @throws Exception - * @throws \Exception - */ - public function saveCatalogPricingRule(CatalogPricingRule $catalogPricingRule, bool $runValidation = true): bool - { - $isNew = !$catalogPricingRule->id; - - if ($isNew) { - $record = Craft::createObject(CatalogPricingRuleRecord::class); - } else { - $record = CatalogPricingRuleRecord::findOne($catalogPricingRule->id); - - if (!$record) { - throw new Exception(Craft::t('commerce', 'No catalog pricing rule exists with the ID “{id}”', - ['id' => $catalogPricingRule->id])); - } - } - - if ($runValidation && !$catalogPricingRule->validate()) { - Craft::info('Catalog pricing rule not saved due to validation error.', __METHOD__); - - return false; - } - - // This was previously in a loops using an array of attributes, but this way gives actual references to the properties in the code - $record->apply = $catalogPricingRule->apply; - $record->applyAmount = $catalogPricingRule->applyAmount; - $record->applyPriceType = $catalogPricingRule->applyPriceType; - $record->dateFrom = $catalogPricingRule->dateFrom; - $record->dateTo = $catalogPricingRule->dateTo; - $record->description = $catalogPricingRule->description; - $record->enabled = $catalogPricingRule->enabled; - $record->isPromotionalPrice = $catalogPricingRule->isPromotionalPrice; - $record->name = $catalogPricingRule->name; - $record->storeId = $catalogPricingRule->storeId; - $record->metadata = $catalogPricingRule->getMetadata(); - - $record->customerCondition = $catalogPricingRule->getCustomerCondition()->getConfig(); - $record->productCondition = $catalogPricingRule->getProductCondition()->getConfig(); - $record->variantCondition = $catalogPricingRule->getVariantCondition()->getConfig(); - $record->purchasableCondition = $catalogPricingRule->getPurchasableCondition()->getConfig(); - - $db = Craft::$app->getDb(); - $transaction = $db->beginTransaction(); - - try { - $record->save(false); - $catalogPricingRule->id = $record->id; - - CatalogPricingRuleUser::deleteAll(['catalogPricingRuleId' => $catalogPricingRule->id]); - - // Batch insert user relationships in case we are dealing with a large number - $userIds = $catalogPricingRule->getUserIds() ?? []; - foreach (array_chunk($userIds, 1000) as $userIdsChunk) { - $userRecords = []; - foreach ($userIdsChunk as $userId) { - $userRecords[] = [$catalogPricingRule->id, $userId]; - } - $db->createCommand() - ->batchInsert( - Table::CATALOG_PRICING_RULES_USERS, - ['catalogPricingRuleId', 'userId'], - $userRecords - ) - ->execute(); - } - - $transaction->commit(); - - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'catalogPricingRuleIds' => [$catalogPricingRule->id], - 'storeId' => $catalogPricingRule->storeId, - ]); - - $this->_clearCaches(); - - return true; - } catch (\Exception $e) { - $transaction->rollBack(); - throw $e; - } - } - - /** - * Delete a catalog pricing rule by its id. - * - * @param int $id - * @return bool - * @throws StaleObjectException - * @throws \Throwable - */ - public function deleteCatalogPricingRuleById(int $id): bool - { - $record = CatalogPricingRuleRecord::findOne($id); - - if (!$record) { - return false; - } - - $this->_clearCaches(); - return (bool)$record->delete(); - } - - protected function _createCatalogPricingRuleQuery(): ?Query - { - return (new Query()) - ->select([ - 'apply', - 'applyAmount', - 'applyPriceType', - 'customerCondition', - 'dateCreated', - 'dateFrom', - 'dateTo', - 'dateUpdated', - 'description', - 'enabled', - 'id', - 'isPromotionalPrice', - 'metadata', - 'name', - 'productCondition', - 'purchasableCondition', - 'storeId', - 'variantCondition', - ]) - ->from(Table::CATALOG_PRICING_RULES); - } - - /** - * Clear memoization caches - */ - protected function _clearCaches(): void - { - $this->_allCatalogPricingRules = null; - $this->_hasCatalogPricingRules = null; - } - - /** - * Takes the results array from a `_createCatalogPricingRuleQuery()` call and creates a collection of CatalogPricingRule models. - * - * @param array $rows - * @return Collection - */ - protected function _createCatalogPricingRuleModels(array $rows): Collection - { - return collect($rows)->map(function($row) { - $row['customerCondition'] ??= ''; - $row['productCondition'] ??= ''; - $row['purchasableCondition'] ??= ''; - $row['variantCondition'] ??= ''; - - return Craft::createObject(CatalogPricingRule::class, ['config' => ['attributes' => $row]]); - })->keyBy('id'); - } -} diff --git a/src/services/Coupons.php b/src/services/Coupons.php deleted file mode 100644 index a76fa6c5eb..0000000000 --- a/src/services/Coupons.php +++ /dev/null @@ -1,265 +0,0 @@ - - * @since 4.0 - * - * @property-read null|array $allCodes - */ -class Coupons extends Component -{ - public const COUPON_FORMAT_REPLACEMENT_CHAR = '#'; - public const DEFAULT_COUPON_FORMAT = '######'; - public const CHARS_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - public const CHARS_LOWER = 'abcdefghijklmnopqrstuvwxyz'; - public const CHARS_NUMBERS = '0123456789'; - public const CHARS_SPECIAL = '!@#$%^&*()-_=+[]{}|;:,.<>/?~'; - - /** - * @var array|null - */ - private ?array $_allCodes = null; - - /** - * @return array|null - */ - public function getAllCodes(): ?array - { - if ($this->_allCodes !== null) { - return $this->_allCodes; - } - - $this->_allCodes = $this->_createCouponQuery() - ->indexBy('id') - ->select(['coupons.code']) - ->column(); - - return $this->_allCodes; - } - - /** - * @param string $code - * @return Coupon|null - * @throws InvalidConfigException - */ - public function getCouponByCode(string $code): ?Coupon - { - $coupon = $this->_createCouponQuery() - ->where(['code' => $code]) - ->one(); - - return $coupon ? Craft::createObject(Coupon::class, ['config' => ['attributes' => $coupon]]) : null; - } - - /** - * @param int $discountId - * @return Coupon[] - * @throws InvalidConfigException - */ - public function getCouponsByDiscountId(int $discountId): array - { - $coupons = $this->_createCouponQuery() - ->where(['discountId' => $discountId]) - ->all(); - - foreach ($coupons as &$coupon) { - $coupon = Craft::createObject(Coupon::class, ['config' => ['attributes' => $coupon]]); - } - - return $coupons; - } - - /** - * @param int $count - * @param string $format - * @param array $existingCodes - * @return string[] - * @throws Exception - */ - public function generateCouponCodes(int $count = 1, string $format = self::DEFAULT_COUPON_FORMAT, array $existingCodes = []): array - { - // Count the number of # characters in the format - $numReplacementChars = strlen($format) - strlen(str_replace(self::COUPON_FORMAT_REPLACEMENT_CHAR, '', $format)); - $numPossibleCodes = strlen(self::CHARS_UPPER) ** $numReplacementChars; - - if ($numPossibleCodes < $count) { - // @TODO Replace this generic Exception with a typed one (e.g. CouponException or InvalidArgumentException) so callers can distinguish format-too-restrictive failures - throw new Exception('The format is too restrictive to generate enough unique codes.'); - } - - $existingCodes = array_unique([...$existingCodes, ...$this->getAllCodes()]); - $coupons = []; - - for ($i = 1; $i <= $count; $i++) { - $code = preg_replace_callback('/([' . self::COUPON_FORMAT_REPLACEMENT_CHAR . ']+)/', static function($matches) { - $length = strlen($matches[0]); - return StringHelper::randomStringWithChars(self::CHARS_UPPER, $length); - }, $format); - - if (!empty($existingCodes) && in_array($code, $existingCodes, true)) { - $i--; - continue; - } - $coupons[] = $code; - $existingCodes[] = $code; - } - - return $coupons; - } - - /** - * @param int $id - * @return bool - * @throws Throwable - * @throws StaleObjectException - */ - public function deleteCouponById(int $id): bool - { - $couponRecord = CouponRecord::findOne($id); - - if (!$couponRecord) { - return false; - } - - return (bool)$couponRecord->delete(); - } - - /** - * @param Discount $discount - * @return bool - * @throws InvalidConfigException - * @since 4.0 - */ - public function saveDiscountCoupons(Discount $discount): bool - { - if (!$discount->id) { - throw new Exception('Discount must be saved before it can have coupons'); - } - - // Get currently saved coupon IDs from the DB - $existingCouponIds = $this->_createCouponQuery() - ->select(['id']) - ->where(['discountId' => $discount->id]) - ->column(); - - $couponIds = []; - foreach ($discount->getCoupons() as $key => $coupon) { - $coupon->discountId = $discount->id; - - if (!Plugin::getInstance()->getCoupons()->saveCoupon($coupon)) { - $discount->addModelErrors($coupon, 'coupon.' . $key); - } - - if ($coupon->id) { - $couponIds[] = $coupon->id; - } - } - - $return = !$discount->hasErrors(); - - if (empty($existingCouponIds) || $existingCouponIds === $couponIds) { - return $return; - } - - $deleteableCouponIds = array_diff($existingCouponIds, $couponIds); - if (empty($deleteableCouponIds)) { - return $return; - } - - foreach ($deleteableCouponIds as $deleteableCouponId) { - $this->deleteCouponById($deleteableCouponId); - } - - return $return; - } - - /** - * @param Coupon $coupon - * @param bool $runValidation - * @return bool - * @throws BadRequestHttpException - */ - public function saveCoupon(Coupon $coupon, bool $runValidation = true): bool - { - if ($coupon->id) { - $record = CouponRecord::findOne($coupon->id); - - if (!$record) { - throw new BadRequestHttpException("Invalid coupon ID: $coupon->id"); - } - } else { - $record = new CouponRecord(); - } - - if ($runValidation && !$coupon->validate()) { - Craft::info('Coupon not saved due to validation error.', __METHOD__); - - return false; - } - - $record->code = $coupon->code; - $record->discountId = $coupon->discountId; - $record->uses = $coupon->uses; - $record->maxUses = $coupon->maxUses; - - // Save it! - $record->save(false); - - // Now that we have a record ID, save it on the model - $coupon->id = $record->id; - - $this->clearCache(); - - return true; - } - - /** - * @return void - */ - protected function clearCache(): void - { - $this->_allCodes = null; - } - - /** - * Returns a Query object prepped for retrieving Coupons. - * - * @return Query The query object. - */ - private function _createCouponQuery(): Query - { - return (new Query()) - ->select([ - 'coupons.id', - 'coupons.code', - 'coupons.uses', - 'coupons.maxUses', - 'coupons.discountId', - ]) - ->from([Table::COUPONS . ' coupons']); - } -} diff --git a/src/services/Currencies.php b/src/services/Currencies.php deleted file mode 100644 index c3fd7e470d..0000000000 --- a/src/services/Currencies.php +++ /dev/null @@ -1,127 +0,0 @@ - - * @since 2.0 - */ -class Currencies extends Component -{ - private ?ISOCurrencies $_isoCurrencies = null; - - public function init() - { - $this->_isoCurrencies = new ISOCurrencies(); - } - - /** - * @var array - */ - private array $_tellersByIso = []; - - /** - * @param \Money\Currency|string $currency - * @return Teller - */ - public function getTeller(\Money\Currency|string $currency): Teller - { - if (is_string($currency)) { - $currency = new \Money\Currency($currency); - } - - $parser = new DecimalMoneyParser($this->_isoCurrencies); - $formatter = new DecimalMoneyFormatter($this->_isoCurrencies); - $roundingMode = Money::ROUND_HALF_UP; - - $iso = $currency->getCode(); - if (isset($this->_tellersByIso[$iso])) { - return $this->_tellersByIso[$iso]; - } - - $this->_tellersByIso[$iso] = new \Money\Teller( - $currency, - $parser, - $formatter, - $roundingMode - ); - - return $this->_tellersByIso[$iso]; - } - - /** - * Get a currency by it's ISO code. - * - * @param string $iso - * @return \Money\Currency|null - */ - public function getCurrencyByIso(string $iso): ?\Money\Currency - { - return $this->getAllCurrencies()->first(fn(\Money\Currency $currency) => $currency->getCode() == $iso); - } - - - /** - * Get a list of all available currencies. - * - * @return Collection<\Money\Currency> - */ - public function getAllCurrencies(): Collection - { - return collect($this->_isoCurrencies); - } - - /** - * @return array - */ - public function getAllCurrenciesList(): array - { - return $this->getAllCurrencies()->map(fn($currency) => [ - 'label' => $currency->getCode(), // @TODO Resolve a localized currency name (e.g. via Intl/Locale) and use it as the label instead of the ISO code - 'value' => $currency->getCode(), - ])->toArray(); - } - - /** - * @param Currency|string $currency - * @return int - */ - public function getSubunitFor(Currency|string $currency) - { - if (is_string($currency)) { - $currency = $this->getCurrencyByIso($currency); - } - - return $this->_isoCurrencies->subunitFor($currency); - } - - /** - * @param Currency|string $currency - * @return int - */ - public function numericCodeFor(Currency|string $currency) - { - if (is_string($currency)) { - $currency = $this->getCurrencyByIso($currency); - } - - return $this->_isoCurrencies->numericCodeFor($currency); - } -} diff --git a/src/services/Customers.php b/src/services/Customers.php deleted file mode 100644 index 72389c9a41..0000000000 --- a/src/services/Customers.php +++ /dev/null @@ -1,508 +0,0 @@ - - * @since 2.0 - */ -class Customers extends Component -{ - // Events - // ------------------------------------------------------------------------- - - /** - * @event UpdatePrimaryPaymentSourceEvent The event that is triggered when a primary payment method is saved. - * - * ```php - * use craft\elements\User; - * use craft\commerce\services\Customers; - * use craft\commerce\events\UpdatePrimaryPaymentSourceEvent; - * use yii\base\Event; - * - * Event::on( - * Customers::class, - * Customers::EVENT_UPDATE_PRIMARY_PAYMENT_SOURCE, - * function(UpdatePrimaryPaymentSourceEvent $event) { - * $previousPrimaryPaymentSourceId = $event->previousPrimaryPaymentSourceId; - * $newPrimaryPaymentSourceId = $event->newPrimaryPaymentSourceId; - * // @var User|CustomerBehavior $customer - * $customer = $event->customer; - * // ... - * } - * ); - * ``` - */ - public const EVENT_UPDATE_PRIMARY_PAYMENT_SOURCE = 'updatePrimaryPaymentSource'; - - /** - * @param User $user - * @param int|null $addressId - * @return bool - */ - public function savePrimaryShippingAddressId(User $user, ?int $addressId): bool - { - $customerRecord = $this->ensureCustomer($user); - $customerRecord->primaryShippingAddressId = $addressId; - /** @var User|CustomerBehavior $user */ - $user->primaryShippingAddressId = $addressId; - return $customerRecord->save(); - } - - /** - * @param User $user - * @param int|null $addressId - * @return bool - */ - public function savePrimaryBillingAddressId(User $user, ?int $addressId): bool - { - $customerRecord = $this->ensureCustomer($user); - $customerRecord->primaryBillingAddressId = $addressId; - /** @var User|CustomerBehavior $user */ - $user->primaryBillingAddressId = $addressId; - return $customerRecord->save(); - } - - /** - * @param User $user - * @param int|null $paymentSourceId - * @return bool - * @since 4.2 - */ - public function savePrimaryPaymentSourceId(User $user, ?int $paymentSourceId): bool - { - $customerRecord = $this->ensureCustomer($user); - - $originalPaymentSourceId = $customerRecord->primaryPaymentSourceId; - - // Only save customer record if the source is not already primary - if ($customerRecord->primaryPaymentSourceId == $paymentSourceId) { - return true; - } - - $customerRecord->primaryPaymentSourceId = $paymentSourceId; - - if (!$customerRecord->save()) { - return false; - } - - /** @var User|CustomerBehavior $user */ - $user->primaryPaymentSourceId = $paymentSourceId; - - if ($originalPaymentSourceId != $paymentSourceId) { - $event = new UpdatePrimaryPaymentSourceEvent([ - 'previousPrimaryPaymentSourceId' => $originalPaymentSourceId, - 'newPrimaryPaymentSourceId' => $paymentSourceId, - 'customer' => $user, - ]); - - // trigger the update primary payment source event - $this->trigger(self::EVENT_UPDATE_PRIMARY_PAYMENT_SOURCE, $event); - } - - return true; - } - - /** - * Handle user login - */ - public function loginHandler(): void - { - $impersonating = Craft::$app->getSession()->get(User::IMPERSONATE_KEY) !== null; - // Don't allow transition of current cart to a user that is being impersonated. - if ($impersonating) { - Plugin::getInstance()->getCarts()->forgetCart(); - } - - Plugin::getInstance()->getCarts()->restorePreviousCartForCurrentUser(); - } - - /** - * Sets the last used addresses on the customer on order completion. - * - * Consolidates any other orders using the same email address. - * - * Duplicates the address records used for the order so they are independent to the - * customers address book. - * - * @param Order $order - */ - public function orderCompleteHandler(Order $order): void - { - // Create a user account if requested - if ($order->registerUserOnOrderComplete) { - $this->_activateUserFromOrder($order); - } - - // Did they want to save addresses to the customers address book? - if ($order->saveBillingAddressOnOrderComplete || $order->saveShippingAddressOnOrderComplete) { - $this->_saveAddressesFromOrder($order); - } - - // clear the primary address flags if they were set as it only applies to the cart - if ($order->makePrimaryBillingAddress || $order->makePrimaryShippingAddress) { - OrderRecord::updateAll([ - 'makePrimaryBillingAddress' => false, - 'makePrimaryShippingAddress' => false, - ], - [ - 'id' => $order->id, - ] - ); - } - } - - /** - * @param array|Order[] $orders - * @return Order[] - * @since 3.2.0 - */ - public function eagerLoadCustomerForOrders(array $orders): array - { - $customerIds = ArrayHelper::getColumn($orders, 'customerId'); - /** @var User[] $users */ - $users = User::find()->id($customerIds)->limit(null)->indexBy('id')->all(); - - foreach ($orders as $key => $order) { - $customerId = $order->getCustomerId(); - if (isset($users[$customerId])) { - $order->setCustomer($users[$customerId]); - $orders[$key] = $order; - } - } - - return $orders; - } - - /** - * Returns a customer record by a user element, creating one if none already exists. - * - * @param User $user - * @return CustomerRecord - */ - public function ensureCustomer(User $user): CustomerRecord - { - /** @var CustomerRecord|null $customerRecord */ - $customerRecord = CustomerRecord::find()->where(['customerId' => $user->id])->one(); - if (!$customerRecord) { - $customerRecord = new CustomerRecord(); - $customerRecord->customerId = $user->id; - $customerRecord->save(); - } - - return $customerRecord; - } - - /** - * @return bool Whether the data moved successfully - * @throws ElementNotFoundException|\yii\db\Exception - * @since 4.1.0 - */ - public function transferCustomerData(User $fromCustomer, User $toCustomer): bool - { - $fromId = $fromCustomer->id; - $toId = $toCustomer->id; - - /** @var User|null $fromUser */ - $fromUser = User::find()->id($fromId)->one(); - /** @var User|null $toUser */ - $toUser = User::find()->id($toId)->one(); - - if ($fromUser === null) { - throw new ElementNotFoundException('User ID:', $fromId); - } - - if ($toUser === null) { - throw new ElementNotFoundException('User ID:', $toId); - } - - $userRefs = [ - Table::ORDERHISTORIES => 'userId', - Table::SUBSCRIPTIONS => 'userId', - Table::TRANSACTIONS => 'userId', - Table::ORDERS => 'customerId', - Table::PAYMENTSOURCES => 'customerId', - ]; - - foreach ($userRefs as $table => $column) { - Db::update($table, [ - $column => $toId, - ], [ - $column => $fromId, - ], [], false); - } - - $previousUses = (new Query())->select(['discountId', 'uses'])->from(Table::CUSTOMER_DISCOUNTUSES)->where(['customerId' => $fromId])->pairs(); - $toUses = (new Query())->select(['discountId', 'uses'])->from(Table::CUSTOMER_DISCOUNTUSES)->where(['customerId' => $toId])->pairs(); - - foreach ($previousUses as $discountId => $uses) { - if (isset($toUses[$discountId])) { - Db::update( - table: Table::CUSTOMER_DISCOUNTUSES, - columns: ['uses' => new Expression("uses + $uses")], - condition: [ - 'customerId' => $toId, - 'discountId' => $discountId, - ], - params: [], - updateTimestamp: false - ); - } else { - Db::insert( - table: Table::CUSTOMER_DISCOUNTUSES, - columns: [ - 'uses' => $uses, - 'customerId' => $toId, - 'discountId' => $discountId, - ] - ); - } - - // Remove uses from fromCustomer - Db::update( - table: Table::CUSTOMER_DISCOUNTUSES, - columns: ['uses' => 0], - condition: [ - 'customerId' => $fromId, - 'discountId' => $discountId, - ], - params: [], - updateTimestamp: false - ); - } - - - $fromEmail = $fromUser->email; - $toEmail = $toUser->email; - - $emailRefs = [ - Table::ORDERS => 'email', - ]; - - foreach ($emailRefs as $table => $column) { - Db::update($table, [ - $column => $toEmail, - ], [ - $column => $fromEmail, - ], [], false); - } - - return true; - } - - /** - * - * @param Order $order - * @return void - * @throws \Throwable - * @throws InvalidElementException - * @throws UnsupportedSiteException - */ - private function _saveAddressesFromOrder(Order $order): void - { - // Only for completed orders - if ($order->isCompleted === false) { - return; - } - - // Check for a credentialed user - if ($order->getCustomer() === null || !$order->getCustomer()->getIsCredentialed()) { - return; - } - - $saveBillingAddress = $order->saveBillingAddressOnOrderComplete && $order->sourceBillingAddressId === null && $order->billingAddressId; - $saveShippingAddress = $order->saveShippingAddressOnOrderComplete && $order->sourceShippingAddressId === null && $order->shippingAddressId; - $newSourceBillingAddressId = null; - $newSourceShippingAddressId = null; - - if ($saveBillingAddress && $saveShippingAddress && $order->hasMatchingAddresses()) { - // Only save one address if they are matching - $newAddress = Craft::$app->getElements()->duplicateElement( - $order->getBillingAddress(), - [ - 'primaryOwner' => $order->getCustomer(), - 'owner' => $order->getCustomer(), - ] - ); - $newSourceBillingAddressId = $newAddress->id; - $newSourceShippingAddressId = $newAddress->id; - } else { - if ($saveBillingAddress) { - $newBillingAddress = Craft::$app->getElements()->duplicateElement($order->getBillingAddress(), - [ - 'primaryOwner' => $order->getCustomer(), - 'owner' => $order->getCustomer(), - ] - ); - $newSourceBillingAddressId = $newBillingAddress->id; - } - - if ($saveShippingAddress) { - $newShippingAddress = Craft::$app->getElements()->duplicateElement( - $order->getShippingAddress(), - [ - 'primaryOwner' => $order->getCustomer(), - 'owner' => $order->getCustomer(), - ] - ); - $newSourceShippingAddressId = $newShippingAddress->id; - } - } - - if ($newSourceBillingAddressId) { - $order->sourceBillingAddressId = $newSourceBillingAddressId; - } - - if ($newSourceShippingAddressId) { - $order->sourceShippingAddressId = $newSourceShippingAddressId; - } - - // Since we saved the primary addresses, we can now set the primary if they chose that also - if ($order->makePrimaryShippingAddress && $order->sourceShippingAddressId) { - $this->savePrimaryShippingAddressId($order->getCustomer(), $order->sourceShippingAddressId); - } - - if ($order->makePrimaryBillingAddress && $order->sourceBillingAddressId) { - $this->savePrimaryBillingAddressId($order->getCustomer(), $order->sourceBillingAddressId); - } - - // Manually update the order DB record to avoid looped element saves - if ($newSourceBillingAddressId || $newSourceShippingAddressId) { - \craft\commerce\records\Order::updateAll([ - 'sourceBillingAddressId' => $order->sourceBillingAddressId, - 'sourceShippingAddressId' => $order->sourceShippingAddressId, - ], - [ - 'id' => $order->id, - ] - ); - } - } - - /** - * Makes sure the user has an email address and sets them to pending and sends the activation email - */ - private function _activateUserFromOrder(Order $order): void - { - $user = $order->getCustomer(); - if (!$user || $user->active || $user->locked || $user->suspended) { - return; - } - - $billingAddress = $order->getBillingAddress(); - $shippingAddress = $order->getShippingAddress(); - - if (!$user->fullName) { - $user->fullName = $billingAddress?->fullName ?? $shippingAddress?->fullName ?? ''; - } - - $user->username = $order->getEmail(); - $user->pending = true; - $user->setScenario(Element::SCENARIO_ESSENTIALS); - - // @TODO Remove this property_exists guard once Commerce requires a Craft version where User::$affiliatedSiteId always exists - if (property_exists($user, 'affiliatedSiteId')) { - $user->affiliatedSiteId = $order->orderSiteId; - } - - if (Craft::$app->getElements()->saveElement($user)) { - Craft::$app->getUsers()->assignUserToDefaultGroup($user); - - Event::once(Mailer::class, Mailer::EVENT_BEFORE_PREP, function(MailEvent $event) use ($user) { - if (!$event->message instanceof Message) { - return; - } - - if ($event->message->key !== 'account_activation') { - return; - } - - if ($event->message->siteId === null && property_exists($user, 'affiliatedSiteId') && $user->affiliatedSiteId) { - $event->message->siteId = $user->affiliatedSiteId; - } - }); - - $emailSent = Craft::$app->getUsers()->sendActivationEmail($user); - - if (!$emailSent) { - Craft::warning('"registerUserOnOrderComplete" used to create the user, but couldn’t send an activation email. Check your email settings.', __METHOD__); - } - - if ($billingAddress || $shippingAddress) { - $newAttributes = [ - 'owner' => $user, - 'primaryOwner' => $user, - ]; - - // If there is only one address make sure we don't add duplicates to the user - if ($order->hasMatchingAddresses()) { - $newAttributes['title'] = Craft::t('app', 'Address'); - $shippingAddress = null; - } - - // Copy addresses to user - if ($billingAddress) { - $newBillingAddress = Craft::$app->getElements()->duplicateElement($billingAddress, $newAttributes); - - /** - * Because we are cloning from an order address the `CustomerAddressBehavior` hasn't been instantiated - * therefore we are unable to simply set the `isPrimaryBilling` property when specifying the new attributes during duplication. - */ - if (!$newBillingAddress->hasErrors()) { - $this->savePrimaryBillingAddressId($user, $newBillingAddress->id); - - if ($order->hasMatchingAddresses()) { - $this->savePrimaryShippingAddressId($user, $newBillingAddress->id); - } - } - } - - if ($shippingAddress) { - $newShippingAddress = Craft::$app->getElements()->duplicateElement($shippingAddress, $newAttributes); - - /** - * Because we are cloning from an order address the `CustomerAddressBehavior` hasn't been instantiated - * therefore we are unable to simply set the `isPrimaryShipping` property when specifying the new attributes during duplication. - */ - if (!$newShippingAddress->hasErrors()) { - $this->savePrimaryShippingAddressId($user, $newShippingAddress->id); - } - } - } - } else { - $errors = $user->getErrors(); - Craft::warning('Could not create user on order completion.', __METHOD__); - Craft::warning($errors, __METHOD__); - } - } -} diff --git a/src/services/Discounts.php b/src/services/Discounts.php deleted file mode 100644 index 69cad478ff..0000000000 --- a/src/services/Discounts.php +++ /dev/null @@ -1,1498 +0,0 @@ - - * @since 2.0y - */ -class Discounts extends Component -{ - /** - * @event DiscountEvent The event that is triggered before a discount is saved. - * - * ```php - * use craft\commerce\events\DiscountEvent; - * use craft\commerce\services\Discounts; - * use craft\commerce\models\Discount; - * use yii\base\Event; - * - * Event::on( - * Discounts::class, - * Discounts::EVENT_BEFORE_SAVE_DISCOUNT, - * function(DiscountEvent $event) { - * // @var Discount $discount - * $discount = $event->discount; - * // @var bool $isNew - * $isNew = $event->isNew; - * - * // Let an external CRM know about a client’s new discount - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_SAVE_DISCOUNT = 'beforeSaveDiscount'; - - /** - * @event DiscountEvent The event that is triggered after a discount is saved. - * - * ```php - * use craft\commerce\events\DiscountEvent; - * use craft\commerce\services\Discounts; - * use craft\commerce\models\Discount; - * use yii\base\Event; - * - * Event::on( - * Discounts::class, - * Discounts::EVENT_AFTER_SAVE_DISCOUNT, - * function(DiscountEvent $event) { - * // @var Discount $discount - * $discount = $event->discount; - * // @var bool $isNew - * $isNew = $event->isNew; - * - * // Set this discount as default in an external CRM - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_SAVE_DISCOUNT = 'afterSaveDiscount'; - - /** - * @event DiscountEvent The event that is triggered after a discount is deleted. - * - * ```php - * use craft\commerce\events\DiscountEvent; - * use craft\commerce\services\Discounts; - * use craft\commerce\models\Discount; - * use yii\base\Event; - * - * Event::on( - * Discounts::class, - * Discounts::EVENT_AFTER_DELETE_DISCOUNT, - * function(DiscountEvent $event) { - * // @var Discount $discount - * $discount = $event->discount; - * - * // Remove this discount from a payment gateway - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_DELETE_DISCOUNT = 'afterDeleteDiscount'; - - /** - * @event MatchLineItemEvent The event that is triggered when a line item is matched with a discount. - * - * This event will be raised if all standard conditions are met. - * You may set the `isValid` property to `false` on the event to prevent the matching of the discount to the line item. - * - * ```php - * use craft\commerce\services\Discounts; - * use craft\commerce\events\MatchLineItemEvent; - * use craft\commerce\models\Discount; - * use craft\commerce\models\LineItem; - * use yii\base\Event; - * - * Event::on( - * Discounts::class, - * Discounts::EVENT_DISCOUNT_MATCHES_LINE_ITEM, - * function(MatchLineItemEvent $event) { - * // @var LineItem $lineItem - * $lineItem = $event->lineItem; - * // @var Discount $discount - * $discount = $event->discount; - * - * // Check some business rules and prevent a match in special cases - * // ... - * } - * ); - * ``` - */ - public const EVENT_DISCOUNT_MATCHES_LINE_ITEM = 'discountMatchesLineItem'; - - /** - * @event MatchOrderEvent The event that is triggered when an order is matched with a discount. - * - * You may set the `isValid` property to `false` on the event to prevent the matching of the discount with the order. - * - * ```php - * use craft\commerce\services\Discounts; - * use craft\commerce\events\MatchOrderEvent; - * use craft\commerce\models\Discount; - * use craft\commerce\elements\Order; - * use yii\base\Event; - * - * Event::on( - * Discounts::class, - * Discounts::EVENT_DISCOUNT_MATCHES_ORDER, - * function(MatchOrderEvent $event) { - * // @var Order $order - * $order = $event->order; - * // @var Discount $discount - * $discount = $event->discount; - * - * // Check some business rules and prevent a match in special cases - * // ... $event->isValid = false; // set to false if you want it to NOT match as it would. - * } - * ); - * ``` - */ - public const EVENT_DISCOUNT_MATCHES_ORDER = 'discountMatchesOrder'; - - /** - * @var Collection[]|null - */ - private ?array $_allDiscounts = null; - - /** - * @var Discount[][]|null - */ - private ?array $_activeDiscountsByKey = null; - - /** - * @var array|null - */ - private ?array $_matchingLineItemCategoryCondition = null; - - /** - * Get a discount by its ID. - * - * @param int $id - * @param int|null $storeId - * @return Discount|null - * @throws InvalidConfigException - * @throws StoreNotFoundException - */ - public function getDiscountById(int $id, ?int $storeId = null): ?Discount - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - // Keep this as a query for the performance boost - $discounts = $this->_createDiscountQuery() - ->andWhere(['[[discounts.id]]' => $id]) - ->andWhere(['storeId' => $storeId]) - ->all(); - - if (!$discounts) { - return null; - } - - return ArrayHelper::firstValue($this->_populateDiscounts($discounts)); - } - - /** - * Get all discounts. - * - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - * @throws StoreNotFoundException - */ - public function getAllDiscounts(?int $storeId = null): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if ($this->_allDiscounts === null || !isset($this->_allDiscounts[$storeId])) { - $discounts = $this->_createDiscountQuery() - ->where(['storeId' => $storeId]) - ->all(); - - if ($this->_allDiscounts === null) { - $this->_allDiscounts = []; - } - - if (!empty($discounts)) { - $this->_allDiscounts[$storeId] = collect($this->_populateDiscounts($discounts)); - } else { - $this->_allDiscounts[$storeId] = collect(); - } - } - - return $this->_allDiscounts[$storeId]; - } - - /** - * Get all currently active discounts - * We pass the Order to attempt ot optimize the query to only possible discounts that might match, - * eliminating ones that definitely will not match. - * - * @param Order|null $order - * @return Discount[] - * @throws \Exception - * @since 2.2.14 - */ - public function getAllActiveDiscounts(?Order $order = null): array - { - $purchasableIds = []; - if ($order) { - $purchasableIds = Collection::make($order->getLineItems())->pluck('purchasableId')->unique()->all(); - } - - // Date condition for use with key - if ($order && $order->dateOrdered) { - $date = $order->dateOrdered; - } else { - // We use a round the time so we can have a cache within the same request (rounded to 1 minute flat, no seconds) - $date = new DateTime(); - $date->setTime((int)$date->format('H'), (int)(round($date->format('i') / 1) * 1)); - } - - $store = $order ? $order->getStore() : Plugin::getInstance()->getStores()->getCurrentStore(); - - // Coupon condition key - $couponKey = ($order && $order->couponCode) ? $order->couponCode : '*'; - $dateKey = DateTimeHelper::toIso8601($date); - $storeKey = $order ? $order->getStore()->id : '*'; - $purchasablesKey = !empty($purchasableIds) ? md5(serialize($purchasableIds)) : '*'; - $itemSubtotalKey = $order ? $order->getItemSubtotal() : '*'; - $orderTotalQtyKey = $order ? $order->getTotalQty() : '*'; - $orderEmailKey = ($order && $order->getEmail()) ? $order->getEmail() : '*'; - - $cacheKey = implode(':', [ - $couponKey, - $dateKey, - $storeKey, - $purchasablesKey, - $itemSubtotalKey, - $orderTotalQtyKey, - $orderEmailKey, - ]); - - $cacheKeyMd5 = md5($cacheKey); - - if (isset($this->_activeDiscountsByKey[$cacheKeyMd5])) { - return $this->_activeDiscountsByKey[$cacheKeyMd5]; - } - - $discountQuery = $this->_createDiscountQuery() - // Restricted by enabled discounts - ->where([ - 'enabled' => true, - ]) - // Restricted by store - ->andWhere(['storeId' => $store->id]) - // Restrict by things that a definitely not in date - ->andWhere([ - 'or', - ['dateFrom' => null], - ['<=', 'dateFrom', Db::prepareDateForDb($date)], - ]) - ->andWhere([ - 'or', - ['dateTo' => null], - ['>=', 'dateTo', Db::prepareDateForDb($date)], - ]) - ->andWhere([ - 'or', - ['totalDiscountUseLimit' => 0], - ['<', 'totalDiscountUses', new Expression('[[totalDiscountUseLimit]]')], - ]); - - // Pre-qualify discounts based on purchase total - if ($order) { - if ($order->getEmail()) { - $emailUsesSubQuery = (new Query()) - ->select([new Expression('COALESCE(SUM([[edu.uses]]), 0)')]) - ->from(['edu' => Table::EMAIL_DISCOUNTUSES]) - ->where(new Expression('[[edu.discountId]] = [[discounts.id]]')) - ->andWhere(['email' => $order->getEmail()]); - - $discountQuery->andWhere([ - 'or', - ['perEmailLimit' => 0], - ['and', ['>', 'perEmailLimit', 0], ['>', 'perEmailLimit', $emailUsesSubQuery]], - ]); - } else { - $discountQuery->andWhere(['perEmailLimit' => 0]); - } - - - $discountQuery->andWhere([ - 'or', - ['purchaseTotal' => 0], - ['and', ['allPurchasables' => true], ['allCategories' => true], ['<=', 'purchaseTotal', $order->getItemSubtotal()]], - ['allPurchasables' => false], - ['allCategories' => false], - ]); - - $discountQuery->andWhere([ - 'or', - ['purchaseQty' => 0, 'maxPurchaseQty' => 0], - ['and', ['allPurchasables' => true], ['allCategories' => true], ['>', 'purchaseQty', 0], ['maxPurchaseQty' => 0], ['<=', 'purchaseQty', $order->getTotalQty()]], - ['and', ['allPurchasables' => true], ['allCategories' => true], ['>', 'maxPurchaseQty', 0], ['purchaseQty' => 0], ['>=', 'maxPurchaseQty', $order->getTotalQty()]], - ['and', ['allPurchasables' => true], ['allCategories' => true], ['>', 'maxPurchaseQty', 0], ['>', 'purchaseQty', 0], ['<=', 'purchaseQty', $order->getTotalQty()], ['>=', 'maxPurchaseQty', $order->getTotalQty()]], - ['allPurchasables' => false], - ['allCategories' => false], - ]); - } - - $couponSubQuery = (new Query()) - ->from(Table::COUPONS) - ->leftJoin(Table::DISCOUNTS . ' disc', '[[disc.id]] = [[discountId]]') - ->where(new Expression('[[discountId]] = [[discounts.id]]')); - - // If the order has a coupon code let's only get discounts for that code, or discounts that do not require a code - if ($order && $order->couponCode) { - if (Craft::$app->getDb()->getIsPgsql()) { - $codeWhere = ['ilike', 'code', $order->couponCode]; - } else { - $codeWhere = ['code' => $order->couponCode]; - } - - $discountQuery->andWhere( - [ - 'or', - // Find discount where the coupon code matches - [ - 'exists', (clone $couponSubQuery) - ->andWhere(['requireCouponCode' => true]) - ->andWhere($codeWhere) - ->andWhere([ - 'or', - ['maxUses' => null], - new Expression('[[uses]] < [[maxUses]]'), - ] - ), - ], - // OR find discounts that do not have a coupon code requirement - ['requireCouponCode' => false], - ] - ); - } elseif ($order && !$order->couponCode) { - // only discounts that do not have a coupon code requirement - $discountQuery->andWhere(['requireCouponCode' => false]); - } - - if ($order && !empty($purchasableIds)) { - $matchPurchasableSubQuery = (new Query()) - ->from(['subdp' => Table::DISCOUNT_PURCHASABLES]) - ->where(new Expression('[[subdp.discountId]] = [[discounts.id]]')) - ->andWhere(['[[subdp.purchasableId]]' => $purchasableIds]); - - $discountQuery->andWhere( - [ - 'or', - ['allPurchasables' => true], - [ - 'exists', $matchPurchasableSubQuery, - ], - ] - ); - } - - $discountResults = $discountQuery->all(); - $discounts = $this->_populateDiscounts($discountResults); - $this->_activeDiscountsByKey[$cacheKeyMd5] = $discounts; - - return $this->_activeDiscountsByKey[$cacheKeyMd5]; - } - - /** - * Is discount coupon available to the order - * - * @param string|null $explanation - * @throws InvalidConfigException - * @throws \Exception - */ - public function orderCouponAvailable(Order $order, string &$explanation = null): bool - { - $discount = $this->getDiscountByCode($order->couponCode, $order->storeId); - - if (!$discount) { - $explanation = Craft::t('commerce', 'Coupon not valid.'); - return false; - } - - if (!$discount->requireCouponCode) { - $explanation = Craft::t('commerce', 'Coupon not valid.'); - return false; - } - - if (!$this->_isDiscountCouponCodeValid($order, $discount)) { - $explanation = Craft::t('commerce', 'Coupon not valid.'); - return false; - } - - if ($discount->hasOrderCondition() && !$discount->getOrderCondition()->matchElement($order)) { - $explanation = Craft::t('commerce', 'Coupon can not apply discount to this order.'); - - return false; - } - - if ($discount->hasCustomerCondition() && (!$order->getCustomer() || !$discount->getCustomerCondition()->matchElement($order->getCustomer()))) { - $explanation = Craft::t('commerce', 'Coupon can not apply discount to this order due to customer mismatch.'); - return false; - } - - if ($discount->hasShippingAddressCondition() && (!$order->getShippingAddress() || !$discount->getShippingAddressCondition()->matchElement($order->getShippingAddress()))) { - $explanation = Craft::t('commerce', 'Coupon can not apply discount to this order due to address mismatch.'); - return false; - } - - if ($discount->hasBillingAddressCondition() && (!$order->getBillingAddress() || !$discount->getBillingAddressCondition()->matchElement($order->getBillingAddress()))) { - $explanation = Craft::t('commerce', 'Coupon can not apply discount to this order due to address mismatch.'); - return false; - } - - if (!$this->_isDiscountConditionFormulaValid($order, $discount)) { - $explanation = Craft::t('commerce', 'Discount is not allowed for the order'); - return false; - } - - - if (!$this->_isDiscountDateValid($order, $discount)) { - $explanation = Craft::t('commerce', 'Discount is out of date.'); - return false; - } - - if (!$this->_isDiscountTotalUseLimitValid($discount)) { - $explanation = Craft::t('commerce', 'Discount use has reached its limit.'); - return false; - } - - if (!$this->_isDiscountPerUserUsageValid($discount, $order->getCustomer())) { - $explanation = Craft::t('commerce', 'This coupon is for registered users and limited to {limit} uses.', [ - 'limit' => $discount->perUserLimit, - ]); - return false; - } - - if (!$this->_isDiscountEmailRequirementValid($discount, $order)) { - $explanation = Craft::t('commerce', 'This coupon requires an email address.'); - return false; - } - - if (!$this->_isDiscountPerEmailLimitValid($discount, $order)) { - $explanation = Craft::t('commerce', 'This coupon is limited to {limit} uses.', [ - 'limit' => $discount->perEmailLimit, - ]); - return false; - } - - return true; - } - - /** - * Returns an enabled discount by its code, regardless of the discount's `requireCouponCode` value. - * - * @throws \Exception - */ - public function getDiscountByCode(?string $code, ?int $storeId = null): ?Discount - { - if ($code === null || $code === '') { - return null; - } - - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - $query = $this->_createDiscountQuery()->where(['storeId' => $storeId]); - $query->innerJoin(Table::COUPONS . ' coupons', '[[coupons.discountId]] = [[discounts.id]]'); - if (Craft::$app->getDb()->getIsPgsql()) { - $query->andWhere(['ilike', '[[coupons.code]]', $code]); - } else { - $query->andWhere(['[[coupons.code]]' => $code]); - } - $discounts = $query->all(); - - if (!$discounts) { - return null; - } - - return ArrayHelper::firstWhere($this->_populateDiscounts($discounts), fn(Discount $discount) => $discount->enabled && - ArrayHelper::contains($discount->getCoupons(), fn(Coupon $coupon) => strcasecmp($coupon->code, $code) === 0)); - } - - /** - * @since 2.2 - */ - public function getDiscountsRelatedToPurchasable(PurchasableInterface $purchasable): array - { - $discounts = []; - - if ($purchasable->getId()) { - // @TODO Optimize this loop on stores with many discounts; the per-discount Category/Entry relatedTo queries make it O(discounts) and can be slow - foreach ($this->getAllDiscounts($purchasable->getStoreId()) as $discount) { - // Get discount by related purchasable - $purchasableIds = $discount->getPurchasableIds(); - $id = $purchasable->getId(); - - // Get discount by related category - $relatedTo = [$discount->categoryRelationshipType => $purchasable->getPromotionRelationSource()]; - $categoryIds = $discount->getCategoryIds(); - $relatedCategories = Category::find()->id($categoryIds)->relatedTo($relatedTo)->ids(); - $relatedEntries = Entry::find()->id($categoryIds)->relatedTo($relatedTo)->ids(); - $relatedCategoriesOrEntries = array_merge($relatedCategories, $relatedEntries); - - if (in_array($id, $purchasableIds, false) || !empty($relatedCategoriesOrEntries)) { - $discounts[$discount->id] = $discount; - } - } - } - - return $discounts; - } - - /** - * Match a line item against a discount. - * - * @throws \Exception - */ - public function matchLineItem(LineItem $lineItem, Discount $discount, bool $matchOrder = false): bool - { - if ($matchOrder && !$this->matchOrder($lineItem->order, $discount)) { - return false; - } - - $siteId = $lineItem->order->orderSiteId ?? Craft::$app->getSites()->getCurrentSite()->id; - - if ($lineItem->getOnPromotion() && $discount->excludeOnPromotion) { - return false; - } - - if (!$lineItem->getIsPromotable()) { - return false; - } - - if ($lineItem->type === LineItemType::Purchasable) { - // can't match something not promotable - /** @var Purchasable|null $purchasable */ - $purchasable = $lineItem->getPurchasable(); - - if (!$discount->allPurchasables && !in_array($purchasable->id, $discount->getPurchasableIds(), false)) { - return false; - } - - // @TODO Rename Discount::$allCategories to $allEntries in Commerce 6.0 to reflect the entryfication (categoryIds may now reference entry IDs) - if (!$discount->allCategories) { - $key = 'relationshipType:' . $discount->categoryRelationshipType . ':purchasableId:' . $purchasable->getId() . ':categoryIds:' . implode('|', $discount->getCategoryIds()); - - if (!isset($this->_matchingLineItemCategoryCondition[$key])) { - $relatedTo = [$discount->categoryRelationshipType => $purchasable->getPromotionRelationSource()]; - - $relatedEntries = Entry::find()->siteId($siteId)->relatedTo($relatedTo)->ids(); - $relatedCategories = Category::find()->siteId($siteId)->relatedTo($relatedTo)->ids(); - - $relatedCategoriesOrEntries = array_merge($relatedEntries, $relatedCategories); - $purchasableIsRelateToOneOrMoreCategories = (bool)array_intersect($relatedCategoriesOrEntries, $discount->getCategoryIds()); - if (!$purchasableIsRelateToOneOrMoreCategories) { - return $this->_matchingLineItemCategoryCondition[$key] = false; - } - $this->_matchingLineItemCategoryCondition[$key] = true; - } elseif ($this->_matchingLineItemCategoryCondition[$key] === false) { - return false; - } - } - } - - $event = new MatchLineItemEvent(compact('lineItem', 'discount')); - - if ($this->hasEventHandlers(self::EVENT_DISCOUNT_MATCHES_LINE_ITEM)) { - $this->trigger(self::EVENT_DISCOUNT_MATCHES_LINE_ITEM, $event); - } - - return $event->isValid; - } - - /** - * @throws \Exception - */ - public function matchOrder(Order $order, Discount $discount): bool - { - if (!$discount->enabled) { - return false; - } - - $allItemsMatch = ($discount->allPurchasables && $discount->allCategories); - - if ($discount->hasOrderCondition() && !$discount->getOrderCondition()->matchElement($order)) { - return false; - } - - if ($discount->hasCustomerCondition() && (!$order->getCustomer() || !$discount->getCustomerCondition()->matchElement($order->getCustomer()))) { - return false; - } - - if ($discount->hasShippingAddressCondition() && (!$order->getShippingAddress() || !$discount->getShippingAddressCondition()->matchElement($order->getShippingAddress()))) { - return false; - } - - if ($discount->hasBillingAddressCondition() && (!$order->getBillingAddress() || !$discount->getBillingAddressCondition()->matchElement($order->getBillingAddress()))) { - return false; - } - - if (!$this->_isDiscountCouponCodeValid($order, $discount)) { - return false; - } - - if (!$this->_isDiscountDateValid($order, $discount)) { - return false; - } - - if (!$this->_isDiscountTotalUseLimitValid($discount)) { - return false; - } - - if (!$this->_isDiscountPerUserUsageValid($discount, $order->getCustomer())) { - return false; - } - - if (!$this->_isDiscountEmailRequirementValid($discount, $order)) { - return false; - } - - if (!$this->_isDiscountPerEmailLimitValid($discount, $order)) { - return false; - } - - if (!$this->_isDiscountConditionFormulaValid($order, $discount)) { - return false; - } - - if ($allItemsMatch && $discount->purchaseTotal > 0 && $order->getItemSubtotal() < $discount->purchaseTotal) { - return false; - } - - if ($allItemsMatch && $discount->purchaseQty > 0 && $order->getTotalQty() < $discount->purchaseQty) { - return false; - } - - if ($allItemsMatch && $discount->maxPurchaseQty > 0 && $order->getTotalQty() > $discount->maxPurchaseQty) { - return false; - } - - // Check to see if we need to match on data related to the lineItems - if (!$discount->allPurchasables || !$discount->allCategories) { - - // Get matching line items but don't match the order again - $matchingItems = collect($order->getLineItems()) - ->filter(fn($item) => $this->matchLineItem($item, $discount)); - - if ($matchingItems->isEmpty()) { - return false; - } - - $matchingQty = $matchingItems->sum('qty'); - $matchingTotal = $matchingItems->sum('subtotal'); - - if ($discount->purchaseTotal > 0 && $matchingTotal < $discount->purchaseTotal) { - return false; - } - - if ($discount->purchaseQty > 0 && $matchingQty < $discount->purchaseQty) { - return false; - } - - if ($discount->maxPurchaseQty > 0 && $matchingQty > $discount->maxPurchaseQty) { - return false; - } - } - - // Raise the 'beforeMatchLineItem' event - $event = new MatchOrderEvent(compact('order', 'discount')); - - if ($this->hasEventHandlers(self::EVENT_DISCOUNT_MATCHES_ORDER)) { - $this->trigger(self::EVENT_DISCOUNT_MATCHES_ORDER, $event); - } - - return $event->isValid; - } - - - /** - * Save a discount. - * - * @param Discount $model the discount being saved - * @param bool $runValidation should we validate this discount before saving. - * @throws \Exception - */ - public function saveDiscount(Discount $model, bool $runValidation = true): bool - { - $isNew = !$model->id; - - if ($model->id) { - $record = DiscountRecord::findOne($model->id); - - if (!$record) { - throw new Exception(Craft::t('commerce', 'No discount exists with the ID “{id}”', ['id' => $model->id])); - } - } else { - $record = new DiscountRecord(); - } - - // Make sure the datetime attributes are populated before firing the event - if (!$isNew) { - $model->dateCreated = DateTimeHelper::toDateTime($record->dateCreated); - $model->dateUpdated = DateTimeHelper::toDateTime($record->dateUpdated); - } - - // Raise the beforeSaveDiscount event - if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_DISCOUNT)) { - $this->trigger(self::EVENT_BEFORE_SAVE_DISCOUNT, new DiscountEvent([ - 'discount' => $model, - 'isNew' => $isNew, - ])); - } - - if ($runValidation && !$model->validate()) { - Craft::info('Discount not saved due to validation error.', __METHOD__); - - return false; - } - - $record->storeId = $model->storeId; - $record->name = $model->name; - $record->description = $model->description; - $record->dateFrom = $model->dateFrom; - $record->dateTo = $model->dateTo; - $record->enabled = $model->enabled; - $record->stopProcessing = $model->stopProcessing; - $record->orderCondition = $model->hasOrderCondition() ? $model->getOrderCondition()->getConfig() : null; - $record->customerCondition = $model->hasCustomerCondition() ? $model->getCustomerCondition()->getConfig() : null; - $record->shippingAddressCondition = $model->hasShippingAddressCondition() ? $model->getShippingAddressCondition()->getConfig() : null; - $record->billingAddressCondition = $model->hasBillingAddressCondition() ? $model->getBillingAddressCondition()->getConfig() : null; - $record->requireCouponCode = $model->requireCouponCode; - $record->orderConditionFormula = $model->orderConditionFormula; - $record->purchaseQty = $model->purchaseQty; - $record->maxPurchaseQty = $model->maxPurchaseQty; - $record->baseDiscount = $model->baseDiscount; - $record->purchaseTotal = $model->purchaseTotal; - $record->perItemDiscount = $model->perItemDiscount; - $record->percentDiscount = $model->percentDiscount; - $record->percentageOffSubject = $model->percentageOffSubject; - $record->hasFreeShippingForMatchingItems = $model->hasFreeShippingForMatchingItems; - $record->hasFreeShippingForOrder = $model->hasFreeShippingForOrder; - $record->excludeOnPromotion = $model->excludeOnPromotion; - $record->perUserLimit = $model->perUserLimit; - $record->perEmailLimit = $model->perEmailLimit; - $record->totalDiscountUseLimit = $model->totalDiscountUseLimit; - $record->ignorePromotions = $model->ignorePromotions; - $record->appliedTo = $model->appliedTo; - $record->purchasableIds = $model->getPurchasableIds(); - $record->categoryIds = $model->getCategoryIds(); - - // If the discount is new, set the sort order to be at the top of the list. - // We will ensure the sort orders are sequential when we save the discount. - $sortOrder = $record->sortOrder ?: 0; - - $record->sortOrder = $sortOrder; - $record->couponFormat = $model->couponFormat; - - $record->categoryRelationshipType = $model->categoryRelationshipType; - if ($record->allCategories = $model->allCategories) { - $model->setCategoryIds([]); - $record->categoryIds = null; - } - if ($record->allPurchasables = $model->allPurchasables) { - $model->setPurchasableIds([]); - $record->purchasableIds = null; - } - - $db = Craft::$app->getDb(); - $transaction = $db->beginTransaction(); - - try { - $record->save(false); - $model->id = $record->id; - - // Update datetime attributes after save - $model->dateCreated = DateTimeHelper::toDateTime($record->dateCreated); - $model->dateUpdated = DateTimeHelper::toDateTime($record->dateUpdated); - - DiscountPurchasableRecord::deleteAll(['discountId' => $model->id]); - DiscountCategoryRecord::deleteAll(['discountId' => $model->id]); - - $siteIds = $model->getStore()->getSites()->pluck('id')->all(); - - foreach ($model->getCategoryIds() as $categoryId) { - $relation = new DiscountCategoryRecord(); - $relation->categoryId = $categoryId; - $relation->discountId = $model->id; - $relation->save(false); - } - - foreach ($model->getPurchasableIds() as $purchasableId) { - $relation = new DiscountPurchasableRecord(); - $element = Craft::$app->getElements()->getElementById($purchasableId, siteId: $siteIds); - $relation->purchasableType = $element::class; - $relation->purchasableId = $purchasableId; - $relation->discountId = $model->id; - $relation->save(false); - } - - Plugin::getInstance()->getCoupons()->saveDiscountCoupons($model); - $transaction->commit(); - - // After saving the discount, ensure the sort order for all discounts is sequential - $this->ensureSortOrder($model->storeId); - - // Raise the afterSaveDiscount event - if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_DISCOUNT)) { - $this->trigger(self::EVENT_AFTER_SAVE_DISCOUNT, new DiscountEvent([ - 'discount' => $model, - 'isNew' => $isNew, - ])); - } - - // Reset internal cache - $this->_allDiscounts = null; - $this->_activeDiscountsByKey = null; - $this->_matchingLineItemCategoryCondition = null; - - return true; - } catch (\Exception $e) { - $transaction->rollBack(); - throw $e; - } - } - - /** - * Delete a discount by its ID. - * - * @throws Throwable - * @throws StaleObjectException - */ - public function deleteDiscountById(int $id): bool - { - $discountRecord = DiscountRecord::findOne($id); - - if (!$discountRecord) { - return false; - } - - // Get the Discount model before deletion to pass to the Event. - $discount = $this->getDiscountById($id, $discountRecord->storeId); - $storeId = $discount->storeId; - - $result = (bool)$discountRecord->delete(); - - //Raise the afterDeleteDiscount event - if ($result) { - // Ensure discount table sort order - $this->ensureSortOrder($storeId); - - if ($this->hasEventHandlers(self::EVENT_AFTER_DELETE_DISCOUNT)) { - $this->trigger(self::EVENT_AFTER_DELETE_DISCOUNT, new DiscountEvent([ - 'discount' => $discount, - 'isNew' => false, - ])); - } - } - - // Reset internal cache - $this->_allDiscounts = null; - $this->_activeDiscountsByKey = null; - $this->_matchingLineItemCategoryCondition = null; - - return $result; - } - - /** - * @return void - * @throws \yii\db\Exception - * @since 4.4.0 - */ - public function ensureSortOrder(?int $storeId = null): void - { - // @TODO Iterate over all stores when no storeId is passed, so sort order is normalized per-store rather than only for the current store - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - $table = Table::DISCOUNTS; - - $isPsql = Craft::$app->getDb()->getIsPgsql(); - - // Make all discount uses with their correct user - if ($isPsql) { - $sql = <<getDb()->createCommand($sql)->execute(); - - // Reset internal cache - $this->_allDiscounts = null; - $this->_activeDiscountsByKey = null; - } - - /** - * @throws \yii\db\Exception - * @since 4.0 - */ - public function clearCustomerUsageHistoryById(int $id): void - { - $db = Craft::$app->getDb(); - - $db->createCommand() - ->delete(Table::CUSTOMER_DISCOUNTUSES, ['discountId' => $id]) - ->execute(); - - // Reset internal cache - $this->_allDiscounts = null; - $this->_activeDiscountsByKey = null; - } - - /** - * @throws \yii\db\Exception - * @since 3.0 - */ - public function clearEmailUsageHistoryById(int $id): void - { - $db = Craft::$app->getDb(); - - $db->createCommand() - ->delete(Table::EMAIL_DISCOUNTUSES, ['discountId' => $id]) - ->execute(); - - // Reset internal cache - $this->_allDiscounts = null; - $this->_activeDiscountsByKey = null; - } - - /** - * Clear total discount uses - * - * @throws \yii\db\Exception - * @since 3.0 - */ - public function clearDiscountUsesById(int $id): void - { - $db = Craft::$app->getDb(); - $db->createCommand() - ->update(Table::DISCOUNTS, ['totalDiscountUses' => 0], ['id' => $id]) - ->execute(); - - // Reset internal cache - $this->_allDiscounts = null; - $this->_activeDiscountsByKey = null; - } - - /** - * Reorder discounts by an array of ids. - * - * @throws \yii\db\Exception - */ - public function reorderDiscounts(array $ids): bool - { - foreach ($ids as $sortOrder => $id) { - Craft::$app->getDb()->createCommand() - ->update(Table::DISCOUNTS, ['sortOrder' => $sortOrder + 1], ['id' => $id]) - ->execute(); - } - - // Reset internal cache - $this->_allDiscounts = null; - $this->_activeDiscountsByKey = null; - - return true; - } - - /** - * Appends a coupon code to an existing discount. - * - * @param int $discountId The discount ID - * @param string|Coupon $coupon The coupon code to append or a Coupon model - * @param int|null $maxUses The maximum number of times this coupon can be used (null for unlimited) - only used if $coupon is a string - * @return bool Whether the coupon was successfully added - * @throws Exception if the discount doesn't exist or doesn't require a coupon code - * @throws InvalidConfigException - */ - public function appendCouponCode(int $discountId, string|Coupon $coupon, ?int $maxUses = null): bool - { - $discount = $this->getDiscountById($discountId); - - if (!$discount) { - throw new Exception('No discount exists with the ID "' . $discountId . '"'); - } - - if (!$discount->requireCouponCode) { - throw new Exception('The discount with ID "' . $discountId . '" does not require a coupon code'); - } - - // If a string was passed, create a new coupon model - if (is_string($coupon)) { - $couponModel = new Coupon(); - $couponModel->discountId = $discountId; - $couponModel->code = $coupon; - $couponModel->maxUses = $maxUses; - $couponModel->uses = 0; - } else { - // Use the provided coupon model - $couponModel = $coupon; - $couponModel->discountId = $discountId; - } - - // Save the coupon - $result = Plugin::getInstance()->getCoupons()->saveCoupon($couponModel); - - if ($result) { - // Reset internal cache - $this->_allDiscounts = null; - $this->_activeDiscountsByKey = null; - } - - return $result; - } - - /** - * Email usage stats for discount - * - * @return array return in the format ['uses' => int, 'emails' => int] - */ - public function getEmailUsageStatsById(int $id): array - { - return (new Query()) - ->select(['COALESCE(SUM(uses), 0) as uses', 'COUNT(email) as emails']) - ->from(Table::EMAIL_DISCOUNTUSES) - ->where(['discountId' => $id]) - ->one(); - } - - /** - * User usage stats for discount - * - * @param int $id - * @return array in the format ['uses' => int, 'users' => int] - */ - public function getCustomerUsageStatsById(int $id): array - { - return (new Query()) - ->select(['COALESCE(SUM(uses), 0) as uses', 'COUNT([[customerId]]) as users']) - ->from(Table::CUSTOMER_DISCOUNTUSES) - ->where(['[[discountId]]' => $id]) - ->one(); - } - - /** - * Updates discount uses counters. - * - * @throws \yii\db\Exception - */ - public function orderCompleteHandler(Order $order): void - { - $discountAdjustments = $order->getAdjustmentsByType(DiscountAdjuster::ADJUSTMENT_TYPE); - - if (empty($discountAdjustments)) { - return; - } - - /* We only need to make counter updates once for each discount. A discount - might be returned multiple times due to it being a lineItem adjustment */ - $discounts = []; - /** @var OrderAdjustment $discountAdjustment */ - foreach ($discountAdjustments as $discountAdjustment) { - $snapshot = $discountAdjustment->sourceSnapshot ?? null; - if (!$snapshot || !isset($snapshot['discountUseId']) || isset($discounts[$snapshot['discountUseId']])) { - continue; - } - - $discounts[$snapshot['discountUseId']] = $snapshot; - } - - if (empty($discounts)) { - return; - } - - $user = $order->getCustomer(); - foreach ($discounts as $discount) { - // Count if there was a user on this order that has authentication - if ($user && $user->getIsCredentialed()) { - $userDiscountUseRecord = CustomerDiscountUse::find()->where(['customerId' => $user->id, 'discountId' => $discount['discountUseId']])->one(); - - if (!$userDiscountUseRecord) { - $userDiscountUseRecord = Craft::createObject(CustomerDiscountUse::class); - Craft::configure($userDiscountUseRecord, [ - 'customerId' => $user->id, - 'discountId' => $discount['discountUseId'], - 'uses' => 1, - ]); - $userDiscountUseRecord->save(); - } else { - Craft::$app->getDb()->createCommand() - ->update(Table::CUSTOMER_DISCOUNTUSES, [ - 'uses' => new Expression('[[uses]] + 1'), - ], [ - 'customerId' => $order->getCustomerId(), - 'discountId' => $discount['discountUseId'], - ]) - ->execute(); - } - } - - // Count email usage - $emailDiscountUseRecord = EmailDiscountUseRecord::find()->where(['email' => $order->getEmail(), 'discountId' => $discount['discountUseId']])->one(); - if (!$emailDiscountUseRecord) { - $emailDiscountUseRecord = new EmailDiscountUseRecord(); - $emailDiscountUseRecord->email = $order->getEmail(); - $emailDiscountUseRecord->discountId = $discount['discountUseId']; - $emailDiscountUseRecord->uses = 1; - $emailDiscountUseRecord->save(); - } else { - Craft::$app->getDb()->createCommand() - ->update(Table::EMAIL_DISCOUNTUSES, [ - 'uses' => new Expression('[[uses]] + 1'), - ], [ - 'email' => $order->getEmail(), - 'discountId' => $discount['discountUseId'], - ]) - ->execute(); - } - - // Update the total uses - Craft::$app->getDb()->createCommand() - ->update(Table::DISCOUNTS, [ - 'totalDiscountUses' => new Expression('[[totalDiscountUses]] + 1'), - ], [ - 'id' => $discount['discountUseId'], - ]) - ->execute(); - - // Check if the total use limit has been exceeded (race condition / oversell scenario) - if (($discount['totalDiscountUseLimit'] ?? 0) > 0) { - $updatedUses = (new Query()) - ->select(['totalDiscountUses']) - ->from([Table::DISCOUNTS]) - ->where(['id' => $discount['discountUseId']]) - ->scalar(); - if ($updatedUses > $discount['totalDiscountUseLimit']) { - $notice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'discountUsageExceeded', - 'attribute' => 'couponCode', - 'message' => Craft::t('commerce', 'The discount "{name}" has exceeded its total usage limit of {limit}.', [ - 'name' => $discount['name'] ?? $discount['discountUseId'], - 'limit' => $discount['totalDiscountUseLimit'], - ]), - 'noticeType' => OrderNoticeType::Admin, - ], - ]); - $order->addNotice($notice); - } - } - - // if there was a coupon on the order update its usage - if ($order->couponCode && $coupon = CouponRecord::findOne(['code' => $order->couponCode, 'discountId' => $discount['discountUseId']])) { - Craft::$app->getDb()->createCommand() - ->update(Table::COUPONS, [ - 'uses' => new Expression('[[uses]] + 1'), - ], [ - 'id' => $coupon->id, - ]) - ->execute(); - - // Check if the coupon's max uses has been exceeded - if ($coupon->maxUses !== null && ($coupon->uses + 1) > $coupon->maxUses) { - $notice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'couponUsageExceeded', - 'attribute' => 'couponCode', - 'message' => Craft::t('commerce', 'The coupon "{code}" has exceeded its usage limit of {limit}.', [ - 'code' => $order->couponCode, - 'limit' => $coupon->maxUses, - ]), - 'noticeType' => OrderNoticeType::Admin, - ], - ]); - $order->addNotice($notice); - } - } - - // Reset internal cache - $this->_allDiscounts = null; - $this->_activeDiscountsByKey = null; - } - } - - - /** - * @param Order $order - * @param Discount $discount - * @return bool - * @throws InvalidConfigException - */ - private function _isDiscountCouponCodeValid(Order $order, Discount $discount): bool - { - // If the discount does not require a coupon code, it's valid - if (!$discount->requireCouponCode) { - return true; - } - - $coupons = $discount->getCoupons(); - // Protect against empty coupon code list if the discount requires a coupon code - if (empty($coupons)) { - return false; - } - - $return = ArrayHelper::firstWhere($coupons, static fn(Coupon $coupon) => (strcasecmp($coupon->code, $order->couponCode) == 0) && ($coupon->maxUses === null || $coupon->maxUses > $coupon->uses)); - return (bool)$return; - } - - /** - * @throws \Exception - */ - private function _isDiscountDateValid(Order $order, Discount $discount): bool - { - $now = new DateTime(); - - if ($order->isCompleted && $order->dateOrdered) { - $now = $order->dateOrdered; - } - - $from = $discount->dateFrom; - $to = $discount->dateTo; - - return !(($from && $from > $now) || ($to && $to < $now)); - } - - /** - * @throws InvalidConfigException - * @throws LoaderError - * @throws SyntaxError - */ - private function _isDiscountConditionFormulaValid(Order $order, Discount $discount): bool - { - if ($discount->orderConditionFormula) { - $fieldsAsArray = $order->getSerializedFieldValues(); - $orderAsArray = $order->toArray([], ['lineItems.snapshot', 'shippingAddress', 'billingAddress']); - $orderConditionParams = [ - 'order' => array_merge($orderAsArray, $fieldsAsArray), - ]; - return Plugin::getInstance()->getFormulas()->evaluateCondition($discount->orderConditionFormula, $orderConditionParams, 'Evaluate Order Discount Condition Formula'); - } - - return true; - } - - private function _isDiscountTotalUseLimitValid(Discount $discount): bool - { - if ($discount->totalDiscountUseLimit > 0) { - if ($discount->totalDiscountUses >= $discount->totalDiscountUseLimit) { - return false; - } - } - - return true; - } - - /** - * @param Discount $discount - * @param User|null $user - * @return bool - */ - private function _isDiscountPerUserUsageValid(Discount $discount, ?User $user): bool - { - if ($discount->perUserLimit > 0) { - if (!$user) { - return false; - } - - if (Craft::$app->getRequest()->getIsSiteRequest()) { - $currentUser = Craft::$app->getUser()->getIdentity(); - $isCustomerCurrentUser = ($currentUser && $currentUser->id == $user->id); - - if (!$isCustomerCurrentUser) { - return false; - } - } - - $usage = (new Query()) - ->select(['uses']) - ->from([Table::CUSTOMER_DISCOUNTUSES]) - ->where(['[[customerId]]' => $user->id, 'discountId' => $discount->id]) - ->scalar(); - - if ($usage && $usage >= $discount->perUserLimit) { - return false; - } - } - - return true; - } - - private function _isDiscountEmailRequirementValid(Discount $discount, Order $order): bool - { - if ($discount->perEmailLimit > 0 && !$order->getEmail()) { - return false; - } - - return true; - } - - private function _isDiscountPerEmailLimitValid(Discount $discount, Order $order): bool - { - if ($discount->perEmailLimit > 0 && $order->getEmail()) { - $usage = (new Query()) - ->select(['uses']) - ->from([Table::EMAIL_DISCOUNTUSES]) - ->where(['email' => $order->getEmail(), 'discountId' => $discount->id]) - ->scalar(); - - if ($usage && $usage >= $discount->perEmailLimit) { - return false; - } - } - - return true; - } - - /** - * @param array $discounts - * @return array - * @throws InvalidConfigException - * @since 2.2.14 - */ - private function _populateDiscounts(array $discounts): array - { - foreach ($discounts as &$discount) { - // @TODO Remove this manual JSON decoding / default-value massaging once the Discount setters accept raw DB values (JSON strings, nulls) directly - - $discount['purchasableIds'] = !empty($discount['purchasableIds']) ? Json::decodeIfJson($discount['purchasableIds'], true) : []; - // IDs can be either category ID or entry ID due to the entryfication - $discount['categoryIds'] = !empty($discount['categoryIds']) ? Json::decodeIfJson($discount['categoryIds'], true) : []; - $discount['orderCondition'] ??= ''; - $discount['customerCondition'] ??= ''; - $discount['billingAddressCondition'] ??= ''; - $discount['shippingAddressCondition'] ??= ''; - - $discount = Craft::createObject([ - 'class' => Discount::class, - 'attributes' => $discount, - ]); - } - - return $discounts; - } - - /** - * Returns a Query object prepped for retrieving discounts - */ - private function _createDiscountQuery(): Query - { - $query = (new Query()) - ->select([ - '[[discounts.allCategories]]', - '[[discounts.allPurchasables]]', - '[[discounts.appliedTo]]', - '[[discounts.baseDiscount]]', - '[[discounts.categoryRelationshipType]]', - '[[discounts.couponFormat]]', - '[[discounts.dateCreated]]', - '[[discounts.dateFrom]]', - '[[discounts.dateTo]]', - '[[discounts.dateUpdated]]', - '[[discounts.description]]', - '[[discounts.enabled]]', - '[[discounts.excludeOnPromotion]]', - '[[discounts.hasFreeShippingForMatchingItems]]', - '[[discounts.hasFreeShippingForOrder]]', - '[[discounts.id]]', - '[[discounts.ignorePromotions]]', - '[[discounts.maxPurchaseQty]]', - '[[discounts.name]]', - '[[discounts.orderCondition]]', - '[[discounts.orderConditionFormula]]', - '[[discounts.percentageOffSubject]]', - '[[discounts.percentDiscount]]', - '[[discounts.perEmailLimit]]', - '[[discounts.perItemDiscount]]', - '[[discounts.perUserLimit]]', - '[[discounts.purchaseTotal]]', - '[[discounts.purchaseQty]]', - '[[discounts.requireCouponCode]]', - '[[discounts.sortOrder]]', - '[[discounts.stopProcessing]]', - '[[discounts.storeId]]', - '[[discounts.totalDiscountUseLimit]]', - '[[discounts.totalDiscountUses]]', - '[[discounts.customerCondition]]', - '[[discounts.shippingAddressCondition]]', - '[[discounts.billingAddressCondition]]', - '[[discounts.purchasableIds]]', - '[[discounts.categoryIds]]', - ]) - ->from(['discounts' => Table::DISCOUNTS]) - ->orderBy(['sortOrder' => SORT_ASC]) - ->leftJoin(Table::DISCOUNT_PURCHASABLES . ' dp', '[[dp.discountId]]=[[discounts.id]]') - ->leftJoin(Table::DISCOUNT_CATEGORIES . ' dpt', '[[dpt.discountId]]=[[discounts.id]]') - ->groupBy(['discounts.id']); - - return $query; - } -} diff --git a/src/services/Emails.php b/src/services/Emails.php deleted file mode 100644 index 48ae622f79..0000000000 --- a/src/services/Emails.php +++ /dev/null @@ -1,1007 +0,0 @@ - - * @since 2.0 - */ -class Emails extends Component -{ - /** - * @event MailEvent The event that is raised before an email is sent. - * You may set [[MailEvent::isValid]] to `false` to prevent the email from being sent. - * - * Plugins can get notified before an email is being sent out. - * - * ```php - * use craft\commerce\events\MailEvent; - * use craft\commerce\services\Emails; - * use yii\base\Event; - * - * Event::on( - * Emails::class, - * Emails::EVENT_BEFORE_SEND_MAIL, - * function(MailEvent $event) { - * // @var Message $message - * $message = $event->craftEmail; - * // @var Email $email - * $email = $event->commerceEmail; - * // @var Order $order - * $order = $event->order; - * // @var OrderHistory $history - * $history = $event->orderHistory; - * - * // Use `$event->isValid = false` to prevent sending - * // based on some business rules or client preferences - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_SEND_MAIL = 'beforeSendEmail'; - - /** - * @event MailEvent The event that is raised after an email is sent - * - * Plugins can get notified after an email has been sent out. - * - * ```php - * use craft\commerce\events\MailEvent; - * use craft\commerce\services\Emails; - * use yii\base\Event; - * - * Event::on( - * Emails::class, - * Emails::EVENT_AFTER_SEND_MAIL, - * function(MailEvent $event) { - * // @var Message $message - * $message = $event->craftEmail; - * // @var Email $email - * $email = $event->commerceEmail; - * // @var Order $order - * $order = $event->order; - * // @var OrderHistory $history - * $history = $event->orderHistory; - * - * // Add the email address to an external CRM - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_SEND_MAIL = 'afterSendEmail'; - - /** - * @event EmailEvent The event that is triggered before an email is saved. - * - * ```php - * use craft\commerce\events\EmailEvent; - * use craft\commerce\services\Emails; - * use craft\commerce\models\Email; - * use yii\base\Event; - * - * Event::on( - * Emails::class, - * Emails::EVENT_BEFORE_SAVE_EMAIL, - * function(EmailEvent $event) { - * // @var Email $email - * $email = $event->email; - * // @var bool $isNew - * $isNew = $event->isNew; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_SAVE_EMAIL = 'beforeSaveEmail'; - - /** - * @event EmailEvent The event that is triggered after an email is saved. - * - * ```php - * use craft\commerce\events\EmailEvent; - * use craft\commerce\services\Emails; - * use craft\commerce\models\Email; - * use yii\base\Event; - * - * Event::on( - * Emails::class, - * Emails::EVENT_AFTER_SAVE_EMAIL, - * function(EmailEvent $event) { - * // @var Email $email - * $email = $event->email; - * // @var bool $isNew - * $isNew = $event->isNew; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_SAVE_EMAIL = 'afterSaveEmail'; - - /** - * @event EmailEvent The event that is triggered before an email is deleted. - * - * ```php - * use craft\commerce\events\EmailEvent; - * use craft\commerce\services\Emails; - * use craft\commerce\models\Email; - * use yii\base\Event; - * - * Event::on( - * Emails::class, - * Emails::EVENT_BEFORE_DELETE_EMAIL, - * function(EmailEvent $event) { - * // @var Email $email - * $email = $event->email; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_DELETE_EMAIL = 'beforeDeleteEmail'; - - /** - * @event EmailEvent The event that is triggered after an email is deleted. - * ```php - * use craft\commerce\events\EmailEvent; - * use craft\commerce\services\Emails; - * use craft\commerce\models\Email; - * use yii\base\Event; - * - * Event::on( - * Emails::class, - * Emails::EVENT_AFTER_DELETE_EMAIL, - * function(EmailEvent $event) { - * // @var Email $email - * $email = $event->email; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_DELETE_EMAIL = 'afterDeleteEmail'; - - public const CONFIG_EMAILS_KEY = 'commerce.emails'; - - /** - * @var Collection[]|null - * @since 5.0.0 - */ - private ?array $_allEmails = null; - - /** - * Get an email by its ID. - */ - public function getEmailById(int $id, ?int $storeId = null): ?Email - { - return $this->getAllEmails($storeId)->firstWhere('id', $id); - } - - /** - * Get all emails. - * - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - * @throws SiteNotFoundException - */ - public function getAllEmails(?int $storeId = null): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if ($this->_allEmails === null || !isset($this->_allEmails[$storeId])) { - $results = $this->_createEmailQuery() - ->where(['storeId' => $storeId]) - ->all(); - - // Start with a blank slate if it isn't memoized - if ($this->_allEmails === null) { - $this->_allEmails = []; - } - - foreach ($results as $result) { - $email = Craft::createObject([ - 'class' => Email::class, - 'attributes' => $result, - ]); - - if (!isset($this->_allEmails[$email->storeId])) { - $this->_allEmails[$email->storeId] = collect(); - } - - $this->_allEmails[$email->storeId]->push($email); - } - } - - if (!isset($this->_allEmails[$storeId])) { - return collect(); - } - - return $this->_allEmails[$storeId]; - } - - /** - * Get all emails that are enabled. - * - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - * @throws SiteNotFoundException - */ - public function getAllEnabledEmails(?int $storeId = null): Collection - { - return $this->getAllEmails($storeId)->where('enabled', true); - } - - /** - * Save an email. - * - * @throws Exception - * @throws ErrorException - * @throws NotSupportedException - * @throws ServerErrorHttpException - */ - public function saveEmail(Email $email, bool $runValidation = true): bool - { - $isNewEmail = !(bool)$email->id; - - // Fire a 'beforeSaveEmail' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_EMAIL)) { - $this->trigger(self::EVENT_BEFORE_SAVE_EMAIL, new EmailEvent([ - 'email' => $email, - 'isNew' => $isNewEmail, - ])); - } - - if ($runValidation && !$email->validate()) { - Craft::info('Email not saved due to validation error(s).', __METHOD__); - return false; - } - - if ($isNewEmail) { - $email->uid = StringHelper::UUID(); - } - - $configPath = self::CONFIG_EMAILS_KEY . '.' . $email->uid; - $configData = $email->getConfig(); - Craft::$app->getProjectConfig()->set($configPath, $configData); - - if ($isNewEmail) { - $email->id = Db::idByUid(Table::EMAILS, $email->uid); - } - - return true; - } - - - /** - * Handle email status change. - * - * @throws Throwable if reasons - */ - public function handleChangedEmail(ConfigEvent $event): void - { - ProjectConfigData::ensureAllStoresProcessed(); - - $emailUid = $event->tokenMatches[0]; - $data = $event->newValue; - - $pdfUid = $data['pdf'] ?? null; - if ($pdfUid) { - Craft::$app->getProjectConfig()->processConfigChanges(Pdfs::CONFIG_PDFS_KEY . '.' . $pdfUid); - } - - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - $emailRecord = $this->_getEmailRecord($emailUid); - $isNewEmail = $emailRecord->getIsNewRecord(); - $store = Plugin::getInstance()->getStores()->getStoreByUid($data['store']); - $renderSite = array_key_exists('renderSite', $data) && $data['renderSite'] !== null ? Craft::$app->getSites()->getSiteByUid($data['renderSite']) : null; - - $emailRecord->storeId = $store->id; - $emailRecord->name = $data['name']; - $emailRecord->subject = $data['subject']; - $emailRecord->recipientType = $data['recipientType']; - $emailRecord->to = $data['to']; - $emailRecord->bcc = $data['bcc']; - $emailRecord->cc = $data['cc'] ?? null; - $emailRecord->replyTo = $data['replyTo'] ?? null; - $emailRecord->enabled = $data['enabled']; - $emailRecord->senderAddress = $data['senderAddress']; - $emailRecord->senderName = $data['senderName']; - $emailRecord->templatePath = $data['templatePath']; - $emailRecord->plainTextTemplatePath = $data['plainTextTemplatePath'] ?? null; - $emailRecord->uid = $emailUid; - $emailRecord->pdfId = $pdfUid ? Db::idByUid(Table::PDFS, $pdfUid) : null; - $emailRecord->language = $data['language'] ?? EmailRecord::LOCALE_ORDER_LANGUAGE; - $emailRecord->renderSiteId = $renderSite?->id ?? null; - - $emailRecord->save(false); - - $transaction->commit(); - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - - // Fire a 'afterSaveEmail' event - if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_EMAIL)) { - $this->trigger(self::EVENT_AFTER_SAVE_EMAIL, new EmailEvent([ - 'email' => $this->getEmailById($emailRecord->id, $emailRecord->storeId), - 'isNew' => $isNewEmail, - ])); - } - - $this->clearCache(); - } - - /** - * Delete an email by its ID. - */ - public function deleteEmailById(int $id): bool - { - $email = EmailRecord::findOne($id); - - if ($email) { - // Fire a 'beforeDeleteEmail' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_DELETE_EMAIL)) { - $this->trigger(self::EVENT_BEFORE_DELETE_EMAIL, new EmailEvent([ - 'email' => $this->getEmailById($id, $email->storeId), - ])); - } - - Craft::$app->getProjectConfig()->remove(self::CONFIG_EMAILS_KEY . '.' . $email->uid); - } - - return true; - } - - /** - * Handle email getting deleted. - * - * @throws Throwable - * @throws StaleObjectException - */ - public function handleDeletedEmail(ConfigEvent $event): void - { - $uid = $event->tokenMatches[0]; - $emailRecord = $this->_getEmailRecord($uid); - - if (!$emailRecord->id) { - return; - } - - $email = $this->getEmailById($emailRecord->id, $emailRecord->storeId); - $emailRecord->delete(); - - // Fire a 'beforeDeleteEmail' event - if ($this->hasEventHandlers(self::EVENT_AFTER_DELETE_EMAIL)) { - $this->trigger(self::EVENT_AFTER_DELETE_EMAIL, new EmailEvent([ - 'email' => $email, - ])); - } - - $this->clearCache(); - } - - /** - * Send a commerce email. - * - * @param array|null $orderData Since the order may have changed by the time the email sends. - * @param string $error The reason this method failed. - * @return bool $result - * @throws Exception - * @throws Throwable - * @throws InvalidConfigException - */ - public function sendEmail(Email $email, Order $order, ?OrderHistory $orderHistory = null, ?array $orderData = null, string &$error = ''): bool - { - if (!$email->enabled) { - $error = Craft::t('commerce', 'Email is not enabled.'); - return false; - } - - if ($email->storeId !== $order->getStore()->id) { - $error = Craft::t('commerce', 'Email unavailable.'); - return false; - } - - // Set Craft to the site template mode - $view = Craft::$app->getView(); - $oldTemplateMode = $view->getTemplateMode(); - $view->setTemplateMode($view::TEMPLATE_MODE_SITE); - $option = 'email'; - $generalConfig = Craft::$app->getConfig()->getGeneral(); - // Temporarily disable lazy transform generation - $generateTransformsBeforePageLoad = $generalConfig->generateTransformsBeforePageLoad; - $generalConfig->generateTransformsBeforePageLoad = true; - - // Make sure date vars are in the correct format - $dateFields = ['dateOrdered', 'datePaid', 'dateFirstPaid']; - foreach ($dateFields as $dateField) { - if (isset($order->{$dateField}) && !($order->{$dateField} instanceof DateTime) && $order->{$dateField}) { - $order->{$dateField} = DateTimeHelper::toDateTime($order->{$dateField}); - } - } - - //sending emails - $renderVariables = compact('order', 'orderHistory', 'option', 'orderData'); - - $mailer = Craft::$app->getMailer(); - /** @var Message $newEmail */ - $newEmail = Craft::createObject(['class' => $mailer->messageClass, 'mailer' => $mailer]); - - $originalLanguage = Craft::$app->language; - $originalFormattingLanguage = Craft::$app->formattingLocale; - $emailLanguage = $email->getRenderLanguage($order); - $emailSite = $email->getRenderSite($order); - - Locale::switchAppLanguage($emailLanguage); - - $fromEmail = $email->getSenderAddress(); - $fromName = $email->getSenderName(); - - if ($fromEmail) { - $newEmail->setFrom($fromEmail); - } - - if ($fromName && $fromEmail) { - $newEmail->setFrom([$fromEmail => $fromName]); - } - - if ($email->recipientType == EmailRecord::TYPE_CUSTOMER) { - if ($order->getCustomer()) { - $newEmail->setTo($order->getEmail()); - } - } - - if ($email->recipientType == EmailRecord::TYPE_CUSTOM) { - // To: - try { - $emails = $view->renderSandboxedString($email->getTo(), $renderVariables); - $emails = preg_split('/[\s,]+/', $emails); - - $newEmail->setTo($emails); - } catch (\Exception $e) { - Craft::$app->getErrorHandler()->logException($e); - - $error = Craft::t('commerce', 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}', [ - 'email' => $email->name, - 'order' => $order->getShortNumber(), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ]); - Craft::error($error, __METHOD__); - - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - } - - if (!$newEmail->getTo()) { - $error = Craft::t('commerce', 'Email error. No email address found for order. Order: “{order}”', ['order' => $order->getShortNumber()]); - Craft::error($error, __METHOD__); - - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - - // BCC: - if ($bccSetting = $email->getBcc()) { - try { - $bcc = $view->renderSandboxedString($bccSetting, $renderVariables); - $bcc = str_replace(';', ',', $bcc); - $bcc = preg_split('/[\s,]+/', $bcc); - - if (array_filter($bcc)) { - $newEmail->setBcc($bcc); - } - } catch (\Exception $e) { - Craft::$app->getErrorHandler()->logException($e); - - $error = Craft::t('commerce', 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}', [ - 'email' => $email->name, - 'order' => $order->getShortNumber(), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ]); - Craft::error($error, __METHOD__); - - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - } - - // CC: - if ($ccSetting = $email->getCc()) { - try { - $cc = $view->renderSandboxedString($ccSetting, $renderVariables); - $cc = str_replace(';', ',', $cc); - $cc = preg_split('/[\s,]+/', $cc); - - if (array_filter($cc)) { - $newEmail->setCc($cc); - } - } catch (\Exception $e) { - Craft::$app->getErrorHandler()->logException($e); - - $error = Craft::t('commerce', 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}', [ - 'email' => $email->name, - 'order' => $order->getShortNumber(), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ]); - Craft::error($error, __METHOD__); - - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - } - - if ($email->replyTo) { - // Reply To: - try { - $newEmail->setReplyTo($view->renderSandboxedString($email->replyTo, $renderVariables)); - } catch (\Exception $e) { - Craft::$app->getErrorHandler()->logException($e); - - $error = Craft::t('commerce', 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}', [ - 'email' => $email->name, - 'order' => $order->getShortNumber(), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ]); - Craft::error($error, __METHOD__); - - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - } - - // Subject: - try { - $newEmail->setSubject($view->renderSandboxedString($email->subject, $renderVariables)); - } catch (\Exception $e) { - Craft::$app->getErrorHandler()->logException($e); - - $error = Craft::t('commerce', 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}', [ - 'email' => $email->name, - 'order' => $order->getShortNumber(), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ]); - Craft::error($error, __METHOD__); - - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - - // Template Path - try { - $templatePath = $view->renderSandboxedString($email->templatePath, $renderVariables); - } catch (\Exception $e) { - Craft::$app->getErrorHandler()->logException($e); - - $error = Craft::t('commerce', 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}', [ - 'email' => $email->name, - 'order' => $order->getShortNumber(), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ]); - Craft::error($error, __METHOD__); - - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - - // Email Body - if (!$view->doesTemplateExist($templatePath)) { - $error = Craft::t('commerce', 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.', [ - 'templatePath' => $email->templatePath, - 'templateParsedPath' => $templatePath, - 'email' => $email->name, - 'order' => $order->getShortNumber(), - ]); - Craft::error($error, __METHOD__); - - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - // Plain Text Template Path - $plainTextTemplatePath = null; - - if ($email->plainTextTemplatePath) { - try { - $plainTextTemplatePath = $view->renderSandboxedString($email->plainTextTemplatePath, $renderVariables); - } catch (\Exception $e) { - Craft::$app->getErrorHandler()->logException($e); - - $error = Craft::t('commerce', 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}', [ - 'email' => $email->name, - 'order' => $order->getShortNumber(), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ]); - Craft::error($error, __METHOD__); - - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - - // Plain Text Body - if ($plainTextTemplatePath && !$view->doesTemplateExist($plainTextTemplatePath)) { - $error = Craft::t('commerce', 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.', [ - 'templatePath' => $email->plainTextTemplatePath, - 'templateParsedPath' => $plainTextTemplatePath, - 'email' => $email->name, - 'order' => $order->getShortNumber(), - ]); - Craft::error($error, __METHOD__); - - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - } - - if ($pdf = $email->getPdf()) { - // Email Body - if (!$view->doesTemplateExist($pdf->templatePath)) { - $error = Craft::t('commerce', 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.', [ - 'templatePath' => $pdf->templatePath, - 'email' => $email->name, - 'order' => $order->getShortNumber(), - ]); - Craft::error($error, __METHOD__); - - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - - try { - $renderedPdf = Plugin::getInstance()->getPdfs()->renderPdfForOrder($order, 'email', null, [], $pdf); - - $tempPath = Assets::tempFilePath('pdf'); - - file_put_contents($tempPath, $renderedPdf); - - $fileName = ''; - $defaultFileName = $pdf->handle . '-' . $order->number; - if ($pdf->fileNameFormat) { - try { - $fileName = $view->renderSandboxedObjectTemplate($pdf->fileNameFormat, $order, $order->getObjectTemplateVariables()); - } catch (\Throwable) { - $fileName = $defaultFileName; - } - } - - if (!$fileName) { - $fileName = $defaultFileName; - } - - // Attachment information - $options = ['fileName' => $fileName . '.pdf', 'contentType' => 'application/pdf']; - $newEmail->attach($tempPath, $options); - } catch (\Exception $e) { - Craft::$app->getErrorHandler()->logException($e); - - $error = Craft::t('commerce', 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}', [ - 'email' => $email->name, - 'order' => $order->getShortNumber(), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ]); - Craft::error($error, __METHOD__); - - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - } - - $originalSiteId = Craft::$app->getSites()->getCurrentSite()->id; - Craft::$app->getSites()->setCurrentSite($emailSite); - - // Render HTML body - try { - $body = $view->renderTemplate($templatePath, $renderVariables); - $newEmail->setHtmlBody($body); - } catch (\Exception $e) { - Craft::$app->getErrorHandler()->logException($e); - - $error = Craft::t('commerce', 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}', [ - 'email' => $email->name, - 'order' => $order->getShortNumber(), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ]); - Craft::error($error, __METHOD__); - - Craft::$app->getSites()->setCurrentSite($originalSiteId); - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - - // Render Plain Text body - if ($plainTextTemplatePath) { - try { - $plainTextBody = $view->renderTemplate($plainTextTemplatePath, $renderVariables); - $newEmail->setTextBody($plainTextBody); - } catch (\Exception $e) { - Craft::$app->getErrorHandler()->logException($e); - - $error = Craft::t('commerce', 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}', [ - 'email' => $email->name, - 'order' => $order->getShortNumber(), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ]); - Craft::error($error, __METHOD__); - - Craft::$app->getSites()->setCurrentSite($originalSiteId); - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - } - - try { - //raising event - $event = new MailEvent([ - 'craftEmail' => $newEmail, - 'commerceEmail' => $email, - 'order' => $order, - 'orderHistory' => $orderHistory, - 'orderData' => $orderData, - ]); - $this->trigger(self::EVENT_BEFORE_SEND_MAIL, $event); - - if (!$event->isValid) { - $notice = Craft::t('commerce', 'Email “{email}” for order {order} was cancelled.', [ - 'email' => $email->name, - 'order' => $order->getShortNumber(), - ]); - - Craft::info($notice, __METHOD__); - - Craft::$app->getSites()->setCurrentSite($originalSiteId); - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - // Plugins that stop a email being sent should not declare that the sending failed, just that it would blocking of the send. - // The blocking of the send will still be logged as an error though for now. - // @TODO Clean up this behavior in Commerce 6.0 so plugins that block a send can signal "blocked" distinctly from "failed" without it being logged as an error #COM-49 - // https://github.com/craftcms/commerce/issues/1842 - return true; - } - - if (!Craft::$app->getMailer()->send($newEmail)) { - $error = Craft::t('commerce', 'Commerce email “{email}” could not be sent for order “{order}”.', [ - 'email' => $email->name, - 'order' => $order->getShortNumber(), - ]); - - Craft::error($error, __METHOD__); - - Craft::$app->getSites()->setCurrentSite($originalSiteId); - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - } catch (\Exception $e) { - Craft::$app->getErrorHandler()->logException($e); - - $error = Craft::t('commerce', 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}', [ - 'error' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'email' => $email->name, - 'order' => $order->getShortNumber(), - ]); - - Craft::error($error, __METHOD__); - - Craft::$app->getSites()->setCurrentSite($originalSiteId); - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - return false; - } - - // Raise an 'afterSendEmail' event - if ($this->hasEventHandlers(self::EVENT_AFTER_SEND_MAIL)) { - $this->trigger(self::EVENT_AFTER_SEND_MAIL, new MailEvent([ - 'craftEmail' => $newEmail, - 'commerceEmail' => $email, - 'order' => $order, - 'orderHistory' => $orderHistory, - 'orderData' => $orderData, - ])); - } - - Craft::$app->getSites()->setCurrentSite($originalSiteId); - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - $view->setTemplateMode($oldTemplateMode); - $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; - - // Clear out the temp PDF file if it was created. - if (!empty($tempPath)) { - unlink($tempPath); - } - - return true; - } - - /** - * Get all emails by an order status ID. - * - * @return Email[] - */ - public function getAllEmailsByOrderStatusId(int $id): array - { - $results = $this->_createEmailQuery() - ->innerJoin(Table::ORDERSTATUS_EMAILS . ' statusEmails', '[[emails.id]] = [[statusEmails.emailId]]') - ->innerJoin(Table::ORDERSTATUSES . ' orderStatuses', '[[statusEmails.orderStatusId]] = [[orderStatuses.id]]') - ->where(['orderStatuses.id' => $id]) - ->all(); - - $emails = []; - - foreach ($results as $row) { - $emails[] = new Email($row); - } - - return $emails; - } - - - /** - * Returns a Query object prepped for retrieving Emails. - */ - private function _createEmailQuery(): Query - { - return (new Query()) - ->select([ - 'emails.bcc', - 'emails.cc', - 'emails.enabled', - 'emails.id', - 'emails.language', - 'emails.name', - 'emails.pdfId', - 'emails.plainTextTemplatePath', - 'emails.recipientType', - 'emails.renderSiteId', - 'emails.replyTo', - 'emails.senderAddress', - 'emails.senderName', - 'emails.storeId', - 'emails.subject', - 'emails.templatePath', - 'emails.to', - 'emails.uid', - ]) - ->orderBy('emails.name') - ->from([Table::EMAILS . ' emails']); - } - - - /** - * Gets an email record by uid. - */ - private function _getEmailRecord(string $uid): EmailRecord - { - if ($email = EmailRecord::findOne(['uid' => $uid])) { - return $email; - } - - return new EmailRecord(); - } - - /** - * @return void - * @since 5.0.0 - */ - protected function clearCache(): void - { - $this->_allEmails = null; - } -} diff --git a/src/services/Formulas.php b/src/services/Formulas.php deleted file mode 100644 index b26df1dbe5..0000000000 --- a/src/services/Formulas.php +++ /dev/null @@ -1,278 +0,0 @@ - - * @since 2.2 - */ -class Formulas extends Component -{ - /** - * @var Environment - */ - private Environment $_twigEnv; - - /** - * @var array Request-level cache for condition evaluation results, keyed by formula+params hash. - */ - private array $_conditionResults = []; - - /** - * Initialize formulas - */ - public function init(): void - { - $tags = $this->_getTags(); - $filters = $this->_getFilters(); - $functions = $this->_getFunctions(); - $methods = $this->_getMethods(); - $properties = $this->_getProperties(); - - $policy = new SecurityPolicy($tags, $filters, $methods, $properties, $functions); - $loader = new FilesystemLoader(); - $sandbox = new SandboxExtension($policy, true); - - $this->_twigEnv = new Environment($loader); - $this->_twigEnv->addExtension($sandbox); - } - - /** - * @param string $condition The condition which will be tested for correct syntax - * @param array $params data passed into the formula - */ - public function validateConditionSyntax(string $condition, array $params): bool - { - try { - $this->evaluateCondition($condition, $params, Craft::t('commerce', 'Validating condition syntax')); - } catch (Exception) { - return false; - } - - return true; - } - - /** - * @param string $formula The formula which will be tested for correct syntax - * @param array $params data passed into the formula - */ - public function validateFormulaSyntax(string $formula, array $params): bool - { - try { - $this->evaluateFormula($formula, $params, null, Craft::t('commerce', 'Validating formula syntax')); - } catch (Exception) { - return false; - } - - return true; - } - - /** - * @param array $params data passed into the condition - * @param string $name The name of the formula, useful for locating template errors in logs and exceptions - * @return bool - * @throws SyntaxError - * @throws LoaderError - */ - public function evaluateCondition(string $formula, array $params, string $name = 'Evaluate Condition'): bool - { - if ($this->_hasDisallowedStrings($formula, ['{%', '%}', '{{', '}}'])) { - throw new SyntaxError('Tags are not allowed in a condition formula.'); - } - - $formulaHash = md5($formula); - $paramsHash = md5(Json::encode($params)); - $requestKey = $formulaHash . $paramsHash; - - if (isset($this->_conditionResults[$requestKey])) { - return $this->_conditionResults[$requestKey]; - } - - $cacheKey = [ - 'formula' => $formulaHash, - 'params' => $paramsHash, - ]; - - $cachedResult = Craft::$app->getCache()->get($cacheKey); - if ($cachedResult !== false) { - return $this->_conditionResults[$requestKey] = ($cachedResult === 'TRUE'); - } - - $twigCode = '{% if '; - $twigCode .= $formula; - $twigCode .= ' %}TRUE{% else %}FALSE{% endif %}'; - - $template = $this->_twigEnv->createTemplate($twigCode, $name); - $output = $template->render($params); - - Craft::$app->getCache()->set($cacheKey, $output); - - return $this->_conditionResults[$requestKey] = ($output === 'TRUE'); - } - - /** - * @param string $formula - * @param array $params data passed into the condition - * @param string|null $setType the type of the response data, passing nothing will leave as a string. Uses \settype(). - * @param string|null $name The name of the formula, useful for locating template errors in logs and exceptions - * @return mixed - * @throws SyntaxError - * @throws LoaderError - */ - public function evaluateFormula(string $formula, array $params, ?string $setType = null, ?string $name = 'Inline formula'): mixed - { - $formula = trim($formula); - - $template = $this->_twigEnv->createTemplate($formula, $name); - $result = $template->render($params); - - if ($setType === null) { - return $result; - } - - settype($result, $setType); - return $result; - } - - private function _hasDisallowedStrings(string $code, array $disallowedStrings = []): bool - { - foreach ($disallowedStrings as $disallowedString) { - if (stripos($code, (string) $disallowedString) !== false) { - return true; - } - } - return false; - } - - private function _getTags(): array - { - return [ - //'apply', - //'autoescape', - //'block', - //'deprecated', - //'do', - //'embed', - //'extends', - //'flush', - 'for', - //'from', - 'if', - //'import', - //'include', - //'macro', - //'sandbox', - 'set', - //'use', - //'verbatim', - //'with', - ]; - } - - private function _getFilters(): array - { - return [ - 'abs', - //'batch', - 'capitalize', - //'column', - //'convert_encoding', - //'country_name', - //'country_timezones', - //'currency_name', - //'currency_symbol', - //'data_uri', - 'date', - //'date_modify', - //'default', - //'escape', - 'filter', - 'first', - //'format', - //'format_currency', - //'format_date', - //'format_datetime', - //'format_number', - //'format_time', - //'inky', - //'inline_css', - 'join', - //'json_encode', - 'keys', - //'language_name', - 'last', - 'length', - //'locale_name', - //'lower', - 'map', - //'markdown', - 'merge', - //'nl2br', - //'number_format', - //'raw', - 'reduce', - 'replace', - 'reverse', - 'round', - 'slice', - 'sort', - //'spaceless', - 'split', - //'striptags', - //'timezone_name', - //'title', - 'trim', - 'upper', - //'url_encode', - ]; - } - - private function _getFunctions(): array - { - return [ - //'attribute', - //'block', - //'constant', - //'cycle', - 'date', - //'dump', - //'html_classes', - //'include', - 'max', - 'min', - //'parent', - 'random', - 'range', - //'source', - //'template_from_string', - ]; - } - - private function _getMethods(): array - { - return []; - } - - private function _getProperties(): array - { - return []; - } -} diff --git a/src/services/Gateways.php b/src/services/Gateways.php deleted file mode 100644 index 33ff8ca7e0..0000000000 --- a/src/services/Gateways.php +++ /dev/null @@ -1,530 +0,0 @@ - - * @since 2.0 - */ -class Gateways extends Component -{ - /** - * @var array|null Gateway setting overrides - */ - private ?array $_overrides = null; - - /** - * @var Collection|null All gateways - */ - private ?Collection $_allGateways = null; - - /** - * @event RegisterComponentTypesEvent The event that is triggered for the registration of additional gateways. - * - * This example registers a custom gateway instance of the `MyGateway` class: - * - * ```php - * use craft\events\RegisterComponentTypesEvent; - * use craft\commerce\services\Purchasables; - * use yii\base\Event; - * - * Event::on( - * Gateways::class, - * Gateways::EVENT_REGISTER_GATEWAY_TYPES, - * function(RegisterComponentTypesEvent $event) { - * $event->types[] = MyGateway::class; - * } - * ); - * ``` - */ - public const EVENT_REGISTER_GATEWAY_TYPES = 'registerGatewayTypes'; - - public const CONFIG_GATEWAY_KEY = 'commerce.gateways'; - - - /** - * Returns all registered gateway types. - * - * @return string[] - */ - public function getAllGatewayTypes(): array - { - $gatewayTypes = [ - Dummy::class, - Manual::class, - ]; - - $event = new RegisterComponentTypesEvent([ - 'types' => $gatewayTypes, - ]); - $this->trigger(self::EVENT_REGISTER_GATEWAY_TYPES, $event); - - return $event->types; - } - - /** - * Returns all customer enabled gateways. - * - * @return Collection All gateways that are enabled for frontend - * @throws DeprecationException - * @throws InvalidConfigException - */ - public function getAllCustomerEnabledGateways(): Collection - { - return $this->getAllGateways()->filter(fn(Gateway $gateway) => $gateway->getIsFrontendEnabled()); - } - - /** - * Returns all customer enabled gateways and allowed for the order/cart. - * - * @return Collection All gateways that are enabled for frontend and allowed for the order/cart. - * @throws DeprecationException - * @throws InvalidConfigException - */ - public function getAllCustomerEnabledGatewaysAndAvailableForUseWithOrder(Order $order): Collection - { - return $this->getAllCustomerEnabledGateways()->filter(fn(Gateway $gateway) => $gateway->availableForUseWithOrder($order)); - } - - /** - * Returns all subscription gateways. - * - * @return Collection All Subscription gateways - * @throws DeprecationException - * @throws InvalidConfigException - */ - public function getAllSubscriptionGateways(): Collection - { - return $this->getAllGateways()->where(fn(Gateway $gateway) => $gateway instanceof SubscriptionGateway); - } - - /** - * Returns all gateways - * - * @return Collection All gateways - * @throws DeprecationException - * @throws InvalidConfigException - */ - public function getAllGateways(): Collection - { - return $this->_getAllGateways()->where('isArchived', false); - } - - /** - * @return array - * @throws DeprecationException - * @throws InvalidConfigException - * @sine 5.3.0 - */ - public function getAllArchivedGateways(): array - { - return ArrayHelper::where($this->_getAllGateways(), 'isArchived', true); - } - - /** - * Archives a gateway by its ID. - * - * @param int $id gateway ID - * @return bool Whether the archiving was successful or not - * @throws ErrorException - * @throws Exception - * @throws InvalidConfigException - * @throws NotSupportedException - * @throws ServerErrorHttpException - * @throws \yii\db\Exception - */ - public function archiveGatewayById(int $id): bool - { - /** @var Gateway $gateway */ - $gateway = $this->getGatewayById($id); - $gateway->isArchived = true; - - if (!$this->saveGateway($gateway)) { - return false; - } - - // remove all payment sources for this gateway - // this will also remove them as the payment source for a cart - Craft::$app->getDb()->createCommand() - ->delete(Table::PAYMENTSOURCES, ['gatewayId' => $id]) - ->execute(); - - // Clear this as the selected gateway from all active carts and orders - Craft::$app->getDb()->createCommand() - ->update(Table::ORDERS, - [ - 'gatewayId' => null, - 'paymentSourceId' => null, - ], - [ - 'gatewayId' => $id, - ], [], false) - ->execute(); - - - return true; - } - - /** - * Returns a gateway by its ID. - * - * @param int $id - * @return Gateway|null The gateway or null if not found. - * @throws DeprecationException - * @throws InvalidConfigException - */ - public function getGatewayById(int $id): ?Gateway - { - return $this->_getAllGateways()->firstWhere('id', $id); - } - - /** - * Returns a gateway by its handle. - * - * @param string $handle - * @return Gateway|null The gateway or null if not found. - * @throws DeprecationException - * @throws InvalidConfigException - */ - public function getGatewayByHandle(string $handle): ?Gateway - { - return $this->_getAllGateways()->firstWhere('handle', $handle); - } - - /** - * Saves a gateway. - * - * @param Gateway $gateway The gateway to be saved. - * @param bool $runValidation Whether the gateway should be validated - * @return bool Whether the gateway was saved successfully or not. - * @throws Exception - * @throws InvalidConfigException - * @throws ErrorException - * @throws NotSupportedException - * @throws ServerErrorHttpException - */ - public function saveGateway(Gateway $gateway, bool $runValidation = true): bool - { - $isNewGateway = $gateway->getIsNew(); - - if ($runValidation && !$gateway->validate()) { - Craft::info('Gateway not saved due to validation error.', __METHOD__); - return false; - } - - if ($isNewGateway) { - $gatewayUid = StringHelper::UUID(); - } else { - $gatewayUid = $gateway->uid; - } - - $existingGateway = $this->getGatewayByHandle($gateway->handle); - - if ($existingGateway && (!$gateway->id || $gateway->id != $existingGateway->id)) { - $gateway->addError('handle', Craft::t('commerce', 'That handle is already in use.')); - return false; - } - - $projectConfig = Craft::$app->getProjectConfig(); - - if ($gateway->isArchived) { - $configData = null; - } else { - $configData = $gateway->getConfig(); - } - - $configPath = self::CONFIG_GATEWAY_KEY . '.' . $gatewayUid; - $projectConfig->set($configPath, $configData); - - if ($isNewGateway) { - $gateway->id = Db::idByUid(Table::GATEWAYS, $gatewayUid); - } - - $this->_allGateways = null; // reset cache - - return true; - } - - /** - * Handle gateway change - * - * @throws Throwable if reasons - */ - public function handleChangedGateway(ConfigEvent $event): void - { - $gatewayUid = $event->tokenMatches[0]; - $data = $event->newValue; - - // Bail if the data is not a valid gateway config array - if (!is_array($data)) { - return; - } - - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - $gatewayRecord = $this->_getGatewayRecord($gatewayUid); - - $gatewayRecord->name = $data['name']; - $gatewayRecord->handle = $data['handle']; - $gatewayRecord->type = $data['type']; - $gatewayRecord->settings = $data['settings'] ?? null; - $gatewayRecord->sortOrder = $data['sortOrder']; - $gatewayRecord->paymentType = $data['paymentType']; - if ($data['isFrontendEnabled'] === null || is_bool($data['isFrontendEnabled'])) { - $data['isFrontendEnabled'] = $data['isFrontendEnabled'] ? '1' : '0'; - } - - $gatewayRecord->isFrontendEnabled = $data['isFrontendEnabled']; - $gatewayRecord->orderCondition = $data['orderCondition'] ?? null; - $gatewayRecord->billingAddressCondition = $data['billingAddressCondition'] ?? null; - $gatewayRecord->shippingAddressCondition = $data['shippingAddressCondition'] ?? null; - $gatewayRecord->isArchived = false; - $gatewayRecord->dateArchived = null; - $gatewayRecord->uid = $gatewayUid; - - // Save the volume - $gatewayRecord->save(false); - - $transaction->commit(); - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - } - - /** - * Handle gateway being archived - * - * @throws Throwable if reasons - */ - public function handleArchivedGateway(ConfigEvent $event): void - { - $gatewayUid = $event->tokenMatches[0]; - - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - $gatewayRecord = $this->_getGatewayRecord($gatewayUid); - - $gatewayRecord->isArchived = true; - $gatewayRecord->dateArchived = Db::prepareDateForDb(new DateTime()); - - // Save the volume - $gatewayRecord->save(false); - - $transaction->commit(); - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - } - - /** - * Reorders gateways by ids. - * - * @param array $ids Array of gateways. - * @return bool Always true. - * @throws ErrorException - * @throws Exception - * @throws InvalidConfigException - * @throws NotSupportedException - * @throws ServerErrorHttpException - */ - public function reorderGateways(array $ids): bool - { - $projectConfig = Craft::$app->getProjectConfig(); - - $uidsByIds = Db::uidsByIds(Table::GATEWAYS, $ids); - - foreach ($ids as $gatewayOrder => $gatewayId) { - if (!empty($uidsByIds[$gatewayId])) { - $gatewayUid = $uidsByIds[$gatewayId]; - $projectConfig->set(self::CONFIG_GATEWAY_KEY . '.' . $gatewayUid . '.sortOrder', $gatewayOrder + 1); - } - } - - $this->_allGateways = null; // reset cache - - return true; - } - - /** - * Creates a gateway with a given config - * - * @param string|array $config The gateway’s class name, or its config, with a `type` value and optionally a `settings` value - * @return Gateway The gateway - * @throws DeprecationException - * @throws InvalidConfigException - */ - public function createGateway(string|array $config): Gateway - { - if (is_string($config)) { - $config = ['type' => $config]; - } - - // Are they overriding any settings? - if (!empty($config['handle']) && ($override = $this->getGatewayOverrides($config['handle'])) !== null) { - // Save a reference to the original config in case the gateway type is missing - $originalConfig = $config; - - // Apply the settings early so the overrides don't get overridden - $config = array_merge(ComponentHelper::mergeSettings($config), $override); - } - - try { - if ($config['type'] == MissingGateway::class) { - throw new MissingComponentException('Missing Gateway Class.'); - } - - /** @var Gateway $gateway */ - $gateway = ComponentHelper::createComponent($config, GatewayInterface::class); - } catch (MissingComponentException $e) { - $config['errorMessage'] = $e->getMessage(); - $config['expectedType'] = $config['type']; - unset($config['type']); - - $gateway = new MissingGateway($config); - } - - return $gateway; - } - - /** - * Returns any custom gateway settings form config file. - * - * @param string $handle The gateway handle - * @throws DeprecationException - * @deprecated in 3.3. Overriding gateway settings using the `commerce-gateways.php` file has been deprecated. Use the gateway’s config file instead. - */ - public function getGatewayOverrides(string $handle): ?array - { - if ($this->_overrides === null) { - $this->_overrides = Craft::$app->getConfig()->getConfigFromFile('commerce-gateways'); - } - - $overrides = $this->_overrides[$handle] ?? null; - - if ($overrides != null) { - Craft::$app->getDeprecator()->log('craft.commerce.gateways.getGatewayOverrides()', 'Overriding gateway settings using the `commerce-gateways.php` file has been deprecated. Use the gateway’s config file instead.'); - } - - return $overrides; - } - - - /** - * Returns a Query object prepped for retrieving gateways. - * - * @return Query The query object. - */ - private function _createGatewayQuery(): Query - { - $query = (new Query()) - ->select([ - 'dateArchived', - 'handle', - 'id', - 'isArchived', - 'isFrontendEnabled', - 'name', - 'paymentType', - 'settings', - 'sortOrder', - 'type', - 'uid', - ]) - ->orderBy(['sortOrder' => SORT_ASC]) - ->from([Table::GATEWAYS]); - - // @TODO Remove these columnExists checks in Commerce 6.0 once the schema guarantees orderCondition / billingAddressCondition / shippingAddressCondition columns on the gateways table - $db = Craft::$app->getDb(); - if ($db->columnExists(Table::GATEWAYS, 'orderCondition')) { - $query->addSelect('orderCondition'); - } - if ($db->columnExists(Table::GATEWAYS, 'billingAddressCondition')) { - $query->addSelect('billingAddressCondition'); - } - if ($db->columnExists(Table::GATEWAYS, 'shippingAddressCondition')) { - $query->addSelect('shippingAddressCondition'); - } - - return $query; - } - - /** - * Gets a gateway's record by uid. - */ - private function _getGatewayRecord(string $uid): GatewayRecord - { - if ($gateway = GatewayRecord::findOne(['uid' => $uid])) { - return $gateway; - } - - return new GatewayRecord(); - } - - /** - * @return Collection - * @throws DeprecationException - * @throws InvalidConfigException - */ - private function _getAllGateways(): Collection - { - if ($this->_allGateways === null) { - $results = $this->_createGatewayQuery() - ->all(); - - if ($this->_allGateways === null) { - $this->_allGateways = collect(); - } - - $gateways = []; - foreach ($results as $result) { - $gateways[] = $this->createGateway($result); - } - - $this->_allGateways = collect($gateways)->keyBy('id'); - } - - return $this->_allGateways; - } -} diff --git a/src/services/Inventory.php b/src/services/Inventory.php deleted file mode 100644 index a124e3356d..0000000000 --- a/src/services/Inventory.php +++ /dev/null @@ -1,985 +0,0 @@ - - * @since 5.0.0 - */ -class Inventory extends Component -{ - /** - * @event UpdateInventoryLevelEvent The event that is triggered after an inventory level update is executed. - * - * ```php - * use craft\commerce\events\UpdateInventoryLevelEvent; - * use craft\commerce\services\Inventory; - * use craft\commerce\models\inventory\UpdateInventoryLevel; - * use yii\base\Event; - * - * Event::on( - * Inventory::class, - * Inventory::EVENT_AFTER_EXECUTE_UPDATE_INVENTORY_LEVEL, - * function(UpdateInventoryLevelEvent $event) { - * // @var UpdateInventoryLevel $updateInventoryLevel - * $updateInventoryLevel = $event->updateInventoryLevel; - * } - * ); - * ``` - */ - public const EVENT_AFTER_EXECUTE_UPDATE_INVENTORY_LEVEL = 'afterExecuteUpdateInventoryLevel'; - - /** - * @event InventoryMovementEvent The event that is triggered after an inventory movement is executed. - * - * ```php - * use craft\commerce\events\InventoryMovementEvent; - * use craft\commerce\services\Inventory; - * use craft\commerce\base\InventoryMovementInterface; - * use yii\base\Event; - * - * Event::on( - * Inventory::class, - * Inventory::EVENT_AFTER_EXECUTE_INVENTORY_MOVEMENT, - * function(InventoryMovementEvent $event) { - * // @var InventoryMovementInterface $inventoryMovement - * $inventoryMovement = $event->inventoryMovement; - * } - * ); - * ``` - */ - public const EVENT_AFTER_EXECUTE_INVENTORY_MOVEMENT = 'afterExecuteInventoryMovement'; - - /** - * @param Purchasable $purchasable - * @return Collection - */ - public function getInventoryLevelsForPurchasable(Purchasable $purchasable): Collection - { - $inventoryLevels = collect(); - - if (!$purchasable->id) { - return $inventoryLevels; // empty collection - } - - // Self-heal a missing inventory item id so callers get accurate levels - // even when the purchasable was loaded before its row was created. - if (!$purchasable->inventoryItemId && $purchasable::hasInventory()) { - $this->getInventoryItemByPurchasable($purchasable); - } - - if (!$purchasable->inventoryItemId) { - return $inventoryLevels; // empty collection - } - - $storeId = $purchasable->getStore()->id; - $storeInventoryLocations = Plugin::getInstance()->getInventoryLocations()->getInventoryLocations($storeId); - - foreach ($storeInventoryLocations as $inventoryLocation) { - $inventoryLevel = $this->getInventoryLevel($purchasable->inventoryItemId, $inventoryLocation->id); - - if (!$inventoryLevel) { - continue; - } - $inventoryLevels->push($inventoryLevel); - } - - return $inventoryLevels; - } - - /** - * @param Purchasable $purchasable - * @return InventoryItem - */ - public function getInventoryItemByPurchasable(Purchasable $purchasable): InventoryItem - { - // Self-heal: if the purchasable has somehow ended up without an associated - // inventory item (e.g. due to a draft-apply or duplicate path that didn't - // create one), find or create one before returning. - if (!$purchasable->inventoryItemId && $purchasable->id) { - $record = $this->ensureInventoryItemRecord($purchasable); - if ($record) { - $purchasable->inventoryItemId = $record->id; - } - } - - return $this->getInventoryItemById($purchasable->inventoryItemId); - } - - /** - * Finds or creates the inventory item record for the given purchasable, always - * keyed by its canonical id so drafts and revisions resolve to the same row as - * their canonical. Returns null if the purchasable type does not track inventory - * or there is no canonical id yet. - * - * @param Purchasable $purchasable - * @return InventoryItemRecord|null - * @since 5.6.4 - */ - public function ensureInventoryItemRecord(Purchasable $purchasable): ?InventoryItemRecord - { - if (!$purchasable::hasInventory()) { - return null; - } - - $canonicalId = $purchasable->getCanonicalId(); - if (!$canonicalId) { - return null; - } - - /** @var InventoryItemRecord|null $record */ - $record = InventoryItemRecord::find() - ->where(['purchasableId' => $canonicalId]) - ->one(); - - if (!$record) { - $record = new InventoryItemRecord(); - $record->purchasableId = $canonicalId; - $record->countryCodeOfOrigin = ''; - $record->administrativeAreaCodeOfOrigin = ''; - $record->harmonizedSystemCode = ''; - $record->save(); - } - - return $record; - } - - /** - * @param int $id - * @return InventoryItem - */ - public function getInventoryItemById(int $id): InventoryItem - { - $inventoryItem = $this->getInventoryItemQuery() - ->where(['id' => $id]) - ->one(); - - return $this->_populateInventoryItem($inventoryItem); - } - - /** - * @param array $ids - * @return Collection - */ - public function getInventoryItemsByIds(array $ids): Collection - { - $inventoryItemsResults = $this->getInventoryItemQuery() - ->where(['id' => $ids]) - ->all(); - - $inventoryItems = collect(); - foreach ($inventoryItemsResults as $inventoryItem) { - $inventoryItems->push($this->_populateInventoryItem($inventoryItem)); - } - - return $inventoryItems; - } - - /** - * Returns an inventory level model which is the sum of all inventory movements types for an item in a location. - * - * @param InventoryItem|int $inventoryItem - * @param InventoryLocation|int $inventoryLocation - * @param bool $withTrashed - * @return ?InventoryLevel - */ - public function getInventoryLevel(InventoryItem|int $inventoryItem, InventoryLocation|int $inventoryLocation, bool $withTrashed = false): ?InventoryLevel - { - $inventoryItemId = $inventoryItem instanceof InventoryItem ? $inventoryItem->id : $inventoryItem; - $inventoryLocationId = $inventoryLocation instanceof InventoryLocation ? $inventoryLocation->id : $inventoryLocation; - - $result = $this->getInventoryLevelQuery(withTrashed: $withTrashed, inventoryLocationId: $inventoryLocationId) - ->andWhere([ - 'inventoryLocationId' => $inventoryLocationId, - 'inventoryItemId' => $inventoryItemId, - ])->one(); - - if (!$result) { - return null; - } - - return $this->_populateInventoryLevel($result); - } - - /** - * @param InventoryItem $inventoryItem - * @param bool $validate - * @return bool - * @throws InvalidConfigException - */ - public function saveInventoryItem(InventoryItem $inventoryItem, bool $validate = true): bool - { - /** @var ?InventoryItemRecord $inventoryItemRecord */ - $inventoryItemRecord = InventoryItemRecord::find() - ->where(['id' => $inventoryItem->id]) - ->one(); - - if ($inventoryItemRecord === null) { - throw new InvalidConfigException('No inventory item exists with the ID “' . $inventoryItem->id . '”'); - } - - $inventoryItemRecord->purchasableId = $inventoryItem->purchasableId; - $inventoryItemRecord->countryCodeOfOrigin = $inventoryItem->countryCodeOfOrigin; - $inventoryItemRecord->administrativeAreaCodeOfOrigin = $inventoryItem->administrativeAreaCodeOfOrigin; - $inventoryItemRecord->harmonizedSystemCode = $inventoryItem->harmonizedSystemCode; - - return $inventoryItemRecord->save(); - } - - /** - * @param array $data - * @return InventoryItem - */ - private function _populateInventoryItem(array $data): InventoryItem - { - return new InventoryItem($data); - } - - /** - * @param array $data - * @return InventoryTransaction - */ - private function _populateInventoryTransaction(array $data): InventoryTransaction - { - return new InventoryTransaction($data); - } - - /** - * @param array $data - * @return InventoryLevel - */ - private function _populateInventoryLevel(array $data): InventoryLevel - { - unset($data['purchasableId']); - return new InventoryLevel($data); - } - - /** - * @param array $data - * @return InventoryFulfillmentLevel - */ - private function _populateInventoryFulfillmentLevel(array $data): InventoryFulfillmentLevel - { - return new InventoryFulfillmentLevel($data); - } - - /** - * @param InventoryLocation $inventoryLocation - * @param bool $withTrashed - * @return Collection - * @throws InvalidConfigException - */ - public function getInventoryLocationLevels(InventoryLocation $inventoryLocation, bool $withTrashed = false): Collection - { - $levels = $this->getInventoryLevelQuery(withTrashed: $withTrashed, inventoryLocationId: $inventoryLocation->id) - ->andWhere(['inventoryLocationId' => $inventoryLocation->id]) - ->andWhere(['not', ['elements.id' => null]]) - ->collect(); - - $inventoryItems = Plugin::getInstance()->getInventory()->getInventoryItemsByIds($levels->pluck('inventoryItemId')->unique()->toArray()); - return $levels->map(function($level) use ($inventoryItems) { - $inventoryLevel = $this->_populateInventoryLevel($level); - if ($item = $inventoryItems->firstWhere('id', $level['inventoryItemId'])) { - $inventoryLevel->setInventoryItem($item); - } - return $inventoryLevel; - }); - } - - /** - * Returns the totals for inventory items grouped by location and purchasable/inventoryItem. - * - * @param int|null $limit - * @param int|null $offset - * @param bool $withTrashed - * @return Query - */ - public function getInventoryLevelQuery(?int $limit = null, ?int $offset = null, bool $withTrashed = false, ?int $inventoryLocationId = null): Query - { - $inventoryTotals = (new Query()) - ->select([ - 'inventoryLocationId' => '[[il.id]]', - 'inventoryItemId' => '[[ii.id]]', - 'type' => '[[it.type]]', - 'quantity' => (new Expression('COALESCE(SUM([[it.quantity]]), 0)')), - ]) - ->from(['il' => Table::INVENTORYLOCATIONS]) // we want a record for every location and... - ->join('CROSS JOIN', ['ii' => Table::INVENTORYITEMS]) // ...every inventory item - ->leftJoin(['it' => Table::INVENTORYTRANSACTIONS], "[[il.id]] = [[it.inventoryLocationId]] AND [[ii.id]] = [[it.inventoryItemId]]") - ->groupBy(['[[il.id]]', '[[ii.id]]', '[[it.type]]']); - - // Scoping the location in the subquery prevents the CROSS JOIN from expanding - // to all locations × all items before the outer WHERE can filter it down. - if ($inventoryLocationId !== null) { - $inventoryTotals->andWhere(['il.id' => $inventoryLocationId]); - } - - $query = (new Query()) - ->select([ - '[[ii.id]] as inventoryItemId', - '[[ii.purchasableId]] as purchasableId', - '[[it.inventoryLocationId]] as inventoryLocationId', - 'SUM(CASE WHEN [[it.type]] = \'available\' THEN [[it.quantity]] ELSE 0 END) as availableTotal', - 'SUM(CASE WHEN [[it.type]] = \'committed\' THEN [[it.quantity]] ELSE 0 END) as committedTotal', - 'SUM(CASE WHEN [[it.type]] = \'reserved\' THEN [[it.quantity]] ELSE 0 END) as reservedTotal', - 'SUM(CASE WHEN [[it.type]] = \'damaged\' THEN [[it.quantity]] ELSE 0 END) as damagedTotal', - 'SUM(CASE WHEN [[it.type]] = \'safety\' THEN [[it.quantity]] ELSE 0 END) as safetyTotal', - 'SUM(CASE WHEN [[it.type]] = \'qualityControl\' THEN [[it.quantity]] ELSE 0 END) as qualityControlTotal', - 'SUM(CASE WHEN [[it.type]] = \'incoming\' THEN [[it.quantity]] ELSE 0 END) as incomingTotal', - 'SUM(CASE WHEN [[it.type]] IN (\'qualityControl\',\'safety\',\'damaged\',\'reserved\') THEN [[it.quantity]] ELSE 0 END) as unavailableTotal', - 'SUM(CASE WHEN [[it.type]] IN (\'qualityControl\',\'safety\',\'damaged\',\'reserved\', \'available\', \'committed\') THEN [[it.quantity]] ELSE 0 END) as onHandTotal', - ]) - ->from(['ii' => Table::INVENTORYITEMS]) - ->leftJoin(['it' => $inventoryTotals], '[[it.inventoryItemId]] = [[ii.id]]') - ->groupBy(["[[ii.id]]", "[[ii.purchasableId]]", "[[it.inventoryLocationId]]"]) - ->limit($limit) - ->offset($offset); - - $query->leftJoin( - ['elements' => CraftTable::ELEMENTS], - '[[ii.purchasableId]] = [[elements.id]] AND [[elements.draftId]] IS NULL AND [[elements.revisionId]] IS NULL' - ); - - if (!$withTrashed) { - $query->andWhere(['elements.dateDeleted' => null]); - } - - return $query; - } - - /** - * @return Query - */ - public function getInventoryItemQuery(): Query - { - return (new Query()) - ->select([ - 'id', - 'purchasableId', - 'countryCodeOfOrigin', - 'administrativeAreaCodeOfOrigin', - 'harmonizedSystemCode', - ]) - ->from(Table::INVENTORYITEMS); - } - - /** - * @param UpdateInventoryLevelCollection $updateInventoryLevels - * @return bool - * @throws Exception - */ - public function executeUpdateInventoryLevels(UpdateInventoryLevelCollection $updateInventoryLevels): bool - { - if ($updateInventoryLevels->count() < 1) { - return true; - } - - $transaction = Craft::$app->getDb()->beginTransaction(); - - try { - foreach ($updateInventoryLevels as $updateInventoryLevel) { - if ($updateInventoryLevel->updateAction === InventoryUpdateQuantityType::SET) { - $this->_setInventoryLevel($updateInventoryLevel); - } else { - $this->_adjustInventoryLevel($updateInventoryLevel); - } - } - - $transaction->commit(); - - // @TODO Consider pushing updateStoreStockCache() into a queued job so inventory updates don't block on cache regeneration - // Update all purchasables stock - foreach ($updateInventoryLevels->getPurchasables() as $purchasable) { - Plugin::getInstance()->getPurchasables()->updateStoreStockCache($purchasable, true); - } - - // Trigger event for each successful update - foreach ($updateInventoryLevels as $updateInventoryLevel) { - if ($this->hasEventHandlers(self::EVENT_AFTER_EXECUTE_UPDATE_INVENTORY_LEVEL)) { - $this->trigger(self::EVENT_AFTER_EXECUTE_UPDATE_INVENTORY_LEVEL, new UpdateInventoryLevelEvent([ - 'updateInventoryLevel' => $updateInventoryLevel, - ])); - } - } - - return true; - } catch (\Exception $e) { - $transaction->rollBack(); - throw $e; - } - } - - /** - * @param int $inventoryItemId - * @param int $quantity - * @param array $updateInventoryLevelAttributes - * @return void - * @throws Exception - * @throws InvalidConfigException - * @since 5.3.0 - */ - public function updateInventoryLevel(int $inventoryItemId, int $quantity, array $updateInventoryLevelAttributes = []) - { - $updateInventoryLevelAttributes += [ - 'quantity' => $quantity, - 'updateAction' => InventoryUpdateQuantityType::SET, - 'inventoryLocationId' => Plugin::getInstance()->getInventoryLocations()->getAllInventoryLocations()->first()->id, - 'type' => InventoryTransactionType::AVAILABLE->value, - ]; - - $updateInventoryLevel = new UpdateInventoryLevel($updateInventoryLevelAttributes); - $updateInventoryLevel->inventoryItemId = $inventoryItemId; - - $updateInventoryLevels = UpdateInventoryLevelCollection::make(); - $updateInventoryLevels->push($updateInventoryLevel); - - Plugin::getInstance()->getInventory()->executeUpdateInventoryLevels($updateInventoryLevels); - } - - /** - * @param Purchasable $purchasable - * @param int $quantity - * @param array $updateInventoryLevelAttributes - * @return void - * @throws Exception - * @throws InvalidConfigException - * @throws \craft\errors\DeprecationException - * @since 5.3.0 - */ - public function updatePurchasableInventoryLevel(Purchasable $purchasable, int $quantity, array $updateInventoryLevelAttributes = []) - { - $inventoryLocation = $purchasable->getStore()->getInventoryLocations()->first(); - - if (!$inventoryLocation) { - // If no inventory location exists, we can't update inventory - // @TODO Change this method's return type and either return false or throw a typed exception when no inventory location is available, so callers can react instead of silently succeeding - return; - } - - $updateInventoryLevelAttributes += [ - 'quantity' => $quantity, - 'updateAction' => InventoryUpdateQuantityType::SET, - 'inventoryItemId' => $purchasable->inventoryItemId, - 'inventoryLocationId' => $inventoryLocation->id, - 'type' => InventoryTransactionType::AVAILABLE->value, - ]; - - $this->updateInventoryLevel($purchasable->inventoryItemId, $quantity, $updateInventoryLevelAttributes); - - // Clear the stock cache for the class instance - unset($purchasable->stock); // set _stock to null - } - - /** - * @param UpdateInventoryLevel|UpdateInventoryLevelInTransfer $updateInventoryLevel - * @return bool - */ - private function _setInventoryLevel(UpdateInventoryLevel|UpdateInventoryLevelInTransfer $updateInventoryLevel): bool - { - $tableName = Table::INVENTORYTRANSACTIONS; - - if ($updateInventoryLevel->type === 'onHand') { - $types = collect(InventoryTransactionType::onHand())->pluck('value'); - } else { - $types = [$updateInventoryLevel->type]; - } - $quantityQuery = (new Query()) - ->select([':quantity - COALESCE(SUM(quantity), 0)']) - ->from($tableName) - ->where([ - 'type' => $types, - 'inventoryItemId' => $updateInventoryLevel->inventoryItemId, - 'inventoryLocationId' => $updateInventoryLevel->inventoryLocationId, - ]) - ->params([':quantity' => $updateInventoryLevel->quantity]) - ->scalar(); - - $type = $updateInventoryLevel->type; - if ($updateInventoryLevel->type === 'onHand') { - $type = InventoryTransactionType::AVAILABLE->value; - } - - $data = [ - 'quantity' => $quantityQuery, - 'type' => $type, - 'inventoryItemId' => $updateInventoryLevel->inventoryItemId, - 'inventoryLocationId' => $updateInventoryLevel->inventoryLocationId, - 'note' => $updateInventoryLevel->note, - 'movementHash' => $this->getMovementHash(), - 'dateCreated' => Db::prepareDateForDb(new \DateTime()), - 'userId' => Craft::$app->getUser()->getIdentity()?->id, - ]; - - if ($updateInventoryLevel instanceof UpdateInventoryLevelInTransfer) { - $data['transfer'] = $updateInventoryLevel->transferId; - } - - Craft::$app->db->createCommand() - ->insert($tableName, $data)->execute(); - - return true; - } - - /** - * @param UpdateInventoryLevel|UpdateInventoryLevelInTransfer $updateInventoryLevel - * @return bool - */ - private function _adjustInventoryLevel(UpdateInventoryLevel|UpdateInventoryLevelInTransfer $updateInventoryLevel): bool - { - $tableName = Table::INVENTORYTRANSACTIONS; - - $type = $updateInventoryLevel->type; - if ($updateInventoryLevel->type === 'onHand') { - $type = 'available'; - } - - Craft::$app->db->createCommand() - ->insert($tableName, [ - 'quantity' => $updateInventoryLevel->quantity, - 'type' => $type, - 'inventoryItemId' => $updateInventoryLevel->inventoryItemId, - 'inventoryLocationId' => $updateInventoryLevel->inventoryLocationId, - 'movementHash' => $this->getMovementHash(), - 'dateCreated' => Db::prepareDateForDb(new \DateTime()), - 'note' => $updateInventoryLevel->note, - ]) - ->execute(); - - return true; - } - - /** - * - * @param InventoryMovementCollection $inventoryMovements - * @return bool - * @throws \yii\db\Exception - */ - public function executeInventoryMovements(InventoryMovementCollection $inventoryMovements): bool - { - $tableName = Table::INVENTORYTRANSACTIONS; - - $db = Craft::$app->getDb(); - $transaction = $db->beginTransaction(); - - try { - /** @var InventoryMovementInterface $inventoryMovement */ - foreach ($inventoryMovements as $inventoryMovement) { - if (!$inventoryMovement->isValid()) { - $transaction->rollBack(); - return false; - } - - $movementDate = Db::prepareDateForDb(new \DateTime()); - - // First insert operation - $fromInsertResult = $db->createCommand() - ->insert($tableName, [ - 'quantity' => -$inventoryMovement->getQuantity(), - 'type' => $inventoryMovement->getFromInventoryTransactionType()->value, - 'inventoryItemId' => $inventoryMovement->getInventoryItem()->id, - 'inventoryLocationId' => $inventoryMovement->getFromInventoryLocation()->id, - 'movementHash' => $inventoryMovement->getInventoryMovementHash(), - 'dateCreated' => $movementDate, - 'transferId' => $inventoryMovement->getTransferId(), - 'lineItemId' => $inventoryMovement->getLineItemId(), - 'userId' => $inventoryMovement->getUserId(), - 'note' => $inventoryMovement->getNote(), - ]) - ->execute(); - - if (!$fromInsertResult) { - $transaction->rollBack(); - return false; - } - - // Second insert operation - $toInsertResult = $db->createCommand() - ->insert($tableName, [ - 'quantity' => $inventoryMovement->getQuantity(), - 'type' => $inventoryMovement->getToInventoryTransactionType()->value, - 'inventoryItemId' => $inventoryMovement->getInventoryItem()->id, - 'inventoryLocationId' => $inventoryMovement->getToInventoryLocation()->id, - 'movementHash' => $inventoryMovement->getInventoryMovementHash(), - 'dateCreated' => $movementDate, - 'transferId' => $inventoryMovement->getTransferId(), - 'lineItemId' => $inventoryMovement->getLineItemId(), - 'userId' => $inventoryMovement->getUserId(), - 'note' => $inventoryMovement->getNote(), - ]) - ->execute(); - - if (!$toInsertResult) { - $transaction->rollBack(); - return false; - } - } - - $transaction->commit(); - - // @TODO Consider pushing the per-movement updateStoreStockCache() calls into a queued job so large batch movements don't block on cache regeneration - foreach ($inventoryMovements as $inventoryMovement) { - // Update all purchasables stock - $purchasable = $inventoryMovement->getInventoryItem()->getPurchasable(); - if ($purchasable) { - Plugin::getInstance()->getPurchasables()->updateStoreStockCache($purchasable, true); - } - } - - // Trigger event for each successful movement - foreach ($inventoryMovements as $inventoryMovement) { - if ($this->hasEventHandlers(self::EVENT_AFTER_EXECUTE_INVENTORY_MOVEMENT)) { - $this->trigger(self::EVENT_AFTER_EXECUTE_INVENTORY_MOVEMENT, new InventoryMovementEvent([ - 'inventoryMovement' => $inventoryMovement, - ])); - } - } - - return true; - } catch (\Exception $e) { - $transaction->rollBack(); - throw $e; - } - } - - - /** - * @return string - */ - public function getMovementHash(): string - { - return md5(uniqid((string)mt_rand(), true)); - } - - /** - * @param InventoryItem|int $inventoryItem - * @param InventoryLocation|int $inventoryLocation - * @return array - */ - public function getUnfulfilledOrders(InventoryItem|int $inventoryItem, InventoryLocation|int $inventoryLocation): array - { - $inventoryItemId = $inventoryItem instanceof InventoryItem ? $inventoryItem->id : $inventoryItem; - $inventoryLocationId = $inventoryLocation instanceof InventoryLocation ? $inventoryLocation->id : $inventoryLocation; - - $inventoryLevel = $this->getInventoryLevel($inventoryItemId, $inventoryLocationId); - - if ($inventoryLevel->committedTotal <= 0) { - return []; - } - - // Get orders that have line items for this inventory level item - $orderIds = (new Query()) - ->select(['lineItems.orderId']) - ->from(['lineItems' => Table::LINEITEMS]) - ->leftJoin(['orders' => Table::ORDERS], '[[lineItems.orderId]] = [[orders.id]]') - ->leftJoin(['it' => Table::INVENTORYTRANSACTIONS], '[[it.lineItemId]] = [[lineItems.id]]') - ->where(['orders.isCompleted' => true]) - ->andWhere(['it.inventoryItemId' => $inventoryItemId]) - ->andWhere(['it.inventoryLocationId' => $inventoryLocationId]) - ->andWhere(['it.type' => InventoryTransactionType::COMMITTED->value]) - ->addSelect(['lineItems.qty']) - ->groupBy(['lineItems.orderId', 'lineItems.id', 'lineItems.qty']) - ->having(new Expression('SUM([[it.quantity]]) >= [[lineItems.qty]]')) - ->column(); - - return Order::find() - ->id($orderIds) - ->all(); - } - - /** - * @return Query - */ - public function getTransactionQuery(): Query - { - return (new Query()) - ->select([ - 'inventoryLocationId', - 'inventoryItemId', - 'movementHash', - 'quantity', - 'type', - 'note', - 'transferId', - 'lineItemId', - 'userId', - 'dateCreated', - ]) - ->orderBy(['dateCreated' => SORT_DESC]) - ->from(Table::INVENTORYTRANSACTIONS); - } - - /** - * @param InventoryItem $inventoryItem - * @param InventoryLocation $inventoryLocation - * @return Collection - */ - public function getInventoryTransactions(InventoryItem $inventoryItem, InventoryLocation $inventoryLocation): Collection - { - $transactions = $this->getTransactionQuery() - ->where(['inventoryItemId' => $inventoryItem->id, 'inventoryLocationId' => $inventoryLocation->id]) - ->all(); - - foreach ($transactions as $key => $transaction) { - $transactions[$key] = $this->_populateInventoryTransaction($transaction); - } - - return collect($transactions); - } - - /** - * @param Order $order - * @return Collection - * @throws InvalidConfigException - * @throws \craft\errors\DeprecationException - */ - public function getInventoryFulfillmentLevels(Order $order): Collection - { - // We don’t limit this to the orders store locations since we want to show all locations that have historical inventory for the order. - $locations = Plugin::getInstance()->getInventoryLocations()->getAllInventoryLocations(); - - $inventoryFulfillmentLevels = []; - foreach ($locations as $location) { - $data = (new Query()) - ->select([ - '[[it.lineItemId]]', - '[[it.inventoryItemId]]', - '[[it.inventoryLocationId]]', - - 'SUM(CASE WHEN (([[it.type]] = :committedType AND quantity > 0) OR ([[it.type]] = :fulfilledType AND quantity < 0)) THEN [[quantity]] ELSE 0 END) AS committedQuantity', - - 'SUM(CASE WHEN [[it.type]] = :committedType THEN [[quantity]] ELSE 0 END) AS outstandingCommittedQuantity', - 'SUM(CASE WHEN [[it.type]] = :fulfilledType THEN [[quantity]] ELSE 0 END) AS fulfilledQuantity', - ]) - ->from(['it' => Table::INVENTORYTRANSACTIONS]) - ->andWhere([ - '[[li.orderId]]' => $order->id, - '[[it.inventoryLocationId]]' => $location->id, - ]) - ->andWhere(['or', - ['it.type' => InventoryTransactionType::COMMITTED->value], - ['it.type' => InventoryTransactionType::FULFILLED->value], - ]) - ->groupBy([ - '[[it.lineItemId]]', - '[[it.inventoryItemId]]', - '[[it.inventoryLocationId]]', - ]) - ->params([ - ':committedType' => InventoryTransactionType::COMMITTED->value, - ':fulfilledType' => InventoryTransactionType::FULFILLED->value, - ]) - ->innerJoin(['li' => Table::LINEITEMS], '[[li.id]] = [[it.lineItemId]]') - ->all(); - - foreach ($data as $row) { - $inventoryFulfillmentLevels[] = $this->_populateInventoryFulfillmentLevel($row); - } - } - - return collect($inventoryFulfillmentLevels); - } - - /** - * @param Order $order - * @return void - * @throws InvalidConfigException - * @throws \yii\db\Exception - */ - public function orderCompleteHandler(Order $order) - { - /** @var Collection[] $allInventoryLevels */ - $allInventoryLevels = []; - $qtyLineItem = []; - foreach ($order->getLineItems() as $lineItem) { - if ($lineItem->type === LineItemType::Custom) { - // Skip custom line items - continue; - } - - $purchasable = $lineItem->getPurchasable(); - // Don't reduce stock of unlimited items. - - if (!$purchasable::hasInventory()) { - continue; - } - - if ($purchasable->inventoryTracked) { - if (!isset($qtyLineItem[$purchasable->id])) { - $qtyLineItem[$purchasable->id] = 0; - } - $qtyLineItem[$purchasable->id] += $lineItem->qty; - $allInventoryLevels[$purchasable->id] = $purchasable->getInventoryLevels(); - } - } - - $selectedInventoryLevelForItem = []; - /** - * @var int $purchasableId - * @var Collection $inventoryLevels - */ - foreach ($allInventoryLevels as $purchasableId => $inventoryLevels) { - foreach ($inventoryLevels as $level) { - if (!isset($selectedInventoryLevelForItem[$purchasableId])) { - $selectedInventoryLevelForItem[$purchasableId] = $level; - - if ($level->availableTotal >= $qtyLineItem[$purchasableId]) { - break; - } - continue; - } - - if ($level->availableTotal >= $qtyLineItem[$purchasableId]) { - $selectedInventoryLevelForItem[$purchasableId] = $level; - break; - } - } - } - - $movements = InventoryMovementCollection::make(); - - $reserveAmountByPurchasableId = []; - $availableTotalByPurchasableIdAndLocationId = []; - - // Loop through line items and create committed movements for the selected inventory location - foreach ($order->getLineItems() as $lineItem) { - if (isset($selectedInventoryLevelForItem[$lineItem->purchasableId])) { - $level = $selectedInventoryLevelForItem[$lineItem->purchasableId]; - - if (!isset($reserveAmountByPurchasableId[$lineItem->purchasableId])) { - $availableTotalByPurchasableIdAndLocationId[$lineItem->purchasableId . '-' . $level->inventoryLocationId] = $level->availableTotal; - $reserveAmountByPurchasableId[$lineItem->purchasableId] = []; - } - - if ($lineItem->qty > $availableTotalByPurchasableIdAndLocationId[$lineItem->purchasableId . '-' . $level->inventoryLocationId]) { - $totalToReserveForLineItem = $lineItem->qty - $availableTotalByPurchasableIdAndLocationId[$lineItem->purchasableId . '-' . $level->inventoryLocationId]; - $reserveAmountByPurchasableId[$lineItem->purchasableId][$lineItem->id] = $totalToReserveForLineItem; - $availableTotalByPurchasableIdAndLocationId[$lineItem->purchasableId . '-' . $level->inventoryLocationId] = 0; - } else { - $availableTotalByPurchasableIdAndLocationId[$lineItem->purchasableId . '-' . $level->inventoryLocationId] -= $lineItem->qty; - } - - $inventoryCommittedMovement = new InventoryCommittedMovement(); - $inventoryCommittedMovement->inventoryItemId = $level->inventoryItemId; - $inventoryCommittedMovement->fromInventoryLocation = $level->getInventoryLocation(); - $inventoryCommittedMovement->toInventoryLocation = $level->getInventoryLocation(); - $inventoryCommittedMovement->fromInventoryTransactionType = InventoryTransactionType::AVAILABLE; - $inventoryCommittedMovement->toInventoryTransactionType = InventoryTransactionType::COMMITTED; - $inventoryCommittedMovement->quantity = $lineItem->qty; - $inventoryCommittedMovement->lineItemId = $lineItem->id; - - $movements->push($inventoryCommittedMovement); - } - } - - // Loop through reserve amounts to reserve the remaining stock in the other inventory locations - foreach ($reserveAmountByPurchasableId as $purchasableId => $r) { - foreach ($r as $lineItemId => $qty) { - foreach ($allInventoryLevels[$purchasableId] as $level) { - if ($level === $selectedInventoryLevelForItem[$purchasableId]) { - continue; - } - - if (!isset($availableTotalByPurchasableIdAndLocationId[$purchasableId . '-' . $level->inventoryLocationId])) { - $availableTotalByPurchasableIdAndLocationId[$purchasableId . '-' . $level->inventoryLocationId] = $level->availableTotal; - } - - $canReserveFullQty = $qty <= $availableTotalByPurchasableIdAndLocationId[$purchasableId . '-' . $level->inventoryLocationId]; - $qtyToReserve = $canReserveFullQty ? $qty : $availableTotalByPurchasableIdAndLocationId[$purchasableId . '-' . $level->inventoryLocationId]; - - if ($qtyToReserve < 1) { - break; - } - - $availableTotalByPurchasableIdAndLocationId[$purchasableId . '-' . $level->inventoryLocationId] -= $qtyToReserve; - - $inventoryManualMovement = new InventoryManualMovement(); - $inventoryManualMovement->inventoryItemId = $level->inventoryItemId; - $inventoryManualMovement->fromInventoryLocation = $level->getInventoryLocation(); - $inventoryManualMovement->toInventoryLocation = $level->getInventoryLocation(); - $inventoryManualMovement->fromInventoryTransactionType = InventoryTransactionType::AVAILABLE; - $inventoryManualMovement->toInventoryTransactionType = InventoryTransactionType::RESERVED; - $inventoryManualMovement->quantity = $qtyToReserve; - $inventoryManualMovement->lineItemId = $lineItemId; - - $movements->push($inventoryManualMovement); - - $qty -= $qtyToReserve; - if ($qty <= 0) { - break; - } - } - } - } - - $this->executeInventoryMovements($movements); - - foreach ($selectedInventoryLevelForItem as $key => $inventoryLevel) { - if ($purchasable = Craft::$app->getElements()->getElementById($key)) { - if ($purchasable instanceof Purchasable) { - Plugin::getInstance()->getPurchasables()->updateStoreStockCache($purchasable, true); - - // If the purchasable doesn't allow out of stock purchases, check whether the movement - // pushed available stock below zero (e.g. due to concurrent orders). - if (!$purchasable->allowOutOfStockPurchases) { - $freshLevel = $this->getInventoryLevel($inventoryLevel->inventoryItemId, $inventoryLevel->inventoryLocationId); - if ($freshLevel && $freshLevel->availableTotal < 0) { - $notice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'inventoryBelowZero', - 'attribute' => 'lineItems', - 'message' => Craft::t('commerce', 'Available inventory for "{description}" has gone below zero.', [ - 'description' => $purchasable->getDescription(), - ]), - 'noticeType' => OrderNoticeType::Admin, - ], - ]); - $order->addNotice($notice); - } - } - } - } - } - } -} diff --git a/src/services/InventoryLocations.php b/src/services/InventoryLocations.php deleted file mode 100644 index 1b0dc030f0..0000000000 --- a/src/services/InventoryLocations.php +++ /dev/null @@ -1,387 +0,0 @@ - - * @since 5.0 - */ -class InventoryLocations extends Component -{ - /** - * @var Collection|null - */ - private ?Collection $_allLocations = null; - - /** - * @var Collection|null - */ - private ?Collection $_allLocationsWithTrashed = null; - - /** - * @var array> Inventory location IDs for a store, indexed by store ID. - */ - private array $_inventoryLocationIdsByStore = []; - - /** - * Returns all inventory locations. - * - * @param bool $withTrashed - * @return Collection All locations - * @throws DeprecationException - * @throws InvalidConfigException - */ - public function getAllInventoryLocations(bool $withTrashed = false): Collection - { - return $this->_getAllInventoryLocations($withTrashed); - } - - /** - * Returns all inventory locations as a list. - * - * @param bool $withTrashed - * @return array All locations as key value list - * @throws DeprecationException - * @throws InvalidConfigException - * @since 5.1.0 - */ - public function getAllInventoryLocationsAsList(bool $withTrashed = false): array - { - return $this->getAllInventoryLocations($withTrashed)->mapWithKeys(fn(InventoryLocation $location) => [$location->id => $location->getUiLabel()])->toArray(); - } - - /** - * Returns an inventory location by its ID. - * - * @param int $id - * @param bool $withTrashed - * @return InventoryLocation|null The inventory location or null if not found. - */ - public function getInventoryLocationById(int $id, bool $withTrashed = false): ?InventoryLocation - { - return $this->_getAllInventoryLocations($withTrashed)->firstWhere('id', $id); - } - - /** - * Gets all inventory locations for a store in order of configuration. - * - * @param ?int $storeId - * - * @return Collection - */ - public function getInventoryLocations(?int $storeId = null, bool $withTrashed = false): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if (!isset($this->_inventoryLocationIdsByStore[$storeId])) { - $this->_inventoryLocationIdsByStore[$storeId] = (new Query()) - ->select(['inventoryLocationId']) - ->from([Table::INVENTORYLOCATIONS_STORES]) - ->orderBy(['sortOrder' => SORT_ASC]) - ->where(['storeId' => $storeId]) - ->column(); - } - - $locationIds = $this->_inventoryLocationIdsByStore[$storeId]; - - // Keep the order of the locationIds - return $this->_getAllInventoryLocations($withTrashed)->whereIn('id', $locationIds)->sortBy(fn($inventoryLocation) => array_search($inventoryLocation->id, $locationIds)); - } - - /** - * Stores the relationship between a Store and its Inventory Locations, ordered by preference. - * - * @param Store $store - * @param array $inventoryLocationIds - * @return bool - * @throws Throwable - * @throws \yii\db\Exception - */ - public function saveStoreInventoryLocations(Store $store, array $inventoryLocationIds): bool - { - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - // Delete existing - Craft::$app->getDb()->createCommand() - ->delete(Table::INVENTORYLOCATIONS_STORES, ['storeId' => $store->id]) - ->execute(); - - $order = 1; - foreach ($inventoryLocationIds as $inventoryLocationId) { - Craft::$app->getDb()->createCommand() - ->insert(Table::INVENTORYLOCATIONS_STORES, [ - 'storeId' => $store->id, - 'inventoryLocationId' => $inventoryLocationId, - 'sortOrder' => $order++, - ]) - ->execute(); - } - - $transaction->commit(); - - // Clear memoization cache - $this->_inventoryLocationIdsByStore = []; - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - - return true; - } - - /** - * @return bool - * @throws Throwable - * @throws \yii\db\Exception - */ - public function executeDeactivateInventoryLocation(DeactivateInventoryLocation $deactivateInventoryLocation): bool - { - // This will ensure that the location has no committed stock or incoming stock before deactivating it. - if (!$deactivateInventoryLocation->validate()) { - return false; - } - - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - /** @var SoftDeleteBehavior $inventoryLocationRecord */ - $inventoryLocationRecord = InventoryLocationRecord::findOne($deactivateInventoryLocation->inventoryLocation->id); - - // Get draft transfers that are destinations for the deactivated inventory location -// /** @var Transfer $draftTransfers */ -// $draftTransfers = Transfer::find() -// ->transferStatus(TransferStatusType::DRAFT) -// ->destinationLocation($deactivateInventoryLocation->inventoryLocation) -// ->all(); - - // Switch the draft transfer to the new destination location -// foreach ($draftTransfers as $draftTransfer) { -// $draftTransfer->destinationLocationId = $deactivateInventoryLocation->destinationInventoryLocation->id; -// Craft::$app->getElements()->saveElement($draftTransfer, false); -// } - - // @TODO Reassign any draft purchase orders that target the deactivated inventory location to the destination location (mirroring the draft transfer handling above) - - $inventoryLevels = Plugin::getInstance()->getInventory()->getInventoryLocationLevels($deactivateInventoryLocation->inventoryLocation); - /** @var InventoryLevel $inventoryLevel */ - foreach ($inventoryLevels as $inventoryLevel) { - $movements = new InventoryMovementCollection(); - foreach (InventoryTransactionType::allowedManualMoveTransactionTypes() as $type) { - if ($inventoryLevel->getTotal($type) > 0) { - $inventoryMovement = new InventoryLocationDeactivatedMovement(); - $inventoryMovement->fromInventoryLocation = $deactivateInventoryLocation->inventoryLocation; - $inventoryMovement->toInventoryLocation = $deactivateInventoryLocation->destinationInventoryLocation; - $inventoryMovement->inventoryItemId = $inventoryLevel->inventoryItemId; - $inventoryMovement->quantity = $inventoryLevel->getTotal($type); - $inventoryMovement->fromInventoryTransactionType = $type; - $inventoryMovement->toInventoryTransactionType = $type; - $inventoryMovement->userId = Craft::$app->getUser()->getIdentity()?->id; - $inventoryMovement->note = Craft::t('commerce', 'Movement from deactivated inventory location'); - $movements->add($inventoryMovement); - } - } - - if ($movements->count() > 0) { - if (!Plugin::getInstance()->getInventory()->executeInventoryMovements($movements)) { - throw new \Exception('Failed to move inventory from deactivated location'); - } - } - } - - $transaction->commit(); - // Finally soft delete it now that it's all migrated - $inventoryLocationRecord->softDelete(); - - // Clear memoization cache - $this->_allLocations = null; - $this->_allLocationsWithTrashed = null; - } catch (Throwable $e) { - $transaction->rollBack(); - - throw $e; - } - - return true; - } - - /** - * Returns a location by its handle. - * - * @param string $handle - * @return InventoryLocation|null The location or null if not found. - * @throws DeprecationException - * @throws InvalidConfigException - */ - public function getInventoryLocationByHandle(string $handle): ?InventoryLocation - { - return $this->getAllInventoryLocations()->firstWhere('handle', $handle); - } - - /** - * Saves an inventory location. - * - */ - public function saveInventoryLocation(InventoryLocation $inventoryLocation, bool $runValidation = true): bool - { - $isNewLocation = !$inventoryLocation->id; - - if ($runValidation && !$inventoryLocation->validate()) { - Craft::info('Inventory Location not saved due to validation error.', __METHOD__); - return false; - } - - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - - /** @var ?InventoryLocationRecord $locationRecord */ - $locationRecord = InventoryLocationRecord::find() - ->where(['id' => $inventoryLocation->id]) - ->one(); - - if ($locationRecord === null) { - $locationRecord = new InventoryLocationRecord(); - } - - $locationRecord->name = $inventoryLocation->name; - $locationRecord->handle = $inventoryLocation->handle; - $locationRecord->addressId = $inventoryLocation->getAddress()->id; - - // Save the inventory location - $locationRecord->save(false); - - if ($isNewLocation) { - $inventoryLocation->id = $locationRecord->id; - } - - $transaction->commit(); - - // Clear memoization cache - $this->_allLocations = null; - $this->_allLocationsWithTrashed = null; - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - - return true; - } - - /** - * Returns a Query object prepped for retrieving locations. - * - * @return Query The query object. - */ - private function _createInventoryLocationsQuery(bool $withTrashed = false): Query - { - $query = (new Query()) - ->select([ - 'id', - 'name', - 'handle', - 'addressId', - 'dateCreated', - 'dateUpdated', - ]) - ->orderBy(['name' => SORT_ASC]) - ->from([Table::INVENTORYLOCATIONS]); - - if (!$withTrashed) { - $query->where(['dateDeleted' => null]); - } - - return $query; - } - - /** - * @return Collection - */ - private function _getAllInventoryLocations(bool $withTrashed = false): Collection - { - if ($withTrashed) { - if ($this->_allLocationsWithTrashed === null) { - $results = $this->_createInventoryLocationsQuery($withTrashed) - ->all(); - - $locations = []; - foreach ($results as $result) { - $locations[] = new InventoryLocation($result); - } - - $this->_allLocationsWithTrashed = collect($locations); - } - - return $this->_allLocationsWithTrashed; - } - - if ($this->_allLocations === null) { - $results = $this->_createInventoryLocationsQuery($withTrashed) - ->all(); - - $locations = []; - foreach ($results as $result) { - $locations[] = new InventoryLocation($result); - } - - $this->_allLocations = collect($locations); - } - - return $this->_allLocations; - } - - /** - * @param AuthorizationCheckEvent $event - * @return void - */ - public function authorizeInventoryLocationAddressView(AuthorizationCheckEvent $event): void - { - if (!$event->element instanceof Address) { - return; - } - - if ($this->getAllInventoryLocations(true)->firstWhere('addressId', $event->element->getCanonicalId()) === null) { - return; - } - - $event->authorized = true; - } - - public function authorizeInventoryLocationAddressEdit(AuthorizationCheckEvent $event): void - { - if (!$event->element instanceof Address) { - return; - } - - if ($this->getAllInventoryLocations(true)->firstWhere('addressId', $event->element->getCanonicalId()) === null) { - return; - } - - $event->authorized = true; - } -} diff --git a/src/services/LineItemStatuses.php b/src/services/LineItemStatuses.php deleted file mode 100644 index 6cfb580667..0000000000 --- a/src/services/LineItemStatuses.php +++ /dev/null @@ -1,371 +0,0 @@ - - * @since 2.0 - */ -class LineItemStatuses extends Component -{ - /** - * @event DefaultLineItemStatusEvent The event that is triggered when getting a default status for a line item. - * You may set [[DefaultLineItemStatusEvent::lineItemStatus]] to a desired LineItemStatus to override the default status set in control panel. - * - * Plugins can get notified when a default line item status is being fetched - * - * ```php - * use craft\commerce\events\DefaultLineItemStatusEvent; - * use craft\commerce\services\LineItemStatuses; - * use yii\base\Event; - * - * Event::on(LineItemStatuses::class, LineItemStatuses::EVENT_DEFAULT_LINE_ITEM_STATUS, function(DefaultLineItemStatusEvent $e) { - * // Perhaps determine a better default line item status than the one set in control panel - * }); - * ``` - */ - public const EVENT_DEFAULT_LINE_ITEM_STATUS = 'defaultLineItemStatus'; - - public const CONFIG_STATUSES_KEY = 'commerce.lineItemStatuses'; - - /** - * @var array|null - * @since 5.0.0 - */ - private ?array $_allLineItemStatuses = null; - - /** - * Get line item status by its handle. - */ - public function getLineItemStatusByHandle(string $handle, ?int $storeId = null): ?LineItemStatus - { - return $this->getAllLineItemStatuses($storeId)->firstWhere('handle', $handle); - } - - /** - * Get default lineItem status ID from the DB - * - * @noinspection PhpUnused - */ - public function getDefaultLineItemStatusId(?int $storeId = null): ?int - { - return $this->getDefaultLineItemStatus($storeId)?->id; - } - - /** - * Get default lineItem status from the DB - */ - public function getDefaultLineItemStatus(?int $storeId = null): ?LineItemStatus - { - return $this->getAllLineItemStatuses($storeId)->firstWhere('default', true); - } - - /** - * Get the default lineItem status for a particular lineItem. Defaults to the default lineItem status as configured - * in the control panel. - */ - public function getDefaultLineItemStatusForLineItem(LineItem $lineItem): ?LineItemStatus - { - if (!$order = $lineItem->getOrder()) { - return null; - } - - $lineItemStatus = $this->getDefaultLineItemStatus($order->getStore()->id); - - $event = new DefaultLineItemStatusEvent(); - $event->lineItemStatus = $lineItemStatus; - $event->lineItem = $lineItem; - - $this->trigger(self::EVENT_DEFAULT_LINE_ITEM_STATUS, $event); - - return $event->lineItemStatus; - } - - /** - * Save the line item status. - * - * @param bool $runValidation should we validate this line item status before saving. - * @throws Exception - * @throws ErrorException - */ - public function saveLineItemStatus(LineItemStatus $lineItemStatus, bool $runValidation = true): bool - { - $isNewStatus = !$lineItemStatus->id; - - if ($runValidation && !$lineItemStatus->validate()) { - Craft::info('Line item status not saved due to validation error.', __METHOD__); - - return false; - } - - if ($isNewStatus) { - $statusUid = StringHelper::UUID(); - } else { - $statusUid = Db::uidById(Table::LINEITEMSTATUSES, $lineItemStatus->id); - } - - // Make sure no statuses that are not archived share the handle - // @TODO Confirm LineItemStatus validation already enforces handle uniqueness per store and remove this duplicate runtime check if so - $existingStatus = $this->getLineItemStatusByHandle($lineItemStatus->handle, $lineItemStatus->storeId); - - if ($existingStatus && (!$lineItemStatus->id || $lineItemStatus->id !== $existingStatus->id)) { - $lineItemStatus->addError('handle', Craft::t('commerce', 'That handle is already in use')); - return false; - } - - $projectConfig = Craft::$app->getProjectConfig(); - - if ($lineItemStatus->isArchived) { - $configData = null; - } else { - $configData = $lineItemStatus->getConfig(); - } - - $configPath = self::CONFIG_STATUSES_KEY . '.' . $statusUid; - $projectConfig->set($configPath, $configData); - - if ($isNewStatus) { - $lineItemStatus->id = Db::idByUid(Table::LINEITEMSTATUSES, $statusUid); - } - - $this->_clearCaches(); - - return true; - } - - /** - * Handle line item status change. - * - * @throws Throwable if reasons - */ - public function handleChangedLineItemStatus(ConfigEvent $event): void - { - ProjectConfigData::ensureAllStoresProcessed(); - - $statusUid = $event->tokenMatches[0]; - $data = $event->newValue; - - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - $statusRecord = $this->_getLineItemStatusRecord($statusUid); - $store = Plugin::getInstance()->getStores()->getStoreByUid($data['store']); - - $statusRecord->storeId = $store->id; - $statusRecord->name = $data['name']; - $statusRecord->handle = $data['handle']; - $statusRecord->color = $data['color']; - $statusRecord->sortOrder = $data['sortOrder'] ?? 99; - $statusRecord->default = $data['default']; - $statusRecord->uid = $statusUid; - $statusRecord->isArchived = false; - $statusRecord->dateArchived = null; - - $statusRecord->save(false); - - if ($statusRecord->default) { - LineItemStatusRecord::updateAll(['default' => 0], ['and', - ['not', ['id' => $statusRecord->id]], - ['storeId' => $statusRecord->storeId], - ]); - } - - $transaction->commit(); - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - } - - /** - * Archive an line item status by it's id. - * - * @throws Throwable - */ - public function archiveLineItemStatusById(int $id, ?int $storeId = null): bool - { - $status = $this->getLineItemStatusById($id, $storeId); - if ($status) { - $status->isArchived = true; - return $this->saveLineItemStatus($status); - } - return false; - } - - - /** - * Handle line item status being archived - * - * @throws Throwable if reasons - */ - public function handleArchivedLineItemStatus(ConfigEvent $event): void - { - $lineItemStatusUid = $event->tokenMatches[0]; - - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - $lineItemStatusRecord = $this->_getLineItemStatusRecord($lineItemStatusUid); - - $lineItemStatusRecord->isArchived = true; - $lineItemStatusRecord->dateArchived = Db::prepareDateForDb(new DateTime()); - - // Save the volume - $lineItemStatusRecord->save(false); - - $transaction->commit(); - - $this->_clearCaches(); - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - } - - /** - * Returns all Order Statuses - * - * @param int|null $storeId - * @return Collection - * @throws SiteNotFoundException - * @throws InvalidConfigException - */ - public function getAllLineItemStatuses(?int $storeId = null): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if ($this->_allLineItemStatuses === null || !isset($this->_allLineItemStatuses[$storeId])) { - $results = $this->_createLineItemStatusesQuery() - ->andWhere(['storeId' => $storeId]) - ->all(); - - // Start with a blank slate if it isn't memoized - if ($this->_allLineItemStatuses === null) { - $this->_allLineItemStatuses = []; - } - - foreach ($results as $result) { - $lineItemStatus = Craft::createObject([ - 'class' => LineItemStatus::class, - 'attributes' => $result, - ]); - - if (!isset($this->_allLineItemStatuses[$lineItemStatus->storeId])) { - $this->_allLineItemStatuses[$lineItemStatus->storeId] = collect(); - } - - $this->_allLineItemStatuses[$lineItemStatus->storeId]->push($lineItemStatus); - } - } - - return $this->_allLineItemStatuses[$storeId] ?? collect(); - } - - /** - * Get a line item status by ID - */ - public function getLineItemStatusById(int $id, ?int $storeId = null): ?LineItemStatus - { - return $this->getAllLineItemStatuses($storeId)->firstWhere('id', $id); - } - - /** - * Reorders the line item statuses. - * - * @throws Exception - * @throws ErrorException - * @throws NotSupportedException - * @throws ServerErrorHttpException - */ - public function reorderLineItemStatuses(array $ids): bool - { - $projectConfig = Craft::$app->getProjectConfig(); - - $uidsByIds = Db::uidsByIds(Table::LINEITEMSTATUSES, $ids); - - foreach ($ids as $lineItemStatus => $statusId) { - if (!empty($uidsByIds[$statusId])) { - $statusUid = $uidsByIds[$statusId]; - $projectConfig->set(self::CONFIG_STATUSES_KEY . '.' . $statusUid . '.sortOrder', $lineItemStatus + 1); - } - } - - $this->_clearCaches(); - - return true; - } - - /** - * Returns a Query object prepped for retrieving line item statuses - */ - private function _createLineItemStatusesQuery(): Query - { - return (new Query()) - ->select([ - 'color', - 'default', - 'handle', - 'id', - 'name', - 'sortOrder', - 'storeId', - 'uid', - ]) - ->where(['isArchived' => false]) - ->orderBy('sortOrder') - ->from([Table::LINEITEMSTATUSES]); - } - - /** - * Gets an lineitem status' record by uid. - */ - private function _getLineItemStatusRecord(string $uid): LineItemStatusRecord - { - if ($lineItemStatus = LineItemStatusRecord::findOne(['uid' => $uid])) { - return $lineItemStatus; - } - - return new LineItemStatusRecord(); - } - - /** - * Clear all memoization - * - * @since 3.2.5 - */ - public function _clearCaches(): void - { - $this->_allLineItemStatuses = null; - } -} diff --git a/src/services/LineItems.php b/src/services/LineItems.php deleted file mode 100644 index 52e945b769..0000000000 --- a/src/services/LineItems.php +++ /dev/null @@ -1,587 +0,0 @@ - - * @since 2.0 - */ -class LineItems extends Component -{ - /** - * @event LineItemEvent The event that is triggered before a line item is saved. - * - * ```php - * use craft\commerce\events\LineItemEvent; - * use craft\commerce\services\LineItems; - * use craft\commerce\models\LineItem; - * use yii\base\Event; - * - * Event::on( - * LineItems::class, - * LineItems::EVENT_BEFORE_SAVE_LINE_ITEM, - * function(LineItemEvent $event) { - * // @var LineItem $lineItem - * $lineItem = $event->lineItem; - * // @var bool $isNew - * $isNew = $event->isNew; - * - * // Notify a third party service about changes to an order - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_SAVE_LINE_ITEM = 'beforeSaveLineItem'; - - /** - * @event LineItemEvent The event that is triggered after a line item is saved. - * - * ```php - * use craft\commerce\events\LineItemEvent; - * use craft\commerce\services\LineItems; - * use craft\commerce\models\LineItem; - * use yii\base\Event; - * - * Event::on( - * LineItems::class, - * LineItems::EVENT_AFTER_SAVE_LINE_ITEM, - * function(LineItemEvent $event) { - * // @var LineItem $lineItem - * $lineItem = $event->lineItem; - * // @var bool $isNew - * $isNew = $event->isNew; - * - * // Reserve stock - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_SAVE_LINE_ITEM = 'afterSaveLineItem'; - - /** - * @event LineItemEvent The event that is triggered after a line item has been created from a purchasable. - * - * ```php - * use craft\commerce\events\LineItemEvent; - * use craft\commerce\services\LineItems; - * use craft\commerce\models\LineItem; - * use yii\base\Event; - * - * Event::on( - * LineItems::class, - * LineItems::EVENT_CREATE_LINE_ITEM, - * function(LineItemEvent $event) { - * // @var LineItem $lineItem - * $lineItem = $event->lineItem; - * // @var bool $isNew - * $isNew = $event->isNew; - * - * // Call a third party service based on the line item options - * // ... - * } - * ); - * ``` - */ - public const EVENT_CREATE_LINE_ITEM = 'createLineItem'; - - /** - * @event LineItemEvent The event that is triggered as a line item is being populated from a purchasable. - * - * ```php - * use craft\commerce\events\LineItemEvent; - * use craft\commerce\services\LineItems; - * use craft\commerce\models\LineItem; - * use yii\base\Event; - * - * Event::on( - * LineItems::class, - * LineItems::EVENT_POPULATE_LINE_ITEM, - * function(LineItemEvent $event) { - * // @var LineItem $lineItem - * $lineItem = $event->lineItem; - * // @var bool $isNew - * $isNew = $event->isNew; - * - * // Modify the price of a line item - * // ... - * } - * ); - * ``` - */ - public const EVENT_POPULATE_LINE_ITEM = 'populateLineItem'; - - /** - * Returns an order's line items, per the order's ID. - * - * @param int $orderId the order's ID - * @return LineItem[] An array of all the line items for the matched order. - */ - public function getAllLineItemsByOrderId(int $orderId): array - { - $results = $this->_createLineItemQuery() - ->where(['orderId' => $orderId]) - ->all(); - - $lineItems = []; - - foreach ($results as $result) { - $result['snapshot'] = Json::decodeIfJson($result['snapshot']); - $lineItem = new LineItem($result); - $lineItems[] = $lineItem; - } - - return $lineItems; - } - - /** - * Takes an order, a purchasable ID, options, and resolves it to a line item. - * - * If a line item is found for that order ID with those exact options, that line item is - * returned. Otherwise, a new line item is returned. - * - * @param Order $order - * @param int $purchasableId the purchasable's ID - * @param array $options Options for the line item - * @return LineItem - * @throws \Exception - */ - public function resolveLineItem(Order $order, int $purchasableId, array $options = [], array $params = []): LineItem - { - $signature = LineItemHelper::generateOptionsSignature($options); - - $result = $order->id ? $this->_createLineItemQuery() - ->where([ - 'orderId' => $order->id, - 'purchasableId' => $purchasableId, - 'optionsSignature' => $signature, - ]) - ->one() : null; - - if ($result) { - $lineItem = new LineItem($result); - } else { - $params = array_merge([ - 'qty' => 1, - 'options' => $options, - 'note' => '', - 'purchasableId' => $purchasableId, - ], $params); - - $lineItem = $this->create($order, $params); - } - - return $lineItem; - } - - /** - * @param Order $order - * @param string $sku - * @param array $options - * @return LineItem - * @throws Exception - * @throws InvalidConfigException - * @throws SiteNotFoundException - * @since 5.1.0 - */ - public function resolveCustomLineItem(Order $order, string $sku, array $options = []): LineItem - { - $signature = LineItemHelper::generateOptionsSignature($options); - - $result = $order->id ? $this->_createLineItemQuery() - ->where([ - 'orderId' => $order->id, - 'sku' => $sku, - 'optionsSignature' => $signature, - 'type' => LineItemType::Custom->value, - ]) - ->one() : null; - - if ($result) { - $lineItem = new LineItem($result); - } else { - $lineItem = $this->create($order, [ - 'sku' => $sku, - 'options' => $options, - ], LineItemType::Custom); - } - - return $lineItem; - } - - /** - * Save a line item. - * - * @param LineItem $lineItem The line item to save. - * @param bool $runValidation Whether the Line Item should be validated. - * @throws Throwable - */ - public function saveLineItem(LineItem $lineItem, bool $runValidation = true): bool - { - $isNewLineItem = !$lineItem->id; - - if ($isNewLineItem) { - $lineItemRecord = new LineItemRecord(); - } else { - $lineItemRecord = LineItemRecord::findOne($lineItem->id); - - if (!$lineItemRecord) { - throw new LineItemNotFoundException('Line with ID ”' . $lineItem->id . '“ not found!'); - } - } - - // Raise a 'beforeSaveLineItem' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_LINE_ITEM)) { - $this->trigger(self::EVENT_BEFORE_SAVE_LINE_ITEM, new LineItemEvent([ - 'lineItem' => $lineItem, - 'isNew' => $isNewLineItem, - ])); - } - - if ($runValidation && !$lineItem->validate()) { - Craft::info('Line Item not saved due to validation error(s).', __METHOD__); - return false; - } - - $lineItemRecord->type = $lineItem->type->value; - - // Set the default for type dependent properties - $lineItemRecord->hasFreeShipping = null; - $lineItemRecord->isPromotable = null; - $lineItemRecord->isShippable = null; - $lineItemRecord->isTaxable = null; - - // Save this information for all line item types, even though live lookups will happen for line items with purchasables - $lineItemRecord->hasFreeShipping = $lineItem->getHasFreeShipping(); - $lineItemRecord->isPromotable = $lineItem->getIsPromotable(); - $lineItemRecord->isShippable = $lineItem->getIsShippable(); - $lineItemRecord->isTaxable = $lineItem->getIsTaxable(); - - $lineItemRecord->purchasableId = $lineItem->purchasableId; - $lineItemRecord->orderId = $lineItem->orderId; - $lineItemRecord->taxCategoryId = $lineItem->taxCategoryId; - $lineItemRecord->shippingCategoryId = $lineItem->shippingCategoryId; - $lineItemRecord->sku = $lineItem->getSku(); - $lineItemRecord->description = $lineItem->getDescription(); - - $lineItemRecord->options = $lineItem->getOptions(); - $lineItemRecord->optionsSignature = $lineItem->getOptionsSignature(); - - $lineItemRecord->qty = $lineItem->qty; - $lineItemRecord->price = $lineItem->price; - $lineItemRecord->promotionalPrice = $lineItem->promotionalPrice; - - $lineItemRecord->weight = $lineItem->weight; - $lineItemRecord->width = $lineItem->width; - $lineItemRecord->length = $lineItem->length; - $lineItemRecord->height = $lineItem->height; - - $lineItemRecord->snapshot = $lineItem->getSnapshot(); - $lineItemRecord->note = LitEmoji::unicodeToShortcode($lineItem->note); - $lineItemRecord->privateNote = LitEmoji::unicodeToShortcode($lineItem->privateNote); - $lineItemRecord->lineItemStatusId = $lineItem->lineItemStatusId; - - $lineItemRecord->promotionalAmount = $lineItem->promotionalAmount; - $lineItemRecord->salePrice = $lineItem->salePrice; - $lineItemRecord->total = $lineItem->getTotal(); - $lineItemRecord->subtotal = $lineItem->getSubtotal(); - - if ($lineItem->uid) { - $lineItemRecord->uid = $lineItem->uid; - } - - if (!$lineItem->hasErrors()) { - $db = Craft::$app->getDb(); - $transaction = $db->beginTransaction(); - - try { - $success = $lineItemRecord->save(false); - - if ($success) { - $dateCreated = DateTimeHelper::toDateTime($lineItemRecord->dateCreated); - $dateUpdated = DateTimeHelper::toDateTime($lineItemRecord->dateUpdated); - $lineItem->dateCreated = $dateCreated; - $lineItem->dateUpdated = $dateUpdated; - $lineItem->uid = $lineItemRecord->uid; - - if ($isNewLineItem) { - $lineItem->id = $lineItemRecord->id; - } - - $transaction->commit(); - } - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - - if ($success && $this->hasEventHandlers(self::EVENT_AFTER_SAVE_LINE_ITEM)) { - $this->trigger(self::EVENT_AFTER_SAVE_LINE_ITEM, new LineItemEvent([ - 'lineItem' => $lineItem, - 'isNew' => $isNewLineItem, - ])); - } - - return $success; - } - - return false; - } - - /** - * Get a line item by its ID. - * - * @param int $id the line item ID - * @return LineItem|null Line item or null, if not found. - */ - public function getLineItemById(int $id): ?LineItem - { - $result = $this->_createLineItemQuery() - ->where(['id' => $id]) - ->one(); - - if ($result) { - // Unpack the snapshot - $result['snapshot'] = Json::decodeIfJson($result['snapshot']); - } - - return $result ? new LineItem($result) : null; - } - - /** - * Create a line item. - * - * @param Order $order The order the line item is associated with - * @param int $purchasableId The ID of the purchasable the line item represents - * @param array $options Options to set on the line item - * @param int $qty The quantity to set on the line item - * @param string $note The note on the line item - * @param string|null $uid - * @throws \Exception - * @deprecated in 5.1.0. Use [[create()]] instead. - */ - public function createLineItem(Order $order, int $purchasableId, array $options, int $qty = 1, string $note = '', string $uid = null): LineItem - { - Craft::$app->getDeprecator()->log(__METHOD__, 'LineItems::createLineItem() has been deprecated. Use LineItems::create() instead.'); - $lineItem = new LineItem(); - $lineItem->qty = $qty; - $lineItem->setOptions($options); - $lineItem->note = $note; - $lineItem->uid = $uid ?: StringHelper::UUID(); - $lineItem->setOrder($order); - - $forCustomer = $order->customerId ?? false; - $purchasable = Plugin::getInstance()->getPurchasables()->getPurchasableById($purchasableId, $order->orderSiteId, $forCustomer); - - if ($purchasable) { - $lineItem->setPurchasable($purchasable); - $lineItem->populate($purchasable); - } else { - throw new InvalidArgumentException('Invalid purchasable ID'); - } - - // Raise a 'createLineItem' event - if ($this->hasEventHandlers(self::EVENT_CREATE_LINE_ITEM)) { - $this->trigger(self::EVENT_CREATE_LINE_ITEM, new LineItemEvent([ - 'lineItem' => $lineItem, - 'isNew' => true, - ])); - } - - $lineItem->refresh(); - - return $lineItem; - } - - /** - * @param Order $order - * @param array $params - * @param LineItemType $type - * @return LineItem - * @throws Exception - * @throws SiteNotFoundException - * @throws InvalidConfigException - * @since 5.1.0 - */ - public function create(Order $order, array $params = [], LineItemType $type = LineItemType::Purchasable): LineItem - { - $params = array_merge([ - 'qty' => 1, - 'options' => [], - 'note' => '', - 'uid' => StringHelper::UUID(), - ], $params); - - $params['order'] = $order; - $params['type'] = $type; - - if ($type === LineItemType::Purchasable && empty($params['purchasableId']) && empty($params['purchasable'])) { - throw new InvalidArgumentException('Purchasable ID or Purchasable must be set'); - } - - $params['class'] = LineItem::class; - /** @var LineItem $lineItem */ - $lineItem = Craft::createObject($params); - - if ($lineItem->type === LineItemType::Purchasable) { - $purchasable = $lineItem->getPurchasable(); - - if ($purchasable) { - $lineItem->setPurchasable($purchasable); - $lineItem->populate($purchasable); - } else { - throw new InvalidArgumentException('Invalid purchasable ID'); - } - } else { - $lineItem->populate(); - } - - // Raise a 'createLineItem' event - if ($this->hasEventHandlers(self::EVENT_CREATE_LINE_ITEM)) { - $this->trigger(self::EVENT_CREATE_LINE_ITEM, new LineItemEvent([ - 'lineItem' => $lineItem, - 'isNew' => true, - ])); - } - - $lineItem->refresh(); - - return $lineItem; - } - - /** - * Deletes all line items associated with an order, per the order's ID. - * - * @param int $orderId the order's ID - * @return bool whether any line items were deleted - */ - public function deleteAllLineItemsByOrderId(int $orderId): bool - { - return (bool)LineItemRecord::deleteAll(['orderId' => $orderId]); - } - - /** - * @param array|Order[] $orders - * @return Order[] - * @since 3.2.0 - */ - public function eagerLoadLineItemsForOrders(array $orders): array - { - $orderIds = ArrayHelper::getColumn($orders, 'id'); - $lineItemsResults = $this->_createLineItemQuery()->andWhere(['orderId' => $orderIds])->all(); - - $lineItems = []; - - foreach ($lineItemsResults as $result) { - $result['snapshot'] = Json::decodeIfJson($result['snapshot']); - $lineItem = new LineItem($result); - $lineItems[$lineItem->orderId] ??= []; - $lineItems[$lineItem->orderId][] = $lineItem; - } - - foreach ($orders as $key => $order) { - if (isset($lineItems[$order->id])) { - $order->setLineItems($lineItems[$order->id]); - $orders[$key] = $order; - } - } - - return $orders; - } - - /** - * - * @throws Throwable - * @since 3.2.5 - */ - public function orderCompleteHandler(LineItem $lineItem, Order $order): void - { - // Called the after order complete method for the purchasable if there is one - if ($lineItem->type === LineItemType::Purchasable && $lineItem->getPurchasable()) { - $lineItem->getPurchasable()->afterOrderComplete($order, $lineItem); - } - - // Retrieve the default status for the current line item. This is a chance for - // developers to hook into an event for finer control - $defaultStatus = Plugin::getInstance()->getLineItemStatuses()->getDefaultLineItemStatusForLineItem($lineItem); - if (!$defaultStatus) { - return; - } - - // Set the status ID and save the line item - $lineItem->setLineItemStatus($defaultStatus); - $this->saveLineItem($lineItem, false); - } - - /** - * Returns a Query object prepped for retrieving line items. - * - * @return Query The query object. - */ - private function _createLineItemQuery(): Query - { - return (new Query()) - ->select([ - 'dateCreated', - 'dateUpdated', - 'description', - 'hasFreeShipping', - 'height', - 'id', - 'isPromotable', - 'isShippable', - 'isTaxable', - 'length', - 'lineItemStatusId', - 'note', - 'options', - 'orderId', - 'price', - 'promotionalPrice', - 'privateNote', - 'purchasableId', - 'qty', - 'shippingCategoryId', - 'sku', - 'snapshot', - 'taxCategoryId', - 'type', - 'uid', - 'weight', - 'width', - ]) - ->from([Table::LINEITEMS . ' lineItems']) - ->orderBy('dateCreated DESC'); - } -} diff --git a/src/services/OrderAdjustments.php b/src/services/OrderAdjustments.php deleted file mode 100644 index c412a44073..0000000000 --- a/src/services/OrderAdjustments.php +++ /dev/null @@ -1,290 +0,0 @@ - - * @since 2.0 - */ -class OrderAdjustments extends Component -{ - /** - * @event RegisterComponentTypesEvent The event that is triggered for registration of additional adjusters. - * - * ```php - * use craft\events\RegisterComponentTypesEvent; - * use craft\commerce\services\OrderAdjustments; - * use yii\base\Event; - * - * Event::on( - * OrderAdjustments::class, - * OrderAdjustments::EVENT_REGISTER_ORDER_ADJUSTERS, - * function(RegisterComponentTypesEvent $event) { - * $event->types[] = MyAdjuster::class; - * } - * ); - * ``` - */ - public const EVENT_REGISTER_ORDER_ADJUSTERS = 'registerOrderAdjusters'; - - /** - * @event RegisterComponentTypesEvent The event that is triggered for registration of additional adjusters. - * @since 3.1.9 - * - * ```php - * use craft\events\RegisterComponentTypesEvent; - * use craft\commerce\services\OrderAdjustments; - * use yii\base\Event; - * - * Event::on( - * OrderAdjustments::class, - * OrderAdjustments::EVENT_REGISTER_DISCOUNT_ADJUSTERS, - * function(RegisterComponentTypesEvent $event) { - * $event->types[] = MyDiscountAdjuster::class; - * } - * ); - * ``` - */ - public const EVENT_REGISTER_DISCOUNT_ADJUSTERS = 'registerDiscountAdjusters'; - - - /** - * Get all order adjusters. - * - * @return class-string[] - * @throws InvalidConfigException - */ - public function getAdjusters(): array - { - $adjusters = []; - - $adjusters[] = Shipping::class; - - foreach ($this->getDiscountAdjusters() as $discountAdjuster) { - $adjusters[] = $discountAdjuster; - } - - $taxEngine = Plugin::getInstance()->getTaxes()->getEngine(); - $adjusters[] = $taxEngine->taxAdjusterClass(); - - - $event = new RegisterComponentTypesEvent([ - 'types' => $adjusters, - ]); - - if ($this->hasEventHandlers(self::EVENT_REGISTER_ORDER_ADJUSTERS)) { - $this->trigger(self::EVENT_REGISTER_ORDER_ADJUSTERS, $event); - } - - return $event->types; - } - - public function getOrderAdjustmentById(int $id): ?OrderAdjustment - { - $row = $this->_createOrderAdjustmentQuery() - ->where(['id' => $id]) - ->one(); - - if (!$row) { - return null; - } - - $row['sourceSnapshot'] = Json::decodeIfJson($row['sourceSnapshot']); - return new OrderAdjustment($row); - } - - /** - * Get all order adjustments by order's ID. - * - * @return OrderAdjustment[] - */ - public function getAllOrderAdjustmentsByOrderId(int $orderId): array - { - $rows = $this->_createOrderAdjustmentQuery() - ->where(['orderId' => $orderId]) - ->all(); - - $adjustments = []; - - foreach ($rows as $row) { - $row['sourceSnapshot'] = Json::decodeIfJson($row['sourceSnapshot']); - $adjustments[] = new OrderAdjustment($row); - } - - return $adjustments; - } - - /** - * Save an order adjustment. - * - * @param bool $runValidation Whether the Order Adjustment should be validated - * @throws Exception - */ - public function saveOrderAdjustment(OrderAdjustment $orderAdjustment, bool $runValidation = true): bool - { - $newAdjustment = !$orderAdjustment->id; - - if ($newAdjustment) { - $record = new OrderAdjustmentRecord(); - } else { - $record = OrderAdjustmentRecord::findOne($orderAdjustment->id); - - if (!$record) { - throw new OrderAdjustmentNotFoundException('Order Adjustment with ID ”' . $orderAdjustment->id . '“ not found!'); - } - } - - if ($runValidation && !$orderAdjustment->validate()) { - Craft::info('Order Adjustment not saved due to validation error(s).', __METHOD__); - return false; - } - - $record->name = $orderAdjustment->name; - $record->type = $orderAdjustment->type; - $record->description = $orderAdjustment->description; - $record->amount = $orderAdjustment->amount; - $record->included = $orderAdjustment->included; - $record->sourceSnapshot = $orderAdjustment->getSourceSnapshot(); - $record->lineItemId = $orderAdjustment->getLineItem()->id ?? null; - $record->orderId = $orderAdjustment->getOrder()->id ?? null; - $record->isEstimated = $orderAdjustment->isEstimated; - - $record->save(false); - - // Update the model with the latest IDs - $orderAdjustment->id = $record->id; - $orderAdjustment->orderId = $record->orderId; - $orderAdjustment->lineItemId = $record->lineItemId; - - return true; - } - - - /** - * Delete all adjustments belonging to an order by its ID. - * - * @noinspection PhpUnused - */ - public function deleteAllOrderAdjustmentsByOrderId(int $orderId): bool - { - return (bool)OrderAdjustmentRecord::deleteAll(['orderId' => $orderId]); - } - - /** - * Delete an order adjustment by its ID. - * - * @throws Throwable - * @throws StaleObjectException - * @noinspection PhpUnused - */ - public function deleteOrderAdjustmentByAdjustmentId(int $adjustmentId): bool - { - $orderAdjustment = OrderAdjustmentRecord::findOne($adjustmentId); - - if (!$orderAdjustment) { - return false; - } - - return $orderAdjustment->delete(); - } - - /** - * @param array|Order[] $orders - * @return Order[] - * @since 3.2.0 - */ - public function eagerLoadOrderAdjustmentsForOrders(array $orders): array - { - $orderIds = ArrayHelper::getColumn($orders, 'id'); - $orderAdjustmentResults = $this->_createOrderAdjustmentQuery()->andWhere(['orderId' => $orderIds])->all(); - - $orderAdjustments = []; - - foreach ($orderAdjustmentResults as $result) { - $result['sourceSnapshot'] = Json::decodeIfJson($result['sourceSnapshot']); - $adjustment = new OrderAdjustment($result); - - $orderAdjustments[$adjustment->orderId] ??= []; - $orderAdjustments[$adjustment->orderId][] = $adjustment; - } - - foreach ($orders as $key => $order) { - if (isset($orderAdjustments[$order->id])) { - $order->setAdjustments($orderAdjustments[$order->id]); - $orders[$key] = $order; - } - } - - return $orders; - } - - /** - * Returns a Query object prepped for retrieving Order Adjustment. - * - * @return Query The query object. - */ - private function _createOrderAdjustmentQuery(): Query - { - return (new Query()) - ->select([ - 'amount', - 'description', - 'id', - 'included', - 'isEstimated', - 'lineItemId', - 'name', - 'orderId', - 'sourceSnapshot', - 'type', - ]) - ->from([Table::ORDERADJUSTMENTS]); - } - - /** - * @return class-string[] - */ - public function getDiscountAdjusters(): array - { - $discountEvent = new RegisterComponentTypesEvent([ - 'types' => [], - ]); - - if ($this->hasEventHandlers(self::EVENT_REGISTER_DISCOUNT_ADJUSTERS)) { - $this->trigger(self::EVENT_REGISTER_DISCOUNT_ADJUSTERS, $discountEvent); - } - - $discountEvent->types[] = Discount::class; - - return $discountEvent->types; - } -} diff --git a/src/services/OrderHistories.php b/src/services/OrderHistories.php deleted file mode 100644 index 5f336bd942..0000000000 --- a/src/services/OrderHistories.php +++ /dev/null @@ -1,232 +0,0 @@ - - * @since 2.0 - */ -class OrderHistories extends Component -{ - /** - * @event OrderStatusEvent The event that is triggered when an order status is changed. - * - * Plugins can get notified when an order status is changed - * - * ```php - * use craft\commerce\events\OrderStatusEvent; - * use craft\commerce\services\OrderHistories; - * use craft\commerce\models\OrderHistory; - * use craft\commerce\elements\Order; - * use yii\base\Event; - * - * Event::on( - * OrderHistories::class, - * OrderHistories::EVENT_ORDER_STATUS_CHANGE, - * function(OrderStatusEvent $event) { - * // @var OrderHistory $orderHistory - * $orderHistory = $event->orderHistory; - * // @var Order $order - * $order = $event->order; - * - * // Let the delivery department know the order’s ready to be delivered - * // ... - * } - * ); - * ``` - */ - public const EVENT_ORDER_STATUS_CHANGE = 'orderStatusChange'; - - /** - * Get order history by its ID. - */ - public function getOrderHistoryById(int $id): ?OrderHistory - { - $result = $this->_createOrderHistoryQuery() - ->where(['id' => $id]) - ->one(); - - return $result ? new OrderHistory($result) : null; - } - - /** - * Get all order histories by an order ID. - * - * @param int $id orderId - * @return OrderHistory[] - */ - public function getAllOrderHistoriesByOrderId(int $id): array - { - $rows = $this->_createOrderHistoryQuery() - ->where(['orderId' => $id]) - ->orderBy('dateCreated desc, id desc') - ->all(); - - $histories = []; - - foreach ($rows as $row) { - $histories[] = new OrderHistory($row); - } - - return $histories; - } - - /** - * Create an order history from an order. - * - * @throws Exception - * @throws InvalidConfigException - * @throws MissingComponentException - */ - public function createOrderHistoryFromOrder(Order $order, ?int $oldStatusId): bool - { - $orderHistoryModel = new OrderHistory(); - $orderHistoryModel->orderId = $order->id; - $orderHistoryModel->prevStatusId = $oldStatusId; - $orderHistoryModel->newStatusId = $order->orderStatusId; - - // By default the user who changed the status is the same as the user who placed the order - $userId = $order->getCustomerId(); - - // If the user is logged in, use the current user - if (!Craft::$app->request->isConsoleRequest - && !Craft::$app->getResponse()->isSent - && (Craft::$app->getSession()->getHasSessionId() || Craft::$app->getSession()->getIsActive()) - && $currentUser = Craft::$app->getUser()->getIdentity() - ) { - $userId = $currentUser->id; - } - - if ($userId) { - $user = Craft::$app->getUsers()->getUserById($userId); - if ($user) { - $orderHistoryModel->userId = $userId; - $orderHistoryModel->userName = $user->fullName ?? $user->email; - } else { - $orderHistoryModel->userName = $order->getEmail(); - } - } - - $orderHistoryModel->message = $order->message; - - if (!$this->saveOrderHistory($orderHistoryModel)) { - return false; - } - - Plugin::getInstance()->getOrderStatuses()->statusChangeHandler($order, $orderHistoryModel); - - // Raising 'orderStatusChange' event - if ($this->hasEventHandlers(self::EVENT_ORDER_STATUS_CHANGE)) { - $this->trigger(self::EVENT_ORDER_STATUS_CHANGE, new OrderStatusEvent([ - 'orderHistory' => $orderHistoryModel, - 'order' => $order, - ])); - } - - return true; - } - - /** - * Save an order history. - * - * @param bool $runValidation Whether the Order Adjustment should be validated - * @throws Exception - */ - public function saveOrderHistory(OrderHistory $model, bool $runValidation = true): bool - { - if ($model->id) { - $record = OrderHistoryRecord::findOne($model->id); - - if (!$record) { - throw new Exception(Craft::t('commerce', 'No order history exists with the ID “{id}”', - ['id' => $model->id])); - } - } else { - $record = new OrderHistoryRecord(); - } - - if ($runValidation && !$model->validate()) { - Craft::info('Order history not saved due to validation error.', __METHOD__); - - return false; - } - - $record->message = $model->message; - $record->newStatusId = $model->newStatusId; - $record->prevStatusId = $model->prevStatusId; - $record->userId = $model->userId; - $record->userName = $model->userName; - $record->orderId = $model->orderId; - - // Save it! - $record->save(false); - - // Now that we have a record ID, save it on the model - $model->id = $record->id; - $model->dateCreated = DateTimeHelper::toDateTime($record->dateCreated); - - return true; - } - - /** - * Delete an order history by its ID. - * - * @throws Throwable - * @throws StaleObjectException - * @noinspection PhpUnused - */ - public function deleteOrderHistoryById(int $id): bool - { - $orderHistory = OrderHistoryRecord::findOne($id); - - if ($orderHistory) { - return (bool)$orderHistory->delete(); - } - - return false; - } - - - /** - * Returns a Query object prepped for retrieving Order History. - * - * @return Query The query object. - */ - private function _createOrderHistoryQuery(): Query - { - return (new Query()) - ->select([ - 'userId', - 'dateCreated', - 'id', - 'message', - 'newStatusId', - 'orderId', - 'prevStatusId', - ]) - ->from([Table::ORDERHISTORIES]); - } -} diff --git a/src/services/OrderNotices.php b/src/services/OrderNotices.php deleted file mode 100644 index 66c7bb276a..0000000000 --- a/src/services/OrderNotices.php +++ /dev/null @@ -1,82 +0,0 @@ - - * @since 3.3 - */ -class OrderNotices extends Component -{ - /** - * @param array|Order[] $orders - * @return Order[] - * @throws InvalidConfigException - * @since 3.3 - */ - public function eagerLoadOrderNoticesForOrders(array $orders): array - { - $orderIds = ArrayHelper::getColumn($orders, 'id'); - $orderNoticesResults = $this->_createOrderNoticeQuery()->andWhere(['orderId' => $orderIds])->all(); - $orderNotices = []; - - foreach ($orderNoticesResults as $result) { - - /** @var OrderNotice $notice */ - $notice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => $result, - ]); - - $orderNotices[$notice->orderId] ??= []; - $orderNotices[$notice->orderId][] = $notice; - } - - foreach ($orders as $key => $order) { - /** @var Order $order */ - if (isset($orderNotices[$order->id])) { - $order->addNotices($orderNotices[$order->id]); - $orders[$key] = $order; - } - } - - return $orders; - } - - /** - * Returns a Query object prepped for retrieving Order Adjustment. - * - * @return Query The query object. - */ - private function _createOrderNoticeQuery(): Query - { - return (new Query()) - ->select([ - 'attribute', - 'noticeType', - 'id', - 'message', - 'orderId', - 'type', - ]) - ->from([Table::ORDERNOTICES]); - } -} diff --git a/src/services/OrderStatuses.php b/src/services/OrderStatuses.php deleted file mode 100644 index 05aa964aa3..0000000000 --- a/src/services/OrderStatuses.php +++ /dev/null @@ -1,573 +0,0 @@ - - * @since 2.0 - */ -class OrderStatuses extends Component -{ - /** - * @event DefaultOrderStatusEvent The event that is triggered when a default order status is being fetched. - * - * Set the event object’s `orderStatus` property to override the default status set in the control panel. - * - * ```php - * use craft\commerce\events\DefaultOrderStatusEvent; - * use craft\commerce\services\OrderStatuses; - * use craft\commerce\models\OrderStatus; - * use craft\commerce\elements\Order; - * use yii\base\Event; - * - * Event::on( - * OrderStatuses::class, - * OrderStatuses::EVENT_DEFAULT_ORDER_STATUS, - * function(DefaultOrderStatusEvent $event) { - * // @var OrderStatus $status - * $status = $event->orderStatus; - * // @var Order $order - * $order = $event->order; - * - * // Choose a more appropriate order status than the control panel default - * // ... - * } - * ); - * ``` - */ - public const EVENT_DEFAULT_ORDER_STATUS = 'defaultOrderStatus'; - - /** - * @event OrderStatusEmailsEvent The email event that is triggered when an order status is changed. - * - * Plugins can get notified when an order status is changed - * - * ```php - * use craft\commerce\events\OrderStatusEmailsEvent; - * use craft\commerce\services\OrderStatuses; - * use craft\commerce\models\OrderHistory; - * use craft\commerce\elements\Order; - * use yii\base\Event; - * - * Event::on( - * OrderStatuses::class, - * OrderStatuses::EVENT_ORDER_STATUS_CHANGE_EMAILS, - * function(OrderStatusEmailsEvent $event) { - * // @var OrderHistory $orderHistory - * $orderHistory = $event->orderHistory; - * // @var Order $order - * $order = $event->order; - * - * // Let the delivery department know the order’s ready to be delivered - * // ... - * } - * ); - * ``` - */ - public const EVENT_ORDER_STATUS_CHANGE_EMAILS = 'orderStatusChangeEmails'; - - public const CONFIG_STATUSES_KEY = 'commerce.orderStatuses'; - - /** - * @var Collection[]|null - */ - private ?array $_allOrderStatuses = null; - - /** - * Returns all Order Statuses - * - * @param int|null $storeId - * @param bool $withTrashed - * @return Collection - * @throws InvalidConfigException - * @throws SiteNotFoundException - * @since 2.2 - */ - public function getAllOrderStatuses(?int $storeId = null, bool $withTrashed = false): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if ($this->_allOrderStatuses === null || !isset($this->_allOrderStatuses[$storeId])) { - $results = $this->_createOrderStatusesQuery(true) - ->where(['storeId' => $storeId]) - ->all(); - - if ($this->_allOrderStatuses === null) { - $this->_allOrderStatuses = []; - } - - foreach ($results as $result) { - $orderStatus = Craft::createObject([ - 'class' => OrderStatus::class, - 'attributes' => $result, - ]); - - if (!isset($this->_allOrderStatuses[$orderStatus->storeId])) { - $this->_allOrderStatuses[$orderStatus->storeId] = collect(); - } - - $this->_allOrderStatuses[$orderStatus->storeId]->push($orderStatus); - } - } - - if (!isset($this->_allOrderStatuses[$storeId])) { - return collect(); - } - - return $this->_allOrderStatuses[$storeId]->filter(fn(OrderStatus $os) => (!$withTrashed && $os->dateDeleted === null) || $withTrashed); - } - - /** - * Get an order status by ID - */ - public function getOrderStatusById(int $id, ?int $storeId = null): ?OrderStatus - { - return $this->getAllOrderStatuses($storeId)->firstWhere('id', $id); - } - - /** - * Get an order status by ID - */ - public function getOrderStatusByUid(string $uid, ?int $storeId = null): ?OrderStatus - { - return $this->getAllOrderStatuses($storeId)->firstWhere('uid', $uid); - } - - /** - * Get order status by its handle. - */ - public function getOrderStatusByHandle(string $handle, ?int $storeId = null): ?OrderStatus - { - return $this->getAllOrderStatuses($storeId)->firstWhere('handle', $handle); - } - - /** - * Get default order status from the DB - */ - public function getDefaultOrderStatus(?int $storeId = null): ?OrderStatus - { - return $this->getAllOrderStatuses($storeId)->firstWhere('default', true); - } - - /** - * Get default order status ID from the DB - * - * @noinspection PhpUnused - */ - public function getDefaultOrderStatusId(?int $storeId = null): ?int - { - return $this->getDefaultOrderStatus($storeId)?->id; - } - - /** - * Get the default order status for a particular order. Defaults to the control-panel-configured default order status. - */ - public function getDefaultOrderStatusForOrder(Order $order): ?OrderStatus - { - $orderStatus = $this->getDefaultOrderStatus($order->storeId); - - $event = new DefaultOrderStatusEvent([ - 'orderStatus' => $orderStatus, - 'order' => $order, - ]); - - if ($this->hasEventHandlers(self::EVENT_DEFAULT_ORDER_STATUS)) { - $this->trigger(self::EVENT_DEFAULT_ORDER_STATUS, $event); - } - - return $event->orderStatus; - } - - /** - * @since 3.0.11 - */ - public function getOrderCountByStatus(?int $storeId = null): array - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - $countGroupedByStatusId = (new Query()) - ->select(['[[o.orderStatusId]]', 'count(o.id) as orderCount']) - ->where([ - '[[o.isCompleted]]' => true, - '[[e.dateDeleted]]' => null, - '[[o.storeId]]' => $storeId, - ]) - ->from([Table::ORDERS . ' o']) - ->innerJoin([CraftTable::ELEMENTS . ' e'], '[[o.id]] = [[e.id]]') - ->groupBy(['[[o.orderStatusId]]']) - ->indexBy('orderStatusId') - ->all(); - - // For those not in the groupBy - $allStatuses = $this->getAllOrderStatuses($storeId); - foreach ($allStatuses as $status) { - if (!isset($countGroupedByStatusId[$status->id])) { - $countGroupedByStatusId[$status->id] = [ - 'orderStatusId' => $status->id, - 'handle' => $status->handle, - 'orderCount' => 0, - ]; - } - - // Make sure all have their handle - $countGroupedByStatusId[$status->id]['handle'] = $status->handle; - } - - return $countGroupedByStatusId; - } - - /** - * Save the order status. - * - * @param bool $runValidation should we validate this order status before saving. - * @throws Exception - */ - public function saveOrderStatus(OrderStatus $orderStatus, array $emailIds = [], bool $runValidation = true, $force = false): bool - { - $isNewStatus = !(bool)$orderStatus->id; - - if ($runValidation && !$orderStatus->validate()) { - Craft::info('Order status not saved due to validation error.', __METHOD__); - - return false; - } - - if ($isNewStatus) { - $statusUid = StringHelper::UUID(); - } else { - $statusUid = Db::uidById(Table::ORDERSTATUSES, $orderStatus->id); - } - - $otherStatuses = $this->getAllOrderStatuses($orderStatus->storeId)->where('uid', '!=', $statusUid)->all(); - - // if this is the only order status, set it as the default - $orderStatus->default = empty($otherStatuses) ? true : $orderStatus->default; - - $projectConfig = Craft::$app->getProjectConfig(); - - if ($orderStatus->dateDeleted) { - $configData = null; - } else { - $configData = $orderStatus->getConfig($emailIds); - } - - $configPath = self::CONFIG_STATUSES_KEY . '.' . $statusUid; - $projectConfig->set($configPath, $configData, force: $force); - - if ($isNewStatus) { - $orderStatus->id = Db::idByUid(Table::ORDERSTATUSES, $statusUid); - $orderStatus->uid = $statusUid; - } - - $this->_allOrderStatuses = null; - - // Make sure this is the only default - if ($orderStatus->default) { - foreach ($otherStatuses as $otherStatus) { - $otherStatus->default = false; - $this->saveOrderStatus($otherStatus, $otherStatus->getEmailIds(), false, true); - } - } - - return true; - } - - /** - * Handle order status change. - * - * @return void - * @throws Throwable if reasons - */ - public function handleChangedOrderStatus(ConfigEvent $event) - { - ProjectConfigData::ensureAllStoresProcessed(); - - $statusUid = $event->tokenMatches[0]; - $data = $event->newValue; - - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - $statusRecord = $this->_getOrderStatusRecord($statusUid); - - // Get store by uid and convert `$data['store']` to `storeId` - $store = Plugin::getInstance()->getStores()->getStoreByUid($data['store']); - - $statusRecord->name = $data['name']; - $statusRecord->storeId = $store->id; - $statusRecord->handle = $data['handle']; - $statusRecord->color = $data['color']; - $statusRecord->description = $data['description'] ?? null; - $statusRecord->sortOrder = $data['sortOrder'] ?? 99; - $statusRecord->default = $data['default']; - $statusRecord->uid = $statusUid; - - // Save the status - if ($wasTrashed = (bool)$statusRecord->dateDeleted) { - $statusRecord->restore(); - } else { - $statusRecord->save(false); - } - - $connection = Craft::$app->getDb(); - // Drop them all and we will recreate the new ones. - $connection->createCommand()->delete(Table::ORDERSTATUS_EMAILS, ['orderStatusId' => $statusRecord->id])->execute(); - - if (!empty($data['emails'])) { - foreach ($data['emails'] as $emailUid) { - Craft::$app->projectConfig->processConfigChanges(Emails::CONFIG_EMAILS_KEY . '.' . $emailUid); - } - - $emailIds = Db::idsByUids(Table::EMAILS, $data['emails']); - - foreach ($emailIds as $emailId) { - $connection->createCommand() - ->insert(Table::ORDERSTATUS_EMAILS, [ - 'orderStatusId' => $statusRecord->id, - 'emailId' => $emailId, - ]) - ->execute(); - } - } - - $transaction->commit(); - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - } - - /** - * Delete an order status by it's id. - * - * @throws Throwable - */ - public function deleteOrderStatusById(int $id, ?int $storeId = null): bool - { - $statuses = $this->getAllOrderStatuses($storeId); - $orderStatus = $this->getOrderStatusById($id, $storeId); - - // Can only delete if we have one that can remain as the default - if (count($statuses) < 2 || $orderStatus == null) { - return false; - } - - // Prevent deletion of order status if there are orders with this status - $orderCounts = $this->getOrderCountByStatus($storeId); - if (!isset($orderCounts[$id]) || $orderCounts[$id]['orderCount'] > 0) { - return false; - } - - Craft::$app->getProjectConfig()->remove(self::CONFIG_STATUSES_KEY . '.' . $orderStatus->uid); - return true; - } - - - /** - * Handle order status being deleted - * - * @return void - * @throws Throwable if reasons - */ - public function handleDeletedOrderStatus(ConfigEvent $event) - { - $orderStatusUid = $event->tokenMatches[0]; - - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - $orderStatusRecord = $this->_getOrderStatusRecord($orderStatusUid); - - // Save the volume - $orderStatusRecord->softDelete(); - - $transaction->commit(); - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - - // Clear caches - $this->_allOrderStatuses = null; - } - - /** - * Prune a deleted email from order statuses. - */ - public function pruneDeletedEmail(EmailEvent $event) - { - $emailUid = $event->email->uid; - - $projectConfig = Craft::$app->getProjectConfig(); - $statuses = $projectConfig->get(self::CONFIG_STATUSES_KEY); - - // Loop through the volumes and prune the UID from field layouts. - if (is_array($statuses)) { - foreach ($statuses as $orderStatusUid => $orderStatus) { - $projectConfig->remove(self::CONFIG_STATUSES_KEY . '.' . $orderStatusUid . '.emails.' . $emailUid); - } - } - } - - /** - * Handler for order status change event - * - * @param Order $order - * @param OrderHistory $orderHistory - * @throws InvalidConfigException - */ - public function statusChangeHandler(Order $order, OrderHistory $orderHistory): void - { - $status = $this->getOrderStatusById($order->orderStatusId, $order->storeId); - - if ($status === null) { - return; - } - - // Raising 'beforeOrderStatusChange' event - $event = new OrderStatusEmailsEvent([ - 'orderHistory' => $orderHistory, - 'order' => $order, - 'emails' => $status->getEmails(), - 'isValid' => !$order->suppressEmails, - ]); - - if ($this->hasEventHandlers(self::EVENT_ORDER_STATUS_CHANGE_EMAILS)) { - $this->trigger(self::EVENT_ORDER_STATUS_CHANGE_EMAILS, $event); - } - - if (!$event->isValid || empty($event->emails)) { - // Don't send emails - return; - } - - $originalLanguage = Craft::$app->language; - $originalFormattingLocale = Craft::$app->formattingLocale; - - foreach ($event->emails as $email) { - if (!$email->enabled) { - continue; - } - - // Set language by email's set locale - // We need to do this here since $order->toArray() uses the locale to format asCurrency attributes - $language = $email->getRenderLanguage($event->order); - Locale::switchAppLanguage($language); - - Queue::push(new SendEmail([ - 'orderId' => $event->order->id, - 'commerceEmailId' => $email->id, - 'orderHistoryId' => $event->orderHistory->id, - 'orderData' => $event->order->toArray(), - ]), 100); - } - - // Set previous language back - Locale::switchAppLanguage($originalLanguage, $originalFormattingLocale->id); - } - - /** - * Reorders the order statuses. - * - * @throws Exception - * @throws ErrorException - * @throws NotSupportedException - * @throws ServerErrorHttpException - */ - public function reorderOrderStatuses(array $ids): bool - { - $projectConfig = Craft::$app->getProjectConfig(); - - $uidsByIds = Db::uidsByIds(Table::ORDERSTATUSES, $ids); - - foreach ($ids as $orderStatus => $statusId) { - if (!empty($uidsByIds[$statusId])) { - $statusUid = $uidsByIds[$statusId]; - $projectConfig->set(self::CONFIG_STATUSES_KEY . '.' . $statusUid . '.sortOrder', $orderStatus + 1); - } - } - - return true; - } - - - /** - * Returns a Query object prepped for retrieving order statuses - * - * @param bool $withTrashed - * @return Query - */ - private function _createOrderStatusesQuery(bool $withTrashed = false): Query - { - $query = (new Query()) - ->select([ - 'color', - 'dateDeleted', - 'default', - 'description', - 'handle', - 'id', - 'name', - 'sortOrder', - 'storeId', - 'uid', - ]) - ->orderBy('sortOrder') - ->from([Table::ORDERSTATUSES]); - - if (!$withTrashed) { - $query->where(['dateDeleted' => null]); - } - - return $query; - } - - /** - * Gets an order status' record by uid. - */ - private function _getOrderStatusRecord(string $uid): OrderStatusRecord - { - /** @var ?OrderStatusRecord $orderStatus */ - $orderStatus = OrderStatusRecord::findWithTrashed()->where(['uid' => $uid])->one(); - return $orderStatus ?: new OrderStatusRecord(); - } -} diff --git a/src/services/Orders.php b/src/services/Orders.php deleted file mode 100644 index 543d1bd2ee..0000000000 --- a/src/services/Orders.php +++ /dev/null @@ -1,322 +0,0 @@ - - * @since 2.0 - */ -class Orders extends Component -{ - public const CONFIG_FIELDLAYOUT_KEY = 'commerce.orders.fieldLayouts'; - - /** - * Handle field layout change - * - * @throws Exception - */ - public function handleChangedFieldLayout(ConfigEvent $event): void - { - $data = $event->newValue; - - ProjectConfigHelper::ensureAllFieldsProcessed(); - $fieldsService = Craft::$app->getFields(); - - if (empty($data) || empty(reset($data))) { - // Delete the field layout - $fieldsService->deleteLayoutsByType(Order::class); - return; - } - - // Save the field layout - $layout = FieldLayout::createFromConfig(reset($data)); - $layout->id = $fieldsService->getLayoutByType(Order::class)->id; - $layout->type = Order::class; - $layout->uid = key($data); - $fieldsService->saveLayout($layout, false); - } - - /** - * Handle field layout being deleted - */ - public function handleDeletedFieldLayout(): void - { - Craft::$app->getFields()->deleteLayoutsByType(Order::class); - } - - /** - * Get an order by its ID. - * - * @param int $id - * @return ?Order - */ - public function getOrderById(int $id): ?Order - { - if (!$id) { - return null; - } - - return Order::find()->id($id)->status(null)->one(); - } - - /** - * Get an order by its number. - */ - public function getOrderByNumber(string $number): ?Order - { - return Order::find()->number($number)->one(); - } - - /** - * Get all orders by their customer. - * - * @param int|User $customer - * @return Order[]|null - */ - public function getOrdersByCustomer(User|int $customer): ?array - { - if (!$customer) { - return null; - } - - $query = Order::find(); - if ($customer instanceof User) { - $query->customer($customer); - } else { - $query->customerId($customer); - } - $query->isCompleted(); - $query->limit(null); - - return $query->all(); - } - - /** - * Get all orders by their email. - * - * @return Order[]|null - */ - public function getOrdersByEmail(string $email): ?array - { - return Order::find()->email($email)->isCompleted()->limit(null)->all(); - } - - /** - * @param array|Order[] $orders - * @return Order[] - * @since 4.0.0 - */ - public function eagerLoadAddressesForOrders(array $orders): array - { - $shippingAddressIds = array_filter(ArrayHelper::getColumn($orders, 'shippingAddressId')); - $billingAddressIds = array_filter(ArrayHelper::getColumn($orders, 'billingAddressId')); - $ids = array_unique(array_merge($shippingAddressIds, $billingAddressIds)); - - // Query addresses as array to avoid instantiating elements immediately - $query = Address::find() - ->id($ids) - ->indexBy('id') - ->asArray(); - /** @var array $addresses */ - $addresses = $query->all(); - - foreach ($orders as $key => $order) { - if (isset($order['shippingAddressId'], $addresses[$order['shippingAddressId']])) { - $data = $addresses[$order['shippingAddressId']]; - $data['owner'] = $order; - /** @var Address $address */ - $address = $query->createElement($data); - - $order->setShippingAddress($address); - } - - if (isset($order['billingAddressId'], $addresses[$order['billingAddressId']])) { - $data = $addresses[$order['billingAddressId']]; - $data['owner'] = $order; - - /** @var Address $address */ - $address = $query->createElement($data); - - $order->setBillingAddress($address); - } - - $orders[$key] = $order; - } - - return $orders; - } - - /** - * Prevent deleting a user if they have any orders. - * - * @param DefineElementDeletionBlockersEvent $event the event. - */ - public function beforeDeleteUserHandler(DefineElementDeletionBlockersEvent $event): void - { - $event->blockers[] = new OrderCustomersDeletionBlocker($event->elements, $event->hardDelete); - } - - /** - * Reassigns orders to a new customer. - * - * @param int|int[] $oldUserId - * @param int $newUserId - * @return int The number of affected orders - * @throws \yii\db\Exception - * @since 5.7.0 - */ - public function reassignOrders(int|array $oldUserId, int $newUserId): int - { - $newUserEmail = (new Query()) - ->select(['email']) - ->from(\craft\db\Table::USERS) - ->where(['id' => $newUserId]) - ->scalar(); - - if (!$newUserEmail) { - throw new InvalidArgumentException('Unable to reassign user id: ' . $newUserId); - } - - $count = Db::update(Table::ORDERS, [ - 'customerId' => $newUserId, - 'email' => $newUserEmail, - ], [ - 'customerId' => $oldUserId, - ], [], false); - - // Invalidate all order caches - Craft::$app->getElements()->invalidateCachesForElementType(Order::class); - - return $count; - } - - /** - * @param int|int[] $orderIds - * @param array $dataToRemove - * @return int - * @throws \yii\db\Exception - * @since 5.7.0 - */ - public function removeCustomerData(int|array $orderIds, array $dataToRemove = ['customerId', 'email']): int - { - $allowedRemovalKeys = [ - 'customerId', - 'email', - 'billingAddressId', - 'shippingAddressId', - 'orderCompletedEmail', - ]; - - $data = []; - foreach ($dataToRemove as $key) { - if (!in_array($key, $allowedRemovalKeys)) { - continue; - } - - // Make sure we are setting the `customerDeleted` flag when removing the `customerId` - if ($key === 'customerId') { - $data['customerDeleted'] = true; - } - - $data[$key] = null; - } - - $count = Db::update(Table::ORDERS, $data, [ - 'id' => $orderIds, - ], [], false); - - Craft::$app->getElements()->invalidateCachesForElementType(Order::class); - - return $count; - } - - /** - * @param ModelEvent $event - * @return void - * @throws Exception - * @throws \Throwable - * @throws ElementNotFoundException - * @throws InvalidElementException - * @throws UnsupportedSiteException - * @since 4.2.11 - */ - public function afterSaveAddressHandler(ModelEvent $event): void - { - - /** @var Address $address */ - $address = $event->sender; - if ($address->getIsDraft()) { - return; - } - - // Find all orders using this address as a source - $idQuery = (new Query()) - ->select(['id']) - ->from(Table::ORDERS) - ->where(['sourceBillingAddressId' => $address->id]) - ->orWhere(['sourceShippingAddressId' => $address->id]); - - /** @var Order[] $carts */ - $carts = Order::find() - ->where(['commerce_orders.id' => $idQuery]) - ->isCompleted(false) - ->all(); - - if (empty($carts)) { - return; - } - - foreach ($carts as $cart) { - // Update the billing address - if ($cart->sourceBillingAddressId === $address->id) { - $newBillingAddress = Craft::$app->getElements()->duplicateElement($address, [ - 'primaryOwner' => $cart, - 'owner' => $cart, - 'title' => Craft::t('commerce', 'Billing Address'), - ]); - $cart->billingAddressId = $newBillingAddress->id; - } - - // Update the shipping address - if ($cart->sourceShippingAddressId === $address->id) { - $newShippingAddress = Craft::$app->getElements()->duplicateElement($address, [ - 'primaryOwner' => $cart, - 'owner' => $cart, - 'title' => Craft::t('commerce', 'Shipping Address'), - ]); - $cart->shippingAddressId = $newShippingAddress->id; - } - - // Save the cart to trigger events and recalculations. - Craft::$app->getElements()->saveElement($cart, false); - } - } -} diff --git a/src/services/PaymentCurrencies.php b/src/services/PaymentCurrencies.php deleted file mode 100644 index 91a96168d7..0000000000 --- a/src/services/PaymentCurrencies.php +++ /dev/null @@ -1,363 +0,0 @@ - - * @since 2.0 - */ -class PaymentCurrencies extends Component -{ - /** - * @event PaymentCurrencyRateEvent The event that is triggered when a payment currency rate is being resolved. - * Set `$event->rate` to override the rate used for conversions and historical transaction snapshots. - * @since 5.7.0 - */ - public const EVENT_DEFINE_PAYMENT_CURRENCY_RATE = 'definePaymentCurrencyRate'; - - /** - * @var null|Collection[] - */ - private ?array $_allPaymentCurrencies = null; - - /** - * Returns the rate for a payment currency, after giving event handlers a chance to override it. - * - * @since 5.7.0 - */ - public function getRateFor(PaymentCurrency $currency, ?Transaction $transaction = null): float - { - $event = new PaymentCurrencyRateEvent([ - 'rate' => $currency->rate, - 'paymentCurrency' => $currency, - 'transaction' => $transaction, - ]); - - $this->trigger(self::EVENT_DEFINE_PAYMENT_CURRENCY_RATE, $event); - - return $event->rate; - } - - /** - * Get payment currency by its ID. - * - * @throws InvalidConfigException if currency has invalid iso code defined - */ - public function getPaymentCurrencyById(int $id, ?int $storeId = null): ?PaymentCurrency - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - $all = $this->getAllPaymentCurrencies($storeId); - - return $all->where('id', $id)->first(); - } - - /** - * Get all payment currencies. - * - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - * @throws SiteNotFoundException - */ - public function getAllPaymentCurrencies(?int $storeId = null): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if ($this->_allPaymentCurrencies === null || !isset($this->_allPaymentCurrencies[$storeId])) { - $results = $this->_createPaymentCurrencyQuery() - ->orderBy(['iso' => SORT_ASC]) - ->where(['storeId' => $storeId]) - ->all(); - - if ($this->_allPaymentCurrencies === null) { - $this->_allPaymentCurrencies = []; - } - - foreach ($results as $result) { - $paymentCurrency = Craft::createObject([ - 'class' => PaymentCurrency::class, - 'attributes' => $result, - ]); - - if (!isset($this->_allPaymentCurrencies[$paymentCurrency->storeId])) { - $this->_allPaymentCurrencies[$paymentCurrency->storeId] = collect(); - } - - $this->_allPaymentCurrencies[$paymentCurrency->storeId]->push($paymentCurrency); - } - } - - return $this->_allPaymentCurrencies[$storeId] ?? collect(); - } - - /** - * Get a payment currency by its ISO code. - * - * @param string $iso - * @param int|null $storeId - * @return PaymentCurrency|null - * @throws CurrencyException if currency does not exist with that iso code - * @throws InvalidConfigException - * @throws SiteNotFoundException - */ - public function getPaymentCurrencyByIso(string $iso, ?int $storeId = null): ?PaymentCurrency - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - return $this->getAllPaymentCurrencies($storeId)->firstWhere('iso', $iso); - } - - /** - * Return the primary currencies ISO code as a string. - */ - public function getPrimaryPaymentCurrencyIso(?int $storeId = null): string - { - return $this->getPrimaryPaymentCurrency($storeId)?->iso ?? 'USD'; - } - - /** - * Returns the primary currency all prices are entered as. - * - * @throws CurrencyException - * @throws InvalidConfigException - */ - public function getPrimaryPaymentCurrency(?int $storeId = null): ?PaymentCurrency - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - $storeCurrency = Plugin::getInstance()->getStores()->getStoreById($storeId)->getCurrency(); - - return $this->getAllPaymentCurrencies($storeId)->firstWhere(fn(PaymentCurrency $currency) => $currency->getCode() == $storeCurrency->getCode()); - } - - /** - * Returns the non primary payment currencies - * - * @return Collection - * @throws CurrencyException - * @throws InvalidConfigException - */ - public function getNonPrimaryPaymentCurrencies(?int $storeId = null): Collection - { - $storeCurrency = Plugin::getInstance()->getStores()->getStoreById($storeId)->getCurrency(); - - return $this->getAllPaymentCurrencies($storeId)->where(fn(PaymentCurrency $currency) => $currency->getCode() != $storeCurrency->getCode()); - } - - /** - * Convert an amount in site's primary currency to a different currency by its ISO code. - * - * @param float $amount This is the unit of price in the primary store currency - * @throws CurrencyException if currency not found by its ISO code - * @throws InvalidConfigException - */ - public function convert(float $amount, string $currency): float - { - $destinationCurrency = $this->getPaymentCurrencyByIso($currency); - - if (!$destinationCurrency) { - throw new CurrencyException('No payment currency found with ISO code: ' . $currency); - } - - return $this->convertCurrency($amount, $this->getPrimaryPaymentCurrencyIso(), $currency); - } - - /** - * Convert an amount between currencies based on rates configured. - * - * @param float $amount - * @param string $fromCurrency - * @param string $toCurrency - * @param bool $round - * @return float - * @throws CurrencyException if currency not found by its ISO code - * @throws InvalidConfigException - * @deprecated 5.0.0 - */ - public function convertCurrency(float $amount, string $fromCurrency, string $toCurrency, bool $round = false): float - { - $fromCurrency = $this->getPaymentCurrencyByIso($fromCurrency); - $toCurrency = $this->getPaymentCurrencyByIso($toCurrency); - - if (!$fromCurrency) { - throw new CurrencyException('Currency not found: ' . $fromCurrency); - } - - if (!$toCurrency) { - throw new CurrencyException('Currency not found: ' . $toCurrency); - } - - if ($this->getPrimaryPaymentCurrency()->iso != $fromCurrency) { - // now the amount is in the primary currency - $amount /= $this->getRateFor($fromCurrency); - } - - $result = $amount * $this->getRateFor($toCurrency); - - if ($round) { - return CurrencyHelper::round($result, $toCurrency); - } - - return $result; - } - - - /** - * Save a payment currency. - * - * @param bool $runValidation should we validate this payment currency before saving. - * @throws Exception - */ - public function savePaymentCurrency(PaymentCurrency $model, bool $runValidation = true): bool - { - if ($model->id) { - $record = PaymentCurrencyRecord::findOne($model->id); - - if (!$record) { - throw new Exception(Craft::t('commerce', 'No currency exists with the ID “{id}”', - ['id' => $model->id])); - } - } else { - $record = new PaymentCurrencyRecord(); - } - - if ($runValidation && !$model->validate()) { - Craft::info('Payment currency not saved due to validation error.', __METHOD__); - - return false; - } - - $originalIso = $record->iso; - $record->iso = strtoupper($model->iso); - $record->storeId = $model->storeId; - // If this rate is primary, the rate must be 1 since it is now the rate all prices are enter in as. - $record->rate = $model->getPrimary() ? 1 : $model->rate; - - $record->save(false); - - // Now that we have a record ID, save it on the model - $model->id = $record->id; - - return true; - } - - /** - * Delete a payment currency by its ID. - * - * @param int $id - * @return bool - * @throws StaleObjectException - */ - public function deletePaymentCurrencyById(int $id): bool - { - $paymentCurrency = PaymentCurrencyRecord::findOne($id); - - if (!$paymentCurrency) { - return false; - } - - $baseCurrency = Plugin::getInstance()->getPaymentCurrencies()->getPrimaryPaymentCurrency($paymentCurrency->storeId); - - Db::update(Table::ORDERS, ['paymentCurrency' => $baseCurrency->iso], ['paymentCurrency' => $paymentCurrency->iso, 'storeId' => $paymentCurrency->storeId]); - - return $paymentCurrency->delete(); - } - - private function _getExchange(?int $storeId = null) - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - $storeCurrency = Plugin::getInstance()->getStores()->getStoreById($storeId)->getCurrency(); - $nonPrimaryCurrencies = $this->getNonPrimaryPaymentCurrencies($storeId)->mapWithKeys(fn(PaymentCurrency $currency) => [$currency->iso => (string)$this->getRateFor($currency)]); - - $exchange = [$storeCurrency->getCode() => $nonPrimaryCurrencies->all()]; - - // Reverse all the rates so we have to opposite conversions - foreach ($nonPrimaryCurrencies->all() as $iso => $rate) { - $exchange[$iso] = [$storeCurrency->getCode() => (string)(1 / (float)$rate)]; - } - - return new FixedExchange($exchange); - } - - /** - * @param Money $amount - * @param Currency|string $currency - * @param int|null $storeId - * @return Money - * @throws CurrencyException - * @throws InvalidConfigException - * @throws \craft\errors\SiteNotFoundException - * @since 5.0.0 - */ - public function convertAmount(Money $amount, Currency|string $currency, ?int $storeId = null): Money - { - if (is_string($currency)) { - $currency = new Currency($currency); - } - - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - $fromPaymentCurrency = $this->getPaymentCurrencyByIso($amount->getCurrency(), $storeId); - $toPaymentCurrency = $this->getPaymentCurrencyByIso($currency, $storeId); - - if (!$fromPaymentCurrency || !$toPaymentCurrency) { - throw new CurrencyException('Currency not found in store: ' . $currency); - } - - $converter = new Converter(new ISOCurrencies(), $this->_getExchange($storeId)); - return $converter->convert($amount, $toPaymentCurrency->getCurrency()); - } - - /** - * Returns a Query object prepped for retrieving Emails - */ - private function _createPaymentCurrencyQuery(): Query - { - return (new Query()) - ->select([ - 'dateCreated', - 'dateUpdated', - 'id', - 'iso', - 'storeId', - 'rate', - ]) - ->from([Table::PAYMENTCURRENCIES]); - } -} diff --git a/src/services/PaymentSources.php b/src/services/PaymentSources.php deleted file mode 100644 index 8542df7adb..0000000000 --- a/src/services/PaymentSources.php +++ /dev/null @@ -1,391 +0,0 @@ - - * @since 2.0 - */ -class PaymentSources extends Component -{ - /** - * @event PaymentSourceEvent The event that is triggered when a payment source is deleted. - * - * ```php - * use craft\commerce\events\PaymentSourceEvent; - * use craft\commerce\services\PaymentSources; - * use craft\commerce\models\PaymentSource; - * use yii\base\Event; - * - * Event::on( - * PaymentSources::class, - * PaymentSources::EVENT_DELETE_PAYMENT_SOURCE, - * function(PaymentSourceEvent $event) { - * // @var PaymentSource $source - * $source = $event->paymentSource; - * - * // Warn a user they don’t have any valid payment sources saved - * // ... - * } - * ); - * ``` - */ - public const EVENT_DELETE_PAYMENT_SOURCE = 'deletePaymentSource'; - - /** - * @event PaymentSourceEvent The event that is triggered before a payment source is added. - * - * ```php - * use craft\commerce\events\PaymentSourceEvent; - * use craft\commerce\services\PaymentSources; - * use craft\commerce\models\PaymentSource; - * use yii\base\Event; - * - * Event::on( - * PaymentSources::class, - * PaymentSources::EVENT_BEFORE_SAVE_PAYMENT_SOURCE, - * function(PaymentSourceEvent $event) { - * // @var PaymentSource $source - * $source = $event->paymentSource; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_SAVE_PAYMENT_SOURCE = 'beforeSavePaymentSource'; - - /** - * @event PaymentSourceEvent The event that is triggered after a payment source is added. - * - * ```php - * use craft\commerce\events\PaymentSourceEvent; - * use craft\commerce\services\PaymentSources; - * use craft\commerce\models\PaymentSource; - * use yii\base\Event; - * - * Event::on( - * PaymentSources::class, - * PaymentSources::EVENT_AFTER_SAVE_PAYMENT_SOURCE, - * function(PaymentSourceEvent $event) { - * // @var PaymentSource $source - * $source = $event->paymentSource; - * - * // Settle any outstanding balance - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_SAVE_PAYMENT_SOURCE = 'afterSavePaymentSource'; - - /** - * Returns a customer's payment sources, per the customer's ID. - * - * @param int|null $customerId the user's ID - * @param int|null $gatewayId the gateway's ID - * @return Collection - * @throws InvalidConfigException - * @throws SiteNotFoundException - * @noinspection PhpUnused - */ - public function getAllPaymentSourcesByCustomerId(?int $customerId = null, ?int $gatewayId = null): Collection - { - if ($customerId === null) { - return collect(); - } - - $query = $this->_createPaymentSourcesQuery() - ->innerJoin(['gateways' => Table::GATEWAYS], 'gateways.id = [[ps.gatewayId]]') - ->where(['customerId' => $customerId]); - - if ($gatewayId) { - $query->andWhere(['gatewayId' => $gatewayId]); - } - - $results = $query->all(); - - $sources = []; - - foreach ($results as $result) { - $sources[] = Craft::createObject([ - 'class' => PaymentSource::class, - 'attributes' => $result, - ]); - } - - return collect($sources); - } - - /** - * Returns all payment sources for a gateway. - * - * @param int|null $gatewayId the gateway's ID - * @return Collection - * @throws InvalidConfigException - */ - public function getAllPaymentSourcesByGatewayId(int $gatewayId = null): Collection - { - if ($gatewayId === null) { - return collect(); - } - - $results = $this->_createPaymentSourcesQuery() - ->where(['gatewayId' => $gatewayId]) - ->all(); - - $sources = []; - - foreach ($results as $result) { - $sources[] = Craft::createObject([ - 'class' => PaymentSource::class, - 'attributes' => $result, - ]); - } - - return collect($sources); - } - - /** - * Returns a customer's payment sources on a gateway, per the customer/user's ID. - * - * @param int|null $gatewayId the gateway's ID - * @param int|null $customerId the user's ID - * @return Collection - * @throws InvalidConfigException - */ - public function getAllGatewayPaymentSourcesByCustomerId(int $gatewayId = null, int $customerId = null): Collection - { - if ($gatewayId === null || $customerId === null) { - return collect(); - } - - $results = $this->_createPaymentSourcesQuery() - ->where(['customerId' => $customerId]) - ->andWhere(['gatewayId' => $gatewayId]) - ->all(); - - $sources = []; - - foreach ($results as $result) { - $sources[] = Craft::createObject([ - 'class' => PaymentSource::class, - 'attributes' => $result, - ]); - } - - return collect($sources); - } - - /** - * Returns a payment source by its gateways token - * - * @param string $token the payment gateway's token - * @param int $gatewayId the gateway's ID - * @return PaymentSource|null - * @throws InvalidConfigException - */ - public function getPaymentSourceByTokenAndGatewayId(string $token, int $gatewayId): ?PaymentSource - { - $result = $this->_createPaymentSourcesQuery() - ->where(['token' => $token]) - ->andWhere(['gatewayId' => $gatewayId]) - ->one(); - - return $result ? Craft::createObject(['class' => PaymentSource::class, 'attributes' => $result]) : null; - } - - /** - * Returns a payment source by its ID. - * - * @param int $sourceId the source ID - * @return PaymentSource|null - * @throws InvalidConfigException - * @throws SiteNotFoundException - */ - public function getPaymentSourceById(int $sourceId): ?PaymentSource - { - $result = $this->_createPaymentSourcesQuery() - ->where(['[[ps.id]]' => $sourceId]) - ->innerJoin(['gateways' => Table::GATEWAYS], 'gateways.id = [[ps.gatewayId]]') // ensure it is a gateway payment source - ->one(); - - return $result ? Craft::createObject(['class' => PaymentSource::class, 'attributes' => $result]) : null; - } - - /** - * Returns a payment source by its ID and user ID. - * - * @param int $sourceId the source ID - * @param int $userId the source's user ID - */ - public function getPaymentSourceByIdAndUserId(int $sourceId, int $userId): ?PaymentSource - { - $result = $this->_createPaymentSourcesQuery() - ->where(['id' => $sourceId]) - ->andWhere(['customerId' => $userId]) - ->one(); - - return $result ? new PaymentSource($result) : null; - } - - /** - * Creates a payment source for a user in the gateway based on a payment form. - * - * @param int $customerId the user's ID - * @param GatewayInterface $gateway the gateway - * @param BasePaymentForm $paymentForm the payment form to use - * @param string|null $sourceDescription the payment form to use - * @return PaymentSource The saved payment source. - * @throws InvalidConfigException - * @throws PaymentSourceException If unable to create the payment source - */ - public function createPaymentSource(int $customerId, GatewayInterface $gateway, BasePaymentForm $paymentForm, string $sourceDescription = null, bool $makePrimarySource = false): PaymentSource - { - $source = $gateway->createPaymentSource($paymentForm, $customerId); - - $source->customerId = $customerId; - - if (!empty($sourceDescription)) { - $source->description = $sourceDescription; - } - - if (!$this->savePaymentSource($source)) { - throw new PaymentSourceException(Craft::t('commerce', 'Could not create the payment source.')); - } - - if ($makePrimarySource) { - Plugin::getInstance()->getCustomers()->savePrimaryPaymentSourceId($source->getCustomer(), $source->id); - } - - return $source; - } - - /** - * Saves a payment source. - * - * @param PaymentSource $paymentSource The payment source being saved. - * @param bool $runValidation should we validate this payment source before saving. - * @return bool Whether the payment source was saved successfully - * @throws InvalidConfigException if the payment source couldn't be found - */ - public function savePaymentSource(PaymentSource $paymentSource, bool $runValidation = true): bool - { - if ($paymentSource->id) { - $record = PaymentSourceRecord::findOne($paymentSource->id); - - if (!$record) { - throw new InvalidConfigException(Craft::t('commerce', 'No payment source exists with the ID “{id}”', - ['id' => $paymentSource->id])); - } - } else { - $record = new PaymentSourceRecord(); - } - - // fire a 'beforeSavePaymentSource' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_PAYMENT_SOURCE)) { - $this->trigger(self::EVENT_BEFORE_SAVE_PAYMENT_SOURCE, new PaymentSourceEvent([ - 'paymentSource' => $paymentSource, - ])); - } - - if ($runValidation && !$paymentSource->validate()) { - Craft::info('Payment source not saved due to validation error.', __METHOD__); - - return false; - } - - $record->customerId = $paymentSource->customerId; - $record->gatewayId = $paymentSource->gatewayId; - $record->token = $paymentSource->token; - $record->description = $paymentSource->description; - $record->response = $paymentSource->response; - - // Save it! - $record->save(false); - - // Now that we have a record ID, save it on the model - $paymentSource->id = $record->id; - - // fire a 'afterSavePaymentSource' event - if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_PAYMENT_SOURCE)) { - $this->trigger(self::EVENT_AFTER_SAVE_PAYMENT_SOURCE, new PaymentSourceEvent([ - 'paymentSource' => $paymentSource, - ])); - } - - return true; - } - - /** - * Delete a payment source by its ID. - * - * @param int $id The ID - * @throws Throwable in case something went wrong when deleting. - */ - public function deletePaymentSourceById(int $id): bool - { - $record = PaymentSourceRecord::findOne($id); - - if ($record) { - $gateway = Plugin::getInstance()->getGateways()->getGatewayById($record->gatewayId); - - $gateway?->deletePaymentSource($record->token); - - $paymentSource = $this->getPaymentSourceById($id); - - // Fire an 'deletePaymentSource' event. - if ($this->hasEventHandlers(self::EVENT_DELETE_PAYMENT_SOURCE)) { - $this->trigger(self::EVENT_DELETE_PAYMENT_SOURCE, new PaymentSourceEvent([ - 'paymentSource' => $paymentSource, - ])); - } - - return (bool)$record->delete(); - } - - return false; - } - - /** - * Returns a Query object prepped for retrieving gateways. - * - * @return Query The query object. - */ - private function _createPaymentSourcesQuery(): Query - { - return (new Query()) - ->select([ - 'ps.description', - 'ps.gatewayId', - 'ps.id', - 'ps.response', - 'ps.token', - 'ps.customerId', - ]) - ->from(['ps' => Table::PAYMENTSOURCES]); - } -} diff --git a/src/services/Payments.php b/src/services/Payments.php deleted file mode 100644 index 111304616c..0000000000 --- a/src/services/Payments.php +++ /dev/null @@ -1,644 +0,0 @@ - - * @since 2.0 - */ -class Payments extends Component -{ - /** - * @event TransactionEvent The event that is triggered when a complete-payment request is made. - * After this event, the customer will be redirected offsite or be redirected to the order success returnUrl. - * - * ```php - * use craft\commerce\events\TransactionEvent; - * use craft\commerce\services\Payments; - * use craft\commerce\models\Transaction; - * use yii\base\Event; - * - * Event::on( - * Payments::class, - * Payments::EVENT_AFTER_COMPLETE_PAYMENT, - * function(TransactionEvent $event) { - * // @var Transaction $transaction - * $transaction = $event->transaction; - * - * // Check whether it was an authorize transaction - * // and make sure that warehouse team is on top of it - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_COMPLETE_PAYMENT = 'afterCompletePayment'; - - /** - * @event TransactionEvent The event that is triggered before a payment transaction is captured. - * - * ```php - * use craft\commerce\events\TransactionEvent; - * use craft\commerce\services\Payments; - * use craft\commerce\models\Transaction; - * use yii\base\Event; - * - * Event::on( - * Payments::class, - * Payments::EVENT_BEFORE_CAPTURE_TRANSACTION, - * function(TransactionEvent $event) { - * // @var Transaction $transaction - * $transaction = $event->transaction; - * - * // Check that shipment’s ready before capturing - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_CAPTURE_TRANSACTION = 'beforeCaptureTransaction'; - - /** - * @event TransactionEvent The event that is triggered after a payment transaction is captured. - * - * ```php - * use craft\commerce\events\TransactionEvent; - * use craft\commerce\services\Payments; - * use craft\commerce\models\Transaction; - * use yii\base\Event; - * - * Event::on( - * Payments::class, - * Payments::EVENT_AFTER_CAPTURE_TRANSACTION, - * function(TransactionEvent $event) { - * // @var Transaction $transaction - * $transaction = $event->transaction; - * - * // Notify the warehouse we're ready to ship - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_CAPTURE_TRANSACTION = 'afterCaptureTransaction'; - - /** - * @event TransactionEvent The event that is triggered before a transaction is refunded. - * - * ```php - * use craft\commerce\events\RefundTransactionEvent; - * use craft\commerce\services\Payments; - * use yii\base\Event; - * - * Event::on( - * Payments::class, - * Payments::EVENT_BEFORE_REFUND_TRANSACTION, - * function(RefundTransactionEvent $event) { - * // @var float $amount - * $amount = $event->amount; - * - * // Do something else if the refund amount’s >50% of the transaction - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_REFUND_TRANSACTION = 'beforeRefundTransaction'; - - /** - * @event TransactionEvent The event that is triggered after a transaction is refunded. - * - * ```php - * use craft\commerce\events\RefundTransactionEvent; - * use craft\commerce\services\Payments; - * use yii\base\Event; - * - * Event::on( - * Payments::class, - * Payments::EVENT_AFTER_REFUND_TRANSACTION, - * function(RefundTransactionEvent $event) { - * // @var float $amount - * $amount = $event->amount; - * - * // Do something else if the refund amount’s >50% of the transaction - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_REFUND_TRANSACTION = 'afterRefundTransaction'; - - /** - * @event ProcessPaymentEvent The event that is triggered before a payment is processed. - * - * You may set the `isValid` property to `false` on the event to prevent the payment from being processed. - * - * ```php - * use craft\commerce\events\ProcessPaymentEvent; - * use craft\commerce\services\Payments; - * use craft\commerce\elements\Order; - * use craft\commerce\models\payments\BasePaymentForm; - * use craft\commerce\models\Transaction; - * use craft\commerce\base\RequestResponseInterface; - * use yii\base\Event; - * - * Event::on( - * Payments::class, - * Payments::EVENT_BEFORE_PROCESS_PAYMENT, - * function(ProcessPaymentEvent $event) { - * // @var Order $order - * $order = $event->order; - * // @var BasePaymentForm $form - * $form = $event->form; - * // @var Transaction $transaction - * $transaction = $event->transaction; - * // @var RequestResponseInterface $response - * $response = $event->response; - * - * // Check some business rules to see whether the transaction is allowed - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_PROCESS_PAYMENT = 'beforeProcessPaymentEvent'; - - /** - * @event ProcessPaymentEvent The event that is triggered after a payment is processed. - * - * ```php - * use craft\commerce\events\ProcessPaymentEvent; - * use craft\commerce\services\Payments; - * use craft\commerce\elements\Order; - * use craft\commerce\models\payments\BasePaymentForm; - * use craft\commerce\models\Transaction; - * use craft\commerce\base\RequestResponseInterface; - * use yii\base\Event; - * - * Event::on( - * Payments::class, - * Payments::EVENT_AFTER_PROCESS_PAYMENT, - * function(ProcessPaymentEvent $event) { - * // @var Order $order - * $order = $event->order; - * // @var BasePaymentForm $form - * $form = $event->form; - * // @var Transaction $transaction - * $transaction = $event->transaction; - * // @var RequestResponseInterface $response - * $response = $event->response; - * - * // Let the accounting department know an order transaction went through - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_PROCESS_PAYMENT = 'afterProcessPaymentEvent'; - - /** - * Process a payment. - * - * @param Order $order the order for which the payment is. - * @param BasePaymentForm $form the payment form. - * @param string|null &$redirect a string parameter by reference that will contain the redirect URL, if any - * @param Transaction|null &$transaction the transaction - * @param array|null &$redirectData the additional data the gateway might need to redirect the user to the payment page. This is useful for ajax payment responses. - * @return void - * @throws InvalidConfigException - * @throws PaymentException if the payment was unsuccessful - * @throws TransactionException - * @throws CurrencyException - */ - public function processPayment(Order $order, BasePaymentForm $form, ?string &$redirect, ?Transaction &$transaction, ?array &$redirectData = []): void - { - // Raise the 'beforeProcessPaymentEvent' event - $event = new ProcessPaymentEvent(compact('order', 'form')); - - $this->trigger(self::EVENT_BEFORE_PROCESS_PAYMENT, $event); - - if (!$event->isValid) { - // This error potentially is going to be displayed in the frontend, so we have to be vague about it. - // Long story short - a plugin said "no." - throw new PaymentException(Craft::t('commerce', 'Unable to make payment at this time.')); - } - - // Order could have zero totalPrice and already considered 'paid'. Free orders complete immediately. - $paymentStrategy = $order->getStore()->getFreeOrderPaymentStrategy(); - if (!$order->hasOutstandingBalance() && !$order->datePaid && $paymentStrategy === Store::FREE_ORDER_PAYMENT_STRATEGY_COMPLETE) { - $order->updateOrderPaidInformation(); - - if ($order->isCompleted) { - return; - } - } - - $gateway = $order->getGateway(); - if (!$gateway) { - throw new InvalidConfigException(Craft::t('commerce', 'Missing Gateway')); - } - - //choosing default action - $defaultAction = $gateway->paymentType; - $defaultAction = ($defaultAction === TransactionRecord::TYPE_PURCHASE) ? $defaultAction : TransactionRecord::TYPE_AUTHORIZE; - - if ($defaultAction === TransactionRecord::TYPE_AUTHORIZE) { - if (!$gateway->supportsAuthorize()) { - throw new PaymentException(Craft::t('commerce', 'Gateway doesn’t support authorize')); - } - } elseif (!$gateway->supportsPurchase()) { - throw new PaymentException(Craft::t('commerce', 'Gateway doesn’t support purchase')); - } - - //creating order, transaction and request - $transaction = Plugin::getInstance()->getTransactions()->createTransaction($order, null, $defaultAction); - - try { - $response = match ($defaultAction) { - TransactionRecord::TYPE_PURCHASE => $gateway->purchase($transaction, $form), - TransactionRecord::TYPE_AUTHORIZE => $gateway->authorize($transaction, $form), - }; - - $this->_updateTransaction($transaction, $response); - - if ($this->hasEventHandlers(self::EVENT_AFTER_PROCESS_PAYMENT)) { - $this->trigger(self::EVENT_AFTER_PROCESS_PAYMENT, new ProcessPaymentEvent(compact('order', 'transaction', 'form', 'response'))); - } - - // For redirects or unsuccessful transactions, save the transaction before bailing - if ($response->isRedirect()) { - $this->_handleRedirect($response, $redirect, $redirectData); - return; - } - - if (!in_array($transaction->status, [TransactionRecord::STATUS_SUCCESS, TransactionRecord::STATUS_PROCESSING])) { - throw new PaymentException($transaction->message); - } - - // Success! - $order->updateOrderPaidInformation(); - } catch (Exception $e) { - $transaction->status = TransactionRecord::STATUS_FAILED; - $transaction->message = $e->getMessage(); - - // If this transactions is already saved, don't even try. - if (!$transaction->id) { - $this->_saveTransaction($transaction); - } - - Craft::$app->getErrorHandler()->logException($e); - throw new PaymentException($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * Capture a transaction. - * - * @param Transaction $transaction the transaction to capture. - * @throws TransactionException if something went wrong when saving the transaction - */ - public function captureTransaction(Transaction $transaction): Transaction - { - // Raise 'beforeCaptureTransaction' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_CAPTURE_TRANSACTION)) { - $this->trigger(self::EVENT_BEFORE_CAPTURE_TRANSACTION, new TransactionEvent([ - 'transaction' => $transaction, - ])); - } - - $transaction = $this->_capture($transaction); - - // Raise 'afterCaptureTransaction' event - if ($this->hasEventHandlers(self::EVENT_AFTER_CAPTURE_TRANSACTION)) { - $this->trigger(self::EVENT_AFTER_CAPTURE_TRANSACTION, new TransactionEvent([ - 'transaction' => $transaction, - ])); - } - - return $transaction; - } - - /** - * Refund a transaction. - * - * @param Transaction $transaction the transaction to refund. - * @param float|null $amount the amount to refund or null for full amount. - * @param string $note the administrators note on the refund - * @throws RefundException if something went wrong during the refund. - */ - public function refundTransaction(Transaction $transaction, ?float $amount = null, string $note = ''): Transaction - { - // Raise 'beforeRefundTransaction' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_REFUND_TRANSACTION)) { - $this->trigger(self::EVENT_BEFORE_REFUND_TRANSACTION, new RefundTransactionEvent(compact('transaction', 'amount'))); - } - - $refundTransaction = $this->_refund($transaction, $amount, $note); - - /// Raise 'afterRefundTransaction' event - if ($this->hasEventHandlers(self::EVENT_AFTER_REFUND_TRANSACTION)) { - $this->trigger(self::EVENT_AFTER_REFUND_TRANSACTION, new RefundTransactionEvent(compact('transaction', 'refundTransaction', 'amount'))); - } - - return $refundTransaction; - } - - /** - * Process return from off-site payment. - * - * @param Transaction $transaction - * @param string|null &$customError - * @return bool - * @throws CurrencyException - * @throws ExitException - * @throws InvalidConfigException - * @throws LoaderError - * @throws RuntimeError - * @throws SyntaxError - * @throws Throwable - * @throws TransactionException - * @throws \craft\commerce\errors\OrderStatusException - * @throws \craft\errors\ElementNotFoundException - * @throws \yii\base\Exception - */ - public function completePayment(Transaction $transaction, ?string &$customError): bool - { - // Only transactions with the status of "redirect" can be completed - if (!in_array($transaction->status, [TransactionRecord::STATUS_REDIRECT, TransactionRecord::STATUS_SUCCESS], true)) { - $customError = $transaction->message; - - return false; - } - - $transactionLockName = 'commerceTransaction:' . $transaction->hash; - $mutex = Craft::$app->getMutex(); - - if (!$mutex->acquire($transactionLockName, 15)) { - throw new Exception('Unable to acquire a lock for transaction: ' . $transaction->hash); - } - - // Make sure we have the latest transaction data - $transaction = Plugin::getInstance()->getTransactions()->getTransactionByHash($transaction->hash); - - // If it's successful already, we're good. - if (Plugin::getInstance()->getTransactions()->isTransactionSuccessful($transaction)) { - $transaction->order->updateOrderPaidInformation(); - $mutex->release($transactionLockName); - return true; - } - - // Load payment driver for the transaction we are trying to complete - $gateway = $transaction->getGateway(); - - switch ($transaction->type) { - case TransactionRecord::TYPE_PURCHASE: - $response = $gateway->completePurchase($transaction); - break; - case TransactionRecord::TYPE_AUTHORIZE: - $response = $gateway->completeAuthorize($transaction); - break; - default: - $mutex->release($transactionLockName); - return false; - } - - $childTransaction = Plugin::getInstance()->getTransactions()->createTransaction(null, $transaction); - $this->_updateTransaction($childTransaction, $response); - - // Success can mean 2 things in this context. - // 1) The transaction completed successfully with the gateway, and is now marked as complete. - // 2) The result of the gateway request was successful but also got a redirect response. We now need to redirect if $redirect is not null. - $success = $response->isSuccessful() || $response->isProcessing(); - $isParentTransactionRedirect = ($transaction->status === TransactionRecord::STATUS_REDIRECT); - - if ($success) { - if ($transaction->status === TransactionRecord::STATUS_SUCCESS || ($isParentTransactionRedirect && $childTransaction->status == TransactionRecord::STATUS_SUCCESS)) { - $transaction->order->updateOrderPaidInformation(); - } - - if ($isParentTransactionRedirect && $childTransaction->status == TransactionRecord::STATUS_PROCESSING) { - $transaction->order->markAsComplete(); - } - } - - if ($this->hasEventHandlers(self::EVENT_AFTER_COMPLETE_PAYMENT)) { - $this->trigger(self::EVENT_AFTER_COMPLETE_PAYMENT, new TransactionEvent([ - 'transaction' => $transaction, - ])); - } - - $redirectData = []; - if ($response->isRedirect() && $transaction->status === TransactionRecord::STATUS_REDIRECT) { - $mutex->release($transactionLockName); - $this->_handleRedirect($response, $redirect, $redirectData); - Craft::$app->getResponse()->redirect($redirect); - Craft::$app->end(); - } - - if (!$success) { - $customError = $response->getMessage(); - } - - $mutex->release($transactionLockName); - - return $success; - } - - /** - * Handles a redirect. - * - * @param RequestResponseInterface $response - * @param string|null $redirect - * @param array|null $redirectData - * @throws ExitException - * @throws LoaderError - * @throws RuntimeError - * @throws SyntaxError - * @throws \yii\base\Exception - */ - private function _handleRedirect(RequestResponseInterface $response, ?string &$redirect, ?array &$redirectData): void - { - // If the gateway tells is it is a GET redirect, let them - if ($response->getRedirectMethod() === 'GET') { - $redirect = $response->getRedirectUrl(); - $redirectData = $response->getRedirectData(); - } else { - $gatewayPostRedirectTemplate = Plugin::getInstance()->getSettings()->gatewayPostRedirectTemplate; - - if (!empty($gatewayPostRedirectTemplate)) { - $variables = []; - $hiddenFields = ''; - - // Gather all post hidden data inputs. - foreach ($response->getRedirectData() as $key => $value) { - $hiddenFields .= sprintf('', htmlentities($key, ENT_QUOTES, 'UTF-8', false), htmlentities($value, ENT_QUOTES, 'UTF-8', false)) . "\n"; - } - - $variables['inputs'] = $hiddenFields; - - // Set the action url to the responses redirect url - $variables['actionUrl'] = $response->getRedirectUrl(); - - // Set Craft to the site template mode - $templatesService = Craft::$app->getView(); - $oldTemplateMode = $templatesService->getTemplateMode(); - $templatesService->setTemplateMode($templatesService::TEMPLATE_MODE_SITE); - - $template = $templatesService->renderPageTemplate($gatewayPostRedirectTemplate, $variables); - - // Restore the original template mode - $templatesService->setTemplateMode($oldTemplateMode); - - // Send the template back to the user. - ob_start(); - echo $template; - Craft::$app->end(); - } - - // Let the gateway's response redirect us - $response->redirect(); - } - } - - /** - * Process a capture or refund exception. - * - * @throws TransactionException if unable to save transaction - * @throws InvalidConfigException - */ - private function _capture(Transaction $parent): Transaction - { - $child = Plugin::getInstance()->getTransactions()->createTransaction(null, $parent, TransactionRecord::TYPE_CAPTURE); - - $gateway = $parent->getGateway(); - - try { - $response = $gateway->capture($child, (string)$parent->reference); - $this->_updateTransaction($child, $response); - } catch (Exception $e) { - $child->status = TransactionRecord::STATUS_FAILED; - $child->message = $e->getMessage(); - $this->_saveTransaction($child); - - Craft::$app->getErrorHandler()->logException($e); - } - - return $child; - } - - /** - * Process a capture or refund exception. - * - * @param float|null $amount - * @param string $note the administrators note on the refund - * @throws RefundException if anything goes wrong during a refund - */ - private function _refund(Transaction $parent, float $amount = null, string $note = ''): Transaction - { - try { - $gateway = $parent->getGateway(); - - if (!$gateway->supportsRefund()) { - throw new SubscriptionException(Craft::t('commerce', 'Gateway doesn’t support refunds.')); - } - - if ($amount < $parent->paymentAmount && !$gateway->supportsPartialRefund()) { - throw new SubscriptionException(Craft::t('commerce', 'Gateway doesn’t support partial refunds.')); - } - - $child = Plugin::getInstance()->getTransactions()->createTransaction(null, $parent, TransactionRecord::TYPE_REFUND); - - // If amount is not supplied refund the full amount - $child->paymentAmount = Currency::round($amount, $child->currency) ?: $parent->getRefundableAmount(); - - // Calculate amount in the primary currency - $child->amount = Currency::round($child->paymentAmount / $parent->paymentRate, $child->currency); - $child->note = $note; - - $gateway = $parent->getGateway(); - - try { - $response = $gateway->refund($child); - $this->_updateTransaction($child, $response); - } catch (Throwable $exception) { - Craft::error(Craft::t('commerce', 'Error refunding transaction: {transactionHash}', ['transactionHash' => $parent->hash]), 'commerce'); - $child->status = TransactionRecord::STATUS_FAILED; - $child->message = $exception->getMessage(); - $this->_saveTransaction($child); - } - - return $child; - } catch (Throwable $exception) { - throw new RefundException($exception->getMessage()); - } - } - - /** - * Save a transaction. - * - * @param Transaction $child - * @throws TransactionException - */ - private function _saveTransaction(Transaction $child): void - { - if (!Plugin::getInstance()->getTransactions()->saveTransaction($child)) { - throw new TransactionException('Error saving transaction: ' . implode(', ', $child->getFirstErrors())); - } - } - - /** - * Updates a transaction. - */ - private function _updateTransaction(Transaction $transaction, RequestResponseInterface $response): void - { - if ($response->isSuccessful()) { - $transaction->status = TransactionRecord::STATUS_SUCCESS; - } elseif ($response->isProcessing()) { - $transaction->status = TransactionRecord::STATUS_PROCESSING; - } elseif ($response->isRedirect()) { - $transaction->status = TransactionRecord::STATUS_REDIRECT; - } else { - $transaction->status = TransactionRecord::STATUS_FAILED; - } - - $transaction->response = $response->getData(); - $transaction->code = $response->getCode(); - $transaction->reference = $response->getTransactionReference(); - $transaction->message = $response->getMessage(); - - $this->_saveTransaction($transaction); - } -} diff --git a/src/services/Pdfs.php b/src/services/Pdfs.php deleted file mode 100644 index d0225495e0..0000000000 --- a/src/services/Pdfs.php +++ /dev/null @@ -1,713 +0,0 @@ - - * @since 2.0 - * - * @property-read null|Pdf $defaultPdf - * @property-read Pdf[] $allEnabledPdfs - * @property-read bool $hasEnabledPdf - * @property-read null|Pdf[] $allPdfs - */ -class Pdfs extends Component -{ - /** - * @var Pdf[]|null - */ - private ?array $_allPdfs = null; - - /** - * @event PdfEvent The event that is triggered before an pdf is saved. - * - * ```php - * use craft\commerce\events\PdfEvent; - * use craft\commerce\services\Pdfs; - * use craft\commerce\models\Pdf; - * use yii\base\Event; - * - * Event::on( - * Pdfs::class, - * Pdfs::EVENT_BEFORE_SAVE_PDF, - * function(PdfEvent $event) { - * // @var Pdf $pdf - * $pdf = $event->pdf; - * // @var bool $isNew - * $isNew = $event->isNew; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_SAVE_PDF = 'beforeSavePdf'; - - /** - * @event PdfEvent The event that is triggered after an PDF is saved. - * - * ```php - * use craft\commerce\events\PdfEvent; - * use craft\commerce\services\Pdfs; - * use craft\commerce\models\Pdf; - * use yii\base\Event; - * - * Event::on( - * Pdfs::class, - * Pdfs::EVENT_AFTER_SAVE_PDF, - * function(PdfEvent $event) { - * // @var Pdf $pdf - * $pdf = $event->pdf; - * // @var bool $isNew - * $isNew = $event->isNew; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_SAVE_PDF = 'afterSavePdf'; - - /** - * @event PdfRenderEvent The event that is triggered before an order’s PDF is rendered. - * - * Event handlers can customize PDF rendering by modifying several properties on the event object: - * - * | Property | Value | - * | ----------- | ------------------------------------------------------------------------------------------------------------------------- | - * | `order` | populated [Order](api:craft\commerce\elements\Order) model | - * | `template` | optional Twig template path (string) to be used for rendering | - * | `variables` | populated with the variables available to the template used for rendering | - * | `option` | optional string for the template that can be used to show different details based on context (example: `receipt`, `ajax`) | - * - * ```php - * use craft\commerce\events\PdfRenderEvent; - * use craft\commerce\services\Pdf; - * use yii\base\Event; - * - * Event::on( - * Pdf::class, - * Pdf::EVENT_BEFORE_RENDER_PDF, - * function(PdfRenderEvent $event) { - * // Modify `$event->order`, `$event->option`, `$event->template`, - * // and `$event->variables` to customize what gets rendered into a PDF - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_RENDER_PDF = 'beforeRenderPdf'; - - /** - * @event PdfRenderEvent The event that is triggered after an order’s PDF has been rendered. - * - * Event handlers can override Commerce’s PDF generation by setting the `pdf` property on the event to a custom-rendered PDF string. The event properties will be the same as those from `beforeRenderPdf`, but `pdf` will contain a rendered PDF string and is the only one for which setting a value will make any difference for the resulting PDF output. - * - * ```php - * use craft\commerce\events\PdfRenderEvent; - * use craft\commerce\services\Pdf; - * use yii\base\Event; - * - * Event::on( - * Pdf::class, - * Pdf::EVENT_AFTER_RENDER_PDF, - * function(PdfRenderEvent $event) { - * // Add a watermark to the PDF or forward it to the accounting department - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_RENDER_PDF = 'afterRenderPdf'; - - /** - * @event PdfRenderOptionsEvent The event that allows additional setting of pdf render options. - * @since 3.2.10 - * - * ```php - * use craft\commerce\events\PdfRenderOptionsEvent; - * use craft\commerce\services\Pdfs; - * use yii\base\Event; - * - * Event::on( - * Pdfs::class, - * Pdfs::EVENT_MODIFY_RENDER_OPTIONS, - * function (PdfRenderOptionsEvent $event) { - * $storagePath = Craft::$app->getPath()->getStoragePath(); - * - * // E.g. of setting additional render options. - * $event->options->setChroot($storagePath); - * } - * ); - *``` - */ - public const EVENT_MODIFY_RENDER_OPTIONS = 'modifyRenderOptions'; - - /** - * @event PdfEvent The event that is triggered before a pdf is deleted. - * - * ```php - * use craft\commerce\events\PdfEvent; - * use craft\commerce\services\Pdfs; - * use craft\commerce\models\Pdf; - * use yii\base\Event; - * - * Event::on( - * Pdfs::class, - * Pdfs::EVENT_BEFORE_DELETE_PDF, - * function(PdfEvent $event) { - * // @var Pdf $pdf - * $pdf = $event->pdf; - * - * // ... - * } - * ); - * ``` - * - * @since 4.0.0 - */ - public const EVENT_BEFORE_DELETE_PDF = 'beforeDeletePdf'; - - public const CONFIG_PDFS_KEY = 'commerce.pdfs'; - - /** - * @param int|null $storeId - * @return Collection - * @throws SiteNotFoundException - * @throws InvalidConfigException - * @since 3.2 - */ - public function getAllPdfs(?int $storeId = null): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if ($this->_allPdfs === null || !isset($this->_allPdfs[$storeId])) { - $results = $this->_createPdfsQuery() - ->where(['storeId' => $storeId]) - ->all(); - - // Start with a blank slate if it isn't memoized, or we're fetching all shipping categories - if ($this->_allPdfs === null) { - $this->_allPdfs = []; - } - - foreach ($results as $result) { - $pdf = Craft::createObject([ - 'class' => Pdf::class, - 'attributes' => $result, - ]); - - if (!isset($this->_allPdfs[$pdf->storeId])) { - $this->_allPdfs[$pdf->storeId] = collect(); - } - - $this->_allPdfs[$pdf->storeId]->push($pdf); - } - } - - return $this->_allPdfs[$storeId] ?? collect(); - } - - /** - * @since 3.2 - */ - public function getHasEnabledPdf(?int $storeId = null): bool - { - return $this->getAllPdfs($storeId)->contains('enabled', true); - } - - /** - * @param int|null $storeId - * @return Collection - * @since 3.2 - */ - public function getAllEnabledPdfs(?int $storeId = null): Collection - { - return $this->getAllPdfs($storeId)->where('enabled', true); - } - - /** - * @since 3.2 - */ - public function getDefaultPdf(?int $storeId = null): ?Pdf - { - return $this->getAllPdfs($storeId)->firstWhere('isDefault', true); - } - - /** - * @since 3.2 - */ - public function getPdfByHandle(string $handle, ?int $storeId = null): ?Pdf - { - return $this->getAllPdfs($storeId)->firstWhere('handle', $handle); - } - - /** - * Get an PDF by its ID. - * - * @since 3.2 - */ - public function getPdfById(int $id, ?int $storeId = null): ?Pdf - { - return $this->getAllPdfs($storeId)->firstWhere('id', $id); - } - - /** - * Save an PDF. - * - * @throws Exception - * @throws ErrorException - * @throws NotSupportedException - * @throws ServerErrorHttpException - * @since 3.2 - */ - public function savePdf(Pdf $pdf, bool $runValidation = true): bool - { - $isNewPdf = !(bool)$pdf->id; - - // Fire a 'beforeSavePdf' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_PDF)) { - $this->trigger(self::EVENT_BEFORE_SAVE_PDF, new PdfEvent([ - 'pdf' => $pdf, - 'isNew' => $isNewPdf, - ])); - } - - if ($runValidation && !$pdf->validate()) { - Craft::info('Pdf not saved due to validation error(s).', __METHOD__); - return false; - } - - if ($isNewPdf) { - $pdf->uid = StringHelper::UUID(); - } - - $configPath = self::CONFIG_PDFS_KEY . '.' . $pdf->uid; - $configData = $pdf->getConfig(); - Craft::$app->getProjectConfig()->set($configPath, $configData); - - if ($isNewPdf) { - $pdf->id = Db::idByUid(Table::PDFS, $pdf->uid); - } - - return true; - } - - /** - * Handle PDF status change. - * - * @throws \yii\db\Exception - * @since 3.2 - */ - public function handleChangedPdf(ConfigEvent $event): void - { - ProjectConfigData::ensureAllStoresProcessed(); - - $pdfUid = $event->tokenMatches[0]; - $data = $event->newValue; - - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - $pdfRecord = $this->_getPdfRecord($pdfUid); - $isNewPdf = $pdfRecord->getIsNewRecord(); - $store = Plugin::getInstance()->getStores()->getStoreByUid($data['store']); - - $pdfRecord->storeId = $store->id; - $pdfRecord->name = $data['name']; - $pdfRecord->handle = $data['handle']; - $pdfRecord->description = $data['description']; - $pdfRecord->templatePath = $data['templatePath'] ?? ''; - $pdfRecord->fileNameFormat = $data['fileNameFormat'] ?? ''; - $pdfRecord->enabled = $data['enabled']; - $pdfRecord->sortOrder = $data['sortOrder']; - $pdfRecord->isDefault = $data['isDefault']; - $pdfRecord->language = $data['language'] ?? PdfRecord::LOCALE_ORDER_LANGUAGE; - $pdfRecord->paperOrientation = $data['paperOrientation'] ?? PdfRecord::PAPER_ORIENTATION_PORTRAIT; - $pdfRecord->paperSize = $data['paperSize'] ?? 'letter'; - $pdfRecord->linkExpiry = $data['linkExpiry'] ?? 86400; - - $pdfRecord->uid = $pdfUid; - - $pdfRecord->save(false); - - if ($pdfRecord->isDefault) { - PdfRecord::updateAll(['isDefault' => false], ['and', - ['not', ['id' => $pdfRecord->id]], - ['storeId' => $pdfRecord->storeId], - ]); - } - - $transaction->commit(); - } catch (\Exception $e) { - $transaction->rollBack(); - throw $e; - } - - // Fire a 'afterSavePdf' event - if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_PDF)) { - $this->trigger(self::EVENT_AFTER_SAVE_PDF, new PdfEvent([ - 'pdf' => $this->getPdfById($pdfRecord->id, $pdfRecord->storeId), - 'isNew' => $isNewPdf, - ])); - } - - $this->_allPdfs = null; // clear cache - } - - /** - * Delete an PDF by its ID. - * - * @since 3.2 - */ - public function deletePdfById(int $id): bool - { - $pdf = PdfRecord::findOne($id); - - if ($pdf) { - // Fire a 'beforeDeletePdf' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_DELETE_PDF)) { - $this->trigger(self::EVENT_BEFORE_DELETE_PDF, new PdfEvent([ - 'pdf' => $this->getPdfById($pdf->id, $pdf->storeId), - ])); - } - Craft::$app->getProjectConfig()->remove(self::CONFIG_PDFS_KEY . '.' . $pdf->uid); - } - - return true; - } - - /** - * Handle email getting deleted. - * - * @throws Throwable - * @throws StaleObjectException - * @since 3.2 - */ - public function handleDeletedPdf(ConfigEvent $event): void - { - $uid = $event->tokenMatches[0]; - $pdfRecord = $this->_getPdfRecord($uid); - - if (!$pdfRecord->id) { - return; - } - - $pdfRecord->delete(); - } - - /** - * @throws ErrorException - * @throws Exception - * @throws NotSupportedException - * @throws ServerErrorHttpException - * @since 3.2 - */ - public function reorderPdfs(array $ids): bool - { - // @TODO Fire BEFORE_REORDER_PDFS / AFTER_REORDER_PDFS events around this loop so plugins can react to PDF sort order changes - // @TODO Align this reorder implementation with how other Commerce features handle reordering (project config-driven, single transaction, consistent event names) - foreach ($ids as $index => $id) { - if ($pdf = $this->getPdfById($id)) { - $pdf->sortOrder = $index + 1; - $this->savePdf($pdf, false); - } - } - - $this->_allPdfs = null; // clear cache - - return true; - } - - /** - * Returns a token-based URL for downloading an order's PDF. - * - * This URL is compatible with the DownloadsController::actionPdf() method - * and includes a secure token for anonymous access. - * - * @param Order $order The order to generate the PDF URL for - * @param string|null $option The option that should be available to the PDF template (e.g. "receipt") - * @param string|null $pdfHandle The handle of the PDF to use. If none is passed the default PDF is used. - * @param bool $inline Whether the PDF should be displayed inline in the browser (default: false) - * @return string The URL to download the order's PDF with a secure token - * @since 4.9.5 - */ - public function getPdfUrl(Order $order, string $option = null, string $pdfHandle = null, bool $inline = false): string - { - // Load the PDF to get its link expiry setting - if ($pdfHandle) { - $pdf = $this->getPdfByHandle($pdfHandle); - } else { - $pdf = $this->getDefaultPdf(); - } - - if (!$pdf) { - throw new \InvalidArgumentException("Can not find a PDF to generate URL."); - } - - $expiryDate = (new \DateTime())->add(new \DateInterval('PT' . $pdf->linkExpiry . 'S')); - - $token = Craft::$app->getTokens()->createToken( - ['commerce/downloads/pdf', ['orderNumber' => $order->number]], - null, - $expiryDate - ); - - // Build the URL parameters - $params = [ - 'number' => $order->number, - 'code' => $token, - ]; - - if ($pdfHandle !== null) { - $params['pdfHandle'] = $pdfHandle; - } - - if ($option) { - $params['option'] = $option; - } - - if ($inline) { - $params['inline'] = true; - } - - $request = Craft::$app->getRequest(); - $isCpRequest = $request->getIsCpRequest(); - - if ($isCpRequest) { - $request->setIsCpRequest(false); - } - - try { - return UrlHelper::actionUrl('commerce/downloads/pdf', $params); - } finally { - if ($isCpRequest) { - $request->setIsCpRequest($isCpRequest); - } - } - } - - /** - * Returns a rendered PDF object for the order. - * - * @param Order $order The order you want passed into the PDFs `order` variable. - * @param string $option A string you want passed into the PDFs `option` variable. - * @param string|null $templatePath The path to the template file in the site templates folder that DOMPDF will use to render the PDF. - * @param array $variables Variables available to the pdf html template. Available to template by the array keys. - * @param Pdf|null $pdf The PDF you want to render. This will override the templatePath argument. - * @return string The PDF data. - * @throws Exception - */ - public function renderPdfForOrder(Order $order, string $option = '', string $templatePath = null, array $variables = [], Pdf $pdf = null): string - { - if ($pdf instanceof Pdf) { - $templatePath = $pdf->templatePath; - } - - if (!$templatePath) { - $templatePath = Plugin::getInstance()->getPdfs()->getDefaultPdf()->templatePath; - } - - // Trigger a 'beforeRenderPdf' event - $event = new PdfRenderEvent([ - 'order' => $order, - 'option' => $option, - 'template' => $templatePath, - 'variables' => $variables, - 'sourcePdf' => $pdf, - ]); - $this->trigger(self::EVENT_BEFORE_RENDER_PDF, $event); - - if ($event->pdf !== null) { - return $event->pdf; - } - - $variables = $event->variables; - $variables['order'] = $event->order; - $variables['option'] = $event->option; - - // Set Craft to the site template mode - $view = Craft::$app->getView(); - $originalLanguage = Craft::$app->language; - $originalFormattingLanguage = Craft::$app->formattingLocale; - $pdfLanguage = $pdf?->getRenderLanguage($order) ?? $originalLanguage; - - // @TODO Fire a BEFORE_SWITCH_PDF_LANGUAGE event here so plugins can override or observe the language used when rendering the PDF - Locale::switchAppLanguage($pdfLanguage); - - $oldTemplateMode = $view->getTemplateMode(); - $view->setTemplateMode(View::TEMPLATE_MODE_SITE); - - if (!$event->template || !$view->doesTemplateExist($event->template)) { - // Restore the original template mode - $view->setTemplateMode($oldTemplateMode); - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - - throw new Exception('PDF template file does not exist.'); - } - - try { - // @TODO Fire a BEFORE_RENDER_PDF_TEMPLATE event around the renderTemplate() call so plugins can inspect or modify variables/template right before HTML is generated - $html = $view->renderTemplate($event->template, $variables); - } catch (\Exception $e) { - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - // Set the pdf html to the render error. - Craft::error('Order PDF render error. Order number: ' . $order->getShortNumber() . '. ' . $e->getMessage()); - Craft::$app->getErrorHandler()->logException($e); - $html = Craft::t('commerce', 'An error occurred while generating this PDF.'); - } - - Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); - // Restore the original template mode - $view->setTemplateMode($oldTemplateMode); - - // Set the config options - $pathService = Craft::$app->getPath(); - $dompdfTempDir = $pathService->getTempPath() . DIRECTORY_SEPARATOR . 'commerce_dompdf'; - $dompdfFontCache = $pathService->getCachePath() . DIRECTORY_SEPARATOR . 'commerce_dompdf'; - $dompdfLogFile = $pathService->getLogPath() . DIRECTORY_SEPARATOR . 'commerce_dompdf.htm'; - - // Ensure directories are created - FileHelper::createDirectory($dompdfTempDir); - FileHelper::createDirectory($dompdfFontCache); - - if (!FileHelper::isWritable($dompdfLogFile)) { - throw new ErrorException("Unable to write to file: $dompdfLogFile"); - } - - if (!FileHelper::isWritable($dompdfFontCache)) { - throw new ErrorException("Unable to write to folder: $dompdfFontCache"); - } - - if (!FileHelper::isWritable($dompdfTempDir)) { - throw new ErrorException("Unable to write to folder: $dompdfTempDir"); - } - - $isRemoteEnabled = Plugin::getInstance()->getSettings()->pdfAllowRemoteImages; - - $options = new Options(); - $options->setTempDir($dompdfTempDir); - $options->setFontCache($dompdfFontCache); - $options->setLogOutputFile($dompdfLogFile); - $options->setIsRemoteEnabled($isRemoteEnabled); - - if ($pdf instanceof Pdf) { - $options->setDefaultPaperOrientation($pdf->paperOrientation); - $options->setDefaultPaperSize($pdf->paperSize); - } - - $renderOptionsEvent = new PdfRenderOptionsEvent([ - 'options' => $options, - ]); - - // Set additional render options - if ($this->hasEventHandlers(self::EVENT_MODIFY_RENDER_OPTIONS)) { - $this->trigger(self::EVENT_MODIFY_RENDER_OPTIONS, $renderOptionsEvent); - } - - // Create and render the PDF - $dompdf = new Dompdf($renderOptionsEvent->options); - $dompdf->loadHtml($html); - $dompdf->render(); - - // Trigger an 'afterRenderPdf' event - $afterEvent = new PdfRenderEvent([ - 'order' => $event->order, - 'option' => $event->option, - 'template' => $event->template, - 'variables' => $variables, - 'pdf' => $dompdf->output(), - 'sourcePdf' => $pdf, - ]); - $this->trigger(self::EVENT_AFTER_RENDER_PDF, $afterEvent); - - return $afterEvent->pdf; - } - - /** - * Gets an PDF record by uid. - * - * @since 3.2 - */ - private function _getPdfRecord(string $uid): PdfRecord - { - if ($pdf = PdfRecord::findOne(['uid' => $uid])) { - return $pdf; - } - - return new PdfRecord(); - } - - /** - * Returns a Query object prepped for retrieving PDFs. - * - * @since 3.2 - */ - private function _createPdfsQuery(): Query - { - $query = (new Query()) - ->select([ - 'description', - 'enabled', - 'fileNameFormat', - 'handle', - 'id', - 'isDefault', - 'language', - 'name', - 'paperOrientation', - 'paperSize', - 'sortOrder', - 'storeId', - 'templatePath', - 'uid', - ]) - ->orderBy('name') - ->from([Table::PDFS]) - ->orderBy(['sortOrder' => SORT_ASC]); - - // @TODO Remove this columnExists check in Commerce 6.0 once the schema guarantees the linkExpiry column on the pdfs table - if (Craft::$app->getDb()->columnExists(Table::PDFS, 'linkExpiry')) { - $query->addSelect('linkExpiry'); - } - - return $query; - } -} diff --git a/src/services/Plans.php b/src/services/Plans.php deleted file mode 100644 index 09964d6060..0000000000 --- a/src/services/Plans.php +++ /dev/null @@ -1,396 +0,0 @@ - - * @since 2.0 - * - * @property array|Plan[] $allEnabledPlans - * @property array|Plan[] $allPlans - */ -class Plans extends Component -{ - /** - * @event PlanEvent The event that is triggered when a plan is archived. - * - * Plugins can get notified whenever a subscription plan is being archived. - * This is useful as sometimes this can be triggered by an action on the gateway. - * - * ```php - * use craft\commerce\events\PlanEvent; - * use craft\commerce\services\Plans; - * use yii\base\Event; - * - * Event::on(Plans::class, Plans::EVENT_ARCHIVE_PLAN, function(PlanEvent $e) { - * // Do something as the plan is being retired. - * }); - * ``` - */ - public const EVENT_ARCHIVE_PLAN = 'archivePlan'; - - /** - * @event PlanEvent The event that is triggered before a plan is saved. - * - * Plugins can get notified before a subscription plan is being saved. - * - * ```php - * use craft\commerce\events\PlanEvent; - * use craft\commerce\services\Plans; - * use yii\base\Event; - * - * Event::on(Plans::class, Plans::EVENT_BEFORE_SAVE_PLAN, function(PlanEvent $e) { - * // Do something - * }); - * ``` - */ - public const EVENT_BEFORE_SAVE_PLAN = 'beforeSavePlan'; - - /** - * @event PlanEvent The event that is triggered after a plan is saved. - * - * Plugins can get notified after a subscription plan is being saved. - * - * ```php - * use craft\commerce\events\PlanEvent; - * use craft\commerce\services\Plans; - * use yii\base\Event; - * - * Event::on(Plans::class, Plans::EVENT_AFTER_SAVE_PLAN, function(PlanEvent $e) { - * // Do something - * }); - * ``` - */ - public const EVENT_AFTER_SAVE_PLAN = 'afterSavePlan'; - - /** - * Memoized array of plans. - * - * @var Plan[]|null - * @since 3.2.8 - */ - private ?array $_allPlans = null; - - /** - * Returns all subscription plans - * - * @return Plan[] - */ - public function getAllPlans(): array - { - return ArrayHelper::where($this->_getAllPlans(), 'isArchived', false); - } - - /** - * Returns all enabled subscription plans - * - * @return Plan[] - * @noinspection PhpUnused - */ - public function getAllEnabledPlans(): array - { - return ArrayHelper::whereMultiple($this->_getAllPlans(), ['enabled' => true, 'isArchived' => false]); - } - - /** - * Return all subscription plans for a gateway. - * - * @return Plan[] - */ - public function getPlansByGatewayId(int $gatewayId): array - { - return ArrayHelper::whereMultiple($this->_getAllPlans(), ['gatewayId' => $gatewayId, 'isArchived' => false]); - } - - /** - * Return all subscription plans for a gateway. - * - * @return Plan[] - * @deprecated in 4.0. Use [[getPlansByGatewayId]] instead. - * @todo remove in Commerce 6.0 - */ - public function getAllGatewayPlans(int $gatewayId): array - { - return $this->getPlansByGatewayId($gatewayId); - } - - /** - * Returns a subscription plan by its id. - * - * @param int $planId The plan id. - */ - public function getPlanById(int $planId): ?Plan - { - return ArrayHelper::firstWhere($this->_getAllPlans(), 'id', $planId); - } - - /** - * Returns a subscription plan by its uid. - * - * @param string $planUid The plan uid. - */ - public function getPlanByUid(string $planUid): ?Plan - { - return ArrayHelper::firstWhere($this->_getAllPlans(), 'uid', $planUid); - } - - /** - * Returns a subscription plan by its handle. - * - * @param string $handle the plan handle - * @noinspection PhpUnused - */ - public function getPlanByHandle(string $handle): ?Plan - { - return ArrayHelper::firstValue(ArrayHelper::whereMultiple($this->_getAllPlans(), ['handle' => $handle, 'isArchived' => false])); - } - - /** - * Returns a subscription plan by its reference. - * - * @param string $reference the plan reference - */ - public function getPlanByReference(string $reference): ?Plan - { - return ArrayHelper::firstWhere($this->_getAllPlans(), 'reference', $reference); - } - - /** - * Returns plans which use the provided Entry for its "information" - * - * @param int $entryId The Entry ID to search by - * @return Plan[] - * @noinspection PhpUnused - */ - public function getPlansByInformationEntryId(int $entryId): array - { - return ArrayHelper::where($this->_getAllPlans(), 'planInformationId', $entryId); - } - - /** - * Save a subscription plan - * - * @param Plan $plan The payment source being saved. - * @param bool $runValidation should we validate this plan before saving. - * @return bool Whether the plan was saved successfully - * @throws InvalidConfigException if subscription plan not found by id. - */ - public function savePlan(Plan $plan, bool $runValidation = true): bool - { - if ($plan->id) { - $record = PlanRecord::findOne($plan->id); - - if (!$record) { - throw new InvalidConfigException(Craft::t('commerce', 'No subscription plan exists with the ID “{id}”', ['id' => $plan->id])); - } - } else { - $record = new PlanRecord(); - } - - // fire a 'beforeSavePlan' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_PLAN)) { - $this->trigger(self::EVENT_BEFORE_SAVE_PLAN, new PlanEvent([ - 'plan' => $plan, - ])); - } - - if ($runValidation && !$plan->validate()) { - Craft::info('Subscription plan not saved due to validation error.', __METHOD__); - - return false; - } - - $record->gatewayId = $plan->gatewayId; - $record->name = $plan->name; - $record->handle = $plan->handle; - $record->planInformationId = $plan->planInformationId; - $record->reference = $plan->reference; - $record->planData = $plan->planData; - $record->enabled = $plan->enabled; - $record->isArchived = $plan->isArchived; - $record->dateArchived = Db::prepareDateForDb($plan->dateArchived); - $record->sortOrder = $plan->sortOrder ?? 99; - - // Save it! - $record->save(false); - - // Now that we have a record ID, save it on the model - $plan->id = $record->id; - - // Fire an 'afterSavePlan' event. - if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_PLAN)) { - $this->trigger(self::EVENT_AFTER_SAVE_PLAN, new PlanEvent([ - 'plan' => $plan, - ])); - } - - // Reset cache/memoization - $this->_allPlans = null; - - return true; - } - - /** - * Archive a subscription plan by its id. - * - * @param int $id The id - * @throws InvalidConfigException - */ - public function archivePlanById(int $id): bool - { - $plan = $this->getPlanById($id); - - if (!$plan) { - return false; - } - - // Fire an 'archivePlan' event. - if ($this->hasEventHandlers(self::EVENT_ARCHIVE_PLAN)) { - $this->trigger(self::EVENT_ARCHIVE_PLAN, new PlanEvent([ - 'plan' => $plan, - ])); - } - - $plan->isArchived = true; - $plan->dateArchived = DateTimeHelper::now(); - - return $this->savePlan($plan); - } - - /** - * Reorders subscription plans by ids. - * - * @param array $ids Array of plans. - * @return bool Always true. - * @throws Exception - */ - public function reorderPlans(array $ids): bool - { - $command = Craft::$app->getDb()->createCommand(); - - foreach ($ids as $planOrder => $planId) { - $command->update(Table::PLANS, ['sortOrder' => $planOrder + 1], ['id' => $planId])->execute(); - } - - // Reset cache/memoization - $this->_allPlans = null; - - return true; - } - - - /** - * Returns a Query object prepped for retrieving gateways. - * - * @return Query The query object. - */ - private function _createPlansQuery(): Query - { - return (new Query()) - ->select([ - 'p.dateArchived', - 'p.dateCreated', - 'p.dateUpdated', - 'p.enabled', - 'p.gatewayId', - 'p.handle', - 'p.id', - 'p.isArchived', - 'p.name', - 'p.planData', - 'p.planInformationId', - 'p.reference', - 'p.sortOrder', - 'p.uid', - ]) - ->leftJoin(['g' => Table::GATEWAYS], '[[g.id]] = [[p.gatewayId]]') - ->where(['g.isArchived' => false]) - ->orderBy(['sortOrder' => SORT_ASC]) - ->from(['p' => Table::PLANS]); - } - - /** - * Populate an array of plans from their database table rows - * - * @return Plan[] - */ - private function _populatePlans(array $results): array - { - $plans = []; - - foreach ($results as $result) { - try { - $plans[] = $this->_populatePlan($result); - } catch (InvalidConfigException) { - continue; // Just skip this - } - } - - return $plans; - } - - /** - * Populate a payment plan model from database table row. - * - * @throws InvalidConfigException if the gateway does not support subscriptions - */ - private function _populatePlan(array $result): Plan - { - $gateway = Plugin::getInstance()->getGateways()->getGatewayById($result['gatewayId']); - - if (!$gateway instanceof SubscriptionGateway) { - throw new InvalidConfigException('This gateway does not support subscriptions'); - } - - $plan = $gateway->getPlanModel(); - - $plan->setAttributes($result, false); - - return $plan; - } - - /** - * Get all plans memoized. - * - * @return array - * @since 3.2.8 - */ - private function _getAllPlans(): array - { - if ($this->_allPlans === null) { - $this->_allPlans = []; - $plans = $this->_createPlansQuery()->all(); - - if (!empty($plans)) { - $plans = $this->_populatePlans($plans); - foreach ($plans as $plan) { - $this->_allPlans[$plan->id] = $plan; - } - } - } - - return $this->_allPlans; - } -} diff --git a/src/services/ProductTypes.php b/src/services/ProductTypes.php deleted file mode 100755 index 31de70e866..0000000000 --- a/src/services/ProductTypes.php +++ /dev/null @@ -1,1069 +0,0 @@ - - * @since 2.0 - */ -class ProductTypes extends Component -{ - /** - * @event ProductTypeEvent The event that is triggered before a product type is saved. - * - * ```php - * use craft\commerce\events\ProductTypeEvent; - * use craft\commerce\services\ProductTypes; - * use craft\commerce\models\ProductType; - * use yii\base\Event; - * - * Event::on( - * ProductTypes::class, - * ProductTypes::EVENT_BEFORE_SAVE_PRODUCTTYPE, - * function(ProductTypeEvent $event) { - * // @var ProductType|null $productType - * $productType = $event->productType; - * - * // Create an audit trail of this action - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_SAVE_PRODUCTTYPE = 'beforeSaveProductType'; - - /** - * @event ProductTypeEvent The event that is triggered after a product type has been saved. - * - * ```php - * use craft\commerce\events\ProductTypeEvent; - * use craft\commerce\services\ProductTypes; - * use craft\commerce\models\ProductType; - * use yii\base\Event; - * - * Event::on( - * ProductTypes::class, - * ProductTypes::EVENT_AFTER_SAVE_PRODUCTTYPE, - * function(ProductTypeEvent $event) { - * // @var ProductType|null $productType - * $productType = $event->productType; - * - * // Prepare some third party system for a new product type - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_SAVE_PRODUCTTYPE = 'afterSaveProductType'; - - public const CONFIG_PRODUCTTYPES_KEY = 'commerce.productTypes'; - - /** - * @var array|null - */ - private ?array $_allProductTypes = null; - - /** - * @var ProductTypeSite[][] - */ - private array $_siteSettingsByProductId = []; - - /** - * @var array interim storage for product types being saved via control panel - */ - private array $_savingProductTypes = []; - - - /** - * Returns all editable product types. - * - * @return ProductType[] An array of all the editable product types. - * @deprecated in 5.7.0. Use [[getViewableProductTypes()]] instead. - */ - public function getEditableProductTypes(): array - { - Craft::$app->getDeprecator()->log(__METHOD__, '`ProductTypes::getEditableProductTypes()` has been deprecated. Use `getViewableProductTypes()` instead.'); - return $this->getViewableProductTypes(); - } - - /** - * Returns all viewable product types. - * - * @return ProductType[] An array of all the viewable product types. - */ - public function getViewableProductTypes(): array - { - if (Craft::$app->getRequest()->getIsConsoleRequest()) { - return $this->getAllProductTypes(); - } - - $user = Craft::$app->getUser()->getIdentity(); - - if (!$user) { - return []; - } - - $viewableProductTypeIds = $this->getViewableProductTypeIds(); - $viewableProductTypes = []; - - foreach ($this->getAllProductTypes() as $productType) { - if (in_array($productType->id, $viewableProductTypeIds)) { - $viewableProductTypes[] = $productType; - } - } - - return $viewableProductTypes; - } - - /** - * Returns all product type IDs that are editable by the current user. - * - * @return array An array of all the editable product types' IDs. - * @deprecated in 5.7.0. Use [[getViewableProductTypeIds()]] instead. - */ - public function getEditableProductTypeIds(bool $anySite = false): array - { - Craft::$app->getDeprecator()->log(__METHOD__, '`ProductTypes::getEditableProductTypeIds()` has been deprecated. Use `getViewableProductTypeIds()` instead.'); - return $this->getViewableProductTypeIds($anySite); - } - - /** - * Returns all product type IDs that are viewable by the current user. - * - * @return array An array of all the viewable product types' IDs. - */ - public function getViewableProductTypeIds(bool $anySite = false): array - { - $viewableIds = []; - $user = Craft::$app->getUser()->getIdentity(); - $allProductTypes = $this->getAllProductTypes(); - - $cpSite = Cp::requestedSite(); - - foreach ($allProductTypes as $productType) { - if (!$user->can('commerce-viewProductType:' . $productType->uid)) { - continue; - } - - if (!$anySite && $cpSite && !isset($productType->getSiteSettings()[$cpSite->id])) { - continue; - } - - $viewableIds[] = $productType->id; - } - - return $viewableIds; - } - - /** - * Returns all product type IDs that are creatable by the current user. - * - * @return array - * @throws InvalidConfigException - */ - public function getCreatableProductTypeIds(): array - { - $creatableIds = []; - $user = Craft::$app->getUser()->getIdentity(); - $allProductTypes = $this->getAllProductTypes(); - - foreach ($allProductTypes as $productType) { - if ($user->can('commerce-createProductType:' . $productType->uid)) { - $creatableIds[] = $productType->id; - } - } - - return $creatableIds; - } - - /** - * Returns all creatable product types. - * @return array - * @throws InvalidConfigException - */ - public function getCreatableProductTypes(): array - { - $creatableProductTypeIds = $this->getCreatableProductTypeIds(); - $creatableProductTypes = []; - - foreach ($this->getAllProductTypes() as $productType) { - if (in_array($productType->id, $creatableProductTypeIds)) { - $creatableProductTypes[] = $productType; - } - } - - return $creatableProductTypes; - } - - /** - * Returns all the product type IDs. - * - * @return array An array of all the product types' IDs. - */ - public function getAllProductTypeIds(): array - { - return collect($this->getAllProductTypes())->pluck('id')->all(); - } - - /** - * Returns all product types. - * - * @return ProductType[] An array of all product types. - */ - public function getAllProductTypes(): array - { - if ($this->_allProductTypes !== null) { - return $this->_allProductTypes; - } - - $this->_allProductTypes = []; - - $results = $this->_createProductTypeQuery()->all(); - foreach ($results as $result) { - $this->_allProductTypes[] = new ProductType($result); - } - - return $this->_allProductTypes; - } - - /** - * Returns a product type by its handle. - * - * @param string $handle The product type's handle. - * @return ProductType|null The product type or `null`. - */ - public function getProductTypeByHandle(string $handle): ?ProductType - { - return collect($this->getAllProductTypes())->where('handle', $handle)->first(); - } - - /** - * Returns an array of product type site settings for a product type by its ID. - * - * @param int $productTypeId the product type ID - * @return array The product type settings. - */ - public function getProductTypeSites(int $productTypeId): array - { - $db = Craft::$app->getDb(); - if (!isset($this->_siteSettingsByProductId[$productTypeId])) { - $query = (new Query()) - ->select([ - 'hasUrls', - 'id', - 'productTypeId', - 'siteId', - 'template', - 'uriFormat', - ]) - ->from(Table::PRODUCTTYPES_SITES) - ->where(['productTypeId' => $productTypeId]); - - if ($db->columnExists(Table::PRODUCTTYPES_SITES, 'enabledByDefault')) { - $query->addSelect('enabledByDefault'); - } - - $rows = $query->all(); - - $this->_siteSettingsByProductId[$productTypeId] = []; - - foreach ($rows as $row) { - $this->_siteSettingsByProductId[$productTypeId][] = new ProductTypeSite($row); - } - } - - return $this->_siteSettingsByProductId[$productTypeId]; - } - - /** - * Saves a product type. - * - * @param ProductType $productType The product type model. - * @param bool $runValidation If validation should be ran. - * @return bool Whether the product type was saved successfully. - * @throws Throwable if reasons - */ - public function saveProductType(ProductType $productType, bool $runValidation = true): bool - { - $isNewProductType = !$productType->id; - - // Fire a 'beforeSaveProductType' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_PRODUCTTYPE)) { - $this->trigger(self::EVENT_BEFORE_SAVE_PRODUCTTYPE, new ProductTypeEvent([ - 'productType' => $productType, - 'isNew' => $isNewProductType, - ])); - } - - if ($runValidation && !$productType->validate()) { - Craft::info('Product type not saved due to validation error.', __METHOD__); - - return false; - } - - if ($isNewProductType) { - $productType->uid = StringHelper::UUID(); - } else { - /** @var ProductTypeRecord|null $existingProductTypeRecord */ - $existingProductTypeRecord = ProductTypeRecord::find() - ->where(['id' => $productType->id]) - ->one(); - - if (!$existingProductTypeRecord) { - throw new ProductTypeNotFoundException("No product type exists with the ID '$productType->id'"); - } - - $productType->uid = $existingProductTypeRecord->uid; - } - - $this->_savingProductTypes[$productType->uid] = $productType; - - $projectConfig = Craft::$app->getProjectConfig(); - - $configData = $productType->getConfig(); - - $configPath = self::CONFIG_PRODUCTTYPES_KEY . '.' . $productType->uid; - $projectConfig->set($configPath, $configData); - - if ($isNewProductType) { - $productType->id = Db::idByUid(Table::PRODUCTTYPES, $productType->uid); - } - - return true; - } - - /** - * Handle a product type change. - * - * @throws Throwable if reasons - */ - public function handleChangedProductType(ConfigEvent $event): void - { - $productTypeUid = $event->tokenMatches[0]; - $data = $event->newValue; - $shouldResaveProducts = false; - - // Make sure fields and sites are processed - ProjectConfigHelper::ensureAllSitesProcessed(); - ProjectConfigHelper::ensureAllFieldsProcessed(); - - $db = Craft::$app->getDb(); - $transaction = $db->beginTransaction(); - - try { - $siteData = $data['siteSettings']; - - // Basic data - $productTypeRecord = $this->_getProductTypeRecord($productTypeUid); - $isNewProductType = $productTypeRecord->getIsNewRecord(); - $fieldsService = Craft::$app->getFields(); - - $productTypeRecord->uid = $productTypeUid; - $productTypeRecord->name = $data['name']; - $productTypeRecord->handle = $data['handle']; - $productTypeRecord->enableVersioning = $data['enableVersioning'] ?? false; - $productTypeRecord->hasDimensions = $data['hasDimensions']; - - $productTypeRecord->productTitleTranslationMethod = $data['productTitleTranslationMethod'] ?? 'site'; - $productTypeRecord->productTitleTranslationKeyFormat = $data['productTitleTranslationKeyFormat'] ?? ''; - - $productTypeRecord->propagationMethod = $data['propagationMethod'] ?? PropagationMethod::All->value; - - // Resave products if propagation method has changed - if ($productTypeRecord->propagationMethod != $productTypeRecord->getOldAttribute('propagationMethod')) { - $shouldResaveProducts = true; - } - - $productTypeRecord->variantTitleTranslationMethod = $data['variantTitleTranslationMethod'] ?? 'site'; - $productTypeRecord->variantTitleTranslationKeyFormat = $data['variantTitleTranslationKeyFormat'] ?? ''; - - // Variant title fields - $hasVariantTitleField = $data['hasVariantTitleField']; - $variantTitleFormat = $data['variantTitleFormat'] ?? '{product.title}'; - if ($productTypeRecord->variantTitleFormat != $variantTitleFormat || - $productTypeRecord->hasVariantTitleField != $hasVariantTitleField) { - $shouldResaveProducts = true; - } - $productTypeRecord->variantTitleFormat = $variantTitleFormat; - $productTypeRecord->hasVariantTitleField = $hasVariantTitleField; - $productTypeRecord->variantUiLabelFormat = $data['variantUiLabelFormat'] ?? '{title}'; - - // Product title fields - $hasProductTitleField = $data['hasProductTitleField']; - $productTitleFormat = $data['productTitleFormat'] ?? 'Title'; - if ($productTypeRecord->productTitleFormat != $productTitleFormat || - $productTypeRecord->hasProductTitleField != $hasProductTitleField) { - $shouldResaveProducts = true; - } - $productTypeRecord->productTitleFormat = $productTitleFormat; - $productTypeRecord->hasProductTitleField = $hasProductTitleField; - $productTypeRecord->productUiLabelFormat = $data['productUiLabelFormat'] ?? '{title}'; - - // Slug fields - $productTypeRecord->showSlugField = $data['showSlugField'] ?? true; - $productTypeRecord->slugTranslationMethod = $data['slugTranslationMethod'] ?? 'site'; - $productTypeRecord->slugTranslationKeyFormat = $data['slugTranslationKeyFormat'] ?? null; - - if ($productTypeRecord->maxVariants != $data['maxVariants']) { - $shouldResaveProducts = true; - } - $productTypeRecord->maxVariants = $data['maxVariants']; - - $skuFormat = $data['skuFormat'] ?? ''; - if ($productTypeRecord->skuFormat != $skuFormat) { - $shouldResaveProducts = true; - } - $productTypeRecord->skuFormat = $skuFormat; - - $descriptionFormat = $data['descriptionFormat'] ?? ''; - if ($productTypeRecord->descriptionFormat != $descriptionFormat) { - $shouldResaveProducts = true; - } - $productTypeRecord->descriptionFormat = $descriptionFormat; - $productTypeRecord->isStructure = $data['isStructure'] ?? false; - $productTypeRecord->maxLevels = $data['maxLevels'] ?? null; - $productTypeRecord->defaultPlacement = $data['defaultPlacement'] ?? ProductType::DEFAULT_PLACEMENT_BEGINNING; - if ($productTypeRecord->isStructure != $productTypeRecord->getOldAttribute('isStructure')) { - $shouldResaveProducts = true; - } - - // Preview targets - if (!empty($data['previewTargets'])) { - $productTypeRecord->previewTargets = ProjectConfigHelper::unpackAssociativeArray($data['previewTargets']); - } else { - $productTypeRecord->previewTargets = null; - } - - if (!empty($data['productFieldLayouts']) && !empty($config = reset($data['productFieldLayouts']))) { - // Save the main field layout - $layout = FieldLayout::createFromConfig($config); - $layout->id = $productTypeRecord->fieldLayoutId; - $layout->type = Product::class; - $layout->uid = key($data['productFieldLayouts']); - $fieldsService->saveLayout($layout, false); - $productTypeRecord->fieldLayoutId = $layout->id; - } elseif ($productTypeRecord->fieldLayoutId) { - // Delete the main field layout - $fieldsService->deleteLayoutById($productTypeRecord->fieldLayoutId); - $productTypeRecord->fieldLayoutId = null; - } - - if (!empty($data['variantFieldLayouts']) && !empty($config = reset($data['variantFieldLayouts']))) { - // Save the variant field layout - $layout = FieldLayout::createFromConfig($config); - $layout->id = $productTypeRecord->variantFieldLayoutId; - $layout->type = Variant::class; - $layout->uid = key($data['variantFieldLayouts']); - $fieldsService->saveLayout($layout, false); - $productTypeRecord->variantFieldLayoutId = $layout->id; - } elseif ($productTypeRecord->variantFieldLayoutId) { - // Delete the variant field layout - $fieldsService->deleteLayoutById($productTypeRecord->variantFieldLayoutId); - $productTypeRecord->variantFieldLayoutId = null; - } - - if ($productTypeRecord->isStructure) { - // Save the structure - $structureUid = $data['structure']['uid']; - $structure = Craft::$app->getStructures()->getStructureByUid($structureUid, true) ?? new Structure(['uid' => $structureUid]); - $isNewStructure = empty($structure->id); - $structure->maxLevels = $data['maxLevels'] ?? null; - Craft::$app->getStructures()->saveStructure($structure); - $productTypeRecord->structureId = $structure->id; - } else { - if ($productTypeRecord->structureId) { - // Delete the old one - Craft::$app->getStructures()->deleteStructureById($productTypeRecord->structureId); - } - - $productTypeRecord->structureId = null; - $isNewStructure = false; - } - - $productTypeRecord->save(false); - - // Update the site settings - // ----------------------------------------------------------------- - - $sitesNowWithoutUrls = []; - $sitesWithNewUriFormats = []; - /** @var array $allOldSiteSettingsRecords */ - $allOldSiteSettingsRecords = []; - - if (!$isNewProductType) { - /** @var array $allOldSiteSettingsRecords */ - $allOldSiteSettingsRecords = ProductTypeSiteRecord::find() - ->where(['productTypeId' => $productTypeRecord->id]) - ->indexBy('siteId') - ->all(); - } - - $siteIdMap = Db::idsByUids('{{%sites}}', array_keys($siteData)); - - /** @var array $siteSettings */ - foreach ($siteData as $siteUid => $siteSettings) { - $siteId = $siteIdMap[$siteUid]; - - // Was this already selected? - if (!$isNewProductType && isset($allOldSiteSettingsRecords[$siteId])) { - $siteSettingsRecord = $allOldSiteSettingsRecords[$siteId]; - } else { - $siteSettingsRecord = new ProductTypeSiteRecord(); - $siteSettingsRecord->productTypeId = $productTypeRecord->id; - $siteSettingsRecord->siteId = $siteId; - } - - $siteSettingsRecord->enabledByDefault = (bool)($siteSettings['enabledByDefault'] ?? true); - - if ($siteSettingsRecord->hasUrls = $siteSettings['hasUrls']) { - $siteSettingsRecord->uriFormat = $siteSettings['uriFormat']; - $siteSettingsRecord->template = $siteSettings['template']; - } else { - $siteSettingsRecord->uriFormat = null; - $siteSettingsRecord->template = null; - } - - if (!$siteSettingsRecord->getIsNewRecord()) { - // Did it used to have URLs, but not anymore? - if ($siteSettingsRecord->isAttributeChanged('hasUrls', false) && !$siteSettings['hasUrls']) { - $sitesNowWithoutUrls[] = $siteId; - } - - // Does it have URLs, and has its URI format changed? - if ($siteSettings['hasUrls'] && $siteSettingsRecord->isAttributeChanged('uriFormat', false)) { - $sitesWithNewUriFormats[] = $siteId; - } - } - - $siteSettingsRecord->save(false); - } - - if (!$isNewProductType) { - // Drop any site settings that are no longer being used, as well as the associated product/element - // site rows - $affectedSiteUids = array_keys($siteData); - - foreach ($allOldSiteSettingsRecords as $siteId => $siteSettingsRecord) { - $siteUid = array_search($siteId, $siteIdMap, false); - if (!in_array($siteUid, $affectedSiteUids, false)) { - $siteSettingsRecord->delete(); - $shouldResaveProducts = true; - } - } - } - - // If the section was just converted to a Structure, - // add the existing entries to the structure - // ----------------------------------------------------------------- - - if ( - $productTypeRecord->isStructure && - !$isNewProductType && - $isNewStructure - ) { - $this->_populateNewStructure($productTypeRecord); - } - - // Finally, deal with the existing products... - // ----------------------------------------------------------------- - - if (!$isNewProductType) { - // Get all the product IDs in this group - $productIds = Product::find() - ->typeId($productTypeRecord->id) - ->status(null) - ->limit(null) - ->ids(); - - // Are there any sites left? - if (!empty($siteData)) { - // Drop the old product URIs for any site settings that don't have URLs - if (!empty($sitesNowWithoutUrls)) { - $db->createCommand() - ->update( - '{{%elements_sites}}', - ['uri' => null], - [ - 'elementId' => $productIds, - 'siteId' => $sitesNowWithoutUrls, - ]) - ->execute(); - } elseif (!empty($sitesWithNewUriFormats)) { - foreach ($productIds as $productId) { - App::maxPowerCaptain(); - - // Loop through each of the changed sites and update all of the products' slugs and - // URIs - foreach ($sitesWithNewUriFormats as $siteId) { - $product = Product::find() - ->id($productId) - ->siteId($siteId) - ->status(null) - ->one(); - - if ($product) { - Craft::$app->getElements()->updateElementSlugAndUri($product, false, false); - } - } - } - } - } - } - - $transaction->commit(); - - if ($shouldResaveProducts) { - Craft::$app->getQueue()->push(new ResaveElements([ - 'elementType' => Product::class, - 'criteria' => [ - 'siteId' => '*', - 'status' => null, - 'typeId' => $productTypeRecord->id, - ], - ])); - } - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - - // Clear caches - $this->_allProductTypes = null; - unset($this->_siteSettingsByProductId[$productTypeRecord->id]); - - // Fire an 'afterSaveProductType' event - if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_PRODUCTTYPE)) { - $this->trigger(self::EVENT_AFTER_SAVE_PRODUCTTYPE, new ProductTypeEvent([ - 'productType' => $this->getProductTypeById($productTypeRecord->id), - 'isNew' => empty($this->_savingProductTypes[$productTypeUid]), - ])); - } - } - - /** - * Adds existing products to a newly-created structure, if the product type was just converted to Orderable. - * - * @param ProductTypeRecord $productTypeRecord - * @throws Exception if reasons - * @see saveProductType() - */ - private function _populateNewStructure(ProductTypeRecord $productTypeRecord): void - { - // Add all the products to the structure - $query = Product::find() - ->typeId($productTypeRecord->id) - ->drafts(null) - ->draftOf(false) - ->site('*') - ->unique() - ->status(null) - ->orderBy(['id' => SORT_ASC]) - ->withStructure(false); - - $structuresService = Craft::$app->getStructures(); - - foreach (Db::each($query) as $product) { - /** @var Product $product */ - $structuresService->appendToRoot($productTypeRecord->structureId, $product, Structures::MODE_INSERT); - } - } - - /** - * Returns all product types by a tax category id. - */ - public function getProductTypesByTaxCategoryId(int $taxCategoryId): array - { - $rows = $this->_createProductTypeQuery() - ->innerJoin(Table::PRODUCTTYPES_TAXCATEGORIES . ' productTypeTaxCategories', '[[productTypes.id]] = [[productTypeTaxCategories.productTypeId]]') - ->where(['productTypeTaxCategories.taxCategoryId' => $taxCategoryId]) - ->all(); - - $productTypes = []; - - foreach ($rows as $row) { - $productTypes[$row['id']] = new ProductType($row); - } - - return $productTypes; - } - - /** - * Returns all product types by a shipping category id. - */ - public function getProductTypesByShippingCategoryId(int $shippingCategoryId): array - { - $rows = $this->_createProductTypeQuery() - ->innerJoin(Table::PRODUCTTYPES_SHIPPINGCATEGORIES . ' productTypeShippingCategories', '[[productTypes.id]] = [[productTypeShippingCategories.productTypeId]]') - ->where(['productTypeShippingCategories.shippingCategoryId' => $shippingCategoryId]) - ->all(); - - $productTypes = []; - - foreach ($rows as $row) { - $productTypes[$row['id']] = new ProductType($row); - } - - return $productTypes; - } - - /** - * Deletes a product type by its ID. - * - * @param int $id the product type's ID - * @return bool Whether the product type was deleted successfully. - * @throws Throwable if reasons - */ - public function deleteProductTypeById(int $id): bool - { - $productType = $this->getProductTypeById($id); - Craft::$app->getProjectConfig()->remove(self::CONFIG_PRODUCTTYPES_KEY . '.' . $productType->uid); - return true; - } - - /** - * Handle a product type getting deleted. - * - * @throws Throwable if reasons - */ - public function handleDeletedProductType(ConfigEvent $event): void - { - $uid = $event->tokenMatches[0]; - $productTypeRecord = $this->_getProductTypeRecord($uid); - - if (!$productTypeRecord->id) { - return; - } - - $db = Craft::$app->getDb(); - $transaction = $db->beginTransaction(); - - try { - $products = Product::find() - ->typeId($productTypeRecord->id) - ->status(null) - ->limit(null) - ->all(); - - foreach ($products as $product) { - Craft::$app->getElements()->deleteElement($product); - } - - $fieldLayoutId = $productTypeRecord->fieldLayoutId; - $variantFieldLayoutId = $productTypeRecord->variantFieldLayoutId; - Craft::$app->getFields()->deleteLayoutById($fieldLayoutId); - - if ($variantFieldLayoutId) { - Craft::$app->getFields()->deleteLayoutById($variantFieldLayoutId); - } - - $productTypeRecord->delete(); - $transaction->commit(); - } catch (Throwable $e) { - $transaction->rollBack(); - - throw $e; - } - - // Clear caches - $this->_allProductTypes = null; - unset($this->_siteSettingsByProductId[$productTypeRecord->id]); - } - - /** - * Prune a deleted site from category group site settings. - */ - public function pruneDeletedSite(DeleteSiteEvent $event): void - { - $siteUid = $event->site->uid; - - $projectConfig = Craft::$app->getProjectConfig(); - $productTypes = $projectConfig->get(self::CONFIG_PRODUCTTYPES_KEY); - - // Loop through the product types and prune the UID from field layouts. - if (is_array($productTypes)) { - foreach ($productTypes as $productTypeUid => $productType) { - $projectConfig->remove(self::CONFIG_PRODUCTTYPES_KEY . '.' . $productTypeUid . '.siteSettings.' . $siteUid); - } - } - } - - /** - * @deprecated in 3.4.17. Unused fields will be pruned automatically as field layouts are resaved. - */ - public function pruneDeletedField(): void - { - } - - /** - * Returns a product type by its ID. - * - * @param int $productTypeId the product type's ID - * @return ProductType|null either the product type or `null` - */ - public function getProductTypeById(int $productTypeId): ?ProductType - { - return collect($this->getAllProductTypes())->where('id', $productTypeId)->first(); - } - - /** - * Returns a product type by its UID. - * - * @param string $uid the product type's UID - * @return ProductType|null either the product type or `null` - */ - public function getProductTypeByUid(string $uid): ?ProductType - { - return collect($this->getAllProductTypes())->where('uid', $uid)->first(); - } - - /** - * Returns whether a product type's products have URLs, and if the template path is valid. - * - * @param ProductType $productType The product for which to validate the template. - * @param int $siteId The site for which to valid for - * @return bool Whether the template is valid. - * @throws Exception - */ - public function isProductTypeTemplateValid(ProductType $productType, int $siteId): bool - { - $productTypeSiteSettings = $productType->getSiteSettings(); - - if (isset($productTypeSiteSettings[$siteId]) && $productTypeSiteSettings[$siteId]->hasUrls && $productTypeSiteSettings[$siteId]->template) { - // Set Craft to the site template mode - $view = Craft::$app->getView(); - $oldTemplateMode = $view->getTemplateMode(); - $view->setTemplateMode($view::TEMPLATE_MODE_SITE); - - // Does the template exist? - $templateExists = Craft::$app->getView()->doesTemplateExist($productTypeSiteSettings[$siteId]->template); - - // Restore the original template mode - $view->setTemplateMode($oldTemplateMode); - - if ($templateExists) { - return true; - } - } - - return false; - } - - /** - * Adds a new product type setting row when a Site is added to Craft. - * - * @param SiteEvent $event The event that triggered this. - * @throws Exception - * @throws ErrorException - * @throws InvalidConfigException - * @throws NotSupportedException - * @throws ServerErrorHttpException - */ - public function afterSaveSiteHandler(SiteEvent $event): void - { - if ($event->isNew && isset($event->oldPrimarySiteId)) { - $oldPrimarySiteUid = Db::uidById(CraftTable::SITES, $event->oldPrimarySiteId); - $projectConfig = Craft::$app->getProjectConfig(); - $existingProductTypeSettings = $projectConfig->get(self::CONFIG_PRODUCTTYPES_KEY); - - if (!$projectConfig->getIsApplyingExternalChanges() && is_array($existingProductTypeSettings)) { - foreach ($existingProductTypeSettings as $productTypeUid => $settings) { - $primarySiteSettings = $settings['siteSettings'][$oldPrimarySiteUid] ?? null; - if ($primarySiteSettings === null) { - continue; - } - - $configPath = self::CONFIG_PRODUCTTYPES_KEY . '.' . $productTypeUid . '.siteSettings.' . $event->site->uid; - $projectConfig->set($configPath, $primarySiteSettings); - } - } - } - } - - /** - * Returns a Query object prepped for retrieving purchasables. - * - * @return Query The query object. - */ - private function _createProductTypeQuery(): Query - { - $query = (new Query()) - ->select([ - 'productTypes.descriptionFormat', - 'productTypes.fieldLayoutId', - 'productTypes.handle', - 'productTypes.hasDimensions', - 'productTypes.hasProductTitleField', - 'productTypes.hasVariantTitleField', - 'productTypes.id', - 'productTypes.name', - 'productTypes.maxVariants', - 'productTypes.productTitleFormat', - 'productTypes.skuFormat', - 'productTypes.uid', - 'productTypes.variantFieldLayoutId', - ]) - ->from([Table::PRODUCTTYPES . ' productTypes']); - - // @TODO Remove this columnExists check in Commerce 6.0 once the schema guarantees the `variantTitleFormat` column on the producttypes table (was renamed from `titleFormat`) - $db = Craft::$app->getDb(); - if ($db->columnExists(Table::PRODUCTTYPES, 'variantTitleFormat')) { - $query->addSelect('productTypes.variantTitleFormat'); - } else { - $query->addSelect('productTypes.titleFormat'); - } - - /** @since 5.0 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'enableVersioning')) { - $query->addSelect('productTypes.enableVersioning'); - } - - /** @since 5.2 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'isStructure')) { - $query->addSelect('productTypes.isStructure'); - $query->addSelect('productTypes.maxLevels'); - } - - /** @since 5.2 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'defaultPlacement')) { - $query->addSelect('productTypes.defaultPlacement'); - } - - /** @since 5.2 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'structureId')) { - $query->addSelect('productTypes.structureId'); - } - - /** @since 5.1 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'productTitleTranslationMethod')) { - $query->addSelect('productTypes.productTitleTranslationMethod'); - } - - /** @since 5.1 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'productTitleTranslationKeyFormat')) { - $query->addSelect('productTypes.productTitleTranslationKeyFormat'); - } - - if ($db->columnExists(Table::PRODUCTTYPES, 'variantTitleTranslationMethod')) { - $query->addSelect('productTypes.variantTitleTranslationMethod'); - } - - /** @since 5.1 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'variantTitleTranslationKeyFormat')) { - $query->addSelect('productTypes.variantTitleTranslationKeyFormat'); - } - - /** @since 5.1 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'propagationMethod')) { - $query->addSelect('productTypes.propagationMethod'); - } - - /** @since 5.5 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'showSlugField')) { - $query->addSelect('productTypes.showSlugField'); - } - - /** @since 5.5 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'slugTranslationMethod')) { - $query->addSelect('productTypes.slugTranslationMethod'); - } - - /** @since 5.5 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'slugTranslationKeyFormat')) { - $query->addSelect('productTypes.slugTranslationKeyFormat'); - } - - /** @since 5.5 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'previewTargets')) { - $query->addSelect('productTypes.previewTargets'); - } - - /** @since 5.6 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'variantUiLabelFormat')) { - $query->addSelect('productTypes.variantUiLabelFormat'); - } - - /** @since 5.6 */ - if ($db->columnExists(Table::PRODUCTTYPES, 'productUiLabelFormat')) { - $query->addSelect('productTypes.productUiLabelFormat'); - } - - return $query; - } - - /** - * Gets a product type's record by uid. - */ - private function _getProductTypeRecord(string $uid): ProductTypeRecord - { - if ($productType = ProductTypeRecord::findOne(['uid' => $uid])) { - return $productType; - } - - return new ProductTypeRecord(); - } - - /** - * Check if user has product type permission. - * - * @param User $user - * @param ProductType $productType - * @param string|null $checkPermissionName detailed product type permission. - * @return bool - * @deprecated in 5.7.0. Use `$user->can()` directly instead. - */ - public function hasPermission(User $user, ProductType $productType, ?string $checkPermissionName = null): bool - { - Craft::$app->getDeprecator()->log(__METHOD__, '`ProductTypes::hasPermission()` has been deprecated. Use `$user->can()` directly instead. Note that permission names have changed: `commerce-editProductType:{uid}` is now `commerce-viewProductType:{uid}` and `commerce-saveProductType:{uid}`; `commerce-createProducts:{uid}` is now `commerce-createProductType:{uid}`; `commerce-deleteProducts:{uid}` is now `commerce-deleteProductType:{uid}`.'); - - if ($checkPermissionName !== null) { - return $user->can($checkPermissionName . ':' . $productType->uid); - } - - return $user->can('commerce-viewProductType:' . $productType->uid); - } -} diff --git a/src/services/Products.php b/src/services/Products.php deleted file mode 100644 index 02d500161f..0000000000 --- a/src/services/Products.php +++ /dev/null @@ -1,73 +0,0 @@ - - * @since 2.0 - */ -class Products extends Component -{ - /** - * Returns a product by its ID. - * - * @param int $id - * @param array|int|string|null $siteId - * @return Product|null - */ - public function getProductById(int $id, array|int|string $siteId = null, array $criteria = []): ?Product - { - if (!$id) { - return null; - } - - // Get the structure ID - if (!isset($criteria['structureId'])) { - $criteria['structureId'] = (new Query()) - ->select(['productTypes.structureId']) - ->from(['products' => \craft\commerce\db\Table::PRODUCTS]) - ->innerJoin(['productTypes' => \craft\commerce\db\Table::PRODUCTTYPES], '[[productTypes.id]] = [[products.typeId]]') - ->where(['products.id' => $id]) - ->scalar(); - } - - return Craft::$app->getElements()->getElementById($id, Product::class, $siteId, $criteria); - } - - /** - * Handle a Site being saved. - */ - public function afterSaveSiteHandler(SiteEvent $event): void - { - if ( - $event->isNew && - isset($event->oldPrimarySiteId) && - Craft::$app->getPlugins()->isPluginInstalled(Plugin::getInstance()->id) - ) { - Queue::push(new PropagateElements([ - 'elementType' => Product::class, - 'criteria' => [ - 'siteId' => $event->oldPrimarySiteId, - 'status' => null, - ], - 'siteId' => $event->site->id, - ])); - } - } -} diff --git a/src/services/Purchasables.php b/src/services/Purchasables.php deleted file mode 100644 index 64ec30af61..0000000000 --- a/src/services/Purchasables.php +++ /dev/null @@ -1,322 +0,0 @@ - - * @since 2.0 - * - * @property array|string[] $allPurchasableElementTypes - */ -class Purchasables extends Component -{ - /** - * @event PurchasableOutOfStockPurchasesAllowedEvent The event that is triggered when checking if the purchasable can be purchased when out of stock. - * - * This example allows users of a certain group to purchase out of stock items. - * - * ```php - * use craft\commerce\events\PurchasableAvailableEvent; - * use craft\commerce\services\Purchasables; - * use yii\base\Event; - * - * Event::on( - * Purchasables::class, - * Purchasables::EVENT_PURCHASABLE_ALLOW_OUT_OF_STOCK_PURCHASES, - * function(PurchasableOutOfStockPurchasesAllowedEvent $event) { - * if($order && $user = $order->getUser()){ - * if($user->isInGroup(1)){ - * $event->outOfStockPurchasesAllowed = true; - * } - * } - * } - * ); - * ``` - */ - public const EVENT_PURCHASABLE_OUT_OF_STOCK_PURCHASES_ALLOWED = 'allowOutOfStockPurchases'; - - /** - * @event PurchasableAvailableEvent The event that is triggered when the availability of a purchasables is checked. - * - * This example stop users of a certain group from having the purchasable be available to them in their order. - * - * ```php - * use craft\commerce\events\PurchasableAvailableEvent; - * use craft\commerce\services\Purchasables; - * use yii\base\Event; - * - * Event::on( - * Purchasables::class, - * Purchasables::EVENT_PURCHASABLE_AVAILABLE, - * function(PurchasableAvailableEvent $event) { - * if($order && $user = $order->getUser()){ - * $event->isAvailable = $event->isAvailable && !$user->isInGroup(1); // Group ID 1 not allowed to have purchasable in the cart. - * } - * } - * ); - * ``` - */ - public const EVENT_PURCHASABLE_AVAILABLE = 'purchasableAvailable'; - - /** - * @event PurchasableShippableEvent The event that is triggered when determining whether a purchasable may be shipped. - * - * This example prevents the purchasable from being shippable in a specific user group's orders: - * - * ```php - * use craft\commerce\events\PurchasableShippableEvent; - * use craft\commerce\services\Purchasables; - * use yii\base\Event; - * - * Event::on( - * Purchasables::class, - * Purchasables::EVENT_PURCHASABLE_SHIPPABLE, - * function(PurchasableShippableEvent $event) { - * if($order && $user = $order->getUser()){ - * $event->isShippable = $event->is && !$user->isInGroup(1); - * } - * } - * ); - * ``` - */ - public const EVENT_PURCHASABLE_SHIPPABLE = 'purchasableShippable'; - - /** - * @event RegisterComponentTypesEvent The event that is triggered for registration of additional purchasables. - * - * This example adds an instance of `MyPurchasable` to the event object’s `types` array: - * - * ```php - * use craft\events\RegisterComponentTypesEvent; - * use craft\commerce\services\Purchasables; - * use yii\base\Event; - * - * Event::on( - * Purchasables::class, - * Purchasables::EVENT_REGISTER_PURCHASABLE_ELEMENT_TYPES, - * function(RegisterComponentTypesEvent $event) { - * $event->types[] = MyPurchasable::class; - * } - * ); - * ``` - */ - public const EVENT_REGISTER_PURCHASABLE_ELEMENT_TYPES = 'registerPurchasableElementTypes'; - - /** - * Memoization of purchasables by ID to avoid duplicate queries. - * - * @var Collection|null - */ - private ?Collection $_purchasableById = null; - - - /** - * @param Purchasable $purchasable - * @param Order|null $order - * @param User|null $currentUser - * @return bool - * @throws Throwable - * @since 5.3.0 - */ - public function isPurchasableOutOfStockPurchasingAllowed(Purchasable $purchasable, Order $order = null, User $currentUser = null): bool - { - if ($currentUser === null) { - $currentUser = Craft::$app->getUser()->getIdentity(); - } - - $outOfStockPurchasesAllowed = $purchasable->allowOutOfStockPurchases; - - $event = new PurchasableOutOfStockPurchasesAllowedEvent(compact('order', 'purchasable', 'currentUser', 'outOfStockPurchasesAllowed')); - - if ($this->hasEventHandlers(self::EVENT_PURCHASABLE_OUT_OF_STOCK_PURCHASES_ALLOWED)) { - $this->trigger(self::EVENT_PURCHASABLE_OUT_OF_STOCK_PURCHASES_ALLOWED, $event); - } - - return $event->outOfStockPurchasesAllowed; - } - - /** - * @param Order|null $order - * @param User|null $currentUser - * @since 3.3.1 - */ - public function isPurchasableAvailable(PurchasableInterface $purchasable, Order $order = null, User $currentUser = null): bool - { - if ($currentUser === null) { - $currentUser = Craft::$app->getUser()->getIdentity(); - } - $isAvailable = $purchasable->getIsAvailable(); - - $event = new PurchasableAvailableEvent(compact('order', 'purchasable', 'currentUser', 'isAvailable')); - - if ($this->hasEventHandlers(self::EVENT_PURCHASABLE_AVAILABLE)) { - $this->trigger(self::EVENT_PURCHASABLE_AVAILABLE, $event); - } - - return $event->isAvailable; - } - - /** - * @param Order|null $order - * @param User|null $currentUser - * @since 3.3.2 - */ - public function isPurchasableShippable(PurchasableInterface $purchasable, Order $order = null, User $currentUser = null): bool - { - if ($currentUser === null) { - $currentUser = Craft::$app->getUser()->getIdentity(); - } - $isShippable = $purchasable->getIsShippable(); - - $event = new PurchasableShippableEvent(compact('order', 'purchasable', 'currentUser', 'isShippable')); - - if ($this->hasEventHandlers(self::EVENT_PURCHASABLE_SHIPPABLE)) { - $this->trigger(self::EVENT_PURCHASABLE_SHIPPABLE, $event); - } - - return $event->isShippable; - } - - /** - * Updated the cached stock value for the purchasable in a store. - * - * @param Purchasable $purchasable - * @param bool $allSites Update across all sites (stores). - * @return void - * @throws \yii\base\InvalidConfigException - * @throws \yii\db\Exception - */ - public function updateStoreStockCache(Purchasable $purchasable, bool $allSites = false): void - { - if ($allSites) { - $purchasables = $purchasable::find() - ->siteId('*') - ->id($purchasable->id) - ->status(null)->all(); - } else { - $purchasables = [$purchasable]; - } - - /** @var Purchasable $purchasable */ - foreach ($purchasables as $purchasable) { - $stock = Plugin::getInstance()->getInventory()->getInventoryLevelsForPurchasable($purchasable)->sum('availableTotal'); - - Craft::$app->getDb()->createCommand() - ->update( - table: Table::PURCHASABLES_STORES, - columns: ['stock' => $stock], - condition: ['purchasableId' => $purchasable->id, 'storeId' => $purchasable->getStore()->id]) - ->execute(); - - // Since we are updating the stock directly in the database, clear the cache - Craft::$app->getElements()->invalidateCachesForElement($purchasable); - } - } - - /** - * Delete a purchasable by its ID. - * - * @throws Throwable - * @noinspection PhpUnused - */ - public function deletePurchasableById(int $purchasableId): bool - { - $this->_purchasableById?->pull($purchasableId); - - return Craft::$app->getElements()->deleteElementById($purchasableId); - } - - /** - * Get a purchasable by its ID. - * - * @param int $purchasableId - * @param int|null $siteId - * @param int|false|null $forCustomer - * @return PurchasableInterface|null - * @throws SiteNotFoundException - */ - public function getPurchasableById(int $purchasableId, ?int $siteId = null, int|false|null $forCustomer = null): ?PurchasableInterface - { - // @TODO Verify that returning the memoized purchasable regardless of the requested $siteId / $forCustomer is safe, or scope the cache key by those args - if ($this->_purchasableById !== null && $this->_purchasableById->has($purchasableId)) { - return $this->_purchasableById->get($purchasableId); - } - - $siteId ??= Craft::$app->getSites()->getCurrentSite()->id; - $elementType = Craft::$app->getElements()->getElementTypeById($purchasableId); - - if ($elementType === null || !class_exists($elementType)) { - return null; - } - - $query = Craft::$app->getElements()->createElementQuery($elementType) - ->id($purchasableId) - ->siteId($siteId) - ->status(null) - ->drafts(null) - ->provisionalDrafts(null) - ->revisions(null); - - if ($query instanceof PurchasableQuery) { - $query->forCustomer($forCustomer); - } - - $purchasable = $query->one(); - if ($purchasable && !$purchasable instanceof PurchasableInterface) { - throw new InvalidArgumentException(sprintf('Element %s does not implement %s', $purchasableId, PurchasableInterface::class)); - } - - if ($this->_purchasableById === null) { - $this->_purchasableById = collect(); - } - - $this->_purchasableById->put($purchasableId, $purchasable); - - return $purchasable; - } - - /** - * Returns all available purchasable element classes. - * - * @return string[] The available purchasable element classes. - */ - public function getAllPurchasableElementTypes(): array - { - $purchasableElementTypes = [ - Variant::class, - ]; - - $event = new RegisterComponentTypesEvent([ - 'types' => $purchasableElementTypes, - ]); - $this->trigger(self::EVENT_REGISTER_PURCHASABLE_ELEMENT_TYPES, $event); - - return $event->types; - } -} diff --git a/src/services/Sales.php b/src/services/Sales.php deleted file mode 100644 index 1d16a0da2e..0000000000 --- a/src/services/Sales.php +++ /dev/null @@ -1,727 +0,0 @@ - - * @since 2.0 - */ -class Sales extends Component -{ - /** - * @event SaleMatchEvent The event that is triggered before Commerce attempts to match a sale to a purchasable. - * - * The `isValid` event property can be set to `false` to prevent the application of the matched sale. - * - * ```php - * use craft\commerce\events\SaleMatchEvent; - * use craft\commerce\services\Sales; - * use craft\commerce\base\PurchasableInterface; - * use craft\commerce\models\Sale; - * use yii\base\Event; - * - * Event::on( - * Sales::class, - * Sales::EVENT_BEFORE_MATCH_PURCHASABLE_SALE, - * function(SaleMatchEvent $event) { - * // @var Sale $sale - * $sale = $event->sale; - * // @var PurchasableInterface $purchasable - * $purchasable = $event->purchasable; - * // @var bool $isNew - * $isNew = $event->isNew; - * - * // Use custom business logic to exclude purchasable from sale - * // with `$event->isValid = false` - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_MATCH_PURCHASABLE_SALE = 'beforeMatchPurchasableSale'; - - /** - * @event SaleEvent The event that is triggered before a sale is saved. - * @since 2.2 - * - * ```php - * use craft\commerce\events\SaleEvent; - * use craft\commerce\services\Sales; - * use craft\commerce\models\Sale; - * use yii\base\Event; - * - * Event::on( - * Sales::class, - * Sales::EVENT_BEFORE_SAVE_SALE, - * function(SaleEvent $event) { - * // @var Sale $sale - * $sale = $event->sale; - * // @var bool $isNew - * $isNew = $event->isNew; - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_SAVE_SALE = 'beforeSaveSale'; - - /** - * @event SaleEvent The event that is triggered after a sale is saved. - * @since 2.2 - * - * ```php - * use craft\commerce\events\SaleEvent; - * use craft\commerce\services\Sales; - * use craft\commerce\models\Sale; - * use yii\base\Event; - * - * Event::on( - * Sales::class, - * Sales::EVENT_BEFORE_SAVE_SALE, - * function(SaleEvent $event) { - * // @var Sale $sale - * $sale = $event->sale; - * // @var bool $isNew - * $isNew = $event->isNew; - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_SAVE_SALE = 'afterSaveSale'; - - /** - * @event SaleEvent The event that is triggered after a sale is deleted. - * - * ```php - * use craft\commerce\events\SaleEvent; - * use craft\commerce\services\Sales; - * use craft\commerce\models\Sale; - * use yii\base\Event; - * - * Event::on( - * Sales::class, - * Sales::EVENT_AFTER_DELETE_SALE, - * function(SaleEvent $event) { - * // @var Sale $sale - * $sale = $event->sale; - * - * // do something - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_DELETE_SALE = 'afterDeleteSale'; - - /** - * @var Sale[]|null - */ - private ?array $_allSales = null; - - /** - * @var Sale[]|null - */ - private ?array $_allActiveSales = null; - - /** - * @var array - */ - private array $_purchasableSaleMatch = []; - - /** - * @return bool - * @throws InvalidConfigException - * @throws SiteNotFoundException - * @since 5.0.0 - */ - public function canUseSales(): bool - { - $singleStore = Plugin::getInstance()->getStores()->getAllStores()->count() === 1; - $noCatalogPricingRules = Plugin::getInstance()->getCatalogPricingRules()->getAllCatalogPricingRules()->isEmpty(); - return $singleStore && $noCatalogPricingRules; - } - - /** - * Get a sale by its ID. - */ - public function getSaleById(int $id): ?Sale - { - foreach ($this->getAllSales() as $sale) { - if ($sale->id == $id) { - return $sale; - } - } - - return null; - } - - /** - * Get all sales. - * - * @return Sale[] - */ - public function getAllSales(): array - { - if (!isset($this->_allSales)) { - $sales = (new Query())->select([ - 'sales.id', - 'sales.name', - 'sales.description', - 'sales.dateFrom', - 'sales.dateTo', - 'sales.apply', - 'sales.applyAmount', - 'sales.stopProcessing', - 'sales.ignorePrevious', - 'sales.allGroups', - 'sales.allPurchasables', - 'sales.allCategories', - 'sales.sortOrder', - 'sales.categoryRelationshipType', - 'sales.enabled', - 'sales.dateCreated', - 'sales.dateUpdated', - 'sp.purchasableId', - 'spt.categoryId', - 'sug.userGroupId', - ]) - ->from(Table::SALES . ' sales') - ->leftJoin(Table::SALE_PURCHASABLES . ' sp', '[[sp.saleId]] = [[sales.id]]') - ->leftJoin(Table::SALE_CATEGORIES . ' spt', '[[spt.saleId]] = [[sales.id]]') - ->leftJoin(Table::SALE_USERGROUPS . ' sug', '[[sug.saleId]] = [[sales.id]]') - ->orderBy(['sales.sortOrder' => 'ASC']) - ->all(); - - $allSalesById = []; - $purchasables = []; - $categories = []; - $groups = []; - - foreach ($sales as $sale) { - $id = $sale['id']; - if ($sale['purchasableId']) { - $purchasables[$id][] = $sale['purchasableId']; - } - - if ($sale['categoryId']) { - $categories[$id][] = $sale['categoryId']; - } - - if ($sale['userGroupId']) { - $groups[$id][] = $sale['userGroupId']; - } - - unset($sale['purchasableId'], $sale['userGroupId'], $sale['categoryId']); - - if (!isset($allSalesById[$id])) { - $allSalesById[$id] = new Sale($sale); - } - } - - foreach ($allSalesById as $id => $sale) { - $sale->setPurchasableIds($purchasables[$id] ?? []); - $sale->setCategoryIds($categories[$id] ?? []); - $sale->setUserGroupIds($groups[$id] ?? []); - } - - $this->_allSales = $allSalesById; - } - - return $this->_allSales; - } - - /** - * Returns the sales that match the purchasable. - * - * @param Order|null $order - * @return Sale[] - * @throws InvalidConfigException - */ - public function getSalesForPurchasable(PurchasableInterface $purchasable, Order $order = null): array - { - $matchedSales = []; - - foreach ($this->_getAllEnabledSales() as $sale) { - if ($this->matchPurchasableAndSale($purchasable, $sale, $order)) { - $matchedSales[] = $sale; - - if ($sale->stopProcessing) { - break; - } - } - } - - return $matchedSales; - } - - /** - * @param PurchasableInterface $purchasable - * @return array - */ - public function getSalesRelatedToPurchasable(PurchasableInterface $purchasable): array - { - /** @var Purchasable $purchasable */ - $sales = []; - $id = $purchasable->getId(); - - if ($id) { - foreach ($this->getAllSales() as $sale) { - // Get related by product specifically - $purchasableIds = $sale->getPurchasableIds(); - - // Get related via category - $relatedTo = [$sale->categoryRelationshipType => $purchasable->getPromotionRelationSource()]; - $saleCategories = $sale->getCategoryIds(); - - $relatedCategories = Category::find() - ->id($saleCategories) - ->relatedTo($relatedTo) - ->siteId($purchasable->siteId) - ->ids(); - $relatedEntries = Entry::find() - ->id($saleCategories) - ->relatedTo($relatedTo) - ->siteId($purchasable->siteId) - ->ids(); - $relatedCategoriesOrEntries = array_merge($relatedCategories, $relatedEntries); - - if (in_array($id, $purchasableIds, false) || !empty($relatedCategoriesOrEntries)) { - $sales[] = $sale; - } - } - } - - return $sales; - } - - /** - * Returns the salePrice of the purchasable based on all the sales. - * - * @param Order|null $order - */ - public function getSalePriceForPurchasable(PurchasableInterface $purchasable, Order $order = null): float - { - $sales = $this->getSalesForPurchasable($purchasable, $order); - $originalPrice = $purchasable->getPrice(); - - $takeOffAmount = 0; - $newPrice = null; - - /** @var Sale $sale */ - foreach ($sales as $sale) { - switch ($sale->apply) { - case SaleRecord::APPLY_BY_PERCENT: - // applyAmount is stored as a negative already - $takeOffAmount += ($sale->applyAmount * $originalPrice); - if ($sale->ignorePrevious) { - $newPrice = $originalPrice + ($sale->applyAmount * $originalPrice); - } - break; - case SaleRecord::APPLY_TO_PERCENT: - // applyAmount needs to be reversed since it is stored as negative - $newPrice = (-$sale->applyAmount * $originalPrice); - break; - case SaleRecord::APPLY_BY_FLAT: - // applyAmount is stored as a negative already - $takeOffAmount += $sale->applyAmount; - if ($sale->ignorePrevious) { - // applyAmount is always negative so add the negative amount to the original price for the new price. - $newPrice = $originalPrice + $sale->applyAmount; - } - break; - case SaleRecord::APPLY_TO_FLAT: - // applyAmount needs to be reversed since it is stored as negative - $newPrice = -$sale->applyAmount; - break; - } - - // If the stop processing flag is true, it must been the last - // since the sales for this purchasable would have returned it last. - if ($sale->stopProcessing) { - break; - } - } - - $salePrice = ($originalPrice + $takeOffAmount); - - // A newPrice has been set so use it. - if ($newPrice !== null) { - $salePrice = $newPrice; - } - - if ($salePrice < 0) { - $salePrice = 0; - } - - return CurrencyHelper::round($salePrice); - } - - /** - * Match a product and a sale and return the result. - * - * @param Order|null $order - * @throws InvalidConfigException - */ - public function matchPurchasableAndSale(PurchasableInterface $purchasable, Sale $sale, Order $order = null): bool - { - /** @var Purchasable $purchasable */ - $purchasableId = $purchasable->id; - $saleId = $sale->id; - - if (!isset($this->_purchasableSaleMatch[$purchasableId])) { - $this->_purchasableSaleMatch[$purchasableId] = []; - } - - if (!isset($this->_purchasableSaleMatch[$purchasableId][$saleId])) { - $this->_purchasableSaleMatch[$purchasableId][$saleId] = null; - } - - // Only use memoized data if we are matching outside of the context of an order - if (!$order && $this->_purchasableSaleMatch[$purchasableId][$saleId] !== null) { - return $this->_purchasableSaleMatch[$purchasableId][$saleId]; - } - - // default response is no match - $this->_purchasableSaleMatch[$purchasableId][$saleId] = false; - - // can't match something not promotable - if (!$purchasable->getIsPromotable()) { - return false; - } - - // Purchasable ID match - if (!$sale->allPurchasables && !in_array($purchasable->getId(), $sale->getPurchasableIds(), false)) { - return false; - } - - $date = new DateTime(); - - if ($order) { - // Date we care about in the context of an order is the date the order was placed. - // If the order is still a cart, use the current date time. - $date = $order->isCompleted ? $order->dateOrdered : $date; - } - - if ($sale->dateFrom && $sale->dateFrom >= $date) { - return false; - } - - if ($sale->dateTo && $sale->dateTo <= $date) { - return false; - } - - if ($order) { - $user = $order->getCustomer(); - - if (!$sale->allGroups) { - // User group condition means we have to have a real user - if (null === $user) { - return false; - } - // User groups of the order's user - $userGroups = ArrayHelper::getColumn($user->getGroups(), 'id'); - if (!$userGroups || !array_intersect($userGroups, $sale->getUserGroupIds())) { - return false; - } - } - } - - // Are we dealing with the current session outside of any cart/order context - if (!$order && !$sale->allGroups) { - // User groups of the currently logged in user - $userGroups = null; - if ($currentUser = Craft::$app->getUser()->getIdentity()) { - $userGroups = ArrayHelper::getColumn($currentUser->getGroups(), 'id'); - } - - if (!$userGroups || !array_intersect($userGroups, $sale->getUserGroupIds())) { - return false; - } - } - - // Category match - if (!$sale->allCategories) { - $relatedTo = [$sale->categoryRelationshipType => $purchasable->getPromotionRelationSource()]; - $saleCategories = $sale->getCategoryIds(); - $relatedCategories = Category::find() - ->id($saleCategories) - ->relatedTo($relatedTo) - ->siteId($purchasable->siteId) - ->ids(); - $relatedEntries = Entry::find() - ->id($saleCategories) - ->relatedTo($relatedTo) - ->siteId($purchasable->siteId) - ->ids(); - $relatedCategoriesOrEntries = array_merge($relatedCategories, $relatedEntries); - if (empty($relatedCategoriesOrEntries)) { - return false; - } - } - - $saleMatchEvent = new SaleMatchEvent(compact('sale', 'purchasable')); - - // Raising the 'beforeMatchPurchasableSale' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_MATCH_PURCHASABLE_SALE)) { - $this->trigger(self::EVENT_BEFORE_MATCH_PURCHASABLE_SALE, $saleMatchEvent); - } - - // If an order has been supplied we do not want to memoize the match - if ($order) { - unset($this->_purchasableSaleMatch[$purchasableId][$saleId]); - return $saleMatchEvent->isValid; - } - - $this->_purchasableSaleMatch[$purchasableId][$saleId] = $saleMatchEvent->isValid; - return $this->_purchasableSaleMatch[$purchasableId][$saleId]; - } - - /** - * Save a Sale. - * - * @param bool $runValidation should we validate this before saving. - * @throws Exception - * @throws \Exception - */ - public function saveSale(Sale $model, bool $runValidation = true): bool - { - $isNewSale = !$model->id; - - if ($isNewSale) { - $record = new SaleRecord(); - } else { - $record = SaleRecord::findOne($model->id); - - if (!$record) { - throw new Exception(Craft::t('commerce', 'No sale exists with the ID “{id}”', - ['id' => $model->id])); - } - } - - if ($runValidation && !$model->validate()) { - Craft::info('Sale not saved due to validation error.', __METHOD__); - - return false; - } - - $fields = [ - 'name', - 'description', - 'dateFrom', - 'dateTo', - 'apply', - 'applyAmount', - 'stopProcessing', - 'ignorePrevious', - 'categoryRelationshipType', - 'enabled', - ]; - foreach ($fields as $field) { - $record->$field = $model->$field; - } - - if ($record->allGroups = $model->allGroups) { - $model->setUserGroupIds([]); - } - if ($record->allCategories = $model->allCategories) { - $model->setCategoryIds([]); - } - if ($record->allPurchasables = $model->allPurchasables) { - $model->setPurchasableIds([]); - } - - // Make sure `dateCreated` and `dateUpdated` are set on the model - if (!$isNewSale) { - $model->dateCreated = DateTimeHelper::toDateTime($record->dateCreated); - $model->dateUpdated = DateTimeHelper::toDateTime($record->dateUpdated); - } - - // Fire an 'beforeSaveSection' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_SALE)) { - $this->trigger(self::EVENT_BEFORE_SAVE_SALE, new SaleEvent([ - 'sale' => $model, - 'isNew' => $isNewSale, - ])); - } - - $db = Craft::$app->getDb(); - $transaction = $db->beginTransaction(); - - try { - $record->save(false); - $model->id = $record->id; - - // Update datetime attributes - $model->dateCreated = DateTimeHelper::toDateTime($record->dateCreated); - $model->dateUpdated = DateTimeHelper::toDateTime($record->dateUpdated); - - SaleUserGroupRecord::deleteAll(['saleId' => $model->id]); - SalePurchasableRecord::deleteAll(['saleId' => $model->id]); - SaleCategoryRecord::deleteAll(['saleId' => $model->id]); - - foreach ($model->getUserGroupIds() as $groupId) { - $relation = new SaleUserGroupRecord(); - $relation->userGroupId = $groupId; - $relation->saleId = $model->id; - $relation->save(); - } - - foreach ($model->getCategoryIds() as $categoryId) { - $relation = new SaleCategoryRecord(); - $relation->categoryId = $categoryId; - $relation->saleId = $model->id; - $relation->save(); - } - - foreach ($model->getPurchasableIds() as $purchasableId) { - $relation = new SalePurchasableRecord(); - $relation->purchasableId = $purchasableId; - $purchasable = Craft::$app->getElements()->getElementById($purchasableId, null, null, ['trashed' => null]); - $relation->purchasableType = $purchasable::class; - $relation->saleId = $model->id; - $relation->save(); - - Craft::$app->getElements()->invalidateCachesForElement($purchasable); - } - - $transaction->commit(); - - $this->_clearCaches(); - - // Fire an 'beforeSaveSection' event - if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_SALE)) { - $this->trigger(self::EVENT_AFTER_SAVE_SALE, new SaleEvent([ - 'sale' => $model, - 'isNew' => $isNewSale, - ])); - } - - return true; - } catch (\Exception $e) { - $transaction->rollBack(); - throw $e; - } - } - - /** - * Reorder Sales based on a list of ids. - * - * @param int[] $ids - * @return bool - * @throws \yii\db\Exception - */ - public function reorderSales(array $ids): bool - { - foreach ($ids as $sortOrder => $id) { - Craft::$app->getDb()->createCommand() - ->update(Table::SALES, ['sortOrder' => $sortOrder + 1], ['id' => $id]) - ->execute(); - } - - $this->_clearCaches(); - - return true; - } - - /** - * Delete a sale by its id. - * - * @param int $id - * @return bool - * @throws StaleObjectException - */ - public function deleteSaleById(int $id): bool - { - $saleRecord = SaleRecord::findOne($id); - - if (!$saleRecord) { - return false; - } - - $sale = $this->getSaleById($saleRecord->id); - - $this->_clearCaches(); - $result = (bool)$saleRecord->delete(); - - //Raise the afterDeleteSale event - if ($result && $this->hasEventHandlers(self::EVENT_AFTER_DELETE_SALE)) { - $this->trigger(self::EVENT_AFTER_DELETE_SALE, new SaleEvent([ - 'sale' => $sale, - 'isNew' => false, - ])); - } - - - return $result; - } - - /** - * Get all enabled sales. - * - * @return array - */ - private function _getAllEnabledSales(): array - { - if (!isset($this->_allActiveSales)) { - $sales = $this->getAllSales(); - $activeSales = []; - foreach ($sales as $sale) { - if ($sale->enabled) { - $activeSales[] = $sale; - } - } - - $this->_allActiveSales = $activeSales; - } - - return $this->_allActiveSales; - } - - /** - * Clear memoization caches - * - * @since 3.1.4 - */ - private function _clearCaches(): void - { - $this->_allActiveSales = null; - $this->_allSales = null; - $this->_purchasableSaleMatch = []; - } -} diff --git a/src/services/ShippingCategories.php b/src/services/ShippingCategories.php deleted file mode 100644 index cdcd84ea8d..0000000000 --- a/src/services/ShippingCategories.php +++ /dev/null @@ -1,391 +0,0 @@ - - * @since 2.0 - */ -class ShippingCategories extends Component -{ - /** - * @var Collection[]|null - */ - private ?array $_allShippingCategories = null; - - /** - * Returns all Shipping Categories - * - * @param int|null $storeId - * @param bool $withTrashed - * @return Collection - * @throws InvalidConfigException - * @throws StoreNotFoundException - */ - public function getAllShippingCategories(?int $storeId = null, bool $withTrashed = false): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if ($this->_allShippingCategories === null || !isset($this->_allShippingCategories[$storeId])) { - $results = $this->_createShippingCategoryQuery(true) - ->where(['storeId' => $storeId]) - ->all(); - - if ($this->_allShippingCategories === null) { - $this->_allShippingCategories = []; - } - - foreach ($results as $result) { - $shippingCategory = Craft::createObject([ - 'class' => ShippingCategory::class, - 'attributes' => $result, - ]); - - if (!isset($this->_allShippingCategories[$shippingCategory->storeId])) { - $this->_allShippingCategories[$shippingCategory->storeId] = collect(); - } - - $this->_allShippingCategories[$shippingCategory->storeId]->push($shippingCategory); - } - } - - if (!isset($this->_allShippingCategories[$storeId])) { - return collect(); - } - - return $this->_allShippingCategories[$storeId]->filter(fn(ShippingCategory $sc) => (!$withTrashed && $sc->dateDeleted === null) || $withTrashed); - } - - /** - * Returns all Shipping category names, by ID. - * - * @throws InvalidConfigException - */ - public function getAllShippingCategoriesAsList(?int $storeId = null): array - { - $categories = $this->getAllShippingCategories($storeId); - - return $categories->mapWithKeys(fn(ShippingCategory $category) => [$category->id => $category->getUiLabel()])->all(); - } - - /** - * Get a shipping category by its ID. - * - * @param int $shippingCategoryId - * @param int|null $storeId - * @return ShippingCategory|null - * @throws InvalidConfigException - */ - public function getShippingCategoryById(int $shippingCategoryId, ?int $storeId = null): ?ShippingCategory - { - $shippingCategories = $this->getAllShippingCategories($storeId); - $first = $shippingCategories->firstWhere('id', $shippingCategoryId); - return $first; - } - - /** - * Get a shipping category by its handle. - * - * @noinspection PhpUnused - * @throws InvalidConfigException - */ - public function getShippingCategoryByHandle(string $shippingCategoryHandle, ?int $storeId = null): ?ShippingCategory - { - return $this->getAllShippingCategories($storeId)->firstWhere('handle', $shippingCategoryHandle); - } - - /** - * Returns the default shipping category. - * - * @throws InvalidConfigException - */ - public function getDefaultShippingCategory(int $storeId): ShippingCategory - { - $categories = $this->getAllShippingCategories($storeId); - - $default = $categories->firstWhere('default', true); - - if (!$default) { - $default = $categories->first(); - } - - if (!$default) { - throw new InvalidConfigException('Commerce must have at least one (default) shipping category set up.'); - } - - return $default; - } - - /** - * @param bool $runValidation should we validate this before saving. - * @throws Exception - * @throws \Exception - */ - public function saveShippingCategory(ShippingCategory $shippingCategory, bool $runValidation = true): bool - { - if ($shippingCategory->id) { - $record = ShippingCategoryRecord::findOne($shippingCategory->id); - - if (!$record) { - throw new Exception(Craft::t('commerce', 'No shipping category exists with the ID “{id}”', - ['id' => $shippingCategory->id])); - } - } else { - $record = new ShippingCategoryRecord(); - } - - if ($runValidation && !$shippingCategory->validate()) { - Craft::info('Shipping category not saved due to validation error.', __METHOD__); - - return false; - } - - $record->name = $shippingCategory->name; - $record->storeId = $shippingCategory->storeId; - $record->handle = $shippingCategory->handle; - $record->description = $shippingCategory->description; - $record->icon = $shippingCategory->icon; - $record->color = $shippingCategory->color; - $record->default = $shippingCategory->default; - - // Save it! - $record->save(false); - - // Now that we have a record ID, save it on the model - $shippingCategory->id = $record->id; - - // If this was the default make all others not the default. - if ($shippingCategory->default) { - $condition = [ - 'and', - ['storeId' => $record->storeId], - ['not', ['id' => $record->id]], - ]; - ShippingCategoryRecord::updateAll(['default' => false], $condition); - } - - // Product type IDs this shipping category is available to - $currentProductTypeIds = (new Query()) - ->select(['productTypeId']) - ->from([Table::PRODUCTTYPES_SHIPPINGCATEGORIES]) - ->where(['shippingCategoryId' => $shippingCategory->id]) - ->column(); - - // Newly set product types this shipping category is available to - $newProductTypeIds = ArrayHelper::getColumn($shippingCategory->getProductTypes(), 'id'); - - // Find product types that are being removed from this shipping category - $removedProductTypeIds = array_diff($currentProductTypeIds, $newProductTypeIds); - - // Update purchasables to default shipping category when product types are removed - if (!empty($removedProductTypeIds)) { - $defaultShippingCategory = $this->getDefaultShippingCategory($shippingCategory->storeId); - - // Get all variant purchasables that currently have this shipping category but whose product type is being removed - $purchasableIds = (new Query()) - ->select(['ps.purchasableId']) - ->from(['ps' => Table::PURCHASABLES_STORES]) - ->innerJoin(['v' => Table::VARIANTS], '[[ps.purchasableId]] = [[v.id]]') - ->innerJoin(['p' => Table::PRODUCTS], '[[v.primaryOwnerId]] = [[p.id]]') - ->where([ - 'ps.shippingCategoryId' => $shippingCategory->id, - 'ps.storeId' => $shippingCategory->storeId, - 'p.typeId' => $removedProductTypeIds, - ]) - ->column(); - - if (!empty($purchasableIds)) { - // Update these purchasables to use the default shipping category - Craft::$app->getDb()->createCommand() - ->update( - Table::PURCHASABLES_STORES, - ['shippingCategoryId' => $defaultShippingCategory->id], - [ - 'purchasableId' => $purchasableIds, - 'storeId' => $shippingCategory->storeId, - 'shippingCategoryId' => $shippingCategory->id, - ] - ) - ->execute(); - } - } - - foreach ($currentProductTypeIds as $oldProductTypeId) { - // If we are removing a product type for this shipping category the products of that type should be re-saved - if (!in_array($oldProductTypeId, $newProductTypeIds, false)) { - // Re-save all variants that no longer have this shipping category available to them - $this->_resaveVariantsByProductTypeId($oldProductTypeId); - } - } - - foreach ($newProductTypeIds as $newProductTypeId) { - // If we are adding a product type for this shipping category the products of that type should be re-saved - if (!in_array($newProductTypeId, $currentProductTypeIds, false)) { - // Re-save all variants when assigning this shipping category available to them - $this->_resaveVariantsByProductTypeId($newProductTypeId); - } - } - - // Remove existing Categories <-> ProductType relationships - Craft::$app->getDb()->createCommand()->delete(Table::PRODUCTTYPES_SHIPPINGCATEGORIES, ['shippingCategoryId' => $shippingCategory->id])->execute(); - - // Add back the new categories - foreach ($shippingCategory->getProductTypes() as $productType) { - $data = ['productTypeId' => (int)$productType->id, 'shippingCategoryId' => (int)$shippingCategory->id]; - Craft::$app->getDb()->createCommand()->insert(Table::PRODUCTTYPES_SHIPPINGCATEGORIES, $data)->execute(); - } - - // Clear Service cache - $this->_allShippingCategories = null; - - return true; - } - - /** - * Re-save variants by product type id - */ - private function _resaveVariantsByProductTypeId(int $productTypeId): void - { - Craft::$app->getQueue()->push(new ResaveElements([ - 'elementType' => Variant::class, - 'updateSearchIndex' => false, - 'criteria' => [ - 'typeId' => $productTypeId, - 'siteId' => '*', - 'unique' => true, - 'status' => null, - ], - ])); - } - - /** - * @throws Throwable - * @throws StaleObjectException - */ - public function deleteShippingCategoryById(int $id): bool - { - /** @var ShippingCategoryRecord|SoftDeleteBehavior|null $shippingCategory */ - $shippingCategory = ShippingCategoryRecord::findOne($id); - - if ($shippingCategory === null || $shippingCategory->default) { - return false; - } - - if ($shippingCategory->softDelete()) { - $this->_allShippingCategories = null; - return true; - } - - return false; - } - - /** - * @param int $productTypeId - * @return array - * @throws InvalidConfigException - */ - public function getShippingCategoriesByProductTypeId(int $productTypeId): array - { - $rows = $this->_createShippingCategoryQuery() - ->innerJoin(Table::PRODUCTTYPES_SHIPPINGCATEGORIES . ' productTypeShippingCategories', '[[shippingCategories.id]] = [[productTypeShippingCategories.shippingCategoryId]]') - ->andWhere(['productTypeShippingCategories.productTypeId' => $productTypeId]) - ->all(); - - // Always need at least the default category - if (empty($rows)) { - try { - // @TODO Stop relying on the default shipping category as a fallback here; either ensure product types always have at least one category linked, or surface the empty state to the caller - $shippingCategory = $this->getAllShippingCategories()->firstWhere('default', true); - } catch (InvalidConfigException) { - return []; - } - - return [$shippingCategory->id => $shippingCategory]; - } - - $shippingCategories = []; - - foreach ($rows as $row) { - $key = $row['id']; - $shippingCategories[$key] = new ShippingCategory($row); - } - - return $shippingCategories; - } - - /** - * @return void - * @since 5.0.0 - */ - public function clearCaches(): void - { - $this->_allShippingCategories = null; - } - - /** - * Returns a Query object prepped for retrieving shipping categories. - * - * @param bool $withTrashed - * @return Query - */ - private function _createShippingCategoryQuery(bool $withTrashed = false): Query - { - $query = (new Query()) - ->select([ - 'shippingCategories.dateCreated', - 'shippingCategories.dateDeleted', - 'shippingCategories.dateUpdated', - 'shippingCategories.default', - 'shippingCategories.description', - 'shippingCategories.handle', - 'shippingCategories.id', - 'shippingCategories.name', - 'shippingCategories.storeId', - ]) - ->from([Table::SHIPPINGCATEGORIES . ' shippingCategories']); - - // Only add icon and color if the columns exist (for pre-migration compatibility) - $db = Craft::$app->getDb(); - $schema = $db->getSchema(); - $tableSchema = $schema->getTableSchema(Table::SHIPPINGCATEGORIES); - - if ($tableSchema && $tableSchema->getColumn('icon') !== null) { - $query->addSelect(['shippingCategories.icon', 'shippingCategories.color']); - } - - if (!$withTrashed) { - $query->where(['dateDeleted' => null]); - } - - return $query; - } -} diff --git a/src/services/ShippingMethods.php b/src/services/ShippingMethods.php deleted file mode 100644 index 6adaf9582a..0000000000 --- a/src/services/ShippingMethods.php +++ /dev/null @@ -1,322 +0,0 @@ - - * @since 2.0 - */ -class ShippingMethods extends Component -{ - /** - * @event RegisterShippingMethods The event that is triggered for registration of additional shipping methods. - * - * This example adds an instance of `MyShippingMethod` to the event object’s `shippingMethods` array: - * - * ```php - * use craft\events\RegisterComponentTypesEvent; - * use craft\commerce\services\ShippingMethods; - * use yii\base\Event; - * - * Event::on( - * ShippingMethods::class, - * ShippingMethods::EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS, - * function(RegisterComponentTypesEvent $event) { - * $event->shippingMethods[] = MyShippingMethod::class; - * } - * ); - * ``` - */ - public const EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS = 'registerAvailableShippingMethods'; - - /** - * @var null|Collection[] - */ - private ?array $_allShippingMethods = null; - - /** - * @var array - */ - private array $_serializedOrdersByNumber = []; - - /** - * Returns the Commerce managed shipping methods stored in the database. - * - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - */ - public function getAllShippingMethods(?int $storeId = null): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if ($this->_allShippingMethods === null || !isset($this->_allShippingMethods[$storeId])) { - $results = $this->_createShippingMethodQuery() - ->where(['storeId' => $storeId]) - ->all(); - - if ($this->_allShippingMethods === null) { - $this->_allShippingMethods = []; - } - - foreach ($results as $result) { - $shippingMethod = Craft::createObject([ - 'class' => ShippingMethod::class, - 'attributes' => $result, - ]); - - if (!isset($this->_allShippingMethods[$shippingMethod->storeId])) { - $this->_allShippingMethods[$shippingMethod->storeId] = collect(); - } - - $this->_allShippingMethods[$shippingMethod->storeId]->push($shippingMethod); - } - } - - return $this->_allShippingMethods[$storeId] ?? collect(); - } - - /** - * Get a shipping method by its handle. - */ - public function getShippingMethodByHandle(string $shippingMethodHandle, ?int $storeId = null): ?ShippingMethod - { - return $this->getAllShippingMethods($storeId)->firstWhere('handle', $shippingMethodHandle); - } - - /** - * Get a shipping method by its ID. - */ - public function getShippingMethodById(int $shippingMethodId, ?int $storeId = null): ?ShippingMethod - { - return $this->getAllShippingMethods($storeId)->firstWhere('id', $shippingMethodId); - } - - /** - * Get all available shipping methods to the order. - * - * @return ShippingMethod[] - */ - public function getMatchingShippingMethods(Order $order): array - { - $matchingMethods = []; - - $methods = $this->getAllShippingMethods($order->storeId); - - $event = new RegisterAvailableShippingMethodsEvent([ - 'shippingMethods' => $methods, - 'order' => $order, - ]); - - if ($this->hasEventHandlers(self::EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS)) { - $this->trigger(self::EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS, $event); - } - - /** @var ShippingMethod $method */ - foreach ($event->getShippingMethods() as $method) { - if ($method->getIsEnabled() && $method->matchOrder($order)) { - // Now we know the method matches, let's get the price - $totalPrice = $method->getPriceForOrder($order); - - $matchingMethods[$method->getHandle()] = [ - 'method' => $method, - 'price' => $totalPrice, // Store the price so we can sort on it before returning - ]; - } - } - - // Sort by price. Using the cached price and don't call `$method->getPriceForOrder($order);` again. - uasort($matchingMethods, static fn($a, $b) => $a['price'] <=> $b['price']); - - $shippingMethods = []; - foreach ($matchingMethods as $shippingMethod) { - $method = $shippingMethod['method']; - $shippingMethods[$method->getHandle()] = $method; // Keep the key being the handle of the method for front-end use. - - // Clear the matching cache in case things change in the future - if ($method instanceof \craft\commerce\base\ShippingMethod) { - $method->clearMatchingShippingRuleCache(); - } - } - - // Clear the memoized data so next time we watch to match rules, we get fresh data. - $this->_serializedOrdersByNumber = []; - - return $shippingMethods; - } - - /** - * Creates an order as an array for matching rules. - * We do this centrally here so that we can clear the memoized data centrally. - * - * @param Order $order - * @return array - * @since 4.7.0 - */ - public function getSerializedOrderForMatchingRules(Order $order): array - { - if (isset($this->_serializedOrdersByNumber[$order->number])) { - return $this->_serializedOrdersByNumber[$order->number]; - } - - $fieldsAsArray = $order->getSerializedFieldValues(); - $orderAsArray = $order->toArray([], ['lineItems.snapshot', 'shippingAddress', 'billingAddress']); - $this->_serializedOrdersByNumber[$order->number] = array_merge($orderAsArray, $fieldsAsArray); - return $this->_serializedOrdersByNumber[$order->number]; - } - - /** - * Get a matching shipping rule for Order and shipping method. - * - * @noinspection PhpUnused - */ - public function getMatchingShippingRule(Order $order, ShippingMethodInterface $method): ?ShippingRuleInterface - { - return $method->getMatchingShippingRule($order); - } - - /** - * Save a shipping method. - * - * @param bool $runValidation should we validate this method before saving. - * @throws Exception - */ - public function saveShippingMethod(ShippingMethod $model, bool $runValidation = true): bool - { - if ($model->id) { - $record = ShippingMethodRecord::findOne($model->id); - - if (!$record) { - throw new Exception(Craft::t('commerce', 'No shipping method exists with the ID “{id}”', - ['id' => $model->id])); - } - } else { - $record = new ShippingMethodRecord(); - } - - if ($runValidation && !$model->validate()) { - Craft::info('Shipping method not saved due to validation error.', __METHOD__); - - return false; - } - - $record->storeId = $model->storeId; - $record->name = $model->name; - $record->handle = $model->handle; - $record->icon = $model->icon; - $record->color = $model->color; - $record->orderCondition = $model->getOrderCondition()->getConfig(); - $record->customerCondition = $model->getCustomerCondition()->getConfig(); - $record->enabled = $model->enabled; - - $record->validate(); - $model->addErrors($record->getErrors()); - - // Save it! - $record->save(false); - - // Now that we have a record ID, save it on the model - $model->id = $record->id; - - $this->clearCache(); - - return true; - } - - /** - * Delete a shipping method by its ID. - * - * @param int $shippingMethodId - * @return bool - * @throws Throwable - */ - public function deleteShippingMethodById(int $shippingMethodId): bool - { - // Delete all rules first. - $db = Craft::$app->getDb(); - $transaction = $db->beginTransaction(); - - try { - $rules = Plugin::getInstance()->getShippingRules()->getAllShippingRulesByShippingMethodId($shippingMethodId); - - foreach ($rules as $rule) { - Plugin::getInstance()->getShippingRules()->deleteShippingRuleById($rule->id); - } - - $record = ShippingMethodRecord::findOne($shippingMethodId); - $record->delete(); - - $transaction->commit(); - $this->clearCache(); - return true; - } catch (\Exception) { - $transaction->rollBack(); - - return false; - } - } - - /** - * Returns a Query object prepped for retrieving shipping methods. - */ - private function _createShippingMethodQuery(): Query - { - $query = (new Query()) - ->select([ - 'dateCreated', - 'dateUpdated', - 'enabled', - 'handle', - 'id', - 'name', - 'orderCondition', - 'customerCondition', - 'storeId', - ]) - ->from([Table::SHIPPINGMETHODS]); - - // Only add icon and color if the columns exist (for pre-migration compatibility) - $db = Craft::$app->getDb(); - $schema = $db->getSchema(); - $tableSchema = $schema->getTableSchema(Table::SHIPPINGMETHODS); - - if ($tableSchema && $tableSchema->getColumn('icon') !== null) { - $query->addSelect(['icon', 'color']); - } - - return $query; - } - - /** - * @return void - * @since 5.0.0 - */ - protected function clearCache(): void - { - $this->_allShippingMethods = null; - } -} diff --git a/src/services/ShippingRuleCategories.php b/src/services/ShippingRuleCategories.php deleted file mode 100644 index 818df44149..0000000000 --- a/src/services/ShippingRuleCategories.php +++ /dev/null @@ -1,200 +0,0 @@ - - * @since 2.0 - */ -class ShippingRuleCategories extends Component -{ - /** - * @var array|null - */ - private ?array $_shippingRuleCategories = null; - - /** - * Returns shipping rule category data without instantiating the classes for performances purposes - * - * @return array - */ - public function getAllShippingRuleCategoriesData(): array - { - if ($this->_shippingRuleCategories === null) { - $data = $this->_createShippingRuleCategoriesQuery()->all(); - - if (!empty($data)) { - $ruleCategories = []; - foreach ($data as $row) { - if (!isset($ruleCategories[$row['shippingRuleId']])) { - $ruleCategories[$row['shippingRuleId']] = []; - } - - $ruleCategories[$row['shippingRuleId']][$row['shippingCategoryId']] = $row; - } - - $this->_shippingRuleCategories = $ruleCategories; - } - } - - return $this->_shippingRuleCategories ?? []; - } - - /** - * Returns an array of shipping rules categories per the rule's ID. - * - * @param int $ruleId the rule's ID - * @return ShippingRuleCategory[] An array of matched shipping rule categories. - */ - public function getShippingRuleCategoriesByRuleId(int $ruleId): array - { - $rules = []; - - $shippingRuleCategories = $this->getAllShippingRuleCategoriesData(); - if (!isset($shippingRuleCategories[$ruleId])) { - return []; - } - - foreach ($shippingRuleCategories[$ruleId] as $row) { - if ($row instanceof ShippingRuleCategory) { - $rules[$row->shippingCategoryId] = $row; - continue; - } - - $id = $row['shippingCategoryId']; - $rules[$id] = new ShippingRuleCategory($row); - } - - $this->_shippingRuleCategories[$ruleId] = $rules; - - return $rules; - } - - /** - * Returns an array of shipping rule categories indexed by rule ID. - * - * @param int[] $ruleIds - * @return array - * @since 5.6.0 - */ - public function getShippingRuleCategoriesByRuleIds(array $ruleIds): array - { - if (empty($ruleIds)) { - return []; - } - - $categoriesByRuleId = []; - - $rows = $this->_createShippingRuleCategoriesQuery() - ->where(['shippingRuleId' => $ruleIds]) - ->all(); - - foreach ($rows as $row) { - $ruleId = $row['shippingRuleId']; - $categoryId = $row['shippingCategoryId']; - $categoriesByRuleId[$ruleId][$categoryId] = new ShippingRuleCategory($row); - } - - return $categoriesByRuleId; - } - - /** - * Save a shipping rule category. - * - * @param ShippingRuleCategory $model The shipping rule model. - * @param bool $runValidation should we validate this rule category before saving. - * @return bool Whether the save was successful. - */ - public function createShippingRuleCategory(ShippingRuleCategory $model, bool $runValidation = true): bool - { - if ($runValidation && !$model->validate()) { - Craft::info('Shipping rule category not saved due to validation error.', __METHOD__); - - return false; - } - - $record = new ShippingRuleCategoryRecord(); - - $fields = [ - 'shippingRuleId', - 'shippingCategoryId', - 'condition', - 'perItemRate', - 'weightRate', - 'percentageRate', - ]; - - foreach ($fields as $field) { - $record->$field = $model->$field; - } - - // Save it! - $record->save(false); - - // Now that we have a record ID, save it on the model - $model->id = $record->id; - - $this->_shippingRuleCategories = null; - - return true; - } - - /** - * Delete a shipping rule category by its ID. - * - * @param int $id the shipping rule category ID. - * @return bool Whether the category was deleted successfully. - * @throws Throwable - * @throws StaleObjectException - * @noinspection PhpUnused - */ - public function deleteShippingRuleCategoryById(int $id): bool - { - $record = ShippingRuleCategoryRecord::findOne($id); - - if ($record) { - // Clear cache if required - $this->_shippingRuleCategories = null; - - return (bool)$record->delete(); - } - - return false; - } - - /** - * Returns a Query object prepped for retrieving shipping rule categories. - * - * @return Query The query object. - */ - private function _createShippingRuleCategoriesQuery(): Query - { - return (new Query()) - ->select([ - 'condition', - 'id', - 'percentageRate', - 'perItemRate', - 'shippingCategoryId', - 'shippingRuleId', - 'weightRate', - ]) - ->from([Table::SHIPPINGRULE_CATEGORIES]); - } -} diff --git a/src/services/ShippingRules.php b/src/services/ShippingRules.php deleted file mode 100644 index aca8d827e7..0000000000 --- a/src/services/ShippingRules.php +++ /dev/null @@ -1,269 +0,0 @@ - - * @since 2.0 - */ -class ShippingRules extends Component -{ - /** - * @var null|Collection - */ - private ?Collection $_allShippingRules = null; - - /** - * Get all shipping rules. - * - * @return Collection - * @throws InvalidConfigException - */ - public function getAllShippingRules(): Collection - { - // @TODO Confirm this per-instance memoization is correct given multi-store contexts; consider keying by storeId if shipping rules diverge across stores - if ($this->_allShippingRules !== null) { - return $this->_allShippingRules; - } - - $results = $this->_createShippingRulesQuery()->all(); - $allShippingRules = []; - - foreach ($results as $result) { - $result['orderCondition'] ??= ''; - $allShippingRules[] = Craft::createObject([ - 'class' => ShippingRule::class, - 'attributes' => $result, - ]); - } - - $this->_allShippingRules = collect($allShippingRules); - - // Eager load shipping rule categories - $this->_eagerLoadShippingRuleCategories($this->_allShippingRules); - - return $this->_allShippingRules; - } - - /** - * Get all shipping rules by a shipping method ID. - * - * @param int $id - * @return Collection - * @throws InvalidConfigException - */ - public function getAllShippingRulesByShippingMethodId(int $id): Collection - { - return $this->getAllShippingRules()->where('methodId', $id); - } - - /** - * Get a shipping rule by its ID. - */ - public function getShippingRuleById(int $id): ?ShippingRule - { - return $this->getAllShippingRules()->firstWhere('id', $id); - } - - /** - * Save a shipping rule. - * - * @param bool $runValidation should we validate this rule before saving. - * @throws Exception - */ - public function saveShippingRule(ShippingRule $model, bool $runValidation = true): bool - { - if ($model->id) { - $record = ShippingRuleRecord::findOne($model->id); - - if (!$record) { - throw new Exception(Craft::t('commerce', 'No shipping rule exists with the ID “{id}”', - ['id' => $model->id])); - } - } else { - $record = new ShippingRuleRecord(); - } - - if ($runValidation && !$model->validate()) { - Craft::info('Shipping rule not saved due to validation error.', __METHOD__); - - return false; - } - - $fields = [ - 'name', - 'description', - 'methodId', - 'enabled', - 'orderConditionFormula', - 'baseRate', - 'perItemRate', - 'weightRate', - 'percentageRate', - 'minRate', - 'maxRate', - ]; - foreach ($fields as $field) { - $record->$field = $model->$field; - } - - $record->orderCondition = $model->getOrderCondition()->getConfig(); - $record->customerCondition = $model->getCustomerCondition()->getConfig(); - - if (empty($record->priority) && empty($model->priority)) { - $count = ShippingRuleRecord::find()->where(['methodId' => $model->methodId])->count(); - $record->priority = $model->priority = $count + 1; - } elseif ($model->priority) { - $record->priority = $model->priority; - } else { - $model->priority = $record->priority; - } - - // Save it! - $record->save(false); - - // Now that we have a record ID, save it on the model - $model->id = $record->id; - - ShippingRuleCategoryRecord::deleteAll(['shippingRuleId' => $model->id]); - - // Generate a rule category record for all categories regardless of data submitted - foreach (Plugin::getInstance()->getShippingCategories()->getAllShippingCategories($model->storeId) as $shippingCategory) { - $ruleCategory = $model->getShippingRuleCategories()[$shippingCategory->id] ?? null; - if ($ruleCategory) { - $ruleCategory = new ShippingRuleCategory([ - 'shippingRuleId' => $model->id, - 'shippingCategoryId' => $shippingCategory->id, - 'condition' => $ruleCategory->condition, - 'perItemRate' => $ruleCategory->perItemRate, - 'weightRate' => $ruleCategory->weightRate, - 'percentageRate' => $ruleCategory->percentageRate, - ]); - } else { - $ruleCategory = new ShippingRuleCategory([ - 'shippingRuleId' => $model->id, - 'shippingCategoryId' => $shippingCategory->id, - 'condition' => ShippingRuleCategoryRecord::CONDITION_ALLOW, - ]); - } - - Plugin::getInstance()->getShippingRuleCategories()->createShippingRuleCategory($ruleCategory, $runValidation); - } - - $this->_allShippingRules = null; // clear cache - - return true; - } - - /** - * Reorders shipping rules by the given array of IDs. - * - * @throws \yii\db\Exception - */ - public function reorderShippingRules(array $ids): bool - { - foreach ($ids as $sortOrder => $id) { - Craft::$app->getDb()->createCommand()->update(Table::SHIPPINGRULES, ['priority' => $sortOrder + 1], ['id' => $id])->execute(); - } - $this->_allShippingRules = null; // clear cache - - return true; - } - - /** - * Deletes a shipping rule by an ID. - * - * @throws Throwable - * @throws StaleObjectException - */ - public function deleteShippingRuleById(int $id): bool - { - $record = ShippingRuleRecord::findOne($id); - - if ($record) { - return (bool)$record->delete(); - } - - $this->_allShippingRules = null; // clear cache - - return false; - } - - /** - * Returns a Query object prepped for retrieving shipping rules. - */ - private function _createShippingRulesQuery(): Query - { - $query = (new Query()) - ->select([ - 'shippingrules.baseRate', - 'shippingrules.description', - 'shippingrules.enabled', - 'shippingrules.id', - 'shippingrules.maxRate', - 'shippingrules.methodId', - 'shippingrules.minRate', - 'shippingrules.name', - 'shippingrules.orderConditionFormula', - 'shippingrules.orderCondition', - 'shippingrules.customerCondition', - 'shippingrules.percentageRate', - 'shippingrules.perItemRate', - 'shippingrules.priority', - 'shippingrules.weightRate', - 'methods.storeId', - ]) - ->orderBy(['methodId' => SORT_ASC, 'priority' => SORT_ASC]) - ->from(Table::SHIPPINGRULES . ' shippingrules') - ->innerJoin(Table::SHIPPINGMETHODS . ' methods', '[[methods.id]] = [[shippingrules.methodId]]'); - - return $query; - } - - /** - * Eager loads shipping rule categories for a collection of shipping rules. - * - * @param Collection $shippingRules - */ - private function _eagerLoadShippingRuleCategories(Collection $shippingRules): void - { - $ruleIds = $shippingRules->pluck('id')->filter()->all(); - - if (empty($ruleIds)) { - return; - } - - $categoriesByRuleId = Plugin::getInstance() - ->getShippingRuleCategories() - ->getShippingRuleCategoriesByRuleIds($ruleIds); - - foreach ($shippingRules as $rule) { - if ($rule->id !== null) { - $rule->setShippingRuleCategories($categoriesByRuleId[$rule->id] ?? []); - } - } - } -} diff --git a/src/services/ShippingZones.php b/src/services/ShippingZones.php deleted file mode 100644 index 32c0212f0f..0000000000 --- a/src/services/ShippingZones.php +++ /dev/null @@ -1,166 +0,0 @@ - - * @since 2.0 - */ -class ShippingZones extends Component -{ - /** - * @var Collection[] - */ - private ?array $_allZones = null; - - /** - * Get all shipping zones. - * - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - */ - public function getAllShippingZones(?int $storeId = null): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if ($this->_allZones === null || !isset($this->_allZones[$storeId])) { - $results = $this->_createQuery()->where(['storeId' => $storeId])->all(); - - if ($this->_allZones === null) { - $this->_allZones = []; - } - - foreach ($results as $result) { - $shippingAddressZone = Craft::createObject([ - 'class' => ShippingAddressZone::class, - 'attributes' => $result, - ]); - - if (!isset($this->_allZones[$shippingAddressZone->storeId])) { - $this->_allZones[$shippingAddressZone->storeId] = collect(); - } - - $this->_allZones[$shippingAddressZone->storeId]->push($shippingAddressZone); - } - } - - return $this->_allZones[$storeId] ?? collect(); - } - - /** - * Get a shipping zone by its ID. - */ - public function getShippingZoneById(int $id, ?int $storeId = null): ?ShippingAddressZone - { - return $this->getAllShippingZones($storeId)->firstWhere('id', $id); - } - - /** - * Save a shipping zone. - * - * @param bool $runValidation should we validate this zone before saving - * @throws \Exception - * @throws Exception - */ - public function saveShippingZone(ShippingAddressZone $model, bool $runValidation = true): bool - { - if ($model->id) { - $record = ShippingZoneRecord::findOne($model->id); - - if (!$record) { - throw new Exception(Craft::t('commerce', 'No shipping zone exists with the ID “{id}”', ['id' => $model->id])); - } - } else { - $record = new ShippingZoneRecord(); - } - - if ($runValidation && !$model->validate()) { - Craft::info('Shipping zone not saved due to validation error.', __METHOD__); - - return false; - } - - //setting attributes - $record->name = $model->name; - $record->storeId = $model->storeId; - $record->description = $model->description; - $record->condition = $model->getCondition()->getConfig(); - $this->_clearCaches(); - - $record->save(); - $model->id = $record->id; - - return true; - } - - /** - * @throws Throwable - * @throws StaleObjectException - */ - public function deleteShippingZoneById(int $id): bool - { - $record = ShippingZoneRecord::findOne($id); - - if ($record) { - $result = (bool)$record->delete(); - if ($result) { - $this->_clearCaches(); - } - - return $result; - } - - return false; - } - - /** - * Returns a Query object prepped for retrieving shipping zones. - */ - private function _createQuery(): Query - { - return (new Query()) - ->select([ - 'condition', - 'dateCreated', - 'dateUpdated', - 'description', - 'id', - 'name', - 'storeId', - ]) - ->orderBy('name') - ->from([Table::SHIPPINGZONES]); - } - - /** - * Clear memoization. - * - * @since 3.2.5 - */ - private function _clearCaches(): void - { - $this->_allZones = []; - } -} diff --git a/src/services/Store.php b/src/services/Store.php deleted file mode 100644 index a5746abe87..0000000000 --- a/src/services/Store.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @deprecated in 5.0.0. Use [[Stores]] service instead. - * @since 4.0 - */ -class Store extends Component -{ - /** - * Returns the current store. - * - * @return StoreModel - * @throws SiteNotFoundException - * @throws InvalidConfigException - * @deprecated in 5.0.0. Use [[Stores::getCurrentStore()]] instead. - */ - public function getStore(): StoreModel - { - Craft::$app->getDeprecator()->log(__METHOD__, 'craft\commerce\services\Store::getStore() has been deprecated. Use craft\commerce\services\Stores::getCurrentStore() instead.'); - return Plugin::getInstance()->getStores()->getCurrentStore(); - } -} diff --git a/src/services/StoreSettings.php b/src/services/StoreSettings.php deleted file mode 100644 index ee49462cb1..0000000000 --- a/src/services/StoreSettings.php +++ /dev/null @@ -1,182 +0,0 @@ - - * @since 5.0 - */ -class StoreSettings extends Component -{ - /** - * @var Collection|null - */ - private ?Collection $_allStoreSettings = null; - - /** - * Returns the store record. - * - * @param int $id - * @return StoreSettingsModel - */ - public function getStoreSettingsById(int $id): StoreSettingsModel - { - $store = Plugin::getInstance()->getStores()->getStoreById($id); - - if (!$store) { - throw new InvalidConfigException('Store not found'); - } - - $storeSettings = $this->getAllStoreSettings()->firstWhere('id', $id); - - if (!$storeSettings) { - $storeSettingsRecord = new StoreSettingsRecord(); - $storeSettingsRecord->id = $id; - - /** @var StoreSettingsModel $storeSettings */ - $storeSettings = Craft::createObject([ - 'class' => StoreSettingsModel::class, - 'id' => $storeSettingsRecord->id, - ]); - - // Create a new blank store location - $locationAddress = $storeSettings->getLocationAddress(); - $storeSettingsRecord->locationAddressId = $locationAddress->id; - - $storeSettingsRecord->save(); - - - $this->getAllStoreSettings()->put($storeSettings->id, $storeSettings); - } - - return $storeSettings; - } - - /** - * @return Collection - * @throws InvalidConfigException - */ - public function getAllStoreSettings(): Collection - { - if ($this->_allStoreSettings === null) { - $this->_allStoreSettings = collect(); - $storeSettings = $this->_createStoreSettingsQuery()->all(); - - foreach ($storeSettings as $storeSetting) { - $this->_allStoreSettings->put($storeSetting['id'], Craft::createObject([ - 'class' => StoreSettingsModel::class, - 'attributes' => $storeSetting, - ])); - } - } - - return $this->_allStoreSettings ?? collect(); - } - - /** - * Saves the store - * - * @param StoreSettingsModel $storeSettings - * @return bool - * @throws InvalidConfigException - */ - public function saveStoreSettings(StoreSettingsModel $storeSettings): bool - { - $storeSettingsRecord = StoreSettingsRecord::findOne($storeSettings->id); - - if (!$storeSettingsRecord) { - throw new InvalidConfigException('Invalid store ID'); - } - - $storeSettingsRecord->countries = $storeSettings->countries; - $storeSettingsRecord->marketAddressCondition = $storeSettings->marketAddressCondition->getConfig(); - - if (!$storeSettingsRecord->save()) { - return false; - } - - $this->getAllStoreSettings()->put($storeSettings->id, $storeSettings); - return true; - } - - /** - * @param AuthorizationCheckEvent $event - * @return void - */ - public function authorizeStoreLocationView(AuthorizationCheckEvent $event): void - { - if (!$storeSettingsRecord = $this->_checkStoreLocationAuthorization($event)) { - return; - } - - // @TODO Authorize the current user against the store from $storeSettingsRecord (e.g. "commerce-manageStore:" permission) rather than always granting view access - $event->authorized = true; - } - - /** - * @param AuthorizationCheckEvent $event - * @return void - */ - public function authorizeStoreLocationEdit(AuthorizationCheckEvent $event): void - { - if (!$storeSettingsRecord = $this->_checkStoreLocationAuthorization($event)) { - return; - } - - // @TODO Authorize the current user against the store from $storeSettingsRecord (e.g. "commerce-manageStore:" permission) rather than always granting edit access - $event->authorized = true; - } - - /** - * @param AuthorizationCheckEvent $event - * @return StoreSettingsRecord|false - */ - private function _checkStoreLocationAuthorization(AuthorizationCheckEvent $event): StoreSettingsRecord|false - { - if (!$event->element instanceof Address) { - return false; - } - - $storeSettings = StoreSettingsRecord::findOne(['locationAddressId' => $event->element->getCanonicalId()]); - if (!$storeSettings) { - return false; - } - - return $storeSettings; - } - - /** - * Returns a Query object prepped for retrieving the store. - */ - private function _createStoreSettingsQuery(): Query - { - return (new Query()) - ->select([ - 'id', - 'marketAddressCondition', - 'locationAddressId', - 'countries', - ]) - ->from([Table::STORESETTINGS]); - } -} diff --git a/src/services/Stores.php b/src/services/Stores.php deleted file mode 100644 index 70328988b0..0000000000 --- a/src/services/Stores.php +++ /dev/null @@ -1,979 +0,0 @@ - - * @since 5.0.0 - * - * @property-read Store $primaryStore - * @property-read Collection $allStores - */ -class Stores extends Component -{ - /** - * @event DeleteStoreEvent The event that is triggered before a store is deleted. - * - * You may set [[\craft\events\CancelableEvent::$isValid]] to `false` to prevent the store from getting deleted. - * - * ```php - * use craft\commerce\events\DeleteStoreEvent; - * use craft\commerce\models\Store; - * use craft\commerce\services\Stores; - * use yii\base\Event; - * - * Event::on( - * Stores::class, - * Stores::EVENT_BEFORE_DELETE_STORE, - * function(DeleteStoreEvent $event) { - * // @var Store $store - * $store = $event->store; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_DELETE_STORE = 'beforeDeleteStore'; - - /** - * @event DeleteStoreEvent The event that is triggered after a store is deleted - * - * ```php - * use craft\commerce\events\DeleteStoreEvent; - * use craft\commerce\models\Store; - * use craft\commerce\services\Stores; - * use yii\base\Event; - * - * Event::on( - * Stores::class, - * Stores::EVENT_AFTER_DELETE_STORE, - * function(DeleteStoreEvent $event) { - * // @var Store $store - * $store = $event->store; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_DELETE_STORE = 'afterDeleteStore'; - - /** - * @event DeleteStoreEvent The event that is triggered before a store delete is applied to the database. - * - * ```php - * use craft\commerce\events\DeleteStoreEvent; - * use craft\commerce\models\Store; - * use craft\commerce\services\Stores; - * use yii\base\Event; - * - * Event::on( - * Stores::class, - * Stores::EVENT_BEFORE_APPLY_STORE_DELETE, - * function(DeleteStoreEvent $event) { - * // @var Store $store - * $store = $event->store; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_APPLY_STORE_DELETE = 'beforeApplyStoreDelete'; - - /** - * @event StoreEvent The event that is triggered before a store is saved. - * - * ```php - * use craft\commerce\events\StoreEvent; - * use craft\commerce\models\Store; - * use craft\commerce\services\Stores; - * use yii\base\Event; - * - * Event::on( - * Stores::class, - * Stores::EVENT_BEFORE_SAVE_STORE, - * function(StoreEvent $event) { - * // @var Store $store - * $store = $event->store; - * // @var bool $isNew - * $isNew = $event->isNew; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_SAVE_STORE = 'beforeSaveStore'; - - /** - * @event StoreEvent The event that is triggered after a store is saved. - * - * ```php - * use craft\commerce\events\StoreEvent; - * use craft\commerce\models\Store; - * use craft\commerce\services\Stores; - * use yii\base\Event; - * - * Event::on( - * Stores::class, - * Stores::EVENT_AFTER_SAVE_STORE, - * function(StoreEvent $event) { - * // @var Store $store - * $store = $event->store; - * // @var bool $isNew - * $isNew = $event->isNew; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_SAVE_STORE = 'afterSaveStore'; - - /** - * The project config path to stores data - */ - public const CONFIG_STORES_KEY = 'commerce.stores'; - - /** - * The project config path to site stores data - */ - public const CONFIG_SITESTORES_KEY = 'commerce.sitestores'; - - /** - * @var Collection|null - */ - private ?Collection $_allStores = null; - - /** - * @var Collection|null - */ - private ?Collection $_allStoresBySiteId = null; - - /** - * @var Collection|null - */ - private ?Collection $_allSiteStores = null; - - /** - * @return void - */ - private function _loadAllStores(): void - { - if (isset($this->_allStores)) { - return; - } - - $results = $this->_createStoreQuery()->all(); - $siteStores = $this->_createSiteStoresQuery() - ->select(['storeId', 'siteId']) - ->all(); - - $allStores = []; - $allStoresBySiteId = []; - - foreach ($results as $row) { - $store = Craft::createObject(array_merge(['class' => Store::class], $row)); - - $allStores[] = $store; - - foreach (ArrayHelper::where($siteStores, 'storeId', $store->id) as $siteStore) { - $allStoresBySiteId[$siteStore['siteId']] = $store; - } - } - - $this->_allStores = collect($allStores); - $this->_allStoresBySiteId = collect($allStoresBySiteId); - } - - /** - * Returns the current store. - * - * @return Store the current store - * @throws SiteNotFoundException - */ - public function getCurrentStore(): Store - { - return $this->getStoreBySiteId(Craft::$app->getSites()->getCurrentSite()->id) ?? $this->getPrimaryStore(); - } - - /** - * @return Collection - */ - public function getAllStores(): Collection - { - if ($this->_allStores === null) { - $this->_loadAllStores(); - } - - return $this->_allStores ?? collect(); - } - - /** - * @param int $id - * @return Store|null - */ - public function getStoreById(int $id): ?Store - { - return $this->getAllStores()->firstWhere('id', $id); - } - - /** - * @param string $uid - * @return Store|null - */ - public function getStoreByUid(string $uid): ?Store - { - return $this->getAllStores()->firstWhere('uid', $uid); - } - - /** - * @param int $siteId - * @return Store|null - */ - public function getStoreBySiteId(int $siteId): ?Store - { - if ($this->_allStoresBySiteId === null) { - // Population of `_allStoresBySiteId` is done in `_loadAllStores()` - $this->_loadAllStores(); - } - - return $this->_allStoresBySiteId?->get($siteId); - } - - /** - * @param string $handle - * @return Store|null - */ - public function getStoreByHandle(string $handle): ?Store - { - return $this->getAllStores()->firstWhere('handle', $handle); - } - - /** - * Returns a collections of stores that are available to a user. - * - * @param int $userId - * @return Collection - * @throws InvalidConfigException - */ - public function getStoresByUserId(int $userId): Collection - { - $user = Craft::$app->getUsers()->getUserById($userId); - - if (!$user) { - throw new InvalidConfigException('Invalid user ID: ' . $userId); - } - - $allStores = $this->getAllStores(); - if (!Craft::$app->getIsMultiSite()) { - return $allStores; - } - - return $allStores->filter(function(Store $store) use ($user) { - $siteUids = $store->getSites()->map(fn(Site $site) => $site->uid); - - foreach ($siteUids as $siteUid) { - if ($user->can('editSite:' . $siteUid)) { - return true; - } - } - - return false; - }); - } - - /** - * Saves a store. - * - * @param Store $store The store to be saved - * @param bool $runValidation Whether the store should be validated - * @return bool - * @throws BusyResourceException - * @throws StaleResourceException - * @throws ErrorException - * @throws YiiBaseException - * @throws InvalidConfigException - * @throws NotSupportedException - * @throws ServerErrorHttpException - */ - public function saveStore(Store $store, bool $runValidation = true): bool - { - $isNewStore = !$store->id; - - // Fire a 'beforeSaveStore' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_STORE)) { - $this->trigger(self::EVENT_BEFORE_SAVE_STORE, new StoreEvent([ - 'store' => $store, - 'isNew' => $isNewStore, - ])); - } - - if ($runValidation && !$store->validate()) { - Craft::info('Store not saved due to validation error.', __METHOD__); - return false; - } - - if ($isNewStore) { - $store->uid = StringHelper::UUID(); - } elseif (!$store->uid) { - $store->uid = Db::uidById(Table::STORES, $store->id); - } - - $projectConfigService = Craft::$app->getProjectConfig(); - $configPath = self::CONFIG_STORES_KEY . "." . $store->uid; - $projectConfigService->set( - $configPath, - $store->getConfig(), - "Save the “{$store->handle}” store" - ); - - // Now that we have a store ID, save it on the model - if ($isNewStore) { - $store->id = Db::idByUid(Table::STORES, $store->uid); - - // Create any default data we need for the store - $orderStatus = Craft::createObject([ - 'class' => OrderStatus::class, - 'attributes' => [ - 'name' => 'New', - 'handle' => 'new', - 'color' => 'green', - 'default' => true, - 'storeId' => $store->id, - ], - ]); - Plugin::getInstance()->getOrderStatuses()->saveOrderStatus($orderStatus); - } - - // Update the other primary store. - if ($store->primary) { - foreach ($projectConfigService->get(self::CONFIG_STORES_KEY) as $uid => $config) { - if ($uid !== $store->uid && isset($config['primary']) && $config['primary'] === true) { - $configPath = self::CONFIG_STORES_KEY . '.' . $uid; - $config['primary'] = false; // Set the other to false - $projectConfigService->set( - $configPath, - $config, - "Set the “{$config['name']}” store to not be primary" - ); - } - } - } - - $this->refreshStores(); - - return true; - } - - /** - * @param int $storeId - * @return bool - * @throws Exception - */ - public function deleteStoreById(int $storeId): bool - { - $store = $this->getStoreById($storeId); - - if (!$store) { - return false; - } - - return $this->deleteStore($store); - } - - /** - * @param Store $store - * @return bool - * @throws Exception - */ - public function deleteStore(Store $store): bool - { - // Make sure this isn't the primary site - if ($store->id === $this->getPrimaryStore()?->id) { - throw new Exception('You cannot delete the primary store.'); - } - - // Fire a 'beforeDeleteStore' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_DELETE_STORE)) { - $this->trigger(self::EVENT_BEFORE_DELETE_STORE, new DeleteStoreEvent([ - 'store' => $store, - ])); - } - - $path = self::CONFIG_STORES_KEY . '.' . $store->uid; - Craft::$app->getProjectConfig()->remove($path, "Delete the “{$store->handle}” store"); - - return true; - } - - /** - * Handle store status change. - * - * @param ConfigEvent $event - * @return void - * @throws Throwable - * @throws YiiDbException - */ - public function handleChangedStore(ConfigEvent $event): void - { - $storeUid = $event->tokenMatches[0]; - $data = $event->newValue; - - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - $storeRecord = $this->_getStoreRecord($storeUid); - $isNewStore = $storeRecord->getIsNewRecord(); - - $storeRecord->uid = $storeUid; - $storeRecord->name = $data['name']; - $storeRecord->handle = $data['handle']; - $storeRecord->primary = $data['primary']; - - $storeRecord->autoSetNewCartAddresses = ($data['autoSetNewCartAddresses'] ?? false); - $storeRecord->autoSetCartShippingMethodOption = ($data['autoSetCartShippingMethodOption'] ?? false); - $storeRecord->autoSetPaymentSource = ($data['autoSetPaymentSource'] ?? false); - $storeRecord->allowEmptyCartOnCheckout = ($data['allowEmptyCartOnCheckout'] ?? false); - $storeRecord->allowCheckoutWithoutPayment = ($data['allowCheckoutWithoutPayment'] ?? false); - $storeRecord->allowPartialPaymentOnCheckout = ($data['allowPartialPaymentOnCheckout'] ?? false); - $storeRecord->requireShippingAddressAtCheckout = ($data['requireShippingAddressAtCheckout'] ?? false); - $storeRecord->requireBillingAddressAtCheckout = ($data['requireBillingAddressAtCheckout'] ?? false); - $storeRecord->requireShippingMethodSelectionAtCheckout = ($data['requireShippingMethodSelectionAtCheckout'] ?? false); - $storeRecord->useBillingAddressForTax = ($data['useBillingAddressForTax'] ?? false); - $storeRecord->validateOrganizationTaxIdAsVatId = ($data['validateOrganizationTaxIdAsVatId'] ?? false); - $storeRecord->freeOrderPaymentStrategy = ($data['freeOrderPaymentStrategy'] ?? 'complete'); - $storeRecord->minimumTotalPriceStrategy = ($data['minimumTotalPriceStrategy'] ?? 'default'); - $storeRecord->orderReferenceFormat = ($data['orderReferenceFormat'] ?? '{{number[:7]}}'); - $storeRecord->currency = ($data['currency'] ?? null); - $storeRecord->sortOrder = ($data['sortOrder'] ?? 99); - - $storeRecord->save(false); - - $transaction->commit(); - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - - // Did the primary site just change? - if ($data['primary']) { - Db::update(Table::STORES, ['primary' => false], ['not', ['id' => $storeRecord->id]]); - Db::update(Table::STORES, ['primary' => true], ['id' => $storeRecord->id]); - } - - $paymentCurrency = Plugin::getInstance()->getPaymentCurrencies()->getPaymentCurrencyByIso($data['currency'] ?? '', $storeRecord->id); - if (!$paymentCurrency) { - $data = [ - 'iso' => $data['currency'] ?? 'USD', - 'storeId' => $storeRecord->id, - 'rate' => 1, - ]; - Craft::$app->getDb()->createCommand()->insert(PaymentCurrency::tableName(), $data)->execute(); - } - - if (Plugin::getInstance()->getShippingCategories()->getAllShippingCategories($storeRecord->id)->isEmpty()) { - $data = [ - 'name' => 'General', - 'handle' => 'general', - 'default' => true, - 'storeId' => $storeRecord->id, - ]; - Craft::$app->getDb()->createCommand()->insert(ShippingCategory::tableName(), $data)->execute(); - } - - $this->refreshStores(); - - // Fire a 'afterSaveStore' event - if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_STORE)) { - $this->trigger(self::EVENT_AFTER_SAVE_STORE, new StoreEvent([ - 'store' => $this->getStoreById($storeRecord->id), - 'isNew' => $isNewStore, - ])); - } - } - - /** - * Handle a deleted Store. - * - * @param ConfigEvent $event - * @throws Throwable - * @throws YiiDbException - */ - public function handleDeletedStore(ConfigEvent $event): void - { - $storeUid = $event->tokenMatches[0]; - $storeRecord = $this->_getStoreRecord($storeUid); - - if (!$storeRecord->id) { - return; - } - - /** @var Store $store */ - $store = $this->getStoreById($storeRecord->id); - - // Fire a 'beforeApplyStoreDelete' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_APPLY_STORE_DELETE)) { - $this->trigger(self::EVENT_BEFORE_APPLY_STORE_DELETE, new DeleteStoreEvent([ - 'store' => $store, - ])); - } - - $transaction = Craft::$app->getDb()->beginTransaction(); - - try { - $locationAddressId = $store->getSettings()->getLocationAddressId(); - - Craft::$app->getDb()->createCommand() - ->delete(Table::STORES, ['id' => $storeRecord->id]) - ->execute(); - - // Delete store address - if ($locationAddressId) { - Craft::$app->getElements()->deleteElementById($locationAddressId, Address::class, hardDelete: true); - } - - $transaction->commit(); - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - - // Refresh stores - $this->refreshStores(); - - // Make sure any site store for this store is reassigned to the primary store - $siteStores = collect($this->getAllSiteStores())->where('storeId', $store->id)->all(); - foreach ($siteStores as $siteStore) { - $siteStore->storeId = $this->getPrimaryStore()->id; - $this->saveSiteStore($siteStore); - } - - // Fire an 'afterDeleteStore' event - if ($this->hasEventHandlers(self::EVENT_AFTER_DELETE_STORE)) { - $this->trigger(self::EVENT_AFTER_DELETE_STORE, new DeleteStoreEvent([ - 'store' => $store, - ])); - } - } - - /** - * Refresh the status of all stores based on the DB data. - * - * @return void - */ - public function refreshStores(): void - { - $this->_allStores = null; - $this->_allStoresBySiteId = null; - $this->_loadAllStores(); - } - - /** - * Returns the primary store. - * - * @return Store|null - */ - public function getPrimaryStore(): ?Store - { - return $this->getAllStores()->firstWhere('primary', true); - } - - /** - * @param array $ids - * @return bool - * @throws BusyResourceException - * @throws ErrorException - * @throws InvalidConfigException - * @throws NotSupportedException - * @throws ServerErrorHttpException - * @throws StaleResourceException - * @throws YiiBaseException - */ - public function reorderStores(array $ids): bool - { - $projectConfig = Craft::$app->getProjectConfig(); - - $uidsByIds = Db::uidsByIds(Table::STORES, $ids); - - foreach ($ids as $sortOrder => $id) { - if (!empty($uidsByIds[$id])) { - $uid = $uidsByIds[$id]; - $projectConfig->set(self::CONFIG_STORES_KEY . '.' . $uid . '.sortOrder', $sortOrder + 1); - } - } - - $this->refreshStores(); - - return true; - } - - /** - * Gets a store record by uid. - * - * @param string $uid - * @return StoreRecord - */ - private function _getStoreRecord(string $uid): StoreRecord - { - if ($store = StoreRecord::findOne(['uid' => $uid])) { - return $store; - } - - return new StoreRecord(); - } - - /** - * Returns a Query object prepped for retrieving the stores. - * - * @return Query - */ - private function _createStoreQuery(): Query - { - $selectColumns = [ - 'handle', - 'id', - 'name', - 'primary', - 'uid', - ]; - - // Added to avoid migration issues, as settings were moved after stores table creation - // @TODO Remove this schemaVersion guard in Commerce 6.0 once all installs are past schema 5.0.72 and the store settings columns are guaranteed to exist - $commerce = Craft::$app->getPlugins()->getStoredPluginInfo('commerce'); - - if ($commerce && version_compare($commerce['schemaVersion'], '5.0.72', '>=')) { - $selectColumns = array_merge($selectColumns, [ - 'allowCheckoutWithoutPayment', - 'allowEmptyCartOnCheckout', - 'allowPartialPaymentOnCheckout', - 'autoSetCartShippingMethodOption', - 'autoSetNewCartAddresses', - 'autoSetPaymentSource', - 'currency', - 'freeOrderPaymentStrategy', - 'minimumTotalPriceStrategy', - 'orderReferenceFormat', - 'requireBillingAddressAtCheckout', - 'requireShippingAddressAtCheckout', - 'requireShippingMethodSelectionAtCheckout', - 'sortOrder', - 'useBillingAddressForTax', - 'validateOrganizationTaxIdAsVatId', - ]); - } - - $query = (new Query()) - ->select($selectColumns) - ->from([Table::STORES]); - - if ($commerce && version_compare($commerce['schemaVersion'], '5.0.72', '>=')) { - $query->orderBy(['sortOrder' => SORT_ASC]); - } - - return $query; - } - - /** - * @param Store $store - * @return Collection - */ - public function getAllSitesForStore(Store $store): Collection - { - $sites = Craft::$app->getSites()->getAllSites(); - - return $this->getAllSiteStores() - ->filter(fn(SiteStore $siteStore) => $siteStore->storeId == $store->id) - ->map(fn(SiteStore $siteStore) => ArrayHelper::firstWhere($sites, 'id', $siteStore->siteId)); - } - - /** - * @return Collection - */ - public function getAllSiteStores(): Collection - { - if ($this->_allSiteStores !== null) { - return $this->_allSiteStores; - } - - $siteStores = []; - foreach ($this->_createSiteStoresQuery()->all() as $store) { - $siteStores[] = new SiteStore($store); - } - - return !empty($siteStores) ? $this->_allSiteStores = collect($siteStores) : collect(); - } - - /** - * Returns sites that are assigned to more than one store assigned, so that other new stores can use them. - * - * @return array - */ - public function getSiteIdsAvailableForAssignmentToNewStores(): array - { - // Sites that are assigned to more than one store - $subQuery = (new Query()) - ->select('storeId') - ->from(Table::SITESTORES) - ->groupBy('storeId') - ->having(['>', new Expression('COUNT([[storeId]])'), 1]); - - return (new Query()) - ->select('siteId') - ->from(Table::SITESTORES) - ->where(['IN', 'storeId', $subQuery]) - ->groupBy('siteId') - ->column(); - } - - /** - * @param SiteStore $siteStore - * @param bool $runValidation - * @return bool - * @throws BusyResourceException - * @throws ErrorException - * @throws InvalidConfigException - * @throws NotSupportedException - * @throws ServerErrorHttpException - * @throws StaleResourceException - * @throws YiiBaseException - */ - public function saveSiteStore(SiteStore $siteStore, bool $runValidation = true): bool - { - if ($runValidation && !$siteStore->validate()) { - Craft::info('Site store mapping not saved due to validation error.', __METHOD__); - return false; - } - - // We use the same UID as the site since we only have one record per site. - // This also makes it easier to see what site a store is mapped to in the project config. - $craftSite = Craft::$app->getSites()->getSiteById($siteStore->siteId); - if (!$craftSite) { - throw new InvalidConfigException('Invalid site ID: ' . $siteStore->siteId); - } - - if (!$siteStore->uid) { - $siteStore->uid = Db::uidById(CraftTable::SITES, $siteStore->siteId); - } - - $projectConfigService = Craft::$app->getProjectConfig(); - $configPath = self::CONFIG_SITESTORES_KEY . "." . $siteStore->uid; - $projectConfigService->set( - $configPath, - $siteStore->getConfig(), - "Save the “{$craftSite->handle}” commerce site store mapping" - ); - - $this->refreshStores(); - - return true; - } - - /** - * Handle site store mapping change. - * - * @param ConfigEvent $event - * @return void - * @throws Throwable - * @throws YiiDbException - */ - public function handleChangedSiteStore(ConfigEvent $event): void - { - ProjectConfigHelper::ensureAllSitesProcessed(); - ProjectConfigData::ensureAllStoresProcessed(); - - $siteStoreUid = $event->tokenMatches[0]; - $data = $event->newValue; - - $transaction = Craft::$app->getDb()->beginTransaction(); - try { - $siteStoreRecord = SiteStoreRecord::findOne(['uid' => $siteStoreUid]); - - if (!$siteStoreRecord) { - $siteStoreRecord = new SiteStoreRecord(); - } - - $siteStoreRecord->siteId = Db::idByUid(CraftTable::SITES, $siteStoreUid); - $siteStoreRecord->storeId = Db::idByUid(Table::STORES, $data['store']); - $siteStoreRecord->uid = $siteStoreUid; - - $siteStoreRecord->save(false); - - $transaction->commit(); - - $this->refreshStores(); - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - } - - /** - * Handle a deleted Store. - * - * @param ConfigEvent $event - * @throws Throwable - * @throws YiiDbException - */ - public function handleDeletedSiteStore(ConfigEvent $event): void - { - $storeStoreUid = $event->tokenMatches[0]; - $siteStoreRecord = SiteStoreRecord::findOne(['uid' => $storeStoreUid]); // site_stores uses the site UID - - if (!$siteStoreRecord) { - return; - } - - $transaction = Craft::$app->getDb()->beginTransaction(); - - try { - Craft::$app->getDb()->createCommand() - ->delete(Table::SITESTORES, ['siteId' => $siteStoreRecord->siteId]) - ->execute(); - - $transaction->commit(); - - $this->refreshStores(); - } catch (Throwable $e) { - $transaction->rollBack(); - throw $e; - } - } - - /** - * - * @param SiteEvent $event - * @return void - * @throws BusyResourceException - * @throws ErrorException - * @throws InvalidConfigException - * @throws NotSupportedException - * @throws ServerErrorHttpException - * @throws StaleResourceException - * @throws YiiBaseException - */ - public function afterSaveCraftSiteHandler(SiteEvent $event): void - { - // Let handleChangedSiteStore() create the mapping from the incoming config instead. - if (Craft::$app->getProjectConfig()->getIsApplyingExternalChanges()) { - return; - } - - $siteStore = SiteStoreRecord::findOne(['siteId' => $event->site->id]); - - // Only create it if it doesn't exist. - // The saving of the store does not currently change the store relation, but if it did, - // we would need to mutate the existing record. - if (!$siteStore) { - $siteStore = new SiteStore(); - $siteStore->siteId = $event->site->id; - $siteStore->storeId = $this->getPrimaryStore()->id; - $siteStore->uid = $event->site->uid; - $this->saveSiteStore($siteStore); - } - } - - /** - * @param SiteEvent $event - * @return void - * @throws BusyResourceException - * @throws ErrorException - * @throws InvalidConfigException - * @throws NotSupportedException - * @throws ServerErrorHttpException - * @throws StaleResourceException - * @throws YiiBaseException - */ - public function afterDeleteCraftSiteHandler(SiteEvent $event): void - { - $siteStores = $this->getAllSiteStores(); - $siteStore = $siteStores->firstWhere('siteId', $event->site->id); - - if (!$siteStore) { - return; - } - - $store = $this->getStoreById($siteStore->storeId); - - $isStoreOrphaned = true; - foreach ($siteStores as $ss) { - if ($ss->siteId !== $siteStore->siteId && $ss->storeId === $siteStore->storeId) { - $isStoreOrphaned = false; - break; - } - } - - // If this was the primary store, make another the primary - if ($store->primary && $isStoreOrphaned) { - // make another store primary - $store = $this->getAllStores()->firstWhere('primary', false); - $store->primary = true; - $this->saveStore($store); - } - - // Delete the old siteStore record - Craft::$app->getProjectConfig()->remove(self::CONFIG_SITESTORES_KEY . '.' . $siteStore->uid); - } - - /** - * @return Query - */ - private function _createSiteStoresQuery(): Query - { - // get the site stores - return (new Query()) - ->select([ - 'siteId', - 'storeId', - 'uid', - ]) - ->from([Table::SITESTORES]); - } -} diff --git a/src/services/Subscriptions.php b/src/services/Subscriptions.php deleted file mode 100644 index 28e3b5198a..0000000000 --- a/src/services/Subscriptions.php +++ /dev/null @@ -1,786 +0,0 @@ - - * @since 2.0 - */ -class Subscriptions extends Component -{ - /** - * @event SubscriptionEvent The event that is triggered after a subscription has expired. - * - * ```php - * use craft\commerce\events\SubscriptionEvent; - * use craft\commerce\services\Subscriptions; - * use craft\commerce\elements\Subscription; - * use yii\base\Event; - * - * Event::on( - * Subscriptions::class, - * Subscriptions::EVENT_AFTER_EXPIRE_SUBSCRIPTION, - * function(SubscriptionEvent $event) { - * // @var Subscription $subscription - * $subscription = $event->subscription; - * - * // Make a call to third party service to de-authorize a user - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_EXPIRE_SUBSCRIPTION = 'afterExpireSubscription'; - - /** - * @event CreateSubscriptionEvent The event that is triggered before a subscription is created. - * - * You may set the `isValid` property to `false` on the event to prevent the user from being subscribed to the plan. - * - * ```php - * use craft\commerce\events\CreateSubscriptionEvent; - * use craft\commerce\services\Subscriptions; - * use craft\elements\User; - * use craft\commerce\base\Plan; - * use craft\commerce\models\subscriptions\SubscriptionForm; - * use yii\base\Event; - * - * Event::on( - * Subscriptions::class, - * Subscriptions::EVENT_BEFORE_CREATE_SUBSCRIPTION, - * function(CreateSubscriptionEvent $event) { - * // @var User $user - * $user = $event->user; - * // @var Plan $plan - * $plan = $event->plan; - * // @var SubscriptionForm $params - * $params = $event->parameters; - * - * // Set the trial days based on some business logic - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_CREATE_SUBSCRIPTION = 'beforeCreateSubscription'; - - /** - * @event SubscriptionEvent The event that is triggered after a subscription is created. - * - * ```php - * use craft\commerce\events\SubscriptionEvent; - * use craft\commerce\services\Subscriptions; - * use craft\commerce\elements\Subscription; - * use yii\base\Event; - * - * Event::on( - * Subscriptions::class, - * Subscriptions::EVENT_AFTER_CREATE_SUBSCRIPTION, - * function(SubscriptionEvent $event) { - * // @var Subscription $subscription - * $subscription = $event->subscription; - * - * // Call a third party service to authorize a user - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_CREATE_SUBSCRIPTION = 'afterCreateSubscription'; - - /** - * @event SubscriptionEvent TThe event that is triggered before a subscription gets reactivated. - * - * You may set the `isValid` property to `false` on the event to prevent the subscription from being reactivated. - * - * ```php - * use craft\commerce\events\SubscriptionEvent; - * use craft\commerce\services\Subscriptions; - * use craft\commerce\elements\Subscription; - * use yii\base\Event; - * - * Event::on( - * Subscriptions::class, - * Subscriptions::EVENT_BEFORE_REACTIVATE_SUBSCRIPTION, - * function(SubscriptionEvent $event) { - * // @var Subscription $subscription - * $subscription = $event->subscription; - * - * // Use business logic to determine whether the user can reactivate - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_REACTIVATE_SUBSCRIPTION = 'beforeReactivateSubscription'; - - /** - * @event SubscriptionEvent The event that is triggered after a subscription gets reactivated. - * - * ```php - * use craft\commerce\events\SubscriptionEvent; - * use craft\commerce\services\Subscriptions; - * use craft\commerce\elements\Subscription; - * use yii\base\Event; - * - * Event::on( - * Subscriptions::class, - * Subscriptions::EVENT_AFTER_REACTIVATE_SUBSCRIPTION, - * function(SubscriptionEvent $event) { - * // @var Subscription $subscription - * $subscription = $event->subscription; - * - * // Re-authorize the user with a third-party service - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_REACTIVATE_SUBSCRIPTION = 'afterReactivateSubscription'; - - /** - * @event SubscriptionSwitchPlansEvent The event that is triggered before a subscription is switched to a different plan. - * - * You may set the `isValid` property to `false` on the event to prevent the switch from happening. - * - * ```php - * use craft\commerce\events\SubscriptionSwitchPlansEvent; - * use craft\commerce\services\Subscriptions; - * use craft\commerce\base\Plan; - * use craft\commerce\elements\Subscription; - * use craft\commerce\models\subscriptions\SwitchPlansForm; - * use yii\base\Event; - * - * Event::on( - * Subscriptions::class, - * Subscriptions::EVENT_BEFORE_SWITCH_SUBSCRIPTION_PLAN, - * function(SubscriptionSwitchPlansEvent $event) { - * // @var Subscription $subscription - * $subscription = $event->subscription; - * // @var Plan $oldPlan - * $oldPlan = $event->oldPlan; - * // @var Plan $newPlan - * $newPlan = $event->newPlan; - * // @var SwitchPlansForm $params - * $params = $event->parameters; - * - * // Modify the switch parameters based on some business logic - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_SWITCH_SUBSCRIPTION_PLAN = 'beforeSwitchSubscriptionPlan'; - - /** - * @event SubscriptionSwitchPlansEvent The event that is triggered after a subscription gets switched to a different plan. - * - * ```php - * use craft\commerce\events\SubscriptionSwitchPlansEvent; - * use craft\commerce\services\Subscriptions; - * use craft\commerce\base\Plan; - * use craft\commerce\elements\Subscription; - * use craft\commerce\models\subscriptions\SwitchPlansForm; - * use yii\base\Event; - * - * Event::on( - * Subscriptions::class, - * Subscriptions::EVENT_AFTER_SWITCH_SUBSCRIPTION_PLAN, - * function(SubscriptionSwitchPlansEvent $event) { - * // @var Subscription $subscription - * $subscription = $event->subscription; - * // @var Plan $oldPlan - * $oldPlan = $event->oldPlan; - * // @var Plan $newPlan - * $newPlan = $event->newPlan; - * // @var SwitchPlansForm $params - * $params = $event->parameters; - * - * // Adjust the user’s permissions on a third party service - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_SWITCH_SUBSCRIPTION_PLAN = 'afterSwitchSubscriptionPlan'; - - /** - * @event CancelSubscriptionEvent The event that is triggered before a subscription is canceled. - * - * You may set the `isValid` property to `false` on the event to prevent the subscription from being canceled. - * - * ```php - * use craft\commerce\events\CancelSubscriptionEvent; - * use craft\commerce\services\Subscriptions; - * use craft\commerce\elements\Subscription; - * use craft\commerce\models\subscriptions\CancelSubscriptionForm; - * use yii\base\Event; - * - * Event::on( - * Subscriptions::class, - * Subscriptions::EVENT_BEFORE_CANCEL_SUBSCRIPTION, - * function(CancelSubscriptionEvent $event) { - * // @var Subscription $subscription - * $subscription = $event->subscription; - * // @var CancelSubscriptionForm $params - * $params = $event->parameters; - * - * // Check whether the user is permitted to cancel the subscription - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_CANCEL_SUBSCRIPTION = 'beforeCancelSubscription'; - - /** - * @event CancelSubscriptionEvent The event that is triggered after a subscription gets canceled. - * - * ```php - * use craft\commerce\events\CancelSubscriptionEvent; - * use craft\commerce\services\Subscriptions; - * use craft\commerce\elements\Subscription; - * use craft\commerce\models\subscriptions\CancelSubscriptionForm; - * use yii\base\Event; - * - * Event::on( - * Subscriptions::class, - * Subscriptions::EVENT_AFTER_CANCEL_SUBSCRIPTION, - * function(CancelSubscriptionEvent $event) { - * // @var Subscription $subscription - * $subscription = $event->subscription; - * // @var CancelSubscriptionForm $params - * $params = $event->parameters; - * - * // Refund the user for the remainder of the subscription - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_CANCEL_SUBSCRIPTION = 'afterCancelSubscription'; - - /** - * @event SubscriptionEvent The event that is triggered before a subscription gets updated. Typically this event is fired when subscription data is updated on the gateway. - * - * ```php - * use craft\commerce\events\SubscriptionEvent; - * use craft\commerce\services\Subscriptions; - * use craft\commerce\elements\Subscription; - * use yii\base\Event; - * - * Event::on( - * Subscriptions::class, - * Subscriptions::EVENT_BEFORE_UPDATE_SUBSCRIPTION, - * function(SubscriptionEvent $event) { - * // @var Subscription $subscription - * $subscription = $event->subscription; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_UPDATE_SUBSCRIPTION = 'beforeUpdateSubscription'; - - /** - * @event SubscriptionPaymentEvent The event that is triggered when a subscription payment is received. - * - * ```php - * use craft\commerce\events\SubscriptionPaymentEvent; - * use craft\commerce\services\Subscriptions; - * use craft\commerce\elements\Subscription; - * use craft\commerce\models\subscriptions\SubscriptionPayment; - * use DateTime; - * use yii\base\Event; - * - * Event::on( - * Subscriptions::class, - * Subscriptions::EVENT_RECEIVE_SUBSCRIPTION_PAYMENT, - * function(SubscriptionPaymentEvent $event) { - * // @var Subscription $subscription - * $subscription = $event->subscription; - * // @var SubscriptionPayment $payment - * $payment = $event->payment; - * // @var DateTime $until - * $until = $event->paidUntil; - * - * // Update loyalty reward data - * // ... - * } - * ); - * ``` - */ - public const EVENT_RECEIVE_SUBSCRIPTION_PAYMENT = 'receiveSubscriptionPayment'; - - public const CONFIG_FIELDLAYOUT_KEY = 'commerce.subscriptions.fieldLayouts'; - - - /** - * Handle field layout change - * - * @throws Exception - */ - public function handleChangedFieldLayout(ConfigEvent $event): void - { - $data = $event->newValue; - - ProjectConfigHelper::ensureAllFieldsProcessed(); - $fieldsService = Craft::$app->getFields(); - - if (empty($data) || empty(reset($data))) { - // Delete the field layout - $fieldsService->deleteLayoutsByType(Subscription::class); - return; - } - - // Save the field layout - $layout = FieldLayout::createFromConfig(reset($data)); - $layout->id = $fieldsService->getLayoutByType(Subscription::class)->id; - $layout->type = Subscription::class; - $layout->uid = key($data); - $fieldsService->saveLayout($layout, false); - } - - /** - * @deprecated in 3.4.17. Unused fields will be pruned automatically as field layouts are resaved. - */ - public function pruneDeletedField(): void - { - } - - /** - * Handle field layout being deleted - */ - public function handleDeletedFieldLayout(): void - { - Craft::$app->getFields()->deleteLayoutsByType(Subscription::class); - } - - /** - * Prevent deleting a user if they have any subscriptions - active or otherwise. - * - * @param DefineElementDeletionBlockersEvent $event the event. - */ - public function beforeDeleteUserHandler(DefineElementDeletionBlockersEvent $event): void - { - /** @var ElementCollection $subscriptions */ - $subscriptions = Subscription::find() - ->userId($event->elements->ids()->all()) - ->status(null) - ->limit(null) - ->collect(); - - foreach ($subscriptions->groupBy(fn(Subscription $subscription) => (string)($subscription->gatewayId ?? 0)) as $gatewaySubscriptions) { - /** @var Subscription $first */ - $first = $gatewaySubscriptions->first(); - $gateway = $first->getGateway(); - - if (!$gateway instanceof SubscriptionGatewayInterface) { - continue; - } - - $event->blockers[] = new SubscriptionCustomersDeletionBlocker( - $event->elements, - $event->hardDelete, - [ - 'gatewayId' => $first->gatewayId, - 'subscriptions' => $gatewaySubscriptions, - ] - ); - } - } - - /** - * Expire a subscription. - * - * @param Subscription $subscription subscription to expire - * @param DateTime|null $dateTime expiry date time - * @return bool whether successfully expired subscription - * @throws ElementNotFoundException - * @throws Exception - * @throws Throwable if cannot expire subscription - */ - public function expireSubscription(Subscription $subscription, DateTime $dateTime = null): bool - { - $subscription->isExpired = true; - $subscription->dateExpired = $dateTime; - - if (!$subscription->dateExpired) { - $subscription->dateExpired = DateTimeHelper::toDateTime('now'); - } - - Craft::$app->getElements()->saveElement($subscription, false); - - // fire an 'expireSubscription' event - if ($this->hasEventHandlers(self::EVENT_AFTER_EXPIRE_SUBSCRIPTION)) { - $this->trigger(self::EVENT_AFTER_EXPIRE_SUBSCRIPTION, new SubscriptionEvent([ - 'subscription' => $subscription, - ])); - } - - return true; - } - - /** - * Returns subscription count for a plan. - */ - public function getSubscriptionCountByPlanId(int $planId): int - { - return SubscriptionRecord::find()->where(['planId' => $planId])->count(); - } - - /** - * Returns subscription count for a plan. - * - * @deprecated in 4.0. Use [[getSubscriptionCountByPlanId]] instead. - */ - public function getSubscriptionCountForPlanById(int $planId): int - { - return $this->getSubscriptionCountByPlanId($planId); - } - - /** - * Return true if the user has any subscriptions at all, even expired ones. - */ - public function doesUserHaveSubscriptions(int $userId): bool - { - return (bool)SubscriptionRecord::find()->where(['userId' => $userId])->count(); - } - - /** - * Return true if the user has any subscriptions at all, even expired ones. - * - * @deprecated in 4.0. Use [[doesUserHaveSubscriptions]] instead. - */ - public function doesUserHaveAnySubscriptions(int $userId): bool - { - return $this->doesUserHaveSubscriptions($userId); - } - - /** - * Subscribe a user to a subscription plan. - * - * @param User $user the user subscribing to a plan - * @param Plan $plan the plan the user is being subscribed to - * @param SubscriptionForm $parameters array of additional parameters to use - * @param array $fieldValues array of content field values to set - * @return Subscription the subscription - * @throws ElementNotFoundException - * @throws Exception - * @throws InvalidConfigException if the gateway does not support subscriptions - * @throws SubscriptionException if something went wrong during subscription - * @throws Throwable - */ - public function createSubscription(User $user, Plan $plan, SubscriptionForm $parameters, array $fieldValues = []): Subscription - { - $gateway = $plan->getGateway(); - - // fire a 'beforeCreateSubscription' event - $event = new CreateSubscriptionEvent(compact('user', 'plan', 'parameters')); - $this->trigger(self::EVENT_BEFORE_CREATE_SUBSCRIPTION, $event); - - if (!$event->isValid) { - $error = Craft::t('commerce', 'Subscription for {user} to {plan} prevented by a plugin.', [ - 'user' => $user->getFriendlyName(), - 'plan' => (string)$plan, - ]); - - Craft::error($error, __METHOD__); - - throw new SubscriptionException(Craft::t('commerce', 'Unable to subscribe at this time.')); - } - - $response = $gateway->subscribe($user, $plan, $event->parameters); - - $failedToStart = $response->isInactive(); - - $subscription = new Subscription(); - $subscription->userId = $user->id; - $subscription->planId = $plan->id; - $subscription->gatewayId = $plan->gatewayId; - $subscription->orderId = null; - $subscription->reference = $response->getReference(); - $subscription->trialDays = $response->getTrialDays(); - $subscription->nextPaymentDate = $response->getNextPaymentDate(); - $subscription->subscriptionData = $response->getData(); - $subscription->isCanceled = false; - $subscription->isExpired = false; - $subscription->hasStarted = !$failedToStart; - $subscription->isSuspended = $failedToStart; - - if ($failedToStart) { - $subscription->dateSuspended = DateTimeHelper::toDateTime('now'); - } - - $subscription->setFieldValues($fieldValues); - - Craft::$app->getElements()->saveElement($subscription, false); - - // Fire an 'afterCreateSubscription' event. - if ($this->hasEventHandlers(self::EVENT_AFTER_CREATE_SUBSCRIPTION)) { - $this->trigger(self::EVENT_AFTER_CREATE_SUBSCRIPTION, new SubscriptionEvent([ - 'subscription' => $subscription, - ])); - } - - return $subscription; - } - - /** - * Reactivate a subscription. - * - * @throws InvalidConfigException if the gateway does not support subscriptions - * @throws Throwable - * @throws ElementNotFoundException - * @throws Exception - */ - public function reactivateSubscription(Subscription $subscription): bool - { - $gateway = $subscription->getGateway(); - - if (!$gateway instanceof SubscriptionGatewayInterface) { - throw new InvalidConfigException('Gateway does not support subscriptions.'); - } - - // fire a 'beforeReactivateSubscription' event - $event = new SubscriptionEvent([ - 'subscription' => $subscription, - ]); - $this->trigger(self::EVENT_BEFORE_REACTIVATE_SUBSCRIPTION, $event); - - if (!$event->isValid) { - $error = Craft::t('commerce', 'Could not reactivate “{reference}”.', [ - 'reference' => $subscription->reference, - ]); - - Craft::error($error, __METHOD__); - - return false; - } - - $response = $gateway->reactivateSubscription($subscription); - - if (!$response->isScheduledForCancellation()) { - $subscription->isCanceled = false; - $subscription->dateCanceled = null; - $subscription->subscriptionData = $response->getData(); - - Craft::$app->getElements()->saveElement($subscription, false); - - // Fire a 'afterReactivateSubscription' event. - if ($this->hasEventHandlers(self::EVENT_AFTER_REACTIVATE_SUBSCRIPTION)) { - $this->trigger(self::EVENT_AFTER_REACTIVATE_SUBSCRIPTION, new SubscriptionEvent([ - 'subscription' => $subscription, - ])); - } - - return true; - } - - return false; - } - - /** - * Switch a subscription to a different subscription plan. - * - * @param Subscription $subscription the subscription to modify - * @param Plan $plan the plan to change the subscription to - * @param SwitchPlansForm $parameters additional parameters to use - * @throws ElementNotFoundException - * @throws Exception - * @throws InvalidConfigException - * @throws Throwable - */ - public function switchSubscriptionPlan(Subscription $subscription, Plan $plan, SwitchPlansForm $parameters): bool - { - $gateway = $subscription->getGateway(); - - if (!$gateway instanceof SubscriptionGatewayInterface) { - throw new InvalidConfigException('Gateway does not support subscriptions.'); - } - - $oldPlan = $subscription->getPlan(); - - if (!$plan->canSwitchFrom($oldPlan)) { - throw new InvalidConfigException('The migration between these plans is not allowed.'); - } - - // fire a 'beforeSwitchSubscriptionPlan' event - $event = new SubscriptionSwitchPlansEvent([ - 'oldPlan' => $oldPlan, - 'subscription' => $subscription, - 'newPlan' => $plan, - 'parameters' => $parameters, - ]); - $this->trigger(self::EVENT_BEFORE_SWITCH_SUBSCRIPTION_PLAN, $event); - - if (!$event->isValid) { - $error = Craft::t('commerce', 'Could not switch “{reference}” to “{plan}”.', [ - 'reference' => $subscription->reference, - 'plan' => $plan->reference, - ]); - - Craft::error($error, __METHOD__); - - return false; - } - - $response = $gateway->switchSubscriptionPlan($subscription, $plan, $parameters); - - $subscription->planId = $plan->id; - $subscription->nextPaymentDate = $response->getNextPaymentDate(); - $subscription->subscriptionData = $response->getData(); - $subscription->isCanceled = false; - $subscription->isExpired = false; - - Craft::$app->getElements()->saveElement($subscription); - - // fire an 'afterSwitchSubscriptionPlan' event - if ($this->hasEventHandlers(self::EVENT_AFTER_SWITCH_SUBSCRIPTION_PLAN)) { - $this->trigger(self::EVENT_AFTER_SWITCH_SUBSCRIPTION_PLAN, new SubscriptionSwitchPlansEvent([ - 'oldPlan' => $oldPlan, - 'subscription' => $subscription, - 'newPlan' => $plan, - 'parameters' => $parameters, - ])); - } - - return true; - } - - /** - * Cancel a subscription. - * - * @throws InvalidConfigException if the gateway does not support subscriptions - * @throws SubscriptionException if something went wrong when canceling subscription - */ - public function cancelSubscription(Subscription $subscription, CancelSubscriptionForm $parameters): bool - { - $gateway = $subscription->getGateway(); - - if (!$gateway instanceof SubscriptionGatewayInterface) { - throw new InvalidConfigException('Gateway does not support subscriptions.'); - } - - // fire a 'beforeCancelSubscription' event - $event = new CancelSubscriptionEvent(compact('subscription', 'parameters')); - $this->trigger(self::EVENT_BEFORE_CANCEL_SUBSCRIPTION, $event); - - if (!$event->isValid) { - $error = Craft::t('commerce', 'Could not cancel “{reference}”.', [ - 'reference' => $subscription->reference, - ]); - - Craft::error($error, __METHOD__); - - return false; - } - - $response = $gateway->cancelSubscription($subscription, $parameters); - - if ($response->isCanceled() || $response->isScheduledForCancellation()) { - if ($response->isScheduledForCancellation()) { - $subscription->isCanceled = true; - $subscription->dateCanceled = DateTimeHelper::toDateTime('now'); - } - - if ($response->isCanceled()) { - $subscription->isExpired = true; - $subscription->isCanceled = true; - $subscription->dateCanceled = DateTimeHelper::toDateTime('now'); - $subscription->dateExpired = DateTimeHelper::toDateTime('now'); - } - - $subscription->setSubscriptionData($response->getData()); - - try { - Craft::$app->getElements()->saveElement($subscription, false); - - // fire an 'afterCancelSubscription' event - if ($this->hasEventHandlers(self::EVENT_AFTER_CANCEL_SUBSCRIPTION)) { - $this->trigger(self::EVENT_AFTER_CANCEL_SUBSCRIPTION, new CancelSubscriptionEvent(compact('subscription', 'parameters'))); - } - } catch (Throwable $exception) { - Craft::warning('Failed to cancel subscription ' . $subscription->reference . ': ' . $exception->getMessage()); - - throw new SubscriptionException(Craft::t('commerce', 'Unable to cancel subscription at this time.')); - } - } - - return true; - } - - /** - * Update a subscription. - * - * @throws Throwable - * @throws ElementNotFoundException - * @throws Exception - */ - public function updateSubscription(Subscription $subscription): bool - { - if ($this->hasEventHandlers(self::EVENT_BEFORE_UPDATE_SUBSCRIPTION)) { - $this->trigger(self::EVENT_BEFORE_UPDATE_SUBSCRIPTION, new SubscriptionEvent([ - 'subscription' => $subscription, - ])); - } - - return Craft::$app->getElements()->saveElement($subscription); - } - - /** - * Receive a payment for a subscription - * - * @throws Throwable - * @throws ElementNotFoundException - * @throws Exception - */ - public function receivePayment(Subscription $subscription, SubscriptionPayment $payment, DateTime $paidUntil): bool - { - if ($this->hasEventHandlers(self::EVENT_RECEIVE_SUBSCRIPTION_PAYMENT)) { - $this->trigger(self::EVENT_RECEIVE_SUBSCRIPTION_PAYMENT, new SubscriptionPaymentEvent(compact('subscription', 'payment', 'paidUntil'))); - } - - $subscription->nextPaymentDate = $paidUntil; - - return Craft::$app->getElements()->saveElement($subscription); - } -} diff --git a/src/services/TaxCategories.php b/src/services/TaxCategories.php deleted file mode 100644 index 3495f0921f..0000000000 --- a/src/services/TaxCategories.php +++ /dev/null @@ -1,309 +0,0 @@ - - * @since 2.0 - */ -class TaxCategories extends Component -{ - /** - * @var TaxCategory[]|null - */ - private ?array $_allTaxCategories = null; - - /** - * @var TaxCategory[]|null - */ - private ?array $_allTaxCategoriesWithTrashed = null; - - /** - * Returns all Tax Categories - * @param bool $withTrashed - * @return TaxCategory[] - */ - public function getAllTaxCategories(bool $withTrashed = false): array - { - if ($this->_allTaxCategories === null || $this->_allTaxCategoriesWithTrashed === null) { - $results = $this->_createTaxCategoryQuery(true)->all(); - - $this->_allTaxCategories = []; - foreach ($results as $result) { - $taxCategory = new TaxCategory($result); - - if (!$taxCategory->dateDeleted) { - $this->_allTaxCategories[] = $taxCategory; - } - $this->_allTaxCategoriesWithTrashed[] = $taxCategory; - } - } - - return $withTrashed ? $this->_allTaxCategoriesWithTrashed : $this->_allTaxCategories; - } - - /** - * Get a tax category by its ID. - */ - public function getTaxCategoryById(int $taxCategoryId): ?TaxCategory - { - $categories = $this->getAllTaxCategories(); - - return ArrayHelper::firstWhere($categories, 'id', $taxCategoryId); - } - - /** - * Get a tax category by its handle. - * - * @noinspection PhpUnused - */ - public function getTaxCategoryByHandle(string $taxCategoryHandle): ?TaxCategory - { - $categories = $this->getAllTaxCategories(); - - return ArrayHelper::firstWhere($categories, 'handle', $taxCategoryHandle); - } - - /** - * Returns all Tax category names, indexed by ID. - */ - public function getAllTaxCategoriesAsList(): array - { - $categories = $this->getAllTaxCategories(); - - return ArrayHelper::map($categories, 'id', 'uiLabel'); - } - - /** - * Get the default tax category - * - * @throws InvalidConfigException - */ - public function getDefaultTaxCategory(): TaxCategory - { - $categories = $this->getAllTaxCategories(); - - $default = ArrayHelper::firstWhere($categories, 'default', true); - - if (!$default) { - $default = ArrayHelper::firstValue($categories); - } - - if (!$default) { - throw new InvalidConfigException('Commerce must have at least one (default) tax category set up.'); - } - - return $default; - } - - /** - * Save a tax category. - * - * @param bool $runValidation should we validate this state before saving. - * @throws Exception - * @throws \Exception - */ - public function saveTaxCategory(TaxCategory $taxCategory, bool $runValidation = true): bool - { - if ($taxCategory->id) { - $record = TaxCategoryRecord::findOne($taxCategory->id); - - if (!$record) { - throw new Exception(Craft::t('commerce', 'No tax category exists with the ID “{id}”', - ['id' => $taxCategory->id])); - } - } else { - $record = new TaxCategoryRecord(); - } - - if ($runValidation && !$taxCategory->validate()) { - Craft::info('Tax category not saved due to validation error.', __METHOD__); - - return false; - } - - $record->name = $taxCategory->name; - $record->handle = $taxCategory->handle; - $record->description = $taxCategory->description; - $record->icon = $taxCategory->icon; - $record->color = $taxCategory->color; - $record->default = $taxCategory->default; - - // Save it! - $record->save(false); - - // Now that we have a record ID, save it on the model - $taxCategory->id = $record->id; - - // If this was the default make all others not the default. - if ($taxCategory->default) { - TaxCategoryRecord::updateAll(['default' => false], ['not', ['id' => $record->id]]); - } - - // Product type IDs this tax category is available to - $currentProductTypeIds = (new Query()) - ->select(['productTypeId']) - ->from([Table::PRODUCTTYPES_TAXCATEGORIES]) - ->where(['taxCategoryId' => $taxCategory->id]) - ->column(); - - // Newly set product types this tax category is available to - $newProductTypeIds = ArrayHelper::getColumn($taxCategory->getProductTypes(), 'id'); - - foreach ($currentProductTypeIds as $oldProductTypeId) { - // If we are removing a product type for this tax category the products of that type should be re-saved - if (!in_array($oldProductTypeId, $newProductTypeIds, false)) { - // Re-save all products that no longer have this tax category available to them - $this->_resaveProductsByProductTypeId($oldProductTypeId); - } - } - - foreach ($newProductTypeIds as $newProductTypeId) { - // If we are adding a product type for this tax category the products of that type should be re-saved - if (!in_array($newProductTypeId, $currentProductTypeIds, false)) { - // Re-save all products when assigning this tax category available to them - $this->_resaveProductsByProductTypeId($newProductTypeId); - } - } - - // Remove existing Categories <-> ProductType relationships - Craft::$app->getDb()->createCommand()->delete(Table::PRODUCTTYPES_TAXCATEGORIES, ['taxCategoryId' => $record->id])->execute(); - - foreach ($taxCategory->getProductTypes() as $productType) { - $data = ['productTypeId' => (int)$productType->id, 'taxCategoryId' => $taxCategory->id]; - Craft::$app->getDb()->createCommand()->insert(Table::PRODUCTTYPES_TAXCATEGORIES, $data)->execute(); - } - - // Clear Service cache - $this->_allTaxCategories = null; - - return true; - } - - /** - * Re-save products by product type id - */ - private function _resaveProductsByProductTypeId(int $productTypeId): void - { - Craft::$app->getQueue()->push(new ResaveElements([ - 'elementType' => Product::class, - 'criteria' => [ - 'typeId' => $productTypeId, - 'siteId' => '*', - 'unique' => true, - 'status' => null, - ], - ])); - } - - /** - * @param int $id - * @return bool - * @throws StaleObjectException - */ - public function deleteTaxCategoryById(int $id): bool - { - /** @var TaxCategoryRecord|SoftDeleteBehavior|null $taxCategory */ - $taxCategory = TaxCategoryRecord::findOne($id); - - if ($taxCategory === null || $taxCategory->default) { - return false; - } - - if ($taxCategory->softDelete()) { - $this->_allTaxCategories = null; - return true; - } - - return false; - } - - /** - * @param int $productTypeId - * @return array - */ - public function getTaxCategoriesByProductTypeId(int $productTypeId): array - { - $rows = $this->_createTaxCategoryQuery() - ->innerJoin(Table::PRODUCTTYPES_TAXCATEGORIES . ' productTypeTaxCategories', '[[taxCategories.id]] = [[productTypeTaxCategories.taxCategoryId]]') - ->andWhere(['productTypeTaxCategories.productTypeId' => $productTypeId]) - ->all(); - - if (empty($rows)) { - try { - $taxCategory = $this->getDefaultTaxCategory(); - } catch (InvalidConfigException) { - return []; - } - - return [$taxCategory->id => $taxCategory]; - } - - $taxCategories = []; - - foreach ($rows as $row) { - $key = $row['id']; - $taxCategories[$key] = new TaxCategory($row); - } - - return $taxCategories; - } - - /** - * Returns a Query object prepped for retrieving tax categories. - */ - private function _createTaxCategoryQuery(bool $withTrashed = false): Query - { - $query = (new Query()) - ->select([ - 'taxCategories.dateCreated', - 'taxCategories.dateDeleted', - 'taxCategories.dateUpdated', - 'taxCategories.default', - 'taxCategories.description', - 'taxCategories.handle', - 'taxCategories.id', - 'taxCategories.name', - ]) - ->from([Table::TAXCATEGORIES . ' taxCategories']); - - // Only add icon and color if the columns exist (for pre-migration compatibility) - $db = Craft::$app->getDb(); - $schema = $db->getSchema(); - $tableSchema = $schema->getTableSchema(Table::TAXCATEGORIES); - - if ($tableSchema && $tableSchema->getColumn('icon') !== null) { - $query->addSelect(['taxCategories.icon', 'taxCategories.color']); - } - - if (!$withTrashed) { - $query->where(['dateDeleted' => null]); - } - - return $query; - } -} diff --git a/src/services/TaxRates.php b/src/services/TaxRates.php deleted file mode 100644 index 75458289ae..0000000000 --- a/src/services/TaxRates.php +++ /dev/null @@ -1,248 +0,0 @@ - - * @since 2.0 - */ -class TaxRates extends Component -{ - /** - * @var Collection[]|null - */ - private ?array $_allTaxRates = null; - - /** - * Returns an array of all existing tax rates. - * - * @param int|null $storeId - * @return Collection - * @throws StoreNotFoundException - * @throws InvalidConfigException - */ - public function getAllTaxRates(?int $storeId = null): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if ($this->_allTaxRates === null || !isset($this->_allTaxRates[$storeId])) { - $results = $this->_createTaxRatesQuery() - ->where(['storeId' => $storeId]) - ->all(); - - if ($this->_allTaxRates === null) { - $this->_allTaxRates = []; - } - - foreach ($results as $result) { - $taxRate = Craft::createObject([ - 'class' => TaxRate::class, - 'attributes' => $result, - ]); - - if (!isset($this->_allTaxRates[$taxRate->storeId])) { - $this->_allTaxRates[$taxRate->storeId] = collect(); - } - - $this->_allTaxRates[$taxRate->storeId]->push($taxRate); - } - } - - return $this->_allTaxRates[$storeId] ?? collect(); - } - - /** - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - * @throws StoreNotFoundException - * @since 5.3.0 - */ - public function getAllEnabledTaxRates(?int $storeId = null): Collection - { - return $this->getAllTaxRates($storeId)->where('enabled', true); - } - - /** - * Returns an array of all rates belonging to the specified zone. - * - * @param int $taxZoneId The ID of the tax zone whose rates we’d like returned - * @param int|null $storeId - * @return Collection - * @throws InvalidConfigException - * @throws StoreNotFoundException - */ - public function getTaxRatesByTaxZoneId(int $taxZoneId, ?int $storeId = null): Collection - { - return $this->getAllTaxRates($storeId)->where('taxZoneId', $taxZoneId); - } - - /** - * Returns a tax rate by ID. - * - * @param int $id The ID of the desired tax rate - * @param int|null $storeId - * @return ?TaxRate - * @throws InvalidConfigException - * @throws StoreNotFoundException - */ - public function getTaxRateById(int $id, ?int $storeId = null): ?TaxRate - { - return $this->getAllTaxRates($storeId)->firstWhere('id', $id); - } - - /** - * Saves a tax rate. - * - * @param TaxRate $model The tax rate model to be saved - * @param bool $runValidation Whether we should validate this rate before saving - * @return bool - * @throws Exception - * @throws \Exception - */ - public function saveTaxRate(TaxRate $model, bool $runValidation = true): bool - { - if ($model->id) { - $record = TaxRateRecord::findOne($model->id); - - if (!$record) { - throw new Exception(Craft::t('commerce', 'No tax rate exists with the ID “{id}”', - ['id' => $model->id])); - } - } else { - $record = new TaxRateRecord(); - } - - if ($runValidation && !$model->validate()) { - Craft::info('Tax rate not saved due to validation error.', __METHOD__); - - return false; - } - - $record->name = $model->name; - $record->code = $model->code; - $record->rate = $model->rate; - $record->storeId = $model->storeId; - - // if not an included tax, then can not be removed. - $record->include = $model->include; - $record->isVat = $model->hasTaxIdValidators(); - $record->removeIncluded = !$record->include ? false : $model->removeIncluded; - $record->removeVatIncluded = (!$record->include || !$record->isVat) ? false : $model->removeVatIncluded; - $record->taxable = $model->taxable; - $record->taxCategoryId = $model->taxCategoryId; - $record->taxZoneId = $model->taxZoneId ?: null; - $record->isEverywhere = $model->getIsEverywhere(); - $record->enabled = $model->enabled; - $record->taxIdValidators = $model->taxIdValidators; - - if (!$record->isEverywhere && $record->taxZoneId && empty($record->getErrors('taxZoneId'))) { - $taxZone = Plugin::getInstance()->getTaxZones()->getTaxZoneById($record->taxZoneId, $record->storeId); - - if (!$taxZone) { - throw new Exception(Craft::t('commerce', 'No tax zone exists with the ID “{id}”', ['id' => $record->taxZoneId])); - } - - if ($record->removeIncluded && !$taxZone->default) { - $model->addError('removeIncluded', Craft::t('commerce', 'Removable included tax rates are only allowed for the default tax zone.')); - - return false; - } - } - - // Save it! - $record->save(false); - - // Now that we have a record ID, save it on the model - $model->id = $record->id; - $this->clearCache(); - - return true; - } - - /** - * Deletes a tax rate by ID. - * - * @throws Throwable - * @throws StaleObjectException - */ - public function deleteTaxRateById(int $id): bool - { - $record = TaxRateRecord::findOne($id); - - if ($record) { - $this->clearCache(); - return (bool)$record->delete(); - } - - return false; - } - - /** - * Returns a Query object prepped for retrieving tax rates - */ - private function _createTaxRatesQuery(): Query - { - $query = (new Query()) - ->select([ - 'code', - 'dateCreated', - 'dateUpdated', - 'id', - 'include', - 'name', - 'rate', - 'removeIncluded', - 'removeVatIncluded', - 'storeId', - 'taxable', - 'taxCategoryId', - 'taxZoneId', - ]) - ->orderBy(['include' => SORT_DESC, 'isVat' => SORT_DESC]) - ->from([Table::TAXRATES]); - - // if enabled column exists add the select - if (Craft::$app->getDb()->columnExists(Table::TAXRATES, 'enabled')) { - $query->addSelect(['enabled']); - } - - // add taxIdValidators select - if (Craft::$app->getDb()->columnExists(Table::TAXRATES, 'taxIdValidators')) { - $query->addSelect(['taxIdValidators']); - } - - return $query; - } - - /** - * @return void - * @since 5.0.0 - */ - protected function clearCache(): void - { - $this->_allTaxRates = null; - } -} diff --git a/src/services/TaxZones.php b/src/services/TaxZones.php deleted file mode 100644 index 58c62bdad2..0000000000 --- a/src/services/TaxZones.php +++ /dev/null @@ -1,181 +0,0 @@ - - * @since 2.0 - */ -class TaxZones extends Component -{ - /** - * @var Collection[] - */ - private ?array $_allZones = null; - - /** - * Get all tax zones. - * - * @param int|null $storeId - * @return Collection - * @throws StoreNotFoundException - * @throws InvalidConfigException - */ - public function getAllTaxZones(?int $storeId = null): Collection - { - $storeId ??= Plugin::getInstance()->getStores()->getCurrentStore()->id; - - if ($this->_allZones === null || !isset($this->_allZones[$storeId])) { - $results = $this->_createQuery() - ->where(['storeId' => $storeId]) - ->all(); - - if ($this->_allZones === null) { - $this->_allZones = []; - } - - foreach ($results as $result) { - $taxRate = Craft::createObject([ - 'class' => TaxAddressZone::class, - 'attributes' => $result, - ]); - - if (!isset($this->_allZones[$taxRate->storeId])) { - $this->_allZones[$taxRate->storeId] = collect(); - } - - $this->_allZones[$taxRate->storeId]->push($taxRate); - } - } - - return $this->_allZones[$storeId] ?? collect(); - } - - /** - * Get a tax zone by its ID. - */ - public function getTaxZoneById(int $id, ?int $storeId = null): ?TaxAddressZone - { - return $this->getAllTaxZones($storeId)->firstWhere('id', $id); - } - - /** - * Save a tax zone. - * - * @param bool $runValidation should we validate this zone before saving - * @throws \Exception - * @throws Exception - */ - public function saveTaxZone(TaxAddressZone $model, bool $runValidation = true): bool - { - if ($model->id) { - $record = TaxZoneRecord::findOne($model->id); - - if (!$record) { - throw new Exception(Craft::t('commerce', 'No tax zone exists with the ID “{id}”', ['id' => $model->id])); - } - } else { - $record = new TaxZoneRecord(); - } - - if ($runValidation && !$model->validate()) { - Craft::info('Tax zone not saved due to validation error.', __METHOD__); - - return false; - } - - //setting attributes - $record->storeId = $model->storeId; - $record->name = $model->name; - $record->description = $model->description; - $record->default = $model->default; - $record->condition = $model->getCondition()->getConfig(); - - $record->save(); - - $model->id = $record->id; - - // If this was the default make all others not the default. - if ($model->default) { - TaxZoneRecord::updateAll( - ['default' => false], - ['and', ['not', ['id' => $model->id]], ['storeId' => $model->storeId]]); - } - - $this->_clearCaches(); - - return true; - } - - /** - * @throws Throwable - * @throws StaleObjectException - */ - public function deleteTaxZoneById(int $id): bool - { - $record = TaxZoneRecord::findOne($id); - - if ($record) { - $result = (bool)$record->delete(); - if ($result) { - $this->_clearCaches(); - } - - return $result; - } - - return false; - } - - /** - * Returns a Query object prepped for retrieving tax zones. - */ - private function _createQuery(): Query - { - return (new Query()) - ->select([ - 'condition', - 'dateCreated', - 'dateUpdated', - 'default', - 'description', - 'id', - 'name', - 'storeId', - ]) - ->orderBy('name') - ->from([Table::TAXZONES]); - } - - /** - * Clear memoization. - * - * @since 3.2.5 - */ - private function _clearCaches(): void - { - $this->_allZones = []; - } -} diff --git a/src/services/Taxes.php b/src/services/Taxes.php deleted file mode 100644 index 74211ccb52..0000000000 --- a/src/services/Taxes.php +++ /dev/null @@ -1,276 +0,0 @@ -validators[] = new MyTaxIdValidator(); - * } - * ); - * ``` - */ - public const EVENT_REGISTER_TAX_ID_VALIDATORS = 'registerTaxIdValidators'; - - /** - * @event TaxEngineEvent The event that is triggered when determining the tax engine. - * @since 3.1 - * - * ```php - * use craft\commerce\base\TaxEngineInterface; - * use craft\commerce\engines\Tax; - * use craft\commerce\events\TaxEngineEvent; - * use craft\commerce\services\Taxes; - * use yii\base\Event; - * - * Event::on( - * Taxes::class, - * Taxes::EVENT_REGISTER_TAX_ENGINE, - * function(TaxEngineEvent $event) { - * // @var TaxEngineInterface $currentEngine - * $currentEngine = $event->engine; - * - * // Set a new tax engine on `$event->engine` - * // ... - * } - * ); - * ``` - */ - public const EVENT_REGISTER_TAX_ENGINE = 'registerTaxEngine'; - - /** - * @var ?TaxEngineInterface $engine The tax engine - */ - private ?TaxEngineInterface $_taxEngine = null; - - /** - * @return Collection - * @throws InvalidConfigException - * @since 5.3.0 - */ - public function getTaxIdValidators(): Collection - { - $validators = []; - $validators[] = new EuVatIdValidator(); - - $event = new TaxIdValidatorsEvent([ - 'validators' => $validators, - ]); - - if ($this->hasEventHandlers(self::EVENT_REGISTER_TAX_ID_VALIDATORS)) { - $this->trigger(self::EVENT_REGISTER_TAX_ID_VALIDATORS, $event); - } - - foreach ($event->validators as $validator) { - if (!$validator instanceof TaxIdValidatorInterface) { - throw new InvalidConfigException('Tax ID validator must implement TaxIdValidatorInterface'); - } - } - - return collect($event->validators); - } - - /** - * @return Collection - * @throws InvalidConfigException - */ - public function getEnabledTaxIdValidators(): Collection - { - return $this->getTaxIdValidators()->filter(fn(TaxIdValidatorInterface $validator) => $validator::isEnabled()); - } - - /** - * Get the current tax engine. - */ - public function getEngine(): TaxEngineInterface - { - if ($this->_taxEngine !== null) { - return $this->_taxEngine; - } - - $event = new TaxEngineEvent(['engine' => new Tax()]); - - if ($this->hasEventHandlers(self::EVENT_REGISTER_TAX_ENGINE)) { - $this->trigger(self::EVENT_REGISTER_TAX_ENGINE, $event); - } - - // Give plugins a chance to register the tax engine - if (!$event->engine instanceof TaxEngineInterface) { - throw new InvalidConfigException('No tax engine has been registered.'); - } - - $this->_taxEngine = $event->engine; - - return $this->_taxEngine; - } - - /** - * @inheritDoc - */ - public function taxAdjusterClass(): string - { - return $this->getEngine()->taxAdjusterClass(); - } - - /** - * @inheritDoc - */ - public function viewTaxCategories(): bool - { - return $this->getEngine()->viewTaxCategories(); - } - - /** - * @inheritDoc - */ - public function createTaxCategories(): bool - { - return $this->getEngine()->createTaxCategories(); - } - - /** - * @inheritDoc - */ - public function editTaxCategories(): bool - { - return $this->getEngine()->editTaxCategories(); - } - - /** - * @inheritDoc - */ - public function deleteTaxCategories(): bool - { - return $this->getEngine()->deleteTaxCategories(); - } - - /** - * @inheritDoc - */ - public function taxCategoryActionHtml(): string - { - return $this->getEngine()->taxCategoryActionHtml(); - } - - /** - * @inheritDoc - */ - public function viewTaxZones(): bool - { - return $this->getEngine()->viewTaxZones(); - } - - /** - * @inheritDoc - */ - public function editTaxZones(): bool - { - return $this->getEngine()->editTaxZones(); - } - - /** - * @inheritDoc - */ - public function viewTaxRates(): bool - { - return $this->getEngine()->viewTaxRates(); - } - - /** - * @inheritDoc - */ - public function editTaxRates(): bool - { - return $this->getEngine()->editTaxRates(); - } - - /** - * @inheritDoc - */ - public function cpTaxNavSubItems(): array - { - return $this->getEngine()->cpTaxNavSubItems(); - } - - /** - * @inheritDoc - */ - public function createTaxZones(): bool - { - return $this->getEngine()->createTaxZones(); - } - - /** - * @inheritDoc - */ - public function deleteTaxZones(): bool - { - return $this->getEngine()->deleteTaxZones(); - } - - /** - * @inheritDoc - */ - public function taxZoneActionHtml(): string - { - return $this->getEngine()->taxZoneActionHtml(); - } - - /** - * @inheritDoc - */ - public function createTaxRates(): bool - { - return $this->getEngine()->createTaxRates(); - } - - /** - * @inheritDoc - */ - public function deleteTaxRates(): bool - { - return $this->getEngine()->deleteTaxRates(); - } - - /** - * @inheritDoc - */ - public function taxRateActionHtml(): string - { - return $this->getEngine()->taxRateActionHtml(); - } -} diff --git a/src/services/Transactions.php b/src/services/Transactions.php deleted file mode 100644 index f994771c60..0000000000 --- a/src/services/Transactions.php +++ /dev/null @@ -1,565 +0,0 @@ - - * @since 2.0 - */ -class Transactions extends Component -{ - /** - * @event TransactionEvent The event that is triggered after a transaction has been saved. - * - * ```php - * use craft\commerce\events\TransactionEvent; - * use craft\commerce\services\Transactions; - * use craft\commerce\models\Transaction; - * use yii\base\Event; - * - * Event::on( - * Transactions::class, - * Transactions::EVENT_AFTER_SAVE_TRANSACTION, - * function(TransactionEvent $event) { - * // @var Transaction $transaction - * $transaction = $event->transaction; - * - * // Run custom logic for failed transactions - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_SAVE_TRANSACTION = 'afterSaveTransaction'; - - /** - * @event TransactionEvent The event that is triggered after a transaction has been created. - * - * ```php - * use craft\commerce\events\TransactionEvent; - * use craft\commerce\services\Transactions; - * use craft\commerce\models\Transaction; - * use yii\base\Event; - * - * Event::on( - * Transactions::class, - * Transactions::EVENT_AFTER_CREATE_TRANSACTION, - * function(TransactionEvent $event) { - * // @var Transaction $transaction - * $transaction = $event->transaction; - * - * // Run custom logic depending on the transaction type - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_CREATE_TRANSACTION = 'afterCreateTransaction'; - - - /** - * Returns true if a specific transaction can be refunded. - * - * @param Transaction $transaction the transaction - */ - public function canCaptureTransaction(Transaction $transaction): bool - { - // Can only capture successful authorize transactions - if ($transaction->type !== TransactionRecord::TYPE_AUTHORIZE || $transaction->status !== TransactionRecord::STATUS_SUCCESS) { - return false; - } - - $gateway = $transaction->getGateway(); - - if (!$gateway) { - return false; - } - - if (!$gateway->supportsCapture()) { - return false; - } - - // And only if we don't have a successful refund transaction for this order already - return !$this->_createTransactionQuery() - ->where([ - 'type' => TransactionRecord::TYPE_CAPTURE, - 'status' => TransactionRecord::STATUS_SUCCESS, - 'orderId' => $transaction->orderId, - 'parentId' => $transaction->id, - ]) - ->exists(); - } - - /** - * Returns true if a specific transaction can be refunded. - * - * @param Transaction $transaction the transaction - */ - public function canRefundTransaction(Transaction $transaction): bool - { - // Can refund only successful purchase or capture transactions - if (!in_array($transaction->type, [TransactionRecord::TYPE_PURCHASE, TransactionRecord::TYPE_CAPTURE], true)) { - return false; - } - - if ($transaction->status !== TransactionRecord::STATUS_SUCCESS) { - return false; - } - - $gateway = $transaction->getGateway(); - - if (!$gateway) { - return false; - } - - if (!$gateway->supportsRefund()) { - return false; - } - - // Allow gateways to help determine if a transaction can be refunded - if (!$gateway->transactionSupportsRefund($transaction)) { - return false; - } - - return ($this->refundableAmountForTransaction($transaction) > 0); - } - - /** - * Return the refundable amount for a transaction. - */ - public function refundableAmountForTransaction(Transaction $transaction): float - { - // We need to use the payment currency to calculate the refundable amount - $teller = Plugin::getInstance()->getCurrencies()->getTeller($transaction->paymentCurrency); - - $amount = (new Query()) - ->where([ - 'type' => TransactionRecord::TYPE_REFUND, - 'status' => TransactionRecord::STATUS_SUCCESS, - 'orderId' => $transaction->orderId, - 'parentId' => $transaction->id, - ]) - ->from([Table::TRANSACTIONS]) - ->sum('[[paymentAmount]]'); - - return (float)$teller->subtract($transaction->paymentAmount, $amount); - } - - /** - * Create a transaction either from an order or a parent transaction. At least one must be present. - * - * @param Order|null $order Order that the transaction is a part of. Ignored, if `$parentTransaction` is specified. - * @param Transaction|null $parentTransaction Parent transaction, if this transaction is a child. Required, if `$order` is not specified. - * @param string|null $typeOverride The type of transaction. If set, this overrides the type of the parent transaction, or sets the type when no parentTransaction is passed. - * @throws TransactionException if neither `$order` or `$parentTransaction` is specified. - * @throws CurrencyException - * @throws InvalidConfigException - */ - public function createTransaction(Order $order = null, Transaction $parentTransaction = null, ?string $typeOverride = null): Transaction - { - if (!$order && !$parentTransaction) { - throw new TransactionException('Tried to create a transaction without order or parent transaction'); - } - - $transaction = new Transaction(); - $transaction->status = TransactionRecord::STATUS_PENDING; - - if ($parentTransaction) { - // Assume parent values instead of Order values. - $transaction->parentId = $parentTransaction->id; - $transaction->gatewayId = $parentTransaction->gatewayId; - $transaction->amount = $parentTransaction->amount; - $transaction->currency = $parentTransaction->currency; - $transaction->paymentAmount = $parentTransaction->paymentAmount; - $transaction->paymentCurrency = $parentTransaction->paymentCurrency; - $transaction->paymentRate = $parentTransaction->paymentRate; - $transaction->setOrder($parentTransaction->getOrder()); - $transaction->reference = $parentTransaction->reference; - $transaction->type = $parentTransaction->type; - } else { - $paymentCurrency = Plugin::getInstance()->getPaymentCurrencies()->getPaymentCurrencyByIso($order->paymentCurrency, $order->getStore()->id); - $currency = Plugin::getInstance()->getPaymentCurrencies()->getPaymentCurrencyByIso($order->currency, $order->getStore()->id); - - /** @var Gateway $gateway */ - $gateway = $order->getGateway(); - $transaction->gatewayId = $gateway->id; - - // Gets the outstanding balance, unless the order had a paymentAmount set in this request - $transaction->currency = $currency->iso; - $transaction->paymentCurrency = $paymentCurrency->iso; - - // Payment amount is the amount in the paymentCurrency - $transaction->paymentAmount = Currency::round($order->getPaymentAmount(), $paymentCurrency); - $amount = $transaction->paymentAmount; - - if ($currency->iso !== $paymentCurrency->iso) { - $tellerTo = Plugin::getInstance()->getCurrencies()->getTeller($paymentCurrency); - $paymentAmount = $tellerTo->convertToMoney($transaction->paymentAmount); - $amount = Plugin::getInstance()->getPaymentCurrencies()->convertAmount($paymentAmount, $currency, $order->getStore()->id); - $amount = (float)$tellerTo->convertToString($amount); - } - - // Amount is always in the base currency - $transaction->amount = $amount; - - $transaction->setOrder($order); - - // Capture historical rate - $transaction->paymentRate = Plugin::getInstance()->getPaymentCurrencies()->getRateFor($paymentCurrency, $transaction); - } - - $user = Craft::$app->getUser()->getIdentity(); - - if ($user) { - $transaction->userId = $user->id; - } - - if ($typeOverride) { - $transaction->type = $typeOverride; - } - - // Raise 'afterCreateTransaction' event - if ($this->hasEventHandlers(self::EVENT_AFTER_CREATE_TRANSACTION)) { - $this->trigger(self::EVENT_AFTER_CREATE_TRANSACTION, new TransactionEvent([ - 'transaction' => $transaction, - ])); - } - - return $transaction; - } - - /** - * Delete a transaction. - * - * @param Transaction $transaction the transaction to delete - * @throws Throwable - * @throws StaleObjectException - * @deprecated in 4.0. Use [[deleteTransactionById]] instead. - */ - public function deleteTransaction(Transaction $transaction): bool - { - $record = TransactionRecord::findOne($transaction->id); - - if ($record) { - return (bool)$record->delete(); - } - - return false; - } - - /** - * Delete a transaction by id. - * - * @param int $id the transaction ID - * @throws Throwable - * @throws StaleObjectException - */ - public function deleteTransactionById(int $id): bool - { - $record = TransactionRecord::findOne($id); - - if ($record) { - return (bool)$record->delete(); - } - - return false; - } - - /** - * @param int $orderId the order's ID - * @return array - * @noinspection PhpUnused - */ - public function getAllTopLevelTransactionsByOrderId(int $orderId): array - { - $transactions = $this->getAllTransactionsByOrderId($orderId); - - foreach ($transactions as $key => $transaction) { - // Remove transactions that have a parentId - if ($transaction->parentId) { - unset($transactions[$key]); - } - } - - return $transactions; - } - - /** - * Returns all transactions for an order, per the order's ID. - * - * @param int $orderId the order's ID - * @return Transaction[] - */ - public function getAllTransactionsByOrderId(int $orderId): array - { - $rows = $this->_createTransactionQuery() - ->where(['orderId' => $orderId]) - ->all(); - - $transactions = []; - - foreach ($rows as $row) { - $transactions[] = new Transaction($row); - } - - return $transactions; - } - - /** - * Get all children transactions, per a parent transaction's ID. - * - * @param int $transactionId the parent transaction's ID - */ - public function getChildrenByTransactionId(int $transactionId): array - { - $rows = $this->_createTransactionQuery() - ->where(['parentId' => $transactionId]) - ->all(); - - $transactions = []; - - foreach ($rows as $row) { - $transactions[] = new Transaction($row); - } - - return $transactions; - } - - /** - * Get a transaction by its hash. - * - * @param string $hash the hash of transaction - */ - public function getTransactionByHash(string $hash): ?Transaction - { - $result = $this->_createTransactionQuery() - ->where(['hash' => $hash]) - ->one(); - - return $result ? new Transaction($result) : null; - } - - /** - * Get a transaction by its reference and status. - * - * @param string $reference the transaction reference - * @param string $status the transaction status - */ - public function getTransactionByReferenceAndStatus(string $reference, string $status): ?Transaction - { - $result = $this->_createTransactionQuery() - ->where(compact('reference', 'status')) - ->one(); - - return $result ? new Transaction($result) : null; - } - - /** - * Get a transaction by its reference. - * - * @param string $reference the transaction reference - * @return Transaction|null - */ - public function getTransactionByReference(string $reference): ?Transaction - { - $result = $this->_createTransactionQuery() - ->where(compact('reference')) - ->one(); - - return $result ? new Transaction($result) : null; - } - - /** - * Get a transaction by its ID. - * - * @param int $id the ID of transaction - */ - public function getTransactionById(int $id): ?Transaction - { - $result = $this->_createTransactionQuery() - ->where(['id' => $id]) - ->one(); - - return $result ? new Transaction($result) : null; - } - - /** - * Returns true if a transaction or a direct child of the transaction is successful. - */ - public function isTransactionSuccessful(Transaction $transaction): bool - { - if ($transaction->status === TransactionRecord::STATUS_SUCCESS) { - return true; - } - - return $this->_createTransactionQuery() - ->where([ - 'parentId' => $transaction->id, - 'status' => TransactionRecord::STATUS_SUCCESS, - 'orderId' => $transaction->orderId, - ]) - ->exists(); - } - - /** - * Save a transaction. - * - * @param Transaction $model the transaction model - * @param bool $runValidation should we validate this transaction before saving. - * @throws Throwable - * @throws TransactionException if an attempt is made to modify an existing transaction - * @throws OrderStatusException - * @throws ElementNotFoundException - * @throws Exception - */ - public function saveTransaction(Transaction $model, bool $runValidation = true): bool - { - if ($model->id) { - throw new TransactionException('Transactions cannot be modified.'); - } - - if ($runValidation && !$model->validate()) { - Craft::info('Transaction not saved due to validation error.', __METHOD__); - - return false; - } - - $fields = [ - 'orderId', - 'hash', - 'gatewayId', - 'type', - 'status', - 'amount', - 'currency', - 'paymentAmount', - 'paymentCurrency', - 'paymentRate', - 'reference', - 'message', - 'note', - 'code', - 'response', - 'userId', - 'parentId', - ]; - - $record = new TransactionRecord(); - - foreach ($fields as $field) { - $record->$field = $model->$field; - } - - $record->save(false); - $model->id = $record->id; - - if ($model->status === TransactionRecord::STATUS_SUCCESS) { - $model->order->updateOrderPaidInformation(); - } - - if ($model->status === TransactionRecord::STATUS_PROCESSING) { - $model->order->markAsComplete(); - } - - $model->getOrder()->setTransactions(null); // clear the local cache of transactions from the order. - - // Raise 'afterSaveTransaction' event - if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_TRANSACTION)) { - $this->trigger(self::EVENT_AFTER_SAVE_TRANSACTION, new TransactionEvent([ - 'transaction' => $model, - ])); - } - - return true; - } - - /** - * @param array|Order[] $orders - * @return Order[] - * @since 3.2.0 - */ - public function eagerLoadTransactionsForOrders(array $orders): array - { - $orderIds = array_filter(ArrayHelper::getColumn($orders, 'id')); - $transactionResults = $this->_createTransactionQuery()->andWhere(['orderId' => $orderIds])->all(); - - $transactions = []; - - foreach ($transactionResults as $result) { - $transaction = new Transaction($result); - $transactions[$transaction->orderId] ??= []; - $transactions[$transaction->orderId][] = $transaction; - } - - foreach ($orders as $key => $order) { - if (isset($transactions[$order->id])) { - $order->setTransactions($transactions[$order->id]); - $orders[$key] = $order; - } - } - - return $orders; - } - - /** - * Returns a Query object prepped for retrieving Transactions. - * - * @return Query The query object. - */ - private function _createTransactionQuery(): Query - { - return (new Query()) - ->select([ - 'amount', - 'code', - 'currency', - 'dateCreated', - 'dateUpdated', - 'gatewayId', - 'hash', - 'id', - 'message', - 'note', - 'orderId', - 'parentId', - 'paymentAmount', - 'paymentCurrency', - 'paymentRate', - 'reference', - 'response', - 'status', - 'type', - 'userId', - ]) - ->from([Table::TRANSACTIONS]) - ->orderBy(['id' => SORT_ASC]); - } -} diff --git a/src/services/Transfers.php b/src/services/Transfers.php deleted file mode 100644 index 7bbe31063e..0000000000 --- a/src/services/Transfers.php +++ /dev/null @@ -1,127 +0,0 @@ -newValue; - - ProjectConfigHelper::ensureAllFieldsProcessed(); - $fieldsService = Craft::$app->getFields(); - - if (empty($data) || empty(reset($data))) { - // Delete the field layout - $fieldsService->deleteLayoutsByType(Transfer::class); - return; - } - - // Save the field layout - $layout = FieldLayout::createFromConfig(reset($data)); - $layout->id = $fieldsService->getLayoutByType(Transfer::class)->id; - $layout->type = Transfer::class; - $layout->uid = key($data); - $fieldsService->saveLayout($layout, false); - } - - /** - * Handle field layout being deleted - */ - public function handleDeletedFieldLayout(): void - { - Craft::$app->getFields()->deleteLayoutsByType(Transfer::class); - } - - /** - * @return FieldLayout - */ - public function getFieldLayout(): FieldLayout - { - $fieldLayout = Craft::$app->getFields()->getLayoutByType(Transfer::class); - - if (!$fieldLayout->isFieldIncluded('transfer-management')) { - $layoutTabs = $fieldLayout->getTabs(); - $transfersTabName = Craft::t('commerce', 'Manage'); - if (ArrayHelper::contains($layoutTabs, 'name', $transfersTabName)) { - $transfersTabName .= ' ' . StringHelper::randomString(10); - } - - $contentTab = new FieldLayoutTab(); - $contentTab->setLayout($fieldLayout); - $contentTab->name = $transfersTabName; - $contentTab->setElements([ - ['type' => TransferManagementField::class], - ]); - - $layoutTabs[] = $contentTab; - $fieldLayout->setTabs($layoutTabs); - } - - return $fieldLayout; - } - - /** - * @param int $transferId - * @return array - */ - public function getTransferDetailsByTransferId(int $transferId): array - { - $results = $this->_createTransferDetailsQuery() - ->where(['transferId' => $transferId]) - ->all(); - - $transferDetails = []; - - foreach ($results as $result) { - $transferDetails[] = new TransferDetail($result); - } - - return $transferDetails; - } - - /** - * @return Query - */ - private function _createTransferDetailsQuery(): Query - { - return (new Query()) - ->select([ - 'id', - 'transferId', - 'inventoryItemId', - 'inventoryItemDescription', - 'quantity', - 'quantityAccepted', - 'quantityRejected', - 'uid', - ]) - ->from([Table::TRANSFERDETAILS]); - } -} diff --git a/src/services/Variants.php b/src/services/Variants.php deleted file mode 100644 index 3136478288..0000000000 --- a/src/services/Variants.php +++ /dev/null @@ -1,96 +0,0 @@ - - * @since 2.0 - */ -class Variants extends Component -{ - /** - * @var array - * @since 3.1.4 - */ - private array $_contentFieldCache = []; - - /** - * Returns a product's variants, per the product's ID. - * - * @param int $productId product ID - * @param int|null $siteId Site ID for which to return the variants. Defaults to `null` which is current site. - * @return Variant[] - */ - public function getAllVariantsByProductId(int $productId, int $siteId = null, bool $includeDisabled = true): array - { - $variantQuery = Variant::find() - ->productId($productId) - ->limit(null) - ->siteId($siteId); - - if ($includeDisabled) { - $variantQuery->status(null); - } - - return $variantQuery->all(); - } - - /** - * Returns a variant by its ID. - * - * @param int $variantId The variant’s ID. - * @param int|null $siteId The site ID for which to fetch the variant. Defaults to `null` which is current site. - */ - public function getVariantById(int $variantId, int $siteId = null): ?Variant - { - return Craft::$app->getElements()->getElementById($variantId, Variant::class, $siteId); - } - - /** - * @throws InvalidConfigException - * @since 3.1.4 - */ - public function getVariantGqlContentArguments(): array - { - if (empty($this->_contentFieldCache)) { - $contentArguments = []; - - foreach (Plugin::getInstance()->getProductTypes()->getAllProductTypes() as $productType) { - if (!GqlCommerceHelper::isSchemaAwareOf(Variant::gqlScopesByContext($productType))) { - continue; - } - - $fieldLayout = $productType->getVariantFieldLayout(); - foreach ($fieldLayout->getCustomFields() as $contentField) { - if (!$contentField instanceof GqlInlineFragmentFieldInterface) { - $contentArguments[$contentField->handle] = [ - 'name' => $contentField->handle, - 'type' => Type::listOf(QueryArgument::getType()), - ]; - } - } - } - - $this->_contentFieldCache = $contentArguments; - } - - return $this->_contentFieldCache; - } -} diff --git a/src/services/Vat.php b/src/services/Vat.php deleted file mode 100644 index 5c24784555..0000000000 --- a/src/services/Vat.php +++ /dev/null @@ -1,82 +0,0 @@ - - * @since 5.0.0 - */ -class Vat extends Component -{ - /** - * @var string - */ - protected string $cacheKeyPrefix = 'commerce:validVatId:'; - - /** - * @var mixed Allows for the possibility of a custom validator - */ - protected mixed $validator; - - /** - * @param string $vatId - * @return bool - */ - public function isValidVatId(string $vatId): bool - { - // Do we have a valid VAT ID in our cache? - $validOrganizationTaxId = Craft::$app->getCache()->exists($this->cacheKeyPrefix . $vatId); - - // If we do not have a valid VAT ID in cache, see if we can get one from the API - if (!$validOrganizationTaxId) { - try { - $validators = Plugin::getInstance()->getTaxes()->getEnabledTaxIdValidators(); - foreach ($validators as $validator) { - if ($validator->validateFormat($vatId) && $validator->validate($vatId)) { - $validOrganizationTaxId = true; - break; - } - } - } catch (Exception $e) { - Craft::error('Communication with VAT API failed: ' . $e->getMessage(), __METHOD__); - - $validOrganizationTaxId = false; - } - } - - if (!$validOrganizationTaxId) { - // Clean up if the API returned false and the item was still in cache - Craft::$app->getCache()->delete($this->cacheKeyPrefix . $vatId); - return false; - } - - Craft::$app->getCache()->set($this->cacheKeyPrefix . $vatId, '1'); - return true; - } - - /** - * @return Validator - * @deprecated in 5.3.0 use Taxes::getEnabledTaxIdValidators() instead - */ - protected function getVatValidator(): Validator - { - if (!isset($this->validator)) { - $this->validator = new Validator(); - } - - return $this->validator; - } -} diff --git a/src/services/Webhooks.php b/src/services/Webhooks.php deleted file mode 100644 index 2ea03ca8c6..0000000000 --- a/src/services/Webhooks.php +++ /dev/null @@ -1,126 +0,0 @@ - - * @since 3.1.9 - */ -class Webhooks extends Component -{ - /** - * @event WebhookEvent The event that is triggered before a Webhook is processed. - * @since 3.2.9 - * - * ```php - * use craft\commerce\events\WebhookEvent; - * use craft\commerce\services\Webhooks; - * use craft\commerce\base\GatewayInterface; - * use yii\base\Event; - * - * Event::on( - * Webhooks::class, - * Webhooks::EVENT_BEFORE_PROCESS_WEBHOOK, - * function(WebhookEvent $event) { - * // @var GatewayInterface $gateway - * $gateway = $event->gateway; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_BEFORE_PROCESS_WEBHOOK = 'beforeProcessWebhook'; - - /** - * @event WebhookEvent The event that is triggered after a Webhook is processed. - * @since 3.2.9 - * - * ```php - * use craft\commerce\events\WebhookEvent; - * use craft\commerce\services\Webhooks; - * use yii\base\Event; - * - * Event::on( - * Webhooks::class, - * Webhooks::EVENT_AFTER_PROCESS_WEBHOOK, - * function(WebhookEvent $event) { - * // @var Response $response - * $response = $event->response; - * - * // ... - * } - * ); - * ``` - */ - public const EVENT_AFTER_PROCESS_WEBHOOK = 'afterProcessWebhook'; - - /** - * @throws Exception - */ - public function processWebhook(GatewayInterface $gateway): Response - { - // Fire a 'beforeProcessWebhook' event - if ($this->hasEventHandlers(self::EVENT_BEFORE_PROCESS_WEBHOOK)) { - $this->trigger(self::EVENT_BEFORE_PROCESS_WEBHOOK, new WebhookEvent([ - 'gateway' => $gateway, - ])); - } - - $transactionHash = $gateway->getTransactionHashFromWebhook(); - $useMutex = (bool)$transactionHash; - $transactionLockName = 'commerceTransaction:' . $transactionHash; - $mutex = Craft::$app->getMutex(); - - if ($useMutex && !$mutex->acquire($transactionLockName, 15)) { - throw new Exception('Unable to acquire a lock for transaction: ' . $transactionHash); - } - - try { - if ($gateway->supportsWebhooks()) { - $response = $gateway->processWebhook(); - } else { - throw new BadRequestHttpException('Gateway not found or does not support webhooks.'); - } - } catch (Throwable $exception) { - $message = 'Exception while processing webhook: ' . $exception->getMessage() . "\n"; - $message .= 'Exception thrown in ' . $exception->getFile() . ':' . $exception->getLine() . "\n"; - $message .= 'Stack trace:' . "\n" . $exception->getTraceAsString(); - - Craft::error($message, 'commerce'); - - $response = Craft::$app->getResponse(); - $response->setStatusCodeByException($exception); - } - - if ($useMutex) { - $mutex->release($transactionLockName); - } - - // Fire a 'afterProcessWebhook' event - if ($this->hasEventHandlers(self::EVENT_AFTER_PROCESS_WEBHOOK)) { - $this->trigger(self::EVENT_AFTER_PROCESS_WEBHOOK, new WebhookEvent([ - 'gateway' => $gateway, - 'response' => $response, - ])); - } - - return $response; - } -} diff --git a/src/stats/AverageOrderTotal.php b/src/stats/AverageOrderTotal.php deleted file mode 100644 index b422f71ca1..0000000000 --- a/src/stats/AverageOrderTotal.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @since 3.0 - */ -class AverageOrderTotal extends Stat -{ - /** - * @inheritdoc - */ - protected string $_handle = 'averageOrderTotal'; - - /** - * @inheritDoc - */ - public function getData(): string|int|bool|null - { - $query = $this->_createStatQuery(); - $query->select([new Expression('ROUND(SUM([[total]]) / COUNT([[orders.id]]), 4) as averageOrderTotal')]); - - return $query->scalar(); - } -} diff --git a/src/stats/NewCustomers.php b/src/stats/NewCustomers.php deleted file mode 100644 index 4e67b4bd48..0000000000 --- a/src/stats/NewCustomers.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @since 3.0 - */ -class NewCustomers extends Stat -{ - /** - * @inheritdoc - */ - protected string $_handle = 'newCustomers'; - - /** - * @inheritDoc - */ - public function getData(): string|int|bool|null - { - $query = $this->_createStatQuery(); - - // Subquery to find customers who have orders before the start date - $existingCustomersQuery = (new Query()) - ->select(['customerId']) - ->from(Table::ORDERS) - ->where(['isCompleted' => true]) - ->andWhere(['not', ['customerId' => null]]) - ->andWhere(['<', 'dateOrdered', Db::prepareDateForDb($this->getStartDate())]); - - $query->select([new Expression('COUNT(DISTINCT [[customerId]]) as newCustomers')]) - ->andWhere(['not', ['customerId' => null]]) - ->andWhere(['not in', 'customerId', $existingCustomersQuery]); - - return $query->scalar(); - } -} diff --git a/src/stats/RepeatCustomers.php b/src/stats/RepeatCustomers.php deleted file mode 100644 index bd1482c890..0000000000 --- a/src/stats/RepeatCustomers.php +++ /dev/null @@ -1,48 +0,0 @@ - - * @since 3.0 - */ -class RepeatCustomers extends Stat -{ - /** - * @inheritdoc - */ - protected string $_handle = 'repeatingCustomers'; - - /** - * @inheritDoc - */ - public function getData(): array - { - $total = (int)$this->_createStatQuery() - ->select(['customerId']) - ->groupBy('customerId') - ->count(); - - $repeatRows = $this->_createStatQuery() - ->select([new Expression('COUNT([[orders.id]])')]) - ->groupBy('customerId') - ->column(); - - - $repeat = count(array_filter($repeatRows, static fn($row) => $row > 1)); - - $percentage = round($total ? ($repeat / $total) * 100 : 0); - - return compact('total', 'repeat', 'percentage'); - } -} diff --git a/src/stats/TopCustomers.php b/src/stats/TopCustomers.php deleted file mode 100644 index a2c472e7c7..0000000000 --- a/src/stats/TopCustomers.php +++ /dev/null @@ -1,95 +0,0 @@ - - * @since 3.0 - */ -class TopCustomers extends Stat -{ - /** - * @inheritdoc - */ - protected string $_handle = 'topCustomers'; - - /** - * @var string Type of start either 'total' or 'average'. - */ - public string $type = 'total'; - - /** - * @var int Number of customers to show. - */ - public int $limit = 5; - - /** - * @inheritDoc - */ - public function __construct(string $dateRange = null, string $type = null, $startDate = null, $endDate = null, ?int $storeId = null) - { - if ($type) { - $this->type = $type; - } - - parent::__construct($dateRange, $startDate, $endDate, $storeId); - } - - /** - * @inheritDoc - */ - public function getData(): array - { - $topCustomers = $this->_createStatQuery() - ->select([ - 'average' => new Expression('ROUND((SUM([[total]]) / COUNT([[orders.id]])), 4)'), - 'count' => new Expression('COUNT([[orders.id]])'), - 'customerId', - 'total' => new Expression('SUM([[total]])'), - 'users.email', - ]) - ->innerJoin(Table::USERS . ' users', '[[orders.customerId]] = [[users.id]]') - ->groupBy(['[[orders.customerId]]', '[[users.email]]']) - ->limit($this->limit); - - if ($this->type == 'average') { - $topCustomers->orderBy(new Expression('ROUND((SUM([[total]]) / COUNT([[orders.id]])), 4) DESC')); - } else { - $topCustomers->orderBy(new Expression('SUM([[total]]) DESC')); - } - - return $topCustomers->all(); - } - - /** - * @inheritDoc - */ - public function getHandle(): string - { - return $this->_handle . $this->type; - } - - /** - * @inheritDoc - */ - public function prepareData($data): mixed - { - foreach ($data as &$topCustomer) { - $topCustomer['customer'] = Craft::$app->getUsers()->getUserById($topCustomer['customerId']); - } - - return $data; - } -} diff --git a/src/stats/TopProductTypes.php b/src/stats/TopProductTypes.php deleted file mode 100644 index e1b6afbe3e..0000000000 --- a/src/stats/TopProductTypes.php +++ /dev/null @@ -1,110 +0,0 @@ - - * @since 3.0 - */ -class TopProductTypes extends Stat -{ - /** - * @inheritdoc - */ - protected string $_handle = 'topProductTypes'; - - /** - * @var string Type either 'qty' or 'revenue'. - */ - public string $type = 'qty'; - - /** - * @var int Number of customers to show. - */ - public int $limit = 5; - - /** - * @inheritDoc - */ - public function __construct(string $dateRange = null, string $type = null, $startDate = null, $endDate = null, ?int $storeId = null) - { - $this->type = $type ?? $this->type; - - parent::__construct($dateRange, $startDate, $endDate, $storeId); - } - - /** - * @inheritDoc - */ - public function getData(): array - { - $primarySite = Craft::$app->getSites()->getPrimarySite(); - $selectTotalQty = new Expression('SUM([[li.qty]]) as qty'); - $orderByQty = new Expression('SUM([[li.qty]]) DESC'); - $selectTotalRevenue = new Expression('SUM([[li.total]]) as revenue'); - $orderByRevenue = new Expression('SUM([[li.total]]) DESC'); - - $viewableProductTypeIds = Plugin::getInstance()->getProductTypes()->getViewableProductTypeIds(); - - $results = $this->_createStatQuery() - ->select([ - '[[pt.id]] as id', - '[[pt.name]]', - $selectTotalQty, - $selectTotalRevenue, - ]) - ->leftJoin(Table::LINEITEMS . ' li', '[[li.orderId]] = [[orders.id]]') - ->leftJoin(Table::PURCHASABLES . ' p', '[[p.id]] = [[li.purchasableId]]') - ->leftJoin(Table::VARIANTS . ' v', '[[v.id]] = [[p.id]]') - ->leftJoin(Table::PRODUCTS . ' pr', '[[pr.id]] = [[v.primaryOwnerId]]') - ->leftJoin(Table::PRODUCTTYPES . ' pt', '[[pt.id]] = [[pr.typeId]]') - ->leftJoin(CraftTable::ELEMENTS_SITES . ' es', [ - 'and', - '[[es.elementId]] = [[v.primaryOwnerId]]', - ['es.siteId' => $primarySite->id], - ]) - ->andWhere(['not', ['pt.name' => null]]) - ->andWhere(['pt.id' => $viewableProductTypeIds]) - ->groupBy('[[pt.id]]') - ->orderBy($this->type == 'revenue' ? $orderByRevenue : $orderByQty) - ->limit($this->limit); - - return $results->all(); - } - - /** - * @inheritDoc - */ - public function getHandle(): string - { - return $this->_handle . $this->type; - } - - /** - * @inheritDoc - */ - public function prepareData($data): mixed - { - if (!empty($data)) { - foreach ($data as &$row) { - $row['productType'] = ($row['id']) ? Plugin::getInstance()->getProductTypes()->getProductTypeById((int)$row['id']) : null; - } - } - - return $data; - } -} diff --git a/src/stats/TopProducts.php b/src/stats/TopProducts.php deleted file mode 100644 index 08975ba76b..0000000000 --- a/src/stats/TopProducts.php +++ /dev/null @@ -1,300 +0,0 @@ - - * @since 3.0 - */ -class TopProducts extends Stat -{ - /** - * Stat returned based on quantity. - * - * @since 3.4 - */ - public const TYPE_QTY = 'qty'; - - /** - * Stat returned based on revenue. - * - * @since 3.4 - */ - public const TYPE_REVENUE = 'revenue'; - - /** - * @since 3.4 - */ - public const REVENUE_OPTION_DISCOUNT = 'discount'; - - /** - * @since 3.4 - */ - public const REVENUE_OPTION_TAX = 'tax'; - - /** - * @since 3.4 - */ - public const REVENUE_OPTION_TAX_INCLUDED = 'tax_included'; - - /** - * @since 3.4 - */ - public const REVENUE_OPTION_SHIPPING = 'shipping'; - - /** - * @inheritdoc - */ - protected string $_handle = 'topProducts'; - - /** - * @var string Type either 'qty' or 'revenue'. - */ - public string $type = self::TYPE_QTY; - - /** - * @var int Number of products to show. - */ - public int $limit = 5; - - /** - * Options to be used when when calculating revenue total. - * - * @var string[] - * @since 3.4 - */ - public array $revenueOptions = []; - - /** - * Default options for calculating revenue total. - * - * @var string[] - * @since 3.4 - */ - private array $_defaultRevenueOptions = [ - self::REVENUE_OPTION_DISCOUNT, - self::REVENUE_OPTION_TAX, - self::REVENUE_OPTION_TAX_INCLUDED, - self::REVENUE_OPTION_SHIPPING, - ]; - - /** - * Used for the correct function name `IFNUll` vs `COALESCE` difference between DB engines. - * - * @var string - */ - private string $_ifNullDbFunc; - - /** - * @inheritDoc - */ - public function __construct(string $dateRange = null, string $type = null, $startDate = null, $endDate = null, array $revenueOptions = null, ?int $storeId = null) - { - $this->_ifNullDbFunc = Craft::$app->getDb()->getIsPgsql() ? 'COALESCE' : 'IFNULL'; - - if ($type) { - $this->type = $type; - } - - // Set defaults - $this->revenueOptions = $this->_defaultRevenueOptions; - if (is_array($revenueOptions)) { - $this->revenueOptions = $revenueOptions; - } - - parent::__construct($dateRange, $startDate, $endDate, $storeId); - } - - /** - * @inheritDoc - */ - public function getData(): array - { - $primarySite = Craft::$app->getSites()->getPrimarySite(); - - $select = [ - '[[v.primaryOwnerId]] as id', - '[[es.title]]', - new Expression('SUM([[li.qty]]) as qty'), - new Expression('SUM([[li.total]]) as revenue'), - new Expression('SUM([[li.subtotal]]) as revenue_subtotal'), - $this->getAdjustmentsSelect(), - ]; - - $topProducts = $this->_createStatQuery() - ->select($select) - ->leftJoin(Table::LINEITEMS . ' li', '[[li.orderId]] = [[orders.id]]') - ->leftJoin(Table::PURCHASABLES . ' p', '[[p.id]] = [[li.purchasableId]]') - ->leftJoin(Table::VARIANTS . ' v', '[[v.id]] = [[p.id]]') - ->leftJoin(Table::PRODUCTS . ' pr', '[[pr.id]] = [[v.primaryOwnerId]]') - ->leftJoin(Table::PRODUCTTYPES . ' pt', '[[pt.id]] = [[pr.typeId]]') - ->leftJoin(CraftTable::ELEMENTS_SITES . ' es', [ - 'and', - '[[es.elementId]] = [[v.primaryOwnerId]]', - ['es.siteId' => $primarySite->id], - ]) - ->leftJoin(['adjustments' => $this->createAdjustmentsSubQuery()], '[[v.primaryOwnerId]] = [[adjustments.primaryOwnerId]]') - ->groupBy($this->getGroupBy()) - ->orderBy($this->getOrderBy()) - ->andWhere(['not', ['[[v.primaryOwnerId]]' => null]]) - ->limit($this->limit); - - return $topProducts->all(); - } - - /** - * @inheritDoc - */ - public function getHandle(): string - { - $handle = $this->_handle . $this->type; - - foreach ($this->revenueOptions as $revenueOption) { - $handle .= '-' . $revenueOption; - } - - return $handle; - } - - /** - * @inheritDoc - */ - public function prepareData($data): mixed - { - if (!empty($data)) { - foreach ($data as &$row) { - if ($row['id']) { - $row['product'] = Plugin::getInstance()->getProducts()->getProductById($row['id']); - } - } - } - - return $data; - } - - /** - * Create select statement for a stat type `custom` based on the options chosen. - * - * @since 3.4 - */ - protected function getAdjustmentsSelect(): Expression - { - $select = 'SUM([[li.subtotal]])'; - - if (is_array($this->revenueOptions)) { - if (in_array(self::REVENUE_OPTION_DISCOUNT, $this->revenueOptions, true)) { - $select .= '+ [[adjustments.discount]]'; - } - - if (!in_array(self::REVENUE_OPTION_TAX_INCLUDED, $this->revenueOptions, true)) { - $select .= '- [[adjustments.tax_included]]'; - } - - if (!in_array(self::REVENUE_OPTION_TAX, $this->revenueOptions, true)) { - $select .= '- [[adjustments.tax]]'; - } - - if (!in_array(self::REVENUE_OPTION_SHIPPING, $this->revenueOptions, true)) { - $select .= '- [[adjustments.shipping]]'; - } - } - - $select = $this->_ifNullDbFunc . '(' . $select . ', SUM([[li.subtotal]]))'; - - return new Expression($select . ' as revenue_custom'); - } - - /** - * Create the adjustments sub query for use with revenue calculation. - * - * @since 3.4 - */ - protected function createAdjustmentsSubQuery(): Query - { - $types = []; - foreach ($this->revenueOptions as $revenueOption) { - $types[] = str_starts_with($revenueOption, 'tax') ? 'tax' : $revenueOption; - } - $types = array_unique($types); - - return (new Query()) - ->select([ - '[[v.primaryOwnerId]]', - 'discount' => new Expression($this->_ifNullDbFunc . '(SUM(CASE WHEN [[oa.type]]=\'discount\' THEN amount END), 0)'), - 'shipping' => new Expression($this->_ifNullDbFunc . '(SUM(CASE WHEN [[oa.type]]=\'shipping\' THEN amount END), 0)'), - 'tax' => new Expression($this->_ifNullDbFunc . '(SUM(CASE WHEN [[oa.type]]=\'tax\' AND included=false THEN amount END), 0)'), - 'tax_included' => new Expression($this->_ifNullDbFunc . '(SUM(CASE WHEN [[oa.type]]=\'tax\' AND included=true THEN amount END), 0)'), - ]) - ->from(Table::ORDERADJUSTMENTS . ' oa') - ->leftJoin(Table::LINEITEMS . ' li', '[[li.id]] = [[lineItemId]]') - ->leftJoin(Table::VARIANTS . ' v', '[[v.id]] = [[li.purchasableId]]') - ->where(['not', ['lineItemId' => null]]) - ->andWhere(['not', ['[[v.primaryOwnerId]]' => null]]) - ->andWhere(['[[oa.type]]' => $types]) - ->groupBy('[[v.primaryOwnerId]]'); - } - - /** - * Return the order by clause for the data query. - * - * @since 3.4 - */ - protected function getOrderBy(): Expression - { - if ($this->type === self::TYPE_QTY) { - return new Expression('SUM([[li.qty]]) DESC'); - } - - // Order by custom revenue options if not all options are selected. - if ($this->type === self::TYPE_REVENUE && count(array_intersect($this->_defaultRevenueOptions, $this->revenueOptions)) !== count($this->_defaultRevenueOptions)) { - return new Expression('[[revenue_custom]] DESC'); - } - - return new Expression('SUM([[li.total]]) DESC'); - } - - /** - * Return group by statement based on state type. - * - * @since 3.4 - */ - protected function getGroupBy(): string - { - $groupBy = '[[v.primaryOwnerId]], [[es.title]]'; - - if (is_array($this->revenueOptions)) { - if (in_array(self::REVENUE_OPTION_DISCOUNT, $this->revenueOptions, true)) { - $groupBy .= ', [[adjustments.discount]]'; - } - - if (!in_array(self::REVENUE_OPTION_TAX_INCLUDED, $this->revenueOptions, true)) { - $groupBy .= ', [[adjustments.tax_included]]'; - } - - if (!in_array(self::REVENUE_OPTION_TAX, $this->revenueOptions, true)) { - $groupBy .= ', [[adjustments.tax]]'; - } - - if (!in_array(self::REVENUE_OPTION_SHIPPING, $this->revenueOptions, true)) { - $groupBy .= ', [[adjustments.shipping]]'; - } - } - - return $groupBy; - } -} diff --git a/src/stats/TopPurchasables.php b/src/stats/TopPurchasables.php deleted file mode 100644 index 8d0eba4417..0000000000 --- a/src/stats/TopPurchasables.php +++ /dev/null @@ -1,89 +0,0 @@ - - * @since 3.0 - */ -class TopPurchasables extends Stat -{ - /** - * @inheritdoc - */ - protected string $_handle = 'topPurchasables'; - - /** - * @var string Type either 'qty' or 'revenue'. - */ - public string $type = 'qty'; - - /** - * @var int Number of customers to show. - */ - public int $limit = 5; - - /** - * @inheritDoc - */ - public function __construct(string $dateRange = null, string $type = null, $startDate = null, $endDate = null, ?int $storeId = null) - { - $this->type = $type ?? $this->type; - - parent::__construct($dateRange, $startDate, $endDate, $storeId); - } - - /** - * @inheritDoc - */ - public function getData(): array - { - $selectTotalQty = new Expression('SUM([[li.qty]]) as qty'); - $orderByQty = new Expression('SUM([[li.qty]]) DESC'); - $selectTotalRevenue = new Expression('SUM([[li.total]]) as revenue'); - $orderByRevenue = new Expression('SUM([[li.total]]) DESC'); - - $viewableProductTypeIds = Plugin::getInstance()->getProductTypes()->getViewableProductTypeIds(); - - $topPurchasables = $this->_createStatQuery() - ->select([ - '[[li.purchasableId]]', - '[[p.description]]', - '[[p.sku]]', - $selectTotalQty, - $selectTotalRevenue, - ]) - ->leftJoin(Table::LINEITEMS . ' li', '[[li.orderId]] = [[orders.id]]') - ->leftJoin(Table::PURCHASABLES . ' p', '[[p.id]] = [[li.purchasableId]]') - ->leftJoin(Table::VARIANTS . ' v', '[[v.id]] = [[p.id]]') - ->leftJoin(Table::PRODUCTS . ' pr', '[[pr.id]] = [[v.primaryOwnerId]]') - ->leftJoin(Table::PRODUCTTYPES . ' pt', '[[pt.id]] = [[pr.typeId]]') - ->andWhere(['pt.id' => $viewableProductTypeIds]) - ->groupBy('[[li.purchasableId]], [[p.sku]], [[p.description]]') - ->orderBy($this->type == 'revenue' ? $orderByRevenue : $orderByQty) - ->addOrderBy('sku ASC') - ->limit($this->limit); - - return $topPurchasables->all(); - } - - /** - * @inheritDoc - */ - public function getHandle(): string - { - return $this->_handle . $this->type; - } -} diff --git a/src/stats/TotalOrders.php b/src/stats/TotalOrders.php deleted file mode 100644 index ad28831fa1..0000000000 --- a/src/stats/TotalOrders.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @since 3.0 - */ -class TotalOrders extends Stat -{ - /** - * @inheritdoc - */ - protected string $_handle = 'totalOrders'; - - /** - * @inheritDoc - */ - public function getData(): array - { - $query = $this->_createStatQuery(); - $query->select([new Expression('COUNT([[orders.id]]) as total')]); - - $chartData = $this->_createChartQuery([ - new Expression('COUNT([[orders.id]]) as total'), - ], [ - 'total' => 0, - ]); - - return [ - 'total' => $query->scalar(), - 'chart' => $chartData, - ]; - } -} diff --git a/src/stats/TotalOrdersByCountry.php b/src/stats/TotalOrdersByCountry.php deleted file mode 100644 index 4b193a895c..0000000000 --- a/src/stats/TotalOrdersByCountry.php +++ /dev/null @@ -1,121 +0,0 @@ - - * @since 3.0 - */ -class TotalOrdersByCountry extends Stat -{ - /** - * @inheritdoc - */ - protected string $_handle = 'totalOrdersByCountry'; - - /** - * @var string Type of stat e.g. 'shipping' or 'billing'. - */ - public string $type = 'shipping'; - - public int $limit = 5; - - /** - * @inheritDoc - */ - public function __construct(string $dateRange = null, string $type = null, $startDate = null, $endDate = null, ?int $storeId = null) - { - $this->type = $type ?? $this->type; - - parent::__construct($dateRange, $startDate, $endDate, $storeId); - } - - /** - * @inheritDoc - */ - public function getData(): array - { - $query = $this->_createStatQuery(); - $query->select([ - 'countryCode' => ($this->type == 'billing' ? '[[b.countryCode]]' : '[[s.countryCode]]'), - 'total' => new Expression('COUNT([[orders.id]])'), - ]); - $query->leftJoin(CraftTable::ADDRESSES . ' s', '[[s.id]] = [[orders.shippingAddressId]]'); - $query->leftJoin(CraftTable::ADDRESSES . ' b', '[[b.id]] = [[orders.billingAddressId]]'); - - if ($this->type == 'billing') { - $query->andWhere(['not', ['[[b.countryCode]]' => null]]); - $query->groupBy('[[b.countryCode]]'); - } else { - $query->andWhere(['not', ['[[s.countryCode]]' => null]]); - $query->groupBy('[[s.countryCode]]'); - } - - $query->orderBy(new Expression('COUNT([[orders.id]]) DESC')); - $query->limit($this->limit); - $rows = $query->all(); - - if (count($rows) < $this->limit) { - return $rows; - } - - $countryCodes = ArrayHelper::getColumn($rows, 'countryCode', false); - - $otherCountries = $this->_createStatQuery() - ->select([ - 'total' => new Expression('COUNT([[orders.id]])'), - 'countryCode' => new Expression('NULL'), - ]) - ->leftJoin(CraftTable::ADDRESSES . ' s', '[[s.id]] = [[orders.shippingAddressId]]') - ->leftJoin(CraftTable::ADDRESSES . ' b', '[[b.id]] = [[orders.billingAddressId]]') - ->andWhere(['not', [($this->type == 'billing' ? '[[b.countryCode]]' : '[[s.countryCode]]') => $countryCodes]]) - ->one(); - - if (empty($otherCountries)) { - return $rows; - } - - $otherCountries['name'] = Craft::t('commerce', 'Other countries'); - $rows[] = $otherCountries; - - return $rows; - } - - /** - * @inheritDoc - */ - public function getHandle(): string - { - return $this->_handle . $this->type; - } - - /** - * @inheritDoc - */ - public function prepareData($data): mixed - { - if (!empty($data)) { - foreach ($data as &$row) { - if (!$row['countryCode']) { - continue; - } - $row['name'] = Craft::$app->getAddresses()->getCountryRepository()->get($row['countryCode'])->getName(); - } - } - - return $data; - } -} diff --git a/src/stats/TotalRevenue.php b/src/stats/TotalRevenue.php deleted file mode 100644 index dbdcf36ae8..0000000000 --- a/src/stats/TotalRevenue.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @since 3.0 - */ -class TotalRevenue extends Stat -{ - /** - * @since 4.1.0 - */ - public const TYPE_TOTAL = 'total'; - - /** - * @since 4.1.0 - */ - public const TYPE_TOTAL_PAID = 'totalPaid'; - - /** - * @var string - * @since 4.1.0 - */ - public string $type = self::TYPE_TOTAL; - - /** - * @inheritdoc - */ - protected string $_handle = 'totalRevenue'; - - /** - * @inheritDoc - */ - public function getData(): ?array - { - $allowedTypes = [self::TYPE_TOTAL, self::TYPE_TOTAL_PAID]; - if (!in_array($this->type, $allowedTypes, true)) { - $this->type = self::TYPE_TOTAL; - } - - return $this->_createChartQuery( - [ - new Expression(sprintf('SUM([[%s]]) as revenue', $this->type)), - new Expression('COUNT([[orders.id]]) as count'), - ], - [ - 'revenue' => 0, - 'count' => 0, - ] - ); - } -} diff --git a/src/taxidvalidators/EuVatIdValidator.php b/src/taxidvalidators/EuVatIdValidator.php deleted file mode 100644 index 1a3f892a61..0000000000 --- a/src/taxidvalidators/EuVatIdValidator.php +++ /dev/null @@ -1,132 +0,0 @@ - - */ -class EuVatIdValidator implements TaxIdValidatorInterface -{ - public const API_URL = 'https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number'; - - /** - * Regular expression patterns per country code - * - * @var array - * @link http://ec.europa.eu/taxation_customs/vies/faq.html?locale=lt#item_11 - */ - private array $_patterns = [ - 'AT' => 'U[A-Z\d]{8}', - 'BE' => '(0|1)\d{9}', - 'BG' => '\d{9,10}', - 'CY' => '\d{8}[A-Z]', - 'CZ' => '\d{8,10}', - 'DE' => '\d{9}', - 'DK' => '(\d{2} ?){3}\d{2}', - 'EE' => '\d{9}', - 'EL' => '\d{9}', - 'ES' => '([A-Z]\d{7}[A-Z]|\d{8}[A-Z]|[A-Z]\d{8})', - 'EU' => '\d{9}', - 'FI' => '\d{8}', - 'FR' => '[A-Z\d]{2}\d{9}', - 'GB' => '(\d{9}|\d{12}|(GD|HA)\d{3})', - 'HR' => '\d{11}', - 'HU' => '\d{8}', - 'IE' => '((\d{7}[A-Z]{1,2})|(\d[A-Z]\d{5}[A-Z]))', - 'IT' => '\d{11}', - 'LT' => '(\d{9}|\d{12})', - 'LU' => '\d{8}', - 'LV' => '\d{11}', - 'MT' => '\d{8}', - 'NL' => '\d{9}B\d{2}', - 'PL' => '\d{10}', - 'PT' => '\d{9}', - 'RO' => '\d{2,10}', - 'SE' => '\d{12}', - 'SI' => '\d{8}', - 'SK' => '\d{10}', - 'SM' => '\d{5}', - ]; - - public static function displayName(): string - { - return \Craft::t('commerce', 'EU VAT ID'); - } - - private function _splitNumber(string $idNumber): array - { - $vatNumber = strtoupper($idNumber); - $country = substr($vatNumber, 0, 2); - $number = substr($vatNumber, 2); - - return [$country, $number]; - } - - public function validateFormat(string $idNumber): bool - { - [$country, $number] = $this->_splitNumber($idNumber); - - if (!isset($this->_patterns[$country])) { - return false; - } - - return preg_match('/^' . $this->_patterns[$country] . '$/', $number) > 0; - } - - public function validateExistence(string $idNumber): bool - { - [$country, $number] = $this->_splitNumber($idNumber); - - try { - $client = Craft::createGuzzleClient(); - $response = $client->post(self::API_URL, [ - 'headers' => [ - 'Content-Type' => 'application/json', - ], - 'body' => json_encode([ - 'countryCode' => $country, - 'vatNumber' => $number, - ]), - ]); - - $responseBody = json_decode($response->getBody(), true); - if ($response->getStatusCode() !== 200) { - return false; - } - - if (!isset($responseBody['valid']) || $responseBody['valid'] !== true) { - return false; - } - - return true; - } catch (\Exception $e) { - \Craft::error($e->getMessage(), __METHOD__); - } - - return false; - } - - /** - * @inheritdoc - */ - public static function isEnabled(): bool - { - return true; - } - - public function validate(string $idNumber): bool - { - try { - return $this->validateFormat($idNumber) && $this->validateExistence($idNumber); - } catch (\Exception $e) { - \Craft::error('Error validating EU VAT ID: ' . $e->getMessage()); - return false; - } - } -} diff --git a/src/templates/index.twig b/src/templates/index.twig deleted file mode 100644 index 7003ccc690..0000000000 --- a/src/templates/index.twig +++ /dev/null @@ -1,64 +0,0 @@ -{% set permissionsToView = { - 'commerce/orders': 'commerce-manageOrders', - 'commerce/subscriptions': 'commerce-manageSubscriptions', - 'commerce/inventory' : 'commerce-manageInventoryStockLevels', - 'commerce/store-management' : 'commerce-manageStoreSettings', - - 'commerce/promotions': 'commerce-managePromotions', - 'commerce/shipping/shippingmethods': 'commerce-manageShipping', - 'commerce/tax/taxrates': 'commerce-manageTaxes', -} %} - -{% set primaryStore = craft.commerce.stores.getPrimaryStore() %} -{% set deprecatedRoutesToNewRoute = { - 'commerce/promotions': "commerce/store-management/#{primaryStore.handle}/discounts", - 'commerce/shipping/shippingmethods': "commerce/store-management/#{primaryStore.handle}/shippingmethods", - 'commerce/tax/taxrates': "commerce/store-management/#{primaryStore.handle}/taxrates", -} %} - -{% set permission = permissionsToView[craft.commerce.settings.defaultView] ?? null %} -{% if craft.commerce.settings.defaultView and permission and currentUser.can(permission) %} - {% if craft.commerce.settings.defaultView in deprecatedRoutesToNewRoute|keys %} - {% redirect deprecatedRoutesToNewRoute[craft.commerce.settings.defaultView] %} - {% endif %} - - {% redirect craft.commerce.settings.defaultView %} -{% endif %} - -{% if craft.commerce.settings.defaultView and craft.commerce.settings.defaultView == 'commerce/products' and craft.commerce.productTypes.getViewableProductTypes()|length > 0 %} - {% redirect 'commerce/products' %} -{% endif %} - -{% if currentUser.can('commerce-manageOrders') %} - {% redirect 'commerce/orders' %} -{% endif %} - -{% if craft.commerce.productTypes.getViewableProductTypes()|length > 0 %} - {% redirect 'commerce/products' %} -{% endif %} - -{% if currentUser.can('commerce-manageStoreSettings') %} - {% redirect "commerce/store-management" %} -{% endif %} - -{% if currentUser.can('commerce-manageInventoryStockLevels') %} - {% redirect "commerce/inventory" %} -{% endif %} - -{% if currentUser.can('commerce-managePromotions') %} - {% redirect "commerce/store-management/#{primaryStore.handle}/discounts" %} -{% endif %} - -{% if currentUser.can('commerce-manageShipping') %} - {% redirect "commerce/store-management/#{primaryStore.handle}/shippingmethods" %} -{% endif %} - -{% if currentUser.can('commerce-manageTaxes') %} - {% redirect "commerce/store-management/#{primaryStore.handle}/taxrates" %} -{% endif %} - -{% if currentUser.can('commerce-manageSubscriptions') %} - {% redirect 'commerce/subscriptions' %} -{% endif %} - -{% exit 403 %} diff --git a/src/templates/inventory-locations/_edit.twig b/src/templates/inventory-locations/_edit.twig deleted file mode 100644 index 0382f77b7a..0000000000 --- a/src/templates/inventory-locations/_edit.twig +++ /dev/null @@ -1,9 +0,0 @@ -{% namespace 'inventoryLocationAddress' %} -{{ form.render()|raw }} -{% endnamespace %} - -{% hook "cp.commerce.inventoryLocation.edit" %} - -{% if not inventoryLocation.id %} -{% js "new Craft.HandleGenerator('##{'name'|namespaceInputId}', '##{'handle'|namespaceInputId}');" %} -{% endif %} \ No newline at end of file diff --git a/src/templates/promotions/sales/_edit.twig b/src/templates/promotions/sales/_edit.twig deleted file mode 100644 index 85d040001d..0000000000 --- a/src/templates/promotions/sales/_edit.twig +++ /dev/null @@ -1,360 +0,0 @@ -{% extends "commerce/_layouts/store-management" %} -{% set isIndex = false %} - -{% set crumbs = [ - { label: 'Commerce'|t('commerce'), url: url('commerce') }, - { label: "Store Management"|t('commerce'), url: url('commerce/store-management/#{storeHandle}') }, - { label: "Sales"|t('commerce'), url: url("commerce/store-management/#{storeHandle}/sales") }, -] %} - -{% set fullPageForm = true %} - -{% import "_includes/forms" as forms %} -{% import "commerce/_includes/forms/commerceForms" as commerceForms %} - -{% set mainFormAttributes = { - id: 'saleform', - method: 'post', - 'accept-charset': 'UTF-8' -} %} - -{% set formActions = [{ - label: 'Save and continue editing'|t('app'), - redirect: (isNewSale ? "commerce/store-management/#{storeHandle}/sales/{id}" : sale.getCpEditUrl())|hash, - retainScroll: true, - shortcut: true, -}] %} - -{% set actionClasses = "" %} -{% if (sale.getErrors('applyAmount') or sale.getErrors('apply')) %} - {% set actionClasses = "error" %} -{% endif %} - -{% set matchingItemsClasses = "" %} -{% if false %} - {% set matchingItemsClasses = "error" %} -{% endif %} - -{% set saleClasses = "" %} -{% if(sale.getErrors('name')) %} - {% set saleClasses = "error" %} -{% endif %} - -{% set tabs = { - sale: {'label':'Sale'|t('commerce'),'url':'#sale','class': saleClasses}, - matchingItems: {'label':'Matching Items'|t('commerce'),'url':'#matching-items'}, - conditions: {'label':'Conditions'|t('commerce'),'url':'#conditions'}, - actions: {'label':'Actions'|t('commerce'),'url':'#actions','class': actionClasses} -} %} - -{% hook "cp.commerce.sales.edit" %} - -{% block details %} - -
- {{ forms.lightSwitchField({ - label: "Enable this sale"|t('commerce'), - id: 'enabled', - name: 'enabled', - value: 1, - on: sale.enabled, - checked: sale.enabled, - errors: sale.getErrors('enabled'), - instructions: 'Whether this sale should be available for use, regardless of other conditions.'|t('commerce') - }) }} -
- - {% if sale and sale.id %} -
-
-
{{ "Created at"|t('app') }}
-
{{ sale.dateCreated|datetime('short') }}
-
-
-
{{ "Updated at"|t('app') }}
-
{{ sale.dateUpdated|datetime('short') }}
-
-
- {% endif %} - - {% hook "cp.commerce.sales.edit.details" %} -{% endblock %} - -{% block content %} - - {{ redirectInput("commerce/store-management/#{storeHandle}/sales") }} - {% if sale.id %} - - - {% endif %} - -
- {{ forms.textField({ - first: true, - label: "Name"|t('commerce'), - instructions: "What this sale will be called in the control panel."|t('commerce'), - id: 'name', - name: 'name', - value: sale.name, - errors: sale.getErrors('name'), - autofocus: true, - required: true, - }) }} - - {{ forms.textField({ - label: "Description"|t('commerce'), - instructions: "Sale description."|t('commerce'), - id: 'description', - name: 'description', - value: sale.description, - errors: sale.getErrors('description'), - }) }} - -
- - - - - - - - {% hook "cp.commerce.sales.edit.content" %} -{% endblock %} - -{% js %} -$(function() { - $('#groups, #productTypes').selectize({ - plugins: ['remove_button'], - dropdownParent: 'body' - }); - - $("form").submit(function() { - $("input[name=ignorePrevious]").prop('disabled', false); - if ($("input[name=ignorePrevious]").prop('checked') == true) { - $("#ignorePrevious-field").css('opacity', 0.25); - } - }); - - $('select[name=apply]').change(function() { - - if (this.value == 'byPercent' || this.value == 'toPercent') { - $('#applyAmount-percent-symbol').removeClass('hidden'); - $('#applyAmount-currency-symbol').addClass('hidden'); - }else{ - $('#applyAmount-percent-symbol').addClass('hidden'); - $('#applyAmount-currency-symbol').removeClass('hidden'); - } - - if (this.value == 'toFlat' || this.value == 'toPercent') { - $('input[name=ignorePrevious]').prop('disabled', true); - $('#ignorePrevious').prop('disabled', true); - $('#ignorePrevious').addClass('disabled', true); - } - if (this.value != 'toFlat' && this.value != 'toPercent') { - $('input[name=ignorePrevious]').prop('disabled', false); - $('#ignorePrevious').prop('disabled', false); - $('#ignorePrevious').removeClass('disabled', true); - } - }); -}); -{% endjs %} diff --git a/src/templates/settings/gateways/_edit.twig b/src/templates/settings/gateways/_edit.twig deleted file mode 100644 index 37669788b7..0000000000 --- a/src/templates/settings/gateways/_edit.twig +++ /dev/null @@ -1,159 +0,0 @@ -{% extends "commerce/_layouts/cp" %} - -{% set crumbs = [ - { label: 'Commerce'|t('commerce'), url: url('commerce') }, - { label: 'Settings'|t('app'), url: url('commerce/settings'), ariaLabel: 'Commerce Settings'|t('commerce') }, - { label: "Gateways"|t('commerce'), url: url('commerce/settings/gateways') }, -] %} - -{% set selectedSubnavItem = 'settings' %} - -{% set fullPageForm = not readOnly %} - -{% if readOnly %} - {% set contentNotice = readOnlyNotice() %} -{% endif %} - -{% import "_includes/forms" as forms %} - -{% block content %} - {{ hiddenInput('id', gateway.id) }} - {{ actionInput('commerce/gateways/save') }} - {{ redirectInput("commerce/settings/gateways") }} - - {{ forms.textField({ - label: 'Name'|t('commerce'), - name: 'name', - id: 'name', - value : gateway.name, - required: true, - errors: gateway.getErrors('name'), - disabled: readOnly, - }) }} - - {{ forms.textField({ - label: 'Handle'|t('commerce'), - name: 'handle', - id: 'handle', - class: 'code', - value : gateway.handle, - required: true, - errors: gateway.getErrors('handle'), - disabled: readOnly, - }) }} - - {% if gateway.supportsWebhooks() %} - {{ forms.textField({ - label: "Webhook URL"|t('commerce'), - instructions: "The webhook URL for this gateway."|t('commerce'), - disabled: true, - value: gateway.webhookUrl, - disabled: readOnly, - }) }} - {% endif %} -
- - {{ forms.selectField({ - first: true, - label: 'Gateway'|t('commerce'), - warning: (gateway.id ? "Changing this value may affect your ability to refund existing transactions."|t('commerce')), - id: 'type', - name: 'type', - options : gatewayOptions, - value : className(gateway), - required: true, - errors: gateway.getErrors('type') ?? null, - toggle: true, - disabled: readOnly, - }) }} - - - - {% for gatewayType in gatewayTypes %} - {% set isCurrent = (gatewayType == className(gateway)) %} - - - {% endfor %} - - {{ forms.booleanMenuField({ - label: "Enabled for customers to select during checkout?"|t('commerce'), - id: 'isFrontendEnabled', - name: 'isFrontendEnabled', - includeEnvVars: true, - value: gateway.isFrontendEnabled(false), - errors: gateway.getErrors('isFrontendEnabled'), - disabled: readOnly, - }) }} - -
- {% set orderConditionInput %} - {{ gateway.getOrderCondition().getBuilderHtml(readOnly)|raw }} - {% endset %} - - {{ forms.field({ - label: 'Match Order'|t('commerce'), - instructions: 'Create rules that allow this gateway to match the order.'|t('commerce'), - errors: gateway.getErrors('orderCondition'), - }, orderConditionInput) }} - - {% set billingAddressConditionInput %} - {{ gateway.getBillingAddressCondition().getBuilderHtml(readOnly)|raw }} - {% endset %} - - {{ forms.field({ - label: 'Match Billing Address'|t('commerce'), - instructions: 'Create rules that allow this gateway to match the billing address.'|t('commerce'), - errors: gateway.getErrors('billingAddressCondition'), - }, billingAddressConditionInput) }} - - {% set shippingAddressConditionInput %} - {{ gateway.getShippingAddressCondition().getBuilderHtml(readOnly)|raw }} - {% endset %} - - {{ forms.field({ - label: 'Match Shipping Address'|t('commerce'), - instructions: 'Create rules that allow this gateway to match the shipping address.'|t('commerce'), - errors: gateway.getErrors('shippingAddressCondition'), - }, shippingAddressConditionInput) }} - -{% endblock %} - -{% js %} - $(function() { - $('#type').change(function() { - $('.gateway-settings').hide().find('select, input, textarea').prop('disabled', true); - if($(this).val()) { - $('#gateway-' + $(this).val()).show().find('select, input, textarea').prop('disabled', false); - } - }).change(); - }); -{% endjs %} - -{% if gateway is not defined or not gateway.handle %} - {% js %} - new Craft.HandleGenerator('#name', '#handle'); - {% endjs %} -{% endif %} diff --git a/src/templates/settings/general/index.twig b/src/templates/settings/general/index.twig deleted file mode 100644 index e9b939e88d..0000000000 --- a/src/templates/settings/general/index.twig +++ /dev/null @@ -1,82 +0,0 @@ -{# @var settings \craft\commerce\models\Settings #} -{% extends "commerce/_layouts/settings" %} - -{% set selectedTab = 'settings' %} -{% set fullPageForm = not readOnly %} - -{% set crumbs = [ - { label: 'Commerce'|t('commerce'), url: url('commerce') }, -] %} - -{% import "_includes/forms" as forms %} - -{% from _self import configWarning %} - -{% block content %} -

{{ "General Settings"|t('commerce') }}

- -
- - {% if not readOnly %} - {{ actionInput('commerce/settings/save-settings') }} - {{ redirectInput('commerce/settings/general') }} - {% endif %} - -

{{ 'Units'|t('commerce') }}

- {{ forms.selectField({ - label: "Weight Unit"|t('commerce'), - instructions: "The unit of measurement that should be used when specifying product weights."|t('commerce'), - name: 'settings[weightUnits]', - value: settings.weightUnits, - options: settings.getWeightUnitsOptions(), - errors: settings.getErrors('weightUnits'), - required: true, - disabled: readOnly, - warning: configWarning('weightUnits', 'commerce'), - }) }} - - {{ forms.selectField({ - label: "Dimension Unit"|t('commerce'), - instructions: "The unit of measurement that should be used when specifying product dimensions."|t('commerce'), - name: 'settings[dimensionUnits]', - value: settings.dimensionUnits, - options: settings.getDimensionUnits(), - errors: settings.getErrors('dimensionUnits'), - required: true, - disabled: readOnly, - warning: configWarning('dimensionUnits', 'commerce'), - }) }} - -
-

{{ 'Subscription Settings'|t('commerce') }}

- {{ forms.autosuggestField({ - label: "Billing detail update URL"|t('commerce'), - instructions: "The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication."|t('commerce'), - id: 'updateBillingDetailsUrl', - name: 'settings[updateBillingDetailsUrl]', - value: settings.updateBillingDetailsUrl, - errors: settings.getErrors('updateBillingDetailsUrl'), - required: false, - suggestEnvVars: true, - suggestAliases: true, - disabled: readOnly, - placeholder: "//example.com/subscriptions/updateBillingDetails", - warning: configWarning('updateBillingDetailsUrl', 'commerce'), - }) }} - -
-

{{ 'Control Panel Settings'|t('commerce') }}

- {{ forms.selectField({ - label: "Default View"|t('commerce'), - instructions: "Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access."|t('commerce'), - name: 'settings[defaultView]', - value: settings.defaultView, - options: settings.getDefaultViewOptions(), - errors: settings.getErrors('defaultView'), - disabled: readOnly, - required: true, - warning: configWarning('defaultView', 'commerce'), - }) }} -
- -{% endblock %} diff --git a/src/templates/settings/subscriptions/_edit.twig b/src/templates/settings/subscriptions/_edit.twig deleted file mode 100644 index 36bbdf1d17..0000000000 --- a/src/templates/settings/subscriptions/_edit.twig +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "commerce/_layouts/settings" %} - -{% import "_includes/forms" as forms %} - -{% set selectedItem = 'subscriptions' %} - -{% set crumbs = [ - { label: 'Commerce'|t('commerce'), url: url('commerce') }, -] %} - -{% set fullPageForm = not readOnly %} - -{% if readOnly %} - {% set contentNotice = readOnlyNotice() %} -{% endif %} - -{% block content %} - {% if not readOnly %} - {{ actionInput('commerce/settings/save-subscription-settings') }} - {{ csrfInput() }} - {% endif %} - {{ redirectInput('commerce/settings/subscriptions') }} - - {{ forms.fieldLayoutDesignerField({ - fieldLayout: fieldLayout, - withCardViewDesigner: true, - disabled: readOnly, - }) }} - -{% endblock %} diff --git a/src/templates/store-management/discounts/_edit.twig b/src/templates/store-management/discounts/_edit.twig deleted file mode 100644 index 962bfedae9..0000000000 --- a/src/templates/store-management/discounts/_edit.twig +++ /dev/null @@ -1,654 +0,0 @@ - -{% set fullPageForm = true %} - -{% import "_includes/forms" as forms %} -{% import "commerce/_includes/forms/commerceForms" as commerceForms %} - -{% set mainFormAttributes = { - id: 'discountform', - method: 'post', - 'accept-charset': 'UTF-8' -} %} - -{% set formActions = [ - { - label: 'Save and continue editing'|t('app'), - redirect: (isNewDiscount ? 'commerce/store-management/#{storeHandle}/discounts/{id}' : discount.getCpEditUrl())|hash, - retainScroll: true, - shortcut: true, - }] -%} - -{% set couponsTable = { - name: 'coupons', - id: 'coupons-table', - cols: { - id: { - type: 'singleline', - heading: 'id'|t('app'), - class: 'hidden', - }, - code: { - type: 'singleline', - heading: 'Code'|t('commerce'), - }, - uses: { - type: 'singleline', - heading: 'Uses'|t('commerce'), - }, - maxUses: { - type: 'singleline', - heading: 'Max Uses'|t('commerce'), - info: 'Leave blank for unlimited uses.'|t('commerce'), - }, - }, - defaultValues: { uses: 0 } -} %} - -{% hook "cp.commerce.discounts.edit" %} - -{% block content %} - {% set formAttributes = { - id: 'discountform', - method: 'post', - 'accept-charset': 'UTF-8', - data: { - saveshortcut: true, - 'saveshortcut-redirect': "commerce/store-management/#{storeHandle}/discounts"|hash, - 'confirm-unload': true - }, - } %} - - {{ hiddenInput('storeId', discount.storeId) }} - {% if discount.id %} - - - {% endif %} - -
- {{ forms.textField({ - first: true, - label: "Name"|t('commerce'), - instructions: "What this discount will be called in the control panel."|t('commerce'), - id: 'name', - name: 'name', - value: discount.name, - errors: discount.getErrors('name'), - autofocus: true, - required: true, - }) }} - - {{ forms.textField({ - label: "Description"|t('commerce'), - instructions: "Discount description."|t('commerce'), - id: 'description', - name: 'description', - value: discount.description, - errors: discount.getErrors('description'), - }) }} - - {% hook "cp.commerce.discount.edit" %} -
- - - - - - - - - - {% hook "cp.commerce.discounts.edit.content" %} -{% endblock %} - -{% js %} -$(function() { - - $('#code').on('keyup blur', function(event) { - if (this.value.length === 0) { - $('#coupon-fields').addClass('hidden'); - } else { - $('#coupon-fields').removeClass('hidden'); - } - }); - - function disableShippingSwitch() { - $('#hasFreeShippingForMatchingItems').data('lightswitch').turnOff(); - $('input[name="hasFreeShippingForMatchingItems"]').prop("disabled", true); - $('#hasFreeShippingForMatchingItems').prop("disabled", true); - $("#hasFreeShippingForMatchingItems").addClass("disabled"); - } - - function enableShippingSwitch() { - $('input[name="hasFreeShippingForMatchingItems"]').prop("disabled", false); - $('#hasFreeShippingForMatchingItems').prop("disabled", false); - $("#hasFreeShippingForMatchingItems").removeClass("disabled"); - } - - if ($('input[name="hasFreeShippingForOrder"]').val() == 1) { - disableShippingSwitch(); - } - - $('#hasFreeShippingForOrder').click(function() { - if ($('input[name="hasFreeShippingForOrder"]').val() == 1) { - disableShippingSwitch(); - } else { - enableShippingSwitch(); - } - }); - - $('.clear-btn.discount-clear-use').click(function(event) { - var $this = $(this); - var $spinner = $($this.data('spinner')); - var $field = $($this.data('field')); - var type = $this.data('type'); - var r = confirm(Craft.t('commerce', 'Are you sure you want to clear this discount usage counter?')); - - if (r == true) { - $spinner.toggleClass('hidden'); - $.ajax({ - type: "POST", - dataType: 'json', - headers: { - "X-CSRF-Token": '{{ craft.app.request.csrfToken }}', - }, - url: '', - data: { - 'action' : 'commerce/discounts/clear-discount-uses', - 'id': '{{ discount.id ?? '' }}', - 'type': type - }, - success: function(data){ - $spinner.toggleClass('hidden'); - $field.val(''); - Craft.cp.displayNotice(Craft.t('commerce', 'Counter has been cleared.')); - $this.attr('disabled', 'disabled').prop('disabled', 'disabled'); - } - }); - } - }); - - new Craft.Commerce.Coupons('#commerce-coupons', { - couponFormat: "{{ discount.couponFormat|e('js') }}", - table: { - name: "{{ couponsTable.name|namespaceInputName|e('js') }}", - cols: {{ couponsTable.cols|json_encode|raw }}, - defaultValues: {{ couponsTable.defaultValues|json_encode|raw }} - }, - }); -}); -{% endjs %} diff --git a/src/templates/store-management/pricing-rules/_edit.twig b/src/templates/store-management/pricing-rules/_edit.twig deleted file mode 100644 index 9ed93b20a4..0000000000 --- a/src/templates/store-management/pricing-rules/_edit.twig +++ /dev/null @@ -1,105 +0,0 @@ -{% import "_includes/forms" as forms %} -{% import "commerce/_includes/forms/commerceForms" as commerceForms %} - -{% if catalogPricingRule.id %} - {{ hiddenInput('id', catalogPricingRule.id) }} -{% endif %} -{{ hiddenInput('storeId', catalogPricingRule.storeId) }} - -
- {{ forms.textField({ - first: true, - label: "Name"|t('commerce'), - instructions: "What this catalog pricing rule will be called in the control panel."|t('commerce'), - id: 'name', - name: 'name', - value: catalogPricingRule.name, - errors: catalogPricingRule.getErrors('name'), - autofocus: true, - required: true, - }) }} - - {{ forms.textField({ - label: "Description"|t('commerce'), - instructions: "Catalog pricing rule description."|t('commerce'), - id: 'description', - name: 'description', - value: catalogPricingRule.description, - errors: catalogPricingRule.getErrors('description'), - }) }} -
- - - - - - {% hook "cp.commerce.catalogPricingRules.edit.content" %} \ No newline at end of file diff --git a/src/templates/store-management/shipping/shippingmethods/_edit.twig b/src/templates/store-management/shipping/shippingmethods/_edit.twig deleted file mode 100644 index b613dc6443..0000000000 --- a/src/templates/store-management/shipping/shippingmethods/_edit.twig +++ /dev/null @@ -1,148 +0,0 @@ -{% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} - - -{% set fullPageForm = true %} - -{% import "_includes/forms" as forms %} - -{% block content %} - {{ hiddenInput('storeId', shippingMethod.storeId) }} - {{ actionInput('commerce/shipping-methods/save') }} - {{ redirectInput("commerce/store-management/#{storeHandle}/shippingmethods/{id}#rules") }} - - {% if shippingMethod.id %} - {{ hiddenInput('shippingMethodId', shippingMethod.id) }} - {% endif %} - - {{ forms.textField({ - first: true, - label: "Name"|t('commerce'), - id: 'name', - name: 'name', - value: shippingMethod.getName(), - errors: shippingMethod.getErrors('name'), - autofocus: true, - required: true, - }) }} - - {{ forms.textField({ - first: true, - label: "Handle"|t('commerce'), - instructions: "How this shipping method will be referred to in templates and forms."|t('commerce'), - id: 'handle', - class: 'code', - name: 'handle', - value: shippingMethod.handle, - errors: shippingMethod.getErrors('handle'), - required: true, - }) }} - -
- {{ forms.iconPickerField({ - label: 'Icon'|t('app'), - id: 'icon', - name: 'icon', - value: shippingMethod.icon, - errors: shippingMethod.getErrors('icon'), - fieldClass: 'width-50', - }) }} - - {{ forms.colorSelectField({ - label: "Color"|t('commerce'), - id: 'color', - name: 'color', - value: shippingMethod.color, - errors: shippingMethod.getErrors('color'), - fieldClass: 'width-50', - }) }} -
- - {% set orderConditionInput %} - {{ shippingMethod.orderCondition.getBuilderHtml()|raw }} - {% endset %} - - {{ forms.field({ - id: 'orderCondition', - label: 'Match Order'|t('commerce'), - errors: shippingMethod.getErrors('orderCondition'), - instructions: 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.'|t('commerce'), - }, orderConditionInput) }} - - {% set customerConditionInput %} - {{ shippingMethod.customerCondition.getBuilderHtml()|raw }} - {% endset %} - - {{ forms.field({ - id: 'customerCondition', - label: 'Match Customer'|t('commerce'), - errors: shippingMethod.getErrors('customerCondition'), - instructions: 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.'|t('commerce'), - }, customerConditionInput) }} - -{% if shippingMethod.id %} - -{% endif %} - - {% hook 'cp.commerce.shippingMethods.edit.content' %} -{% endblock %} - -{% set tableData = [] %} -{% for shippingRule in shippingRules %} - - {% set details %} - {% if shippingRule.baseRate > 0 or - shippingRule.minRate > 0 or - shippingRule.maxRate > 0 or - shippingRule.perItemRate > 0 or - shippingRule.weightRate > 0 or - shippingRule.percentageRate > 0 %} -

Costs

- {% endif %} - {% if shippingRule.baseRate > 0 %}{{ "Base Rate"|t('commerce') }}: {{shippingRule.baseRate|commerceCurrency}}
{% endif %} - {% if shippingRule.minRate > 0 %}{{ "Minimum Total Shipping Cost"|t('commerce') }}: {{shippingRule.minRate|commerceCurrency}}
{% endif %} - {% if shippingRule.maxRate > 0 %}{{ "Maximum Total Shipping Cost"|t('commerce') }}: {{shippingRule.maxRate|commerceCurrency}}
{% endif %} - {% if shippingRule.perItemRate > 0 %}{{ "Default Per Item Rate"|t('commerce') }}: {{shippingRule.perItemRate|commerceCurrency}}
{% endif %} - {% if shippingRule.weightRate > 0 %}{{ "Default Weight Rate"|t('commerce') }}: {{shippingRule.weightRate|commerceCurrency}}
{% endif %} - {% if shippingRule.percentageRate > 0 %}{{ "Default Percentage Rate"|t('commerce') }}: {{shippingRule.percentageRate|trim('0', 'right')}}
{% endif %} - {% endset %} - - {% set tableData = tableData|merge([{ - id: shippingRule.id, - title: shippingRule.name|t('site')|e, - url: url("commerce/store-management/#{storeHandle}/shippingmethods/#{shippingMethod.id}/shippingrules/#{shippingRule.id}"), - status: shippingRule.enabled ? true : false, - description: shippingRule.description|t('site')|e, - detail: { handle: tag('span', { 'data-icon': 'info', title: 'Show rule details'|t('commerce')|e }) , content: details } - }]) %} -{% endfor %} - -{% js %} - {% if not shippingMethod.id %}new Craft.HandleGenerator('#name', '#handle');{% endif %} - - var columns = [ - { name: '__slot:title', title: Craft.t('commerce', 'Shipping Rule') }, - { name: 'description', title: Craft.t('commerce', 'Description') }, - { name: '__slot:detail', title: '', titleClass: 'thin' }, - ]; - - new Craft.VueAdminTable({ - columns: columns, - container: '#rules-vue-admin-table', - deleteAction: 'commerce/shipping-rules/delete', - emptyMessage: Craft.t('commerce', 'No shipping rules exist yet.'), - tableData: {{ tableData|json_encode|raw }}, - reorderAction: 'commerce/shipping-rules/reorder', - reorderFailMessage: Craft.t('commerce', 'Couldn’t reorder rules.'), - reorderSuccessMessage: Craft.t('commerce', 'Rules reordered.'), - }); -{% endjs %} diff --git a/src/templates/store-management/shipping/shippingrules/_edit.twig b/src/templates/store-management/shipping/shippingrules/_edit.twig deleted file mode 100644 index 0470062ad2..0000000000 --- a/src/templates/store-management/shipping/shippingrules/_edit.twig +++ /dev/null @@ -1,400 +0,0 @@ -{% extends "commerce/_layouts/store-management" %} -{% set isIndex = false %} -{% set crumbs = [ - { label: "Shipping Methods"|t('commerce'), url: url("commerce/store-management/#{storeHandle}/shippingmethods") }, - { label: shippingMethod.getName()|t('commerce'), url: url("commerce/store-management/#{storeHandle}/shippingmethods/#{methodId}") }, -] %} - -{% set selectedSubnavItem = 'shipping' %} - -{% set fullPageForm = true %} - -{% import "_includes/forms" as forms %} -{% import "commerce/_includes/forms/commerceForms" as commerceForms %} - -{% set tabs = { - rule: {'label':'Rule'|t('commerce'),'url':'#rule-tab'}, - conditions: {'label':'Conditions'|t('commerce'),'url':'#conditions-tab'}, - costs: {'label':'Costs'|t('commerce'),'url':'#costs-tab'} -} %} -{% set currency = shippingMethod.getStore().getCurrency() %} -{% set decimals = craft.commerce.currencies.getSubunitFor(currency) %} - -{% block actionButton %} -
- - {% if shippingRule.id %} - - - {% endif %} -
-{% endblock %} - -{% block details %} -
- {{ forms.lightSwitchField({ - label: "Enable this shipping rule"|t('commerce'), - id: 'enabled', - name: 'enabled', - value: 1, - on: shippingRule.enabled, - checked: shippingRule.enabled, - errors: shippingRule.getErrors('enabled') - }) }} -
- - {% if shippingRule and shippingRule.id %} -
-
-
{{ "Created at"|t('app') }}
-
{{ shippingRule.dateCreated|datetime('short') }}
-
-
-
{{ "Updated at"|t('app') }}
-
{{ shippingRule.dateUpdated|datetime('short') }}
-
-
- {% endif %} -{% endblock %} - -{% block content %} - {{ actionInput('commerce/shipping-rules/save') }} - {{ hiddenInput('methodId', methodId) }} - {{ hiddenInput('storeId', storeId) }} - {{ redirectInput("commerce/store-management/#{storeHandle}/shippingmethods/#{methodId}#rules") }} - - {% if shippingRule.id %}{% endif %} - -
- {{ forms.textField({ - first: true, - label: "Name"|t('commerce'), - instructions: "What this shipping rule will be called in the control panel."|t('commerce'), - id: 'name', - name: 'name', - value: shippingRule.name, - errors: shippingRule.getErrors('name'), - autofocus: true, - required: true, - }) }} - - {{ forms.textField({ - first: true, - label: "Description"|t('commerce'), - instructions: "Describe this rule."|t('commerce'), - name: 'description', - value: shippingRule.description, - errors: shippingRule.getErrors('description'), - }) }} -
- - -{% endblock %} - -{% css %}.commerce-shipping-rules-text-right { text-align: right; }{% endcss %} - -{% js %} - window.shippingCategories = {{ craft.commerce.shippingCategories.allShippingCategories(shippingMethod.storeId).all()|json_encode|raw }}; - - function toggleCategoryOverridesHeading() { - if ($('.js-category-override-row.hidden').length == Object.keys(window.shippingCategories).length) { - $('.js-category-overrides-heading').addClass('hidden'); - } else { - $('.js-category-overrides-heading').removeClass('hidden'); - } - } - - $("select[name^='ruleCategories']").change(function() { - var id = $(this).closest('tr').data('id'); - var value = $(this).val(); - var $rateRow = $("#shipping-categories-rates").find("tr[data-id="+id+"]"); - - if (value == 'disallow') { - $rateRow.addClass('hidden'); - } else { - $rateRow.removeClass('hidden'); - } - - toggleCategoryOverridesHeading(); - }); - - toggleCategoryOverridesHeading(); -{% endjs %} diff --git a/src/templates/store-management/shipping/shippingzones/_fields.twig b/src/templates/store-management/shipping/shippingzones/_fields.twig deleted file mode 100644 index e2a3fedf05..0000000000 --- a/src/templates/store-management/shipping/shippingzones/_fields.twig +++ /dev/null @@ -1,30 +0,0 @@ -{% import "_includes/forms" as forms %} - -{% do view.registerTranslations('commerce', [ - "Example" -]) %} - - {{ forms.textField({ - first: true, - label: "Name"|t('commerce'), - instructions: "What this shipping zone will be called in the control panel."|t('commerce'), - id: 'name', - name: 'name', - value: shippingZone is defined ? shippingZone.name, - errors: shippingZone is defined ? shippingZone.getErrors('name'), - autofocus: true, - required: true, - }) }} - - {{ forms.textField({ - label: "Description"|t('commerce'), - instructions: "Describe this shipping zone."|t('commerce'), - id: 'description', - name: 'description', - value: shippingZone is defined ? shippingZone.description, - errors: shippingZone is defined ? shippingZone.getErrors('description'), - }) }} - - {{ forms.field({ - label: 'Address Condition'|t('app'), - }, condition.builderHtml) }} \ No newline at end of file diff --git a/src/templates/store-management/tax/taxzones/_fields.twig b/src/templates/store-management/tax/taxzones/_fields.twig deleted file mode 100644 index 541c9aaf42..0000000000 --- a/src/templates/store-management/tax/taxzones/_fields.twig +++ /dev/null @@ -1,49 +0,0 @@ -{% import "_includes/forms" as forms %} - -{% do view.registerTranslations('commerce', [ - "Example" -]) %} - - {% if store is defined %} - {{ hiddenInput('storeId', store.id) }} - {% endif %} - - {{ forms.textField({ - first: true, - label: "Name"|t('commerce'), - instructions: "What this tax zone will be called in the control panel."|t('commerce'), - id: 'name', - name: 'name', - value: taxZone is defined ? taxZone.name, - errors: taxZone is defined ? taxZone.getErrors('name'), - autofocus: true, - required: true, - }) }} - - {{ forms.textField({ - label: "Description"|t('commerce'), - instructions: "Describe this tax zone."|t('commerce'), - id: 'description', - name: 'description', - value: taxZone is defined ? taxZone.description, - errors: taxZone is defined ? taxZone.getErrors('description'), - }) }} - - {% if store.getUseBillingAddressForTax() %} - {% set labelDefault = "Default to this tax zone when no billing address is set"|t('commerce') %} - {% else %} - {% set labelDefault = "Default to this tax zone when no shipping address is set"|t('commerce') %} - {% endif %} - - {{ forms.lightswitchField({ - label: labelDefault, - id: 'default', - name: 'default', - value: 1, - on: taxZone is defined ? taxZone.default, - errors: taxZone is defined ? taxZone.getErrors('default') - }) }} - - {{ forms.field({ - label: 'Address Condition'|t('app'), - }, condition.builderHtml) }} diff --git a/src/templates/subscriptions/_edit.twig b/src/templates/subscriptions/_edit.twig deleted file mode 100644 index 61028a15b9..0000000000 --- a/src/templates/subscriptions/_edit.twig +++ /dev/null @@ -1,247 +0,0 @@ -{% extends "_layouts/cp" %} - -{% set selectedSubnavItem = "subscriptions" %} -{% set bodyClass = (bodyClass is defined ? bodyClass~' ' : '') ~ "commercesubscriptions commercesubscriptionsedit" %} - -{% set title = subscription %} - -{% set crumbs = [ - { label: 'Commerce'|t('commerce'), url: url('commerce') }, - { label: "Subscriptions"|t('commerce'), url: url('commerce/subscriptions') } -] %} - -{% import "_includes/forms" as forms %} - -{% block header %} - {{ block('pageTitle') }} -
- - {% block actionButton %} -
- -
- {% endblock %} - -{% endblock %} - -{% block content %} -
-
- - - {{ redirectInput('commerce/subscriptions') }} - {{ csrfInput() }} - {{ fieldsHtml|raw }} -
- -
-
-

{{ 'Manage subscription'|t('commerce') }}

- - {% if subscription.gateway.supportsPlanSwitch and not subscription.isCanceled and not subscription.isExpired %} - {% set plans = subscription.alternativePlans %} - {% set planOptions = [{label: 'Pick a plan'|t('commerce'), value: ''}] %} - - {% for plan in plans %} - {% set planOptions = planOptions|merge([{label: plan.name, value:plan.id}]) %} - {% endfor %} - - {{ forms.selectField({ - label: 'Switch plan'|t('commerce'), - options: planOptions, - id: 'switchPlans' - }) }} - - {% for plan in plans %} - - {% endfor %} - {% else %} -
{{ 'Cannot switch plans for this subscription.'|t('commerce') }}
- {% endif %} - - {% if subscription.canReactivate() %} -
- - - {{ redirectInput(continueEditingUrl) }} - {{ csrfInput() }} - - -
- {% endif %} - -
- - {% if not subscription.isCanceled and not subscription.isExpired %} -
-

{{ 'Cancel subscription'|t('commerce') }}

- -
- - - {{ redirectInput(continueEditingUrl) }} - {{ csrfInput() }} - - {{ subscription.plan.getGateway().getCancelSubscriptionFormHtml(subscription)|raw }} - -
-
-
- -
-
- {% endif %} - -
-

Payment history

- {% set payments = subscription.getAllPayments() %} - - - - - - - - - - - - {% for payment in payments %} - {% set info = [ - { label: "Reference", type: 'code', value: payment.paymentReference }, - { label: "Gateway response", type: 'response', value: payment.response|raw }, - ] %} - - - - - - - {% endfor %} - - -
{{ 'Invoice date'|t('commerce') }}{{ 'Invoice amount'|t('commerce') }}{{ 'Status'|t('commerce') }}{{ 'Info'|t('commerce') }}s
{{ payment.paymentDate|datetime }}{{ payment.paymentCurrency }} {{ payment.paymentAmount }} - {{ payment.paid ? 'Paid'|t('commerce') : 'Unpaid'|t('commerce') }} -
- - {% if not subscription.isCanceled and not subscription.isExpired %} -
- - - {{ redirectInput(continueEditingUrl) }} - {{ csrfInput() }} - -
-
-
-
- {% endif %} -
- - {% hook 'cp.commerce.subscriptions.edit.content' %} -
-
-{% endblock %} - - -{% block details %} - -
- - - -
-
{{ 'Plan'|t('commerce') }}
-
{{ subscription.getPlan().name }}
-
- -
-
{{ 'Reference'|t('commerce') }}
-
- {% include '_includes/forms/copytext' with { - id: 'commerce-sub-reference', - buttonId: 'commerce-sub-reference-copy-btn', - value: subscription.reference, - class: ['code', not subscription.reference ? 'disabled' : '']|filter, - - } %} -
-
- -
-
{{ 'Created'|t('commerce') }}
-
{{ subscription.dateCreated|datetime }}
-
- -
-
{{ 'Trial days credited'|t('commerce') }}
-
{{ subscription.trialDays }}
-
- -{% if subscription.trialDays %} -
-
{{ 'Trial expiration'|t('commerce') }}
-
{{ subscription.trialExpires|datetime }}
-
-{% endif %} - -
-
{{ 'Next payment'|t('commerce') }}
-
{{ subscription.nextPaymentDate|datetime }}
-
- -
-
{{ 'Expiry'|t('commerce') }}
-
{{ subscription.dateExpired ? subscription.dateExpired|datetime : '' }}
-
- -
-
{{ 'Cancellation'|t('commerce') }}
-
{{ subscription.dateCanceled ? subscription.dateCanceled|datetime : '' }}
-
- -
-
{{ 'Billing issues'|t('commerce') }}
-
{{ subscription.getBillingIssueDescription() }}
-
- -
-{% hook 'cp.commerce.subscriptions.edit.meta' %} -{% endblock %} - -{% js %} - $(document).ready(function () { - $('#switchPlans').on('change', function (ev) { - $('.switchPlansForm').addClass('hidden'); - $('#switch-'+ev.currentTarget.value).removeClass('hidden'); - }); - - $.each($('.tableRowInfo'), function () { - new Craft.Commerce.TableRowAdditionalInfoIcon(this); - }); - - $('#saveCustomFieldsSubmit').click(function(){ - $('form#customFields').submit(); - }); - }); - -{% endjs %} - -{% do view.registerAssetBundle("craft\\web\\assets\\prismjs\\PrismJsAsset") %} diff --git a/src/templates/subscriptions/_index.twig b/src/templates/subscriptions/_index.twig deleted file mode 100644 index 436d1c7d80..0000000000 --- a/src/templates/subscriptions/_index.twig +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "_layouts/elementindex" %} - -{% set title = "Subscriptions"|t('commerce') %} -{% set docTitle = title~' - '~'Commerce' %} -{% set elementType = 'craft\\commerce\\elements\\Subscription' %} -{% set selectedTab = 'subscriptions' %} -{% set selectedSubnavItem = "subscriptions" %} -{% set bodyClass = (bodyClass is defined ? bodyClass~' ' : '') ~ "commercesubscriptions commercesubscriptionsindex" %} - -{% set crumbs = [ - { label: 'Commerce'|t('commerce'), url: url('commerce') }, -] %} - -{% js %} - if (typeof Craft.Commerce === 'undefined') { - Craft.Commerce = {}; - } - -{% endjs %} diff --git a/src/templates/subscriptions/plans/_edit.twig b/src/templates/subscriptions/plans/_edit.twig deleted file mode 100644 index 0558c24b70..0000000000 --- a/src/templates/subscriptions/plans/_edit.twig +++ /dev/null @@ -1,77 +0,0 @@ -{% import "_includes/forms" as forms %} - -{% block content %} - {% if gatewayOptions|length > 0 or plan is not null %} - {% if plan is not null and plan.id %} - - {% endif %} - -
- {{ forms.textField({ - first: true, - label: "Name"|t('commerce'), - instructions: "What this subscription plan will be called in the control panel."|t('commerce'), - id: 'name', - name: 'name', - value: plan ? plan.name : '', - errors: plan ? plan.getErrors('name') : [], - autofocus: true, - required: true - }) }} - - {{ forms.textField({ - label: "Handle"|t('commerce'), - instructions: "How you’ll refer to this subscription plan in the templates."|t('commerce'), - id: 'handle', - class: 'code', - name: 'handle', - value: plan ? plan.handle : '', - errors: plan ? plan.getErrors('handle') : [], - required: true - }) }} - - {{ forms.elementSelectField({ - elementType: entryElementType, - elements: (plan and plan.planInformationId) ? craft.entries.status(null).id(plan.planInformationId).all() : null, - instructions: "The entry that contains the description for this subscription’s plan."|t('commerce'), - id: 'planInformation', - label: "Description"|t(','), - class: 'ltr', - name: 'planInformation', - limit: 1 - }) }} - - {{ forms.selectField({ - label: "Gateway"|t('commerce'), - instructions: "The payment gateway that will be used for the subscription plan."|t('commerce'), - id: 'gatewayId', - class: 'gateway-select code ltr', - name: 'gatewayId', - value: plan ? plan.gatewayId, - options: gatewayOptions, - errors: plan ? plan.getErrors('gatewayId') : [] - }) }} - - {% for gateway in supportedGateways %} - {% set isCurrent = plan and (gateway.id == plan.gatewayId) %} - -
- {% namespace 'gateway['~gateway.id~']' %} - {{ gateway.getPlanSettingsHtml({'plan': plan, 'gateway': gateway})|raw }} - {% endnamespace %} -
- {% endfor %} -
- {% else %} -

{{ 'You must set up at least one gateway that supports subscriptions first.'|t('commerce', {'link': url('commerce/settings/gateways')})|raw }}

- {% endif %} -{% endblock %} - -{% js %} - {% if plan is null or not plan.handle %}new Craft.HandleGenerator('#name', '#handle');{% endif %} - - $('#gatewayId').on('change', function (ev) { - $('.gateway-settings').addClass('hidden'); - $('#gateway-settings-' + ev.currentTarget.value).removeClass('hidden'); - }); -{% endjs %} diff --git a/src/templates/subscriptions/plans/index.twig b/src/templates/subscriptions/plans/index.twig deleted file mode 100644 index 90dbae0076..0000000000 --- a/src/templates/subscriptions/plans/index.twig +++ /dev/null @@ -1,59 +0,0 @@ -{% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} -{% do view.registerTranslations('commerce', [ - 'Active subscriptions', - 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.', - 'Couldn’t reorder plans.', - 'Enabled?', - 'Gateway', - 'Handle', - 'Information linked?', - 'Name', - 'No subscription plans exist yet.', - 'No', - 'Plans reordered.', - 'Yes', -]) %} - -{% block content %} -
-{% endblock %} - -{% set tableData = [] %} -{% for plan in plans %} - {% set tableData = tableData|merge([{ - id: plan.id, - title: plan.name|t('site')|e, - url: plan.getCpEditUrl(), - status: plan.enabled ? true : false, - handle: plan.handle|e, - gateway: plan.gateway.name|t('site')|e, - subscriptions: plan.subscriptionCount, - information: plan.planInformationId ? 'Yes'|t('commerce')|e : 'No'|t('commerce')|e, - }]) %} -{% endfor %} - - -{% js %} -var columns = [ - { name: '__slot:title', title: Craft.t('commerce', 'Name') }, - { name: '__slot:handle', title: Craft.t('commerce', 'Handle') }, - { name: 'gateway', title: Craft.t('commerce', 'Gateway') }, - { name: 'subscriptions', title: Craft.t('commerce', 'Active subscriptions') }, - { name: 'information', title: Craft.t('commerce', 'Information linked?'), - callback: function(value) { - return ''+Craft.escapeHtml(value)+''; - } - } -]; - -new Craft.VueAdminTable({ - columns: columns, - container: '#plans-vue-admin-table', - deleteAction: 'commerce/plans/archive-plan', - deleteConfirmationMessage: Craft.t('commerce', 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.'), - reorderAction: 'commerce/plans/reorder', - reorderSuccessMessage: Craft.t('commerce', 'Plans reordered.'), - reorderFailMessage: Craft.t('commerce', 'Couldn’t reorder plans.'), - tableData: {{ tableData|json_encode|raw }} - }); -{% endjs %} diff --git a/src/test/fixtures/elements/ProductFixture.php b/src/test/fixtures/elements/ProductFixture.php deleted file mode 100644 index aa36f5c348..0000000000 --- a/src/test/fixtures/elements/ProductFixture.php +++ /dev/null @@ -1,150 +0,0 @@ - - * @author Robuust digital | Bob Olde Hampsink - * @author Global Network Group | Giel Tettelaar - * @since 2.1 - */ -class ProductFixture extends BaseElementFixture -{ - /** - * @var array - */ - protected array $productTypeIds = []; - - private ?VariantCollection $_variants = null; - - /** - * {@inheritdoc} - */ - public function init(): void - { - parent::init(); - - // Ensure loaded - $commerce = Plugin::getInstance(); - if (!$commerce) { - throw new InvalidArgumentException('Commerce plugin needs to be loaded before using the ProductFixture'); - } - - // Get all product type id's - $this->productTypeIds = $this->_getProductTypeIds(); - } - - /** - * @inheritdoc - */ - public function afterLoad(): void - { - $this->productTypeIds = $this->_getProductTypeIds(); - - // Generate catalog pricing - Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); - } - - protected function createElement(): ElementInterface - { - return new Product(); - } - - /** - * Get array of product type IDs indexed by handle. - * This uses a raw query to avoid service level caching/memoization. - * - * @todo Review whether this raw-query workaround for service-level memoization is still needed in Commerce 6.0 #COM-54 - */ - private function _getProductTypeIds(): array - { - return (new Query()) - ->select([ - 'productTypes.id', - 'productTypes.handle', - ]) - ->from([Table::PRODUCTTYPES . ' productTypes']) - ->indexBy('handle') - ->column(); - } - - /** - * @inheritdoc - * @param Product $element - */ - protected function populateElement(ElementInterface $element, array $attributes): void - { - foreach ($attributes as $name => $value) { - if ($name !== '_variants') { - $element->$name = $value; - } else { - $this->_variants = VariantCollection::make($value); - $element->setVariants($value); - } - } - } - - /** - * @inheritdoc - */ - protected function saveElement(ElementInterface $element): bool - { - $return = parent::saveElement($element); - - // Save the variants - $this->_variants->each(function(Variant $v) use ($element) { - if ((new Query()) - ->from(Table::VARIANTS . ' v') - ->leftJoin(Table::PURCHASABLES . ' p', '[[p.id]] = [[v.id]]') - ->where(['primaryOwnerId' => $element->id]) - ->andWhere(['p.sku' => $v->getSku()]) - ->exists() - ) { - return; - } - - $v->setPrimaryOwnerId($element->id); - $v->setOwnerId($element->id); - \Craft::$app->getElements()->saveElement($v,false); - }); - - $this->_variants = null; - - return $return; - } - - /** - * @inheritdoc - */ - protected function deleteElement(ElementInterface $element): bool - { - /** @var Product $element */ - $variants = $element->getVariants(true); - - foreach ($variants as $variant) { - Craft::$app->getElements()->deleteElement($variant, true); - } - - return parent::deleteElement($element); - } -} diff --git a/src/test/mockclasses/Purchasable.php b/src/test/mockclasses/Purchasable.php deleted file mode 100644 index 606d1d333a..0000000000 --- a/src/test/mockclasses/Purchasable.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @author Global Network Group | Giel Tettelaar - * @since 2.1 - */ -class Purchasable extends BasePurchasable -{ - public bool $isPromotable = true; - - public float $price = 25.10; - - public function getIsPromotable(): bool - { - return $this->isPromotable; - } - - public function getPrice(string|Store|null $store = null): ?float - { - return 25.10; - } - - public function getSku(): string - { - return 'commerce_testing_unique_sku'; - } -} diff --git a/src/translations/de/commerce.php b/src/translations/de/commerce.php deleted file mode 100644 index 05f7435bc3..0000000000 --- a/src/translations/de/commerce.php +++ /dev/null @@ -1,1429 +0,0 @@ - '(neuer Preis)', - '(of original price)' => '(vom ursprünglichen Preis)', - '(off original price)' => '(Nachlass vom ursprünglichen Preis)', - 'A cart number must be specified.' => 'Es muss eine Warenkorbnummer angegeben werden.', - 'A cart recovery link has been sent to {email}.' => 'Ein neuer Warenkorb-Wiederherstellungslink wurde an {email} gesendet.', - 'A cart recovery link will be sent to {email}.' => 'Ein Warenkorb-Wiederherstellungslink wird an {email} gesendet.', - 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Eine benutzerfreundliche Referenznummer auf Grundlage dieses Formats wird erstellt, sobald der Warenkorb abgeschlossen und die Artikel darin bestellt wurden. Beispielsweise {ex1} oder
{ex2}. Das Ergebnis dieses Formats muss einmalig sein.', - 'A new download link has been sent to {email}' => 'Ein neuer Download-Link wurde an {email} gesendet', - 'A new download link will be sent to {email}' => 'Ein neuer Download-Link wird an {email} gesendet', - 'A valid email is required to create a customer.' => 'Eine gültige E-Mail wird erfordert, um einen Kunden zu erstellen.', - 'Accept' => 'Akzeptieren', - 'Accepted' => 'Akzeptiert', - 'Actions' => 'Aktionen', - 'Active Carts' => 'Aktive Einkaufskörbe', - 'Active subscriptions' => 'Aktive Abonnements', - 'Active' => 'Aktiv', - 'Add Address' => 'Adresse hinzufügen', - 'Add a coupon' => 'Coupon hinzufügen', - 'Add a custom line item' => 'Benutzerdefinierten Einzelposten hinzufügen', - 'Add a line item' => 'Einen Einzelposten hinzufügen', - 'Add a product' => 'Ein Produkt hinzufügen', - 'Add a variant' => 'Variante hinzufügen', - 'Add an adjustment' => 'Eine Anpassung hinzufügen', - 'Add an item' => 'Posten hinzufügen', - 'Add an option' => 'Eine Option hinzufügen', - 'Add catalog price' => 'Katalogpreis hinzufügen', - 'Add' => 'Hinzufügen', - 'Additional Actions' => 'Zusätzliche Aktionen', - 'Additional recipients that should receive this email. Twig code can be used here.' => 'Zusätzliche Empfänger, die diese E-Mail erhalten sollen. Twig-Code kann hier verwendet werden.', - 'Address 1' => 'Adresse 1', - 'Address 2' => 'Adresse 2', - 'Address 3' => 'Adresse 3', - 'Address Line 1' => 'Adresszeile 1', - 'Address Line 2' => 'Adresszeile 2', - 'Address Updated.' => 'Adresse aktualisiert.', - 'Address copied to user.' => 'Adresse an Benutzer kopiert.', - 'Address not found.' => 'Adresse nicht gefunden.', - 'Adjust Quantity' => 'Menge anpassen', - 'Adjust by' => 'Anpassen durch', - 'Adjust price when included rate is disqualified?' => 'Preis anpassen, wenn der enthaltene Steuersatz nicht dafür qualifiziert ist?', - 'Adjustments' => 'Anpassungen', - 'Admin Notices' => 'Administratorhinweise', - 'Administrative Area Code of Origin' => 'Code des Verwaltungsgebiets', - 'Advanced' => 'Erweitert', - 'All Orders' => 'Alle Bestellungen', - 'All Totals' => 'Alle Summen', - 'All Transfers' => 'Alle Übertragungen', - 'All active subscriptions' => 'Alle aktive Abonnements', - 'All customers' => 'Alle Kunden', - 'All products' => 'Alle Produkte', - 'All variants must have a SKU.' => 'Alle Varianten müssen eine SKU haben.', - 'All' => 'Alle', - 'Allow Checkout Without Payment' => 'Bestellung ohne Zahlung erlauben', - 'Allow Empty Cart On Checkout' => 'Leeren Warenkorb an der Kasse erlauben', - 'Allow Partial Payment On Checkout' => 'Teilzahlung an der Kasse erlauben', - 'Allow out of stock purchases' => 'Erlauben Sie Käufe von nicht vorrätigen Artikeln', - 'Allow' => 'Zulassen', - 'Allowed Qty' => 'Höchstmenge', - 'Alternative Phone' => 'Alternative Telefonnummer', - 'Amount' => 'Menge', - 'An ID must be provided' => 'Ein Ausweis muss beigestellt werden', - 'An error occurred while generating this PDF.' => 'Beim Erstellen dieser PDF ist ein Fehler aufgetreten.', - 'Any' => 'Beliebig', - 'Anywhere' => 'Überall', - 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Sind Sie sicher, dass Sie den Abonnementplan „{name}“ wirklich archivieren möchten? Dadurch werden die bestehenden Abonnements NICHT gekündigt.', - 'Are you sure you want to capture this transaction?' => 'Sind Sie sicher, dass Sie diese Transaktion erfassen möchten?', - 'Are you sure you want to complete this order?' => 'Sind Sie sicher, dass Sie diese Bestellung abschließen möchten?', - 'Are you sure you want to delete the selected orders?' => 'Sind Sie sicher, dass Sie die ausgewählten Bestellungen löschen möchten?', - 'Are you sure you want to delete the selected product and its variants?' => 'Sind Sie sicher, dass Sie die ausgewählten Produkte und ihre Varianten löschen möchten?', - 'Are you sure you want to delete this shipping rule?' => 'Möchten Sie diese Versandregel wirklich löschen?', - 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Sind Sie sicher, dass Sie "{name}" und alle seine Produkte löschen möchten? Bitte vergewissern Sie sich, dass Sie eine Sicherungskopie Ihrer Datenbank besitzen, bevor Sie diese Löschaktion ausführen.', - 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Sind Sie sicher, dass Sie „{name}“ löschen möchten? Dies wird alle Einzelposten mit diesem Status zu „Kein Status“ umstellen.', - 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Sind Sie sicher, dass Sie diese Übertragung als ausstehend markieren möchten? Sie wird am Zielort als eingehend angezeigt.', - 'Are you sure you want to overwrite the billing address?' => 'Sind Sie sicher, dass Sie die Rechnungsadresse überschreiben möchten?', - 'Are you sure you want to overwrite the shipping address?' => 'Sind Sie sicher, dass Sie die Lieferadresse überschreiben möchten?', - 'Are you sure you want to permanently delete this store and everything in it?' => 'Möchten Sie diesen Shop und alle darin enthaltenen Artikel wirklich dauerhaft löschen?', - 'Are you sure you want to refund this transaction?' => 'Sind Sie sicher, dass Sie diese Transaktion erstatten möchten?', - 'Are you sure you want to remove this customer?' => 'Sind Sie sicher, dass Sie diesen Kunden entfernen möchten?', - 'Are you sure you want to save this as a new shipping rule?' => 'Möchten Sie dies wirklich als eine neue Versandregel speichern?', - 'Are you sure you want to send email: {name}?' => 'Sind Sie sicher dass Sie die E-Mail „{name}“ versenden möchten?', - 'At least one site must be enabled for the product type.' => 'Mindestens eine Website muss für den Produkttyp aktiviert sein.', - 'Attempted Payments' => 'Versuchte Zahlungen', - 'Attention' => 'Achtung', - 'Authorize Only (Manually Capture)' => 'Nur autorisieren (Manuelle Erfassung)', - 'Auto Set Cart Shipping Method Option' => 'Versandart des Warenkorbs autom. setzen', - 'Auto Set New Cart Addresses' => 'Neue Warenkorbadressen autom. setzen', - 'Auto Set Payment Source' => 'Zahlungsquelle autom. setzen', - 'Automatic SKU Format' => 'Automatisches Format für die Bestandseinheit (SKU)', - 'Available Shipping Categories' => 'Verfügbare Versandkategorien', - 'Available Tax Categories' => 'Verfügbare Steuerkategorien', - 'Available for purchase' => 'Für den Kauf verfügbar', - 'Available for purchase?' => 'Für den Kauf verfügbar?', - 'Available inventory for "{description}" has gone below zero.' => 'Der verfügbare Lagerbestand für „{description}“ ist unter null gesunken.', - 'Available to Product Types' => 'Für Produkttypen verfügbar', - 'Available' => 'Verfügbar', - 'Available?' => 'Verfügbar?', - 'Average Order Total' => 'Durchschnittlicher Bestellungspreis', - 'Average' => 'Durchschnitt', - 'BCC’d Recipient' => 'Empfänger auf BCC gesetzt', - 'Bad Request' => 'Ungültige Anfrage', - 'Bad address ID.' => 'Ungültige Lieferadressen-ID.', - 'Bad order ID.' => 'Ungültige Bestellungs-ID.', - 'Base Price' => 'Grundpreis', - 'Base Promotional Price' => 'Grundpreis der Aktion', - 'Base Rate' => 'Basissatz', - 'Base' => 'Basis', - 'Bcc' => 'BCC', - 'Billing Address' => 'Rechnungsadresse', - 'Billing Business Name' => 'Geschäftsname (Rechnung)', - 'Billing First Name' => 'Vorname (Rechnung)', - 'Billing Full Name' => 'Vollständiger Name (Rechnung)', - 'Billing Last Name' => 'Nachname (Rechnung)', - 'Billing address required.' => 'Rechnungsadresse erforderlich.', - 'Billing detail update URL' => 'URL zum Aktualisieren der Zahlungsinformationen', - 'Billing issues' => 'Rechnungprobleme', - 'Billing' => 'Abrechnung', - 'Both (Line item price + Line item shipping costs)' => 'Beide (Einzelpostenpries + Einzelposten-Versandkosten)', - 'Business ID' => 'Unternehmens-ID', - 'Business Name' => 'Name des Unternehmens', - 'Business Tax ID' => 'Gewerbesteuer-ID', - 'CC’d Recipient' => 'Empfänger auf CC gesetzt', - 'CVV' => 'CVC', - 'Can be used as an internal reference.' => 'Kann als interne Referenz verwendet werden.', - 'Can not complete payment for missing transaction.' => 'Zahlung für fehlende Transaktion konnte nicht abgeschlossen werden.', - 'Can not create a new order' => 'Neue Bestellung konnte nicht erstellt werden', - 'Can not find an order to pay.' => 'Konnte keine zu Bestellung zum Bezahlen finden.', - 'Can not find enabled email.' => 'Es wurde keine aktivierte E-Mail-Addresse gefunden.', - 'Can not find order' => 'Bestellung konnte nicht gefunden werden', - 'Can not find order.' => 'Bestellung konnte nicht gefunden werden.', - 'Can not find the transaction to refund' => 'Die zu erstattende Transaktion konnte nicht gefunden werden', - 'Can not move between these inventory types.' => 'Sie können nicht zwischen diesen Bestandstypen wechseln.', - 'Can not refund amount greater than the remaining amount' => 'Es kann kein Betrag über dem Restbetrag zurückerstattet werden', - 'Cancel subscription' => 'Abonnement stornieren', - 'Cancel with gateway now' => 'Jetzt über das Zahlungsportal stornieren', - 'Cancel' => 'Abbrechen', - 'Cancellation date' => 'Stornierungsdatum', - 'Cancellation' => 'Stornierung', - 'Cannot switch plans for this subscription.' => 'Pläne für dieses Abonnement können nicht gewechselt werden.', - 'Can’t preview this email.' => 'E-Mail-Vorschau für diese E-Mail nicht verfügbar.', - 'Capture payment' => 'Zahlung erfassen', - 'Capture' => 'Erfassen', - 'Card Holder' => 'Karteninhaber', - 'Card Number' => 'Kartennummer', - 'Card' => 'Karte', - 'Cart Recovery Link' => 'Warenkorb-Wiederherstellungslink', - 'Cart forgotten.' => 'Warenkorb vergessen.', - 'Cart updated.' => 'Warenkorb aktualisiert.', - 'Cart {number}' => 'Warenkorb {number}', - 'Catalog Pricing Rule' => 'Katalogpreisregel', - 'Catalog pricing rule description.' => 'Beschreibung der Katalogpreisregel.', - 'Catalog pricing rule saved.' => 'Katalogpreisregel gespeichert.', - 'Catalog pricing rules deleted.' => 'Katalogpreisregeln gelöscht.', - 'Catalog pricing rules updated.' => 'Katalogpreisregeln aktualisiert.', - 'Categories Relationship Type' => 'Kategorien Beziehungstyp', - 'Categories' => 'Kategorien', - 'Category Rate Overrides' => 'Preiskategorie-Overrides', - 'Centimeters (cm)' => 'Zentimeter (cm)', - 'Changing this value may affect your ability to refund existing transactions.' => 'Das Ändern dieses Wertes könnte Ihre Fähigkeit, bestehende Transaktionen zurück zu erstatten, beeinflussen.', - 'Choose a color to represent the order’s status' => 'Wählen Sie eine Farbe, die den Status der Bestellung repräsentieren soll', - 'Choose a new customer' => 'Neuen Kunden wählen', - 'Choose adjustment values to include when calculating the product revenue total.' => 'Wählen Sie Anpassungswerte, die bei der Berechnung der Summe der Produktumsätze berücksichtigt werden sollen.', - 'Choose the currency’s ISO code.' => 'Wählen Sie den ISO-Code der Währung aus.', - 'Choose the destination inventory location for the existing on hand stock.' => 'Wählen Sie den Ziellagerbestand für den vorhandenen Lagerbestand.', - 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Wählen Sie, in welchen Websites dieser Produkttyp verfügbar sein soll, und konfigurieren Sie die sitespezifischen Einstellungen.', - 'City' => 'Stadt', - 'Clear counter' => 'Zähler zurücksetzen', - 'Clear notices' => 'Anmerkungen entfernen', - 'Close' => 'Schließen', - 'Code' => 'Code', - 'Collated PDF' => 'Zusammengestellte PDF', - 'Color' => 'Farbe', - 'Commerce Products' => 'Commerce Produkte', - 'Commerce Settings' => 'Commerce Einstellungen', - 'Commerce Variants' => 'Commerce-Varianten', - 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Die Commerce E-Mail "{email}" konnte nicht für die Bestellung "{order}" gesendet werden.', - 'Commerce order exports' => 'Exporte von Commerce Bestellungen', - 'Commerce' => 'Commerce', - 'Committed' => 'Zugewiesen', - 'Completed Email' => 'Abgeschlossene E-Mail-Adresse', - 'Completed' => 'Abgeschlossen', - 'Completing order failed.' => 'Bestellungsabschluss fehlgeschlagen.', - 'Condition' => 'Bedingung', - 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Die Bedingungen werden hier mit einer Bestellung abgeglichen, bevor die Regeln abgesucht werden. Dies ist nützlich, wenn Sie die Verfügbarkeit einer Methode frühzeitig prüfen möchten oder wenn es gemeinsame Bedingungen für alle Regeln für diese Methode gibt.', - 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Die Bedingungen werden hier mit denen des Kunden abgeglichen, bevor die Regeln abgesucht werden. Dies ist nützlich, wenn Sie die Verfügbarkeit einer Methode frühzeitig prüfen möchten oder wenn es gemeinsame Bedingungen für alle Regeln für diese Methode gibt.', - 'Conditions' => 'Bedingungen', - 'Contains Purchasables' => 'Enthält kaufbare Artikel', - 'Control Panel Settings' => 'Control Panel Einstellungen', - 'Control panel' => 'Control Panel', - 'Conversion Rate' => 'Wechselkurs', - 'Converted Price' => 'Umgerechneter Preis', - 'Copied!' => 'Kopiert!', - 'Copy the URL' => 'Die URL kopieren', - 'Copy to {location}' => 'Zu {location} kopieren', - 'Copy' => 'Kopieren', - 'Costs' => 'Kosten', - 'Could not archive gateway.' => 'Gateway konnte nicht archiviert werden.', - 'Could not cancel “{reference}”.' => '"{reference}" konnte nicht storniert werden.', - 'Could not create the payment source.' => 'Die Zahlungsquelle konnte nicht erstellt werden.', - 'Could not delete shipping rule' => 'Lieferregel konnte nicht gelöscht werden', - 'Could not delete shipping zone' => 'Lieferzone konnte nicht gelöscht werden', - 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Fehler beim Löschen von {count, number} {count, plural, one{Versandkategorie} other{Versandkategorien}}.', - 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Fehler beim Löschen von {count, number} {count, plural, one{Versandmethode} other{Versandmethoden}} und Regeln.', - 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Fehler beim Löschen von {count, number} {count, plural, one{Steuerkategorie} other{Steuerkategorien}}.', - 'Could not find the email or template.' => 'E-Mail oder Vorlage konnte nicht gefunden werden.', - 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Bestellung {number} konnte nicht als abgeschlossen markiert werden. Speichern der Bestellung ist beim Bestellungsabschluss aufgrund von Fehlern gescheitert: {order}', - 'Could not reactivate “{reference}”.' => '"{reference}" konnte nicht reaktiviert werden.', - 'Could not send email' => 'E-Mail konnte nicht versendet werden', - 'Could not switch “{reference}” to “{plan}”.' => '"{reference}" konnte nicht zu "{plan}" gewechselt werden.', - 'Could not update orders address.' => 'Bestellungsadresse konnte nicht aktualisiert werden.', - 'Couldn’t archive Line Item Status.' => 'Einzelpostenstatus konnte nicht archiviert werden.', - 'Couldn’t archive Order Status.' => 'Bestellungsstatus konnte nicht archiviert werden.', - 'Couldn’t capture transaction.' => 'Transaktion konnte nicht erfasst werden.', - 'Couldn’t capture transaction: {message}' => 'Die Transaktion konnte nicht erfasst werden: {message}', - 'Couldn’t delete email.' => 'E-Mail konnte nicht gelöscht werden.', - 'Couldn’t delete the payment source.' => 'Die Zahlungsquelle konnte nicht gelöscht werden.', - 'Couldn’t get order.' => 'Bestellung konnte nicht empfangen werden.', - 'Couldn’t recalculate order.' => 'Konnte Bestellung nicht erneut berechnen.', - 'Couldn’t refund transaction.' => 'Transaktion konnte nicht erstattet werden.', - 'Couldn’t refund transaction: {message}' => 'Konnte die Transaktion nicht erstatten: {message}', - 'Couldn’t reorder Line Item Statuses.' => 'Status von Einzelposten konnten nicht neu sortiert werden.', - 'Couldn’t reorder Order Statuses.' => 'Die Bestellstatus konnten nicht neu sortiert werden.', - 'Couldn’t reorder PDFs.' => 'PDFs konnten nicht neu sortiert werden.', - 'Couldn’t reorder discounts.' => 'Die Rabatte konnten nicht neu sortiert werden.', - 'Couldn’t reorder gateways.' => 'Gateways konnten nicht neu sortiert werden.', - 'Couldn’t reorder plans.' => 'Pläne konnten nicht neu geordnet werden.', - 'Couldn’t reorder rules.' => 'Regeln konnten nicht umsortiert werden.', - 'Couldn’t reorder sale.' => 'Aktion konnte nicht umsortiert werden.', - 'Couldn’t reorder sales.' => 'Aktionen konnten nicht umsortiert werden.', - 'Couldn’t reorder statuses.' => 'Status konnten nicht umsortiert werden.', - 'Couldn’t reorder stores.' => 'Shops konnten nicht umsortiert werden.', - 'Couldn’t save PDF.' => 'PDF konnte nicht gespeichert werden.', - 'Couldn’t save catalog pricing rule.' => 'Katalogpreisregel konnte nicht gespeichert werden.', - 'Couldn’t save currency.' => 'Währung konnte nicht gespeichert werden.', - 'Couldn’t save discount.' => 'Rabatt konnte nicht gespeichert werden.', - 'Couldn’t save email.' => 'E-Mail konnte nicht gespeichert werden.', - 'Couldn’t save gateway.' => 'Gateway konnte nicht gespeichert werden.', - 'Couldn’t save inventory location.' => 'Lagerbestandsort konnte nicht gespeichert werden.', - 'Couldn’t save line item status.' => 'Einzelpostenstatus konnte nicht gespeichert werden.', - 'Couldn’t save order fields.' => 'Bestellfelder konnten nicht gespeichert werden.', - 'Couldn’t save order status.' => 'Bestellstatus konnte nicht gespeichert werden.', - 'Couldn’t save order.' => 'Die Bestellung konnte nicht gespeichert werden.', - 'Couldn’t save product type.' => 'Produkttyp konnte nicht gespeichert werden.', - 'Couldn’t save sale.' => 'Aktion konnte nicht gespeichert werden.', - 'Couldn’t save settings.' => 'Einstellungen konnten nicht gespeichert werden.', - 'Couldn’t save shipping category.' => 'Die Versandkategorie konnte nicht gespeichert werden.', - 'Couldn’t save shipping method.' => 'Versandart konnte nicht gespeichert werden.', - 'Couldn’t save shipping rule.' => 'Versandregel konnte nicht gespeichert werden.', - 'Couldn’t save shipping zone.' => 'Versandzone konnte nicht gespeichert werden.', - 'Couldn’t save store.' => 'Shop konnte nicht gespeichert werden.', - 'Couldn’t save subscription fields.' => 'Abonnementfelder konnten nicht gespeichert werden.', - 'Couldn’t save subscription plan.' => 'Abonnementplan konnte nicht gespeichert werden.', - 'Couldn’t save subscription.' => 'Abonnement konnte nicht gespeichert werden.', - 'Couldn’t save tax category.' => 'Steuerklasse konnte nicht gespeichert werden.', - 'Couldn’t save tax rate.' => 'Steuersatz konnte nicht gespeichert werden.', - 'Couldn’t save tax zone.' => 'Steuerzone konnte nicht gespeichert werden.', - 'Couldn’t save transfer fields.' => 'Übertragungsfelder konnten nicht gespeichert werden.', - 'Couldn’t update catalog pricing rule statuses.' => 'Status der Katalogpreisregeln konnte nicht aktualisiert werden.', - 'Couldn’t update status.' => 'Status konnte nicht aktualisiert werden.', - 'Couldn’t updated sales status.' => 'Aktionsstatus konnte nicht aktualisiert werden.', - 'Country Code of Origin' => 'Land – Ursprünglicher Code', - 'Country List' => 'Länderliste', - 'Country not allowed.' => 'Land nicht erlaubt.', - 'Country' => 'Land', - 'Coupon Code' => 'Coupon-Code', - 'Coupon can not apply discount to this order due to address mismatch.' => 'Der Coupon kann aufgrund einer Adressabweichung keinen Rabatt auf diese Bestellung anwenden.', - 'Coupon can not apply discount to this order due to customer mismatch.' => 'Der Coupon kann aufgrund einer Unstimmigkeit seitens des Kunden keinen Rabatt auf diese Bestellung gewähren.', - 'Coupon can not apply discount to this order.' => 'Der Coupon kann keinen Rabatt auf diese Bestellung anwenden.', - 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Gutscheincode “{code}” wird bereits vom Rabatt “{name}” verwendet.', - 'Coupon codes cannot be blank.' => 'Coupon-Codes dürfen nicht leer sein.', - 'Coupon codes must be unique.' => 'Coupon-Codes müssen einzigartig sein.', - 'Coupon format is required and must contain at least one `#`.' => 'Das Coupon-Format ist erforderlich und muss mindestens ein `#` enthalten.', - 'Coupon not valid.' => 'Coupon ungültig.', - 'Coupon removed: {explanation}' => 'Coupon entfernt: {explanation}', - 'Coupons' => 'Coupons', - 'Craft Commerce - Administration' => 'Craft Commerce - Administration', - 'Craft Commerce - Inventory' => 'Craft Commerce - Lagerbestand', - 'Craft Commerce - Orders' => 'Craft Commerce - Bestellungen', - 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Produkttyp - {name}', - 'Craft Commerce - Subscriptions' => 'Craft Commerce - Abonnements', - 'Create a Discount' => 'Rabatt erstellen', - 'Create a Subscription Plan' => 'Einen Abonnementplan erstellen', - 'Create a new PDF' => 'Neue PDF erstellen', - 'Create a new catalog pricing rule' => 'Neue Katalogpreisregel erstellen', - 'Create a new currency' => 'Neue Währung erstellen', - 'Create a new email' => 'Eine neue E-Mail erstellen', - 'Create a new gateway' => 'Ein neues Gateway erstellen', - 'Create a new line item status' => 'Neuen Einzelpostenstatus erstellen', - 'Create a new order status' => 'Neuen Bestellstatus anlegen', - 'Create a new product type' => 'Neuen Produkttyp erstellen', - 'Create a new sale' => 'Eine neue Aktion erstellen', - 'Create a new shipping category' => 'Eine neue Versandkategorie erstellen', - 'Create a new shipping method' => 'Eine neue Versandart anlegen', - 'Create a new shipping rule' => 'Eine neue Versandregel erstellen', - 'Create a new tax category' => 'Eine neue Steuerklasse anlegen.', - 'Create a new tax rate' => 'Neuen Steuersatz anlegen', - 'Create a product type' => 'Einen Produkttyp erstellen', - 'Create a shipping zone' => 'Versandzone erstellen', - 'Create a tax zone' => 'Eine Steuerzone anlegen', - 'Create catalog pricing rules' => 'Katalogpreisregeln erstellen', - 'Create customer: “{email}”' => 'Erstelle Kunde: „{email}“', - 'Create discounts' => 'Rabatte erstellen', - 'Create discount…' => 'Rabatt erstellen…', - 'Create rules that allow this discount to match the order.' => 'Erstellen Sie Regeln, die diesen Rabatt für die Bestellung zugehörig machen.', - 'Create rules that allow this discount to match the order’s billing address.' => 'Erstellen Sie Regeln, die diesen Rabatt für die Bestelladresse zugehörig machen.', - 'Create rules that allow this discount to match the order’s customer.' => 'Erstellen Sie Regeln, die diesen Rabatt für den Bestellkunden zugehörig machen.', - 'Create rules that allow this discount to match the order’s shipping address.' => 'Erstellen Sie Regeln, die diesen Rabatt für die Versandadresse zugehörig machen.', - 'Create rules that allow this gateway to match the billing address.' => 'Erstellen Sie Regeln, die es diesem Gateway ermöglichen, die Rechnungsadresse zuzuordnen.', - 'Create rules that allow this gateway to match the order.' => 'Erstellen Sie Regeln, die es diesem Gateway ermöglichen, die Bestellung zuzuordnen.', - 'Create rules that allow this gateway to match the shipping address.' => 'Erstellen Sie Regeln, die es diesem Gateway ermöglichen, die Versandadresse zuzuordnen.', - 'Create sales' => 'Aktionen erstellen', - 'Create sale…' => 'Aktion erstellen…', - 'Created' => 'Erstellt', - 'Credit Card Payment Type' => 'Zahlungsmethode Kreditkarte', - 'Currency Code' => 'Währungs-Code', - 'Currency saved.' => 'Währung gespeichert.', - 'Currency' => 'Währung', - 'Current' => 'laufende Rechnung', - 'Custom 1' => 'Benutzerdefiniert 1', - 'Custom 2' => 'Benutzerdefiniert 2', - 'Custom 3' => 'Benutzerdefiniert 3', - 'Custom 4' => 'Benutzerdefiniert 4', - 'Custom' => 'Benutzerdefiniert', - 'Customer Enabled?' => 'Kunde freigegeben?', - 'Customer ID is required.' => 'Kunden-ID erforderlich.', - 'Customer Note' => 'Kundenanmerkung', - 'Customer Notices' => 'Kundenanmerkungen', - 'Customer data' => 'Kundendaten', - 'Customer' => 'Kunde', - 'Damaged' => 'Beschädigt', - 'Data shown might be outdated.' => 'Die angezeigten Daten können veraltet sein.', - 'Date Authorized' => 'Autorisierungsdatum', - 'Date Created' => 'Erstellungsdatum', - 'Date First Paid' => 'Erstes Zahlungsdatum', - 'Date Ordered' => 'Bestelldatum', - 'Date Paid' => 'Bezahldatum', - 'Date Updated' => 'Aktualisierungsdatum', - 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Datum, an dem die Katalogpreisregel aktiviert wird. Keine Angabe für unbegrenztes Startdatum', - 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Datum, an dem der Rabatt aktiv wird. Keine Eingabe, wenn das Startdatum nicht begrenzt ist', - 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Datum, an dem die Aktion aktiviert wird. Keine Angabe für unbegrenztes Startdatum', - 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Datum, an dem die Katalogpreisregel beendet wird. Keine Eingabe, wenn die Aktion unbefristet läuft', - 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Datum, zu dem die Rabattaktion beendet sein wird. Keine Eingabe bei unbefristeter Aktion.', - 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Datum, an dem die Aktion beendet wird. Keine Eingabe, wenn die Aktion unbefristet läuft', - 'Date' => 'Datum', - 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Standard - Erlaubt es dem Preis, negativ zu sein, wenn Rabatte größer als der Wert der Bestellung sind.', - 'Default Category' => 'Standardkategorie', - 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Die Standardansicht des Commerce Control Panels. Wenn der Benutzer keine Zugangsrechte besitzt, wird auf eine Seite zurückgegriffen, auf die er Zugriff hat.', - 'Default Order PDF' => 'Standard Bestell-PDF', - 'Default Per Item Rate' => 'Standard Pro Artikel-Satz', - 'Default Percentage Rate' => 'Standardprozentsatz', - 'Default Status?' => 'Standardstatus?', - 'Default View' => 'Standardansicht', - 'Default Weight Rate' => 'Standard Gewichtspreis', - 'Default Zone' => 'Standardzone', - 'Default status?' => 'Standardstatus?', - 'Default to this tax zone when no billing address is set' => 'Standardmäßig auf diese Steuerzone festlegen, wenn keine Rechnungsadresse angegeben wurde', - 'Default to this tax zone when no shipping address is set' => 'Diese Steuerzone als Standard wählen, wenn keine Versandadresse angegeben wurde', - 'Default variant updated.' => 'Standardvariante aktualisiert.', - 'Default' => 'Standard', - 'Default?' => 'Standard?', - 'Delete catalog pricing rules' => 'Katalogpreisregeln löschen', - 'Delete discounts' => 'Rabatte löschen', - 'Delete orders' => 'Bestellungen löschen', - 'Delete sales' => 'Aktionen löschen', - 'Delete' => 'Löschen', - 'Deleting the {location} location.' => 'Löschen des Ortes {location}.', - 'Describe this rule.' => 'Diese Regel beschreiben.', - 'Describe this shipping zone.' => 'Beschreiben Sie diese Versandzone.', - 'Describe this tax zone.' => 'Beschreiben Sie diese Steuerzone.', - 'Description' => 'Beschreibung', - 'Destination Inventory Location' => 'Ziellagerbestandsort', - 'Destination' => 'Zielort', - 'Details' => 'Details', - 'Dimension Unit' => 'Maßeinheit', - 'Dimensions' => 'Abmessungen', - 'Disabled' => 'Deaktiviert', - 'Disallow' => 'Verbieten', - 'Discount all line items' => 'Alle Einzelposten rabattieren', - 'Discount description.' => 'Beschreibung des Rabatts.', - 'Discount is not allowed for the order' => 'Der Rabatt ist für die Bestellung nicht zugelassen', - 'Discount is out of date.' => 'Rabatt ist veraltet.', - 'Discount saved.' => 'Rabatt gespeichert.', - 'Discount the matching items only' => 'Nur die entsprechenden Artikel rabattieren', - 'Discount use has reached its limit.' => 'Die Nutzungsgrenze des Rabattes wurde erreicht.', - 'Discount' => 'Rabatt', - 'Discounted Item Subtotal' => 'Zwischensumme rabattierter Artikel', - 'Discounted Items' => 'Ermäßigte Artikel', - 'Discounts deleted.' => 'Rabatte entfernt.', - 'Discounts reordered.' => 'Rabatte umsortiert.', - 'Discounts updated.' => 'Rabatte aktualisiert.', - 'Discounts' => 'Rabatte', - 'Disqualify with valid business tax ID?' => 'Mit gültiger Gewerbesteueridentifikationsnummer disqualifizieren?', - 'Do not apply subsequent matching sales beyond applying this sale.' => 'Nach dem Anwenden dieses Verkaufs keine weiteren passenden Verkäufe mehr suchen.', - 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Wenden Sie diesen Steuersatz nicht an, wenn die Bestelladresse eine der ausgewählten gültigen Gewerbesteueridentifikationsnummern hat.', - 'Do not attach a PDF to this email' => 'Keine PDF an diese E-Mail anhängen', - 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Recalculate bei dieser Bestellung (Nummer: {orderNumber}) nicht aufrufen, wenn Fehler vorhanden sind.', - 'Donation can not be zero.' => 'Spendenbetrag kann nicht null sein.', - 'Donation needs to be an amount.' => 'Spenden müssen ein Betrag sein.', - 'Donation settings saved.' => 'Spendeneinstellungen gespeichert.', - 'Donation' => 'Spende', - 'Donations' => 'Spenden', - 'Done' => 'Fertig', - 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Keine nachfolgenden Rabatte bei einer Bestellung anwenden, wenn dieser Rabatt angewendet wird', - 'Download PDF' => 'PDF herunterladen', - 'Download PDF…' => 'PDF herunterladen …', - 'Download Type' => 'Art des Downloads', - 'Download' => 'Herunterladen', - 'Draft' => 'Entwurf', - 'Dummy gateway payment failed.' => 'Platzhalter Gateway Zahlung fehlgeschlagen.', - 'Duplicate options exist' => 'Es existieren Duplikate in den Optionen', - 'Duration' => 'Dauer', - 'EU VAT ID' => 'EU-STEUERNUMMER', - 'Edit address' => 'Adresse bearbeiten', - 'Edit adjustments' => 'Anpassungen bearbeiten', - 'Edit catalog pricing rules' => 'Katalogpreisregeln bearbeiten', - 'Edit discounts' => 'Rabatte bearbeiten', - 'Edit options' => 'Optionen bearbeiten', - 'Edit orders' => 'Bestellungen bearbeiten', - 'Edit sales' => 'Aktionen bearbeiten', - 'Edit' => 'Bearbeiten', - 'Effect' => 'Ausführen', - 'Either (Default) - The relationship field is on the purchasable or the category' => 'Beide (Standard) - Das Beziehungsfeld ist bei der Kaufoption oder Kategorie', - 'Either way' => 'Egal', - 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'E-Mail PDF Generierungsfehler für E-Mail "{email}". Bestellung: "{order}". PDF-Vorlagenfehler: "{message}" {file}:{line}', - 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'Die E-Mail PDF-Vorlage unter "{templatePath}" ist nicht für die E-Mail "{email}" vorhanden. Bestellung: "{order}".', - 'Email Subject' => 'E-Mail-Betreff', - 'Email error. No email address found for order. Order: “{order}”' => 'E-Mail-Fehler. Für die folgende Bestellung wurde keine E-Mail-Adresse gefunden. Bestellung: "{order}"', - 'Email is not enabled.' => 'Die E-Mail ist nicht aktiviert.', - 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Die Klartext-E-Mail-Vorlage unter "{templatePath}", die zu "{templateParsedPath}" für die E-Mail "{email}" führte ist nicht vorhanden. Bestellung: "{order}".', - 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parsingfehler der Klartext E-Mail Vorlage für E-Mail "{email}" Bestellung "{order}". Vorlagenfehler: "{message}" {file}:{line}', - 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Pfad-Parsingfehler der E-Mail Klartext-Vorlage für E-Mail "{email}" in "Vorlagenpfad". Bestellung "{order}". Vorlagenfehler: "{message}" {file}:{line}', - 'Email required to make payments on a completed order.' => 'Sie benötigen eine E-Mail-Adresse, um Zahlungen für eine abgeschlossene Bestellung vorzunehmen.', - 'Email saved.' => 'E-Mail gespeichert.', - 'Email sent' => 'E-Mail gesendet', - 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Die E-Mail-Vorlage unter "{templatePath}", die zu "{templateParsedPath}" für die E-Mail „{email}“ führte, ist nicht vorhanden. Bestellung: "{order}".', - 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parsingfehler in der E-Mail Vorlage für benutzerdefinierte E-Mail "{email}" in "An:". Bestellung "{order}". Vorlagenfehler: "{message}" {file}:{line}', - 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-Mail Vorlagen-Parserfehler für E-Mail "{email}" in "BCC:". Bestellung: "{order}". Vorlagenfehler: "{message}" {file}:{line}', - 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-Mail-Vorlagen Parserfehler für E-Mail "{email}" in "CC:". Bestellung: "{order}". Vorlagenfehler: "{message}" {file}:{line}', - 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-Mail-Vorlagen Parserfehler für E-Mail "{email}" in "Antwort an:". Bestellung: "{order}". Vorlagenfehler: "{message}" {file}:{line}', - 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-Mail-Vorlagen Parserfehler für E-Mail "{email}" in "Betreff:". Bestellung: "{order}". Vorlagenfehler: "{message}" {file}:{line}', - 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-Mail-Vorlage Parsingfehler für E-Mail "{email}" Bestellung "{order}". Vorlagenfehler: "{message}" {file}:{line}', - 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Pfad-Parsingfehler bei der E-Mail Vorlage für E-Mail "{email}" in "Vorlagenpfad". Bestellung "{order}". Vorlagenfehler: "{message}" {file}:{line}', - 'Email unavailable.' => 'E-Mail nicht verfügbar.', - 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'E-Mail "{email}" konnte nicht für die Bestellung "{order}" versendet werden. Fehler: {error} {file}:{line}', - 'Email “{email}” for order {order} was cancelled.' => 'E-Mail "{email}", für die Bestellung "{order}" wurde abgebrochen.', - 'Email' => 'E-Mail', - 'Emails' => 'E-Mails', - 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Aktivieren Sie diese Option, wenn dieser Steuersatz in den steuerpflichtigen Einzelpreis einkalkuliert werden soll, anstatt der Bestellung zusätzliche Kosten hinzuzufügen.', - 'Enable structure for products of this type' => 'Struktur für Produkte dieser Art aktivieren', - 'Enable this discount' => 'Diesen Rabatt aktivieren', - 'Enable this rule' => 'Diese Regel aktivieren', - 'Enable this sale' => 'Diese Aktion aktivieren', - 'Enable this shipping method on the front end' => 'Diese Versandart auf dem Frontend verfügbar machen', - 'Enable this shipping rule' => 'Die Versandart aktivieren', - 'Enable this tax rate' => 'Diesen Steuersatz aktivieren', - 'Enabled for customers to select during checkout?' => 'Ist den Kunden die Auswahl an der Kasse gestattet?', - 'Enabled for customers to select?' => 'Wurde die Auswahl für Kunden aktiviert?', - 'Enabled' => 'Aktiviert', - 'Enabled?' => 'Aktiviert?', - 'End Date' => 'Enddatum', - 'Enter SKU' => 'Bestandseinheit (SKU) eingeben', - 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Geben Sie einen verständlichen Namen für diesen Steuersatz ein, der im Control Panel verwendet werden soll.', - 'Enter a percentage like {ex1} or {ex2}.' => 'Geben Sie einen Prozentsatz ein, z. B. {ex1} oder {ex2}.', - 'Enter coupon code' => 'Gutscheincode eingeben', - 'Enter reference' => 'Referenz eingeben', - 'Error refunding transaction: {transactionHash}' => 'Fehler bei der Zurückerstattung der Transaktion: {transactionHash}', - 'Every new store must be assigned to at least one site.' => 'Jeder neue Shop muss mindestens einer Website zugeordnet werden.', - 'Everywhere' => 'Überall', - 'Example' => 'Beispiel', - 'Exclude this discount for products that are already on promotion' => 'Diesen Rabatt nicht auf Produkte anwenden, die bereits Teil einer Aktion sind', - 'Expired Link' => 'Abgelaufener Link', - 'Expired' => 'Abgelaufen', - 'Expiry Date' => 'Ablaufdatum', - 'Expiry date' => 'Ablaufdatum', - 'Expiry' => 'Verfall', - 'Failed to receive transfer: {error}' => 'Übertragung nicht empfangen: {error}', - 'Failed to send email. Please try again.' => 'E-Mail kann nicht gesendet werden. Bitte versuchen Sie es erneut.', - 'Failed to start' => 'Konnte nicht gestartet werden', - 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Fehler beim Aktualisieren {num, plural, =1{des Bestellstatus} other{der Bestellstatus}}.', - 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Fehler beim Aktualisieren des Bestellstatus der {num, plural, =1{Bestellung} other{Bestellungen}}.', - 'Feet (ft)' => 'Fuß (ft)', - 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Filterbedingungen zur Beschreibung, auf welche Bestellungen diese Regel angewandt werden kann. 0 eingeben, um eine Bedingung auszulassen.', - 'First Name' => 'Vorname', - 'Flat Amount Off Order' => 'Pauschalbetrag abgezogen von der Bestellung', - 'Flat Order Discount Amount Off' => 'Pauschaler Bestellungsrabattbetrag', - 'Free Order Payment Strategy' => 'Zahlungsstrategie für kostenlose Bestellungen', - 'Free Shipping' => 'Kostenloser Versand', - 'Free orders are processed by the payment gateway' => 'Kostenlose Bestellungen werden vom Zahlungsgateway verarbeitet', - 'Free orders complete immediately' => 'Kostenlose Bestellungen schließen sofort ab', - 'Free shipping can only be for whole order or matching items, not both.' => 'Kostenloser Versand kann nur auf die gesamte Bestellung oder auf passende Artikel angewendet werden, nicht auf beides gleichzeitig.', - 'From Name' => 'Absender', - 'Fulfill' => 'Erfüllen', - 'Fulfilled' => 'Erfüllt', - 'Fulfillment' => 'Erfüllung', - 'Full Name' => 'Vollständiger Name', - 'Gateway Code' => 'Gateway Code', - 'Gateway Message' => 'Gatewaynachricht', - 'Gateway Reference' => 'Gateway Referenz', - 'Gateway Response' => 'Antwort des Gateways', - 'Gateway doesn’t support authorize' => 'Gateway unterstützt Autorisierung nicht', - 'Gateway doesn’t support partial refunds.' => 'Gateway unterstützt keine teilweise Rückerstattungen.', - 'Gateway doesn’t support purchase' => 'Gateway unterstützt Kauf nicht', - 'Gateway doesn’t support refunds.' => 'Gateway unterstützt keine Rückerstattungen.', - 'Gateway saved.' => 'Gateway wurde gespeichert.', - 'Gateway' => 'Zahlungseingang', - 'Gateways reordered.' => 'Gateways umsortiert.', - 'Gateways' => 'Gateways', - 'General Settings' => 'Allgemeine Einstellungen', - 'General' => 'Allgemein', - 'Generate' => 'Erzeugen', - 'Generated Coupon Format' => 'Erzeugtes Coupon-Format', - 'Grams (g)' => 'Gramm (g)', - 'Groups for which this sale will be applicable to.' => 'Gruppen, auf die dieses Angebot angewendet werden kann.', - 'HTML Email Template Path' => 'Template-Pfad für HTML-E-Mails', - 'Handle' => 'Kurzname', - 'Harmonized System Code' => 'Code des Harmonisierten Systems', - 'Has Admin Notices' => 'Enthält Administratorhinweise', - 'Has Emails?' => 'Hat E-Mails?', - 'Has Free Shipping' => 'Mit kostenlosem Versand', - 'Has Orders' => 'Hat Bestellungen', - 'Has Purchasable' => 'Hat Kaufoptionen', - 'Has Variants?' => 'Hat Varianten?', - 'Height ({unit})' => 'Höhe ({unit})', - 'Height' => 'Höhe', - 'Hide snapshot' => 'Schattenkopie ausblenden', - 'History' => 'Verlauf', - 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Wie lange (in Sekunden) ein PDF-Download-Link gültig bleiben soll, bevor er abläuft. Der Standardwert ist 86400 (24 Stunden).', - 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Wie häufig eine E-Mail-Adresse diesen Rabatt nutzen darf. Dies gilt für alle vorherigen Bestellungen, egal, ob sie von einem Gast oder Benutzer kommen. Legen Sie hier zero fest für eine unbegrenzte Nutzung durch Gäste oder Benutzer.', - 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Wie oft ein Benutzer diesen Rabatt in Anspruch nehmen kann. Wenn dieser Wert auf etwas anderes als Null gesetzt wird, ist der Rabatt nur für angemeldete Benutzer verfügbar.', - 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Wie oft dieser Rabatt insgesamt von Gästen oder angemeldeten Benutzern genutzt werden kann. Wählen Sie Null für die unbegrenzte Nutzung.', - 'How products should be labeled within the control panel.' => 'Wie Produkte im Control Panel bezeichnet werden sollen.', - 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'Wie Kaufoptionen und Kategorien zusammenhängen und was die übereinstimmenden Artikel bestimmt. Siehe [Beziehungsterminologie]({link}).', - 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'So wird dieses Produkt innerhalb eines Posten in einer Bestellung beschrieben. Sie können Tags hinzufügen, die Eigenschaften ausgeben, wie z. B. {ex1} oder {ex2}', - 'How this shipping method will be referred to in templates and forms.' => 'Wie Sie in Ihren Templates und Formularen auf diese Versandart verweisen.', - 'How variants should be labeled within the control panel.' => 'Wie Varianten im Control Panel bezeichnet werden sollen.', - 'How you’ll refer to this PDF in the templates.' => 'Wie Sie in den Vorlagen auf diese PDF Bezug nehmen.', - 'How you’ll refer to this product type in the templates.' => 'Wie Sie sich in den Templates auf diesen Produkttyp beziehen.', - 'How you’ll refer to this shipping category in the templates.' => 'Unter diesem Namen wird diese Versandkategorie in den Vorlagen erscheinen.', - 'How you’ll refer to this status in the templates.' => 'Wie Sie in Ihren Template auf diesen Status verweisen.', - 'How you’ll refer to this subscription plan in the templates.' => 'Die Art und Weise, wie Sie auf diesen Abonnementplan in den Vorlagen verweisen.', - 'How you’ll refer to this tax category in the templates.' => 'Wie Sie sich in den Templates auf diese Steuerklasse beziehen.', - 'ID' => 'ID', - 'IP Address' => 'IP-Adresse', - 'If disabled, this PDF will not be available or sent with emails.' => 'Wenn die Option deaktiviert ist, wird diese PDF nicht verfügbar sein oder mit E-Mails versendet werden.', - 'If disabled, this email will not send.' => 'Wenn die Option deaktiviert ist, wird die E-Mail nicht gesendet.', - 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Wenn diese Option aktiviert ist und dieser Steuersatz nicht mit der Bestellung übereinstimmt, wird der Betrag vom Einzelpreis im Warenkorb entfernt.', - 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Wenn "Nur Autorisieren" ausgewählt ist, müssen Sie Zahlungen manuell erfassen, bevor Gelder auf Ihr Konto überwiesen werden. Der Gateway muss die ausgewählte Option unterstützen.', - 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Wenn Sie den Prozentsatz als „Nachlass vom reduzierten Artikelpreis“ wählen, wird dieser den „Betrag pro Artikel“ als auch jegliche weitere Rabatte enthalten, die vor diesem angewendet wurden.', - 'Ignore Promotions?' => 'Aktionen ignorieren?', - 'Ignore previous matching sales if this sale matches.' => 'Ignorieren Sie vorherige übereinstimmende Verkäufe, wenn dieser Verkauf übereinstimmt.', - 'Ignore promotional prices when this discount is applied to matching line items' => 'Aktionspreise ignorieren, wenn der Rabatt auf übereinstimmende Einzelposten angewendet wird', - 'Inactive Carts' => 'Inaktive Einkaufskörbe', - 'Inches (in)' => 'Zoll (in)', - 'Include built-in line item tax.' => 'Beziehen Sie die integrierte Einzelpostensteuer mit ein.', - 'Include in price?' => 'In Preis mit einbeziehen?', - 'Include line item discounts.' => 'Beziehen Sie die Einzelpostenrabatte mit ein.', - 'Include line item shipping costs.' => 'Beziehen Sie die Einzelpostenversandkosten mit ein.', - 'Include separate line item tax.' => 'Beziehen Sie eine separate Einzelpostensteuer mit ein.', - 'Included in price?' => 'In Preis mit einbeziehen?', - 'Included' => 'Enthalten', - 'Incoming transfer from Transfer ID: ' => 'Eingehende Übertragung von Übertragung mit ID: ', - 'Incoming' => 'Eingehend', - 'Info' => 'Info', - 'Information linked?' => 'Wurden die Informationen verlinkt?', - 'Information' => 'Information', - 'Invalid JSON' => 'Ungültiges JSON', - 'Invalid Order ID' => 'Ungültige Bestellungs-ID', - 'Invalid VAT ID.' => 'Ungültige USt-IdNr.', - 'Invalid condition syntax' => 'Üngültige Bedingungssyntax', - 'Invalid email.' => 'Ungültige E-Mail-Adresse.', - 'Invalid formula syntax' => 'Ungültige Formelsyntax', - 'Invalid gateway: {value}' => 'Ungültiges Gateway: {value}', - 'Invalid inventory movements.' => 'Ungültige Lagerbestandsbewegungen.', - 'Invalid order condition syntax.' => 'Ungültige Bestellungsbedingungssyntax.', - 'Invalid payment or order. Please review.' => 'Ungültige Zahlung oder Bestellung. Bitte überprüfen.', - 'Invalid payment source ID: {value}' => 'Ungültige Zahlungsquellen-ID: {value}', - 'Invalid store.' => 'Der Shop ist ungültig.', - 'Invalid user.' => 'Ungültiger Benutzer.', - 'Inventory Item' => 'Lagerbestandsposten aktualisiert', - 'Inventory Location' => 'Lagerbestandsort', - 'Inventory Locations' => 'Lagerbestandsorte', - 'Inventory Tracked' => 'Lagerbestand verfolgt', - 'Inventory Transfers' => 'Lagerbestandsübertragungen', - 'Inventory could not be set.' => 'Lagerbestand konnte nicht gesetzt werden.', - 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'Der Lagerbestandsort hat einen zugewiesenen Bestand, die Bestellung(en) müssen erst erfüllt werden.', - 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Der Lagerbestandsort hat einen zugewiesenen Bestand, die Übertragung(en) müssen erst erfüllt werden.', - 'Inventory location is already deactivated.' => 'Der Lagerbestandsort ist bereits deaktiviert.', - 'Inventory location saved.' => 'Lagerbestandsort wurde gespeichert.', - 'Inventory locations not saved.' => 'Lagerbestandsort wurde nicht gespeichert.', - 'Inventory movement could not be saved.' => 'Die Lagerbestandsverschiebung konnte nicht gespeichert werden.', - 'Inventory movement saved.' => 'Lagerbestandsverschiebung gespeichert.', - 'Inventory updated.' => 'Lagerbestand aktualisiert.', - 'Inventory was not updated.' => 'Lagerbestand nicht aktualisiert.', - 'Inventory' => 'Lagerbestand', - 'Invoice amount' => 'Rechnungsbetrag', - 'Invoice date' => 'Rechnungsdatum', - 'Is Promotable' => 'Ist werbeaktionsfähig', - 'Is Promotional Price?' => 'Ist Aktionspreis?', - 'Is Shippable' => 'Ist lieferbar', - 'Is Taxable' => 'Ist steuerbar', - 'Item Rates' => 'Artikel-Preise', - 'Item Subtotal' => 'Zwischensumme Artikel', - 'Item Total' => 'Artikel Gesamtmenge', - 'Item' => 'Artikel', - 'Items' => 'Artikel', - 'Kilograms (kg)' => 'Kilogramm (kg)', - 'Label' => 'Bezeichnung', - 'Landscape' => 'Querformat', - 'Language' => 'Sprache', - 'Last Name' => 'Nachname', - 'Last Updated' => 'Zuletzt aktualisiert', - 'Leave a category rate override blank to use the rate from above.' => 'Lassen Sie den Kategoriepreis-Override leer, um den Preis von oben zu verwenden.', - 'Leave blank for unlimited uses.' => 'Leer lassen für unbegrenzte Nutzung.', - 'Leave blank if products don’t have URLs' => 'Bei Produkten ohne URL leer lassen', - 'Leave gateway subscription as-is' => 'Gateway-Abonnement unverändert lassen', - 'Length ({unit})' => 'Länge ({unit})', - 'Length' => 'Länge', - 'Let each product choose which sites it should be saved to' => 'Lassen Sie jedes Produkt wählen, zu welchen Websites es gespeichert werden soll', - 'Limit which orders this discount applies to based on its line items.' => 'Schränken Sie die Bestellungen, für die dieser Rabatt gilt, anhand ihrer Einzelposten ein.', - 'Limit which purchasables this sale applies to.' => 'Schränken Sie ein, für welche Kaufoptionen diese Aktion gilt.', - 'Limit' => 'Begrenzung', - 'Line Item Statuses' => 'Einzelpostenstatus', - 'Line Item' => 'Einzelposten', - 'Line Items' => 'Einzelposten', - 'Line item price (minus discounts)' => 'Einzelpostenpreis (abzüglich Rabatte)', - 'Line item shipping cost' => 'Versandkosten für Einzelposten', - 'Line item statuses reordered.' => 'Einzelpostenstatus umsortiert.', - 'Link Duration' => 'Link-Dauer', - 'Link Sent' => 'Link gesendet', - 'Link to a product' => 'Link zu einem Produkt', - 'Link to a variant' => 'Mit einer Variante verlinken', - 'Link' => 'Link', - 'Live' => 'Aktiv', - 'Location' => 'Standort', - 'Locations that should be available for previewing products in this product type.' => 'Standorte, die für die Vorschau von Produkten dieses Produkttyps verfügbar sein sollten.', - 'MM' => 'MM', - 'Make a payment' => 'Eine Zahlung tätigen', - 'Make this the primary store' => 'Als primären Shop verwenden', - 'Manage Inventory' => 'Lagerbestand verwalten', - 'Manage donation settings' => 'Spendeneinstellungen verwalten', - 'Manage general store settings' => 'Allgemeine Geschäftseinstellungen verwalten', - 'Manage inventory locations' => 'Lagerbestandsorte verwalten', - 'Manage inventory stock levels' => 'Lagerbestände verwalten', - 'Manage inventory transfers' => 'Lagerbestandsübertragungen verwalten', - 'Manage orders' => 'Bestellungen verwalten', - 'Manage payment currencies' => 'Zahlungswährungen verwalten', - 'Manage promotions' => 'Werbeaktionen verwalten', - 'Manage shipping' => 'Versand verwalten', - 'Manage store settings' => 'Geschäftseinstellungen verwalten', - 'Manage subscription plans' => 'Abonnementpläne verwalten', - 'Manage subscription' => 'Abonnement verwalten', - 'Manage subscriptions' => 'Abonnements verwalten', - 'Manage taxes' => 'Steuern verwalten', - 'Manage' => 'Verwalten', - 'Mark as Pending' => 'Als ausstehend markieren', - 'Mark as completed' => 'Als abgeschlossen markieren', - 'Match Billing Address' => 'Abhängig von Rechnungsadresse', - 'Match Customer' => 'Abhängig von Kunde', - 'Match Order' => 'Abhängig von Bestellung', - 'Match Orders' => 'Bestellungen abgleichen', - 'Match Product' => 'Produkt abgleichen', - 'Match Purchasable' => 'Abhängig von Kaufoption', - 'Match Shipping Address' => 'Abhängig von Versandadresse', - 'Match Variant' => 'Variante abgleichen', - 'Matching Items' => 'Übereinstimmende Artikel', - 'Max Qty' => 'Max. Menge', - 'Max Uses' => 'Max. Verwendungen', - 'Max Variants' => 'Max. Varianten', - 'Max quantity must greater than min.' => 'Die maximale Menge muss größer sein als die minimale.', - 'Maximum Purchase Quantity' => 'Maximale Bestellmenge', - 'Maximum Total Shipping Cost' => 'Maximale Gesamt-Versandkosten', - 'Maximum allowed quantity' => 'Erlaubte Höchstmenge', - 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Maximale Anzahl von betreffenden Artikeln, die bestellt werden können, damit dieser Rabatt wirksam ist. Ein Wert von Null sorgt für ein Überspringen dieser Bedingung.', - 'Maximum order quantity for this item is {num}.' => 'Die maximale Bestellmenge für diesen Artikel beträgt {num}.', - 'Message' => 'Nachricht', - 'Meters (m)' => 'Meter (m)', - 'Millimeters (mm)' => 'Millimeter (mm)', - 'Min Qty' => 'Min. Menge', - 'Min quantity must be less than max.' => 'Die minimale Menge muss größer sein als die maximale.', - 'Minimum Purchase Quantity' => 'Mindestabnahmemenge', - 'Minimum Total Price Strategy' => 'Strategie für den minimalen Gesamtpreis', - 'Minimum Total Shipping Cost' => 'Minimale Gesamt-Versandkosten', - 'Minimum allowed quantity' => 'Erlaubte Mindestmenge', - 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Minimale Anzahl von betreffenden Artikeln, die bestellt werden müssen, damit dieser Rabatt wirksam ist.', - 'Minimum order quantity for this item is {num}.' => 'Die Mindestbestellmenge für diesen Artikel beträgt {num}.', - 'Missing Gateway' => 'Fehlendes Gateway', - 'Missing a default inventory location.' => 'Es fehlt ein Standard-Lagerbestandsort.', - 'Move Inventory' => 'Lagerbestand verschieben', - 'Move To' => 'Verschieben nach', - 'Move {qty} from {fromType} to {toType}' => '{qty} von {fromType} nach {toType} verschieben', - 'Move' => 'Verschieben', - 'Movement from deactivated inventory location' => 'Verschieben von deaktiviertem Lagerbestandsort', - 'Movement' => 'Verschieben', - 'Must have at least one variant.' => 'Muss mindestens eine Variante haben.', - 'Name Field' => 'Namensfeld', - 'Name' => 'Name', - 'New Customer' => 'Neukunde', - 'New Customers' => 'Neukunden', - 'New Order' => 'Neue Bestellung', - 'New PDF' => 'Neue PDF', - 'New address' => 'Neue Adresse', - 'New catalog pricing rule' => 'Neue Katalogpreisregel', - 'New currency' => 'Neue Währung', - 'New discount' => 'Neuer Rabatt', - 'New email' => 'Neue E-Mail', - 'New gateway' => 'Neuer Gateway', - 'New line item status' => 'Neuer Einzelpostenstatus', - 'New line items get this status by default when the order is completed' => 'Neue Einzelposten erhalten diesen Status automatisch wenn die Bestellung abgeschlossen ist', - 'New location' => 'Neuer Standort', - 'New order status' => 'Neuer Bestellstatus', - 'New orders get this status by default' => 'Neue Bestellungen erhalten standardmäßig diesen Status', - 'New product type' => 'Neuer Produkttyp', - 'New product' => 'Neues Produkt', - 'New product, choose a type' => 'Neues Produkt, wählen Sie einen Typ', - 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Neue Produkte sind standardmäßig Teil der ersten für sie verfügbaren Steuerklasse. Sollte keine verfügbar sein, wird diese Kategorie verwendet.', - 'New sale' => 'Neue Aktion', - 'New shipping category' => 'Neue Versandkategorie', - 'New shipping method' => 'Neue Versandart', - 'New shipping rule' => 'Neue Versandregel', - 'New shipping zone' => 'Neue Versandzone', - 'New subscription plan' => 'Neuer Abonnementplan', - 'New tax category' => 'Neue Steuerklasse', - 'New tax rate' => 'Neuer Steuersatz', - 'New tax zone' => 'Neue Steuerzone', - 'New transfer' => 'Neue Übertragung', - 'New {productType} product' => 'Neues Produkt vom Typ {productType}', - 'New' => 'Neue', - 'Next payment' => 'Nächste Zahlung', - 'No Address' => 'Keine Adresse', - 'No PDFs exist yet.' => 'Es gibt noch keine PDFs.', - 'No access given to any specific store management features.' => 'Kein Zugriff auf spezielle Funktionen zur Verwaltung des Shops gewährt.', - 'No additional payment currencies exist yet.' => 'Es existieren noch keine zusätzlichen Bezahlwährungen.', - 'No address' => 'Keine Adresse', - 'No billing address' => 'Keine Rechnungsadresse', - 'No catalog pricing rule exists with the ID “{id}”' => 'Es existiert keine Katalogpreisregel mit der ID "{id}"', - 'No catalog pricing rules exist yet.' => 'Es gibt noch keine Katalogpreisregeln.', - 'No currency exists with the ID “{id}”' => 'Es gibt keine Währung mit der ID "{id}"', - 'No customer email address exists on this cart.' => 'Mit diesem Warenkorb ist keine Kunden E-Mail-Adresse verknüpft.', - 'No description' => 'Keine Beschreibung', - 'No discount exists with the ID “{id}”' => 'Es gibt keinen Rabatt mit der ID "{id}"', - 'No discounts exist yet.' => 'Es existieren noch keine Rabatte.', - 'No donation amount supplied.' => 'Keine Spendenmenge geliefert.', - 'No emails exist yet.' => 'Es existieren noch keine E-Mails.', - 'No inventory changes made.' => 'Keine Lagerbestandsänderungen vorgenommen.', - 'No inventory found.' => 'Kein Lagerbestand gefunden.', - 'No inventory movements made.' => 'Keine Lagerbestandsverschiebungen vorgenommen.', - 'No inventory transactions for this location.' => 'Keine Lagerbestandstransaktionen für diesen Standort.', - 'No new customer selected.' => 'Es wurde kein neuer Kunde ausgewählt.', - 'No order history exists with the ID “{id}”' => 'Es gibt keinen Bestellverlauf mit der ID "{id}"', - 'No order status history items will exist until the cart becomes an order.' => 'Artikel werden erst dann im Bestellverlauf angezeigt, wenn für die Artikel im Warenkorb die Bestellung abgeschlossen wird.', - 'No payment source exists with the ID “{id}”' => 'Es ist keine Zahlungsquelle mit der ID "{id}" vorhanden', - 'No private Note.' => 'Keine private Anmerkung.', - 'No product available.' => 'Kein Produkt verfügbar.', - 'No product types exist yet.' => 'Es existieren noch keine Produkttypen.', - 'No purchasable available.' => 'Keine Kaufoption verfügbar.', - 'No sale exists with the ID “{id}”' => 'Es gibt keine Aktion mit der ID "{id}"', - 'No sales exist yet.' => 'Es existieren noch keine Aktionen.', - 'No shipping address' => 'Keine Lieferadresse', - 'No shipping category exists with the ID “{id}”' => 'Es gibt keine Versandkategorie mit der ID "{id}"', - 'No shipping method exists with the ID “{id}”' => 'Es gibt keine Versandart mit der ID "{id}"', - 'No shipping rule exists with the ID “{id}”' => 'Es gibt keine Versandregel mit der ID "{id}"', - 'No shipping rules exist yet.' => 'Es existieren noch keine Versandregeln.', - 'No shipping zone exists with the ID “{id}”' => 'Es existiert keine Versandzone mit der ID "{id}"', - 'No stats available.' => 'Keine Statistik verfügbar.', - 'No subscription plan exists with the ID “{id}”' => 'Es ist kein Abonnementplan mit der ID "{id}" vorhanden', - 'No subscription plans exist yet.' => 'Es ist noch kein Abonnementplan vorhanden.', - 'No tax category exists with the ID “{id}”' => 'Es gibt keine Steuerklasse mit der ID "{id}"', - 'No tax rate exists with the ID “{id}”' => 'Es gibt keinen Steuersatz mit der ID "{id}"', - 'No tax zone exists with the ID “{id}”' => 'Es gibt keine Steuerzone mit der ID "{id}"', - 'No transactions exist.' => 'Keine Transaktionen existieren.', - 'No user authenticated.' => 'Kein Benutzer authentifiziert.', - 'No' => 'Nein', - 'None on hand' => 'Nicht vorrätig', - 'None' => 'Keine', - 'Not a valid address type' => 'Kein gültiger Adresstyp', - 'Not a valid credit card number.' => 'Keine gültige Kreditkartennummer.', - 'Not all SKUs are unique.' => 'Nicht alle SKUs sind einmalig.', - 'Note' => 'Hinweis', - 'Notes' => 'Anmerkungen', - 'Number of Coupons' => 'Anzahl der Coupons', - 'Number' => 'Zahl', - 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Von den oben aktivierten Websites, unter welchen Websites sollen Produkte in diesem Produkttyp gespeichert werden?', - 'On Hand' => 'Auf Lager', - 'Only allow this gateway to be used for zero value orders?' => 'Darf dieser Gateway nur bei Bestellungen ohne Wert verwendet werden?', - 'Only match certain purchasables…' => 'Nur bei bestimmten Kaufoptionen …', - 'Only match purchasables related to…' => 'Nur Kaufoptionen in Zusammenhang mit …', - 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Es werden nur Bestellungen mit den folgenden Bestellstatus berücksichtigt. Lassen Sie das Feld leer, um alle Status zu berücksichtigen.', - 'Only save product to the site they were created in' => 'Produkte nur auf der Website speichern, auf der sie erstellt wurden', - 'Options' => 'Einstellungen', - 'Order Condition Formula' => 'Bestellungsbedingungsformel', - 'Order Description Format' => 'Format für Bestellbeschreibung', - 'Order Details' => 'Bestelldetails', - 'Order Fields' => 'Bestellfelder', - 'Order PDF Download Link' => 'Bestellung-PDF-Download-Link', - 'Order PDF Filename Format' => 'Dateinamensformat für Bestell-PDF', - 'Order Reference Number Format' => 'Format der Bestellreferenznummer', - 'Order Settings' => 'Bestelleinstellungen', - 'Order Site' => 'Bestellseite', - 'Order Status description.' => 'Beschreibung des Bestellstatus.', - 'Order Status' => 'Bestellstatus', - 'Order Statuses' => 'Bestellstatus', - 'Order can not be empty.' => 'Bestellung kann nicht leer sein.', - 'Order count' => 'Bestellungsanzahl', - 'Order customer data removed.' => 'Kundenbestelldaten gelöscht.', - 'Order deleted.' => 'Bestellung gelöscht.', - 'Order fields saved.' => 'Die Bestellungsfelder wurden gespeichert.', - 'Order not found.' => 'Bestellung nicht gefunden.', - 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Zahlungsdifferenz der Bestellung ist {outstandingBalanceAsCurrency}. Das ist der höchste Wert, der berechnet wird.', - 'Order recalculated.' => 'Bestellung neu berechnet.', - 'Order status saved.' => 'Bestellungsstatus gespeichert.', - 'Order statuses reordered.' => 'Bestellungsstatus umsortiert.', - 'Order total shipping cost' => 'Gesamtlieferkosten der Bestellung', - 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Versteuerbarer Gesamtbestellpreis (Einzelposten-Zwischensumme + Gesamtrabatte + Gesamtlieferkosten)', - 'Order' => 'Bestellung', - 'Orders (Legacy)' => 'Bestellungen (Veraltet)', - 'Orders deleted.' => 'Bestellungen gelöscht.', - 'Orders not restored.' => 'Bestellungen wurden nicht wiederhergestellt.', - 'Orders restored.' => 'Bestellungen wiederhergestellt.', - 'Orders' => 'Bestellungen', - 'Organization Name' => 'Name der Organisation', - 'Organization Tax ID' => 'Steuernummer der Organisation', - 'Origin and destination cannot be the same.' => 'Ursprung und Zielort können nicht identisch sein.', - 'Origin' => 'Ursprung', - 'Original Price' => 'Ursprünglicher Preis', - 'Original price' => 'Ursprünglicher Preis', - 'Original promotional price' => 'Ursprünglicher Aktionspreis', - 'Other Languages' => 'Andere Sprachen', - 'Other countries' => 'Andere Länder', - 'Outgoing transfer from Transfer ID: ' => 'Ausgehende Übertragung von Übertragung mit ID: ', - 'Overpaid' => 'Überbezahlt', - 'Overrides previous?' => 'Vorherige überschreiben?', - 'PDF Attachment' => 'PDF Anhang', - 'PDF Template Path' => 'PDF-Vorlagenpfad', - 'PDF saved.' => 'PDF gespeichert.', - 'PDF' => 'PDF', - 'PDFs & Emails' => 'PDFs & E-Mails', - 'PDFs' => 'PDFs', - 'Paid Amount' => 'Bezahlter Betrag', - 'Paid Status' => 'Bezahlstatus', - 'Paid' => 'Bezahlt', - 'Paper Orientation' => 'Papierausrichtung', - 'Paper Size' => 'Papiergröße', - 'Partial payment not allowed.' => 'Teilzahlung ist nicht erlaubt.', - 'Partial' => 'Teilweise', - 'Past year' => 'Vergangenes Jahr', - 'Past {num} days' => 'Vergangene {num} Tage', - 'Pay {amount} of {currency} on the order.' => 'Bezahlen Sie {amount} in {currency} für die Bestellung.', - 'Pay' => 'Bezahlen', - 'Payment Amount' => 'Zahlungsbetrag', - 'Payment Currencies' => 'Zahlungswährungen', - 'Payment Gateway' => 'Zahlungsgateway', - 'Payment Method' => 'Zahlungsmethode', - 'Payment error: {message}' => 'Zahlungsfehler: {message}', - 'Payment method issue' => 'Problem mit der Zahlungsmethode', - 'Payment source created.' => 'Zahlungsquelle wurde erstellt.', - 'Payment source deleted.' => 'Zahlungsquelle wurde gelöscht.', - 'Payments' => 'Zahlungen', - 'Pending' => 'Ausstehend', - 'Per Email Address Discount Limit' => 'Rabattlimit pro E-Mail-Adresse', - 'Per Item Amount Off' => 'Rabattbetrag pro Artikel', - 'Per Item Discount' => 'Rabatt pro Artikel', - 'Per Item Percentage Off' => 'Pro Artikel Prozentnachlass', - 'Per Item Rate' => 'Preis je Einheit', - 'Per User Discount Limit' => 'Rabattlimit pro Nutzer', - 'Percentage Rate' => 'Prozentsatz', - 'Phone (Alt)' => 'Telefon (alt)', - 'Phone' => 'Telefon', - 'Pick a plan' => 'Einen Plan auswählen', - 'Plain Text Email Template Path' => 'Klartext-E-Mail Vorlagenpfad', - 'Plan' => 'Plan', - 'Plans reordered.' => 'Pläne umsortiert.', - 'Portrait' => 'Hochformat', - 'Post Date' => 'Veröffentlichungsdatum', - 'Postal Code Formula' => 'Postleitzahl-Formel', - 'Pounds (lb)' => 'Pfund (lb)', - 'Preview' => 'Vorschau', - 'Previous Status' => 'Voriger Status', - 'Price' => 'Preis', - 'Prices' => 'Preise', - 'Pricing Rules' => 'Preisregeln', - 'Pricing jobs are currently running.' => 'Es laufen derzeit Aufgaben zur Preisgestaltung.', - 'Pricing' => 'Preisgestaltung', - 'Primary Billing Address' => 'Hauptrechnungsadresse', - 'Primary Shipping Address' => 'Hauptlieferadresse', - 'Primary payment source updated.' => 'Primäre Zahlungsquelle aktualisiert.', - 'Primary' => 'Primär', - 'Private Note' => 'Private Anmerkung', - 'Product Fields' => 'Produktfelder', - 'Product ID is required.' => 'Produkt-ID erforderlich.', - 'Product Template' => 'Produkt-Template', - 'Product Title Format' => 'Format für Produkttitel', - 'Product Type' => 'Produkttyp', - 'Product Types' => 'Produkttypen', - 'Product URI Format' => 'Produkt-URL Format', - 'Product Variant' => 'Produktvariante', - 'Product Variants' => 'Produktvarianten', - 'Product type saved.' => 'Produkttyp gespeichert.', - 'Product type settings' => 'Produkttyp-Einstellungen', - 'Product' => 'Produkt', - 'Products and Variants deleted.' => 'Produkte und Varianten gelöscht.', - 'Products not restored.' => 'Produkte nicht wiederhergestellt.', - 'Products restored.' => 'Produkte wurden wiederhergestellt.', - 'Products' => 'Produkte', - 'Promotable' => 'Aktionsfähig', - 'Promotable?' => 'Werbeaktionsfähig?', - 'Promotional Amount' => 'Aktionsbetrag', - 'Promotional Price' => 'Aktionspreis', - 'Purchasable Categories' => 'Kategorien der Kaufoption', - 'Purchasable ID and Sale ID are required.' => 'Kaufoptions-ID und Aktions-ID erforderlich.', - 'Purchasable ID is required.' => 'Kaufoptions-ID erforderlich.', - 'Purchasable Type' => 'Typ der Kaufoption', - 'Purchasable' => 'Kaufoption', - 'Purchase (Authorize and Capture Immediately)' => 'Kauf (sofortige Autorisierung und Erfassung)', - 'Purchase Total' => 'Gesamtbetrag', - 'Qty' => 'Menge', - 'Quality Control' => 'Qualitätskontrolle', - 'Quantity' => 'Menge', - 'Rate' => 'Satz', - 'Reassign {numOrders, plural, =1{order} other{orders}}' => '{numOrders, plural, one {}=1{Bestellung} other{Bestellungen}} neu zuweisen', - 'Recalculate order' => 'Bestellung neu berechnen', - 'Receive Inventory' => 'Lagerbestand empfangen', - 'Receive Transfer' => 'Übertragung empfangen', - 'Receive' => 'Empfangen', - 'Received' => 'Empfangen', - 'Recent Orders' => 'Neueste Bestellungen', - 'Recipient' => 'Empfänger', - 'Recover Cart' => 'Warenkorb wiederherstellen', - 'Reduce price' => 'Preis verringern', - 'Reduce the price by a fixed amount' => 'Den Preis um einen festen Betrag verringern', - 'Reduce the price by a percentage of the original price' => 'Den Preis auf einen Prozentsatz des ursprünglichen Preises reduzieren', - 'Reference' => 'Referenz', - 'Refresh payment history' => 'Zahlungsverlauf aktualisieren', - 'Refund note' => 'Rückerstattungsbenachrichtigung', - 'Refund payment' => 'Zahlung rückerstatten', - 'Refund' => 'Erstattung', - 'Reject' => 'Ablehnen', - 'Rejected' => 'Abgelehnt', - 'Relationship Type' => 'Beziehungstyp', - 'Removable included tax rates are only allowed for the default tax zone.' => 'Die entfernbaren, enthaltenen Steuersätze sind nur für die Standard-Steuerzone zulässig.', - 'Remove address' => 'Adresse entfernen', - 'Remove all shipping costs from the order' => 'Alle Versandgebühren von der Bestellung entfernen', - 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Entfernen Sie die Kundenzuordnung und die E-Mail-Adresse aus {numOrders, plural, one {}=1{der Bestellung} other{den Bestellungen}}. Wählen Sie gegebenenfalls unten weitere Kundendaten aus, die Sie entfernen möchten', - 'Remove customer data' => 'Kundendaten entfernen', - 'Remove from price?' => 'Vom Preis entfernen?', - 'Remove shipping costs for matching items only' => 'Versandkosten nur für passende Artikel entfernen', - 'Remove the included tax when a valid organization tax ID is present?' => 'Die enthaltene Steuer entfernen, wenn eine gültige Steuer-ID der Organisation vorliegt?', - 'Remove' => 'Entfernen', - 'Removed' => 'Entfernt', - 'Repeat Customers' => 'Stammkunden', - 'Reply To' => 'Antwort an', - 'Require Billing Address At Checkout' => 'Rechnungsadresse an der Kasse verlangen', - 'Require Coupon Code' => 'Gutscheincode erfordern', - 'Require Shipping Address At Checkout' => 'Versandadresse an der Kasse verlangen', - 'Require Shipping Method Selection At Checkout' => 'Auswahl der Versandart an der Kasse verlangen', - 'Require' => 'Erfordern', - 'Reserved' => 'Reserviert', - 'Reset usage' => 'Verwendung zurücksetzen', - 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Den Rabatt auf die Bestellungen beschränken, bei denen der Kunde einen Mindestbestellwert mit passenden Artikel erreicht hat.', - 'Revenue Options' => 'Umsatz-Optionen', - 'Revenue' => 'Einnahmen', - 'Rule' => 'Regel', - 'Rules reordered.' => 'Regeln umsortiert.', - 'SKU' => 'Bestandseinheit (SKU)', - 'Safety' => 'Sicherheit', - 'Sale Price' => 'Aktionspreis', - 'Sale description.' => 'Beschreibung der Aktion.', - 'Sale reordered.' => 'Aktionen umsortiert.', - 'Sale saved.' => 'Aktion gespeichert.', - 'Sale' => 'Aktion', - 'Sales deleted.' => 'Verkäufe wurden gelöscht.', - 'Sales updated.' => 'Aktionen aktualisiert.', - 'Sales' => 'Verkäufe', - 'Save and continue editing' => 'Speichern und mit Bearbeitung fortfahren', - 'Save and return to all orders' => 'Speichern und zur Bestellungsübersicht zurückkehren', - 'Save and set rules' => 'Speichern und Regeln angeben', - 'Save as a new rule' => 'Als eine neue Regel speichern', - 'Save product to all sites enabled for this product type' => 'Produkt auf allen Websites speichern, die für diesen Produkttyp aktiviert sind', - 'Save product to other sites in the same site group' => 'Produkt auf anderen Websites in derselben Websitegruppe speichern', - 'Save product to other sites with the same language' => 'Produkt auf anderen Websites mit der gleichen Sprache speichern', - 'Save' => 'Speichern', - 'Search customer…' => 'Kunden suchen…', - 'Search inventory' => 'Warenbestand suchen', - 'Search or enter customer email…' => 'Kunden-E-Mail suchen oder eingeben…', - 'Search…' => 'Suchen…', - 'See Orders' => 'Bestellungen anzeigen', - 'Select a gateway' => 'Einen Gateway auswählen', - 'Select a tax category.' => 'Steuerklasse auswählen.', - 'Select a tax zone. If empty, this rate will match anywhere.' => 'Wählen Sie eine Steuerzone. Bei Leerlassen wird dieser Steuersatz überall angewendet.', - 'Select address' => 'Adresse auswählen', - 'Select an item' => 'Lagerbestandsposten auswählen', - 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Wählen Sie, wie die Katalogpreisregel auf die Kaufoption(en) angewendet wird.', - 'Select how the sale will be applied to the purchasable(s).' => 'Wählen Sie, wie der Verkauf auf die Kaufoption(en) angewendet wird.', - 'Select product type' => 'Produkttyp auswählen', - 'Select the emails that will be sent when transitioning to this status.' => 'E-Mails auswählen, die versendet werden beim Wechsel in diesen Status.', - 'Select what this rate should be applied to.' => 'Wählen Sie aus, auf was dieser Steuersatz angewendet werden soll.', - 'Send Email' => 'E-Mail senden', - 'Send to custom recipient' => 'An benutzerdefinierten Empfänger senden', - 'Send to the customer' => 'An Kunden senden', - 'Set Quantity' => 'Menge setzen', - 'Set default category' => 'Standardkategorie einstellen', - 'Set default variant' => 'Als Standardvariante festlegen', - 'Set or Adjust' => 'Setzen oder Anpassen', - 'Set price' => 'Preis einstellen', - 'Set status' => 'Status festlegen', - 'Set the price to a flat amount' => 'Den Preis auf einen Pauschalbetrag setzen', - 'Set the price to a percentage of the original price' => 'Den Preis auf einen Prozentsatz des ursprünglichen Preises setzen', - 'Set the sale price to a flat amount' => 'Den Aktionspreis auf einen Pauschalbetrag stellen', - 'Set the sale price to a percentage of the original price' => 'Den Aktionspreis auf einen Prozentsatz des ursprünglichen Preises einstellen', - 'Set to' => 'Einstellen auf', - 'Settings saved.' => 'Einstellungen gespeichert.', - 'Settings' => 'Einstellungen', - 'Share cart…' => 'Warenkorb teilen…', - 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Versand - Die Versandkosten sind die minimalen Kosten, sollte der Preis der Bestellung geringer sein als die Versandkosten.', - 'Shipping Address Zone' => 'Lieferadresszone', - 'Shipping Address' => 'Lieferadresse', - 'Shipping Business Name' => 'Geschäftsname (Versand)', - 'Shipping Categories' => 'Versandkategorien', - 'Shipping Category Conditions' => 'Versandkategorie-Bedingungen', - 'Shipping Category' => 'Versandkategorie', - 'Shipping First Name' => 'Vorname (Versand)', - 'Shipping Full Name' => 'Vollständiger Name (Versand)', - 'Shipping Last Name' => 'Nachname (Versand)', - 'Shipping Method' => 'Versandart', - 'Shipping Methods' => 'Versandarten', - 'Shipping Rule' => 'Versandregel', - 'Shipping Zones' => 'Versandzonen', - 'Shipping address required.' => 'Lieferadresse erforderlich.', - 'Shipping categories deleted.' => 'Versandkategorien gelöscht.', - 'Shipping category saved.' => 'Versandkategorie gespeichert.', - 'Shipping category updated.' => 'Versandkategorie aktualisiert.', - 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Versandkosten werden in Gänze zur Rechnung hinzugefügt, bevor Prozent-, Artikel- und Gewichtssätze angewendet werden. Auf Null setzen, um diesen Satz auszuschalten. Die gesamte Regel, inklusive dieses Basissatzes wird nicht übereinstimmen und angewendet werden, wenn der Warenkorb nur nicht-lieferbare Artikel wie digitale Produkte enthält.', - 'Shipping method saved.' => 'Versandart gespeichert.', - 'Shipping methods and rules deleted.' => 'Versandmethoden und Regeln gelöscht.', - 'Shipping methods updated.' => 'Versandmethoden aktualisiert.', - 'Shipping rule saved.' => 'Versandregel gespeichert.', - 'Shipping zone saved.' => 'Versandzone gespeichert.', - 'Shipping' => 'Versand', - 'Short Number' => 'Kurzwahlnummer', - 'Show Chart?' => 'Grafik anzeigen?', - 'Show Order Count?' => 'Bestellungsanzahl anzeigen?', - 'Show all prices' => 'Alle Preise anzeigen', - 'Show archived gateways' => 'Archivierte Gateways anzeigen', - 'Show order count line on chart.' => 'Bestellungsanzahlzeile in Grafik anzeigen.', - 'Show related sales' => 'Zugehörige Aktionen anzeigen', - 'Show rule details' => 'Regelinformationen anzeigen', - 'Show the Dimensions and Weight fields for products of this type' => 'Felder "Abmessungen" und "Gewicht" für Produkte diesen Typs anzeigen', - 'Show the Title field for products' => 'Das Titelfeld für Produkte anzeigen', - 'Show the Title field for variants' => 'Titelfeld für Varianten anzeigen', - 'Signed In' => 'Angemeldet', - 'Site Languages' => 'Seitensprachen', - 'Site store mapping saved.' => 'Zuordnung des Webshops gespeichert.', - 'Sites' => 'Websites', - 'Slug' => 'Slug', - 'Snapshot' => 'Schattenkopie', - 'Snapshots' => 'Schattenkopien', - 'Some orders restored.' => 'Einige Bestellungen wurden wiederhergestellt.', - 'Some products restored.' => 'Einige Produkte wurden wiederhergestellt.', - 'Some variants restored.' => 'Einige Varianten wurden wiederhergestellt.', - 'Something changed with the order before payment, please review your order and submit payment again.' => 'Etwas hat sich bei der Bestellung vor der Bezahlung verändert. Bitte überprüfen Sie Ihre Bestellung und führen Sie die Bezahlung erneut durch.', - 'Sorry, no matching options.' => 'Keine passenden Optionen.', - 'Source - The purchasable relationship field is on the category' => 'Quelle - Das Kaufoption-Beziehungsfeld ist in der Kategorie', - 'Source' => 'Quelle', - 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Bitte spezifizieren Sie eine Twig Bedingung, die feststellt, ob ein Rabatt auf eine gegebene Bestellung angewendet werden soll. (Die Bestellung kann über eine `order` Variable referenziert werden.)', - 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Bitte spezifizieren Sie eine Twig Bedingung, die feststellt, ob eine Versandregel auf eine gegebene Bestellung angewendet werden soll. (Die Bestellung kann über eine `order` Variable referenziert werden.)', - 'Start Date' => 'Startdatum', - 'State' => 'Bundesstaat', - 'Status Email Address' => 'Adresse für Status-E-Mails', - 'Status Emails' => 'Status-Benachrichtigungen', - 'Status History' => 'Statusverlauf', - 'Status Updated.' => 'Status wurde aktualisiert.', - 'Status change message' => 'Nachricht zur Statusänderung', - 'Status' => 'Status', - 'Stock' => 'Lager', - 'Stops Processing?' => 'Bearbeitung wird angehalten?', - 'Stops subsequent?' => 'Stoppt nachfolgende?', - 'Store Location' => 'Standort des Geschäfts', - 'Store Management' => 'Shopverwaltung', - 'Store Markets' => 'Shopmärkte', - 'Store Rule' => 'Shopregel', - 'Store saved.' => 'Shop gespeichert.', - 'Store' => 'Shop', - 'Stores & Sites' => 'Shops & Websites', - 'Stores' => 'Shops', - 'Strategy to apply when an order is free or has a zero balance.' => 'Strategie zum Anwenden, wenn eine Bestellung kostenlos ist oder ein Nullsaldo hat.', - 'Strategy to apply when calculating the minimum order price.' => 'Anzuwendende Strategie beim Berechnen des Mindestbestellungspreises.', - 'Subject' => 'Betreff', - 'Subscribing user' => 'Abonnierter Benutzer', - 'Subscription Fields' => 'Abonnementfelder', - 'Subscription Plans' => 'Abonnementpläne', - 'Subscription Settings' => 'Abonnementeinstellungen', - 'Subscription cancelled.' => 'Abonnement gekündigt.', - 'Subscription date' => 'Abonnementdatum', - 'Subscription fields saved.' => 'Abonnementfelder gespeichert.', - 'Subscription for {user} to {plan} prevented by a plugin.' => 'Abonnement von {user} für {plan} wurde durch ein Plug-in verhindert.', - 'Subscription plan saved.' => 'Abonnementplan wurde gespeichert.', - 'Subscription plan' => 'Abonnementplan', - 'Subscription plans' => 'Abonnementpläne', - 'Subscription reactivated.' => 'Abonnement reaktiviert.', - 'Subscription reference' => 'Abonnementreferenz', - 'Subscription started.' => 'Abonnement gestartet.', - 'Subscription switched.' => 'Abonnement gewechselt.', - 'Subscription to “{plan}”' => 'Abonnement von "{plan}"', - 'Subscription' => 'Abonnement', - 'Subscriptions on hold' => 'Abonnements pausiert', - 'Subscriptions' => 'Abonnements', - 'Suppress emails' => 'E-Mails unterdrücken', - 'Switch plan' => 'Plan wechseln', - 'Switch' => 'Wechseln', - 'System' => 'System', - 'Table Columns' => 'Tabellenspalten', - 'Target - The category relationship field is on the purchasable' => 'Ziel - Das Kategorien-Beziehungsfeld ist bei der Kaufoption', - 'Tax & Shipping' => 'Versandkosten und Steuern', - 'Tax (inc)' => 'Steuer (inkl.)', - 'Tax Categories' => 'Steuerklasse', - 'Tax Category' => 'Steuerklasse', - 'Tax Rates' => 'Steuersatz', - 'Tax Zone' => 'Steuerzone', - 'Tax Zones' => 'Steuerzonen', - 'Tax categories deleted.' => 'Steuerkategorien gelöscht.', - 'Tax category saved.' => 'Steuerklasse gespeichert.', - 'Tax category updated.' => 'Steuerklasse aktualisiert.', - 'Tax rate saved.' => 'Steuersatz gespeichert.', - 'Tax rates updated.' => 'Steuersatz aktualisiert.', - 'Tax zone saved.' => 'Steuerzone gespeichert.', - 'Tax' => 'Steuer', - 'Taxable Subject' => 'Steuerpflichtige Person', - 'Template Path' => 'Template-Pfad', - 'That handle is already in use' => 'Dieser Identifikator wird bereits verwendet', - 'That handle is already in use.' => 'Dieser Identifikator wird bereits verwendet.', - 'The PDF to attach to this email.' => 'Die PDF, die an diese E-Mail angehängt werden soll.', - 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'Die URL zu der Seite, auf der Zahlungsinformationen für ein Abonnement aktualisiert und die 3DS-Authentifizierung durchgeführt werden können.', - 'The address provided is outside the store’s market.' => 'Die angegebene Adresse liegt außerhalb des Marktes des Shops.', - 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'Die Rabattmenge, die auf die ganze Bestellung angewendet wird. Dieser Betrag wird auf die Posten verteilt, vom höchsten Preis bis zum niedrigsten, bis der Rabatt aufgebraucht ist.', - 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'Der Basisrabatt kann Artikel im Warenkorb nur bis zu einem Preis von Null reduzieren, bis er aufgebraucht ist, und kann den Preis der Bestellung nicht negativ machen.', - 'The cart recovery link is invalid. Please request a new one.' => 'Der Warenkorb-Wiederherstellungslink ist ungültig. Bitte fordern Sie einen neuen an.', - 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Der Wechselkurs, der benutzt wird, wenn ein Betrag in diese Währung umgerechnet wird. Wenn ein Artikel zum Beispiel {amount1} kostet, würde sich aus einem Wechselkurs von {rate} ein Betrag von {amount2} in der Zweitwährung ergeben.', - 'The countries that orders are allowed to be placed from.' => 'Die Länder, aus denen Bestellungen aufgegeben werden dürfen.', - 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'Der Coupon „{code}“ hat sein Nutzungslimit von {limit} überschritten.', - 'The customer for this order has been deleted.' => 'Der Kunde für diese Bestellung wurde gelöscht.', - 'The default shipping category is automatically available to all product types.' => 'Die Standardversandkategorie ist automatisch für alle Produkttypen verfügbar.', - 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'Der Rabatt „{name}“ hat sein Gesamtnutzungslimit von {limit} überschritten.', - 'The download link has expired. Please request a new one.' => 'Der Download-Link ist abgelaufen. Bitte fordern Sie einen neuen an.', - 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'Die E-Mail-Adresse von der E-Mails zum Bestellstatus gesendet werden. Keine Angabe, wenn die E-Mail-Adresse aus den allgemeinen Einstellungen in Craft verwendet werden soll.', - 'The entry that contains the description for this subscription’s plan.' => 'Die Eingabe, die eine Beschreibung dieses Abonnementsplans enthält.', - 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'Der Pauschalwert, der von jedem Artikel abgezogen werden soll, z. B. “3” für 3 $ Rabatt auf jeden Artikel.', - 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'Das Format, in dem neue Coupons generiert werden, z. B. {example}. Alle #-Zeichen werden durch einen zufälligen Buchstaben ersetzt.', - 'The from and to inventory locations must be different.' => 'Der Ausgangs- und Ziellagerbestand müssen unterschiedlich sein.', - 'The inventory locations this store uses.' => 'Die Lagerbestandsorte, die dieser Shop verwendet.', - 'The item is not enabled for sale.' => 'Der Artikel ist nicht für den Verkauf freigegeben.', - 'The language the order was made in.' => 'Die Sprache, in der die Bestellung aufgegeben wurde.', - 'The language to be used when this email is rendered.' => 'Die Sprache, die verwendet werden soll, wenn diese E-Mail wiedergegeben wird.', - 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Die maximale Zahl an Hierachiestufen, die dieses Produkt haben kann. Wenn es egal ist, leer lassen.', - 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Der Maximalbetrag, den der Kunde für den Versand ausgeben soll. Zum Deaktivieren auf Null setzen.', - 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Der Minimalbetrag, den der Kunde für den Versand ausgeben soll. Zum Deaktivieren auf Null setzen.', - 'The order is not valid.' => 'Die Bestellung ist nicht gültig.', - 'The payment gateway that will be used for the subscription plan.' => 'Der Zahlungs-Gateway, der für den Abonnementplan verwendet wird.', - 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'Der Prozentwert, der von jedem Artikel nachgelassen werden soll. z.B {ex1} für {ex2} Nachlass. -Prozentwerte sind auf zwei Dezimalstellen gerundet.', - 'The previously-selected shipping method is no longer available.' => 'Die zuvor gewählte Versandoption ist nicht mehr verfügbar.', - 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Der Preis von {description} wurde von {originalSalePriceAsCurrency} auf {newSalePriceAsCurrency} erhöht', - 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Der Preis von {description} wurde von {originalSalePriceAsCurrency} auf {newSalePriceAsCurrency} gesenkt', - 'The primary currency cannot be changed after orders are placed.' => 'Die primäre Währung kann nicht geändert werden, nachdem Bestellungen aufgegeben wurden.', - 'The purchasable defines the relationship' => 'Die Kaufoption definiert die Beziehung', - 'The purchasable is related by another element' => 'Die Kaufoption ist mit einem anderen Element verknüpft', - 'The recipient of the email. Twig code can be used here.' => 'Der Empfänger der E-Mail. Twig-Code kann hier verwendet werden.', - 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'Die E-Mail-Adresse, an die Antworten gesendet werden sollen. Für die normale Antwort-E-Mail-Adresse des Senders frei lassen. Twig-Code kann hier verwendet werden.', - 'The site the order was made in.' => 'Die Website, von der aus die Bestellung aufgegeben wurde.', - 'The site to be used when this email is rendered.' => 'Die Website, die verwendet werden soll, wenn diese E-Mail wiedergegeben wird.', - 'The subject line of the email. Twig code can be used here.' => 'Die Betreffzeile der E-Mail. Twig-Code kann hier verwendet werden.', - 'The template that the PDF should be generated from.' => 'Die Vorlage, aus der die PDF generiert werden soll.', - 'The template to be used for HTML emails.' => 'Das Template, das für HTML-E-Mails genutzt werden soll.', - 'The template to be used for plain text emails. Twig code can be used here.' => 'Die Vorlage, die für Klartext-E-Mails verwendet werden soll. Twig-Code kann verwendet werden.', - 'The template to use when a product’s URL is requested.' => 'Das Template, das genutzt werden soll, wenn die URL eines Produktes angefordert wird.', - 'The total number of order adjustments changed.' => 'Die Gesamtanzahl der Angebotsanpassungen hat sich geändert.', - 'The total price of the order changed.' => 'Der Gesamtpreis der Bestellung hat sich geändert.', - 'The total quantity of items within the order changed.' => 'Die Gesamtanzahl der Artikel in der Bestellung hat sich geändert.', - 'The unique SKU of the donation purchasable.' => 'Die einzigartige SKU für das Spenden-Kaufoption.', - 'The unit of measurement that should be used when specifying product dimensions.' => 'Maßeinheit, die verwendet werden soll, um die Abmessungen eines Produktes anzugeben.', - 'The unit of measurement that should be used when specifying product weights.' => 'Die Maßeinheit, die bei der Gewichtsangabe von Produkten verwendet werden soll.', - 'The webhook URL for this gateway.' => 'Die Webhook URL für dieses Gateway.', - 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'Der Name im Feld “von” wird genutzt, wenn E-Mails zum Bestellstatus versandt werden. Wenn der Absendername aus den allgemeinen Einstellungen in Craft genutzt werden soll, Feld leer lassen.', - 'There are errors on the order' => 'Es gibt Fehler bei dieser Bestellung', - 'There are only {num} “{description}” items left in stock.' => 'Es sind nur noch {num} “{description}” Artikel auf Lager.', - 'There aren’t any product types to select yet.' => 'Es gibt noch keine Produkttypen zur Auswahl.', - 'There is no gateway or payment source available for use with this order.' => 'Es sind keine Gateways oder Zahlungsquellen zur Verwendung für diese Bestellung verfügbar.', - 'There is no gateway selected that supports payment sources.' => 'Es wurde kein Gateway ausgewählt, der Zahlungsquellen unterstützt.', - 'There is no shipping method selected for this order.' => 'Für diese Bestellung ist keine Versandart ausgewählt.', - 'This URL will load the cart into the user’s session, making it the active cart.' => 'Diese URL wird den Warenkorb in die Sitzung des Nutzers laden, wodurch dieser zum aktiven Warenkorb wird.', - 'This action is not allowed for the current user.' => 'Diese Aktion ist für den aktuellen Benutzer nicht zulässig.', - 'This category will be used as the default for all purchasables in this store.' => 'Diese Kategorie wird als Standard für alle in diesem Shop erhältlichen Artikel verwendet.', - 'This coupon is for registered users and limited to {limit} uses.' => 'Dieser Coupon ist nur für registrierte Benutzer und auf {limit} Anwendungen limitiert.', - 'This coupon is limited to {limit} uses.' => 'Dieser Coupon ist auf {limit} Anwendungen begrenzt.', - 'This coupon requires an email address.' => 'Dieser Coupon erfordert eine E-Mail-Adresse.', - 'This gateway does not support that functionality.' => 'Dieser Gateway unterstützt diese Funktionalität nicht.', - 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Dies wird durch die {setting}-Konfigurationseinstellung in `config/{file}.php` überschrieben.', - 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Dies ist die Adresse Ihres Geschäfts. Sie kann von mehreren Plug-ins dazu verwendet werden, um Dinge wie Lieferung und Steuern festzulegen. Sie kann auch in PDF-Belegen verwendet werden.', - 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Das ist die Standard-PDF, die erstellt wird, wenn die Bestellungs-PDF angefordert wird.', - 'This is the last location for the {store} store.' => 'Dies ist der letzte Standort für den Shop {store}.', - 'This month' => 'Dieser Monat', - 'This order has unsaved changes.' => 'Diese Bestellung hat ungespeicherte Änderungen.', - 'This week' => 'Diese Woche', - 'This year' => 'Dieses Jahr', - 'Times Used' => 'Häufigkeit des Gebrauchs', - 'Title' => 'Titel', - 'To' => 'An', - 'Today' => 'Heute', - 'Too many variants for this product.' => 'Zu viele Varianten für dieses Produkt.', - 'Top Customers by Average Order' => 'Beste Kunden nach durchschnittlicher Bestellung', - 'Top Customers by Total Revenue' => 'Top Kunden nach Gesamtumsatz', - 'Top Customers' => 'Top-Kunden', - 'Top Product Types by Qty Sold' => 'Top Produkttypen nach verkaufter Menge', - 'Top Product Types by Revenue' => 'Top Produkttypen nach Umsatz', - 'Top Product Types' => 'Top-Produkttypen', - 'Top Products by Qty Sold' => 'Top Produkte nach verkaufter Menge', - 'Top Products by Revenue' => 'Top Produkte nach Umsatz', - 'Top Products' => 'Top Produkte', - 'Top Purchasables by Qty Sold' => 'Top-Kaufoptionen nach verkaufter Menge', - 'Top Purchasables by Revenue' => 'Top Kaufoptionen nach Umsatz', - 'Top Purchasables' => 'Top-Kaufoptionen', - 'Total ' => 'Gesamt ', - 'Total Discount Use Limit' => 'Gesamter Rabatt-Nutzungslimit', - 'Total Discount' => 'Gesamtrabatt', - 'Total Included Tax' => 'Vollständige enthaltene Steuer', - 'Total Orders by Billing Country' => 'Gesamtbestellungen nach Rechnungsland', - 'Total Orders by Country' => 'Gesamtbestellungen nach Land', - 'Total Orders by Shipping Country' => 'Gesamtbestellungen nach Lieferland', - 'Total Orders' => 'Bestellungen insgesamt', - 'Total Paid' => 'Insgesamt bezahlt', - 'Total Price' => 'Gesamtpreis', - 'Total Qty' => 'Gesamtmenge', - 'Total Revenue' => 'Gesamteinnahmen', - 'Total Shipping' => 'Gesamtversandkosten', - 'Total Tax' => 'Steuern gesamt', - 'Total Weight' => 'Gesamtgewicht', - 'Total' => 'Insgesamt', - 'Track Inventory' => 'Lagerbestand verfolgen', - 'Transaction Hash' => 'Transaktions-Hashwert', - 'Transaction ID' => 'Transaktions-ID', - 'Transaction captured successfully: {message}' => 'Transaktion erfolgreich erfasst: {message}', - 'Transaction refunded successfully: {message}' => 'Transaktion erfolgreich erstattet: {message}', - 'Transactions' => 'Transaktionen', - 'Transfer Fields' => 'Übertragungsfelder', - 'Transfer Items' => 'Übertragungsbestandsposten', - 'Transfer Settings' => 'Übertragungseinstellungen', - 'Transfer Status' => 'Übertragungsstatus', - 'Transfer fields saved.' => 'Die Übertragungsfelder wurden gespeichert.', - 'Transfer must have at least one item.' => 'Die Übertragung muss mindestens einen Lagerbestandsposten enthalten.', - 'Transfer' => 'Übertragung', - 'Transfers' => 'Übertragungen', - 'Trial days credited' => 'Testtage gutgeschrieben', - 'Trial expiration' => 'Ablauf der Testphase', - 'Trial expiry date' => 'Ablaufdatum der Testphase', - 'Type not in allowed options.' => 'Typ ist nicht in erlaubten Optionen.', - 'Type' => 'Typ', - 'URI' => 'URI', - 'Unable to cancel subscription at this time.' => 'Zurzeit kann das Abonnement nicht storniert werden.', - 'Unable to complete order: another request is already in progress.' => 'Die Bestellung kann nicht abgeschlossen werden: Eine andere Anfrage wird bereits bearbeitet.', - 'Unable to find variant.' => 'Variante nicht gefunden.', - 'Unable to generate coupon codes: {message}' => 'Gutscheincodes können nicht erzeugt werden: {message}', - 'Unable to make payment at this time.' => 'Die Zahlung kann zurzeit nicht durchgeführt werden.', - 'Unable to modify subscription at this time.' => 'Das Abonnement kann zurzeit nicht verändert werden.', - 'Unable to reactivate subscription at this time.' => 'Zurzeit kann das Abonnement nicht wieder aktiviert werden.', - 'Unable to reassign orders.' => 'Bestellungen können nicht neu zugewiesen werden.', - 'Unable to remove order data.' => 'Die Bestelldaten konnten nicht gelöscht werden.', - 'Unable to retrieve Sale and Purchasable.' => 'Aktion und Kaufoptionen konnten nicht abgerufen werden.', - 'Unable to retrieve cart.' => 'Warenkorb konnte nicht abgerufen werden.', - 'Unable to retrieve customer.' => 'Kunde konnte nicht abgerufen werden.', - 'Unable to retrieve load cart URL' => 'URL zum Laden vom Warenkorb konnte nicht abgerufen werden', - 'Unable to retrieve payment source.' => 'Zahlungsquelle konnte nicht abgerufen werden.', - 'Unable to set default shipping category.' => 'Standardversandkategorie konnte nicht eingestellt werden.', - 'Unable to set default tax category.' => 'Standardsteuerkategorie konnte nicht eingestellt werden.', - 'Unable to set primary payment source.' => 'Primäre Zahlungsquelle konnte nicht festgelegt werden.', - 'Unable to start the subscription. Please check your payment details.' => 'Das Abonnement kann nicht gestartet werden. Bitte überprüfen Sie Ihre Zahlungsinformationen.', - 'Unable to subscribe at this time.' => 'Zurzeit kann kein Abonnement abgeschlossen werden.', - 'Unable to update cart.' => 'Warenkorb konnte nicht aktualisiert werden.', - 'Unable to validate address.' => 'Adresse konnte nicht validiert werden.', - 'Unit Price' => 'Einheitspreis', - 'Unit price (minus discounts)' => 'Einheitspreis (abzüglich Rabatte)', - 'Units' => 'Einheiten', - 'Unpaid' => 'Nicht bezahlt', - 'Unsubscribe' => 'Abbestellen', - 'Update Address' => 'Adresse aktualisieren', - 'Update Order Status' => 'Bestellstatus aktualisieren', - 'Update Order Status…' => 'Bestellstatus aktualisieren…', - 'Update order' => 'Bestellung aktualisieren', - 'Update subscription' => 'Abonnement aktualisieren', - 'Update' => 'Aktualisieren', - 'Updated By' => 'Aktualisiert von', - 'Updated committed stock successfully.' => 'Zugewiesener Bestand erfolgreich aktualisiert.', - 'Updated' => 'Aktualisiert', - 'Use Billing Address For Tax' => 'Rechnungsadresse für Steuern verwenden', - 'Use as the primary billing address' => 'Als primäre Zahlungsadresse verwenden', - 'Use as the primary shipping address' => 'Als primäre Versandadresse verwenden', - 'Used By Tax Rates' => 'Verwendet nach Steuersatz', - 'Used by Tax Rates' => 'Verwendet nach Steuersatz', - 'User Groups' => 'Benutzergruppen', - 'User not found.' => 'Benutzer nicht gefunden.', - 'User' => 'Benutzer', - 'Uses' => 'Verwendungen', - 'Validate Business Tax ID as Vat ID' => 'Geschäftliche Steuer-ID als USt-IdNr validieren', - 'Validating condition syntax' => 'Bedingungssyntax wird validiert', - 'Validating formula syntax' => 'Formelsyntax wird validiert', - 'Variant Fields' => 'Variantenfelder', - 'Variant Has Untracked Stock' => 'Variante hat nicht verfolgten Lagerbestand', - 'Variant Price' => 'Preis der Variante', - 'Variant SKU' => 'SKU der Variante', - 'Variant Search' => 'Variantensuche', - 'Variant Stock' => 'Lagerbestand der Variante', - 'Variant Title Format' => 'Format für Variantentitel', - 'Variant Tracks Stock' => 'Variante mit Lagerbestandsverfolgung', - 'Variant UI Label Format' => 'Varianten-UI-Bezeichnung-Format', - 'Variant has no product.' => 'Variante hat kein Produkt.', - 'Variants not restored.' => 'Varianten wurden nicht wiederhergestellt.', - 'Variants restored.' => 'Varianten wurden wiederhergestellt.', - 'Variants' => 'Varianten', - 'View customer' => 'Kunde anzeigen', - 'View order' => 'Bestellung anzeigen', - 'View product type - {productType}' => 'Produkttyp anzeigen - {productType}', - 'View user' => 'Benutzer anzeigen', - 'View' => 'Ansicht', - 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Warnung, das Löschen dieser Währung wird alle Zahlungen und Rückerstattungen in dieser Währung aufhalten. Sind Sie sicher, dass Sie „{name}“ löschen möchten?', - 'Web' => 'Web', - 'Webhook URL' => 'Webhook URL', - 'Weight ({unit})' => 'Gewicht ({unit})', - 'Weight Rate' => 'Gewichtspreis', - 'Weight Unit' => 'Gewichtseinheit', - 'Weight' => 'Gewicht', - 'What product URIs should look like for the site.' => 'Wie Produkt-URIs für diese Website aussehen sollen.', - 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Wie die automatisch generierten Produkttitel aussehen sollten. Sie können auch Schlagwörter nutzen, die Produkteigenschaften ausgeben, wie z. B. {ex1} oder {ex2}. Alle benutzerdefinierten Felder müssen als erforderlich markiert sein.', - 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Wie die automatisch generierten Variantentitel aussehen sollten. Sie können auch Tags nutzen, die Varianteneigenschaften ausgeben, wie z.B. {ex1} oder {ex2}. Alle benutzerdefinierten Felder müssen als erforderlich markiert sein.', - 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Wie der PDF-Dateiname der Bestellung aussehen sollte (ohne Dateiendung). Sie können Tags hinzufügen die Bestelleigenschaften ausgeben, wie etwa {ex1} or {ex2}.', - 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Wie die einzigartige auto-generierte Bestandseinheit (SKU) aussehen sollte, wenn ein Bestandseinheits-Feld ohne Wert abgesendet wurde. Sie können Tags einbeziehen, die Eigenschaften ausgeben, wie z.B. {ex1} oder {ex2)', - 'What this PDF will be called in the control panel.' => 'Wie diese PDF im Control Panel genannt wird.', - 'What this catalog pricing rule will be called in the control panel.' => 'Wie diese Katalogpreisregel im Control Panel genannt wird.', - 'What this discount will be called in the control panel.' => 'Wie dieser Rabatt im Control Panel genannt wird.', - 'What this email will be called in the control panel.' => 'Wie diese E-Mail im Control Panel genannt wird.', - 'What this product type will be called in the control panel.' => 'Wie dieser Produkttyp im Control Panel genannt wird.', - 'What this sale will be called in the control panel.' => 'Wie diese Aktion im Control Panel genannt wird.', - 'What this shipping category will be called in the control panel.' => 'Wie diese Versandkategorie im Control Panel genannt wird.', - 'What this shipping rule will be called in the control panel.' => 'Wie diese Versandregel im Control Panel genannt wird.', - 'What this shipping zone will be called in the control panel.' => 'Wie diese Versandzone im Control Panel genannt wird.', - 'What this status will be called in the control panel.' => 'Wie dieser Status im Control Panel genannt wird.', - 'What this subscription plan will be called in the control panel.' => 'Wie dieser Abonnementplan im Control Panel genannt wird.', - 'What this tax category will be called in the control panel.' => 'Wie diese Steuerklasse im Control Panel genannt wird.', - 'What this tax zone will be called in the control panel.' => 'Wie diese Steuerzone im Control Panel genannt wird.', - 'When this discount is applied to an order, which line items should be discounted?' => 'Wenn dieser Rabatt auf eine Bestellung angewendet wird, welche Einzelposten sollen dann rabattiert werden?', - 'Whether the first available shipping method option should be set automatically on carts.' => 'Ob die erste verfügbare Versandartoption in Warenkörben automatisch gesetzt werden soll.', - 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Ob die primäre Zahlungsquelle des Benutzers bei neuen Warenkörben automatisch festgelegt werden soll.', - 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Ob die primäre Versand- und Rechnungsadresse des Benutzers bei neuen Warenkörben automatisch gesetzt werden soll.', - 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Ob diese Katalogpreisregel unabhängig von anderen Bedingungen genutzt werden kann.', - 'Whether this sale should be available for use, regardless of other conditions.' => 'Ob dieser Verkauf unabhängig von anderen Bedingungen genutzt werden kann.', - 'Which data to display in the name column in the results table.' => 'Welche Daten in der Namensspalte der Ergebnistabelle angezeigt werden.', - 'Which product types should this category be available to?' => 'Für welche Produkttypen sollte diese Kategorie verfügbar sein?', - 'Which template should be loaded when a product’s URL is requested.' => 'Die Vorlage, die bei Anforderung der URL eines Produktes geladen werden soll.', - 'Width ({unit})' => 'Breite ({unit})', - 'Width' => 'Breite', - 'YYYY' => 'JJJJ', - 'Yes' => 'Ja', - 'You are not allowed to add a line item.' => 'Sie dürfen keinen Einzelposten hinzufügen.', - 'You currently have no emails configured to select for this status.' => 'Sie haben aktuell keine E-Mails zur Auswahl für diesen Status konfiguriert.', - 'You do not have permission to load this cart.' => 'Sie haben keine Berechtigung, diesen Warenkorb zu laden.', - 'You must set up at least one gateway that supports subscriptions first.' => 'Sie müssen mindestens einen Gateway einrichten, der zuerst Abonnements unterstützt.', - 'You must be logged in or provide a valid token to load this cart.' => 'Sie müssen angemeldet sein oder ein gültiges Token bereitstellen, um diesen Warenkorb zu laden.', - 'You must be signed in to create a payment source.' => 'Sie müssen angemeldet sein, um eine Zahlungsquelle zu erstellen.', - 'You must be signed in to set a primary payment source.' => 'Sie müssen angemeldet sein, um eine primäre Zahlungsquelle festzulegen.', - 'You must make a payment to complete the order.' => 'Sie müssen eine Zahlung tätigen, um die Bestellung abzuschließen.', - 'Your Cart Recovery Link' => 'Ihr Warenkorb-Wiederherstellungslink', - 'Your Order PDF Download Link' => 'Ihr Bestellung-PDF-Download-Link', - 'Your order is empty' => 'Ihre Bestellungen ist leer', - 'ZIP file' => 'ZIP-Datei', - 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Null - Minimaler Preis ist Null, sollten die Rabatte größer sein als der Wert der Bestellung.', - 'Zip Code' => 'Postleitzahl', - 'all' => 'alle', - 'any' => 'beliebig', - 'average order total' => 'durchschnittlicher Bestellungspreis', - 'billing address' => 'Rechnungsadresse', - 'donation' => 'Spende', - 'donations' => 'Spenden', - 'info' => 'Info', - 'inventory location' => 'Lagerbestandsort', - 'new customers' => 'Neukunden', - 'on hand' => 'auf Lager', - 'only' => 'nur', - 'order' => 'Bestellung', - 'orders' => 'Bestellungen', - 'price' => 'Preis', - 'prices' => 'Preise', - 'product variant' => 'Produktvariante', - 'product variants' => 'Produktvarianten', - 'product' => 'Produkt', - 'products' => 'Produkte', - 'repeat customers' => 'Stammkunden', - 'shipping address' => 'Lieferadresse', - 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'shippingSameAsBilling und billingSameAsShipping können nicht gleichzeitig festgelegt sein.', - 'subscription' => 'Abonnement', - 'subscriptions' => 'Abonnements', - 'to' => 'bis', - 'transfer' => 'übertragung', - 'transfers' => 'übertragungen', - '{amount} included' => '{amount} enthalten', - '{count} Unfulfilled Orders' => '{count} unerfüllte Bestellungen', - '{description} is no longer available.' => '{description} nicht länger verfügbar.', - '{description} only has {stock} in stock.' => '{description} hat nur noch {stock} auf Lager.', - '{from} to {to}' => '{from} nach {to}', - '{name} (Primary)' => '{name} (Hauptname)', - '{name} (Trashed)' => '{name} (Verworfen)', - '{name} catalog price' => '{name} Katalogpreis', - '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, =1{Bestellung} other{Bestellungen}} aktualisiert.', - '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, one {}=1{Bestellung ist} other{Bestellungen sind}} mit {numUsers, plural, one {}=1{dem Nutzer} other{den Nutzern}} verknüpft.', - '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, one {}=1{Abonnement ist} other{Abonnements sind}} für {numUsers, plural, one {}=1{den Nutzer} other{die Nutzer}} aktiviert.', - '{number} more…' => '{number} mehr …', - '{pct} off the discounted item price' => '{pct} Nachlass vom reduzierten Artikelpreis', - '{pct} off the original item price' => '{pct} Nachlass vom ursprünglichen Artikelpreis', - '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, one {}=1{wurde} other{wurden}} keiner Website zugewiesen.', - '{total} in total revenue' => '{total} in Gesamtumsatz', - '{total} orders' => '{total} Bestellungen', - '{total} saleable across {locationCount} location(s)' => '{total} verkaufbar über {locationCount} Standort(e)', - '{uses} uses across {emails} email addresses' => '{uses} Nutzen über {emails} E-Mail-Adressen', - '{uses} uses across {users} users' => '{uses} Verwendungen über {users} Kunden', - '“{description}” is currently out of stock.' => '“{description}” ist aktuell nicht vorrätig.', - '“{key}” has invalid JSON' => '„{key}“ enthält ungültiges JSON', -]; diff --git a/src/translations/en-GB/commerce.php b/src/translations/en-GB/commerce.php deleted file mode 100644 index 2c2a8412a9..0000000000 --- a/src/translations/en-GB/commerce.php +++ /dev/null @@ -1,1428 +0,0 @@ - '(new price)', - '(of original price)' => '(of original price)', - '(off original price)' => '(off original price)', - 'A cart number must be specified.' => 'A cart number must be specified.', - 'A cart recovery link has been sent to {email}.' => 'A cart recovery link has been sent to {email}.', - 'A cart recovery link will be sent to {email}.' => 'A cart recovery link will be sent to {email}.', - 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.', - 'A new download link has been sent to {email}' => 'A new download link has been sent to {email}', - 'A new download link will be sent to {email}' => 'A new download link will be sent to {email}', - 'A valid email is required to create a customer.' => 'A valid email is required to create a customer.', - 'Accept' => 'Accept', - 'Accepted' => 'Accepted', - 'Actions' => 'Actions', - 'Active Carts' => 'Active Carts', - 'Active subscriptions' => 'Active subscriptions', - 'Active' => 'Active', - 'Add Address' => 'Add Address', - 'Add a coupon' => 'Add a coupon', - 'Add a custom line item' => 'Add a custom line item', - 'Add a line item' => 'Add a line item', - 'Add a product' => 'Add a product', - 'Add a variant' => 'Add a variant', - 'Add an adjustment' => 'Add an adjustment', - 'Add an item' => 'Add an item', - 'Add an option' => 'Add an option', - 'Add catalog price' => 'Add catalogue price', - 'Add' => 'Add', - 'Additional Actions' => 'Additional Actions', - 'Additional recipients that should receive this email. Twig code can be used here.' => 'Additional recipients that should receive this email. Twig code can be used here.', - 'Address 1' => 'Address 1', - 'Address 2' => 'Address 2', - 'Address 3' => 'Address 3', - 'Address Line 1' => 'Address Line 1', - 'Address Line 2' => 'Address Line 2', - 'Address Updated.' => 'Address Updated.', - 'Address copied to user.' => 'Address copied to user.', - 'Address not found.' => 'Address not found.', - 'Adjust Quantity' => 'Adjust Quantity', - 'Adjust by' => 'Adjust by', - 'Adjust price when included rate is disqualified?' => 'Adjust price when included rate is disqualified?', - 'Adjustments' => 'Adjustments', - 'Admin Notices' => 'Admin Notices', - 'Administrative Area Code of Origin' => 'Administrative Area Code of Origin', - 'Advanced' => 'Advanced', - 'All Orders' => 'All Orders', - 'All Totals' => 'All Totals', - 'All Transfers' => 'All Transfers', - 'All active subscriptions' => 'All active subscriptions', - 'All customers' => 'All customers', - 'All products' => 'All products', - 'All variants must have a SKU.' => 'All variants must have a SKU.', - 'All' => 'All', - 'Allow Checkout Without Payment' => 'Allow Checkout Without Payment', - 'Allow Empty Cart On Checkout' => 'Allow Empty Cart On Checkout', - 'Allow Partial Payment On Checkout' => 'Allow Partial Payment On Checkout', - 'Allow out of stock purchases' => 'Allow out of stock purchases', - 'Allow' => 'Allow', - 'Allowed Qty' => 'Allowed Qty', - 'Alternative Phone' => 'Alternative Phone', - 'Amount' => 'Amount', - 'An ID must be provided' => 'An ID must be provided', - 'An error occurred while generating this PDF.' => 'An error occurred while generating this PDF.', - 'Any' => 'Any', - 'Anywhere' => 'Anywhere', - 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.', - 'Are you sure you want to capture this transaction?' => 'Are you sure you want to capture this transaction?', - 'Are you sure you want to complete this order?' => 'Are you sure you want to complete this order?', - 'Are you sure you want to delete the selected orders?' => 'Are you sure you want to delete the selected orders?', - 'Are you sure you want to delete the selected product and its variants?' => 'Are you sure you want to delete the selected product and its variants?', - 'Are you sure you want to delete this shipping rule?' => 'Are you sure you want to delete this shipping rule?', - 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.', - 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Are you sure you want to delete “{name}”? This will set all line items with this status to no status.', - 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.', - 'Are you sure you want to overwrite the billing address?' => 'Are you sure you want to overwrite the billing address?', - 'Are you sure you want to overwrite the shipping address?' => 'Are you sure you want to overwrite the shipping address?', - 'Are you sure you want to permanently delete this store and everything in it?' => 'Are you sure you want to permanently delete this store and everything in it?', - 'Are you sure you want to refund this transaction?' => 'Are you sure you want to refund this transaction?', - 'Are you sure you want to remove this customer?' => 'Are you sure you want to remove this customer?', - 'Are you sure you want to save this as a new shipping rule?' => 'Are you sure you want to save this as a new shipping rule?', - 'Are you sure you want to send email: {name}?' => 'Are you sure you want to send email: {name}?', - 'At least one site must be enabled for the product type.' => 'At least one site must be enabled for the product type.', - 'Attempted Payments' => 'Attempted Payments', - 'Attention' => 'Attention', - 'Authorize Only (Manually Capture)' => 'Authorise Only (Manually Capture)', - 'Auto Set Cart Shipping Method Option' => 'Auto Set Cart Shipping Method Option', - 'Auto Set New Cart Addresses' => 'Auto Set New Cart Addresses', - 'Auto Set Payment Source' => 'Auto Set Payment Source', - 'Automatic SKU Format' => 'Automatic SKU Format', - 'Available Shipping Categories' => 'Available Shipping Categories', - 'Available Tax Categories' => 'Available Tax Categories', - 'Available for purchase' => 'Available for purchase', - 'Available for purchase?' => 'Available for purchase?', - 'Available inventory for "{description}" has gone below zero.' => 'Available inventory for "{description}" has gone below zero.', - 'Available to Product Types' => 'Available to Product Types', - 'Available' => 'Available', - 'Available?' => 'Available?', - 'Average Order Total' => 'Average Order Total', - 'Average' => 'Average', - 'BCC’d Recipient' => 'BCC’d Recipient', - 'Bad Request' => 'Bad Request', - 'Bad address ID.' => 'Bad address ID.', - 'Bad order ID.' => 'Bad order ID.', - 'Base Price' => 'Base Price', - 'Base Promotional Price' => 'Base Promotional Price', - 'Base Rate' => 'Base Rate', - 'Base' => 'Base', - 'Bcc' => 'Bcc', - 'Billing Address' => 'Billing Address', - 'Billing Business Name' => 'Billing Business Name', - 'Billing First Name' => 'Billing First Name', - 'Billing Full Name' => 'Billing Full Name', - 'Billing Last Name' => 'Billing Last Name', - 'Billing address required.' => 'Billing address required.', - 'Billing detail update URL' => 'Billing detail update URL', - 'Billing issues' => 'Billing issues', - 'Billing' => 'Billing', - 'Both (Line item price + Line item shipping costs)' => 'Both (Line item price + Line item shipping costs)', - 'Business ID' => 'Business ID', - 'Business Name' => 'Business Name', - 'Business Tax ID' => 'Business Tax ID', - 'CC’d Recipient' => 'CC’d Recipient', - 'CVV' => 'CVV', - 'Can be used as an internal reference.' => 'Can be used as an internal reference.', - 'Can not complete payment for missing transaction.' => 'Cannot complete payment for missing transaction.', - 'Can not create a new order' => 'Cannot create a new order', - 'Can not find an order to pay.' => 'Cannot find an order to pay.', - 'Can not find enabled email.' => 'Cannot find enabled email.', - 'Can not find order' => 'Cannot find order', - 'Can not find order.' => 'Cannot find order.', - 'Can not find the transaction to refund' => 'Cannot find the transaction to refund', - 'Can not move between these inventory types.' => 'Cannot move between these inventory types.', - 'Can not refund amount greater than the remaining amount' => 'Cannot refund amount greater than the remaining amount', - 'Cancel subscription' => 'Cancel subscription', - 'Cancel with gateway now' => 'Cancel with gateway now', - 'Cancel' => 'Cancel', - 'Cancellation date' => 'Cancellation date', - 'Cancellation' => 'Cancellation', - 'Cannot switch plans for this subscription.' => 'Cannot switch plans for this subscription.', - 'Can’t preview this email.' => 'Can’t preview this email.', - 'Capture payment' => 'Capture payment', - 'Capture' => 'Capture', - 'Card Holder' => 'Card Holder', - 'Card Number' => 'Card Number', - 'Card' => 'Card', - 'Cart Recovery Link' => 'Cart Recovery Link', - 'Cart forgotten.' => 'Cart forgotten.', - 'Cart updated.' => 'Cart updated.', - 'Cart {number}' => 'Cart {number}', - 'Catalog Pricing Rule' => 'Catalogue Pricing Rule', - 'Catalog pricing rule description.' => 'Catalogue pricing rule description.', - 'Catalog pricing rule saved.' => 'Catalogue pricing rule saved.', - 'Catalog pricing rules deleted.' => 'Catalogue pricing rules deleted.', - 'Catalog pricing rules updated.' => 'Catalogue pricing rules updated.', - 'Categories Relationship Type' => 'Categories Relationship Type', - 'Categories' => 'Categories', - 'Category Rate Overrides' => 'Category Rate Overrides', - 'Centimeters (cm)' => 'Centimetres (cm)', - 'Changing this value may affect your ability to refund existing transactions.' => 'Changing this value may affect your ability to refund existing transactions.', - 'Choose a color to represent the order’s status' => 'Choose a colour to represent the order’s status', - 'Choose a new customer' => 'Choose a new customer', - 'Choose adjustment values to include when calculating the product revenue total.' => 'Choose adjustment values to include when calculating the product revenue total.', - 'Choose the currency’s ISO code.' => 'Choose the currency’s ISO code.', - 'Choose the destination inventory location for the existing on hand stock.' => 'Choose the destination inventory location for the existing on hand stock.', - 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Choose which sites this product type should be available in, and configure the site-specific settings.', - 'City' => 'City', - 'Clear counter' => 'Clear counter', - 'Clear notices' => 'Clear notices', - 'Close' => 'Close', - 'Code' => 'Code', - 'Collated PDF' => 'Collated PDF', - 'Color' => 'Colour', - 'Commerce Products' => 'Commerce Products', - 'Commerce Settings' => 'Commerce Settings', - 'Commerce Variants' => 'Commerce Variants', - 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Commerce email “{email}” could not be sent for order “{order}”.', - 'Commerce order exports' => 'Commerce order exports', - 'Commerce' => 'Commerce', - 'Committed' => 'Committed', - 'Completed Email' => 'Completed Email', - 'Completed' => 'Completed', - 'Completing order failed.' => 'Failed to complete order.', - 'Condition' => 'Condition', - 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availabililty early or if there are common conditions to all rules for this method.', - 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.', - 'Conditions' => 'Conditions', - 'Contains Purchasables' => 'Contains Purchasables', - 'Control Panel Settings' => 'Control Panel Settings', - 'Control panel' => 'Control panel', - 'Conversion Rate' => 'Conversion Rate', - 'Converted Price' => 'Converted Price', - 'Copied!' => 'Copied!', - 'Copy the URL' => 'Copy the URL', - 'Copy to {location}' => 'Copy to {location}', - 'Copy' => 'Copy', - 'Costs' => 'Costs', - 'Could not archive gateway.' => 'Could not archive gateway.', - 'Could not cancel “{reference}”.' => 'Could not cancel “{reference}”.', - 'Could not create the payment source.' => 'Could not create the payment source.', - 'Could not delete shipping rule' => 'Could not delete shipping rule', - 'Could not delete shipping zone' => 'Could not delete shipping zone', - 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.', - 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.', - 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.', - 'Could not find the email or template.' => 'Could not find the email or template.', - 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}', - 'Could not reactivate “{reference}”.' => 'Could not reactivate “{reference}”.', - 'Could not send email' => 'Could not send email', - 'Could not switch “{reference}” to “{plan}”.' => 'Could not switch “{reference}” to “{plan}”.', - 'Could not update orders address.' => 'Could not update orders address.', - 'Couldn’t archive Line Item Status.' => 'Couldn’t archive Line Item Status.', - 'Couldn’t archive Order Status.' => 'Couldn’t archive Order Status.', - 'Couldn’t capture transaction.' => 'Couldn’t capture transaction.', - 'Couldn’t capture transaction: {message}' => 'Couldn’t capture transaction: {message}', - 'Couldn’t delete email.' => 'Couldn’t delete email.', - 'Couldn’t delete the payment source.' => 'Couldn’t delete the payment source.', - 'Couldn’t get order.' => 'Couldn’t get order.', - 'Couldn’t recalculate order.' => 'Couldn’t recalculate order.', - 'Couldn’t refund transaction.' => 'Couldn’t refund transaction.', - 'Couldn’t refund transaction: {message}' => 'Couldn’t refund transaction: {message}', - 'Couldn’t reorder Line Item Statuses.' => 'Couldn’t reorder Line Item Statuses.', - 'Couldn’t reorder Order Statuses.' => 'Couldn’t reorder Order Statuses.', - 'Couldn’t reorder PDFs.' => 'Couldn’t reorder PDFs.', - 'Couldn’t reorder discounts.' => 'Couldn’t reorder discounts.', - 'Couldn’t reorder gateways.' => 'Couldn’t reorder gateways.', - 'Couldn’t reorder plans.' => 'Couldn’t reorder plans.', - 'Couldn’t reorder rules.' => 'Couldn’t reorder rules.', - 'Couldn’t reorder sale.' => 'Couldn’t reorder sale.', - 'Couldn’t reorder sales.' => 'Couldn’t reorder sales.', - 'Couldn’t reorder statuses.' => 'Couldn’t reorder statuses.', - 'Couldn’t reorder stores.' => 'Couldn’t reorder stores.', - 'Couldn’t save PDF.' => 'Couldn’t save PDF.', - 'Couldn’t save catalog pricing rule.' => 'Couldn’t save catalog pricing rule.', - 'Couldn’t save currency.' => 'Couldn’t save currency.', - 'Couldn’t save discount.' => 'Couldn’t save discount.', - 'Couldn’t save email.' => 'Couldn’t save email.', - 'Couldn’t save gateway.' => 'Couldn’t save gateway.', - 'Couldn’t save inventory location.' => 'Couldn’t save inventory location.', - 'Couldn’t save line item status.' => 'Couldn’t save line item status.', - 'Couldn’t save order fields.' => 'Couldn’t save order fields.', - 'Couldn’t save order status.' => 'Couldn’t save order status.', - 'Couldn’t save order.' => 'Couldn’t save order.', - 'Couldn’t save product type.' => 'Couldn’t save product type.', - 'Couldn’t save sale.' => 'Couldn’t save sale.', - 'Couldn’t save settings.' => 'Couldn’t save settings.', - 'Couldn’t save shipping category.' => 'Couldn’t save shipping category.', - 'Couldn’t save shipping method.' => 'Couldn’t save shipping method.', - 'Couldn’t save shipping rule.' => 'Couldn’t save shipping rule.', - 'Couldn’t save shipping zone.' => 'Couldn’t save shipping zone.', - 'Couldn’t save store.' => 'Could not save store.', - 'Couldn’t save subscription fields.' => 'Couldn’t save subscription fields.', - 'Couldn’t save subscription plan.' => 'Couldn’t save subscription plan.', - 'Couldn’t save subscription.' => 'Couldn’t save subscription.', - 'Couldn’t save tax category.' => 'Couldn’t save tax category.', - 'Couldn’t save tax rate.' => 'Couldn’t save tax rate.', - 'Couldn’t save tax zone.' => 'Couldn’t save tax zone.', - 'Couldn’t save transfer fields.' => 'Couldn’t save transfer fields.', - 'Couldn’t update catalog pricing rule statuses.' => 'Couldn’t update catalogue pricing rules status.', - 'Couldn’t update status.' => 'Couldn’t update status.', - 'Couldn’t updated sales status.' => 'Couldn’t update sales status.', - 'Country Code of Origin' => 'Country Code of Origin', - 'Country List' => 'Country List', - 'Country not allowed.' => 'Country not allowed.', - 'Country' => 'Country', - 'Coupon Code' => 'Coupon Code', - 'Coupon can not apply discount to this order due to address mismatch.' => 'Coupon can not apply discount to this order due to address mismatch.', - 'Coupon can not apply discount to this order due to customer mismatch.' => 'Coupon can not apply discount to this order due to customer mismatch.', - 'Coupon can not apply discount to this order.' => 'Coupon can not apply discount to this order.', - 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Coupon code “{code}” is already in use by discount “{name}”.', - 'Coupon codes cannot be blank.' => 'Coupon codes cannot be blank.', - 'Coupon codes must be unique.' => 'Coupon codes must be unique.', - 'Coupon format is required and must contain at least one `#`.' => 'Coupon format is required and must contain at least one # sign.', - 'Coupon not valid.' => 'Coupon not valid.', - 'Coupon removed: {explanation}' => 'Coupon removed: {explanation}', - 'Coupons' => 'Coupons', - 'Craft Commerce - Administration' => 'Craft Commerce - Administration', - 'Craft Commerce - Inventory' => 'Craft Commerce - Inventory', - 'Craft Commerce - Orders' => 'Craft Commerce - Orders', - 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Product Type - {name}', - 'Craft Commerce - Subscriptions' => 'Craft Commerce - Subscriptions', - 'Create a Discount' => 'Create a Discount', - 'Create a Subscription Plan' => 'Create a Subscription Plan', - 'Create a new PDF' => 'Create a new PDF', - 'Create a new catalog pricing rule' => 'Create a new catalogue pricing rule', - 'Create a new currency' => 'Create a new currency', - 'Create a new email' => 'Create a new email', - 'Create a new gateway' => 'Create a new gateway', - 'Create a new line item status' => 'Create a new line item status', - 'Create a new order status' => 'Create a new order status', - 'Create a new product type' => 'Create a new product type', - 'Create a new sale' => 'Create a new sale', - 'Create a new shipping category' => 'Create a new shipping category', - 'Create a new shipping method' => 'Create a new shipping method', - 'Create a new shipping rule' => 'Create a new shipping rule', - 'Create a new tax category' => 'Create a new tax category', - 'Create a new tax rate' => 'Create a new tax rate', - 'Create a product type' => 'Create a product type', - 'Create a shipping zone' => 'Create a shipping zone', - 'Create a tax zone' => 'Create a tax zone', - 'Create catalog pricing rules' => 'Create catalogue pricing rules', - 'Create customer: “{email}”' => 'Create customer: “{email}”', - 'Create discounts' => 'Create discounts', - 'Create discount…' => 'Create discount…', - 'Create rules that allow this discount to match the order.' => 'Create rules that allow this discount to match the order.', - 'Create rules that allow this discount to match the order’s billing address.' => 'Create rules that allow this discount to match the order’s billing address.', - 'Create rules that allow this discount to match the order’s customer.' => 'Create rules that allow this discount to match the order’s customer.', - 'Create rules that allow this discount to match the order’s shipping address.' => 'Create rules that allow this discount to match the order’s shipping address.', - 'Create rules that allow this gateway to match the billing address.' => 'Create rules that allow this gateway to match the billing address.', - 'Create rules that allow this gateway to match the order.' => 'Create rules that allow this gateway to match the order.', - 'Create rules that allow this gateway to match the shipping address.' => 'Create rules that allow this gateway to match the shipping address.', - 'Create sales' => 'Create sales', - 'Create sale…' => 'Create sale…', - 'Created' => 'Created', - 'Credit Card Payment Type' => 'Credit Card Payment Type', - 'Currency Code' => 'Currency Code', - 'Currency saved.' => 'Currency saved.', - 'Currency' => 'Currency', - 'Current' => 'Current', - 'Custom 1' => 'Custom 1', - 'Custom 2' => 'Custom 2', - 'Custom 3' => 'Custom 3', - 'Custom 4' => 'Custom 4', - 'Custom' => 'Custom', - 'Customer Enabled?' => 'Customer Enabled?', - 'Customer ID is required.' => 'Customer ID is required.', - 'Customer Note' => 'Customer Note', - 'Customer Notices' => 'Customer Notices', - 'Customer data' => 'Customer data', - 'Customer' => 'Customer', - 'Damaged' => 'Damaged', - 'Data shown might be outdated.' => 'Data shown might be outdated.', - 'Date Authorized' => 'Date Authorised', - 'Date Created' => 'Date Created', - 'Date First Paid' => 'Date First Paid', - 'Date Ordered' => 'Date Ordered', - 'Date Paid' => 'Date Paid', - 'Date Updated' => 'Date Updated', - 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Date from which the catalogue pricing rule will be active. Leave blank for unlimited start date', - 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Date from which the discount will be active. Leave blank for unlimited start date', - 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Date from which the sale will be active. Leave blank for unlimited start date', - 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Date when the catalogue pricing rule will be finished. Leave blank for unlimited end date', - 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Date when the discount will be finished. Leave blank for unlimited end date', - 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Date when the sale will be finished. Leave blank for unlimited end date', - 'Date' => 'Date', - 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Default - Allow the price to be negative if discounts are greater than the order value.', - 'Default Category' => 'Default Category', - 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.', - 'Default Order PDF' => 'Default Order PDF', - 'Default Per Item Rate' => 'Default Per Item Rate', - 'Default Percentage Rate' => 'Default Percentage Rate', - 'Default Status?' => 'Default Status?', - 'Default View' => 'Default View', - 'Default Weight Rate' => 'Default Weight Rate', - 'Default Zone' => 'Default Zone', - 'Default status?' => 'Default status?', - 'Default to this tax zone when no billing address is set' => 'Default to this tax zone when no billing address is set', - 'Default to this tax zone when no shipping address is set' => 'Default to this tax zone when no shipping address is set', - 'Default variant updated.' => 'Default variant updated.', - 'Default' => 'Default', - 'Default?' => 'Default?', - 'Delete catalog pricing rules' => 'Delete catalogue pricing rules', - 'Delete discounts' => 'Delete discounts', - 'Delete orders' => 'Delete orders', - 'Delete sales' => 'Delete sales', - 'Delete' => 'Delete', - 'Deleting the {location} location.' => 'Deleting the {location} location.', - 'Describe this rule.' => 'Describe this rule.', - 'Describe this shipping zone.' => 'Describe this shipping zone.', - 'Describe this tax zone.' => 'Describe this tax zone.', - 'Description' => 'Description', - 'Destination Inventory Location' => 'Destination Inventory Location', - 'Destination' => 'Destination', - 'Details' => 'Details', - 'Dimension Unit' => 'Dimension Unit', - 'Dimensions' => 'Dimensions', - 'Disabled' => 'Disabled', - 'Disallow' => 'Disallow', - 'Discount all line items' => 'Discount all line items', - 'Discount description.' => 'Discount description.', - 'Discount is not allowed for the order' => 'Discount is not allowed for order', - 'Discount is out of date.' => 'Discount is out of date.', - 'Discount saved.' => 'Discount saved.', - 'Discount the matching items only' => 'Discount matching items only', - 'Discount use has reached its limit.' => 'Discount use has reached its limit.', - 'Discount' => 'Discount', - 'Discounted Item Subtotal' => 'Discounted Item Subtotal', - 'Discounted Items' => 'Discounted Items', - 'Discounts deleted.' => 'Discounts deleted.', - 'Discounts reordered.' => 'Discounts reordered.', - 'Discounts updated.' => 'Discounts updated.', - 'Discounts' => 'Discounts', - 'Disqualify with valid business tax ID?' => 'Disqualify with valid business tax ID?', - 'Do not apply subsequent matching sales beyond applying this sale.' => 'Do not apply subsequent matching sales beyond applying this sale.', - 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Do not apply this rate if the order address has any of the selected valid business tax IDs.', - 'Do not attach a PDF to this email' => 'Do not attach a PDF to this email', - 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.', - 'Donation can not be zero.' => 'Donation cannot be zero.', - 'Donation needs to be an amount.' => 'Donation needs to be an amount.', - 'Donation settings saved.' => 'Donation settings saved.', - 'Donation' => 'Donation', - 'Donations' => 'Donations', - 'Done' => 'Done', - 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Don’t apply any subsequent discounts to an order if this discount is applied', - 'Download PDF' => 'Download PDF', - 'Download PDF…' => 'Download PDF…', - 'Download Type' => 'Download Type', - 'Download' => 'Download', - 'Draft' => 'Draft', - 'Dummy gateway payment failed.' => 'Dummy gateway payment failed.', - 'Duplicate options exist' => 'Duplicate options exist', - 'Duration' => 'Duration', - 'EU VAT ID' => 'EU VAT ID', - 'Edit address' => 'Edit address', - 'Edit adjustments' => 'Edit adjustments', - 'Edit catalog pricing rules' => 'Edit catalogue pricing rules', - 'Edit discounts' => 'Edit discounts', - 'Edit options' => 'Edit options', - 'Edit orders' => 'Edit orders', - 'Edit sales' => 'Edit sales', - 'Edit' => 'Edit', - 'Effect' => 'Effect', - 'Either (Default) - The relationship field is on the purchasable or the category' => 'Either (Default) - The relationship field is on the purchasable or the category', - 'Either way' => 'Either way', - 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}', - 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.', - 'Email Subject' => 'Email Subject', - 'Email error. No email address found for order. Order: “{order}”' => 'Email error. No email address found for order. Order: “{order}”', - 'Email is not enabled.' => 'Email is not enabled.', - 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.', - 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email required to make payments on a completed order.' => 'Email required to make payments on a completed order.', - 'Email saved.' => 'Email saved.', - 'Email sent' => 'Email sent', - 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.', - 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email unavailable.' => 'Email unavailable.', - 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}', - 'Email “{email}” for order {order} was cancelled.' => 'Email “{email}” for order {order} was cancelled.', - 'Email' => 'Email', - 'Emails' => 'Emails', - 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.', - 'Enable structure for products of this type' => 'Enable structure for products of this type', - 'Enable this discount' => 'Enable this discount', - 'Enable this rule' => 'Enable this rule', - 'Enable this sale' => 'Enable this sale', - 'Enable this shipping method on the front end' => 'Enable this shipping method on the front end', - 'Enable this shipping rule' => 'Enable this shipping rule', - 'Enable this tax rate' => 'Enable this tax rate', - 'Enabled for customers to select during checkout?' => 'Enabled for customers to select during checkout?', - 'Enabled for customers to select?' => 'Enabled for customers to select?', - 'Enabled' => 'Enabled', - 'Enabled?' => 'Enabled?', - 'End Date' => 'End Date', - 'Enter SKU' => 'Enter SKU', - 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Enter a human-friendly name for this tax rate to be used in the control panel.', - 'Enter a percentage like {ex1} or {ex2}.' => 'Enter a percentage like {ex1} or {ex2}.', - 'Enter coupon code' => 'Enter coupon code', - 'Enter reference' => 'Enter reference', - 'Error refunding transaction: {transactionHash}' => 'Error refunding transaction: {transactionHash}', - 'Every new store must be assigned to at least one site.' => 'Every new store must be assigned to at least one site.', - 'Everywhere' => 'Everywhere', - 'Example' => 'Example', - 'Exclude this discount for products that are already on promotion' => 'Exclude this discount for products that are already on promotion', - 'Expired Link' => 'Expired Link', - 'Expired' => 'Expired', - 'Expiry Date' => 'Expiry Date', - 'Expiry date' => 'Expiry date', - 'Expiry' => 'Expiry', - 'Failed to receive transfer: {error}' => 'Failed to receive transfer: {error}', - 'Failed to send email. Please try again.' => 'Failed to send email. Please try again.', - 'Failed to start' => 'Failed to start', - 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Failed to update {num, plural, one {}=1{order status} other{order statuses}}.', - 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Failed updating order status on {num, plural, one {}=1{order} other{orders}}.', - 'Feet (ft)' => 'Feet (ft)', - 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Filtering conditions which describe which orders this rule is applicable to. Write 0 to skip a condition.', - 'First Name' => 'First Name', - 'Flat Amount Off Order' => 'Flat Amount Off Order', - 'Flat Order Discount Amount Off' => 'Flat Order Discount Amount Off', - 'Free Order Payment Strategy' => 'Free Order Payment Strategy', - 'Free Shipping' => 'Free Shipping', - 'Free orders are processed by the payment gateway' => 'Free orders are processed by the payment gateway', - 'Free orders complete immediately' => 'Free orders complete immediately', - 'Free shipping can only be for whole order or matching items, not both.' => 'Free shipping can only be for either the whole order or matching items, not both.', - 'From Name' => 'From Name', - 'Fulfill' => 'Fulfill', - 'Fulfilled' => 'Fulfilled', - 'Fulfillment' => 'Fulfillment', - 'Full Name' => 'Full Name', - 'Gateway Code' => 'Gateway Code', - 'Gateway Message' => 'Gateway Message', - 'Gateway Reference' => 'Gateway Reference', - 'Gateway Response' => 'Gateway Response', - 'Gateway doesn’t support authorize' => 'Gateway doesn’t support authorise', - 'Gateway doesn’t support partial refunds.' => 'Gateway doesn’t support partial refunds.', - 'Gateway doesn’t support purchase' => 'Gateway doesn’t support purchase', - 'Gateway doesn’t support refunds.' => 'Gateway doesn’t support refunds.', - 'Gateway saved.' => 'Gateway saved.', - 'Gateway' => 'Gateway', - 'Gateways reordered.' => 'Gateways reordered.', - 'Gateways' => 'Gateways', - 'General Settings' => 'General Settings', - 'General' => 'General', - 'Generate' => 'Generate', - 'Generated Coupon Format' => 'Generated Coupon Format', - 'Grams (g)' => 'Grams (g)', - 'Groups for which this sale will be applicable to.' => 'Groups for which this sale will be applicable.', - 'HTML Email Template Path' => 'HTML Email Template Path', - 'Handle' => 'Handle', - 'Harmonized System Code' => 'Harmonized System Code', - 'Has Admin Notices' => 'Has Admin Notices', - 'Has Emails?' => 'Has Emails?', - 'Has Free Shipping' => 'Has Free Shipping', - 'Has Orders' => 'Has Orders', - 'Has Purchasable' => 'Has Purchasable', - 'Has Variants?' => 'Has Variants?', - 'Height ({unit})' => 'Height ({unit})', - 'Height' => 'Height', - 'Hide snapshot' => 'Hide snapshot', - 'History' => 'History', - 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).', - 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.', - 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.', - 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.', - 'How products should be labeled within the control panel.' => 'How products should be labelled within the control panel.', - 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).', - 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}', - 'How this shipping method will be referred to in templates and forms.' => 'How this shipping method will be referred to in templates and forms.', - 'How variants should be labeled within the control panel.' => 'How variants should be labelled within the control panel.', - 'How you’ll refer to this PDF in the templates.' => 'How you’ll refer to this PDF in the templates.', - 'How you’ll refer to this product type in the templates.' => 'How you’ll refer to this product type in the templates.', - 'How you’ll refer to this shipping category in the templates.' => 'How you’ll refer to this shipping category in the templates.', - 'How you’ll refer to this status in the templates.' => 'How you’ll refer to this status in the templates.', - 'How you’ll refer to this subscription plan in the templates.' => 'How you’ll refer to this subscription plan in the templates.', - 'How you’ll refer to this tax category in the templates.' => 'How you’ll refer to this tax category in the templates.', - 'ID' => 'ID', - 'IP Address' => 'IP Address', - 'If disabled, this PDF will not be available or sent with emails.' => 'If disabled, this PDF will not be available or sent with emails.', - 'If disabled, this email will not send.' => 'If disabled, this email will not send.', - 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.', - 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'If set to Authorise Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.', - 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.', - 'Ignore Promotions?' => 'Ignore Promotions?', - 'Ignore previous matching sales if this sale matches.' => 'Ignore previous matching sales if this sale matches.', - 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignore promotional prices when this discount is applied to matching line items', - 'Inactive Carts' => 'Inactive Carts', - 'Inches (in)' => 'Inches (in)', - 'Include built-in line item tax.' => 'Include built-in line item tax.', - 'Include in price?' => 'Include in price?', - 'Include line item discounts.' => 'Include line item discounts.', - 'Include line item shipping costs.' => 'Include line item shipping costs.', - 'Include separate line item tax.' => 'Include separate line item tax.', - 'Included in price?' => 'Included in price?', - 'Included' => 'Included', - 'Incoming transfer from Transfer ID: ' => 'Incoming transfer from Transfer ID: ', - 'Incoming' => 'Incoming', - 'Info' => 'Info', - 'Information linked?' => 'Information linked?', - 'Information' => 'Information', - 'Invalid JSON' => 'Invalid JSON', - 'Invalid Order ID' => 'Invalid Order ID', - 'Invalid VAT ID.' => 'Invalid VAT ID.', - 'Invalid condition syntax' => 'Invalid condition syntax', - 'Invalid email.' => 'Invalid email.', - 'Invalid formula syntax' => 'Invalid formula syntax', - 'Invalid gateway: {value}' => 'Invalid gateway: {value}', - 'Invalid inventory movements.' => 'Invalid inventory movements.', - 'Invalid order condition syntax.' => 'Invalid order condition syntax.', - 'Invalid payment or order. Please review.' => 'Invalid payment or order. Please review.', - 'Invalid payment source ID: {value}' => 'Invalid payment source ID: {value}', - 'Invalid store.' => 'Invalid store.', - 'Invalid user.' => 'Invalid user.', - 'Inventory Item' => 'Inventory Item', - 'Inventory Location' => 'Inventory Location', - 'Inventory Locations' => 'Inventory Locations', - 'Inventory Tracked' => 'Inventory Tracked', - 'Inventory Transfers' => 'Inventory Transfers', - 'Inventory could not be set.' => 'Inventory could not be set.', - 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'Inventory location has committed stock, the order(s) must first be fulfilled.', - 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Inventory location has incoming stock, the transfer(s) must first be completed.', - 'Inventory location is already deactivated.' => 'Inventory location is already deactivated.', - 'Inventory location saved.' => 'Inventory location saved.', - 'Inventory locations not saved.' => 'Inventory locations not saved.', - 'Inventory movement could not be saved.' => 'Inventory movement could not be saved.', - 'Inventory movement saved.' => 'Inventory movement saved.', - 'Inventory updated.' => 'Inventory updated.', - 'Inventory was not updated.' => 'Inventory was not updated.', - 'Inventory' => 'Inventory', - 'Invoice amount' => 'Invoice amount', - 'Invoice date' => 'Invoice date', - 'Is Promotable' => 'Is Promotable', - 'Is Promotional Price?' => 'Is Promotional Price?', - 'Is Shippable' => 'Is Shippable', - 'Is Taxable' => 'Is Taxable', - 'Item Rates' => 'Item Rates', - 'Item Subtotal' => 'Item Subtotal', - 'Item Total' => 'Item Total', - 'Item' => 'Item', - 'Items' => 'Items', - 'Kilograms (kg)' => 'Kilograms (kg)', - 'Label' => 'Label', - 'Landscape' => 'Landscape', - 'Language' => 'Language', - 'Last Name' => 'Last Name', - 'Last Updated' => 'Last Updated', - 'Leave a category rate override blank to use the rate from above.' => 'Leave a category rate override blank to use the rate from above.', - 'Leave blank for unlimited uses.' => 'Leave blank for unlimited uses.', - 'Leave blank if products don’t have URLs' => 'Leave blank if products don’t have URLs', - 'Leave gateway subscription as-is' => 'Leave gateway subscription as-is', - 'Length ({unit})' => 'Length ({unit})', - 'Length' => 'Length', - 'Let each product choose which sites it should be saved to' => 'Let each product choose which sites it should be saved to', - 'Limit which orders this discount applies to based on its line items.' => 'Limit which orders this discount applies to based on its line items.', - 'Limit which purchasables this sale applies to.' => 'Limit which purchasables this sale applies to.', - 'Limit' => 'Limit', - 'Line Item Statuses' => 'Line Item Statuses', - 'Line Item' => 'Line Item', - 'Line Items' => 'Line Items', - 'Line item price (minus discounts)' => 'Line item price (minus discounts)', - 'Line item shipping cost' => 'Line item shipping cost', - 'Line item statuses reordered.' => 'Line item statuses reordered.', - 'Link Duration' => 'Link Duration', - 'Link Sent' => 'Link Sent', - 'Link to a product' => 'Link to a product', - 'Link to a variant' => 'Link to a variant', - 'Link' => 'Link', - 'Live' => 'Live', - 'Location' => 'Location', - 'Locations that should be available for previewing products in this product type.' => 'Locations that should be available for previewing products in this product type.', - 'MM' => 'MM', - 'Make a payment' => 'Make a payment', - 'Make this the primary store' => 'Make this the primary store', - 'Manage Inventory' => 'Manage Inventory', - 'Manage donation settings' => 'Manage donation settings', - 'Manage general store settings' => 'Manage general store settings', - 'Manage inventory locations' => 'Manage inventory locations', - 'Manage inventory stock levels' => 'Manage inventory stock levels', - 'Manage inventory transfers' => 'Manage inventory transfers', - 'Manage orders' => 'Manage orders', - 'Manage payment currencies' => 'Manage payment currencies', - 'Manage promotions' => 'Manage promotions', - 'Manage shipping' => 'Manage shipping', - 'Manage store settings' => 'Manage store settings', - 'Manage subscription plans' => 'Manage subscription plans', - 'Manage subscription' => 'Manage subscription', - 'Manage subscriptions' => 'Manage subscriptions', - 'Manage taxes' => 'Manage taxes', - 'Manage' => 'Manage', - 'Mark as Pending' => 'Mark as Pending', - 'Mark as completed' => 'Mark as completed', - 'Match Billing Address' => 'Match Billing Address', - 'Match Customer' => 'Match Customer', - 'Match Order' => 'Match Order', - 'Match Orders' => 'Match Orders', - 'Match Product' => 'Match Product', - 'Match Purchasable' => 'Match Purchasable', - 'Match Shipping Address' => 'Match Shipping Address', - 'Match Variant' => 'Match Variant', - 'Matching Items' => 'Matching Items', - 'Max Qty' => 'Max Qty', - 'Max Uses' => 'Max Uses', - 'Max Variants' => 'Max Variants', - 'Max quantity must greater than min.' => 'Max quantity must greater than min.', - 'Maximum Purchase Quantity' => 'Maximum Purchase Quantity', - 'Maximum Total Shipping Cost' => 'Maximum Total Shipping Cost', - 'Maximum allowed quantity' => 'Maximum allowed quantity', - 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.', - 'Maximum order quantity for this item is {num}.' => 'Maximum order quantity for this item is {num}.', - 'Message' => 'Message', - 'Meters (m)' => 'Metres (m)', - 'Millimeters (mm)' => 'Millimetres (mm)', - 'Min Qty' => 'Min Qty', - 'Min quantity must be less than max.' => 'Min quantity must be less than max.', - 'Minimum Purchase Quantity' => 'Minimum Purchase Quantity', - 'Minimum Total Price Strategy' => 'Minimum Total Price Strategy', - 'Minimum Total Shipping Cost' => 'Minimum Total Shipping Cost', - 'Minimum allowed quantity' => 'Minimum allowed quantity', - 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Minimum number of matching items that need to be ordered for this discount to apply.', - 'Minimum order quantity for this item is {num}.' => 'Minimum order quantity for this item is {num}.', - 'Missing Gateway' => 'Missing Gateway', - 'Missing a default inventory location.' => 'Missing a default inventory location.', - 'Move Inventory' => 'Move Inventory', - 'Move To' => 'Move To', - 'Move {qty} from {fromType} to {toType}' => 'Move {qty} from {fromType} to {toType}', - 'Move' => 'Move', - 'Movement from deactivated inventory location' => 'Movement from deactivated inventory location', - 'Movement' => 'Movement', - 'Must have at least one variant.' => 'Must have at least one variant.', - 'Name Field' => 'Name Field', - 'Name' => 'Name', - 'New Customer' => 'New Customer', - 'New Customers' => 'New Customers', - 'New Order' => 'New order', - 'New PDF' => 'New PDF', - 'New address' => 'New address', - 'New catalog pricing rule' => 'New catalogue pricing rule', - 'New currency' => 'New currency', - 'New discount' => 'New discount', - 'New email' => 'New email', - 'New gateway' => 'New gateway', - 'New line item status' => 'New line item status', - 'New line items get this status by default when the order is completed' => 'New line items get this status by default when the order is completed', - 'New location' => 'New location', - 'New order status' => 'New order status', - 'New orders get this status by default' => 'New orders get this status by default', - 'New product type' => 'New product type', - 'New product' => 'New product', - 'New product, choose a type' => 'New product, choose a type', - 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'New products default to the first tax category available to them. If none are available, this category will be used.', - 'New sale' => 'New sale', - 'New shipping category' => 'New shipping category', - 'New shipping method' => 'New shipping method', - 'New shipping rule' => 'New shipping rule', - 'New shipping zone' => 'New shipping zone', - 'New subscription plan' => 'New subscription plan', - 'New tax category' => 'New tax category', - 'New tax rate' => 'New tax rate', - 'New tax zone' => 'New tax zone', - 'New transfer' => 'New transfer', - 'New {productType} product' => 'New {productType} product', - 'New' => 'New', - 'Next payment' => 'Next payment', - 'No Address' => 'No Address', - 'No PDFs exist yet.' => 'No PDFs exist yet.', - 'No access given to any specific store management features.' => 'No access given to any specific store management features.', - 'No additional payment currencies exist yet.' => 'No additional payment currencies exist yet.', - 'No address' => 'No address', - 'No billing address' => 'No billing address', - 'No catalog pricing rule exists with the ID “{id}”' => 'No catalogue pricing rule exists with the ID “{id}”', - 'No catalog pricing rules exist yet.' => 'No catalogue pricing rules exist yet.', - 'No currency exists with the ID “{id}”' => 'No currency exists with the ID “{id}”', - 'No customer email address exists on this cart.' => 'No customer email address exists on this cart.', - 'No description' => 'No description', - 'No discount exists with the ID “{id}”' => 'No discount exists with the ID “{id}”', - 'No discounts exist yet.' => 'No discounts exist yet.', - 'No donation amount supplied.' => 'No donation amount supplied.', - 'No emails exist yet.' => 'No emails exist yet.', - 'No inventory changes made.' => 'No inventory changes made.', - 'No inventory found.' => 'No inventory found.', - 'No inventory movements made.' => 'No inventory movements made.', - 'No inventory transactions for this location.' => 'No inventory transactions for this location.', - 'No new customer selected.' => 'No new customer selected.', - 'No order history exists with the ID “{id}”' => 'No order history exists with the ID “{id}”', - 'No order status history items will exist until the cart becomes an order.' => 'No order status history items will exist until the cart becomes an order.', - 'No payment source exists with the ID “{id}”' => 'No payment source exists with the ID “{id}”', - 'No private Note.' => 'No private Note.', - 'No product available.' => 'No product available.', - 'No product types exist yet.' => 'No product types exist yet.', - 'No purchasable available.' => 'No purchasable available.', - 'No sale exists with the ID “{id}”' => 'No sale exists with the ID “{id}”', - 'No sales exist yet.' => 'No sales exist yet.', - 'No shipping address' => 'No shipping address', - 'No shipping category exists with the ID “{id}”' => 'No shipping category exists with the ID “{id}”', - 'No shipping method exists with the ID “{id}”' => 'No shipping method exists with the ID “{id}”', - 'No shipping rule exists with the ID “{id}”' => 'No shipping rule exists with the ID “{id}”', - 'No shipping rules exist yet.' => 'No shipping rules exist yet.', - 'No shipping zone exists with the ID “{id}”' => 'No shipping zone exists with the ID “{id}”', - 'No stats available.' => 'No stats available.', - 'No subscription plan exists with the ID “{id}”' => 'No subscription plan exists with the ID “{id}”', - 'No subscription plans exist yet.' => 'No subscription plans exist yet.', - 'No tax category exists with the ID “{id}”' => 'No tax category exists with the ID “{id}”', - 'No tax rate exists with the ID “{id}”' => 'No tax rate exists with the ID “{id}”', - 'No tax zone exists with the ID “{id}”' => 'No tax zone exists with the ID “{id}”', - 'No transactions exist.' => 'No transactions exist.', - 'No user authenticated.' => 'No user authenticated.', - 'No' => 'No', - 'None on hand' => 'None on hand', - 'None' => 'None', - 'Not a valid address type' => 'Not a valid address type', - 'Not a valid credit card number.' => 'Not a valid credit card number.', - 'Not all SKUs are unique.' => 'Not all SKUs are unique.', - 'Note' => 'Note', - 'Notes' => 'Notes', - 'Number of Coupons' => 'Number of Coupons', - 'Number' => 'Number', - 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Of the enabled sites above, which sites should products in this product type be saved to?', - 'On Hand' => 'On Hand', - 'Only allow this gateway to be used for zero value orders?' => 'Only allow this gateway to be used for zero value orders?', - 'Only match certain purchasables…' => 'Only match certain purchasables…', - 'Only match purchasables related to…' => 'Only match purchasables related to…', - 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Only orders with the following order statuses will be included. Leave blank to include all statuses.', - 'Only save product to the site they were created in' => 'Only save product to the site they were created in', - 'Options' => 'Options', - 'Order Condition Formula' => 'Order Condition Formula', - 'Order Description Format' => 'Order Description Format', - 'Order Details' => 'Order Details', - 'Order Fields' => 'Order Fields', - 'Order PDF Download Link' => 'Order PDF Download Link', - 'Order PDF Filename Format' => 'Order PDF Filename Format', - 'Order Reference Number Format' => 'Order Reference Number Format', - 'Order Settings' => 'Order Settings', - 'Order Site' => 'Order Site', - 'Order Status description.' => 'Order Status description.', - 'Order Status' => 'Order Status', - 'Order Statuses' => 'Order Statuses', - 'Order can not be empty.' => 'Order cannot be empty.', - 'Order count' => 'Order count', - 'Order customer data removed.' => 'Order customer data removed.', - 'Order deleted.' => 'Order deleted.', - 'Order fields saved.' => 'Order fields saved.', - 'Order not found.' => 'Order not found.', - 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.', - 'Order recalculated.' => 'Order recalculated.', - 'Order status saved.' => 'Order status saved.', - 'Order statuses reordered.' => 'Order statuses reordered.', - 'Order total shipping cost' => 'Order total shipping cost', - 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)', - 'Order' => 'Order', - 'Orders (Legacy)' => 'Orders (Legacy)', - 'Orders deleted.' => 'Orders deleted.', - 'Orders not restored.' => 'Orders not restored.', - 'Orders restored.' => 'Orders restored.', - 'Orders' => 'Orders', - 'Organization Name' => 'Organisation Name', - 'Organization Tax ID' => 'Organisation Tax ID', - 'Origin and destination cannot be the same.' => 'Origin and destination cannot be the same.', - 'Origin' => 'Origin', - 'Original Price' => 'Original Price', - 'Original price' => 'Original price', - 'Original promotional price' => 'Original promotional price', - 'Other Languages' => 'Other Languages', - 'Other countries' => 'Other countries', - 'Outgoing transfer from Transfer ID: ' => 'Outgoing transfer from Transfer ID: ', - 'Overpaid' => 'Overpaid', - 'Overrides previous?' => 'Overrides previous?', - 'PDF Attachment' => 'PDF Attachment', - 'PDF Template Path' => 'PDF Template Path', - 'PDF saved.' => 'PDF saved.', - 'PDF' => 'PDF', - 'PDFs & Emails' => 'PDFs & Emails', - 'PDFs' => 'PDFs', - 'Paid Amount' => 'Paid Amount', - 'Paid Status' => 'Paid Status', - 'Paid' => 'Paid', - 'Paper Orientation' => 'Paper Orientation', - 'Paper Size' => 'Paper Size', - 'Partial payment not allowed.' => 'Partial payment not allowed.', - 'Partial' => 'Partial', - 'Past year' => 'Past year', - 'Past {num} days' => 'Past {num} days', - 'Pay {amount} of {currency} on the order.' => 'Pay {amount} {currency} on the order.', - 'Pay' => 'Pay', - 'Payment Amount' => 'Payment Amount', - 'Payment Currencies' => 'Payment Currencies', - 'Payment Gateway' => 'Payment Gateway', - 'Payment Method' => 'Payment Method', - 'Payment error: {message}' => 'Payment error: {message}', - 'Payment method issue' => 'Payment method issue', - 'Payment source created.' => 'Payment source created.', - 'Payment source deleted.' => 'Payment source deleted.', - 'Payments' => 'Payments', - 'Pending' => 'Pending', - 'Per Email Address Discount Limit' => 'Per Email Address Discount Limit', - 'Per Item Amount Off' => 'Per Item Amount Off', - 'Per Item Discount' => 'Per Item Discount', - 'Per Item Percentage Off' => 'Per Item Percentage Off', - 'Per Item Rate' => 'Per Item Rate', - 'Per User Discount Limit' => 'Per User Discount Limit', - 'Percentage Rate' => 'Percentage Rate', - 'Phone (Alt)' => 'Phone (Alt)', - 'Phone' => 'Phone', - 'Pick a plan' => 'Pick a plan', - 'Plain Text Email Template Path' => 'Plain Text Email Template Path', - 'Plan' => 'Plan', - 'Plans reordered.' => 'Plans reordered.', - 'Portrait' => 'Portrait', - 'Post Date' => 'Post Date', - 'Postal Code Formula' => 'Postal Code Formula', - 'Pounds (lb)' => 'Pounds (lb)', - 'Preview' => 'Preview', - 'Previous Status' => 'Previous Status', - 'Price' => 'Price', - 'Prices' => 'Prices', - 'Pricing Rules' => 'Pricing Rules', - 'Pricing jobs are currently running.' => 'Pricing jobs are currently running.', - 'Pricing' => 'Pricing', - 'Primary Billing Address' => 'Primary Billing Address', - 'Primary Shipping Address' => 'Primary Shipping Address', - 'Primary payment source updated.' => 'Primary payment source updated.', - 'Primary' => 'Primary', - 'Private Note' => 'Private Note', - 'Product Fields' => 'Product Fields', - 'Product ID is required.' => 'Product ID is required.', - 'Product Template' => 'Product Template', - 'Product Title Format' => 'Product Title Format', - 'Product Type' => 'Product Type', - 'Product Types' => 'Product Types', - 'Product URI Format' => 'Product URI Format', - 'Product Variant' => 'Product Variant', - 'Product Variants' => 'Product Variants', - 'Product type saved.' => 'Product type saved.', - 'Product type settings' => 'Product type settings', - 'Product' => 'Product', - 'Products and Variants deleted.' => 'Products and Variants deleted.', - 'Products not restored.' => 'Products not restored.', - 'Products restored.' => 'Products restored.', - 'Products' => 'Products', - 'Promotable' => 'Promotable', - 'Promotable?' => 'Promotable?', - 'Promotional Amount' => 'Promotional Amount', - 'Promotional Price' => 'Promotional Price', - 'Purchasable Categories' => 'Purchasable Categories', - 'Purchasable ID and Sale ID are required.' => 'Purchasable ID and Sale ID are required.', - 'Purchasable ID is required.' => 'Purchasable ID is required.', - 'Purchasable Type' => 'Purchasable Type', - 'Purchasable' => 'Purchasable', - 'Purchase (Authorize and Capture Immediately)' => 'Purchase (Authorise and Capture Immediately)', - 'Purchase Total' => 'Purchase Total', - 'Qty' => 'Qty', - 'Quality Control' => 'Quality Control', - 'Quantity' => 'Quantity', - 'Rate' => 'Rate', - 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Reassign {numOrders, plural, =1{order} other{orders}}', - 'Recalculate order' => 'Recalculate order', - 'Receive Inventory' => 'Receive Inventory', - 'Receive Transfer' => 'Receive Transfer', - 'Receive' => 'Receive', - 'Received' => 'Received', - 'Recent Orders' => 'Recent Orders', - 'Recipient' => 'Recipient', - 'Recover Cart' => 'Recover Cart', - 'Reduce price' => 'Reduce price', - 'Reduce the price by a fixed amount' => 'Reduce the price by a fixed amount', - 'Reduce the price by a percentage of the original price' => 'Reduce the price by a percentage of the original price', - 'Reference' => 'Reference', - 'Refresh payment history' => 'Refresh payment history', - 'Refund note' => 'Refund note', - 'Refund payment' => 'Refund payment', - 'Refund' => 'Refund', - 'Reject' => 'Reject', - 'Rejected' => 'Rejected', - 'Relationship Type' => 'Relationship Type', - 'Removable included tax rates are only allowed for the default tax zone.' => 'Removable included tax rates are only allowed for the default tax zone.', - 'Remove address' => 'Remove address', - 'Remove all shipping costs from the order' => 'Remove all shipping costs from the order', - 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below', - 'Remove customer data' => 'Remove customer data', - 'Remove from price?' => 'Remove from price?', - 'Remove shipping costs for matching items only' => 'Remove shipping costs for matching items only', - 'Remove the included tax when a valid organization tax ID is present?' => 'Remove the included tax when a valid organization tax ID is present?', - 'Remove' => 'Remove', - 'Removed' => 'Removed', - 'Repeat Customers' => 'Repeat Customers', - 'Reply To' => 'Reply To', - 'Require Billing Address At Checkout' => 'Require Billing Address At Checkout', - 'Require Coupon Code' => 'Require Coupon Code', - 'Require Shipping Address At Checkout' => 'Require Shipping Address At Checkout', - 'Require Shipping Method Selection At Checkout' => 'Require Shipping Method Selection At Checkout', - 'Require' => 'Require', - 'Reserved' => 'Reserved', - 'Reset usage' => 'Reset usage', - 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.', - 'Revenue Options' => 'Revenue Options', - 'Revenue' => 'Revenue', - 'Rule' => 'Rule', - 'Rules reordered.' => 'Rules reordered.', - 'SKU' => 'SKU', - 'Safety' => 'Safety', - 'Sale Price' => 'Sale Price', - 'Sale description.' => 'Sale description.', - 'Sale reordered.' => 'Sale reordered.', - 'Sale saved.' => 'Sale saved.', - 'Sale' => 'Sale', - 'Sales deleted.' => 'Sales deleted.', - 'Sales updated.' => 'Sales updated.', - 'Sales' => 'Sales', - 'Save and continue editing' => 'Save and continue editing', - 'Save and return to all orders' => 'Save and return to all orders', - 'Save and set rules' => 'Save and set rules', - 'Save as a new rule' => 'Save as a new rule', - 'Save product to all sites enabled for this product type' => 'Save product to all sites enabled for this product type', - 'Save product to other sites in the same site group' => 'Save product to other sites in the same site group', - 'Save product to other sites with the same language' => 'Save product to other sites with the same language', - 'Save' => 'Save', - 'Search customer…' => 'Search customer…', - 'Search inventory' => 'Search inventory', - 'Search or enter customer email…' => 'Search or enter customer email…', - 'Search…' => 'Search…', - 'See Orders' => 'See Orders', - 'Select a gateway' => 'Select a gateway', - 'Select a tax category.' => 'Select a tax category.', - 'Select a tax zone. If empty, this rate will match anywhere.' => 'Select a tax zone. If empty, this rate will match anywhere.', - 'Select address' => 'Select address', - 'Select an item' => 'Select an item', - 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Select how the catalogue pricing rule will be applied to the purchasable(s).', - 'Select how the sale will be applied to the purchasable(s).' => 'Select how the sale will be applied to the purchasable(s).', - 'Select product type' => 'Select product type', - 'Select the emails that will be sent when transitioning to this status.' => 'Select the emails that will be sent when transitioning to this status.', - 'Select what this rate should be applied to.' => 'Select what this rate should be applied to.', - 'Send Email' => 'Send Email', - 'Send to custom recipient' => 'Send to custom recipient', - 'Send to the customer' => 'Send to the customer', - 'Set Quantity' => 'Set Quantity', - 'Set default category' => 'Set default category', - 'Set default variant' => 'Set default variant', - 'Set or Adjust' => 'Set or Adjust', - 'Set price' => 'Set price', - 'Set status' => 'Set status', - 'Set the price to a flat amount' => 'Set the price to a flat amount', - 'Set the price to a percentage of the original price' => 'Set the price to a percentage of the original price', - 'Set the sale price to a flat amount' => 'Set the sale price to a flat amount', - 'Set the sale price to a percentage of the original price' => 'Set the sale price to a percentage of the original price', - 'Set to' => 'Set to', - 'Settings saved.' => 'Settings saved.', - 'Settings' => 'Settings', - 'Share cart…' => 'Share cart…', - 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.', - 'Shipping Address Zone' => 'Shipping Address Zone', - 'Shipping Address' => 'Shipping Address', - 'Shipping Business Name' => 'Shipping Business Name', - 'Shipping Categories' => 'Shipping Categories', - 'Shipping Category Conditions' => 'Shipping Category Conditions', - 'Shipping Category' => 'Shipping Category', - 'Shipping First Name' => 'Shipping First Name', - 'Shipping Full Name' => 'Shipping Full Name', - 'Shipping Last Name' => 'Shipping Last Name', - 'Shipping Method' => 'Shipping Method', - 'Shipping Methods' => 'Shipping Methods', - 'Shipping Rule' => 'Shipping Rule', - 'Shipping Zones' => 'Shipping Zones', - 'Shipping address required.' => 'Shipping address required.', - 'Shipping categories deleted.' => 'Shipping categories deleted.', - 'Shipping category saved.' => 'Shipping category saved.', - 'Shipping category updated.' => 'Shipping category updated.', - 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.', - 'Shipping method saved.' => 'Shipping method saved.', - 'Shipping methods and rules deleted.' => 'Shipping methods and rules deleted.', - 'Shipping methods updated.' => 'Shipping methods updated.', - 'Shipping rule saved.' => 'Shipping rule saved.', - 'Shipping zone saved.' => 'Shipping zone saved.', - 'Shipping' => 'Shipping', - 'Short Number' => 'Short Number', - 'Show Chart?' => 'Show Chart?', - 'Show Order Count?' => 'Show Order Count?', - 'Show all prices' => 'Show all prices', - 'Show archived gateways' => 'Show archived gateways', - 'Show order count line on chart.' => 'Show order count line on chart.', - 'Show related sales' => 'Show related sales', - 'Show rule details' => 'Show rule details', - 'Show the Dimensions and Weight fields for products of this type' => 'Show the Dimensions and Weight fields for products of this type', - 'Show the Title field for products' => 'Show the Title field for products', - 'Show the Title field for variants' => 'Show the Title field for variants', - 'Signed In' => 'Signed In', - 'Site Languages' => 'Site Languages', - 'Site store mapping saved.' => 'Site store mapping saved.', - 'Sites' => 'Sites', - 'Slug' => 'Slug', - 'Snapshot' => 'Snapshot', - 'Snapshots' => 'Snapshots', - 'Some orders restored.' => 'Some orders restored.', - 'Some products restored.' => 'Some products restored.', - 'Some variants restored.' => 'Some variants restored.', - 'Something changed with the order before payment, please review your order and submit payment again.' => 'Something changed with the order before payment, please review your order and submit payment again.', - 'Sorry, no matching options.' => 'Sorry, no matching options.', - 'Source - The purchasable relationship field is on the category' => 'Source - The purchasable relationship field is on the category', - 'Source' => 'Source', - 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)', - 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)', - 'Start Date' => 'Start Date', - 'State' => 'State', - 'Status Email Address' => 'Status Email Address', - 'Status Emails' => 'Status Emails', - 'Status History' => 'Status History', - 'Status Updated.' => 'Status Updated.', - 'Status change message' => 'Status change message', - 'Status' => 'Status', - 'Stock' => 'Stock', - 'Stops Processing?' => 'Stops Processing?', - 'Stops subsequent?' => 'Stops subsequent?', - 'Store Location' => 'Store Location', - 'Store Management' => 'Store Management', - 'Store Markets' => 'Store Markets', - 'Store Rule' => 'Store Rule', - 'Store saved.' => 'Store saved.', - 'Store' => 'Store', - 'Stores & Sites' => 'Stores & Sites', - 'Stores' => 'Stores', - 'Strategy to apply when an order is free or has a zero balance.' => 'Strategy to apply when an order is free or has a zero balance.', - 'Strategy to apply when calculating the minimum order price.' => 'Strategy to apply when calculating the minimum order price.', - 'Subject' => 'Subject', - 'Subscribing user' => 'Subscribing user', - 'Subscription Fields' => 'Subscription Fields', - 'Subscription Plans' => 'Subscription Plans', - 'Subscription Settings' => 'Subscription Settings', - 'Subscription cancelled.' => 'Subscription cancelled.', - 'Subscription date' => 'Subscription date', - 'Subscription fields saved.' => 'Subscription fields saved.', - 'Subscription for {user} to {plan} prevented by a plugin.' => 'Subscription for {user} to {plan} prevented by a plugin.', - 'Subscription plan saved.' => 'Subscription plan saved.', - 'Subscription plan' => 'Subscription plan', - 'Subscription plans' => 'Subscription plans', - 'Subscription reactivated.' => 'Subscription reactivated.', - 'Subscription reference' => 'Subscription reference', - 'Subscription started.' => 'Subscription started.', - 'Subscription switched.' => 'Subscription switched.', - 'Subscription to “{plan}”' => 'Subscription to “{plan}”', - 'Subscription' => 'Subscription', - 'Subscriptions on hold' => 'Subscriptions on hold', - 'Subscriptions' => 'Subscriptions', - 'Suppress emails' => 'Suppress emails', - 'Switch plan' => 'Switch plan', - 'Switch' => 'Switch', - 'System' => 'System', - 'Table Columns' => 'Table Columns', - 'Target - The category relationship field is on the purchasable' => 'Target - The category relationship field is on the purchasable', - 'Tax & Shipping' => 'Tax & Shipping', - 'Tax (inc)' => 'Tax (inc)', - 'Tax Categories' => 'Tax Categories', - 'Tax Category' => 'Tax Category', - 'Tax Rates' => 'Tax Rates', - 'Tax Zone' => 'Tax Zone', - 'Tax Zones' => 'Tax Zones', - 'Tax categories deleted.' => 'Tax categories deleted.', - 'Tax category saved.' => 'Tax category saved.', - 'Tax category updated.' => 'Tax category updated.', - 'Tax rate saved.' => 'Tax rate saved.', - 'Tax rates updated.' => 'Tax rates updated.', - 'Tax zone saved.' => 'Tax zone saved.', - 'Tax' => 'Tax', - 'Taxable Subject' => 'Taxable Subject', - 'Template Path' => 'Template Path', - 'That handle is already in use' => 'That handle is already in use', - 'That handle is already in use.' => 'That handle is already in use.', - 'The PDF to attach to this email.' => 'The PDF to attach to this email.', - 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'The URL to the page where billing details for a subscription can be updated, as well as where 3DS authentication is handled.', - 'The address provided is outside the store’s market.' => 'The address provided is outside the store’s market.', - 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'The discount amount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.', - 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'The base discount can only discount items in the cart to down to zero until it is used up, it cannot make the order negative.', - 'The cart recovery link is invalid. Please request a new one.' => 'The cart recovery link is invalid. Please request a new one.', - 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.', - 'The countries that orders are allowed to be placed from.' => 'The countries that orders can be placed from.', - 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'The coupon "{code}" has exceeded its usage limit of {limit}.', - 'The customer for this order has been deleted.' => 'The customer for this order has been deleted.', - 'The default shipping category is automatically available to all product types.' => 'The default shipping category is automatically available to all product types.', - 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'The discount "{name}" has exceeded its total usage limit of {limit}.', - 'The download link has expired. Please request a new one.' => 'The download link has expired. Please request a new one.', - 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.', - 'The entry that contains the description for this subscription’s plan.' => 'The entry that contains the description for this subscription’s plan.', - 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'The flat value by which each item should be discounted, e.g. “3” for $3 off each item.', - 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'The format used to generate new coupons, e.g. {example}. Any # characters will be replaced with a random letter.', - 'The from and to inventory locations must be different.' => 'The from and to inventory locations must be different.', - 'The inventory locations this store uses.' => 'The inventory locations this store uses.', - 'The item is not enabled for sale.' => 'The item is not enabled for sale.', - 'The language the order was made in.' => 'The language the order was made in.', - 'The language to be used when this email is rendered.' => 'The language to be used when this email is rendered.', - 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'The maximum number of levels this product type can have. Leave blank if you don’t care.', - 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'The maximum the customer should spend on shipping. Set to zero to disable.', - 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'The minimum the customer should spend on shipping. Set to zero to disable.', - 'The order is not valid.' => 'The order is not valid.', - 'The payment gateway that will be used for the subscription plan.' => 'The payment gateway that will be used for the subscription plan.', - 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'The percentage value by which each item should be discounted, e.g. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.', - 'The previously-selected shipping method is no longer available.' => 'The previously-selected shipping method is no longer available.', - 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}', - 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}', - 'The primary currency cannot be changed after orders are placed.' => 'The primary currency cannot be changed after orders are placed.', - 'The purchasable defines the relationship' => 'The purchasable defines the relationship', - 'The purchasable is related by another element' => 'The purchasable is related by another element', - 'The recipient of the email. Twig code can be used here.' => 'The recipient of the email. Twig code can be used here.', - 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'The "reply to" email address. Leave blank for normal "reply to" of email sender. Twig code can be used here.', - 'The site the order was made in.' => 'The site the order was made in.', - 'The site to be used when this email is rendered.' => 'The site to be used when this email is rendered.', - 'The subject line of the email. Twig code can be used here.' => 'The subject line of the email. Twig code can be used here.', - 'The template that the PDF should be generated from.' => 'The template that the PDF should be generated from.', - 'The template to be used for HTML emails.' => 'The template to be used for HTML emails.', - 'The template to be used for plain text emails. Twig code can be used here.' => 'The template to be used for plain text emails. Twig code can be used here.', - 'The template to use when a product’s URL is requested.' => 'The template to use when a product’s URL is requested.', - 'The total number of order adjustments changed.' => 'The total number of order adjustments changed.', - 'The total price of the order changed.' => 'The total price of the order changed.', - 'The total quantity of items within the order changed.' => 'The total quantity of items within the order changed.', - 'The unique SKU of the donation purchasable.' => 'The unique SKU of the donation purchasable.', - 'The unit of measurement that should be used when specifying product dimensions.' => 'The unit of measurement that should be used when specifying product dimensions.', - 'The unit of measurement that should be used when specifying product weights.' => 'The unit of measurement that should be used when specifying product weights.', - 'The webhook URL for this gateway.' => 'The webhook URL for this gateway.', - 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.', - 'There are errors on the order' => 'There are errors on the order', - 'There are only {num} “{description}” items left in stock.' => 'There are only {num} “{description}” items left in stock.', - 'There aren’t any product types to select yet.' => 'There are no product types to select yet.', - 'There is no gateway or payment source available for use with this order.' => 'There is no gateway or payment source available for use with this order.', - 'There is no gateway selected that supports payment sources.' => 'There is no gateway selected that supports payment sources.', - 'There is no shipping method selected for this order.' => 'There is no shipping method selected for this order.', - 'This URL will load the cart into the user’s session, making it the active cart.' => 'This URL will load the cart into the user’s session, making it the active cart.', - 'This action is not allowed for the current user.' => 'This action is not allowed for the current user.', - 'This category will be used as the default for all purchasables in this store.' => 'This category will be used as the default for all purchasables in this store.', - 'This coupon is for registered users and limited to {limit} uses.' => 'This coupon is for registered users and limited to {limit} uses.', - 'This coupon is limited to {limit} uses.' => 'This coupon is limited to {limit} uses.', - 'This coupon requires an email address.' => 'This coupon requires an email address.', - 'This gateway does not support that functionality.' => 'This gateway does not support that functionality.', - 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'This is being overridden by the {setting} config setting in `config/{file}.php`.', - 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.', - 'This is the default PDF that will be rendered when requesting the order PDF.' => 'This is the default PDF that will be rendered when requesting the order PDF.', - 'This is the last location for the {store} store.' => 'This is the last location for the {store} store.', - 'This month' => 'This month', - 'This order has unsaved changes.' => 'This order has unsaved changes.', - 'This week' => 'This week', - 'This year' => 'This year', - 'Times Used' => 'Times Used', - 'Title' => 'Title', - 'To' => 'To', - 'Today' => 'Today', - 'Too many variants for this product.' => 'Too many variants for this product.', - 'Top Customers by Average Order' => 'Top Customers by Average Order', - 'Top Customers by Total Revenue' => 'Top Customers by Total Revenue', - 'Top Customers' => 'Top Customers', - 'Top Product Types by Qty Sold' => 'Top Product Types by Qty Sold', - 'Top Product Types by Revenue' => 'Top Product Types by Revenue', - 'Top Product Types' => 'Top Product Types', - 'Top Products by Qty Sold' => 'Top Products by Qty Sold', - 'Top Products by Revenue' => 'Top Products by Revenue', - 'Top Products' => 'Top Products', - 'Top Purchasables by Qty Sold' => 'Top Purchasables by Qty Sold', - 'Top Purchasables by Revenue' => 'Top Purchasables by Revenue', - 'Top Purchasables' => 'Top Purchasables', - 'Total ' => 'Total ', - 'Total Discount Use Limit' => 'Total Discount Use Limit', - 'Total Discount' => 'Total Discount', - 'Total Included Tax' => 'Total Included Tax', - 'Total Orders by Billing Country' => 'Total Orders by Billing Country', - 'Total Orders by Country' => 'Total Orders by Country', - 'Total Orders by Shipping Country' => 'Total Orders by Shipping Country', - 'Total Orders' => 'Total Orders', - 'Total Paid' => 'Total Paid', - 'Total Price' => 'Total Price', - 'Total Qty' => 'Total Qty', - 'Total Revenue' => 'Total Revenue', - 'Total Shipping' => 'Total Shipping', - 'Total Tax' => 'Total Tax', - 'Total Weight' => 'Total Weight', - 'Total' => 'Total', - 'Track Inventory' => 'Track Inventory', - 'Transaction Hash' => 'Transaction Hash', - 'Transaction ID' => 'Transaction ID', - 'Transaction captured successfully: {message}' => 'Transaction captured successfully: {message}', - 'Transaction refunded successfully: {message}' => 'Transaction refunded successfully: {message}', - 'Transactions' => 'Transactions', - 'Transfer Fields' => 'Transfer Fields', - 'Transfer Items' => 'Transfer Items', - 'Transfer Settings' => 'Transfer Settings', - 'Transfer Status' => 'Transfer Status', - 'Transfer fields saved.' => 'Transfer fields saved.', - 'Transfer must have at least one item.' => 'Transfer must have at least one item.', - 'Transfer' => 'Transfer', - 'Transfers' => 'Transfers', - 'Trial days credited' => 'Trial days credited', - 'Trial expiration' => 'Trial expiration', - 'Trial expiry date' => 'Trial expiry date', - 'Type not in allowed options.' => 'Type not in allowed options.', - 'Type' => 'Type', - 'URI' => 'URI', - 'Unable to cancel subscription at this time.' => 'Unable to cancel subscription at this time.', - 'Unable to complete order: another request is already in progress.' => 'Unable to complete order: another request is already in progress.', - 'Unable to find variant.' => 'Unable to find variant.', - 'Unable to generate coupon codes: {message}' => 'Unable to generate coupon codes: {message}', - 'Unable to make payment at this time.' => 'Unable to make payment at this time.', - 'Unable to modify subscription at this time.' => 'Unable to modify subscription at this time.', - 'Unable to reactivate subscription at this time.' => 'Unable to reactivate subscription at this time.', - 'Unable to reassign orders.' => 'Unable to reassign orders.', - 'Unable to remove order data.' => 'Unable to remove order data.', - 'Unable to retrieve Sale and Purchasable.' => 'Unable to retrieve Sale and Purchasable.', - 'Unable to retrieve cart.' => 'Unable to retrieve cart.', - 'Unable to retrieve customer.' => 'Unable to retrieve customer.', - 'Unable to retrieve load cart URL' => 'Unable to retrieve load cart URL', - 'Unable to retrieve payment source.' => 'Unable to retrieve payment source.', - 'Unable to set default shipping category.' => 'Unable to set default shipping category.', - 'Unable to set default tax category.' => 'Unable to set default tax category.', - 'Unable to set primary payment source.' => 'Unable to set primary payment source.', - 'Unable to start the subscription. Please check your payment details.' => 'Unable to start the subscription. Please check your payment details.', - 'Unable to subscribe at this time.' => 'Unable to subscribe at this time.', - 'Unable to update cart.' => 'Unable to update cart.', - 'Unable to validate address.' => 'Unable to validate address.', - 'Unit Price' => 'Unit Price', - 'Unit price (minus discounts)' => 'Unit price (minus discounts)', - 'Units' => 'Units', - 'Unpaid' => 'Unpaid', - 'Unsubscribe' => 'Unsubscribe', - 'Update Address' => 'Update Address', - 'Update Order Status' => 'Update Order Status', - 'Update Order Status…' => 'Update Order Status…', - 'Update order' => 'Update order', - 'Update subscription' => 'Update subscription', - 'Update' => 'Update', - 'Updated By' => 'Updated By', - 'Updated committed stock successfully.' => 'Updated committed stock successfully.', - 'Updated' => 'Updated', - 'Use Billing Address For Tax' => 'Use Billing Address For Tax', - 'Use as the primary billing address' => 'Use as the primary billing address', - 'Use as the primary shipping address' => 'Use as the primary shipping address', - 'Used By Tax Rates' => 'Used By Tax Rates', - 'Used by Tax Rates' => 'Used by Tax Rates', - 'User Groups' => 'User Groups', - 'User not found.' => 'User not found.', - 'User' => 'User', - 'Uses' => 'Uses', - 'Validate Business Tax ID as Vat ID' => 'Validate Business Tax ID as Vat ID', - 'Validating condition syntax' => 'Validating condition syntax', - 'Validating formula syntax' => 'Validating formula syntax', - 'Variant Fields' => 'Variant Fields', - 'Variant Has Untracked Stock' => 'Variant Has Untracked Stock', - 'Variant Price' => 'Variant Price', - 'Variant SKU' => 'Variant SKU', - 'Variant Search' => 'Variant Search', - 'Variant Stock' => 'Variant Stock', - 'Variant Title Format' => 'Variant Title Format', - 'Variant Tracks Stock' => 'Variant Tracks Stock', - 'Variant UI Label Format' => 'Variant UI Label Format', - 'Variant has no product.' => 'Variant has no product.', - 'Variants not restored.' => 'Variants not restored.', - 'Variants restored.' => 'Variants restored.', - 'Variants' => 'Variants', - 'View customer' => 'View customer', - 'View order' => 'View order', - 'View product type - {productType}' => 'View product type - {productType}', - 'View user' => 'View user', - 'View' => 'View', - 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Warning: deleting this currency will stop all payments and refunds in this currency. Are you sure you want to delete “{name}”?', - 'Web' => 'Web', - 'Webhook URL' => 'Webhook URL', - 'Weight ({unit})' => 'Weight ({unit})', - 'Weight Rate' => 'Weight Rate', - 'Weight Unit' => 'Weight Unit', - 'Weight' => 'Weight', - 'What product URIs should look like for the site.' => 'What product URIs should look like for the site.', - 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.', - 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.', - 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.', - 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}', - 'What this PDF will be called in the control panel.' => 'What this PDF will be called in the control panel.', - 'What this catalog pricing rule will be called in the control panel.' => 'What this catalogue pricing rule will be called in the control panel.', - 'What this discount will be called in the control panel.' => 'What this discount will be called in the control panel.', - 'What this email will be called in the control panel.' => 'What this email will be called in the control panel.', - 'What this product type will be called in the control panel.' => 'What this product type will be called in the control panel.', - 'What this sale will be called in the control panel.' => 'What this sale will be called in the control panel.', - 'What this shipping category will be called in the control panel.' => 'What this shipping category will be called in the control panel.', - 'What this shipping rule will be called in the control panel.' => 'What this shipping rule will be called in the control panel.', - 'What this shipping zone will be called in the control panel.' => 'What this shipping zone will be called in the control panel.', - 'What this status will be called in the control panel.' => 'What this status will be called in the control panel.', - 'What this subscription plan will be called in the control panel.' => 'What this subscription plan will be called in the control panel.', - 'What this tax category will be called in the control panel.' => 'What this tax category will be called in the control panel.', - 'What this tax zone will be called in the control panel.' => 'What this tax zone will be called in the control panel.', - 'When this discount is applied to an order, which line items should be discounted?' => 'When this discount is applied to an order, which line items should be discounted?', - 'Whether the first available shipping method option should be set automatically on carts.' => 'Whether the first available shipping method option should be set automatically on carts.', - 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Whether the user’s primary payment source should be set automatically on new carts.', - 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.', - 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Whether this catalogue pricing rule should be available for use, regardless of other conditions.', - 'Whether this sale should be available for use, regardless of other conditions.' => 'Whether this sale should be available for use, regardless of other conditions.', - 'Which data to display in the name column in the results table.' => 'Which data to display in the name column in the results table.', - 'Which product types should this category be available to?' => 'Which product types should this category be available to?', - 'Which template should be loaded when a product’s URL is requested.' => 'Which template should be loaded when a product’s URL is requested.', - 'Width ({unit})' => 'Width ({unit})', - 'Width' => 'Width', - 'YYYY' => 'YYYY', - 'Yes' => 'Yes', - 'You are not allowed to add a line item.' => 'You are not allowed to add a line item.', - 'You currently have no emails configured to select for this status.' => 'You currently have no emails configured to select for this status.', - 'You do not have permission to load this cart.' => 'You do not have permission to load this cart.', - 'You must set up at least one gateway that supports subscriptions first.' => 'You must set up at least one gateway that supports subscriptions first.', - 'You must be logged in or provide a valid token to load this cart.' => 'You must be logged in or provide a valid token to load this cart.', - 'You must be signed in to create a payment source.' => 'You must be signed in to create a payment source.', - 'You must be signed in to set a primary payment source.' => 'You must be signed in to set a primary payment source.', - 'You must make a payment to complete the order.' => 'You must make a payment to complete the order.', - 'Your Cart Recovery Link' => 'Your Cart Recovery Link', - 'Your Order PDF Download Link' => 'Your Order PDF Download Link', - 'Your order is empty' => 'Your order is empty', - 'ZIP file' => 'ZIP file', - 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Zero - Minimum price is zero if discounts are greater than the order value.', - 'Zip Code' => 'Zip Code', - 'all' => 'all', - 'any' => 'any', - 'average order total' => 'average order total', - 'billing address' => 'billing address', - 'donation' => 'donation', - 'donations' => 'donations', - 'info' => 'info', - 'inventory location' => 'inventory location', - 'new customers' => 'new customers', - 'on hand' => 'on hand', - 'only' => 'only', - 'order' => 'order', - 'orders' => 'orders', - 'price' => 'price', - 'prices' => 'prices', - 'product variant' => 'product variant', - 'product variants' => 'product variants', - 'product' => 'product', - 'products' => 'products', - 'repeat customers' => 'repeat customers', - 'shipping address' => 'shipping address', - 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'shippingSameAsBilling and billingSameAsShipping can’t both be set.', - 'subscription' => 'subscription', - 'subscriptions' => 'subscriptions', - 'to' => 'to', - 'transfer' => 'transfer', - 'transfers' => 'transfers', - '{amount} included' => '{amount} included', - '{count} Unfulfilled Orders' => '{count} Unfulfilled Orders', - '{description} is no longer available.' => '{description} is no longer available.', - '{description} only has {stock} in stock.' => '{description} only has {stock} in stock.', - '{from} to {to}' => '{from} to {to}', - '{name} (Primary)' => '{name} (Primary)', - '{name} (Trashed)' => '{name} (Trashed)', - '{name} catalog price' => '{name} catalog price', - '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, one {}=1{order} other{orders}} updated.', - '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.', - '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.', - '{number} more…' => '{number} more…', - '{pct} off the discounted item price' => '{pct} off the discounted item price', - '{pct} off the original item price' => '{pct} off the original item price', - '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.', - '{total} in total revenue' => '{total} in total revenue', - '{total} orders' => '{total} orders', - '{total} saleable across {locationCount} location(s)' => '{total} saleable across {locationCount} location(s)', - '{uses} uses across {emails} email addresses' => '{uses} uses across {emails} email addresses', - '{uses} uses across {users} users' => '{uses} uses across {users} users', - '“{description}” is currently out of stock.' => '“{description}” is currently out of stock.', - '“{key}” has invalid JSON' => '“{key}” has invalid JSON', -]; diff --git a/src/translations/en/commerce.php b/src/translations/en/commerce.php deleted file mode 100644 index bc19414f47..0000000000 --- a/src/translations/en/commerce.php +++ /dev/null @@ -1,1428 +0,0 @@ - '(new price)', - '(of original price)' => '(of original price)', - '(off original price)' => '(off original price)', - 'A cart number must be specified.' => 'A cart number must be specified.', - 'A cart recovery link has been sent to {email}.' => 'A cart recovery link has been sent to {email}.', - 'A cart recovery link will be sent to {email}.' => 'A cart recovery link will be sent to {email}.', - 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.', - 'A new download link has been sent to {email}' => 'A new download link has been sent to {email}', - 'A new download link will be sent to {email}' => 'A new download link will be sent to {email}', - 'A valid email is required to create a customer.' => 'A valid email is required to create a customer.', - 'Accept' => 'Accept', - 'Accepted' => 'Accepted', - 'Actions' => 'Actions', - 'Active Carts' => 'Active Carts', - 'Active subscriptions' => 'Active subscriptions', - 'Active' => 'Active', - 'Add Address' => 'Add Address', - 'Add a coupon' => 'Add a coupon', - 'Add a custom line item' => 'Add a custom line item', - 'Add a line item' => 'Add a line item', - 'Add a product' => 'Add a product', - 'Add a variant' => 'Add a variant', - 'Add an adjustment' => 'Add an adjustment', - 'Add an item' => 'Add an item', - 'Add an option' => 'Add an option', - 'Add catalog price' => 'Add catalog price', - 'Add' => 'Add', - 'Additional Actions' => 'Additional Actions', - 'Additional recipients that should receive this email. Twig code can be used here.' => 'Additional recipients that should receive this email. Twig code can be used here.', - 'Address 1' => 'Address 1', - 'Address 2' => 'Address 2', - 'Address 3' => 'Address 3', - 'Address Line 1' => 'Address Line 1', - 'Address Line 2' => 'Address Line 2', - 'Address Updated.' => 'Address Updated.', - 'Address copied to user.' => 'Address copied to user.', - 'Address not found.' => 'Address not found.', - 'Adjust Quantity' => 'Adjust Quantity', - 'Adjust by' => 'Adjust by', - 'Adjust price when included rate is disqualified?' => 'Adjust price when included rate is disqualified?', - 'Adjustments' => 'Adjustments', - 'Admin Notices' => 'Admin Notices', - 'Administrative Area Code of Origin' => 'Administrative Area Code of Origin', - 'Advanced' => 'Advanced', - 'All Orders' => 'All Orders', - 'All Totals' => 'All Totals', - 'All Transfers' => 'All Transfers', - 'All active subscriptions' => 'All active subscriptions', - 'All customers' => 'All customers', - 'All products' => 'All products', - 'All variants must have a SKU.' => 'All variants must have a SKU.', - 'All' => 'All', - 'Allow Checkout Without Payment' => 'Allow Checkout Without Payment', - 'Allow Empty Cart On Checkout' => 'Allow Empty Cart On Checkout', - 'Allow Partial Payment On Checkout' => 'Allow Partial Payment On Checkout', - 'Allow out of stock purchases' => 'Allow out of stock purchases', - 'Allow' => 'Allow', - 'Allowed Qty' => 'Allowed Qty', - 'Alternative Phone' => 'Alternative Phone', - 'Amount' => 'Amount', - 'An ID must be provided' => 'An ID must be provided', - 'An error occurred while generating this PDF.' => 'An error occurred while generating this PDF.', - 'Any' => 'Any', - 'Anywhere' => 'Anywhere', - 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.', - 'Are you sure you want to capture this transaction?' => 'Are you sure you want to capture this transaction?', - 'Are you sure you want to complete this order?' => 'Are you sure you want to complete this order?', - 'Are you sure you want to delete the selected orders?' => 'Are you sure you want to delete the selected orders?', - 'Are you sure you want to delete the selected product and its variants?' => 'Are you sure you want to delete the selected product and its variants?', - 'Are you sure you want to delete this shipping rule?' => 'Are you sure you want to delete this shipping rule?', - 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.', - 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?', - 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.', - 'Are you sure you want to overwrite the billing address?' => 'Are you sure you want to overwrite the billing address?', - 'Are you sure you want to overwrite the shipping address?' => 'Are you sure you want to overwrite the shipping address?', - 'Are you sure you want to permanently delete this store and everything in it?' => 'Are you sure you want to permanently delete this store and everything in it?', - 'Are you sure you want to refund this transaction?' => 'Are you sure you want to refund this transaction?', - 'Are you sure you want to remove this customer?' => 'Are you sure you want to remove this customer?', - 'Are you sure you want to save this as a new shipping rule?' => 'Are you sure you want to save this as a new shipping rule?', - 'Are you sure you want to send email: {name}?' => 'Are you sure you want to send email: {name}?', - 'At least one site must be enabled for the product type.' => 'At least one site must be enabled for the product type.', - 'Attempted Payments' => 'Attempted Payments', - 'Attention' => 'Attention', - 'Authorize Only (Manually Capture)' => 'Authorize Only (Manually Capture)', - 'Auto Set Cart Shipping Method Option' => 'Auto Set Cart Shipping Method Option', - 'Auto Set New Cart Addresses' => 'Auto Set New Cart Addresses', - 'Auto Set Payment Source' => 'Auto Set Payment Source', - 'Automatic SKU Format' => 'Automatic SKU Format', - 'Available Shipping Categories' => 'Available Shipping Categories', - 'Available Tax Categories' => 'Available Tax Categories', - 'Available for purchase' => 'Available for purchase', - 'Available for purchase?' => 'Available for purchase?', - 'Available inventory for "{description}" has gone below zero.' => 'Available inventory for "{description}" has gone below zero.', - 'Available to Product Types' => 'Available to Product Types', - 'Available' => 'Available', - 'Available?' => 'Available?', - 'Average Order Total' => 'Average Order Total', - 'Average' => 'Average', - 'BCC’d Recipient' => 'BCC’d Recipient', - 'Bad Request' => 'Bad Request', - 'Bad address ID.' => 'Bad address ID.', - 'Bad order ID.' => 'Bad order ID.', - 'Base Price' => 'Base Price', - 'Base Promotional Price' => 'Base Promotional Price', - 'Base Rate' => 'Base Rate', - 'Base' => 'Base', - 'Bcc' => 'Bcc', - 'Billing Address' => 'Billing Address', - 'Billing Business Name' => 'Billing Business Name', - 'Billing First Name' => 'Billing First Name', - 'Billing Full Name' => 'Billing Full Name', - 'Billing Last Name' => 'Billing Last Name', - 'Billing address required.' => 'Billing address required.', - 'Billing detail update URL' => 'Billing detail update URL', - 'Billing issues' => 'Billing issues', - 'Billing' => 'Billing', - 'Both (Line item price + Line item shipping costs)' => 'Both (Line item price + Line item shipping costs)', - 'Business ID' => 'Business ID', - 'Business Name' => 'Business Name', - 'Business Tax ID' => 'Business Tax ID', - 'CC’d Recipient' => 'CC’d Recipient', - 'CVV' => 'CVV', - 'Can be used as an internal reference.' => 'Can be used as an internal reference.', - 'Can not complete payment for missing transaction.' => 'Can not complete payment for missing transaction.', - 'Can not create a new order' => 'Can not create a new order', - 'Can not find an order to pay.' => 'Can not find an order to pay.', - 'Can not find enabled email.' => 'Can not find enabled email.', - 'Can not find order' => 'Can not find order', - 'Can not find order.' => 'Can not find order.', - 'Can not find the transaction to refund' => 'Can not find the transaction to refund', - 'Can not move between these inventory types.' => 'Can not move between these inventory types.', - 'Can not refund amount greater than the remaining amount' => 'Can not refund amount greater than the remaining amount', - 'Cancel subscription' => 'Cancel subscription', - 'Cancel with gateway now' => 'Cancel with gateway now', - 'Cancel' => 'Cancel', - 'Cancellation date' => 'Cancellation date', - 'Cancellation' => 'Cancellation', - 'Cannot switch plans for this subscription.' => 'Cannot switch plans for this subscription.', - 'Can’t preview this email.' => 'Can’t preview this email.', - 'Capture payment' => 'Capture payment', - 'Capture' => 'Capture', - 'Card Holder' => 'Card Holder', - 'Card Number' => 'Card Number', - 'Card' => 'Card', - 'Cart Recovery Link' => 'Cart Recovery Link', - 'Cart forgotten.' => 'Cart forgotten.', - 'Cart updated.' => 'Cart updated.', - 'Cart {number}' => 'Cart {number}', - 'Catalog Pricing Rule' => 'Catalog Pricing Rule', - 'Catalog pricing rule description.' => 'Catalog pricing rule description.', - 'Catalog pricing rule saved.' => 'Catalog pricing rule saved.', - 'Catalog pricing rules deleted.' => 'Catalog pricing rules deleted.', - 'Catalog pricing rules updated.' => 'Catalog pricing rules updated.', - 'Categories Relationship Type' => 'Categories Relationship Type', - 'Categories' => 'Categories', - 'Category Rate Overrides' => 'Category Rate Overrides', - 'Centimeters (cm)' => 'Centimeters (cm)', - 'Changing this value may affect your ability to refund existing transactions.' => 'Changing this value may affect your ability to refund existing transactions.', - 'Choose a color to represent the order’s status' => 'Choose a color to represent the order’s status', - 'Choose a new customer' => 'Choose a new customer', - 'Choose adjustment values to include when calculating the product revenue total.' => 'Choose adjustment values to include when calculating the product revenue total.', - 'Choose the currency’s ISO code.' => 'Choose the currency’s ISO code.', - 'Choose the destination inventory location for the existing on hand stock.' => 'Choose the destination inventory location for the existing on hand stock.', - 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Choose which sites this product type should be available in, and configure the site-specific settings.', - 'City' => 'City', - 'Clear counter' => 'Clear counter', - 'Clear notices' => 'Clear notices', - 'Close' => 'Close', - 'Code' => 'Code', - 'Collated PDF' => 'Collated PDF', - 'Color' => 'Color', - 'Commerce Products' => 'Commerce Products', - 'Commerce Settings' => 'Commerce Settings', - 'Commerce Variants' => 'Commerce Variants', - 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Commerce email “{email}” could not be sent for order “{order}”.', - 'Commerce order exports' => 'Commerce order exports', - 'Commerce' => 'Commerce', - 'Committed' => 'Committed', - 'Completed Email' => 'Completed Email', - 'Completed' => 'Completed', - 'Completing order failed.' => 'Completing order failed.', - 'Condition' => 'Condition', - 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.', - 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.', - 'Conditions' => 'Conditions', - 'Contains Purchasables' => 'Contains Purchasables', - 'Control Panel Settings' => 'Control Panel Settings', - 'Control panel' => 'Control panel', - 'Conversion Rate' => 'Conversion Rate', - 'Converted Price' => 'Converted Price', - 'Copied!' => 'Copied!', - 'Copy the URL' => 'Copy the URL', - 'Copy to {location}' => 'Copy to {location}', - 'Copy' => 'Copy', - 'Costs' => 'Costs', - 'Could not archive gateway.' => 'Could not archive gateway.', - 'Could not cancel “{reference}”.' => 'Could not cancel “{reference}”.', - 'Could not create the payment source.' => 'Could not create the payment source.', - 'Could not delete shipping rule' => 'Could not delete shipping rule', - 'Could not delete shipping zone' => 'Could not delete shipping zone', - 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.', - 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.', - 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.', - 'Could not find the email or template.' => 'Could not find the email or template.', - 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}', - 'Could not reactivate “{reference}”.' => 'Could not reactivate “{reference}”.', - 'Could not send email' => 'Could not send email', - 'Could not switch “{reference}” to “{plan}”.' => 'Could not switch “{reference}” to “{plan}”.', - 'Could not update orders address.' => 'Could not update orders address.', - 'Couldn’t archive Line Item Status.' => 'Couldn’t archive Line Item Status.', - 'Couldn’t archive Order Status.' => 'Couldn’t archive Order Status.', - 'Couldn’t capture transaction.' => 'Couldn’t capture transaction.', - 'Couldn’t capture transaction: {message}' => 'Couldn’t capture transaction: {message}', - 'Couldn’t delete email.' => 'Couldn’t delete email.', - 'Couldn’t delete the payment source.' => 'Couldn’t delete the payment source.', - 'Couldn’t get order.' => 'Couldn’t get order.', - 'Couldn’t recalculate order.' => 'Couldn’t recalculate order.', - 'Couldn’t refund transaction.' => 'Couldn’t refund transaction.', - 'Couldn’t refund transaction: {message}' => 'Couldn’t refund transaction: {message}', - 'Couldn’t reorder Line Item Statuses.' => 'Couldn’t reorder Line Item Statuses.', - 'Couldn’t reorder Order Statuses.' => 'Couldn’t reorder Order Statuses.', - 'Couldn’t reorder PDFs.' => 'Couldn’t reorder PDFs.', - 'Couldn’t reorder discounts.' => 'Couldn’t reorder discounts.', - 'Couldn’t reorder gateways.' => 'Couldn’t reorder gateways.', - 'Couldn’t reorder plans.' => 'Couldn’t reorder plans.', - 'Couldn’t reorder rules.' => 'Couldn’t reorder rules.', - 'Couldn’t reorder sale.' => 'Couldn’t reorder sale.', - 'Couldn’t reorder sales.' => 'Couldn’t reorder sales.', - 'Couldn’t reorder statuses.' => 'Couldn’t reorder statuses.', - 'Couldn’t reorder stores.' => 'Couldn’t reorder stores.', - 'Couldn’t save PDF.' => 'Couldn’t save PDF.', - 'Couldn’t save catalog pricing rule.' => 'Couldn’t save catalog pricing rule.', - 'Couldn’t save currency.' => 'Couldn’t save currency.', - 'Couldn’t save discount.' => 'Couldn’t save discount.', - 'Couldn’t save email.' => 'Couldn’t save email.', - 'Couldn’t save gateway.' => 'Couldn’t save gateway.', - 'Couldn’t save inventory location.' => 'Couldn’t save inventory location.', - 'Couldn’t save line item status.' => 'Couldn’t save line item status.', - 'Couldn’t save order fields.' => 'Couldn’t save order fields.', - 'Couldn’t save order status.' => 'Couldn’t save order status.', - 'Couldn’t save order.' => 'Couldn’t save order.', - 'Couldn’t save product type.' => 'Couldn’t save product type.', - 'Couldn’t save sale.' => 'Couldn’t save sale.', - 'Couldn’t save settings.' => 'Couldn’t save settings.', - 'Couldn’t save shipping category.' => 'Couldn’t save shipping category.', - 'Couldn’t save shipping method.' => 'Couldn’t save shipping method.', - 'Couldn’t save shipping rule.' => 'Couldn’t save shipping rule.', - 'Couldn’t save shipping zone.' => 'Couldn’t save shipping zone.', - 'Couldn’t save store.' => 'Couldn’t save store.', - 'Couldn’t save subscription fields.' => 'Couldn’t save subscription fields.', - 'Couldn’t save subscription plan.' => 'Couldn’t save subscription plan.', - 'Couldn’t save subscription.' => 'Couldn’t save subscription.', - 'Couldn’t save tax category.' => 'Couldn’t save tax category.', - 'Couldn’t save tax rate.' => 'Couldn’t save tax rate.', - 'Couldn’t save tax zone.' => 'Couldn’t save tax zone.', - 'Couldn’t save transfer fields.' => 'Couldn’t save transfer fields.', - 'Couldn’t update catalog pricing rule statuses.' => 'Couldn’t update catalog pricing rule statuses.', - 'Couldn’t update status.' => 'Couldn’t update status.', - 'Couldn’t updated sales status.' => 'Couldn’t updated sales status.', - 'Country Code of Origin' => 'Country Code of Origin', - 'Country List' => 'Country List', - 'Country not allowed.' => 'Country not allowed.', - 'Country' => 'Country', - 'Coupon Code' => 'Coupon Code', - 'Coupon can not apply discount to this order due to address mismatch.' => 'Coupon can not apply discount to this order due to address mismatch.', - 'Coupon can not apply discount to this order due to customer mismatch.' => 'Coupon can not apply discount to this order due to customer mismatch.', - 'Coupon can not apply discount to this order.' => 'Coupon can not apply discount to this order.', - 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Coupon code “{code}” is already in use by discount “{name}”.', - 'Coupon codes cannot be blank.' => 'Coupon codes cannot be blank.', - 'Coupon codes must be unique.' => 'Coupon codes must be unique.', - 'Coupon format is required and must contain at least one `#`.' => 'Coupon format is required and must contain at least one `#`.', - 'Coupon not valid.' => 'Coupon not valid.', - 'Coupon removed: {explanation}' => 'Coupon removed: {explanation}', - 'Coupons' => 'Coupons', - 'Craft Commerce - Administration' => 'Craft Commerce - Administration', - 'Craft Commerce - Inventory' => 'Craft Commerce - Inventory', - 'Craft Commerce - Orders' => 'Craft Commerce - Orders', - 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Product Type - {name}', - 'Craft Commerce - Subscriptions' => 'Craft Commerce - Subscriptions', - 'Create a Discount' => 'Create a Discount', - 'Create a Subscription Plan' => 'Create a Subscription Plan', - 'Create a new PDF' => 'Create a new PDF', - 'Create a new catalog pricing rule' => 'Create a new catalog pricing rule', - 'Create a new currency' => 'Create a new currency', - 'Create a new email' => 'Create a new email', - 'Create a new gateway' => 'Create a new gateway', - 'Create a new line item status' => 'Create a new line item status', - 'Create a new order status' => 'Create a new order status', - 'Create a new product type' => 'Create a new product type', - 'Create a new sale' => 'Create a new sale', - 'Create a new shipping category' => 'Create a new shipping category', - 'Create a new shipping method' => 'Create a new shipping method', - 'Create a new shipping rule' => 'Create a new shipping rule', - 'Create a new tax category' => 'Create a new tax category', - 'Create a new tax rate' => 'Create a new tax rate', - 'Create a product type' => 'Create a product type', - 'Create a shipping zone' => 'Create a shipping zone', - 'Create a tax zone' => 'Create a tax zone', - 'Create catalog pricing rules' => 'Create catalog pricing rules', - 'Create customer: “{email}”' => 'Create customer: “{email}”', - 'Create discounts' => 'Create discounts', - 'Create discount…' => 'Create discount…', - 'Create rules that allow this discount to match the order.' => 'Create rules that allow this discount to match the order.', - 'Create rules that allow this discount to match the order’s billing address.' => 'Create rules that allow this discount to match the order’s billing address.', - 'Create rules that allow this discount to match the order’s customer.' => 'Create rules that allow this discount to match the order’s customer.', - 'Create rules that allow this discount to match the order’s shipping address.' => 'Create rules that allow this discount to match the order’s shipping address.', - 'Create rules that allow this gateway to match the billing address.' => 'Create rules that allow this gateway to match the billing address.', - 'Create rules that allow this gateway to match the order.' => 'Create rules that allow this gateway to match the order.', - 'Create rules that allow this gateway to match the shipping address.' => 'Create rules that allow this gateway to match the shipping address.', - 'Create sales' => 'Create sales', - 'Create sale…' => 'Create sale…', - 'Created' => 'Created', - 'Credit Card Payment Type' => 'Credit Card Payment Type', - 'Currency Code' => 'Currency Code', - 'Currency saved.' => 'Currency saved.', - 'Currency' => 'Currency', - 'Current' => 'Current', - 'Custom 1' => 'Custom 1', - 'Custom 2' => 'Custom 2', - 'Custom 3' => 'Custom 3', - 'Custom 4' => 'Custom 4', - 'Custom' => 'Custom', - 'Customer Enabled?' => 'Customer Enabled?', - 'Customer ID is required.' => 'Customer ID is required.', - 'Customer Note' => 'Customer Note', - 'Customer Notices' => 'Customer Notices', - 'Customer data' => 'Customer data', - 'Customer' => 'Customer', - 'Damaged' => 'Damaged', - 'Data shown might be outdated.' => 'Data shown might be outdated.', - 'Date Authorized' => 'Date Authorized', - 'Date Created' => 'Date Created', - 'Date First Paid' => 'Date First Paid', - 'Date Ordered' => 'Date Ordered', - 'Date Paid' => 'Date Paid', - 'Date Updated' => 'Date Updated', - 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date', - 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Date from which the discount will be active. Leave blank for unlimited start date', - 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Date from which the sale will be active. Leave blank for unlimited start date', - 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date', - 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Date when the discount will be finished. Leave blank for unlimited end date', - 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Date when the sale will be finished. Leave blank for unlimited end date', - 'Date' => 'Date', - 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Default - Allow the price to be negative if discounts are greater than the order value.', - 'Default Category' => 'Default Category', - 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.', - 'Default Order PDF' => 'Default Order PDF', - 'Default Per Item Rate' => 'Default Per Item Rate', - 'Default Percentage Rate' => 'Default Percentage Rate', - 'Default Status?' => 'Default Status?', - 'Default View' => 'Default View', - 'Default Weight Rate' => 'Default Weight Rate', - 'Default Zone' => 'Default Zone', - 'Default status?' => 'Default status?', - 'Default to this tax zone when no billing address is set' => 'Default to this tax zone when no billing address is set', - 'Default to this tax zone when no shipping address is set' => 'Default to this tax zone when no shipping address is set', - 'Default variant updated.' => 'Default variant updated.', - 'Default' => 'Default', - 'Default?' => 'Default?', - 'Delete catalog pricing rules' => 'Delete catalog pricing rules', - 'Delete discounts' => 'Delete discounts', - 'Delete orders' => 'Delete orders', - 'Delete sales' => 'Delete sales', - 'Delete' => 'Delete', - 'Deleting the {location} location.' => 'Deleting the {location} location.', - 'Describe this rule.' => 'Describe this rule.', - 'Describe this shipping zone.' => 'Describe this shipping zone.', - 'Describe this tax zone.' => 'Describe this tax zone.', - 'Description' => 'Description', - 'Destination Inventory Location' => 'Destination Inventory Location', - 'Destination' => 'Destination', - 'Details' => 'Details', - 'Dimension Unit' => 'Dimension Unit', - 'Dimensions' => 'Dimensions', - 'Disabled' => 'Disabled', - 'Disallow' => 'Disallow', - 'Discount all line items' => 'Discount all line items', - 'Discount description.' => 'Discount description.', - 'Discount is not allowed for the order' => 'Discount is not allowed for the order', - 'Discount is out of date.' => 'Discount is out of date.', - 'Discount saved.' => 'Discount saved.', - 'Discount the matching items only' => 'Discount the matching items only', - 'Discount use has reached its limit.' => 'Discount use has reached its limit.', - 'Discount' => 'Discount', - 'Discounted Item Subtotal' => 'Discounted Item Subtotal', - 'Discounted Items' => 'Discounted Items', - 'Discounts deleted.' => 'Discounts deleted.', - 'Discounts reordered.' => 'Discounts reordered.', - 'Discounts updated.' => 'Discounts updated.', - 'Discounts' => 'Discounts', - 'Disqualify with valid business tax ID?' => 'Disqualify with valid business tax ID?', - 'Do not apply subsequent matching sales beyond applying this sale.' => 'Do not apply subsequent matching sales beyond applying this sale.', - 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Do not apply this rate if the order address has any of the selected valid business tax IDs.', - 'Do not attach a PDF to this email' => 'Do not attach a PDF to this email', - 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.', - 'Donation can not be zero.' => 'Donation can not be zero.', - 'Donation needs to be an amount.' => 'Donation needs to be an amount.', - 'Donation settings saved.' => 'Donation settings saved.', - 'Donation' => 'Donation', - 'Donations' => 'Donations', - 'Done' => 'Done', - 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Don’t apply any subsequent discounts to an order if this discount is applied', - 'Download PDF' => 'Download PDF', - 'Download PDF…' => 'Download PDF…', - 'Download Type' => 'Download Type', - 'Download' => 'Download', - 'Draft' => 'Draft', - 'Dummy gateway payment failed.' => 'Dummy gateway payment failed.', - 'Duplicate options exist' => 'Duplicate options exist', - 'Duration' => 'Duration', - 'EU VAT ID' => 'EU VAT ID', - 'Edit address' => 'Edit address', - 'Edit adjustments' => 'Edit adjustments', - 'Edit catalog pricing rules' => 'Edit catalog pricing rules', - 'Edit discounts' => 'Edit discounts', - 'Edit options' => 'Edit options', - 'Edit orders' => 'Edit orders', - 'Edit sales' => 'Edit sales', - 'Edit' => 'Edit', - 'Effect' => 'Effect', - 'Either (Default) - The relationship field is on the purchasable or the category' => 'Either (Default) - The relationship field is on the purchasable or the category', - 'Either way' => 'Either way', - 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}', - 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.', - 'Email Subject' => 'Email Subject', - 'Email error. No email address found for order. Order: “{order}”' => 'Email error. No email address found for order. Order: “{order}”', - 'Email is not enabled.' => 'Email is not enabled.', - 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.', - 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email required to make payments on a completed order.' => 'Email required to make payments on a completed order.', - 'Email saved.' => 'Email saved.', - 'Email sent' => 'Email sent', - 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.', - 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}', - 'Email unavailable.' => 'Email unavailable.', - 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}', - 'Email “{email}” for order {order} was cancelled.' => 'Email “{email}” for order {order} was cancelled.', - 'Email' => 'Email', - 'Emails' => 'Emails', - 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.', - 'Enable structure for products of this type' => 'Enable structure for products of this type', - 'Enable this discount' => 'Enable this discount', - 'Enable this rule' => 'Enable this rule', - 'Enable this sale' => 'Enable this sale', - 'Enable this shipping method on the front end' => 'Enable this shipping method on the front end', - 'Enable this shipping rule' => 'Enable this shipping rule', - 'Enable this tax rate' => 'Enable this tax rate', - 'Enabled for customers to select during checkout?' => 'Enabled for customers to select during checkout?', - 'Enabled for customers to select?' => 'Enabled for customers to select?', - 'Enabled' => 'Enabled', - 'Enabled?' => 'Enabled?', - 'End Date' => 'End Date', - 'Enter SKU' => 'Enter SKU', - 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Enter a human-friendly name for this tax rate to be used in the control panel.', - 'Enter a percentage like {ex1} or {ex2}.' => 'Enter a percentage like {ex1} or {ex2}.', - 'Enter coupon code' => 'Enter coupon code', - 'Enter reference' => 'Enter reference', - 'Error refunding transaction: {transactionHash}' => 'Error refunding transaction: {transactionHash}', - 'Every new store must be assigned to at least one site.' => 'Every new store must be assigned to at least one site.', - 'Everywhere' => 'Everywhere', - 'Example' => 'Example', - 'Exclude this discount for products that are already on promotion' => 'Exclude this discount for products that are already on promotion', - 'Expired Link' => 'Expired Link', - 'Expired' => 'Expired', - 'Expiry Date' => 'Expiry Date', - 'Expiry date' => 'Expiry date', - 'Expiry' => 'Expiry', - 'Failed to receive transfer: {error}' => 'Failed to receive transfer: {error}', - 'Failed to send email. Please try again.' => 'Failed to send email. Please try again.', - 'Failed to start' => 'Failed to start', - 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Failed to update {num, plural, =1{order status} other{order statuses}}.', - 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Failed updating order status on {num, plural, =1{order} other{orders}}.', - 'Feet (ft)' => 'Feet (ft)', - 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.', - 'First Name' => 'First Name', - 'Flat Amount Off Order' => 'Flat Amount Off Order', - 'Flat Order Discount Amount Off' => 'Flat Order Discount Amount Off', - 'Free Order Payment Strategy' => 'Free Order Payment Strategy', - 'Free Shipping' => 'Free Shipping', - 'Free orders are processed by the payment gateway' => 'Free orders are processed by the payment gateway', - 'Free orders complete immediately' => 'Free orders complete immediately', - 'Free shipping can only be for whole order or matching items, not both.' => 'Free shipping can only be for whole order or matching items, not both.', - 'From Name' => 'From Name', - 'Fulfill' => 'Fulfill', - 'Fulfilled' => 'Fulfilled', - 'Fulfillment' => 'Fulfillment', - 'Full Name' => 'Full Name', - 'Gateway Code' => 'Gateway Code', - 'Gateway Message' => 'Gateway Message', - 'Gateway Reference' => 'Gateway Reference', - 'Gateway Response' => 'Gateway Response', - 'Gateway doesn’t support authorize' => 'Gateway doesn’t support authorize', - 'Gateway doesn’t support partial refunds.' => 'Gateway doesn’t support partial refunds.', - 'Gateway doesn’t support purchase' => 'Gateway doesn’t support purchase', - 'Gateway doesn’t support refunds.' => 'Gateway doesn’t support refunds.', - 'Gateway saved.' => 'Gateway saved.', - 'Gateway' => 'Gateway', - 'Gateways reordered.' => 'Gateways reordered.', - 'Gateways' => 'Gateways', - 'General Settings' => 'General Settings', - 'General' => 'General', - 'Generate' => 'Generate', - 'Generated Coupon Format' => 'Generated Coupon Format', - 'Grams (g)' => 'Grams (g)', - 'Groups for which this sale will be applicable to.' => 'Groups for which this sale will be applicable to.', - 'HTML Email Template Path' => 'HTML Email Template Path', - 'Handle' => 'Handle', - 'Harmonized System Code' => 'Harmonized System Code', - 'Has Admin Notices' => 'Has Admin Notices', - 'Has Emails?' => 'Has Emails?', - 'Has Free Shipping' => 'Has Free Shipping', - 'Has Orders' => 'Has Orders', - 'Has Purchasable' => 'Has Purchasable', - 'Has Variants?' => 'Has Variants?', - 'Height ({unit})' => 'Height ({unit})', - 'Height' => 'Height', - 'Hide snapshot' => 'Hide snapshot', - 'History' => 'History', - 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).', - 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.', - 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.', - 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.', - 'How products should be labeled within the control panel.' => 'How products should be labeled within the control panel.', - 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).', - 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}', - 'How this shipping method will be referred to in templates and forms.' => 'How this shipping method will be referred to in templates and forms.', - 'How variants should be labeled within the control panel.' => 'How variants should be labeled within the control panel.', - 'How you’ll refer to this PDF in the templates.' => 'How you’ll refer to this PDF in the templates.', - 'How you’ll refer to this product type in the templates.' => 'How you’ll refer to this product type in the templates.', - 'How you’ll refer to this shipping category in the templates.' => 'How you’ll refer to this shipping category in the templates.', - 'How you’ll refer to this status in the templates.' => 'How you’ll refer to this status in the templates.', - 'How you’ll refer to this subscription plan in the templates.' => 'How you’ll refer to this subscription plan in the templates.', - 'How you’ll refer to this tax category in the templates.' => 'How you’ll refer to this tax category in the templates.', - 'ID' => 'ID', - 'IP Address' => 'IP Address', - 'If disabled, this PDF will not be available or sent with emails.' => 'If disabled, this PDF will not be available or sent with emails.', - 'If disabled, this email will not send.' => 'If disabled, this email will not send.', - 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.', - 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.', - 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.', - 'Ignore Promotions?' => 'Ignore Promotions?', - 'Ignore previous matching sales if this sale matches.' => 'Ignore previous matching sales if this sale matches.', - 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignore promotional prices when this discount is applied to matching line items', - 'Inactive Carts' => 'Inactive Carts', - 'Inches (in)' => 'Inches (in)', - 'Include built-in line item tax.' => 'Include built-in line item tax.', - 'Include in price?' => 'Include in price?', - 'Include line item discounts.' => 'Include line item discounts.', - 'Include line item shipping costs.' => 'Include line item shipping costs.', - 'Include separate line item tax.' => 'Include separate line item tax.', - 'Included in price?' => 'Included in price?', - 'Included' => 'Included', - 'Incoming transfer from Transfer ID: ' => 'Incoming transfer from Transfer ID: ', - 'Incoming' => 'Incoming', - 'Info' => 'Info', - 'Information linked?' => 'Information linked?', - 'Information' => 'Information', - 'Invalid JSON' => 'Invalid JSON', - 'Invalid Order ID' => 'Invalid Order ID', - 'Invalid VAT ID.' => 'Invalid VAT ID.', - 'Invalid condition syntax' => 'Invalid condition syntax', - 'Invalid email.' => 'Invalid email.', - 'Invalid formula syntax' => 'Invalid formula syntax', - 'Invalid gateway: {value}' => 'Invalid gateway: {value}', - 'Invalid inventory movements.' => 'Invalid inventory movements.', - 'Invalid order condition syntax.' => 'Invalid order condition syntax.', - 'Invalid payment or order. Please review.' => 'Invalid payment or order. Please review.', - 'Invalid payment source ID: {value}' => 'Invalid payment source ID: {value}', - 'Invalid store.' => 'Invalid store.', - 'Invalid user.' => 'Invalid user.', - 'Inventory Item' => 'Inventory Item', - 'Inventory Location' => 'Inventory Location', - 'Inventory Locations' => 'Inventory Locations', - 'Inventory Tracked' => 'Inventory Tracked', - 'Inventory Transfers' => 'Inventory Transfers', - 'Inventory could not be set.' => 'Inventory could not be set.', - 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'Inventory location has committed stock, the order(s) must first be fulfilled.', - 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Inventory location has incoming stock, the transfer(s) must first be completed.', - 'Inventory location is already deactivated.' => 'Inventory location is already deactivated.', - 'Inventory location saved.' => 'Inventory location saved.', - 'Inventory locations not saved.' => 'Inventory locations not saved.', - 'Inventory movement could not be saved.' => 'Inventory movement could not be saved.', - 'Inventory movement saved.' => 'Inventory movement saved.', - 'Inventory updated.' => 'Inventory updated.', - 'Inventory was not updated.' => 'Inventory was not updated.', - 'Inventory' => 'Inventory', - 'Invoice amount' => 'Invoice amount', - 'Invoice date' => 'Invoice date', - 'Is Promotable' => 'Is Promotable', - 'Is Promotional Price?' => 'Is Promotional Price?', - 'Is Shippable' => 'Is Shippable', - 'Is Taxable' => 'Is Taxable', - 'Item Rates' => 'Item Rates', - 'Item Subtotal' => 'Item Subtotal', - 'Item Total' => 'Item Total', - 'Item' => 'Item', - 'Items' => 'Items', - 'Kilograms (kg)' => 'Kilograms (kg)', - 'Label' => 'Label', - 'Landscape' => 'Landscape', - 'Language' => 'Language', - 'Last Name' => 'Last Name', - 'Last Updated' => 'Last Updated', - 'Leave a category rate override blank to use the rate from above.' => 'Leave a category rate override blank to use the rate from above.', - 'Leave blank for unlimited uses.' => 'Leave blank for unlimited uses.', - 'Leave blank if products don’t have URLs' => 'Leave blank if products don’t have URLs', - 'Leave gateway subscription as-is' => 'Leave gateway subscription as-is', - 'Length ({unit})' => 'Length ({unit})', - 'Length' => 'Length', - 'Let each product choose which sites it should be saved to' => 'Let each product choose which sites it should be saved to', - 'Limit which orders this discount applies to based on its line items.' => 'Limit which orders this discount applies to based on its line items.', - 'Limit which purchasables this sale applies to.' => 'Limit which purchasables this sale applies to.', - 'Limit' => 'Limit', - 'Line Item Statuses' => 'Line Item Statuses', - 'Line Item' => 'Line Item', - 'Line Items' => 'Line Items', - 'Line item price (minus discounts)' => 'Line item price (minus discounts)', - 'Line item shipping cost' => 'Line item shipping cost', - 'Line item statuses reordered.' => 'Line item statuses reordered.', - 'Link Duration' => 'Link Duration', - 'Link Sent' => 'Link Sent', - 'Link to a product' => 'Link to a product', - 'Link to a variant' => 'Link to a variant', - 'Link' => 'Link', - 'Live' => 'Live', - 'Location' => 'Location', - 'Locations that should be available for previewing products in this product type.' => 'Locations that should be available for previewing products in this product type.', - 'MM' => 'MM', - 'Make a payment' => 'Make a payment', - 'Make this the primary store' => 'Make this the primary store', - 'Manage Inventory' => 'Manage Inventory', - 'Manage donation settings' => 'Manage donation settings', - 'Manage general store settings' => 'Manage general store settings', - 'Manage inventory locations' => 'Manage inventory locations', - 'Manage inventory stock levels' => 'Manage inventory stock levels', - 'Manage inventory transfers' => 'Manage inventory transfers', - 'Manage orders' => 'Manage orders', - 'Manage payment currencies' => 'Manage payment currencies', - 'Manage promotions' => 'Manage promotions', - 'Manage shipping' => 'Manage shipping', - 'Manage store settings' => 'Manage store settings', - 'Manage subscription plans' => 'Manage subscription plans', - 'Manage subscription' => 'Manage subscription', - 'Manage subscriptions' => 'Manage subscriptions', - 'Manage taxes' => 'Manage taxes', - 'Manage' => 'Manage', - 'Mark as Pending' => 'Mark as Pending', - 'Mark as completed' => 'Mark as completed', - 'Match Billing Address' => 'Match Billing Address', - 'Match Customer' => 'Match Customer', - 'Match Order' => 'Match Order', - 'Match Orders' => 'Match Orders', - 'Match Product' => 'Match Product', - 'Match Purchasable' => 'Match Purchasable', - 'Match Shipping Address' => 'Match Shipping Address', - 'Match Variant' => 'Match Variant', - 'Matching Items' => 'Matching Items', - 'Max Qty' => 'Max Qty', - 'Max Uses' => 'Max Uses', - 'Max Variants' => 'Max Variants', - 'Max quantity must greater than min.' => 'Max quantity must greater than min.', - 'Maximum Purchase Quantity' => 'Maximum Purchase Quantity', - 'Maximum Total Shipping Cost' => 'Maximum Total Shipping Cost', - 'Maximum allowed quantity' => 'Maximum allowed quantity', - 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.', - 'Maximum order quantity for this item is {num}.' => 'Maximum order quantity for this item is {num}.', - 'Message' => 'Message', - 'Meters (m)' => 'Meters (m)', - 'Millimeters (mm)' => 'Millimeters (mm)', - 'Min Qty' => 'Min Qty', - 'Min quantity must be less than max.' => 'Min quantity must be less than max.', - 'Minimum Purchase Quantity' => 'Minimum Purchase Quantity', - 'Minimum Total Price Strategy' => 'Minimum Total Price Strategy', - 'Minimum Total Shipping Cost' => 'Minimum Total Shipping Cost', - 'Minimum allowed quantity' => 'Minimum allowed quantity', - 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Minimum number of matching items that need to be ordered for this discount to apply.', - 'Minimum order quantity for this item is {num}.' => 'Minimum order quantity for this item is {num}.', - 'Missing Gateway' => 'Missing Gateway', - 'Missing a default inventory location.' => 'Missing a default inventory location.', - 'Move Inventory' => 'Move Inventory', - 'Move To' => 'Move To', - 'Move {qty} from {fromType} to {toType}' => 'Move {qty} from {fromType} to {toType}', - 'Move' => 'Move', - 'Movement from deactivated inventory location' => 'Movement from deactivated inventory location', - 'Movement' => 'Movement', - 'Must have at least one variant.' => 'Must have at least one variant.', - 'Name Field' => 'Name Field', - 'Name' => 'Name', - 'New Customer' => 'New Customer', - 'New Customers' => 'New Customers', - 'New Order' => 'New order', - 'New PDF' => 'New PDF', - 'New address' => 'New address', - 'New catalog pricing rule' => 'New catalog pricing rule', - 'New currency' => 'New currency', - 'New discount' => 'New discount', - 'New email' => 'New email', - 'New gateway' => 'New gateway', - 'New line item status' => 'New line item status', - 'New line items get this status by default when the order is completed' => 'New line items get this status by default when the order is completed', - 'New location' => 'New location', - 'New order status' => 'New order status', - 'New orders get this status by default' => 'New orders get this status by default', - 'New product type' => 'New product type', - 'New product' => 'New product', - 'New product, choose a type' => 'New product, choose a type', - 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'New products default to the first tax category available to them. If none are available, this category will be used.', - 'New sale' => 'New sale', - 'New shipping category' => 'New shipping category', - 'New shipping method' => 'New shipping method', - 'New shipping rule' => 'New shipping rule', - 'New shipping zone' => 'New shipping zone', - 'New subscription plan' => 'New subscription plan', - 'New tax category' => 'New tax category', - 'New tax rate' => 'New tax rate', - 'New tax zone' => 'New tax zone', - 'New transfer' => 'New transfer', - 'New {productType} product' => 'New {productType} product', - 'New' => 'New', - 'Next payment' => 'Next payment', - 'No Address' => 'No Address', - 'No PDFs exist yet.' => 'No PDFs exist yet.', - 'No access given to any specific store management features.' => 'No access given to any specific store management features.', - 'No additional payment currencies exist yet.' => 'No additional payment currencies exist yet.', - 'No address' => 'No address', - 'No billing address' => 'No billing address', - 'No catalog pricing rule exists with the ID “{id}”' => 'No catalog pricing rule exists with the ID “{id}”', - 'No catalog pricing rules exist yet.' => 'No catalog pricing rules exist yet.', - 'No currency exists with the ID “{id}”' => 'No currency exists with the ID “{id}”', - 'No customer email address exists on this cart.' => 'No customer email address exists on this cart.', - 'No description' => 'No description', - 'No discount exists with the ID “{id}”' => 'No discount exists with the ID “{id}”', - 'No discounts exist yet.' => 'No discounts exist yet.', - 'No donation amount supplied.' => 'No donation amount supplied.', - 'No emails exist yet.' => 'No emails exist yet.', - 'No inventory changes made.' => 'No inventory changes made.', - 'No inventory found.' => 'No inventory found.', - 'No inventory movements made.' => 'No inventory movements made.', - 'No inventory transactions for this location.' => 'No inventory transactions for this location.', - 'No new customer selected.' => 'No new customer selected.', - 'No order history exists with the ID “{id}”' => 'No order history exists with the ID “{id}”', - 'No order status history items will exist until the cart becomes an order.' => 'No order status history items will exist until the cart becomes an order.', - 'No payment source exists with the ID “{id}”' => 'No payment source exists with the ID “{id}”', - 'No private Note.' => 'No private Note.', - 'No product available.' => 'No product available.', - 'No product types exist yet.' => 'No product types exist yet.', - 'No purchasable available.' => 'No purchasable available.', - 'No sale exists with the ID “{id}”' => 'No sale exists with the ID “{id}”', - 'No sales exist yet.' => 'No sales exist yet.', - 'No shipping address' => 'No shipping address', - 'No shipping category exists with the ID “{id}”' => 'No shipping category exists with the ID “{id}”', - 'No shipping method exists with the ID “{id}”' => 'No shipping method exists with the ID “{id}”', - 'No shipping rule exists with the ID “{id}”' => 'No shipping rule exists with the ID “{id}”', - 'No shipping rules exist yet.' => 'No shipping rules exist yet.', - 'No shipping zone exists with the ID “{id}”' => 'No shipping zone exists with the ID “{id}”', - 'No stats available.' => 'No stats available.', - 'No subscription plan exists with the ID “{id}”' => 'No subscription plan exists with the ID “{id}”', - 'No subscription plans exist yet.' => 'No subscription plans exist yet.', - 'No tax category exists with the ID “{id}”' => 'No tax category exists with the ID “{id}”', - 'No tax rate exists with the ID “{id}”' => 'No tax rate exists with the ID “{id}”', - 'No tax zone exists with the ID “{id}”' => 'No tax zone exists with the ID “{id}”', - 'No transactions exist.' => 'No transactions exist.', - 'No user authenticated.' => 'No user authenticated.', - 'No' => 'No', - 'None on hand' => 'None on hand', - 'None' => 'None', - 'Not a valid address type' => 'Not a valid address type', - 'Not a valid credit card number.' => 'Not a valid credit card number.', - 'Not all SKUs are unique.' => 'Not all SKUs are unique.', - 'Note' => 'Note', - 'Notes' => 'Notes', - 'Number of Coupons' => 'Number of Coupons', - 'Number' => 'Number', - 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Of the enabled sites above, which sites should products in this product type be saved to?', - 'On Hand' => 'On Hand', - 'Only allow this gateway to be used for zero value orders?' => 'Only allow this gateway to be used for zero value orders?', - 'Only match certain purchasables…' => 'Only match certain purchasables…', - 'Only match purchasables related to…' => 'Only match purchasables related to…', - 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Only orders with the following order statuses will be included. Leave blank to include all statuses.', - 'Only save product to the site they were created in' => 'Only save product to the site they were created in', - 'Options' => 'Options', - 'Order Condition Formula' => 'Order Condition Formula', - 'Order Description Format' => 'Order Description Format', - 'Order Details' => 'Order Details', - 'Order Fields' => 'Order Fields', - 'Order PDF Download Link' => 'Order PDF Download Link', - 'Order PDF Filename Format' => 'Order PDF Filename Format', - 'Order Reference Number Format' => 'Order Reference Number Format', - 'Order Settings' => 'Order Settings', - 'Order Site' => 'Order Site', - 'Order Status description.' => 'Order Status description.', - 'Order Status' => 'Order Status', - 'Order Statuses' => 'Order Statuses', - 'Order can not be empty.' => 'Order can not be empty.', - 'Order count' => 'Order count', - 'Order customer data removed.' => 'Order customer data removed.', - 'Order deleted.' => 'Order deleted.', - 'Order fields saved.' => 'Order fields saved.', - 'Order not found.' => 'Order not found.', - 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.', - 'Order recalculated.' => 'Order recalculated.', - 'Order status saved.' => 'Order status saved.', - 'Order statuses reordered.' => 'Order statuses reordered.', - 'Order total shipping cost' => 'Order total shipping cost', - 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)', - 'Order' => 'Order', - 'Orders (Legacy)' => 'Orders (Legacy)', - 'Orders deleted.' => 'Orders deleted.', - 'Orders not restored.' => 'Orders not restored.', - 'Orders restored.' => 'Orders restored.', - 'Orders' => 'Orders', - 'Organization Name' => 'Organization Name', - 'Organization Tax ID' => 'Organization Tax ID', - 'Origin and destination cannot be the same.' => 'Origin and destination cannot be the same.', - 'Origin' => 'Origin', - 'Original Price' => 'Original Price', - 'Original price' => 'Original price', - 'Original promotional price' => 'Original promotional price', - 'Other Languages' => 'Other Languages', - 'Other countries' => 'Other countries', - 'Outgoing transfer from Transfer ID: ' => 'Outgoing transfer from Transfer ID: ', - 'Overpaid' => 'Overpaid', - 'Overrides previous?' => 'Overrides previous?', - 'PDF Attachment' => 'PDF Attachment', - 'PDF Template Path' => 'PDF Template Path', - 'PDF saved.' => 'PDF saved.', - 'PDF' => 'PDF', - 'PDFs & Emails' => 'PDFs & Emails', - 'PDFs' => 'PDFs', - 'Paid Amount' => 'Paid Amount', - 'Paid Status' => 'Paid Status', - 'Paid' => 'Paid', - 'Paper Orientation' => 'Paper Orientation', - 'Paper Size' => 'Paper Size', - 'Partial payment not allowed.' => 'Partial payment not allowed.', - 'Partial' => 'Partial', - 'Past year' => 'Past year', - 'Past {num} days' => 'Past {num} days', - 'Pay {amount} of {currency} on the order.' => 'Pay {amount} of {currency} on the order.', - 'Pay' => 'Pay', - 'Payment Amount' => 'Payment Amount', - 'Payment Currencies' => 'Payment Currencies', - 'Payment Gateway' => 'Payment Gateway', - 'Payment Method' => 'Payment Method', - 'Payment error: {message}' => 'Payment error: {message}', - 'Payment method issue' => 'Payment method issue', - 'Payment source created.' => 'Payment source created.', - 'Payment source deleted.' => 'Payment source deleted.', - 'Payments' => 'Payments', - 'Pending' => 'Pending', - 'Per Email Address Discount Limit' => 'Per Email Address Discount Limit', - 'Per Item Amount Off' => 'Per Item Amount Off', - 'Per Item Discount' => 'Per Item Discount', - 'Per Item Percentage Off' => 'Per Item Percentage Off', - 'Per Item Rate' => 'Per Item Rate', - 'Per User Discount Limit' => 'Per User Discount Limit', - 'Percentage Rate' => 'Percentage Rate', - 'Phone (Alt)' => 'Phone (Alt)', - 'Phone' => 'Phone', - 'Pick a plan' => 'Pick a plan', - 'Plain Text Email Template Path' => 'Plain Text Email Template Path', - 'Plan' => 'Plan', - 'Plans reordered.' => 'Plans reordered.', - 'Portrait' => 'Portrait', - 'Post Date' => 'Post Date', - 'Postal Code Formula' => 'Postal Code Formula', - 'Pounds (lb)' => 'Pounds (lb)', - 'Preview' => 'Preview', - 'Previous Status' => 'Previous Status', - 'Price' => 'Price', - 'Prices' => 'Prices', - 'Pricing Rules' => 'Pricing Rules', - 'Pricing jobs are currently running.' => 'Pricing jobs are currently running.', - 'Pricing' => 'Pricing', - 'Primary Billing Address' => 'Primary Billing Address', - 'Primary Shipping Address' => 'Primary Shipping Address', - 'Primary payment source updated.' => 'Primary payment source updated.', - 'Primary' => 'Primary', - 'Private Note' => 'Private Note', - 'Product Fields' => 'Product Fields', - 'Product ID is required.' => 'Product ID is required.', - 'Product Template' => 'Product Template', - 'Product Title Format' => 'Product Title Format', - 'Product Type' => 'Product Type', - 'Product Types' => 'Product Types', - 'Product URI Format' => 'Product URI Format', - 'Product Variant' => 'Product Variant', - 'Product Variants' => 'Product Variants', - 'Product type saved.' => 'Product type saved.', - 'Product type settings' => 'Product type settings', - 'Product' => 'Product', - 'Products and Variants deleted.' => 'Products and Variants deleted.', - 'Products not restored.' => 'Products not restored.', - 'Products restored.' => 'Products restored.', - 'Products' => 'Products', - 'Promotable' => 'Promotable', - 'Promotable?' => 'Promotable?', - 'Promotional Amount' => 'Promotional Amount', - 'Promotional Price' => 'Promotional Price', - 'Purchasable Categories' => 'Purchasable Categories', - 'Purchasable ID and Sale ID are required.' => 'Purchasable ID and Sale ID are required.', - 'Purchasable ID is required.' => 'Purchasable ID is required.', - 'Purchasable Type' => 'Purchasable Type', - 'Purchasable' => 'Purchasable', - 'Purchase (Authorize and Capture Immediately)' => 'Purchase (Authorize and Capture Immediately)', - 'Purchase Total' => 'Purchase Total', - 'Qty' => 'Qty', - 'Quality Control' => 'Quality Control', - 'Quantity' => 'Quantity', - 'Rate' => 'Rate', - 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Reassign {numOrders, plural, =1{order} other{orders}}', - 'Recalculate order' => 'Recalculate order', - 'Receive Inventory' => 'Receive Inventory', - 'Receive Transfer' => 'Receive Transfer', - 'Receive' => 'Receive', - 'Received' => 'Received', - 'Recent Orders' => 'Recent Orders', - 'Recipient' => 'Recipient', - 'Recover Cart' => 'Recover Cart', - 'Reduce price' => 'Reduce price', - 'Reduce the price by a fixed amount' => 'Reduce the price by a fixed amount', - 'Reduce the price by a percentage of the original price' => 'Reduce the price by a percentage of the original price', - 'Reference' => 'Reference', - 'Refresh payment history' => 'Refresh payment history', - 'Refund note' => 'Refund note', - 'Refund payment' => 'Refund payment', - 'Refund' => 'Refund', - 'Reject' => 'Reject', - 'Rejected' => 'Rejected', - 'Relationship Type' => 'Relationship Type', - 'Removable included tax rates are only allowed for the default tax zone.' => 'Removable included tax rates are only allowed for the default tax zone.', - 'Remove address' => 'Remove address', - 'Remove all shipping costs from the order' => 'Remove all shipping costs from the order', - 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below', - 'Remove customer data' => 'Remove customer data', - 'Remove from price?' => 'Remove from price?', - 'Remove shipping costs for matching items only' => 'Remove shipping costs for matching items only', - 'Remove the included tax when a valid organization tax ID is present?' => 'Remove the included tax when a valid organization tax ID is present?', - 'Remove' => 'Remove', - 'Removed' => 'Removed', - 'Repeat Customers' => 'Repeat Customers', - 'Reply To' => 'Reply To', - 'Require Billing Address At Checkout' => 'Require Billing Address At Checkout', - 'Require Coupon Code' => 'Require Coupon Code', - 'Require Shipping Address At Checkout' => 'Require Shipping Address At Checkout', - 'Require Shipping Method Selection At Checkout' => 'Require Shipping Method Selection At Checkout', - 'Require' => 'Require', - 'Reserved' => 'Reserved', - 'Reset usage' => 'Reset usage', - 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.', - 'Revenue Options' => 'Revenue Options', - 'Revenue' => 'Revenue', - 'Rule' => 'Rule', - 'Rules reordered.' => 'Rules reordered.', - 'SKU' => 'SKU', - 'Safety' => 'Safety', - 'Sale Price' => 'Sale Price', - 'Sale description.' => 'Sale description.', - 'Sale reordered.' => 'Sale reordered.', - 'Sale saved.' => 'Sale saved.', - 'Sale' => 'Sale', - 'Sales deleted.' => 'Sales deleted.', - 'Sales updated.' => 'Sales updated.', - 'Sales' => 'Sales', - 'Save and continue editing' => 'Save and continue editing', - 'Save and return to all orders' => 'Save and return to all orders', - 'Save and set rules' => 'Save and set rules', - 'Save as a new rule' => 'Save as a new rule', - 'Save product to all sites enabled for this product type' => 'Save product to all sites enabled for this product type', - 'Save product to other sites in the same site group' => 'Save product to other sites in the same site group', - 'Save product to other sites with the same language' => 'Save product to other sites with the same language', - 'Save' => 'Save', - 'Search customer…' => 'Search customer…', - 'Search inventory' => 'Search inventory', - 'Search or enter customer email…' => 'Search or enter customer email…', - 'Search…' => 'Search…', - 'See Orders' => 'See Orders', - 'Select a gateway' => 'Select a gateway', - 'Select a tax category.' => 'Select a tax category.', - 'Select a tax zone. If empty, this rate will match anywhere.' => 'Select a tax zone. If empty, this rate will match anywhere.', - 'Select address' => 'Select address', - 'Select an item' => 'Select an item', - 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Select how the catalog pricing rule will be applied to the purchasable(s).', - 'Select how the sale will be applied to the purchasable(s).' => 'Select how the sale will be applied to the purchasable(s).', - 'Select product type' => 'Select product type', - 'Select the emails that will be sent when transitioning to this status.' => 'Select the emails that will be sent when transitioning to this status.', - 'Select what this rate should be applied to.' => 'Select what this rate should be applied to.', - 'Send Email' => 'Send Email', - 'Send to custom recipient' => 'Send to custom recipient', - 'Send to the customer' => 'Send to the customer', - 'Set Quantity' => 'Set Quantity', - 'Set default category' => 'Set default category', - 'Set default variant' => 'Set default variant', - 'Set or Adjust' => 'Set or Adjust', - 'Set price' => 'Set price', - 'Set status' => 'Set status', - 'Set the price to a flat amount' => 'Set the price to a flat amount', - 'Set the price to a percentage of the original price' => 'Set the price to a percentage of the original price', - 'Set the sale price to a flat amount' => 'Set the sale price to a flat amount', - 'Set the sale price to a percentage of the original price' => 'Set the sale price to a percentage of the original price', - 'Set to' => 'Set to', - 'Settings saved.' => 'Settings saved.', - 'Settings' => 'Settings', - 'Share cart…' => 'Share cart…', - 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.', - 'Shipping Address Zone' => 'Shipping Address Zone', - 'Shipping Address' => 'Shipping Address', - 'Shipping Business Name' => 'Shipping Business Name', - 'Shipping Categories' => 'Shipping Categories', - 'Shipping Category Conditions' => 'Shipping Category Conditions', - 'Shipping Category' => 'Shipping Category', - 'Shipping First Name' => 'Shipping First Name', - 'Shipping Full Name' => 'Shipping Full Name', - 'Shipping Last Name' => 'Shipping Last Name', - 'Shipping Method' => 'Shipping Method', - 'Shipping Methods' => 'Shipping Methods', - 'Shipping Rule' => 'Shipping Rule', - 'Shipping Zones' => 'Shipping Zones', - 'Shipping address required.' => 'Shipping address required.', - 'Shipping categories deleted.' => 'Shipping categories deleted.', - 'Shipping category saved.' => 'Shipping category saved.', - 'Shipping category updated.' => 'Shipping category updated.', - 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.', - 'Shipping method saved.' => 'Shipping method saved.', - 'Shipping methods and rules deleted.' => 'Shipping methods and rules deleted.', - 'Shipping methods updated.' => 'Shipping methods updated.', - 'Shipping rule saved.' => 'Shipping rule saved.', - 'Shipping zone saved.' => 'Shipping zone saved.', - 'Shipping' => 'Shipping', - 'Short Number' => 'Short Number', - 'Show Chart?' => 'Show Chart?', - 'Show Order Count?' => 'Show Order Count?', - 'Show all prices' => 'Show all prices', - 'Show archived gateways' => 'Show archived gateways', - 'Show order count line on chart.' => 'Show order count line on chart.', - 'Show related sales' => 'Show related sales', - 'Show rule details' => 'Show rule details', - 'Show the Dimensions and Weight fields for products of this type' => 'Show the Dimensions and Weight fields for products of this type', - 'Show the Title field for products' => 'Show the Title field for products', - 'Show the Title field for variants' => 'Show the Title field for variants', - 'Signed In' => 'Signed In', - 'Site Languages' => 'Site Languages', - 'Site store mapping saved.' => 'Site store mapping saved.', - 'Sites' => 'Sites', - 'Slug' => 'Slug', - 'Snapshot' => 'Snapshot', - 'Snapshots' => 'Snapshots', - 'Some orders restored.' => 'Some orders restored.', - 'Some products restored.' => 'Some products restored.', - 'Some variants restored.' => 'Some variants restored.', - 'Something changed with the order before payment, please review your order and submit payment again.' => 'Something changed with the order before payment, please review your order and submit payment again.', - 'Sorry, no matching options.' => 'Sorry, no matching options.', - 'Source - The purchasable relationship field is on the category' => 'Source - The purchasable relationship field is on the category', - 'Source' => 'Source', - 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)', - 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)', - 'Start Date' => 'Start Date', - 'State' => 'State', - 'Status Email Address' => 'Status Email Address', - 'Status Emails' => 'Status Emails', - 'Status History' => 'Status History', - 'Status Updated.' => 'Status Updated.', - 'Status change message' => 'Status change message', - 'Status' => 'Status', - 'Stock' => 'Stock', - 'Stops Processing?' => 'Stops Processing?', - 'Stops subsequent?' => 'Stops subsequent?', - 'Store Location' => 'Store Location', - 'Store Management' => 'Store Management', - 'Store Markets' => 'Store Markets', - 'Store Rule' => 'Store Rule', - 'Store saved.' => 'Store saved.', - 'Store' => 'Store', - 'Stores & Sites' => 'Stores & Sites', - 'Stores' => 'Stores', - 'Strategy to apply when an order is free or has a zero balance.' => 'Strategy to apply when an order is free or has a zero balance.', - 'Strategy to apply when calculating the minimum order price.' => 'Strategy to apply when calculating the minimum order price.', - 'Subject' => 'Subject', - 'Subscribing user' => 'Subscribing user', - 'Subscription Fields' => 'Subscription Fields', - 'Subscription Plans' => 'Subscription Plans', - 'Subscription Settings' => 'Subscription Settings', - 'Subscription cancelled.' => 'Subscription cancelled.', - 'Subscription date' => 'Subscription date', - 'Subscription fields saved.' => 'Subscription fields saved.', - 'Subscription for {user} to {plan} prevented by a plugin.' => 'Subscription for {user} to {plan} prevented by a plugin.', - 'Subscription plan saved.' => 'Subscription plan saved.', - 'Subscription plan' => 'Subscription plan', - 'Subscription plans' => 'Subscription plans', - 'Subscription reactivated.' => 'Subscription reactivated.', - 'Subscription reference' => 'Subscription reference', - 'Subscription started.' => 'Subscription started.', - 'Subscription switched.' => 'Subscription switched.', - 'Subscription to “{plan}”' => 'Subscription to “{plan}”', - 'Subscription' => 'Subscription', - 'Subscriptions on hold' => 'Subscriptions on hold', - 'Subscriptions' => 'Subscriptions', - 'Suppress emails' => 'Suppress emails', - 'Switch plan' => 'Switch plan', - 'Switch' => 'Switch', - 'System' => 'System', - 'Table Columns' => 'Table Columns', - 'Target - The category relationship field is on the purchasable' => 'Target - The category relationship field is on the purchasable', - 'Tax & Shipping' => 'Tax & Shipping', - 'Tax (inc)' => 'Tax (inc)', - 'Tax Categories' => 'Tax Categories', - 'Tax Category' => 'Tax Category', - 'Tax Rates' => 'Tax Rates', - 'Tax Zone' => 'Tax Zone', - 'Tax Zones' => 'Tax Zones', - 'Tax categories deleted.' => 'Tax categories deleted.', - 'Tax category saved.' => 'Tax category saved.', - 'Tax category updated.' => 'Tax category updated.', - 'Tax rate saved.' => 'Tax rate saved.', - 'Tax rates updated.' => 'Tax rates updated.', - 'Tax zone saved.' => 'Tax zone saved.', - 'Tax' => 'Tax', - 'Taxable Subject' => 'Taxable Subject', - 'Template Path' => 'Template Path', - 'That handle is already in use' => 'That handle is already in use', - 'That handle is already in use.' => 'That handle is already in use.', - 'The PDF to attach to this email.' => 'The PDF to attach to this email.', - 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.', - 'The address provided is outside the store’s market.' => 'The address provided is outside the store’s market.', - 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.', - 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.', - 'The cart recovery link is invalid. Please request a new one.' => 'The cart recovery link is invalid. Please request a new one.', - 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.', - 'The countries that orders are allowed to be placed from.' => 'The countries that orders are allowed to be placed from.', - 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'The coupon "{code}" has exceeded its usage limit of {limit}.', - 'The customer for this order has been deleted.' => 'The customer for this order has been deleted.', - 'The default shipping category is automatically available to all product types.' => 'The default shipping category is automatically available to all product types.', - 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'The discount "{name}" has exceeded its total usage limit of {limit}.', - 'The download link has expired. Please request a new one.' => 'The download link has expired. Please request a new one.', - 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.', - 'The entry that contains the description for this subscription’s plan.' => 'The entry that contains the description for this subscription’s plan.', - 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'The flat value which should discount each item. i.e “3” for $3 off each item.', - 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.', - 'The from and to inventory locations must be different.' => 'The from and to inventory locations must be different.', - 'The inventory locations this store uses.' => 'The inventory locations this store uses.', - 'The item is not enabled for sale.' => 'The item is not enabled for sale.', - 'The language the order was made in.' => 'The language the order was made in.', - 'The language to be used when this email is rendered.' => 'The language to be used when this email is rendered.', - 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'The maximum number of levels this product type can have. Leave blank if you don’t care.', - 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'The maximum the customer should spend on shipping. Set to zero to disable.', - 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'The minimum the customer should spend on shipping. Set to zero to disable.', - 'The order is not valid.' => 'The order is not valid.', - 'The payment gateway that will be used for the subscription plan.' => 'The payment gateway that will be used for the subscription plan.', - 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.', - 'The previously-selected shipping method is no longer available.' => 'The previously-selected shipping method is no longer available.', - 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}', - 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}', - 'The primary currency cannot be changed after orders are placed.' => 'The primary currency cannot be changed after orders are placed.', - 'The purchasable defines the relationship' => 'The purchasable defines the relationship', - 'The purchasable is related by another element' => 'The purchasable is related by another element', - 'The recipient of the email. Twig code can be used here.' => 'The recipient of the email. Twig code can be used here.', - 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.', - 'The site the order was made in.' => 'The site the order was made in.', - 'The site to be used when this email is rendered.' => 'The site to be used when this email is rendered.', - 'The subject line of the email. Twig code can be used here.' => 'The subject line of the email. Twig code can be used here.', - 'The template that the PDF should be generated from.' => 'The template that the PDF should be generated from.', - 'The template to be used for HTML emails.' => 'The template to be used for HTML emails.', - 'The template to be used for plain text emails. Twig code can be used here.' => 'The template to be used for plain text emails. Twig code can be used here.', - 'The template to use when a product’s URL is requested.' => 'The template to use when a product’s URL is requested.', - 'The total number of order adjustments changed.' => 'The total number of order adjustments changed.', - 'The total price of the order changed.' => 'The total price of the order changed.', - 'The total quantity of items within the order changed.' => 'The total quantity of items within the order changed.', - 'The unique SKU of the donation purchasable.' => 'The unique SKU of the donation purchasable.', - 'The unit of measurement that should be used when specifying product dimensions.' => 'The unit of measurement that should be used when specifying product dimensions.', - 'The unit of measurement that should be used when specifying product weights.' => 'The unit of measurement that should be used when specifying product weights.', - 'The webhook URL for this gateway.' => 'The webhook URL for this gateway.', - 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.', - 'There are errors on the order' => 'There are errors on the order', - 'There are only {num} “{description}” items left in stock.' => 'There are only {num} “{description}” items left in stock.', - 'There aren’t any product types to select yet.' => 'There aren’t any product types to select yet.', - 'There is no gateway or payment source available for use with this order.' => 'There is no gateway or payment source available for use with this order.', - 'There is no gateway selected that supports payment sources.' => 'There is no gateway selected that supports payment sources.', - 'There is no shipping method selected for this order.' => 'There is no shipping method selected for this order.', - 'This URL will load the cart into the user’s session, making it the active cart.' => 'This URL will load the cart into the user’s session, making it the active cart.', - 'This action is not allowed for the current user.' => 'This action is not allowed for the current user.', - 'This category will be used as the default for all purchasables in this store.' => 'This category will be used as the default for all purchasables in this store.', - 'This coupon is for registered users and limited to {limit} uses.' => 'This coupon is for registered users and limited to {limit} uses.', - 'This coupon is limited to {limit} uses.' => 'This coupon is limited to {limit} uses.', - 'This coupon requires an email address.' => 'This coupon requires an email address.', - 'This gateway does not support that functionality.' => 'This gateway does not support that functionality.', - 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'This is being overridden by the {setting} config setting in `config/{file}.php`.', - 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.', - 'This is the default PDF that will be rendered when requesting the order PDF.' => 'This is the default PDF that will be rendered when requesting the order PDF.', - 'This is the last location for the {store} store.' => 'This is the last location for the {store} store.', - 'This month' => 'This month', - 'This order has unsaved changes.' => 'This order has unsaved changes.', - 'This week' => 'This week', - 'This year' => 'This year', - 'Times Used' => 'Times Used', - 'Title' => 'Title', - 'To' => 'To', - 'Today' => 'Today', - 'Too many variants for this product.' => 'Too many variants for this product.', - 'Top Customers by Average Order' => 'Top Customers by Average Order', - 'Top Customers by Total Revenue' => 'Top Customers by Total Revenue', - 'Top Customers' => 'Top Customers', - 'Top Product Types by Qty Sold' => 'Top Product Types by Qty Sold', - 'Top Product Types by Revenue' => 'Top Product Types by Revenue', - 'Top Product Types' => 'Top Product Types', - 'Top Products by Qty Sold' => 'Top Products by Qty Sold', - 'Top Products by Revenue' => 'Top Products by Revenue', - 'Top Products' => 'Top Products', - 'Top Purchasables by Qty Sold' => 'Top Purchasables by Qty Sold', - 'Top Purchasables by Revenue' => 'Top Purchasables by Revenue', - 'Top Purchasables' => 'Top Purchasables', - 'Total ' => 'Total ', - 'Total Discount Use Limit' => 'Total Discount Use Limit', - 'Total Discount' => 'Total Discount', - 'Total Included Tax' => 'Total Included Tax', - 'Total Orders by Billing Country' => 'Total Orders by Billing Country', - 'Total Orders by Country' => 'Total Orders by Country', - 'Total Orders by Shipping Country' => 'Total Orders by Shipping Country', - 'Total Orders' => 'Total Orders', - 'Total Paid' => 'Total Paid', - 'Total Price' => 'Total Price', - 'Total Qty' => 'Total Qty', - 'Total Revenue' => 'Total Revenue', - 'Total Shipping' => 'Total Shipping', - 'Total Tax' => 'Total Tax', - 'Total Weight' => 'Total Weight', - 'Total' => 'Total', - 'Track Inventory' => 'Track Inventory', - 'Transaction Hash' => 'Transaction Hash', - 'Transaction ID' => 'Transaction ID', - 'Transaction captured successfully: {message}' => 'Transaction captured successfully: {message}', - 'Transaction refunded successfully: {message}' => 'Transaction refunded successfully: {message}', - 'Transactions' => 'Transactions', - 'Transfer Fields' => 'Transfer Fields', - 'Transfer Items' => 'Transfer Items', - 'Transfer Settings' => 'Transfer Settings', - 'Transfer Status' => 'Transfer Status', - 'Transfer fields saved.' => 'Transfer fields saved.', - 'Transfer must have at least one item.' => 'Transfer must have at least one item.', - 'Transfer' => 'Transfer', - 'Transfers' => 'Transfers', - 'Trial days credited' => 'Trial days credited', - 'Trial expiration' => 'Trial expiration', - 'Trial expiry date' => 'Trial expiry date', - 'Type not in allowed options.' => 'Type not in allowed options.', - 'Type' => 'Type', - 'URI' => 'URI', - 'Unable to cancel subscription at this time.' => 'Unable to cancel subscription at this time.', - 'Unable to complete order: another request is already in progress.' => 'Unable to complete order: another request is already in progress.', - 'Unable to find variant.' => 'Unable to find variant.', - 'Unable to generate coupon codes: {message}' => 'Unable to generate coupon codes: {message}', - 'Unable to make payment at this time.' => 'Unable to make payment at this time.', - 'Unable to modify subscription at this time.' => 'Unable to modify subscription at this time.', - 'Unable to reactivate subscription at this time.' => 'Unable to reactivate subscription at this time.', - 'Unable to reassign orders.' => 'Unable to reassign orders.', - 'Unable to remove order data.' => 'Unable to remove order data.', - 'Unable to retrieve Sale and Purchasable.' => 'Unable to retrieve Sale and Purchasable.', - 'Unable to retrieve cart.' => 'Unable to retrieve cart.', - 'Unable to retrieve customer.' => 'Unable to retrieve customer.', - 'Unable to retrieve load cart URL' => 'Unable to retrieve load cart URL', - 'Unable to retrieve payment source.' => 'Unable to retrieve payment source.', - 'Unable to set default shipping category.' => 'Unable to set default shipping category.', - 'Unable to set default tax category.' => 'Unable to set default tax category.', - 'Unable to set primary payment source.' => 'Unable to set primary payment source.', - 'Unable to start the subscription. Please check your payment details.' => 'Unable to start the subscription. Please check your payment details.', - 'Unable to subscribe at this time.' => 'Unable to subscribe at this time.', - 'Unable to update cart.' => 'Unable to update cart.', - 'Unable to validate address.' => 'Unable to validate address.', - 'Unit Price' => 'Unit Price', - 'Unit price (minus discounts)' => 'Unit price (minus discounts)', - 'Units' => 'Units', - 'Unpaid' => 'Unpaid', - 'Unsubscribe' => 'Unsubscribe', - 'Update Address' => 'Update Address', - 'Update Order Status' => 'Update Order Status', - 'Update Order Status…' => 'Update Order Status…', - 'Update order' => 'Update order', - 'Update subscription' => 'Update subscription', - 'Update' => 'Update', - 'Updated By' => 'Updated By', - 'Updated committed stock successfully.' => 'Updated committed stock successfully.', - 'Updated' => 'Updated', - 'Use Billing Address For Tax' => 'Use Billing Address For Tax', - 'Use as the primary billing address' => 'Use as the primary billing address', - 'Use as the primary shipping address' => 'Use as the primary shipping address', - 'Used By Tax Rates' => 'Used By Tax Rates', - 'Used by Tax Rates' => 'Used by Tax Rates', - 'User Groups' => 'User Groups', - 'User not found.' => 'User not found.', - 'User' => 'User', - 'Uses' => 'Uses', - 'Validate Business Tax ID as Vat ID' => 'Validate Business Tax ID as Vat ID', - 'Validating condition syntax' => 'Validating condition syntax', - 'Validating formula syntax' => 'Validating formula syntax', - 'Variant Fields' => 'Variant Fields', - 'Variant Has Untracked Stock' => 'Variant Has Untracked Stock', - 'Variant Price' => 'Variant Price', - 'Variant SKU' => 'Variant SKU', - 'Variant Search' => 'Variant Search', - 'Variant Stock' => 'Variant Stock', - 'Variant Title Format' => 'Variant Title Format', - 'Variant Tracks Stock' => 'Variant Tracks Stock', - 'Variant UI Label Format' => 'Variant UI Label Format', - 'Variant has no product.' => 'Variant has no product.', - 'Variants not restored.' => 'Variants not restored.', - 'Variants restored.' => 'Variants restored.', - 'Variants' => 'Variants', - 'View customer' => 'View customer', - 'View order' => 'View order', - 'View product type - {productType}' => 'View product type - {productType}', - 'View user' => 'View user', - 'View' => 'View', - 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?', - 'Web' => 'Web', - 'Webhook URL' => 'Webhook URL', - 'Weight ({unit})' => 'Weight ({unit})', - 'Weight Rate' => 'Weight Rate', - 'Weight Unit' => 'Weight Unit', - 'Weight' => 'Weight', - 'What product URIs should look like for the site.' => 'What product URIs should look like for the site.', - 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.', - 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.', - 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.', - 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}', - 'What this PDF will be called in the control panel.' => 'What this PDF will be called in the control panel.', - 'What this catalog pricing rule will be called in the control panel.' => 'What this catalog pricing rule will be called in the control panel.', - 'What this discount will be called in the control panel.' => 'What this discount will be called in the control panel.', - 'What this email will be called in the control panel.' => 'What this email will be called in the control panel.', - 'What this product type will be called in the control panel.' => 'What this product type will be called in the control panel.', - 'What this sale will be called in the control panel.' => 'What this sale will be called in the control panel.', - 'What this shipping category will be called in the control panel.' => 'What this shipping category will be called in the control panel.', - 'What this shipping rule will be called in the control panel.' => 'What this shipping rule will be called in the control panel.', - 'What this shipping zone will be called in the control panel.' => 'What this shipping zone will be called in the control panel.', - 'What this status will be called in the control panel.' => 'What this status will be called in the control panel.', - 'What this subscription plan will be called in the control panel.' => 'What this subscription plan will be called in the control panel.', - 'What this tax category will be called in the control panel.' => 'What this tax category will be called in the control panel.', - 'What this tax zone will be called in the control panel.' => 'What this tax zone will be called in the control panel.', - 'When this discount is applied to an order, which line items should be discounted?' => 'When this discount is applied to an order, which line items should be discounted?', - 'Whether the first available shipping method option should be set automatically on carts.' => 'Whether the first available shipping method option should be set automatically on carts.', - 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Whether the user’s primary payment source should be set automatically on new carts.', - 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.', - 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Whether this catalog pricing rule should be available for use, regardless of other conditions.', - 'Whether this sale should be available for use, regardless of other conditions.' => 'Whether this sale should be available for use, regardless of other conditions.', - 'Which data to display in the name column in the results table.' => 'Which data to display in the name column in the results table.', - 'Which product types should this category be available to?' => 'Which product types should this category be available to?', - 'Which template should be loaded when a product’s URL is requested.' => 'Which template should be loaded when a product’s URL is requested.', - 'Width ({unit})' => 'Width ({unit})', - 'Width' => 'Width', - 'YYYY' => 'YYYY', - 'Yes' => 'Yes', - 'You are not allowed to add a line item.' => 'You are not allowed to add a line item.', - 'You currently have no emails configured to select for this status.' => 'You currently have no emails configured to select for this status.', - 'You do not have permission to load this cart.' => 'You do not have permission to load this cart.', - 'You must set up at least one gateway that supports subscriptions first.' => 'You must set up at least one gateway that supports subscriptions first.', - 'You must be logged in or provide a valid token to load this cart.' => 'You must be logged in or provide a valid token to load this cart.', - 'You must be signed in to create a payment source.' => 'You must be signed in to create a payment source.', - 'You must be signed in to set a primary payment source.' => 'You must be signed in to set a primary payment source.', - 'You must make a payment to complete the order.' => 'You must make a payment to complete the order.', - 'Your Cart Recovery Link' => 'Your Cart Recovery Link', - 'Your Order PDF Download Link' => 'Your Order PDF Download Link', - 'Your order is empty' => 'Your order is empty', - 'ZIP file' => 'ZIP file', - 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Zero - Minimum price is zero if discounts are greater than the order value.', - 'Zip Code' => 'Zip Code', - 'all' => 'all', - 'any' => 'any', - 'average order total' => 'average order total', - 'billing address' => 'billing address', - 'donation' => 'donation', - 'donations' => 'donations', - 'info' => 'info', - 'inventory location' => 'inventory location', - 'new customers' => 'new customers', - 'on hand' => 'on hand', - 'only' => 'only', - 'order' => 'order', - 'orders' => 'orders', - 'price' => 'price', - 'prices' => 'prices', - 'product variant' => 'product variant', - 'product variants' => 'product variants', - 'product' => 'product', - 'products' => 'products', - 'repeat customers' => 'repeat customers', - 'shipping address' => 'shipping address', - 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'shippingSameAsBilling and billingSameAsShipping can’t both be set.', - 'subscription' => 'subscription', - 'subscriptions' => 'subscriptions', - 'to' => 'to', - 'transfer' => 'transfer', - 'transfers' => 'transfers', - '{amount} included' => '{amount} included', - '{count} Unfulfilled Orders' => '{count} Unfulfilled Orders', - '{description} is no longer available.' => '{description} is no longer available.', - '{description} only has {stock} in stock.' => '{description} only has {stock} in stock.', - '{from} to {to}' => '{from} to {to}', - '{name} (Primary)' => '{name} (Primary)', - '{name} (Trashed)' => '{name} (Trashed)', - '{name} catalog price' => '{name} catalog price', - '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, =1{order} other{orders}} updated.', - '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.', - '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.', - '{number} more…' => '{number} more…', - '{pct} off the discounted item price' => '{pct} off the discounted item price', - '{pct} off the original item price' => '{pct} off the original item price', - '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.', - '{total} in total revenue' => '{total} in total revenue', - '{total} orders' => '{total} orders', - '{total} saleable across {locationCount} location(s)' => '{total} saleable across {locationCount} location(s)', - '{uses} uses across {emails} email addresses' => '{uses} uses across {emails} email addresses', - '{uses} uses across {users} users' => '{uses} uses across {users} users', - '“{description}” is currently out of stock.' => '“{description}” is currently out of stock.', - '“{key}” has invalid JSON' => '“{key}” has invalid JSON', -]; diff --git a/src/translations/fr-CA/commerce.php b/src/translations/fr-CA/commerce.php deleted file mode 100644 index 4fed31337c..0000000000 --- a/src/translations/fr-CA/commerce.php +++ /dev/null @@ -1,1428 +0,0 @@ - '(nouveau prix)', - '(of original price)' => '(du prix initial)', - '(off original price)' => '(en moins sur le prix initial)', - 'A cart number must be specified.' => 'Un numéro de panier doit être indiqué.', - 'A cart recovery link has been sent to {email}.' => 'Un lien de récupération de panier a été envoyé à {email}.', - 'A cart recovery link will be sent to {email}.' => 'Un lien de récupération de panier sera envoyé à {email}.', - 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Un numéro de référence simple sera généré sur la base de ce format lors de la finalisation d’un panier et de sa conversion en commande. Par exemple {ex1}, ou
{ex2}. Le résultat de ce format doit être unique.', - 'A new download link has been sent to {email}' => 'Un nouveau lien de téléchargement a été envoyé à {email}.', - 'A new download link will be sent to {email}' => 'Un nouveau lien de téléchargement sera envoyé à {email}.', - 'A valid email is required to create a customer.' => 'Un courriel valide est requis pour créer un client.', - 'Accept' => 'Accepter', - 'Accepted' => 'Accepté', - 'Actions' => 'Actions', - 'Active Carts' => 'Paniers actifs', - 'Active subscriptions' => 'Abonnements actifs', - 'Active' => 'Actif', - 'Add Address' => 'Ajouter une adresse', - 'Add a coupon' => 'Ajouter un coupon', - 'Add a custom line item' => 'Ajouter un article personnalisé', - 'Add a line item' => 'Ajouter un article', - 'Add a product' => 'Ajouter un produit', - 'Add a variant' => 'Ajouter une variation', - 'Add an adjustment' => 'Ajouter un ajustement', - 'Add an item' => 'Ajouter un article', - 'Add an option' => 'Ajouter une option', - 'Add catalog price' => 'Ajouter un prix catalogue', - 'Add' => 'Ajouter', - 'Additional Actions' => 'Actions supplémentaires', - 'Additional recipients that should receive this email. Twig code can be used here.' => 'Destinataires supplémentaires qui devraient recevoir ce courriel. Du code Twig peut être utilisé ici.', - 'Address 1' => 'Adresse 1', - 'Address 2' => 'Adresse 2', - 'Address 3' => 'Adresse 3', - 'Address Line 1' => 'Adresse ligne 1', - 'Address Line 2' => 'Adresse ligne 2', - 'Address Updated.' => 'Adresse mise à jour.', - 'Address copied to user.' => 'Adresse copiée pour l\'utilisateur.', - 'Address not found.' => 'Adresse non trouvée.', - 'Adjust Quantity' => 'Ajuster la quantité', - 'Adjust by' => 'Ajuster par', - 'Adjust price when included rate is disqualified?' => 'Ajuster le prix lorsque le taux de taxe inclus est disqualifié?', - 'Adjustments' => 'Ajustements', - 'Admin Notices' => 'Avis de l\'administrateur', - 'Administrative Area Code of Origin' => 'Code d\'origine Zone administrative ', - 'Advanced' => 'Avancé', - 'All Orders' => 'Toutes les commandes', - 'All Totals' => 'Tous les totaux', - 'All Transfers' => 'Tous les transferts', - 'All active subscriptions' => 'Tous les abonnements actifs', - 'All customers' => 'Tous les clients', - 'All products' => 'Tous les produits', - 'All variants must have a SKU.' => 'Toutes les variantes doivent avoir une UGS.', - 'All' => 'Tout', - 'Allow Checkout Without Payment' => 'Autoriser le passage à la caisse sans paiement', - 'Allow Empty Cart On Checkout' => 'Autoriser les paniers vides à la fin du processus de paiement', - 'Allow Partial Payment On Checkout' => 'Autoriser les paiements partiels à la sortie', - 'Allow out of stock purchases' => 'Permettre les achats de produits en rupture de stock', - 'Allow' => 'Autoriser', - 'Allowed Qty' => 'Quantité autorisée', - 'Alternative Phone' => 'Autre téléphone', - 'Amount' => 'Montant', - 'An ID must be provided' => 'Un identifiant doit être fourni', - 'An error occurred while generating this PDF.' => 'Une erreur est survenue lors la création de ce fichier PDF.', - 'Any' => 'Chaque', - 'Anywhere' => 'Partout', - 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Voulez-vous vraiment archiver le plan d’abonnement « {name} »? Cela N’ANNULERA PAS les abonnements existants.', - 'Are you sure you want to capture this transaction?' => 'Êtes-vous certain(e) de vouloir saisir cette transaction?', - 'Are you sure you want to complete this order?' => 'Êtes-vous sûr de vouloir terminer cette commande?', - 'Are you sure you want to delete the selected orders?' => 'Voulez-vous vraiment supprimer les commandes sélectionnées?', - 'Are you sure you want to delete the selected product and its variants?' => 'Voulez-vous vraiment supprimer le produit sélectionné et ses variantes?', - 'Are you sure you want to delete this shipping rule?' => 'Êtes-vous sûr de vouloir supprimer cette règle d\'expédition?', - 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Êtes-vous certain(e) de vouloir effacer “{name}” et tout ses produits? Veuillez-vous assurer que vous avez une copie de sauvegarde de votre base de données avant de compléter cette action.', - 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Voulez-vous vraiment supprimer « {name} »? Cela définira tous les articles avec ce statut comme n\'ayant aucun statut.', - 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Voulez-vous vraiment marquer ce transfert comme étant en attente? Il apparaîtra comme entrant à la destination.', - 'Are you sure you want to overwrite the billing address?' => 'Êtes-vous sûr de vouloir écraser l\'adresse de facturation?', - 'Are you sure you want to overwrite the shipping address?' => 'Êtes-vous sûr de vouloir écraser l\'adresse d\'expédition?', - 'Are you sure you want to permanently delete this store and everything in it?' => 'Voulez-vous vraiment supprimer ce magasin et tout ce qu\'il contient?', - 'Are you sure you want to refund this transaction?' => 'Êtes-vous certain(e) de vouloir rembourser cette transaction?', - 'Are you sure you want to remove this customer?' => 'Êtes-vous sûr de vouloir supprimer ce client?', - 'Are you sure you want to save this as a new shipping rule?' => 'Êtes-vous sûr de vouloir enregistrer cette règle comme nouvelle règle d\'expédition?', - 'Are you sure you want to send email: {name}?' => 'Êtes-vous sûr de vouloir envoyer le courriel : {name}?', - 'At least one site must be enabled for the product type.' => 'Au moins un site doit être activé pour le type de produit.', - 'Attempted Payments' => 'Tentatives de paiement', - 'Attention' => 'Attention', - 'Authorize Only (Manually Capture)' => 'Autoriser uniquement (collecter manuellement)', - 'Auto Set Cart Shipping Method Option' => 'Définir automatiquement l\'option de méthode d\'expédition du panier', - 'Auto Set New Cart Addresses' => 'Définir automatiquement les adresses des nouveaux paniers', - 'Auto Set Payment Source' => 'Source de paiement auto-définie', - 'Automatic SKU Format' => 'Format automatique d\'UGS', - 'Available Shipping Categories' => 'Catégories d’expédition disponibles', - 'Available Tax Categories' => 'Catégories de taxes disponibles', - 'Available for purchase' => 'Disponible à l’achat', - 'Available for purchase?' => 'Disponible à l’achat?', - 'Available inventory for "{description}" has gone below zero.' => 'Le stock disponible pour « {description} » est désormais inférieur à zéro.', - 'Available to Product Types' => 'Disponibles dans Types de produits', - 'Available' => 'Disponible', - 'Available?' => 'Disponible?', - 'Average Order Total' => 'Total de commande moyen', - 'Average' => 'Moyenne', - 'BCC’d Recipient' => 'Copie conforme invisible envoyée au destinataire', - 'Bad Request' => 'Requête incorrecte', - 'Bad address ID.' => 'Mauvais identifiant d\'adresse.', - 'Bad order ID.' => 'Identifiant de commande incorrect.', - 'Base Price' => 'Prix de base', - 'Base Promotional Price' => 'Prix promotionnel de base', - 'Base Rate' => 'Taux de base', - 'Base' => 'Base', - 'Bcc' => 'CCI', - 'Billing Address' => 'Adresse de facturation', - 'Billing Business Name' => 'Nom de l’entreprise de facturation', - 'Billing First Name' => 'Nom pour la facturation', - 'Billing Full Name' => 'Nom complet pour la facturation', - 'Billing Last Name' => 'Nom pour la facturation', - 'Billing address required.' => 'Adresse de facturation requise.', - 'Billing detail update URL' => 'URL de mise à jour des informations de facturation', - 'Billing issues' => 'Problèmes de facturation', - 'Billing' => 'Facturation', - 'Both (Line item price + Line item shipping costs)' => 'Les deux (prix de l\'article + frais de livraison de l\'article)', - 'Business ID' => 'Numéro d’entreprise', - 'Business Name' => 'Nom de l’entreprise', - 'Business Tax ID' => 'Numéro d’entreprise', - 'CC’d Recipient' => 'Destinataire en copie', - 'CVV' => 'CVV', - 'Can be used as an internal reference.' => 'Peut être utilisé comme référence interne.', - 'Can not complete payment for missing transaction.' => 'Impossible de terminer le paiement pour la transaction manquante.', - 'Can not create a new order' => 'Impossible de créer une nouvelle commande', - 'Can not find an order to pay.' => 'Impossible de trouver une commande à payer.', - 'Can not find enabled email.' => 'Impossible de trouver le courriel activé.', - 'Can not find order' => 'Impossible de trouver la commande', - 'Can not find order.' => 'Impossible de trouver la commande.', - 'Can not find the transaction to refund' => 'Impossible de trouver la transaction à rembourser', - 'Can not move between these inventory types.' => 'Il n\'est pas possible de passer d\'un type de stocks à l\'autre.', - 'Can not refund amount greater than the remaining amount' => 'Impossible de rembourser un montant supérieur au montant restant', - 'Cancel subscription' => 'Annuler l’abonnement', - 'Cancel with gateway now' => 'Annuler avec la passerelle maintenant', - 'Cancel' => 'Annuler', - 'Cancellation date' => 'Date d’annulation', - 'Cancellation' => 'Annulation', - 'Cannot switch plans for this subscription.' => 'Impossible de changer de plan pour cet abonnement.', - 'Can’t preview this email.' => 'Impossible de prévisualiser ce courriel.', - 'Capture payment' => 'Collecter le paiement', - 'Capture' => 'Saisir', - 'Card Holder' => 'Titulaire de la carte', - 'Card Number' => 'Numéro de carte', - 'Card' => 'Carte', - 'Cart Recovery Link' => 'Lien de récupération du panier', - 'Cart forgotten.' => 'Panier oublié.', - 'Cart updated.' => 'Panier mis à jour.', - 'Cart {number}' => 'Panier {number}', - 'Catalog Pricing Rule' => 'Règle de tarification du catalogue', - 'Catalog pricing rule description.' => 'Description de la règle de tarification du catalogue.', - 'Catalog pricing rule saved.' => 'Règle de tarification du catalogue sauvegardée.', - 'Catalog pricing rules deleted.' => 'Les règles de tarification du catalogue sont supprimées.', - 'Catalog pricing rules updated.' => 'Mise à jour des règles de tarification du catalogue.', - 'Categories Relationship Type' => 'Type de relation des catégories', - 'Categories' => 'Catégories', - 'Category Rate Overrides' => 'Remplacements du taux de catégorie', - 'Centimeters (cm)' => 'Centimètres (cm)', - 'Changing this value may affect your ability to refund existing transactions.' => 'La modification de cette valeur peut affecter votre capacité à rembourser les transactions existantes.', - 'Choose a color to represent the order’s status' => 'Choisissez une couleur pour représenter le statut de la commande', - 'Choose a new customer' => 'Choisir un nouveau client', - 'Choose adjustment values to include when calculating the product revenue total.' => 'Choisissez les valeurs d\'ajustement à inclure lors du calcul du total des revenus du produit.', - 'Choose the currency’s ISO code.' => 'Sélectionner le code ISO de la devise.', - 'Choose the destination inventory location for the existing on hand stock.' => 'Sélectionnez l\'emplacement de destination des stocks pour les stocks disponibles existants.', - 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Choisir les sites pour lesquels ce type de produit sera disponible et configurer les paramètres spécifiques aux sites.', - 'City' => 'Ville', - 'Clear counter' => 'Réinitialiser le compteur', - 'Clear notices' => 'Effacer les avis', - 'Close' => 'Fermer', - 'Code' => 'Code', - 'Collated PDF' => 'PDF unique', - 'Color' => 'Couleur', - 'Commerce Products' => 'Produits de Commerce', - 'Commerce Settings' => 'Paramètres commerciaux', - 'Commerce Variants' => 'Variantes de Commerce', - 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Le courriel Commerce « {email} » n’a pas été envoyé pour la commande « {order} ».', - 'Commerce order exports' => 'Exportations de commandes Commerce', - 'Commerce' => 'Commerce', - 'Committed' => 'Validé', - 'Completed Email' => 'Adresse courriel indiquée', - 'Completed' => 'Terminé', - 'Completing order failed.' => 'Échec de l\'exécution de la commande.', - 'Condition' => 'Condition', - 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Les conditions sont comparées à un ordre avant d\'examiner les règles. Cette fonction est utile si vous souhaitez vérifier la disponibilité d\'une méthode à un stade précoce ou s\'il existe des conditions communes à toutes les règles relatives à cette méthode.', - 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Les conditions sont comparées à la commande du client avant d\'examiner les règles. Cette fonction est utile si vous souhaitez vérifier la disponibilité d\'une méthode à un stade précoce ou s\'il existe des conditions communes à toutes les règles relatives à cette méthode.', - 'Conditions' => 'Conditions', - 'Contains Purchasables' => 'Contient des articles achetables', - 'Control Panel Settings' => 'Réglages du panneau de configuration', - 'Control panel' => 'Panneau de configuration', - 'Conversion Rate' => 'Taux de conversion', - 'Converted Price' => 'Prix converti', - 'Copied!' => 'Copié!', - 'Copy the URL' => 'Copier l\'URL', - 'Copy to {location}' => 'Copier vers {location}', - 'Copy' => 'Copier', - 'Costs' => 'Frais', - 'Could not archive gateway.' => 'Impossible d’archiver la passerelle.', - 'Could not cancel “{reference}”.' => 'Échec de l\'annulation de « {reference} ».', - 'Could not create the payment source.' => 'Impossible de créer la source de paiement.', - 'Could not delete shipping rule' => 'Impossible de supprimer la règle de livraison', - 'Could not delete shipping zone' => 'Impossible de supprimer la zone de livraison', - 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Impossible de supprimer {count, number} {count, plural,one{catégorie} other{catégories}} d\'expédition.', - 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Impossible de supprimer {count, number} {count, plural,one{mode} other{modes}} et règles d\'expédition.', - 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Impossible de supprimer {count, number} {count, plural,one{catégorie} other{catégories}} de taxes.', - 'Could not find the email or template.' => 'Courriel ou modèle introuvable.', - 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Impossible de marquer la commande {number} comme finalisée. L’enregistrement de la commande a échoué au cours de la finalisation avec des erreurs : {order}', - 'Could not reactivate “{reference}”.' => 'Échec de la réactivation de « {reference} ».', - 'Could not send email' => 'Impossible d\'envoyer le courriel', - 'Could not switch “{reference}” to “{plan}”.' => 'Impossible de passer « {reference} » à « {plan} ».', - 'Could not update orders address.' => 'Impossible de mettre à jour l\'adresse des commandes.', - 'Couldn’t archive Line Item Status.' => 'Impossible d\'archiver le statut de l\'article.', - 'Couldn’t archive Order Status.' => 'Impossible d\'archiver le statut de la commande.', - 'Couldn’t capture transaction.' => 'Impossible de collecter la transaction.', - 'Couldn’t capture transaction: {message}' => 'Impossible de collecter la transaction : {message}', - 'Couldn’t delete email.' => 'Impossible de supprimer le courriel.', - 'Couldn’t delete the payment source.' => 'Impossible de supprimer la source de paiement.', - 'Couldn’t get order.' => 'Impossible d\'obtenir la commande.', - 'Couldn’t recalculate order.' => 'Impossible de recalculer la commande.', - 'Couldn’t refund transaction.' => 'Impossible de rembourser la transaction.', - 'Couldn’t refund transaction: {message}' => 'Impossible de rembourser la transaction : {message}', - 'Couldn’t reorder Line Item Statuses.' => 'Impossible de réorganiser les statuts d\'article.', - 'Couldn’t reorder Order Statuses.' => 'Impossible de réorganiser les statuts des commandes.', - 'Couldn’t reorder PDFs.' => 'Impossible de réorganiser les PDF.', - 'Couldn’t reorder discounts.' => 'Impossible de réorganiser les remises.', - 'Couldn’t reorder gateways.' => 'Impossible de réorganiser les passerelles.', - 'Couldn’t reorder plans.' => 'Impossible de réorganiser les projets.', - 'Couldn’t reorder rules.' => 'Impossible de réorganiser les règles.', - 'Couldn’t reorder sale.' => 'Impossible de réorganiser la promotion.', - 'Couldn’t reorder sales.' => 'Impossible de réorganiser les promotions.', - 'Couldn’t reorder statuses.' => 'Impossible de réorganiser les statuts.', - 'Couldn’t reorder stores.' => 'Impossible de commander à nouveau dans les magasins.', - 'Couldn’t save PDF.' => 'Impossible d’enregistrer le PDF.', - 'Couldn’t save catalog pricing rule.' => 'Impossible d\'enregistrer la règle de tarification du catalogue.', - 'Couldn’t save currency.' => 'Impossible d’enregistrer la devise.', - 'Couldn’t save discount.' => 'Impossible d\'enregistrer le rabais.', - 'Couldn’t save email.' => 'Impossible d\'enregistrer le courriel.', - 'Couldn’t save gateway.' => 'Impossible d’enregistrer la passerelle.', - 'Couldn’t save inventory location.' => 'Impossible d\'enregistrer l\'emplacement des stocks.', - 'Couldn’t save line item status.' => 'Impossible d\'enregistrer le statut de l\'article.', - 'Couldn’t save order fields.' => 'Impossible d’enregistrer les champs de commande.', - 'Couldn’t save order status.' => 'Impossible d\'enregistrer le statut de commande.', - 'Couldn’t save order.' => 'Impossible d\'enregistrer la commande.', - 'Couldn’t save product type.' => 'Impossible d\'enregistrer le type de produit.', - 'Couldn’t save sale.' => 'Impossible d\'enregistrer la promotion.', - 'Couldn’t save settings.' => 'Impossible d’enregistrer les paramètres.', - 'Couldn’t save shipping category.' => 'Impossible d\'enregistrer cette catégorie de livraison.', - 'Couldn’t save shipping method.' => 'Impossible d\'enregistrer la méthode d\'expédition.', - 'Couldn’t save shipping rule.' => 'Impossible d’enregistrer la règle d\'expédition.', - 'Couldn’t save shipping zone.' => 'Impossible d\'enregistrer la zone de livraison.', - 'Couldn’t save store.' => 'Impossible d\'enregistrer la boutique.', - 'Couldn’t save subscription fields.' => 'Impossible d’enregistrer les champs d\'abonnement.', - 'Couldn’t save subscription plan.' => 'Impossible d’enregistrer le plan d’abonnement.', - 'Couldn’t save subscription.' => 'Impossible d’enregistrer l’abonnement.', - 'Couldn’t save tax category.' => 'Impossible d\'enregistrer la catégorie de taxe.', - 'Couldn’t save tax rate.' => 'Impossible d\'enregistrer le taux de taxe.', - 'Couldn’t save tax zone.' => 'Impossible d’enregistrer la zone de taxes.', - 'Couldn’t save transfer fields.' => 'Impossible d’enregistrer les champs de transfert.', - 'Couldn’t update catalog pricing rule statuses.' => 'Impossible de mettre à jour le statut des règles de tarification du catalogue.', - 'Couldn’t update status.' => 'Impossible de mettre à jour l\'état.', - 'Couldn’t updated sales status.' => 'Impossible de mettre à jour le statut des ventes.', - 'Country Code of Origin' => 'Code d\'origine Pays', - 'Country List' => 'Liste des pays', - 'Country not allowed.' => 'Pays non autorisé.', - 'Country' => 'Pays', - 'Coupon Code' => 'Code de coupon', - 'Coupon can not apply discount to this order due to address mismatch.' => 'Le coupon ne peut pas être utilisé pour appliquer une remise à cette commande, car l\'adresse ne correspond pas.', - 'Coupon can not apply discount to this order due to customer mismatch.' => 'Le coupon ne peut pas être utilisé pour appliquer une remise à cette commande, car le client ne correspond pas.', - 'Coupon can not apply discount to this order.' => 'Le coupon ne peut pas être utilisé pour appliquer une remise à cette commande.', - 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Le code promotionnel « {code} » est déjà utilisé par la réduction « {name} ».', - 'Coupon codes cannot be blank.' => 'Les codes de coupons ne peuvent pas être vides.', - 'Coupon codes must be unique.' => 'Les codes de coupons doivent être uniques.', - 'Coupon format is required and must contain at least one `#`.' => 'Le format du coupon est requis et doit contenir au moins un « # ».', - 'Coupon not valid.' => 'Coupon non valide.', - 'Coupon removed: {explanation}' => 'Coupon supprimé : {explanation}', - 'Coupons' => 'Coupons', - 'Craft Commerce - Administration' => 'Craft Commerce - Administration', - 'Craft Commerce - Inventory' => 'Craft Commerce - Stocks', - 'Craft Commerce - Orders' => 'Craft Commerce - Commandes', - 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Type de produit - {name}', - 'Craft Commerce - Subscriptions' => 'Craft Commerce - Abonnements', - 'Create a Discount' => 'Créer un rabais', - 'Create a Subscription Plan' => 'Créer un abonnement', - 'Create a new PDF' => 'Créer un nouveau PDF', - 'Create a new catalog pricing rule' => 'Créer une nouvelle règle de tarification pour le catalogue', - 'Create a new currency' => 'Créer une nouvelle devise', - 'Create a new email' => 'Créer un nouveau courriel', - 'Create a new gateway' => 'Créer une nouvelle passerelle', - 'Create a new line item status' => 'Créer un nouveau statut d\'article', - 'Create a new order status' => 'Créer un nouveau statut de commande', - 'Create a new product type' => 'Créer un nouveau type de produit', - 'Create a new sale' => 'Créer une nouvelle promotion', - 'Create a new shipping category' => 'Créer une nouvelle catégorie d’expédition', - 'Create a new shipping method' => 'Créer une nouvelle méthode d\'expédition', - 'Create a new shipping rule' => 'Créer une nouvelle règle de livraison', - 'Create a new tax category' => 'Créer une nouvelle catégorie de taxes', - 'Create a new tax rate' => 'Créer un nouveau taux de taxes', - 'Create a product type' => 'Créer un type de produit', - 'Create a shipping zone' => 'Créer une zone de livraison', - 'Create a tax zone' => 'Créer une zone de taxes', - 'Create catalog pricing rules' => 'Créer des règles de tarification pour les catalogues', - 'Create customer: “{email}”' => 'Créer le client : « {email} »', - 'Create discounts' => 'Créer des remises', - 'Create discount…' => 'Créer un rabais…', - 'Create rules that allow this discount to match the order.' => 'Créez des règles qui permettent à ce rabais de correspondre à la commande.', - 'Create rules that allow this discount to match the order’s billing address.' => 'Créez des règles qui permettent à ce rabais de correspondre à l\'adresse de facturation de la commande.', - 'Create rules that allow this discount to match the order’s customer.' => 'Créez des règles qui permettent à ce rabais de correspondre au client de la commande.', - 'Create rules that allow this discount to match the order’s shipping address.' => 'Créez des règles qui permettent à ce rabais de correspondre à l\'adresse d\'expédition de la commande.', - 'Create rules that allow this gateway to match the billing address.' => 'Créez des règles qui permettent à cette passerelle de faire correspondre l\'adresse de facturation.', - 'Create rules that allow this gateway to match the order.' => 'Créez des règles qui permettent à ce portail de correspondre à la commande.', - 'Create rules that allow this gateway to match the shipping address.' => 'Créez des règles qui permettent à cette passerelle de faire correspondre l\'adresse de livraison.', - 'Create sales' => 'Créer des promotions', - 'Create sale…' => 'Créer une vente…', - 'Created' => 'Créé', - 'Credit Card Payment Type' => 'Type de paiement par carte de crédit', - 'Currency Code' => 'Code de devise', - 'Currency saved.' => 'Devise enregistrée.', - 'Currency' => 'Devise', - 'Current' => 'Actuel', - 'Custom 1' => 'Personnalisé 1', - 'Custom 2' => 'Personnalisé 2', - 'Custom 3' => 'Personnalisé 3', - 'Custom 4' => 'Personnalisé 4', - 'Custom' => 'Personnalisé', - 'Customer Enabled?' => 'Activée pour les clients ?', - 'Customer ID is required.' => 'Un identifiant client est requis.', - 'Customer Note' => 'Note client', - 'Customer Notices' => 'Avis aux clients', - 'Customer data' => 'Données de clients', - 'Customer' => 'Client', - 'Damaged' => 'Endommagé', - 'Data shown might be outdated.' => 'Les données présentées peuvent être obsolètes.', - 'Date Authorized' => 'Date d\'autorisation', - 'Date Created' => 'Date de création', - 'Date First Paid' => 'Date du premier paiement', - 'Date Ordered' => 'Date de commande', - 'Date Paid' => 'Date de paiement', - 'Date Updated' => 'Date de mise à jour', - 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Date à partir de laquelle la règle de tarification du catalogue sera active. Laisser vide pour une date de début illimitée', - 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Date à partir de laquelle le rabais sera actif. Laisser vide pour une date de début illimitée', - 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Date à partir de laquelle la vente sera active. Laisser vide pour une date de début illimitée', - 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Date à laquelle la règle de tarification du catalogue sera terminée. Laisser vide pour une date de fin illimitée', - 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Date à laquelle le rabais sera terminé. Laisser vide pour une date de fin illimitée', - 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Date à laquelle la vente sera terminée. Laisser vide pour une date de fin illimitée', - 'Date' => 'Date', - 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Par défaut - Permet au prix d\'être négatif si les rabais dépassent la valeur de la commande.', - 'Default Category' => 'Catégorie par défaut', - 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Vue par défaut du panneau de contrôle de Commerce. Si l\'utilisateur n\'a pas la permission, il se rabattra sur un emplacement auquel il peut accéder.', - 'Default Order PDF' => 'PDF de commande par défaut', - 'Default Per Item Rate' => 'Taux par article par défaut', - 'Default Percentage Rate' => 'Pourcentage par défaut', - 'Default Status?' => 'État par défaut?', - 'Default View' => 'Vue par défaut', - 'Default Weight Rate' => 'Taux par poids par défaut', - 'Default Zone' => 'Zone par défaut ', - 'Default status?' => 'Statut par défaut?', - 'Default to this tax zone when no billing address is set' => 'Définir cette zone de taxes comme zone par défaut lorsqu’aucune adresse de facturation n’est spécifiée', - 'Default to this tax zone when no shipping address is set' => 'Zone de taxes par défaut si aucune adresse de livraison n’est définie', - 'Default variant updated.' => 'Mise à jour de la variante par défaut.', - 'Default' => 'Valeur par défaut', - 'Default?' => 'Par défaut?', - 'Delete catalog pricing rules' => 'Supprimer les règles de tarification du catalogue', - 'Delete discounts' => 'Supprimer les rabais', - 'Delete orders' => 'Supprimer les commandes', - 'Delete sales' => 'Supprimer les promotions', - 'Delete' => 'Supprimer', - 'Deleting the {location} location.' => 'Suppression de l\'emplacement {location}.', - 'Describe this rule.' => 'Décrivez cette règle.', - 'Describe this shipping zone.' => 'Décrivez cette zone d’expédition.', - 'Describe this tax zone.' => 'Décrivez cette zone de taxes.', - 'Description' => 'Description', - 'Destination Inventory Location' => 'Emplacement de destination des stocks', - 'Destination' => 'Destination', - 'Details' => 'Détails', - 'Dimension Unit' => 'Unités de dimensions', - 'Dimensions' => 'Dimensions', - 'Disabled' => 'Désactivé', - 'Disallow' => 'Ne pas autoriser', - 'Discount all line items' => 'Effectuer un rabais sur tous les articles', - 'Discount description.' => 'Description du rabais.', - 'Discount is not allowed for the order' => 'Aucun rabais n\'est autorisé pour cette commande', - 'Discount is out of date.' => 'Le rabais est échu.', - 'Discount saved.' => 'Rabais enregistré.', - 'Discount the matching items only' => 'Ne faire de rabais que sur les articles correspondants', - 'Discount use has reached its limit.' => 'L’utilisation du rabais a atteint sa limite.', - 'Discount' => 'Rabais', - 'Discounted Item Subtotal' => 'Sous-total de l\'article avec remise', - 'Discounted Items' => 'Articles en réduction', - 'Discounts deleted.' => 'Rabais supprimés.', - 'Discounts reordered.' => 'Rabais réorganisés.', - 'Discounts updated.' => 'Rabais mis à jour.', - 'Discounts' => 'Rabais', - 'Disqualify with valid business tax ID?' => 'Disqualifier avec un identifiant fiscal d\'entreprise valide?', - 'Do not apply subsequent matching sales beyond applying this sale.' => 'N’appliquez pas les ventes correspondantes ultérieures après avoir appliqué cette vente.', - 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Ne pas appliquer ce taux si l\'adresse de la commande comporte l\'un des identifiants de taxe professionnelle valides sélectionnés.', - 'Do not attach a PDF to this email' => 'Ne pas attacher de PDF à ce courriel', - 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Ne pas calculer de nouveau la commande (Numéro : {orderNumber}) si des erreurs sont présentes.', - 'Donation can not be zero.' => 'Un don ne peut être nul.', - 'Donation needs to be an amount.' => 'Le don doit être un montant.', - 'Donation settings saved.' => 'Paramètres de don enregistrés.', - 'Donation' => 'Don', - 'Donations' => 'Dons', - 'Done' => 'Terminé', - 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Ne pas appliquer de rabais supplémentaires à cette commande si ce rabais est appliqué', - 'Download PDF' => 'Télécharger le PDF', - 'Download PDF…' => 'Télécharger le PDF…', - 'Download Type' => 'Type de téléchargement', - 'Download' => 'Télécharger', - 'Draft' => 'Brouillon', - 'Dummy gateway payment failed.' => 'Le paiement par la passerelle factice a échoué.', - 'Duplicate options exist' => 'Il y a des options en double', - 'Duration' => 'Durée', - 'EU VAT ID' => 'N° TVA DE L\'UE', - 'Edit address' => 'Modifier l’adresse', - 'Edit adjustments' => 'Modifier les ajustements', - 'Edit catalog pricing rules' => 'Modifier les règles de tarification du catalogue', - 'Edit discounts' => 'Modifier les rabais', - 'Edit options' => 'Modifier les options', - 'Edit orders' => 'Modifier les commandes', - 'Edit sales' => 'Modifier les promotions', - 'Edit' => 'Modifier', - 'Effect' => 'Effet', - 'Either (Default) - The relationship field is on the purchasable or the category' => 'Soit (par défaut) - Le champ de relation est dans le champ achetable ou la catégorie', - 'Either way' => 'L\'un ou l\'autre', - 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Erreur de génération de PDF de courriel pour le courriel « {email} ». Commande : « {order} ». Erreur de modèle PDF : « {message} » {file} : {line}', - 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'Le modèle au format PDF du courriel n’existe pas à l’emplacement « {templatePath} » pour le courriel « {email} ». Commande : « {order} ».', - 'Email Subject' => 'Objet du courriel', - 'Email error. No email address found for order. Order: “{order}”' => 'Erreur de courriel. Aucune adresse courriel n’a été trouvée pour la commande. Commande : « {order} »', - 'Email is not enabled.' => 'Les courriels ne sont pas activés.', - 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Comme il n’existe pas de modèle de courriel en texte clair à l’emplacement « {templatePath} », ce qui a entraîné « {templateParsedPath} » pour le courriel « {email} ». Commande : « {order} ».', - 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de texte clair de courriel pour le courriel « {email} ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du chemin d\'accès au modèle de texte clair pour le courriel « {email} » dans « Chemin du modèle ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email required to make payments on a completed order.' => 'Courriel requis pour effectuer des paiements sur une commande finalisée.', - 'Email saved.' => 'Courriel enregistré.', - 'Email sent' => 'Courriel envoyé', - 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Le modèle de courriel n’existe pas à l’emplacement « {templatePath} », ce qui a entraîné « {templateParsedPath} » pour le courriel « {email} ». Commande : « {order} ».', - 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel personnalisé « {email} » dans « À : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel pour le courriel {email} dans « Cci : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel pour le courriel {email} dans « CC : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel pour le courriel {email} dans « Répondre à : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel pour le courriel « {email} » dans « Objet : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel pour le « {email} ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle de courriel pour le courriel « {email} » dans « Chemin du modèle ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email unavailable.' => 'Courriel non disponible.', - 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'Le courriel « {email} » n\'a pu être envoyé pour la commande « {order} ». Erreur : {error} {file} : {line}', - 'Email “{email}” for order {order} was cancelled.' => 'Le courriel « {email} » pour la commande « {order} » a été annulé.', - 'Email' => 'Courriel', - 'Emails' => 'Courriels', - 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Activer si ce taux doit être intégré au prix de l\'objet imposable au lieu d\'ajouter un coût à la commande.', - 'Enable structure for products of this type' => 'Activer la structure pour les produits de ce type', - 'Enable this discount' => 'Activer ce rabais', - 'Enable this rule' => 'Activer cette règle', - 'Enable this sale' => 'Activer cette vente', - 'Enable this shipping method on the front end' => 'Activer cette méthode d’expédition à l’accueil', - 'Enable this shipping rule' => 'Activer cette règle d’expédition', - 'Enable this tax rate' => 'Activer ce taux de taxe', - 'Enabled for customers to select during checkout?' => 'Possibilité pour les clients de sélectionner au moment de passer à la caisse?', - 'Enabled for customers to select?' => 'Activé pour permettre la sélection par les clients?', - 'Enabled' => 'Activé', - 'Enabled?' => 'Activé?', - 'End Date' => 'Date de fin', - 'Enter SKU' => 'Entrer l\'UGS', - 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Saisir un nom convivial pour ce taux d\'imposition, qui sera utilisé dans le panneau de configuration.', - 'Enter a percentage like {ex1} or {ex2}.' => 'Entrer un pourcentage comme {ex1} ou {ex2}.', - 'Enter coupon code' => 'Entrer le code du coupon', - 'Enter reference' => 'Entrer la référence', - 'Error refunding transaction: {transactionHash}' => 'Erreur lors du remboursement de la transaction : {transactionHash}', - 'Every new store must be assigned to at least one site.' => 'Chaque nouveau point de vente doit être affecté à au moins un site.', - 'Everywhere' => 'Partout', - 'Example' => 'Exemple', - 'Exclude this discount for products that are already on promotion' => 'Exclure cette remise pour les produits déjà en promotion', - 'Expired Link' => 'Lien expiré', - 'Expired' => 'Expiré', - 'Expiry Date' => 'Date d’expiration', - 'Expiry date' => 'Date d’expiration', - 'Expiry' => 'Expiration', - 'Failed to receive transfer: {error}' => 'Échec de la réception du transfert : {error}', - 'Failed to send email. Please try again.' => 'Échec de l\'envoi du courriel. Veuillez réessayer.', - 'Failed to start' => 'Échec du démarrage', - 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Échec de la mise à jour {num, plural, =1{du statut de la commande} other{des statuts des commandes}}.', - 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Échec de la mise à jour du statut {num, plural, =1{de la commande} other{des commandes}}.', - 'Feet (ft)' => 'Pieds (pi)', - 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Les conditions de filtrage qui définissent à quelles commandes cette règle s’applique. Écrire 0 pour ignorer cette condition.', - 'First Name' => 'Prénom', - 'Flat Amount Off Order' => 'Montant fixe de remise sur la commande', - 'Flat Order Discount Amount Off' => 'Montant du rabais forfaitaire sur la commande', - 'Free Order Payment Strategy' => 'Stratégie de paiement des commandes gratuites', - 'Free Shipping' => 'Expédition gratuite', - 'Free orders are processed by the payment gateway' => 'Les commandes gratuites sont traitées à travers la passerelle de paiement', - 'Free orders complete immediately' => 'Les commandes gratuites s\'exécutent immédiatement', - 'Free shipping can only be for whole order or matching items, not both.' => 'La livraison gratuite ne peut concerner que l\'ensemble de la commande ou les articles correspondants, pas les deux.', - 'From Name' => 'Nom de l’expéditeur', - 'Fulfill' => 'Réaliser', - 'Fulfilled' => 'Réalisé', - 'Fulfillment' => 'Réalisation', - 'Full Name' => 'Nom complet', - 'Gateway Code' => 'Code de la passerelle', - 'Gateway Message' => 'Message de passerelle', - 'Gateway Reference' => 'Référence de la passerelle', - 'Gateway Response' => 'Réponse de la passerelle', - 'Gateway doesn’t support authorize' => 'La passerelle ne prend pas en charge l’autorisation', - 'Gateway doesn’t support partial refunds.' => 'La passerelle ne prend pas en charge les remboursements partiels.', - 'Gateway doesn’t support purchase' => 'La passerelle de paiement ne supporte pas les achats', - 'Gateway doesn’t support refunds.' => 'La passerelle ne prend pas en charge les remboursements.', - 'Gateway saved.' => 'Passerelle enregistrée.', - 'Gateway' => 'Passerelle', - 'Gateways reordered.' => 'Passerelles réorganisées.', - 'Gateways' => 'Passerelles', - 'General Settings' => 'Paramètres généraux', - 'General' => 'Général', - 'Generate' => 'Générer', - 'Generated Coupon Format' => 'Format des coupons générés', - 'Grams (g)' => 'Grammes (g)', - 'Groups for which this sale will be applicable to.' => 'Groupes auxquels cette promotion s’appliquera.', - 'HTML Email Template Path' => 'Chemin du modèle de courriel HTML', - 'Handle' => 'Identifiant', - 'Harmonized System Code' => 'Code du système harmonisé', - 'Has Admin Notices' => 'Contient des avis de l\'administrateur', - 'Has Emails?' => 'A des courriels?', - 'Has Free Shipping' => 'Possède la livraison gratuite', - 'Has Orders' => 'Comprend des commandes', - 'Has Purchasable' => 'Comprend des achetables', - 'Has Variants?' => 'Possède des variantes?', - 'Height ({unit})' => 'Hauteur ({unit})', - 'Height' => 'Hauteur', - 'Hide snapshot' => 'Masquer l\'instantané', - 'History' => 'Historique', - 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Combien de temps (en secondes) un lien de téléchargement PDF doit rester valide avant d\'expirer. La valeur par défaut est 86 400 (24 heures).', - 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Combien de fois une adresse courriel peut utiliser ce rabais. Ceci est applicable à toutes les commandes précédentes, par des invités ou des utilisateurs. Indiquez zéro pour une utilisation illimitée par invités ou utilisateurs.', - 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Combien de fois un utilisateur est autorisé à utiliser cette remise. Si cette valeur est différente de zéro, la remise ne sera disponible que pour les utilisateurs connectés.', - 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Le nombre de fois maximum que cette réduction peut être utilisée par des invités ou des utilisateurs inscrits. Mettez zéro pour une utilisation illimitée.', - 'How products should be labeled within the control panel.' => 'Comment les produits doivent être étiquetés dans le panneau de contrôle.', - 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'La relation entre les biens à acheter et les catégories, qui détermine les articles correspondants. Voir [Terminologie des relations]({link}).', - 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'Comment ce produit sera-t-il décrit dans une ligne d’article dans une commande. Vous pouvez inclure des étiquettes qui indiquent les propriétés, telles que {ex1} ou {ex2}', - 'How this shipping method will be referred to in templates and forms.' => 'Comment s’appellera cette méthode dans les modèles et les formulaires.', - 'How variants should be labeled within the control panel.' => 'Comment les variantes doivent être étiquetées dans le panneau de contrôle.', - 'How you’ll refer to this PDF in the templates.' => 'La façon dont vous allez faire référence à ce PDF dans les modèles.', - 'How you’ll refer to this product type in the templates.' => 'Comment s’appellera ce type de produit dans les modèles.', - 'How you’ll refer to this shipping category in the templates.' => 'Comment vous faites référence à cette catégorie d’expédition dans les modèles.', - 'How you’ll refer to this status in the templates.' => 'Comment s’appellera cet état dans les modèles', - 'How you’ll refer to this subscription plan in the templates.' => 'La façon dont vous désignerez ce plan d’abonnement dans les modèles.', - 'How you’ll refer to this tax category in the templates.' => 'Comment s’appellera cette catégorie de taxes dans les modèles.', - 'ID' => 'ID', - 'IP Address' => 'Adresse IP', - 'If disabled, this PDF will not be available or sent with emails.' => 'S\'il est désactivé, ce PDF ne sera pas disponible ou envoyé avec les courriels.', - 'If disabled, this email will not send.' => 'S’il est désactivé, ce courriel ne sera pas envoyé.', - 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Si cette option est activée et que ce tarif ne correspond pas à la commande, le taux sera supprimé du prix de l\'article dans le panier.', - 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Si cette valeur est réglée à « Autorisation seulement », il faut manuellement saisir les paiements avant que les fonds ne soient transférés à votre compte. La passerelle a besoin d’accepter l’option sélectionnée.', - 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Si vous choisissez le pourcentage pour qu\'il corresponde au « rabais sur l\'article en promotion », cela inclura le « montant par article » ainsi que tout autre rabais appliqué avant celui-ci.', - 'Ignore Promotions?' => 'Ignorer les promotions?', - 'Ignore previous matching sales if this sale matches.' => 'Ignorez les ventes correspondantes précédentes si cette vente correspond.', - 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignorer les prix promotionnels lorsque cette remise est appliquée à des postes correspondants.', - 'Inactive Carts' => 'Paniers inactifs', - 'Inches (in)' => 'Pouces (po)', - 'Include built-in line item tax.' => 'Inclure la taxe intégrée sur le type d\'article.', - 'Include in price?' => 'Inclure dans le prix?', - 'Include line item discounts.' => 'Inclure les rabais par type d\'article.', - 'Include line item shipping costs.' => 'Inclure les coûts d\'envoi par type d\'article.', - 'Include separate line item tax.' => 'Inclure une ligne distincte pour la taxe par type d\'article.', - 'Included in price?' => 'Inclus dans le prix?', - 'Included' => 'Inclus', - 'Incoming transfer from Transfer ID: ' => 'Transfert entrant à partir de l\'ID de transfert : ', - 'Incoming' => 'À venir', - 'Info' => 'Infos', - 'Information linked?' => 'Des renseignements sont-ils associés?', - 'Information' => 'Informations', - 'Invalid JSON' => 'JSON invalide', - 'Invalid Order ID' => 'ID de commande invalide', - 'Invalid VAT ID.' => 'ID TVA invalide.', - 'Invalid condition syntax' => 'Syntaxe conditionnelle invalide', - 'Invalid email.' => 'L\'adresse courriel est invalide.', - 'Invalid formula syntax' => 'Syntaxe de formule non valide', - 'Invalid gateway: {value}' => 'Passerelle non valide : {value}', - 'Invalid inventory movements.' => 'Mouvements des stocks non valides.', - 'Invalid order condition syntax.' => 'Syntaxe conditionnelle de commande invalide.', - 'Invalid payment or order. Please review.' => 'Paiement ou commande non valide. Veuillez vérifier votre saisie.', - 'Invalid payment source ID: {value}' => 'Identifiant de la source de paiement non valide : {value}', - 'Invalid store.' => 'Magasin non valide.', - 'Invalid user.' => 'Utilisateur non valide.', - 'Inventory Item' => 'Article d\'inventaire', - 'Inventory Location' => 'Emplacement des stocks', - 'Inventory Locations' => 'Emplacements des stocks', - 'Inventory Tracked' => 'Stocks tracés', - 'Inventory Transfers' => 'Transferts d\'inventaire', - 'Inventory could not be set.' => 'Les stocks n\'ont pas pu être définis.', - 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'L\'emplacement des stocks a validé des stocks, la ou les commandes doivent d\'abord être réalisées.', - 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Si l\'emplacement des stocks a des stocks entrants, le(s) transfert(s) doit(vent) d\'abord être effectué(s).', - 'Inventory location is already deactivated.' => 'L\'emplacement des stocks est déjà désactivé.', - 'Inventory location saved.' => 'Emplacement des stocks enregistré.', - 'Inventory locations not saved.' => 'Emplacements des stocks non enregistrés.', - 'Inventory movement could not be saved.' => 'Le mouvement de stocks n\'a pas pu être enregistré.', - 'Inventory movement saved.' => 'Mouvement de stocks enregistré.', - 'Inventory updated.' => 'Stocks mis à jour.', - 'Inventory was not updated.' => 'Les stocks n\'ont pas été mis à jour.', - 'Inventory' => 'Stocks', - 'Invoice amount' => 'Montant de la facture', - 'Invoice date' => 'Date de facturation', - 'Is Promotable' => 'Peut être promu', - 'Is Promotional Price?' => 'Est-ce un prix promotionnel?', - 'Is Shippable' => 'Peut être expédié', - 'Is Taxable' => 'Peut être taxé', - 'Item Rates' => 'Taux de l’item', - 'Item Subtotal' => 'Sous-total de l\'article', - 'Item Total' => 'Total de l\'article', - 'Item' => 'Article', - 'Items' => 'Articles', - 'Kilograms (kg)' => 'Kilogrammes (kg)', - 'Label' => 'Étiquette', - 'Landscape' => 'Landscape', - 'Language' => 'Langue', - 'Last Name' => 'Nom de famille', - 'Last Updated' => 'Dernière mise à jour', - 'Leave a category rate override blank to use the rate from above.' => 'Laissez un taux de catégorie vide pour utiliser le taux ci-dessus.', - 'Leave blank for unlimited uses.' => 'Laisser vide pour une utilisation illimitée.', - 'Leave blank if products don’t have URLs' => 'Laisser vide si les produits n’ont pas d’URL', - 'Leave gateway subscription as-is' => 'Conserver l\'abonnement à la passerelle tel quel', - 'Length ({unit})' => 'Longueur ({unit})', - 'Length' => 'Longueur', - 'Let each product choose which sites it should be saved to' => 'Laisser chaque produit choisir les sites sur lesquels il doit être enregistré', - 'Limit which orders this discount applies to based on its line items.' => 'Limiter les commandes auxquelles ce rabais s\'applique en fonction de leurs articles.', - 'Limit which purchasables this sale applies to.' => 'Limiter les produits achetables auxquels cette promotion s\'applique.', - 'Limit' => 'Limite', - 'Line Item Statuses' => 'Statuts de l\'article', - 'Line Item' => 'Article', - 'Line Items' => 'Articles', - 'Line item price (minus discounts)' => 'Prix de l\'article (moins les remises)', - 'Line item shipping cost' => 'Frais de livraison de l’article', - 'Line item statuses reordered.' => 'Articles réorganisés.', - 'Link Duration' => 'Durée du lien', - 'Link Sent' => 'Lien envoyé', - 'Link to a product' => 'Lien vers un produit', - 'Link to a variant' => 'Lier à une variante', - 'Link' => 'Lien', - 'Live' => 'En direct', - 'Location' => 'Emplacement', - 'Locations that should be available for previewing products in this product type.' => 'Emplacements à proposer pour la prévisualisation des produits de ce type de produit.', - 'MM' => 'MM', - 'Make a payment' => 'Effectuer un paiement', - 'Make this the primary store' => 'En faire le magasin principal', - 'Manage Inventory' => 'Gérer les stocks', - 'Manage donation settings' => 'Gérer les paramètres de don', - 'Manage general store settings' => 'Gérer les paramètres généraux du magasin', - 'Manage inventory locations' => 'Gérer les emplacements des stocks', - 'Manage inventory stock levels' => 'Gérer les niveaux de stocks', - 'Manage inventory transfers' => 'Gérer les transferts d\'inventaire', - 'Manage orders' => 'Gérer les commandes', - 'Manage payment currencies' => 'Gérer les devises de paiement', - 'Manage promotions' => 'Gérer des promotions', - 'Manage shipping' => 'Gérer l\'expédition', - 'Manage store settings' => 'Gérer les paramètres du magasin', - 'Manage subscription plans' => 'Gérer les abonnements', - 'Manage subscription' => 'Gérer l’abonnement', - 'Manage subscriptions' => 'Gérer les abonnements', - 'Manage taxes' => 'Gérer les taxes', - 'Manage' => 'Gérer', - 'Mark as Pending' => 'Marquer comme en attente', - 'Mark as completed' => 'Marquer comme terminé', - 'Match Billing Address' => 'Faire correspondre l\'adresse de facturation', - 'Match Customer' => 'Faire correspondre le client', - 'Match Order' => 'Faire correspondre la commande', - 'Match Orders' => 'Faire correspondre les commandes', - 'Match Product' => 'Faire correspondre le produit', - 'Match Purchasable' => 'Faire correspondre achetables', - 'Match Shipping Address' => 'Faire correspondre l\'adresse de livraison', - 'Match Variant' => 'Faire correspondre la variante', - 'Matching Items' => 'Articles correspondants', - 'Max Qty' => 'Qté max.', - 'Max Uses' => 'Nombre max d\'utilisations', - 'Max Variants' => 'Variantes max.', - 'Max quantity must greater than min.' => 'La quantité maximale doit être supérieure à la quantité minimale.', - 'Maximum Purchase Quantity' => 'Quantité d’achat maximum', - 'Maximum Total Shipping Cost' => 'Frais d’expédition totaux maximum', - 'Maximum allowed quantity' => 'Quantité maximale autorisée', - 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Le nombre maximum d’éléments correspondants qui peuvent être commandés pour que cette réduction s’applique. Une valeur zéro ici évitera cette condition.', - 'Maximum order quantity for this item is {num}.' => 'La quantité maximale pouvant être commandée pour cet article est {num}.', - 'Message' => 'Message', - 'Meters (m)' => 'Mètres (m)', - 'Millimeters (mm)' => 'Millimètres (mm)', - 'Min Qty' => 'Qté min.', - 'Min quantity must be less than max.' => 'La quantité minimale doit être inférieure à la quantité maximale.', - 'Minimum Purchase Quantity' => 'Quantité d’achat minimale', - 'Minimum Total Price Strategy' => 'Stratégie de prix minimum', - 'Minimum Total Shipping Cost' => 'Coût d’expédition minimum total', - 'Minimum allowed quantity' => 'Quantité minimale autorisée', - 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Nombre minimum d’articles correspondants qui doivent être commandés pour que cette réduction s’applique.', - 'Minimum order quantity for this item is {num}.' => 'La quantité minimale devant être commandée pour cet article est {num}.', - 'Missing Gateway' => 'Passerelle manquante', - 'Missing a default inventory location.' => 'Il manque un emplacement de stocks par défaut.', - 'Move Inventory' => 'Déplacer les stocks', - 'Move To' => 'Déplacer vers', - 'Move {qty} from {fromType} to {toType}' => 'Déplacer {qty} de {fromType} vers {toType}', - 'Move' => 'Déplacer', - 'Movement from deactivated inventory location' => 'Mouvement à partir d\'un lieu de stocks désactivé', - 'Movement' => 'Mouvement', - 'Must have at least one variant.' => 'Doit avoir au moins une variante.', - 'Name Field' => 'Champ de nom', - 'Name' => 'Nom', - 'New Customer' => 'Nouveau client', - 'New Customers' => 'Nouveaux clients', - 'New Order' => 'Nouvelle commande', - 'New PDF' => 'Nouveau PDF', - 'New address' => 'Nouvelle adresse', - 'New catalog pricing rule' => 'Nouvelle règle de tarification des catalogues', - 'New currency' => 'Nouvelle devise', - 'New discount' => 'Nouveau rabais', - 'New email' => 'Nouveau courriel', - 'New gateway' => 'Nouvelle passerelle', - 'New line item status' => 'Nouveau statut d\'article', - 'New line items get this status by default when the order is completed' => 'Les nouveaux articles obtiennent ce statut par défaut lorsque la commande est terminée', - 'New location' => 'Nouvel emplacement', - 'New order status' => 'Nouvel état de commande', - 'New orders get this status by default' => 'Les nouvelles commandes reçoivent cet état par défaut', - 'New product type' => 'Nouveau type de produit', - 'New product' => 'Nouveau produit', - 'New product, choose a type' => 'Nouveau produit, choisir un type', - 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Les nouveaux produits passent par défaut à la première catégorie de taxe disponible. Si aucune n\'est disponible, cette catégorie sera utilisée.', - 'New sale' => 'Nouvelle vente', - 'New shipping category' => 'Nouvelle catégorie d’expédition', - 'New shipping method' => 'Nouvelle méthode d’expédition', - 'New shipping rule' => 'Nouvelle règle d’expédition', - 'New shipping zone' => 'Nouvelle zone d’expédition', - 'New subscription plan' => 'Nouveau plan d’abonnement', - 'New tax category' => 'Nouvelle catégorie de taxes', - 'New tax rate' => 'Nouveau taux de taxes', - 'New tax zone' => 'Nouvelle zone de taxes', - 'New transfer' => 'Nouveau transfert', - 'New {productType} product' => 'Nouveau produit {productType}', - 'New' => 'Nouveau', - 'Next payment' => 'Prochain paiement', - 'No Address' => 'Pas d\adresse', - 'No PDFs exist yet.' => 'Il n\'existe pour l\'instant aucun PDF.', - 'No access given to any specific store management features.' => 'Aucun accès n\'est donné à des fonctions spécifiques de gestion de magasin.', - 'No additional payment currencies exist yet.' => 'Il n’existe plus aucune monnaie de paiement supplémentaire.', - 'No address' => 'Aucune adresse', - 'No billing address' => 'Aucune adresse de facturation', - 'No catalog pricing rule exists with the ID “{id}”' => 'Aucune règle de tarification du catalogue n\'existe avec l\'ID « {id} »', - 'No catalog pricing rules exist yet.' => 'Il n\'existe pas encore de règles de tarification pour les catalogues.', - 'No currency exists with the ID “{id}”' => 'Il n\'existe aucune devise avec l’identifiant « {id} »', - 'No customer email address exists on this cart.' => 'Aucune adresse courriel de client n’existe dans ce panier.', - 'No description' => 'Aucune description', - 'No discount exists with the ID “{id}”' => 'Il n’existe aucun rabais avec l’identifiant « {id} »', - 'No discounts exist yet.' => 'Aucun rabais n’a été créé.', - 'No donation amount supplied.' => 'Aucun montant de don fourni.', - 'No emails exist yet.' => 'Aucun courriel n’a été créé.', - 'No inventory changes made.' => 'Aucune modification de stocks n\'a été effectuée.', - 'No inventory found.' => 'Aucun stock n\'a été trouvé.', - 'No inventory movements made.' => 'Aucun mouvement de stocks n\'a été effectué.', - 'No inventory transactions for this location.' => 'Aucune transaction de stocks pour ce site.', - 'No new customer selected.' => 'Aucun nouveau client sélectionné.', - 'No order history exists with the ID “{id}”' => 'Il n’existe aucun historique de commande avec l’identifiant « {id} »', - 'No order status history items will exist until the cart becomes an order.' => 'Aucun élément n’existe dans l’historique des statuts de la commande jusqu’à ce que le panier soit converti en commande.', - 'No payment source exists with the ID “{id}”' => 'Il n\'existe aucune source de paiement avec l’identifiant « {id} »', - 'No private Note.' => 'Aucune note privée.', - 'No product available.' => 'Aucun produit disponible.', - 'No product types exist yet.' => 'Aucun type de produit n’a été créé.', - 'No purchasable available.' => 'Aucun achetable disponible.', - 'No sale exists with the ID “{id}”' => 'Il n’existe aucune promotion avec l’identifiant « {id} »', - 'No sales exist yet.' => 'Aucune vente n’a été créée.', - 'No shipping address' => 'Aucune adresse de livraison', - 'No shipping category exists with the ID “{id}”' => 'Il n\'existe aucune catégorie de livraison possédant l\'identifiant « {id} »', - 'No shipping method exists with the ID “{id}”' => 'Il n\'existe aucune méthode d\'expédition avec l\'identifiant « {id} »', - 'No shipping rule exists with the ID “{id}”' => 'Il n\'existe pas de règle de livraison avec l\'identifiant « {id} »', - 'No shipping rules exist yet.' => 'Aucune règle d’expédition n’a été créée.', - 'No shipping zone exists with the ID “{id}”' => 'Aucune zone de livraison avec l\'identifiant « {id} »', - 'No stats available.' => 'Aucune statistique disponible.', - 'No subscription plan exists with the ID “{id}”' => 'Il n\'existe aucun d’abonnement avec l’identifiant « {id} »', - 'No subscription plans exist yet.' => 'Aucun plan d’abonnement n’existe encore.', - 'No tax category exists with the ID “{id}”' => 'Il n’existe aucune catégorie de taxes avec l’identifiant « {id} »', - 'No tax rate exists with the ID “{id}”' => 'Il n\'existe aucun taux de taxe avec l\'identifiant « {id} »', - 'No tax zone exists with the ID “{id}”' => 'Il n\'existe aucune zone de taxe avec l\'identifiant « {id} »', - 'No transactions exist.' => 'Aucune transaction existante.', - 'No user authenticated.' => 'Aucun utilisateur authentifié.', - 'No' => 'Non', - 'None on hand' => 'Aucun à disposition', - 'None' => 'Aucun', - 'Not a valid address type' => 'N\'est pas un type d\'adresse valide', - 'Not a valid credit card number.' => 'Numéro de carte non valide.', - 'Not all SKUs are unique.' => 'Toutes les UGS ne sont pas uniques.', - 'Note' => 'Remarque', - 'Notes' => 'Notes', - 'Number of Coupons' => 'Nombre de coupons', - 'Number' => 'Numéro', - 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Parmi les sites activés ci-dessus, sur quels sites les produits de ce type de produit doivent-ils être enregistrés?', - 'On Hand' => 'Sur place', - 'Only allow this gateway to be used for zero value orders?' => 'Permettre uniquement l’utilisation de cette passerelle pour les commandes dont la valeur est zéro?', - 'Only match certain purchasables…' => 'Ne faire correspondre qu\'à certains produits achetables…', - 'Only match purchasables related to…' => 'Ne faire correspondre que les produits achetables liés à…', - 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Seules les commandes ayant les états de commande suivants seront incluses. Laissez vide pour inclure tous les états.', - 'Only save product to the site they were created in' => 'N’enregistrer les produits que sur le site où ils ont été créés', - 'Options' => 'Options', - 'Order Condition Formula' => 'Formule de la condition de commande', - 'Order Description Format' => 'Format de la description de la commande', - 'Order Details' => 'Détails de la commande', - 'Order Fields' => 'Champs de la commande', - 'Order PDF Download Link' => 'Lien de téléchargement du PDF de la commande', - 'Order PDF Filename Format' => 'Format du nom de fichier du PDF de commande', - 'Order Reference Number Format' => 'Format du numéro de référence de la commande', - 'Order Settings' => 'Paramètres de la commande', - 'Order Site' => 'Site de commande', - 'Order Status description.' => 'Description du statut de la commande.', - 'Order Status' => 'État de la commande', - 'Order Statuses' => 'États des commandes', - 'Order can not be empty.' => 'La commande ne peut pas être vide.', - 'Order count' => 'Nombre de commandes', - 'Order customer data removed.' => 'Données des clients supprimées des commandes.', - 'Order deleted.' => 'Commande supprimée.', - 'Order fields saved.' => 'Champs de commande enregistrés.', - 'Order not found.' => 'Commande non trouvée.', - 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Le solde de paiement de la commande est {outstandingBalanceAsCurrency}. Il s\'agit du montant maximum facturé.', - 'Order recalculated.' => 'Commande recalculée.', - 'Order status saved.' => 'Statut de commande enregistré.', - 'Order statuses reordered.' => 'Statuts des commandes réorganisés.', - 'Order total shipping cost' => 'Frais de livraison totaux de la commande', - 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Total taxable de la commande (sous-total des articles + total des rabais + total des frais de livraison)', - 'Order' => 'Commande', - 'Orders (Legacy)' => 'Commandes (Legacy)', - 'Orders deleted.' => 'Commandes supprimées.', - 'Orders not restored.' => 'Commandes non restaurées.', - 'Orders restored.' => 'Commandes restaurées.', - 'Orders' => 'Commandes', - 'Organization Name' => 'Nom de l\'organisation', - 'Organization Tax ID' => 'ID fiscal de l\'organisation', - 'Origin and destination cannot be the same.' => 'L\'origine et la destination ne peuvent pas être les mêmes.', - 'Origin' => 'Origine', - 'Original Price' => 'Prix d’origine', - 'Original price' => 'Prix original', - 'Original promotional price' => 'Prix promotionnel original', - 'Other Languages' => 'Autres langues', - 'Other countries' => 'Autres pays', - 'Outgoing transfer from Transfer ID: ' => 'Transfert sortant à partir de l\'ID de transfert : ', - 'Overpaid' => 'Surpayé', - 'Overrides previous?' => 'Remplace le précédent?', - 'PDF Attachment' => 'Fichier PDF joint', - 'PDF Template Path' => 'Chemin du modèle au format PDF', - 'PDF saved.' => 'PDF enregistré.', - 'PDF' => 'PDF', - 'PDFs & Emails' => 'PDF et courriels', - 'PDFs' => 'Fichiers PDF', - 'Paid Amount' => 'Montant payé', - 'Paid Status' => 'État du paiement', - 'Paid' => 'Payé', - 'Paper Orientation' => 'Orientation du papier', - 'Paper Size' => 'Format du papier', - 'Partial payment not allowed.' => 'Paiement partiel non autorisé.', - 'Partial' => 'Partiel', - 'Past year' => 'L\'année dernière', - 'Past {num} days' => '{num} derniers jours', - 'Pay {amount} of {currency} on the order.' => 'Payer {amount} {currency} sur la commande.', - 'Pay' => 'Payer', - 'Payment Amount' => 'Montant du paiement', - 'Payment Currencies' => 'Devises de paiement', - 'Payment Gateway' => 'Portail de paiement', - 'Payment Method' => 'Méthode de paiement', - 'Payment error: {message}' => 'Erreur de paiement : {message}', - 'Payment method issue' => 'Problème de moyen de paiement', - 'Payment source created.' => 'Source de paiement créée.', - 'Payment source deleted.' => 'Source de paiement supprimée.', - 'Payments' => 'Paiements', - 'Pending' => 'En cours', - 'Per Email Address Discount Limit' => 'Limite de rabais par adresse courriel', - 'Per Item Amount Off' => 'Montant de la remise par article', - 'Per Item Discount' => 'Rabais par article', - 'Per Item Percentage Off' => 'Montant de remise par article', - 'Per Item Rate' => 'Taux par article', - 'Per User Discount Limit' => 'Limite de rabais par utilisateur', - 'Percentage Rate' => 'Taux en pourcentage', - 'Phone (Alt)' => 'Téléphone (autre)', - 'Phone' => 'Numéro de téléphone', - 'Pick a plan' => 'Choisir un plan', - 'Plain Text Email Template Path' => 'Chemin du modèle de courriel de texte brut', - 'Plan' => 'Projet', - 'Plans reordered.' => 'Plans réorganisés.', - 'Portrait' => 'Portrait', - 'Post Date' => 'Date de publication', - 'Postal Code Formula' => 'Formule du code postal', - 'Pounds (lb)' => 'Livres (lb)', - 'Preview' => 'Aperçu', - 'Previous Status' => 'État précédent', - 'Price' => 'Prix', - 'Prices' => 'Prix', - 'Pricing Rules' => 'Règles de tarification', - 'Pricing jobs are currently running.' => 'Des travaux de tarification sont actuellement en cours.', - 'Pricing' => 'Tarification', - 'Primary Billing Address' => 'Adresse de facturation principale', - 'Primary Shipping Address' => 'Adresse de livraison principale', - 'Primary payment source updated.' => 'Source de paiement principale mise à jour.', - 'Primary' => 'Principal', - 'Private Note' => 'Note privée', - 'Product Fields' => 'Champs produit', - 'Product ID is required.' => 'Un identifiant produit est requis.', - 'Product Template' => 'Modèle du produit', - 'Product Title Format' => 'Format du titre de produit', - 'Product Type' => 'Type de produit', - 'Product Types' => 'Types de produits', - 'Product URI Format' => 'Format d\'URI produit', - 'Product Variant' => 'Variante de produit', - 'Product Variants' => 'Variantes de produit', - 'Product type saved.' => 'Type de produit enregistré.', - 'Product type settings' => 'Paramètres du type de produit', - 'Product' => 'Produit', - 'Products and Variants deleted.' => 'Produits et variantes supprimés.', - 'Products not restored.' => 'Produits non restaurés.', - 'Products restored.' => 'Produits restaurés.', - 'Products' => 'Produits', - 'Promotable' => 'Promouvable', - 'Promotable?' => 'Promouvable?', - 'Promotional Amount' => 'Montant promotionnel', - 'Promotional Price' => 'Prix promotionnel', - 'Purchasable Categories' => 'Catégories achetables', - 'Purchasable ID and Sale ID are required.' => 'Des identifiants achetable et de vente sont requis.', - 'Purchasable ID is required.' => 'Un identifiant achetable est requis.', - 'Purchasable Type' => 'Type d\'achat', - 'Purchasable' => 'Achetable', - 'Purchase (Authorize and Capture Immediately)' => 'Acheter (autoriser et collecter immédiatement)', - 'Purchase Total' => 'Total d’achat', - 'Qty' => 'Qté', - 'Quality Control' => 'Contrôle de la qualité', - 'Quantity' => 'Quantité', - 'Rate' => 'Taux', - 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Réaffecter {numOrders, plural, =1{la commande} other{les commandes}}', - 'Recalculate order' => 'Recalculer la commande', - 'Receive Inventory' => 'Recevoir l\'inventaire', - 'Receive Transfer' => 'Recevoir le transfert', - 'Receive' => 'Recevoir', - 'Received' => 'Reçu', - 'Recent Orders' => 'Commandes récentes', - 'Recipient' => 'Destinataire', - 'Recover Cart' => 'Récupérer le panier', - 'Reduce price' => 'Diminuer le prix', - 'Reduce the price by a fixed amount' => 'Diminuer le prix d’un montant fixe', - 'Reduce the price by a percentage of the original price' => 'Réduire le prix selon un pourcentage du prix initial', - 'Reference' => 'Référence', - 'Refresh payment history' => 'Actualiser l\'historique de paiement', - 'Refund note' => 'Note de remboursement', - 'Refund payment' => 'Rembourser le paiement', - 'Refund' => 'Remboursement', - 'Reject' => 'Rejeter', - 'Rejected' => 'Rejeté', - 'Relationship Type' => 'Type de relation', - 'Removable included tax rates are only allowed for the default tax zone.' => 'Les taux de taxes inclus supprimables sont uniquement autorisés pour la zone de taxes par défaut.', - 'Remove address' => 'Supprimer l\'adresse', - 'Remove all shipping costs from the order' => 'Retirer tous les coûts d\'expédition de la commande', - 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Supprimer l\'association avec le client et l\'adresse courriel {numOrders, plural, =1{de la commande} other{des commandes}}. Vous pouvez également sélectionner d\'autres données de clients à supprimer ci-dessous', - 'Remove customer data' => 'Supprimer les données de clients', - 'Remove from price?' => 'Retirer du prix?', - 'Remove shipping costs for matching items only' => 'Retirer les coûts d\'expédition uniquement pour les articles correspondants', - 'Remove the included tax when a valid organization tax ID is present?' => 'Supprimer la taxe incluse lorsqu\'un identifiant fiscal d\'organisation valide est présent?', - 'Remove' => 'Supprimer', - 'Removed' => 'Supprimé', - 'Repeat Customers' => 'Clients réguliers', - 'Reply To' => 'Répondre à', - 'Require Billing Address At Checkout' => 'Exiger l\'adresse de facturation au moment du paiement', - 'Require Coupon Code' => 'Demander un code de promotionnel', - 'Require Shipping Address At Checkout' => 'Exiger l\'adresse de livraison au moment du paiement', - 'Require Shipping Method Selection At Checkout' => 'Exiger le choix du mode d\'expédition au moment de la validation de la commande', - 'Require' => 'Demander', - 'Reserved' => 'Réservée', - 'Reset usage' => 'Réinitialiser l\'utilisation', - 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Restreindre le rabais aux commandes où le client a acheté une valeur totale minimale d’articles correspondants.', - 'Revenue Options' => 'Options de revenus', - 'Revenue' => 'Revenu', - 'Rule' => 'Règle', - 'Rules reordered.' => 'Règles réorganisées.', - 'SKU' => 'UGS', - 'Safety' => 'Sécurité', - 'Sale Price' => 'Prix en promotion', - 'Sale description.' => 'Description de la vente.', - 'Sale reordered.' => 'Promotion réorganisée.', - 'Sale saved.' => 'Promotion enregistrée.', - 'Sale' => 'Vente', - 'Sales deleted.' => 'Ventes supprimées.', - 'Sales updated.' => 'Ventes mises à jour.', - 'Sales' => 'Ventes', - 'Save and continue editing' => 'Enregistrer et continuer la modification', - 'Save and return to all orders' => 'Enregistrer et revenir à l\'ensemble des commandes', - 'Save and set rules' => 'Enregistrer et appliquer les règles', - 'Save as a new rule' => 'Enregistrer en tant que nouvelle règle', - 'Save product to all sites enabled for this product type' => 'Enregistrer les produits sur tous les sites activés pour ce type de produit', - 'Save product to other sites in the same site group' => 'Enregistrer le produit sur les autres sites du même groupe de sites', - 'Save product to other sites with the same language' => 'Enregistrer le produit sur les autres sites ayant la même langue', - 'Save' => 'Enregistrer', - 'Search customer…' => 'Rechercher un client…', - 'Search inventory' => 'Rechercher dans les stocks', - 'Search or enter customer email…' => 'Rechercher ou saisir un courriel client…', - 'Search…' => 'Rechercher…', - 'See Orders' => 'Voir les commandes', - 'Select a gateway' => 'Sélectionner une passerelle', - 'Select a tax category.' => 'Sélectionner une catégorie de taxes.', - 'Select a tax zone. If empty, this rate will match anywhere.' => 'Sélectionner une zone fiscale. Si aucune zone n\'est sélectionnée, ce taux s\'appliquera partout.', - 'Select address' => 'Sélectionner l\'adresse', - 'Select an item' => 'Sélectionner un article', - 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Sélectionnez la manière dont la règle de tarification du catalogue sera appliquée au(x) produit(s) acheté(s).', - 'Select how the sale will be applied to the purchasable(s).' => 'Indiquez comment la promotion sera appliquée aux articles en vente.', - 'Select product type' => 'Sélectionner le type de produit', - 'Select the emails that will be sent when transitioning to this status.' => 'Sélectionner les courriels qui seront envoyés lors de la transition à cet état.', - 'Select what this rate should be applied to.' => 'Sélectionner ce à quoi ce taux doit être appliqué.', - 'Send Email' => 'Envoyer le courriel', - 'Send to custom recipient' => 'Envoyer au destinataire personnalisé', - 'Send to the customer' => 'Envoyer au client', - 'Set Quantity' => 'Définir Quantité', - 'Set default category' => 'Définir la catégorie par défaut', - 'Set default variant' => 'Définir la variante par défaut', - 'Set or Adjust' => 'Régler ou ajuster', - 'Set price' => 'Définir le prix', - 'Set status' => 'Définir le statut', - 'Set the price to a flat amount' => 'Fixer le prix à un montant forfaitaire', - 'Set the price to a percentage of the original price' => 'Fixer le prix à un pourcentage du prix d\'origine', - 'Set the sale price to a flat amount' => 'Définir le prix en promotion à l\'aide d\'un montant fixe', - 'Set the sale price to a percentage of the original price' => 'Définir le prix en promotion comme un pourcentage du prix original', - 'Set to' => 'Définir sur', - 'Settings saved.' => 'Paramètres enregistrés.', - 'Settings' => 'Paramètres', - 'Share cart…' => 'Partager le panier…', - 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Expédition - Le coût minimum est le coût d\'expédition, si le prix de la commande est inférieur au coût d\'expédition.', - 'Shipping Address Zone' => 'Zone d\'adresse de livraison', - 'Shipping Address' => 'Adresse de livraison', - 'Shipping Business Name' => 'Nom de l\'entreprise pour la livraison', - 'Shipping Categories' => 'Catégories d’expédition', - 'Shipping Category Conditions' => 'Conditions de la catégorie d’expédition', - 'Shipping Category' => 'Catégorie d’expédition', - 'Shipping First Name' => 'Nom pour la livraison', - 'Shipping Full Name' => 'Nom complet pour la livraison', - 'Shipping Last Name' => 'Nom pour la livraison', - 'Shipping Method' => 'Méthode d’expédition', - 'Shipping Methods' => 'Méthodes d’expédition', - 'Shipping Rule' => 'Règle de livraison', - 'Shipping Zones' => 'Zones d’expédition', - 'Shipping address required.' => 'Adresse de livraison requise.', - 'Shipping categories deleted.' => 'Catégories d\'expédition supprimées.', - 'Shipping category saved.' => 'Catégorie d’expédition enregistrée.', - 'Shipping category updated.' => 'Catégorie de livraison mise à jour.', - 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Coûts d\'expédition ajoutés à la commande en un tout avant qu\'un pourcentage, un article et des tarifs au poids ne s\'appliquent. Régler à zéro pour désactiver ce taux. La règle entière, y compris ce taux de base, ne répondra pas et ne s\'appliquera pas si le panier ne contient que des articles non expédiables comme les produits numériques.', - 'Shipping method saved.' => 'Méthode d’expédition enregistrée.', - 'Shipping methods and rules deleted.' => 'Modes et règles d\'expédition supprimés.', - 'Shipping methods updated.' => 'Modes de livraison mis à jour.', - 'Shipping rule saved.' => 'Règle de livraison enregistrée.', - 'Shipping zone saved.' => 'Zone d’expédition enregistrée.', - 'Shipping' => 'Expédition', - 'Short Number' => 'Numéro court', - 'Show Chart?' => 'Afficher le graphique?', - 'Show Order Count?' => 'Afficher le nombre de commandes?', - 'Show all prices' => 'Afficher tous les prix', - 'Show archived gateways' => 'Afficher les portails archivés', - 'Show order count line on chart.' => 'Afficher la ligne de décompte des commandes sur le graphique.', - 'Show related sales' => 'Ventes liées au salon', - 'Show rule details' => 'Afficher les détails de la règle', - 'Show the Dimensions and Weight fields for products of this type' => 'Montrer les champs des dimensions et du poids pour les produits de ce type', - 'Show the Title field for products' => 'Afficher le champ Titre pour les produits', - 'Show the Title field for variants' => 'Montrer le champ titre pour les variantes', - 'Signed In' => 'Connexion effectuée', - 'Site Languages' => 'Langues du site', - 'Site store mapping saved.' => 'Cartographie du magasin du site enregistrée.', - 'Sites' => 'Sites', - 'Slug' => 'Identificateur', - 'Snapshot' => 'Instantané', - 'Snapshots' => 'Instantanés', - 'Some orders restored.' => 'Certaines commandes ont été restaurées.', - 'Some products restored.' => 'Certains produits ont été restaurés.', - 'Some variants restored.' => 'Certaines variantes ont été restaurées.', - 'Something changed with the order before payment, please review your order and submit payment again.' => 'Quelque chose a changé dans la commande avant le paiement, merci de la vérifier puis de soumettre le paiement de nouveau.', - 'Sorry, no matching options.' => 'Désolé, aucune option ne correspond.', - 'Source - The purchasable relationship field is on the category' => 'Source - Le champ de la relation d\'achat se trouve dans la catégorie', - 'Source' => 'Source', - 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Préciser une condition Twig déterminant si la réduction doit s\'appliquer à une commande donnée (la commande peut être référencée via la variable `order`).', - 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Préciser une condition Twig déterminant si la règle d\'expédition doit s\'appliquer à une commande donnée (la commande peut être référencée via la variable `order`).', - 'Start Date' => 'Date de début', - 'State' => 'État', - 'Status Email Address' => 'Adresse courriel d’état', - 'Status Emails' => 'Courriels d’état', - 'Status History' => 'Historique des statuts', - 'Status Updated.' => 'Statut mis à jour.', - 'Status change message' => 'Message de changement d\état', - 'Status' => 'État', - 'Stock' => 'Inventaire', - 'Stops Processing?' => 'Arrête le traitement?', - 'Stops subsequent?' => 'Arrête le suivant?', - 'Store Location' => 'Emplacement du magasin', - 'Store Management' => 'Gestion de magasin', - 'Store Markets' => 'Marchés de la boutique', - 'Store Rule' => 'Règle de magasin', - 'Store saved.' => 'Boutique enregistrée.', - 'Store' => 'Magasin', - 'Stores & Sites' => 'Magasins et sites', - 'Stores' => 'Magasins', - 'Strategy to apply when an order is free or has a zero balance.' => 'Stratégie à appliquer lorsqu\'une commande est gratuite ou a un solde nul.', - 'Strategy to apply when calculating the minimum order price.' => 'Stratégie à employer lors du calcul du prix de la commande minimale.', - 'Subject' => 'Objet', - 'Subscribing user' => 'Utilisateur abonné', - 'Subscription Fields' => 'Champs d’abonnement', - 'Subscription Plans' => 'Plans d’abonnement', - 'Subscription Settings' => 'Paramètres d\'abonnement', - 'Subscription cancelled.' => 'Abonnement annulé.', - 'Subscription date' => 'Date d’abonnement', - 'Subscription fields saved.' => 'Champs d’abonnement enregistrés.', - 'Subscription for {user} to {plan} prevented by a plugin.' => 'Un plugiciel a empêché l’abonnement de {user} à {plan}.', - 'Subscription plan saved.' => 'Abonnement enregistré.', - 'Subscription plan' => 'Plan d’abonnement', - 'Subscription plans' => 'Plans d’abonnement', - 'Subscription reactivated.' => 'Abonnement réactivé.', - 'Subscription reference' => 'Référence de l’abonnement', - 'Subscription started.' => 'Abonnement démarré.', - 'Subscription switched.' => 'Abonnement modifié.', - 'Subscription to “{plan}”' => 'Abonnement à « {plan} »', - 'Subscription' => 'Abonnement', - 'Subscriptions on hold' => 'Abonnements en attente', - 'Subscriptions' => 'Abonnements', - 'Suppress emails' => 'Réduire les courriels', - 'Switch plan' => 'Changer de plan', - 'Switch' => 'Changer', - 'System' => 'Système', - 'Table Columns' => 'Colonnes du tableau', - 'Target - The category relationship field is on the purchasable' => 'Cible - Le champ de la relation de catégorie se trouve sur la liste des produits achetables.', - 'Tax & Shipping' => 'Taxes et expédition', - 'Tax (inc)' => 'Taxe (incl.)', - 'Tax Categories' => 'Catégories de taxes', - 'Tax Category' => 'Catégorie de taxes', - 'Tax Rates' => 'Taux de taxes', - 'Tax Zone' => 'Zone de taxes', - 'Tax Zones' => 'Zones de taxes', - 'Tax categories deleted.' => 'Catégories de taxe supprimées.', - 'Tax category saved.' => 'Catégorie de taxes enregistrée.', - 'Tax category updated.' => 'Catégorie de taxe mise à jour.', - 'Tax rate saved.' => 'Taux de taxes enregistré.', - 'Tax rates updated.' => 'Taux de taxe mis à jour.', - 'Tax zone saved.' => 'Zone de taxe enregistrée.', - 'Tax' => 'Taxe', - 'Taxable Subject' => 'Objet taxable', - 'Template Path' => 'Chemin du modèle', - 'That handle is already in use' => 'Cet identificateur est déjà utilisé', - 'That handle is already in use.' => 'Cet identificateur est déjà utilisé.', - 'The PDF to attach to this email.' => 'Le PDF à attacher à ce courriel.', - 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'L\'URL de la page permettant de mettre à jour les détails de facturation d\'un abonnement, ainsi que de gérer l\'authentification 3DS.', - 'The address provided is outside the store’s market.' => 'L\'adresse fournie est en dehors du marché de la boutique.', - 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'Le montant du rabais qui est appliqué à l\'ensemble de la commande. Ce montant est réparti sur les articles dans l\'ordre, du prix le plus élevé au prix le plus bas, jusqu\'à la fin du rabais.', - 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'Le rabais de base ne peut réduire le prix des articles du panier à zéro ni rendre le montant de la commande négatif.', - 'The cart recovery link is invalid. Please request a new one.' => 'Le lien de récupération du panier n\'est pas valide. Veuillez en demander un nouveau.', - 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Le taux de conversion qui sera utilisé lors de la conversion d’un montant en cette devise. Par exemple, si un article coût {amount1}, un taux de conversion de {rate} aboutira à {amount2} dans la devise alternative.', - 'The countries that orders are allowed to be placed from.' => 'Les pays depuis lesquels il est possible de passer une commande.', - 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'Le coupon « {code} » a dépassé la limite de {limit} utilisations.', - 'The customer for this order has been deleted.' => 'Le client associé à cette commande a été supprimé.', - 'The default shipping category is automatically available to all product types.' => 'La catégorie d\'expédition par défaut est automatiquement disponible pour tous les types de produits.', - 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'La remise « {name} » a dépassé sa limite d\'utilisation totale de {limit}.', - 'The download link has expired. Please request a new one.' => 'Le lien de téléchargement a expiré. Veuillez en demander un nouveau.', - 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'L’adresse courriel à partir de laquelle les courriels d’état de commande sont envoyés. Laisser vide pour utiliser l’adresse courriel de système définie dans les paramètres généraux de Craft.', - 'The entry that contains the description for this subscription’s plan.' => 'L’entrée qui contient la description de ce plan d’abonnement.', - 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'La remise forfaitaire appliquée à chaque article. Par ex : « 3 » pour 3 $ de remise par article.', - 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'Le format utilisé pour générer de nouveaux coupons, par exemple {example}. Tout caractère « # » sera remplacé par une lettre aléatoire.', - 'The from and to inventory locations must be different.' => 'Les emplacements des stocks de départ et d\'arrivée doivent être différents.', - 'The inventory locations this store uses.' => 'Les emplacements des stocks utilisés par ce magasin.', - 'The item is not enabled for sale.' => 'Cet article n’est pas activé pour la vente.', - 'The language the order was made in.' => 'La langue dans laquelle la commande a été passée.', - 'The language to be used when this email is rendered.' => 'La langue à utiliser lors de l\'affichage de ce courriel.', - 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Le nombre maximum de niveaux que ce type de produit peut avoir. Laisser vide si cela n\'est pas pertinent.', - 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Le maximum que le client doit dépenser sur l’expédition. Mettez à zéro pour désactiver.', - 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Le minimum que le client doit dépenser sur l’expédition. Mettez à zéro pour désactiver.', - 'The order is not valid.' => 'La commande n\'est pas valide.', - 'The payment gateway that will be used for the subscription plan.' => 'Quelle passerelle de paiement sera utilisée pour le plan d’abonnement?', - 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'La valeur du pourcentage de rabais qui doit être appliqué à chaque article, par exemple {ex1} pour une remise de {ex2}. Les pourcentages sont arrondis à 2 décimales.', - 'The previously-selected shipping method is no longer available.' => 'La méthode d\'expédition précédemment sélectionnée n\'est plus disponible.', - 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Le prix de {description} a augmenté, il est passé de {originalSalePriceAsCurrency} à {newSalePriceAsCurrency}', - 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Le prix de {description} a baissé, il est passé de {originalSalePriceAsCurrency} à {newSalePriceAsCurrency}', - 'The primary currency cannot be changed after orders are placed.' => 'La devise principale ne peut être modifiée après la validation des commandes.', - 'The purchasable defines the relationship' => 'Le produit achetable définit la relation', - 'The purchasable is related by another element' => 'Le produit achetable est lié par un autre élément', - 'The recipient of the email. Twig code can be used here.' => 'Le destinataire du courriel. Du code Twig peut être utilisé ici.', - 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'L\'adresse courriel de réponse. Laisser vide pour une réponse normale à l\'expéditeur du courriel. Du code Twig peut être utilisé ici.', - 'The site the order was made in.' => 'Le site sur lequel la commande a été passée.', - 'The site to be used when this email is rendered.' => 'Le site à utiliser lorsque ce courriel est affiché.', - 'The subject line of the email. Twig code can be used here.' => 'L\'objet du courriel. Du code Twig peut être utilisé ici.', - 'The template that the PDF should be generated from.' => 'Le modèle à partir duquel le PDF doit être généré.', - 'The template to be used for HTML emails.' => 'Le modèle à utiliser pour les courriels HTML.', - 'The template to be used for plain text emails. Twig code can be used here.' => 'Le modèle à utiliser pour les courriels en texte brut. Du code Twig peut être utilisé ici.', - 'The template to use when a product’s URL is requested.' => 'Le modèle à utiliser lorsque l’URL d’un produit est demandé.', - 'The total number of order adjustments changed.' => 'Le nombre total d’ajustements de commande a changé.', - 'The total price of the order changed.' => 'Le montant total de la commande a été modifié.', - 'The total quantity of items within the order changed.' => 'La quantité totale d’articles dans la commande a changé.', - 'The unique SKU of the donation purchasable.' => 'L\'unique UGS de don pouvant être achetée.', - 'The unit of measurement that should be used when specifying product dimensions.' => 'L’unité de mesure qui devrait être utilisée pour indiquer les dimensions d’un produit.', - 'The unit of measurement that should be used when specifying product weights.' => 'L’unité de mesure qui devrait être utilisée pour indiquer les poids d’un produit.', - 'The webhook URL for this gateway.' => 'L\'URL du webhook pour cette passerelle.', - 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'Le nom de l’expéditeur à utiliser lors de l’envoi des courriels d’état de commande. Laisser vide pour utiliser le nom d’expéditeur défini dans les paramètres généraux de Craft.', - 'There are errors on the order' => 'Il y a des erreurs dans la commande', - 'There are only {num} “{description}” items left in stock.' => 'Il ne reste que {num} articles « {description} » en stock.', - 'There aren’t any product types to select yet.' => 'Il n\'y a pas encore de types de produits à sélectionner.', - 'There is no gateway or payment source available for use with this order.' => 'Aucun portail ou source de paiement disponible pour cette commande.', - 'There is no gateway selected that supports payment sources.' => 'Aucune passerelle prenant en charge les modes de paiement n’est sélectionnée.', - 'There is no shipping method selected for this order.' => 'Aucune méthode d\'expédition sélectionnée pour cette commande.', - 'This URL will load the cart into the user’s session, making it the active cart.' => 'Cette URL chargera le panier dans la session de l\'utilisateur, ce qui en fera le panier actif.', - 'This action is not allowed for the current user.' => 'Cette action n\'est pas autorisée pour l\'utilisateur actuel.', - 'This category will be used as the default for all purchasables in this store.' => 'Cette catégorie sera utilisée par défaut pour tous les articles pouvant être achetés dans ce magasin.', - 'This coupon is for registered users and limited to {limit} uses.' => 'Ce coupon est limité à {limit} utilisations pour les utilisateurs inscrits.', - 'This coupon is limited to {limit} uses.' => 'Ce coupon est limité à {limit} utilisations.', - 'This coupon requires an email address.' => 'Ce rabais nécessite une adresse courriel.', - 'This gateway does not support that functionality.' => 'Cette passerelle ne prend pas en charge cette fonctionnalité.', - 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Cela est annulé par le paramètre de configuration {setting} dans `config/{file}.php`.', - 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Il s’agit de l’adresse physique de votre magasin. Elle peut être utilisée par divers plugiciels pour déterminer des éléments comme la livraison et les taxes. Elle peut aussi être utilisée dans les reçus PDF.', - 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Ceci est le PDF par défaut qui sera fourni lors de la demande du PDF de commande.', - 'This is the last location for the {store} store.' => 'Il s\'agit du dernier emplacement pour le magasin {store}.', - 'This month' => 'Ce mois-ci', - 'This order has unsaved changes.' => 'Cette commande a des modifications non enregistrées.', - 'This week' => 'Cette semaine', - 'This year' => 'Cette année', - 'Times Used' => 'Nombre de fois utilisé', - 'Title' => 'Titre', - 'To' => 'À', - 'Today' => 'Aujourd’hui', - 'Too many variants for this product.' => 'Trop de variantes pour ce produit.', - 'Top Customers by Average Order' => 'Meilleurs clients par commande moyenne', - 'Top Customers by Total Revenue' => 'Meilleurs clients par recettes totales', - 'Top Customers' => 'Meilleurs clients', - 'Top Product Types by Qty Sold' => 'Meilleurs types de produits par Qté vendue', - 'Top Product Types by Revenue' => 'Meilleurs types de produits par recettes', - 'Top Product Types' => 'Meilleurs types de produits', - 'Top Products by Qty Sold' => 'Meilleurs produits par Qté vendue', - 'Top Products by Revenue' => 'Meilleurs produits par recettes', - 'Top Products' => 'Meilleurs produits', - 'Top Purchasables by Qty Sold' => 'Meilleurs achetables par Qté vendue', - 'Top Purchasables by Revenue' => 'Meilleurs achetables par recettes', - 'Top Purchasables' => 'Meilleurs achetables', - 'Total ' => 'Total ', - 'Total Discount Use Limit' => 'Limite d\'utilisation totale des rabais', - 'Total Discount' => 'Rabais total', - 'Total Included Tax' => 'Taxes totales incluses', - 'Total Orders by Billing Country' => 'Total des commandes par pays de facturation', - 'Total Orders by Country' => 'Total des commandes par pays', - 'Total Orders by Shipping Country' => 'Total des commandes par pays de livraison', - 'Total Orders' => 'Nombre total de commandes', - 'Total Paid' => 'Total payé', - 'Total Price' => 'Prix total', - 'Total Qty' => 'Qté totale', - 'Total Revenue' => 'Total des recettes', - 'Total Shipping' => 'Total de la livraison', - 'Total Tax' => 'Total des taxes', - 'Total Weight' => 'Poids total', - 'Total' => 'Total', - 'Track Inventory' => 'Suivi des stocks', - 'Transaction Hash' => 'Hachage de la transaction', - 'Transaction ID' => 'ID de la transaction', - 'Transaction captured successfully: {message}' => 'Transaction collectée avec succès : {message}', - 'Transaction refunded successfully: {message}' => 'Transaction remboursée avec succès : {message}', - 'Transactions' => 'Transactions', - 'Transfer Fields' => 'Champs de transfert', - 'Transfer Items' => 'Articles de transfert', - 'Transfer Settings' => 'Paramètres de transfert', - 'Transfer Status' => 'Statut du transfert', - 'Transfer fields saved.' => 'Champs de transfert enregistrés.', - 'Transfer must have at least one item.' => 'Le transfert doit comporter au moins un article.', - 'Transfer' => 'Transférer', - 'Transfers' => 'Transferts', - 'Trial days credited' => 'Jours d’essai crédités', - 'Trial expiration' => 'Expiration de l\'essai', - 'Trial expiry date' => 'Date d’expiration de la version d’essai', - 'Type not in allowed options.' => 'Type non autorisé dans les options.', - 'Type' => 'Type', - 'URI' => 'URI', - 'Unable to cancel subscription at this time.' => 'Impossible d\'annuler l’abonnement actuellement.', - 'Unable to complete order: another request is already in progress.' => 'Impossible de terminer la commande : une autre demande est déjà en cours.', - 'Unable to find variant.' => 'Impossible de trouver la variante.', - 'Unable to generate coupon codes: {message}' => 'Impossible de générer des codes de réduction : {message}', - 'Unable to make payment at this time.' => 'Impossible d’effectuer le paiement pour le moment.', - 'Unable to modify subscription at this time.' => 'Impossible de modifier l’abonnement actuellement.', - 'Unable to reactivate subscription at this time.' => 'Impossible de réactiver l’abonnement pour le moment.', - 'Unable to reassign orders.' => 'Impossible de réattribuer les commandes.', - 'Unable to remove order data.' => 'Impossible de supprimer les données de la commande.', - 'Unable to retrieve Sale and Purchasable.' => 'Impossible de récupérer les promotions et les achetables.', - 'Unable to retrieve cart.' => 'Impossible de récupérer le panier.', - 'Unable to retrieve customer.' => 'Impossible de récupérer le client.', - 'Unable to retrieve load cart URL' => 'Impossible de récupérer et charger l\'URL du panier', - 'Unable to retrieve payment source.' => 'Impossible de récupérer la source de paiement.', - 'Unable to set default shipping category.' => 'Impossible de définir la catégorie d\'expédition par défaut.', - 'Unable to set default tax category.' => 'Impossible de définir la catégorie de taxe par défaut.', - 'Unable to set primary payment source.' => 'Impossible de définir la source de paiement principale.', - 'Unable to start the subscription. Please check your payment details.' => 'Impossible de démarrer l’abonnement. Veuillez vérifier vos informations de paiement.', - 'Unable to subscribe at this time.' => 'Impossible de s’abonner pour le moment.', - 'Unable to update cart.' => 'Impossible de mettre à jour le panier.', - 'Unable to validate address.' => 'Impossible de valider l’adresse.', - 'Unit Price' => 'Prix unitaire', - 'Unit price (minus discounts)' => 'Prix unitaire (moins les remises)', - 'Units' => 'Unités', - 'Unpaid' => 'Non payé', - 'Unsubscribe' => 'Annuler l’abonnement', - 'Update Address' => 'Mettre à jour l’adresse', - 'Update Order Status' => 'Mettre à jour le statut de la commande', - 'Update Order Status…' => 'Mettre à jour le statut de la commande…', - 'Update order' => 'Mettre à jour la commande', - 'Update subscription' => 'Mettre à jour l\'abonnement', - 'Update' => 'Mettre à jour', - 'Updated By' => 'Mis à jour par', - 'Updated committed stock successfully.' => 'Validation des stocks engagés réussie.', - 'Updated' => 'Mis à jour', - 'Use Billing Address For Tax' => 'Utiliser l\'adresse de facturation pour les taxes', - 'Use as the primary billing address' => 'Utiliser comme adresse de facturation principale', - 'Use as the primary shipping address' => 'Utiliser comme adresse de livraison principale', - 'Used By Tax Rates' => 'Utilisé par les taux de taxes', - 'Used by Tax Rates' => 'Utilisé par le taux de taxe', - 'User Groups' => 'Groupes d’utilisateurs', - 'User not found.' => 'Utilisateur non trouvé.', - 'User' => 'Utilisateur', - 'Uses' => 'Utilisations', - 'Validate Business Tax ID as Vat ID' => 'Valider l\'ID de la taxe d\'affaires en tant qu\'ID de TPS/TVQ', - 'Validating condition syntax' => 'Validation de la syntaxe conditionnelle', - 'Validating formula syntax' => 'Validation de la syntaxe de la formule', - 'Variant Fields' => 'Champs de variante', - 'Variant Has Untracked Stock' => 'La variante a un stock non tracé', - 'Variant Price' => 'Prix de la variante', - 'Variant SKU' => 'UGS de la variante', - 'Variant Search' => 'Rechercher une variante', - 'Variant Stock' => 'Stock de la variante', - 'Variant Title Format' => 'Format de titre de variante', - 'Variant Tracks Stock' => 'La variante suit les stocks', - 'Variant UI Label Format' => 'Format des étiquettes de l\'interface de variantes', - 'Variant has no product.' => 'La variante n\'a pas de produit.', - 'Variants not restored.' => 'Variantes non restaurées.', - 'Variants restored.' => 'Variantes restaurées.', - 'Variants' => 'Variantes', - 'View customer' => 'Voir le client', - 'View order' => 'Afficher la commande', - 'View product type - {productType}' => 'Voir le type de produit - {productType}', - 'View user' => 'Voir l\'utilisateur', - 'View' => 'Voir', - 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Attention, la suppression de cette devise entraînera l\'arrêt de tous les paiements et remboursements dans cette devise, êtes-vous sûr de vouloir supprimer « {name} »?', - 'Web' => 'Web', - 'Webhook URL' => 'URL du webhook', - 'Weight ({unit})' => 'Poids ({unit})', - 'Weight Rate' => 'Taux de poids', - 'Weight Unit' => 'Unité de poids', - 'Weight' => 'Poids', - 'What product URIs should look like for the site.' => 'Ce à quoi les URIs de produits devraient ressembler pour le site.', - 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Ce à quoi devraient ressembler les titres de produits générés automatiquement. Vous pouvez inclure des balises qui produisent des propriétés de produits, comme {ex1} ou {ex2}. Tous les champs personnalisés utilisés doivent être définis comme étant obligatoires.', - 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Ce à quoi les titres de variante auto-générés devraient ressembler. Vous pouvez inclure des étiquettes qui sélectionnent les propriétés de la variante, comme {ex1} ou {ex2}. Tous les champs personnalisés utilisés doivent être exigés.', - 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Ce à quoi le nom de fichier PDF de commande devrait ressembler (sans extension). Vous pouvez inclure des étiquettes représentant certaines caractéristiques de la commande, telles que {ex1} ou {ex2}.', - 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Ce à quoi les UGS auto-générés devraient ressembler, lorsqu’un champ d\'UGS est soumis à vide. Vous pouvez inclure des étiquettes qui sélectionnent les propriétés, comme {ex1} ou {ex2}.', - 'What this PDF will be called in the control panel.' => 'Nom de ce PDF dans le panneau de configuration.', - 'What this catalog pricing rule will be called in the control panel.' => 'Le nom de cette règle de tarification du catalogue dans le panneau de configuration.', - 'What this discount will be called in the control panel.' => 'Nom de ce rabais dans le panneau de configuration.', - 'What this email will be called in the control panel.' => 'Nom de ce courriel dans le panneau de configuration.', - 'What this product type will be called in the control panel.' => 'Nom de ce type de produit dans le panneau de configuration.', - 'What this sale will be called in the control panel.' => 'Nom de cette promotion dans le panneau de configuration.', - 'What this shipping category will be called in the control panel.' => 'Nom de cette catégorie de livraison dans le panneau de configuration.', - 'What this shipping rule will be called in the control panel.' => 'Nom de cette règle de livraison dans le panneau de configuration.', - 'What this shipping zone will be called in the control panel.' => 'Nom de cette zone de livraison dans le panneau de configuration.', - 'What this status will be called in the control panel.' => 'Nom de ce statut dans le panneau de configuration.', - 'What this subscription plan will be called in the control panel.' => 'Nom de cet abonnement dans le panneau de configuration.', - 'What this tax category will be called in the control panel.' => 'Nom de cette catégorie de taxe dans le panneau de configuration.', - 'What this tax zone will be called in the control panel.' => 'Nom de cette zone de taxe dans le panneau de configuration.', - 'When this discount is applied to an order, which line items should be discounted?' => 'Lorsque ce rabais est appliqué à une commande, quels postes doivent faire l\'objet d\'un rabais?', - 'Whether the first available shipping method option should be set automatically on carts.' => 'Indique si la première option de méthode d\'expédition disponible doit être définie automatiquement dans les paniers.', - 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Indique si la source de paiement principale de l\'utilisateur doit être définie automatiquement pour les nouveaux paniers.', - 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Indique si les adresses principales de livraison et de facturation de l\'utilisateur doivent être définies automatiquement pour les nouveaux paniers.', - 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Indique si cette règle de tarification du catalogue doit pouvoir être utilisée, indépendamment d\'autres conditions.', - 'Whether this sale should be available for use, regardless of other conditions.' => 'Si cette vente doit être disponible ou non, indépendamment des autres conditions.', - 'Which data to display in the name column in the results table.' => 'Les données à afficher dans la colonne des noms dans le tableau des résultats.', - 'Which product types should this category be available to?' => 'Dans quels types de produits cette catégorie devrait-elle être offerte?', - 'Which template should be loaded when a product’s URL is requested.' => 'Quel modèle devrait être chargé quand l’URL d’un produit est demandée.', - 'Width ({unit})' => 'Largeur ({unit})', - 'Width' => 'Largeur', - 'YYYY' => 'AAAA', - 'Yes' => 'Oui', - 'You are not allowed to add a line item.' => 'Vous n\'êtes pas autorisé à ajouter un article.', - 'You currently have no emails configured to select for this status.' => 'Vous n\'avez actuellement aucune adresse courriel configurée à sélectionner pour ce statut.', - 'You do not have permission to load this cart.' => 'Vous n\'êtes pas autorisé à charger ce panier.', - 'You must set up at least one gateway that supports subscriptions first.' => 'Vous devez définir au moins une passerelle qui prend d’abord en charge les abonnements.', - 'You must be logged in or provide a valid token to load this cart.' => 'Vous devez être connecté ou fournir un jeton valide pour charger ce panier.', - 'You must be signed in to create a payment source.' => 'Vous devez être inscrit pour créer une source de paiement.', - 'You must be signed in to set a primary payment source.' => 'Vous devez être inscrit pour définir une source de paiement principale.', - 'You must make a payment to complete the order.' => 'Vous devez effectuer un paiement pour terminer la commande.', - 'Your Cart Recovery Link' => 'Votre lien de récupération de panier', - 'Your Order PDF Download Link' => 'Lien de téléchargement du PDF de votre commande', - 'Your order is empty' => 'Votre commande est vide', - 'ZIP file' => 'Fichier ZIP', - 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Zéro - Le prix minimum est zéro si les rabais dépassent la valeur de la commande.', - 'Zip Code' => 'Code postal', - 'all' => 'tous', - 'any' => 'n\'importe quel', - 'average order total' => 'total de commande moyen', - 'billing address' => 'adresse de facturation', - 'donation' => 'don', - 'donations' => 'dons', - 'info' => 'infos', - 'inventory location' => 'emplacement des stocks', - 'new customers' => 'nouveaux clients', - 'on hand' => 'disponible', - 'only' => 'seulement', - 'order' => 'commande', - 'orders' => 'commandes', - 'price' => 'prix', - 'prices' => 'prix', - 'product variant' => 'variante du produit', - 'product variants' => 'variantes du produit', - 'product' => 'produit', - 'products' => 'produits', - 'repeat customers' => 'clients réguliers', - 'shipping address' => 'adresse de livraison', - 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'Les valeurs shippingSameAsBilling et billingSameAsShipping ne peuvent être définies.', - 'subscription' => 'abonnement', - 'subscriptions' => 'abonnements', - 'to' => 'à', - 'transfer' => 'transférer', - 'transfers' => 'transferts', - '{amount} included' => '{amount} inclus', - '{count} Unfulfilled Orders' => '{count} commandes non satisfaites', - '{description} is no longer available.' => '{description} n\'est plus disponible.', - '{description} only has {stock} in stock.' => '{description} n\'a que {stock} unités en stock.', - '{from} to {to}' => '{from} à {to}', - '{name} (Primary)' => '{name} (primaire)', - '{name} (Trashed)' => '{name} (mis à la poubelle)', - '{name} catalog price' => 'prix catalogue {name}', - '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, one {}=1{commande mise à jour} other{commandes mises à jour}}.', - '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, =1{commande est associée} other{commandes sont associées}} {numUsers, plural, =1{à l\'utilisateur} other{aux utilisateurs}}.', - '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, =1{abonnement est activé} other{abonnements sont activés}} pour {numUsers, plural, =1{l\'utilisateur} other{les utilisateurs}}.', - '{number} more…' => '{number} plus…', - '{pct} off the discounted item price' => '{pct} sur le prix de l\'article incluant le rabais', - '{pct} off the original item price' => '{pct} sur le prix original de l\'article', - '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, =1{n\'a pas été assigné} other{n\'ont pas été assignés}} à un site.', - '{total} in total revenue' => '{total} dans les recettes totales', - '{total} orders' => '{total} commandes', - '{total} saleable across {locationCount} location(s)' => '{total} vendables parmi {locationCount} emplacement(s).', - '{uses} uses across {emails} email addresses' => '{uses} utilisations pour {emails} adresses courriel', - '{uses} uses across {users} users' => '{uses} utilisations pour {users} utilisateurs', - '“{description}” is currently out of stock.' => '« {description} » est actuellement épuisé.', - '“{key}” has invalid JSON' => '« {key} » possède une valeur JSON non valide', -]; diff --git a/src/translations/fr/commerce.php b/src/translations/fr/commerce.php deleted file mode 100644 index 9ca79d8abd..0000000000 --- a/src/translations/fr/commerce.php +++ /dev/null @@ -1,1428 +0,0 @@ - '(nouveau prix)', - '(of original price)' => '(par rapport au prix d’origine)', - '(off original price)' => '(en moins par rapport au prix d’origine)', - 'A cart number must be specified.' => 'Un numéro de panier doit être spécifié.', - 'A cart recovery link has been sent to {email}.' => 'Un lien de récupération du panier a été envoyé à {email}.', - 'A cart recovery link will be sent to {email}.' => 'Un lien de récupération du panier sera envoyé à {email}.', - 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Un numéro de référence simple sera généré sur la base de ce format lors de la finalisation d’un panier et de sa conversion en commande. Par exemple {ex1}, ou
{ex2}. Le résultat de ce formatage doit être unique.', - 'A new download link has been sent to {email}' => 'Un nouveau lien de téléchargement a été envoyé à {email}', - 'A new download link will be sent to {email}' => 'Un nouveau lien de téléchargement sera envoyé à {email}', - 'A valid email is required to create a customer.' => 'Un e-mail valide est requis pour créer un client.', - 'Accept' => 'Accepter', - 'Accepted' => 'Accepté', - 'Actions' => 'Actions', - 'Active Carts' => 'Paniers actifs', - 'Active subscriptions' => 'Abonnements actifs', - 'Active' => 'Actif', - 'Add Address' => 'Ajouter l’adresse', - 'Add a coupon' => 'Ajouter un coupon', - 'Add a custom line item' => 'Ajouter un article personnalité', - 'Add a line item' => 'Ajouter un article', - 'Add a product' => 'Ajouter un produit', - 'Add a variant' => 'Ajouter une variante', - 'Add an adjustment' => 'Ajouter un ajustement', - 'Add an item' => 'Ajouter un article', - 'Add an option' => 'Ajouter une option', - 'Add catalog price' => 'Ajouter le prix de catalogue', - 'Add' => 'Ajouter', - 'Additional Actions' => 'Actions supplémentaires', - 'Additional recipients that should receive this email. Twig code can be used here.' => 'Destinataires additionnels qui devraient recevoir cet e-mail. Du code Twig peut être utilisé ici.', - 'Address 1' => 'Adresse 1', - 'Address 2' => 'Adresse 2', - 'Address 3' => 'Adresse 3', - 'Address Line 1' => 'Adresse ligne 1', - 'Address Line 2' => 'Adresse ligne 2', - 'Address Updated.' => 'Adresse mise à jour.', - 'Address copied to user.' => 'Adresse copiée pour l\'utilisateur.', - 'Address not found.' => 'Adresse non trouvée.', - 'Adjust Quantity' => 'Ajuster la quantité', - 'Adjust by' => 'Ajuster par', - 'Adjust price when included rate is disqualified?' => 'Ajuster le prix lorsque le taux de taxe inclus est disqualifié ?', - 'Adjustments' => 'Ajustements', - 'Admin Notices' => 'Avis de l\'administrateur', - 'Administrative Area Code of Origin' => 'Code d\'origine de la zone administrative', - 'Advanced' => 'Avancé', - 'All Orders' => 'Toutes les commandes', - 'All Totals' => 'Tous les totaux', - 'All Transfers' => 'Tous les transferts', - 'All active subscriptions' => 'Tous les abonnements actifs', - 'All customers' => 'Tous les clients', - 'All products' => 'Tous les produits', - 'All variants must have a SKU.' => 'Toutes les variantes doivent avoir un SKU.', - 'All' => 'Tous', - 'Allow Checkout Without Payment' => 'Autoriser le passage de commande sans paiement', - 'Allow Empty Cart On Checkout' => 'Autoriser le panier vide au passage de commande', - 'Allow Partial Payment On Checkout' => 'Autoriser le paiement partiel au passage de commande', - 'Allow out of stock purchases' => 'Permettre les achats de produits en rupture de stock', - 'Allow' => 'Autoriser', - 'Allowed Qty' => 'Quantité autorisée', - 'Alternative Phone' => 'Autre téléphone', - 'Amount' => 'Montant', - 'An ID must be provided' => 'Un identifiant doit être fourni', - 'An error occurred while generating this PDF.' => 'Une erreur est survenue lors la création de ce fichier PDF.', - 'Any' => 'N\'importe quel', - 'Anywhere' => 'Partout', - 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Souhaitez-vous réellement archiver l’abonnement "{name}" ? Cette opération N’ANNULERA PAS les abonnements existants.', - 'Are you sure you want to capture this transaction?' => 'Êtes-vous sûr de vouloir collecter cette transaction ?', - 'Are you sure you want to complete this order?' => 'Êtes-vous sûr de vouloir terminer cette commande ?', - 'Are you sure you want to delete the selected orders?' => 'Êtes-vous sûr de vouloir supprimer les commandes sélectionnées ?', - 'Are you sure you want to delete the selected product and its variants?' => 'Êtes-vous sûr de vouloir supprimer le produit sélectionné et ses variantes ?', - 'Are you sure you want to delete this shipping rule?' => 'Êtes-vous sûr de vouloir supprimer cette règle de livraison ?', - 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Êtes-vous sûr de vouloir supprimer « {name} » et tous ses produits ? Veuillez vous assurer que vous avez effectué une sauvegarde de votre base de données avant de réaliser cette action destructrice.', - 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Voulez-vous vraiment supprimer « {name} » ? Cela définira tous les articles avec ce statut sur aucun statut.', - 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Voulez-vous vraiment marquer ce transfert comme étant en attente ? Il apparaîtra comme entrant à la destination.', - 'Are you sure you want to overwrite the billing address?' => 'Êtes-vous sûr de vouloir écraser les adresses de facturation ?', - 'Are you sure you want to overwrite the shipping address?' => 'Êtes-vous sûr de vouloir écraser les adresses d\'expédition ?', - 'Are you sure you want to permanently delete this store and everything in it?' => 'Voulez-vous vraiment définitivement supprimer ce magasin et tout ce qu\'il contient ?', - 'Are you sure you want to refund this transaction?' => 'Êtes-vous sûr de vouloir rembourser cette transaction ?', - 'Are you sure you want to remove this customer?' => 'Êtes-vous sûr de vouloir supprimer ce client ?', - 'Are you sure you want to save this as a new shipping rule?' => 'Êtes-vous sûr de vouloir enregistrer cette règle comme nouvelle règle de livraison ?', - 'Are you sure you want to send email: {name}?' => 'Êtes-vous sûr de vouloir envoyer l\'e-mail : {name} ?', - 'At least one site must be enabled for the product type.' => 'Au moins un site doit être activé pour le type de produit.', - 'Attempted Payments' => 'Tentatives de paiement effectuées', - 'Attention' => 'Attention', - 'Authorize Only (Manually Capture)' => 'Autoriser uniquement (collecter manuellement)', - 'Auto Set Cart Shipping Method Option' => 'Définir automatiquement l\'option du mode d\'expédition', - 'Auto Set New Cart Addresses' => 'Définir automatiquement les nouvelles adresses du panier', - 'Auto Set Payment Source' => 'Définir automatiquement la source du paiement', - 'Automatic SKU Format' => 'Formatage automatique du code article interne', - 'Available Shipping Categories' => 'Catégories de livraison disponibles', - 'Available Tax Categories' => 'Catégories de taxes disponibles', - 'Available for purchase' => 'Disponible à l’achat', - 'Available for purchase?' => 'Disponible à l’achat ?', - 'Available inventory for "{description}" has gone below zero.' => 'L\'inventaire disponible pour « {description} » est passé en dessous de zéro.', - 'Available to Product Types' => 'Disponible dans types de produits', - 'Available' => 'Disponible', - 'Available?' => 'Disponible ?', - 'Average Order Total' => 'Total de commande moyen', - 'Average' => 'Moyenne', - 'BCC’d Recipient' => 'Destinataire en copie cachée', - 'Bad Request' => 'Requête incorrecte', - 'Bad address ID.' => 'Mauvais identifiant d\'adresse.', - 'Bad order ID.' => 'Mauvais identifiant de commande.', - 'Base Price' => 'Prix de base', - 'Base Promotional Price' => 'Prix promotionnel de base', - 'Base Rate' => 'Frais de base', - 'Base' => 'Base', - 'Bcc' => 'Cci', - 'Billing Address' => 'Adresse de facturation', - 'Billing Business Name' => 'Nom commercial pour la facture', - 'Billing First Name' => 'Nom pour la facturation', - 'Billing Full Name' => 'Nom complet pour la facture', - 'Billing Last Name' => 'Nom pour la facturation', - 'Billing address required.' => 'Adresse de facturation requise.', - 'Billing detail update URL' => 'URL de mise à jour des informations de facturation', - 'Billing issues' => 'Problèmes de facturation', - 'Billing' => 'Facturation', - 'Both (Line item price + Line item shipping costs)' => 'Les deux (Prix de l’article + Frais de livraison de l’article)', - 'Business ID' => 'Numéro d\'identification de l\'entreprise', - 'Business Name' => 'Nom commercial', - 'Business Tax ID' => 'Identifiant fiscal de l\'entreprise (ex. numéro de TVA intracommunautaire)', - 'CC’d Recipient' => 'Destinataire en copie', - 'CVV' => 'CVV', - 'Can be used as an internal reference.' => 'Peut être utilisé comme référence interne.', - 'Can not complete payment for missing transaction.' => 'Impossible de finaliser le paiement pour la transaction manquante.', - 'Can not create a new order' => 'Impossible de créer une nouvelle commande', - 'Can not find an order to pay.' => 'Impossible de trouver une commande à payer.', - 'Can not find enabled email.' => 'Impossible de trouver l\'e-mail activé.', - 'Can not find order' => 'Impossible de trouver la commande', - 'Can not find order.' => 'Impossible de trouver la commande.', - 'Can not find the transaction to refund' => 'Impossible de trouver la transaction à rembourser', - 'Can not move between these inventory types.' => 'Impossible de se déplacer entre ces types d\'inventaire.', - 'Can not refund amount greater than the remaining amount' => 'Impossible de rembourser un montant supérieur au montant restant', - 'Cancel subscription' => 'Annuler l’abonnement', - 'Cancel with gateway now' => 'Annuler avec la passerelle maintenant', - 'Cancel' => 'Annuler', - 'Cancellation date' => 'Date d’annulation', - 'Cancellation' => 'Annulation', - 'Cannot switch plans for this subscription.' => 'Cet abonnement ne peut pas être basculé vers un autre abonnement.', - 'Can’t preview this email.' => 'Impossible de prévisualiser cet e-mail.', - 'Capture payment' => 'Capturer le paiement', - 'Capture' => 'Collecter', - 'Card Holder' => 'Titulaire de la carte', - 'Card Number' => 'Numéro de carte', - 'Card' => 'Carte', - 'Cart Recovery Link' => 'Lien de récupération du panier', - 'Cart forgotten.' => 'Panier oublié.', - 'Cart updated.' => 'Panier mis à jour.', - 'Cart {number}' => 'Panier {number}', - 'Catalog Pricing Rule' => 'Règle de tarification du catalogue', - 'Catalog pricing rule description.' => 'Description de la règle de tarification du catalogue.', - 'Catalog pricing rule saved.' => 'Règle de tarification du catalogue enregistrée.', - 'Catalog pricing rules deleted.' => 'Règles de tarification du catalogue supprimées.', - 'Catalog pricing rules updated.' => 'Mise à jour des règles de tarification du catalogue.', - 'Categories Relationship Type' => 'Type de relation des catégories', - 'Categories' => 'Catégories', - 'Category Rate Overrides' => 'Dépassements des taux de catégorie', - 'Centimeters (cm)' => 'Centimètres (cm)', - 'Changing this value may affect your ability to refund existing transactions.' => 'La modification de cette valeur peut affecter votre capacité à rembourser les transactions existantes.', - 'Choose a color to represent the order’s status' => 'Choisissez une couleur pour représenter le statut de la commande', - 'Choose a new customer' => 'Choisissez un nouveau client', - 'Choose adjustment values to include when calculating the product revenue total.' => 'Choisissez les valeurs d\'ajustement à inclure lors du calcul du total des revenus du produit.', - 'Choose the currency’s ISO code.' => 'Sélectionner le code ISO de la devise.', - 'Choose the destination inventory location for the existing on hand stock.' => 'Choisissez l\'emplacement de l\'inventaire de destination pour le stock existant.', - 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Choisir les sites pour lesquels ce type de produit sera disponible et configurer les paramètres spécifiques aux sites.', - 'City' => 'Ville', - 'Clear counter' => 'Effacer le compteur', - 'Clear notices' => 'Effacer les avis', - 'Close' => 'Fermer', - 'Code' => 'Code', - 'Collated PDF' => 'PDF unique', - 'Color' => 'Couleur', - 'Commerce Products' => 'Produits de Commerce', - 'Commerce Settings' => 'Paramètres de Craft Commerce', - 'Commerce Variants' => 'Variantes Commerce', - 'Commerce email “{email}” could not be sent for order “{order}”.' => 'L’e-mail de Commerce « {email} » n’a pas pu être envoyé pour la commande « {order} ».', - 'Commerce order exports' => 'Exportations de commandes Commerce', - 'Commerce' => 'Commerce', - 'Committed' => 'Validé', - 'Completed Email' => 'E-mail terminé', - 'Completed' => 'Terminé', - 'Completing order failed.' => 'Échec de l\'exécution de la commande.', - 'Condition' => 'Condition', - 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Les conditions sont comparées à une commande avant d\'examiner les règles. Cette fonction est utile si vous souhaitez vérifier la disponibilité d\'une méthode à un stade précoce ou s\'il existe des conditions communes à toutes les règles relatives à cette méthode.', - 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Les conditions sont comparées à la commande du client avant d\'examiner les règles. Cette fonction est utile si vous souhaitez vérifier la disponibilité d\'une méthode à un stade précoce ou s\'il existe des conditions communes à toutes les règles relatives à cette méthode.', - 'Conditions' => 'Conditions', - 'Contains Purchasables' => 'Contient des articles disponibles à l\'achat', - 'Control Panel Settings' => 'Paramètres du panneau de contrôle', - 'Control panel' => 'Panneau de contrôle', - 'Conversion Rate' => 'Taux de conversion', - 'Converted Price' => 'Prix converti', - 'Copied!' => 'Copié !', - 'Copy the URL' => 'Copier l\'URL', - 'Copy to {location}' => 'Copier vers {location}', - 'Copy' => 'Copier', - 'Costs' => 'Coûts', - 'Could not archive gateway.' => 'Impossible d’archiver le portail.', - 'Could not cancel “{reference}”.' => 'Échec de l\'annulation de « {reference} ».', - 'Could not create the payment source.' => 'Impossible de créer la source de paiement.', - 'Could not delete shipping rule' => 'Impossible de supprimer la règle de livraison', - 'Could not delete shipping zone' => 'Impossible de supprimer la zone de livraison', - 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Impossible de supprimer {count, number} {count, plural,one{catégorie} other{catégories}} de livraison.', - 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Impossible de supprimer {count, number} {count, plural,one{mode} other{modes}} et règles de livraison.', - 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Impossible de supprimer {count, number} {count, plural,one{catégorie} other{catégories}} de taxes.', - 'Could not find the email or template.' => 'E-mail ou modèle introuvable.', - 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Impossible de marquer la commande {number} comme finalisée. L’enregistrement de la commande a échoué au cours de la finalisation avec des erreurs : {order}', - 'Could not reactivate “{reference}”.' => 'Échec de la réactivation de « {reference} ».', - 'Could not send email' => 'Impossible d\'envoyer l\'e-mail', - 'Could not switch “{reference}” to “{plan}”.' => 'Impossible de passe « {reference} » à « {plan} ».', - 'Could not update orders address.' => 'Impossible de mettre à jour l\'adresse des commandes.', - 'Couldn’t archive Line Item Status.' => 'Impossible d\'archiver le statut de l\'article.', - 'Couldn’t archive Order Status.' => 'Impossible d\'archiver le statut de commande.', - 'Couldn’t capture transaction.' => 'Impossible de collecter la transaction.', - 'Couldn’t capture transaction: {message}' => 'Impossible de collecter la transaction : {message}', - 'Couldn’t delete email.' => 'Impossible de supprimer l\'e-mail.', - 'Couldn’t delete the payment source.' => 'Impossible de supprimer la source de paiement.', - 'Couldn’t get order.' => 'Impossible d\'obtenir la commande.', - 'Couldn’t recalculate order.' => 'Impossible de recalculer la commande.', - 'Couldn’t refund transaction.' => 'Impossible de rembourser la transaction.', - 'Couldn’t refund transaction: {message}' => 'Impossible de rembourser la transaction : {message}', - 'Couldn’t reorder Line Item Statuses.' => 'Nous n\'avons pas pu réorganiser le statut des articles.', - 'Couldn’t reorder Order Statuses.' => 'Nous n\'avons pas pu réorganiser le statut des commandes.', - 'Couldn’t reorder PDFs.' => 'Impossible de réorganiser les PDF.', - 'Couldn’t reorder discounts.' => 'Nous n\'avons pas pu réorganiser les réductions.', - 'Couldn’t reorder gateways.' => 'Impossible de réorganiser les portails.', - 'Couldn’t reorder plans.' => 'Impossible de réorganiser les abonnements.', - 'Couldn’t reorder rules.' => 'Impossible de réorganiser les règles.', - 'Couldn’t reorder sale.' => 'Impossible de réorganiser la promotion.', - 'Couldn’t reorder sales.' => 'Impossible de réorganiser les ventes.', - 'Couldn’t reorder statuses.' => 'Impossible de réorganiser les statuts.', - 'Couldn’t reorder stores.' => 'Impossible de commander à nouveau dans les magasins.', - 'Couldn’t save PDF.' => 'Impossible d’enregistrer le PDF.', - 'Couldn’t save catalog pricing rule.' => 'Impossible d\'enregistrer la règle de tarification du catalogue.', - 'Couldn’t save currency.' => 'La devise n\'a pas pu être enregistrée.', - 'Couldn’t save discount.' => 'Impossible d\'enregistrer la remise.', - 'Couldn’t save email.' => 'Impossible d\'enregistrer l\'e-mail.', - 'Couldn’t save gateway.' => 'Impossible d’enregistrer le portail.', - 'Couldn’t save inventory location.' => 'Impossible d\'enregistrer l\'emplacement de l\'inventaire.', - 'Couldn’t save line item status.' => 'Impossible d\'enregistrer le statut de l\'article.', - 'Couldn’t save order fields.' => 'Impossible d’enregistrer les champs de commande.', - 'Couldn’t save order status.' => 'Impossible d\'enregistrer le statut de commande.', - 'Couldn’t save order.' => 'Impossible d\'enregistrer la commande.', - 'Couldn’t save product type.' => 'Impossible d\'enregistrer le type de produit.', - 'Couldn’t save sale.' => 'Impossible d\'enregistrer la promotion.', - 'Couldn’t save settings.' => 'Impossible d\'enregistrer les paramètres.', - 'Couldn’t save shipping category.' => 'Nous n\'avons pas pu enregistrer cette catégorie de livraison.', - 'Couldn’t save shipping method.' => 'Impossible d\'enregistrer le mode de livraison.', - 'Couldn’t save shipping rule.' => 'Impossible d\'enregistrer la règle de livraison.', - 'Couldn’t save shipping zone.' => 'Impossible d\'enregistrer la zone de livraison.', - 'Couldn’t save store.' => 'Impossible d\'enregistrer la boutique.', - 'Couldn’t save subscription fields.' => 'Impossible d’enregistrer les champs d\'abonnement.', - 'Couldn’t save subscription plan.' => 'Impossible d’enregistrer l’abonnement.', - 'Couldn’t save subscription.' => 'Impossible d’enregistrer l’abonnement.', - 'Couldn’t save tax category.' => 'Impossible d\'enregistrer la catégorie de taxe.', - 'Couldn’t save tax rate.' => 'Impossible d\'enregistrer le taux de taxe.', - 'Couldn’t save tax zone.' => 'Impossible d\'enregistrer la zone de taxe.', - 'Couldn’t save transfer fields.' => 'Impossible d’enregistrer les champs de transfert.', - 'Couldn’t update catalog pricing rule statuses.' => 'Impossible de mettre à jour le statut des règles de tarification du catalogue.', - 'Couldn’t update status.' => 'Impossible de mettre à jour l\'état.', - 'Couldn’t updated sales status.' => 'Impossible de mettre à jour l\'état des ventes.', - 'Country Code of Origin' => 'Code du pays d\'origine', - 'Country List' => 'Liste des pays', - 'Country not allowed.' => 'Pays non autorisé.', - 'Country' => 'Pays', - 'Coupon Code' => 'Code promotionnel', - 'Coupon can not apply discount to this order due to address mismatch.' => 'Le coupon ne peut pas être utilisé pour appliquer une réduction à cette commande, car l\'adresse ne correspond pas.', - 'Coupon can not apply discount to this order due to customer mismatch.' => 'Le coupon ne peut pas être utilisé pour appliquer une réduction à cette commande, car le client ne correspond pas.', - 'Coupon can not apply discount to this order.' => 'Le coupon ne peut pas être utilisé pour appliquer une réduction à cette commande.', - 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Le code promotionnel « {code} » est déjà utilisé par la réduction « {name} ».', - 'Coupon codes cannot be blank.' => 'Les codes de coupons ne peuvent pas être vides.', - 'Coupon codes must be unique.' => 'Les codes de coupons doivent être uniques.', - 'Coupon format is required and must contain at least one `#`.' => 'Le format du coupon est requis et doit contenir au moins un « # ».', - 'Coupon not valid.' => 'Coupon invalide.', - 'Coupon removed: {explanation}' => 'Coupon supprimé : {explanation}', - 'Coupons' => 'Coupons', - 'Craft Commerce - Administration' => 'Craft Commerce - Administration', - 'Craft Commerce - Inventory' => 'Craft Commerce - Inventaire', - 'Craft Commerce - Orders' => 'Craft Commerce - Commandes', - 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Type de produit - {name}', - 'Craft Commerce - Subscriptions' => 'Craft Commerce - Abonnements', - 'Create a Discount' => 'Créer une remise', - 'Create a Subscription Plan' => 'Créer un abonnement', - 'Create a new PDF' => 'Créer un nouveau PDF', - 'Create a new catalog pricing rule' => 'Créer une nouvelle règle de tarification du catalogue', - 'Create a new currency' => 'Créer une nouvelle devise', - 'Create a new email' => 'Créer un nouvel e-mail', - 'Create a new gateway' => 'Créer un nouveau portail', - 'Create a new line item status' => 'Créer un nouveau statut d\'article', - 'Create a new order status' => 'Créer un nouveau statut de commande', - 'Create a new product type' => 'Créer un nouveau type de produit', - 'Create a new sale' => 'Créer une nouvelle promotion', - 'Create a new shipping category' => 'Créer une nouvelle catégorie de livraison', - 'Create a new shipping method' => 'Créer un nouveau mode de livraison', - 'Create a new shipping rule' => 'Créer une nouvelle règle de livraison', - 'Create a new tax category' => 'Créer une nouvelle catégorie de taxe', - 'Create a new tax rate' => 'Créer un nouveau taux de taxe', - 'Create a product type' => 'Créer un type de produit', - 'Create a shipping zone' => 'Créer une zone de livraison', - 'Create a tax zone' => 'Créer une nouvelle zone de taxe', - 'Create catalog pricing rules' => 'Créer des règles de tarification du catalogue', - 'Create customer: “{email}”' => 'Créer le client : « {email} »', - 'Create discounts' => 'Créer des remises', - 'Create discount…' => 'Créer une remise…', - 'Create rules that allow this discount to match the order.' => 'Créez des règles qui permettent à cette remise de correspondre à la commande.', - 'Create rules that allow this discount to match the order’s billing address.' => 'Créez des règles qui permettent à cette remise de correspondre à l\'adresse de facturation de la commande.', - 'Create rules that allow this discount to match the order’s customer.' => 'Créez des règles qui permettent à cette remise de correspondre au client de la commande.', - 'Create rules that allow this discount to match the order’s shipping address.' => 'Créez des règles qui permettent à cette remise de correspondre à l\'adresse d\'expédition de la commande.', - 'Create rules that allow this gateway to match the billing address.' => 'Créez des règles qui permettent à ce portail de faire correspondre l\'adresse de facturation.', - 'Create rules that allow this gateway to match the order.' => 'Créez des règles qui permettent à ce portail de correspondre à la commande.', - 'Create rules that allow this gateway to match the shipping address.' => 'Créez des règles qui permettent à ce portail de faire correspondre l\'adresse de livraison.', - 'Create sales' => 'Créer des promotions', - 'Create sale…' => 'Créer une promotion…', - 'Created' => 'Créé', - 'Credit Card Payment Type' => 'Type de paiement par carte de crédit', - 'Currency Code' => 'Code monétaire', - 'Currency saved.' => 'Devise enregistrée.', - 'Currency' => 'Devise', - 'Current' => 'Actuel', - 'Custom 1' => 'Personnalisé 1', - 'Custom 2' => 'Personnalisé 2', - 'Custom 3' => 'Personnalisé 3', - 'Custom 4' => 'Personnalisé 4', - 'Custom' => 'Personnalisé', - 'Customer Enabled?' => 'Activé pour le client ?', - 'Customer ID is required.' => 'Un ID client est obligatoire.', - 'Customer Note' => 'Note client ', - 'Customer Notices' => 'Avis aux clients', - 'Customer data' => 'Données de clients', - 'Customer' => 'Client', - 'Damaged' => 'Endommagé', - 'Data shown might be outdated.' => 'Les données présentées peuvent être obsolètes.', - 'Date Authorized' => 'Date d\'autorisation', - 'Date Created' => 'Date de création', - 'Date First Paid' => 'Date du premier paiement', - 'Date Ordered' => 'Date de la commande', - 'Date Paid' => 'Date du paiement', - 'Date Updated' => 'Date de mise à jour', - 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Date à partir de laquelle la règle de tarification du catalogue sera active. Laissez l\'espace vide si vous voulez que la date de début ne soit pas définie', - 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Date à partir de laquelle la remise sera active. Laisser vide si vous voulez que la date de départ ne soit pas définie.', - 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Date à partir de laquelle la promotion sera active. Laisser vide si vous voulez que la date de départ ne soit pas définie.', - 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Date à laquelle la règle de tarification du catalogue sera terminée. Laissez l\'espace vide si vous voulez que la date de fin ne soit pas définie', - 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Date à laquelle la remise ne sera plus effective. Laisser vide si vous voulez que la date de fin ne soit pas définie', - 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Date à laquelle la promotion sera terminée. Laisser vide si vous voulez que la date de fin ne soit pas définie.', - 'Date' => 'Date', - 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Par défaut - Permet au prix d\'être négatif si les remises sont supérieures à la valeur de la commande.', - 'Default Category' => 'Catégorie par défaut', - 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Vue par défaut du panneau de contrôle de Commerce. Si l\'utilisateur n\'a pas la permission, il se rabattra sur un emplacement auquel il peut accéder.', - 'Default Order PDF' => 'PDF de commande par défaut', - 'Default Per Item Rate' => 'Frais par article par défaut', - 'Default Percentage Rate' => 'Frais en pourcentage par défaut', - 'Default Status?' => 'Statut par défaut ?', - 'Default View' => 'Vue par défaut', - 'Default Weight Rate' => 'Frais par poids par défaut', - 'Default Zone' => 'Zone par défaut ', - 'Default status?' => 'Statut par défaut ?', - 'Default to this tax zone when no billing address is set' => 'Définir cette zone fiscale comme zone par défaut en l’absence d’adresse de facturation', - 'Default to this tax zone when no shipping address is set' => 'Utiliser par défaut cette zone de taxe lorsqu\'aucune adresse de livraison n\'est définie', - 'Default variant updated.' => 'Variante par défaut mise à jour.', - 'Default' => 'Par défaut', - 'Default?' => 'Par défaut ?', - 'Delete catalog pricing rules' => 'Supprimer les règles de tarification du catalogue', - 'Delete discounts' => 'Supprimer les remises', - 'Delete orders' => 'Supprimer les commandes', - 'Delete sales' => 'Supprimer les promotions', - 'Delete' => 'Supprimer', - 'Deleting the {location} location.' => 'Suppression de l\'emplacement {location}.', - 'Describe this rule.' => 'Décrire cette règle.', - 'Describe this shipping zone.' => 'Décrire cette zone de livraison.', - 'Describe this tax zone.' => 'Décrire cette zone de taxe.', - 'Description' => 'Description', - 'Destination Inventory Location' => 'Emplacement de l\'inventaire de destination', - 'Destination' => 'Destination', - 'Details' => 'Détails', - 'Dimension Unit' => 'Unité de mesure', - 'Dimensions' => 'Dimensions', - 'Disabled' => 'Désactivé', - 'Disallow' => 'Refuser', - 'Discount all line items' => 'Faire une réduction sur tous les articles', - 'Discount description.' => 'Description de la remise', - 'Discount is not allowed for the order' => 'La réduction n\'est pas autorisée pour cette commande', - 'Discount is out of date.' => 'La remise est échue.', - 'Discount saved.' => 'Remise enregistrée.', - 'Discount the matching items only' => 'Ne faire de remise que sur les articles correspondants', - 'Discount use has reached its limit.' => 'L’utilisation de la remise a atteint son plafond.', - 'Discount' => 'Remise', - 'Discounted Item Subtotal' => 'Sous-total des articles à prix réduit', - 'Discounted Items' => 'Articles en réduction', - 'Discounts deleted.' => 'Rabais supprimés.', - 'Discounts reordered.' => 'Remises réordonnées.', - 'Discounts updated.' => 'Remises mises à jour.', - 'Discounts' => 'Remises', - 'Disqualify with valid business tax ID?' => 'Disqualifier avec un numéro fiscal d\'entreprise valide ?', - 'Do not apply subsequent matching sales beyond applying this sale.' => 'Ne pas appliquer d’autres ventes correspondantes après l’application de cette vente.', - 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Ne pas appliquer ce taux si l\'adresse de la commande comporte l\'un des numéros de taxe professionnelle valides sélectionnés.', - 'Do not attach a PDF to this email' => 'Ne pas attacher de PDF à cet e-mail', - 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Ne pas recalculer la commande (Numéro : {orderNumber}) si des erreurs sont présentes.', - 'Donation can not be zero.' => 'Un don ne peut pas être nul.', - 'Donation needs to be an amount.' => 'Le don doit être un montant.', - 'Donation settings saved.' => 'Paramètres de don enregistrés.', - 'Donation' => 'Don', - 'Donations' => 'Dons', - 'Done' => 'Terminé', - 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Ne pas appliquer de réductions supplémentaires à cette commande si cette réduction est elle-même appliquée', - 'Download PDF' => 'Télécharger le PDF', - 'Download PDF…' => 'Télécharger le PDF…', - 'Download Type' => 'Type de téléchargement', - 'Download' => 'Télécharger', - 'Draft' => 'Brouillon', - 'Dummy gateway payment failed.' => 'Le paiement de la passerelle factice a échoué.', - 'Duplicate options exist' => 'Il y a des options en double', - 'Duration' => 'Durée', - 'EU VAT ID' => 'N° TVA DE L\'UE', - 'Edit address' => 'Éditer l\'adresse', - 'Edit adjustments' => 'Modifier les ajustements', - 'Edit catalog pricing rules' => 'Modifier les règles de tarification du catalogue', - 'Edit discounts' => 'Modifier les remises', - 'Edit options' => 'Modifier les options', - 'Edit orders' => 'Modifier les commandes', - 'Edit sales' => 'Modifier les promotions', - 'Edit' => 'Modifier', - 'Effect' => 'Effet', - 'Either (Default) - The relationship field is on the purchasable or the category' => 'Soit (par défaut) - Le champ de relation est dans le champ achetable ou la catégorie', - 'Either way' => 'Dans tous les cas', - 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Erreur de génération de PDF d\'e-mail pour l\'e-mail « {email} ». Commande : « {order} ». Erreur de modèle PDF : « {message} » {file} : {line}', - 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'Il n’existe pas de template PDF d’e-mail à l’emplacement « {templatePath} » pour l’e-mail « {email} ». Commande : « {order} ».', - 'Email Subject' => 'Objet de l\'email', - 'Email error. No email address found for order. Order: “{order}”' => 'Erreur d\'e-mail. Aucune adresse e-mail n\'a été trouvée pour cette commande. Commande : « {order} »', - 'Email is not enabled.' => 'L\'e-mail n\'est pas activé.', - 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Comme il n’existe pas de modèle d’e-mail en texte brut à l’emplacement « {templatePath} », le chemin « {templateParsedPath} » a donc été généré pour l’e-mail « {email} ». Commande : « {order} ».', - 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail en texte brut « {email} ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail pour l\'e-mail « {email} » dans « Chemin du modèle ». Commande : « {order} ». Erreur de modèles : « {message} » {file} : {line}', - 'Email required to make payments on a completed order.' => 'E-mail requis pour effectuer des paiements sur une commande finalisée.', - 'Email saved.' => 'E-mail enregistré.', - 'Email sent' => 'E-mail envoyé', - 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Comme il n’existe pas de modèle d’e-mail à l’emplacement « {templatePath} », le chemin « {templateParsedPath} » a donc été généré pour l’e-mail « {email} ». Commande : « {order} ».', - 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail personnalisé « {email} » dans « À : » « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail pour l\'e-mail {email} dans « Cci : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail pour l\'e-mail {email} dans « CC : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail pour l\'e-mail {email} dans « Répondre à : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail pour l\'e-mail {email} dans « Objet : ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail « {email} ». Commande : « {order} ». Erreur de modèle : « {message} » {file} : {line}', - 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erreur d\'analyse du modèle d\'e-mail pour l\'e-mail « {email} » dans « Chemin du modèle ». Commande : « {order} ». Erreur de modèles : « {message} » {file} : {line}', - 'Email unavailable.' => 'E-mail indisponible.', - 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'L\'e-mail « {email} » n\'a pu être envoyé pour la commande « {order} ». Erreur : {error} {file} : {line}', - 'Email “{email}” for order {order} was cancelled.' => 'L\'e-mail « {email} » pour la commande « {order} » a été annulé.', - 'Email' => 'E-mail', - 'Emails' => 'E-mails', - 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Activer si ce taux doit être intégré au prix de l\'objet imposable au lieu d\'ajouter un coût à la commande.', - 'Enable structure for products of this type' => 'Activer la structure pour les produits de ce type', - 'Enable this discount' => 'Activer cette remise', - 'Enable this rule' => 'Activer cette règle', - 'Enable this sale' => 'Activer cette promotion', - 'Enable this shipping method on the front end' => 'Activer ce mode de livraison sur le site web', - 'Enable this shipping rule' => 'Activer cette règle de livraison', - 'Enable this tax rate' => 'Activer ce taux de taxe', - 'Enabled for customers to select during checkout?' => 'Activé pour une sélection par les clients lors du paiement ?', - 'Enabled for customers to select?' => 'Possibilité de sélection par les clients ?', - 'Enabled' => 'Activé', - 'Enabled?' => 'Actif ?', - 'End Date' => 'Date de fin', - 'Enter SKU' => 'Entrer le code article interne', - 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Saisir un nom convivial pour ce taux d\'imposition, qui sera utilisé dans le panneau de configuration.', - 'Enter a percentage like {ex1} or {ex2}.' => 'Entrer un pourcentage comme {ex1} ou {ex2}.', - 'Enter coupon code' => 'Entrer le code du coupon', - 'Enter reference' => 'Entrer la référence', - 'Error refunding transaction: {transactionHash}' => 'Erreur lors du remboursement de la transaction : {transactionHash}', - 'Every new store must be assigned to at least one site.' => 'Chaque nouveau point de vente doit être affecté à au moins un site.', - 'Everywhere' => 'Partout', - 'Example' => 'Exemple', - 'Exclude this discount for products that are already on promotion' => 'Exclure cette remise pour les produits déjà en promotion', - 'Expired Link' => 'Lien expiré', - 'Expired' => 'A expiré', - 'Expiry Date' => 'Date d\'échéance', - 'Expiry date' => 'Date d’expiration', - 'Expiry' => 'Expiration', - 'Failed to receive transfer: {error}' => 'Échec de la réception du transfert : {error}', - 'Failed to send email. Please try again.' => 'Échec de l\'envoi de l\'e-mail. Veuillez réessayer.', - 'Failed to start' => 'Échec du démarrage', - 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Échec de la mise à jour {num, plural, =1{du statut de la commande} other{des statuts des commandes}}.', - 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Échec de la mise à jour du statut de {num, plural, =1{la commande} other{des commandes}}.', - 'Feet (ft)' => 'Pieds (ft)', - 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Les conditions de filtrage qui décrivent pour quelles commandes cette règle est applicable. Entrez 0 pour ignorer une condition.', - 'First Name' => 'Prénom', - 'Flat Amount Off Order' => 'Montant fixe de remise sur la commande', - 'Flat Order Discount Amount Off' => 'Montant de la remise forfaitaire sur la commande', - 'Free Order Payment Strategy' => 'Stratégie de paiement des commandes gratuites', - 'Free Shipping' => 'Livraison gratuite', - 'Free orders are processed by the payment gateway' => 'Les commandes gratuites sont traitées par le portail de paiement', - 'Free orders complete immediately' => 'Les commandes gratuites s\'exécutent immédiatement', - 'Free shipping can only be for whole order or matching items, not both.' => 'La livraison gratuite ne peut concerner que l\'ensemble de la commande ou les articles correspondants, pas les deux.', - 'From Name' => 'Nom de l\'expéditeur', - 'Fulfill' => 'Réaliser', - 'Fulfilled' => 'Réalisé', - 'Fulfillment' => 'Réalisation', - 'Full Name' => 'Nom complet', - 'Gateway Code' => 'Code portail', - 'Gateway Message' => 'Message du portail', - 'Gateway Reference' => 'Référence de portail', - 'Gateway Response' => 'Réponse du portail', - 'Gateway doesn’t support authorize' => 'La passerelle de paiement ne supporte pas l\'autorisation', - 'Gateway doesn’t support partial refunds.' => 'Le portail ne prend pas en charge les remboursements partiels.', - 'Gateway doesn’t support purchase' => 'La passerelle de paiement ne supporte pas l\'achat', - 'Gateway doesn’t support refunds.' => 'Le portail ne prend pas en charge les remboursements.', - 'Gateway saved.' => 'Portail enregistré.', - 'Gateway' => 'Passerelle de paiement', - 'Gateways reordered.' => 'Portails réordonnés.', - 'Gateways' => 'Portails', - 'General Settings' => 'Paramètres généraux', - 'General' => 'Général', - 'Generate' => 'Générer', - 'Generated Coupon Format' => 'Format des coupons générés', - 'Grams (g)' => 'Grammes (g)', - 'Groups for which this sale will be applicable to.' => 'Groupes auxquels cette promotion s’appliquera.', - 'HTML Email Template Path' => 'Chemin du modèle d\'email HTML', - 'Handle' => 'Traiter', - 'Harmonized System Code' => 'Code du système harmonisé', - 'Has Admin Notices' => 'Contient des avis de l\'administrateur', - 'Has Emails?' => 'A des emails ?', - 'Has Free Shipping' => 'Possède la livraison gratuite', - 'Has Orders' => 'Comprend des commandes', - 'Has Purchasable' => 'Comprend des articles achetables', - 'Has Variants?' => 'Possède des variantes ?', - 'Height ({unit})' => 'Hauteur ({unit})', - 'Height' => 'Hauteur', - 'Hide snapshot' => 'Masquer l\'instantané', - 'History' => 'Historique', - 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Combien de temps (en secondes) un lien de téléchargement PDF doit rester valide avant d\'expirer. La valeur par défaut est 86 400 (24 heures).', - 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Le nombre de fois qu\'une adresse email donnée peut utiliser cette remise. Cela s\'applique à toutes les commandes précédentes, passées comme invité ou comme utilisateur. Mettez zéro (0) pour une utilisation illimitée par les invités ou les utilisateurs enregistrés.', - 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Combien de fois un utilisateur est autorisé à utiliser cette remise. Si cette valeur est différente de zéro, la remise ne sera disponible que pour les utilisateurs connectés.', - 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Le nombre maximal d\'utilisations de cette remise par des invités ou des utilisateurs connectés. Mettez cette valeur à zéro pour permettre une utilisation illimitée.', - 'How products should be labeled within the control panel.' => 'Comment les produits doivent être étiquetés dans le panneau de contrôle.', - 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'La relation entre les biens à acheter et les catégories, qui détermine les articles correspondants. Voir [Terminologie relations]({link}).', - 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'La façon dont ce produit sera décrit sur le poste d\'une commande. Vous pouvez inclure des tags qui décrivent des propriétés comme {ex1} ou {ex2}', - 'How this shipping method will be referred to in templates and forms.' => 'La façon dont vous allez faire référence à ce mode de livraison dans les modèles et formulaires.', - 'How variants should be labeled within the control panel.' => 'Comment les variantes doivent être étiquetées dans le panneau de contrôle.', - 'How you’ll refer to this PDF in the templates.' => 'La façon dont vous allez faire référence à ce PDF dans les modèles.', - 'How you’ll refer to this product type in the templates.' => 'La façon dont vous allez faire référence à ce type de produit dans les modèles.', - 'How you’ll refer to this shipping category in the templates.' => 'La façon dont vous vous référerez à cette catégorie de livraison dans les modèles.', - 'How you’ll refer to this status in the templates.' => 'La façon dont vous allez faire référence à ce statut dans les modèles.', - 'How you’ll refer to this subscription plan in the templates.' => 'Indique comment vous allez faire référence à cet abonnement dans les templates.', - 'How you’ll refer to this tax category in the templates.' => 'La façon dont vous allez faire référence à cette catégorie de taxe dans les modèles.', - 'ID' => 'Identifiant', - 'IP Address' => 'Adresse IP', - 'If disabled, this PDF will not be available or sent with emails.' => 'S\'il est désactivé, ce PDF ne sera pas disponible ou envoyé avec des e-mails.', - 'If disabled, this email will not send.' => 'S’il est désactivé, cet email ne sera pas envoyé.', - 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Si cette option est activée et que ce tarif ne correspond pas à la commande, le taux sera supprimé du prix de l\'article dans le panier.', - 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Si configuré sur Autoriser Uniquement, vous devrez collecter les règlements manuellement avant que les fonds ne soient transférés sur votre compte. La passerelle de paiement doit supporter l\'option sélectionnée.', - 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Si vous choisissez le pourcentage « de réduction sur l\'article en promotion », cela inclura le « montant par article » ainsi que toute autre promotion appliquée avant celle-ci.', - 'Ignore Promotions?' => 'Ignorer les promotions ?', - 'Ignore previous matching sales if this sale matches.' => 'Ignorer les ventes correspondantes précédentes si cette vente correspond.', - 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignorer les prix promotionnels lorsque cette remise est appliquée aux articles correspondants', - 'Inactive Carts' => 'Paniers inactifs', - 'Inches (in)' => 'Pouces (in)', - 'Include built-in line item tax.' => 'Inclure la taxe intégrée sur le type d\'article.', - 'Include in price?' => 'Inclure dans le prix ?', - 'Include line item discounts.' => 'Inclure les rabais par type d\'article.', - 'Include line item shipping costs.' => 'Inclure les coûts d\'envoi par type d\'article.', - 'Include separate line item tax.' => 'Inclure une ligne distincte pour la taxe par type d\'article.', - 'Included in price?' => 'Inclus dans le prix ?', - 'Included' => 'Inclus', - 'Incoming transfer from Transfer ID: ' => 'Transfert entrant à partir de l\'ID de transfert : ', - 'Incoming' => 'Entrant', - 'Info' => 'Info', - 'Information linked?' => 'Des informations sont-elles associées ?', - 'Information' => 'Informations', - 'Invalid JSON' => 'JSON invalide', - 'Invalid Order ID' => 'ID de commande invalide', - 'Invalid VAT ID.' => 'ID TVA invalide.', - 'Invalid condition syntax' => 'Syntaxe conditionnelle invalide', - 'Invalid email.' => 'E-mail invalide.', - 'Invalid formula syntax' => 'Syntaxe de formule invalide', - 'Invalid gateway: {value}' => 'Portail non valide : {value}', - 'Invalid inventory movements.' => 'Mouvements d\'inventaire non valides.', - 'Invalid order condition syntax.' => 'Syntaxe conditionnelle de commande invalide.', - 'Invalid payment or order. Please review.' => 'Paiement ou commande non valide. Veuillez vérifier votre saisie.', - 'Invalid payment source ID: {value}' => 'ID de la source de paiement non valide : {value}', - 'Invalid store.' => 'Magasin non valide.', - 'Invalid user.' => 'Utilisateur non valide.', - 'Inventory Item' => 'Article d\'inventaire', - 'Inventory Location' => 'Emplacement de l\'inventaire', - 'Inventory Locations' => 'Emplacements de l\'inventaire', - 'Inventory Tracked' => 'Inventaire suivi', - 'Inventory Transfers' => 'Transferts d\'inventaire', - 'Inventory could not be set.' => 'L\'inventaire n\'a pas pu être établi.', - 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'L\'emplacement de l\'inventaire a un stock validé, la commande ou les commandes doivent d\'abord être exécutées.', - 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'L\'emplacement de l\'inventaire a un stock entrant, le ou les transferts doivent d\'abord être effectués.', - 'Inventory location is already deactivated.' => 'L\'emplacement de l\'inventaire est déjà désactivé.', - 'Inventory location saved.' => 'Emplacement de l\'inventaire enregistré.', - 'Inventory locations not saved.' => 'Emplacements de l\'inventaire non enregistrés.', - 'Inventory movement could not be saved.' => 'Le mouvement de l\'inventaire n\'a pas pu être enregistré.', - 'Inventory movement saved.' => 'Mouvement d\'inventaire enregistré.', - 'Inventory updated.' => 'Inventaire mis à jour.', - 'Inventory was not updated.' => 'L\'inventaire n\'a pas été mis à jour.', - 'Inventory' => 'Inventaire', - 'Invoice amount' => 'Montant de la facture', - 'Invoice date' => 'Date de la facture', - 'Is Promotable' => 'Peut être promu', - 'Is Promotional Price?' => 'S\'agit-il d\'un prix promotionnel ?', - 'Is Shippable' => 'Peut être expédié', - 'Is Taxable' => 'Peut être taxé', - 'Item Rates' => 'Taux de l\'article', - 'Item Subtotal' => 'Sous-total de l\'article', - 'Item Total' => 'Total de l\'article', - 'Item' => 'Article', - 'Items' => 'Articles', - 'Kilograms (kg)' => 'Kilogrammes (kg)', - 'Label' => 'Étiquette', - 'Landscape' => 'Paysage', - 'Language' => 'Langue', - 'Last Name' => 'Nom de famille', - 'Last Updated' => 'Dernière mise à jour', - 'Leave a category rate override blank to use the rate from above.' => 'Laissez un taux de catégorie vide pour utiliser le taux ci-dessus.', - 'Leave blank for unlimited uses.' => 'Laisser vide pour une utilisation illimitée.', - 'Leave blank if products don’t have URLs' => 'Laissez la zone vide si les produits n’ont pas d’URL', - 'Leave gateway subscription as-is' => 'Laisser l\'abonnement au portail tel quel', - 'Length ({unit})' => 'Longueur ({unit})', - 'Length' => 'Longueur', - 'Let each product choose which sites it should be saved to' => 'Laissez chaque produit choisir dans quels sites ils devront être enregistrées', - 'Limit which orders this discount applies to based on its line items.' => 'Limiter les commandes auxquelles cette remise s\'applique en fonction de leurs articles.', - 'Limit which purchasables this sale applies to.' => 'Limiter les produits achetables auxquels cette promotion s\'applique.', - 'Limit' => 'Limite', - 'Line Item Statuses' => 'Statuts de l\'article', - 'Line Item' => 'Article de ligne', - 'Line Items' => 'Articles de ligne', - 'Line item price (minus discounts)' => 'Prix de l\'article (moins les remises)', - 'Line item shipping cost' => 'Frais de livraison de l’article', - 'Line item statuses reordered.' => 'Articles réordonnés.', - 'Link Duration' => 'Durée du lien', - 'Link Sent' => 'Lien envoyé', - 'Link to a product' => 'Lien vers un produit', - 'Link to a variant' => 'Lien vers une variante', - 'Link' => 'Lien', - 'Live' => 'Live', - 'Location' => 'Emplacement', - 'Locations that should be available for previewing products in this product type.' => 'Emplacements à proposer pour la prévisualisation des produits de ce type de produit.', - 'MM' => 'MM', - 'Make a payment' => 'Effectuer un paiement', - 'Make this the primary store' => 'En faire le magasin principal', - 'Manage Inventory' => 'Gérer l\'inventaire', - 'Manage donation settings' => 'Gérer les paramètres de don', - 'Manage general store settings' => 'Gérer les paramètres généraux du magasin', - 'Manage inventory locations' => 'Gérer les emplacements de l\'inventaire', - 'Manage inventory stock levels' => 'Gérer les niveaux de l\'inventaire', - 'Manage inventory transfers' => 'Gérer les transferts d\'inventaire', - 'Manage orders' => 'Gérez les commandes', - 'Manage payment currencies' => 'Gérer les devises de paiement', - 'Manage promotions' => 'Gérez les promotions', - 'Manage shipping' => 'Gérer l\'expédition', - 'Manage store settings' => 'Gérer les paramètres du magasin', - 'Manage subscription plans' => 'Gérer les abonnements', - 'Manage subscription' => 'Gérer l’abonnement', - 'Manage subscriptions' => 'Gérer les abonnements', - 'Manage taxes' => 'Gérer les taxes', - 'Manage' => 'Gérer', - 'Mark as Pending' => 'Marquer comme en attente', - 'Mark as completed' => 'Marquer comme terminé', - 'Match Billing Address' => 'Faire correspondre l\'adresse de facturation', - 'Match Customer' => 'Faire correspondre le client', - 'Match Order' => 'Faire correspondre la commande', - 'Match Orders' => 'Faire correspondre les commandes', - 'Match Product' => 'Faire correspondre le produit', - 'Match Purchasable' => 'Faire correspondre les produits achetables', - 'Match Shipping Address' => 'Faire correspondre l\'adresse de livraison', - 'Match Variant' => 'Faire correspondre la variante', - 'Matching Items' => 'Articles correspondants', - 'Max Qty' => 'Qté max.', - 'Max Uses' => 'Nombre max d\'utilisations', - 'Max Variants' => 'Variantes max.', - 'Max quantity must greater than min.' => 'La quantité maximale doit être supérieure à min.', - 'Maximum Purchase Quantity' => 'Quantité maximum d\'achat', - 'Maximum Total Shipping Cost' => 'Coût total maximum de la livraison', - 'Maximum allowed quantity' => 'Quantité maximale autorisée', - 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Nombre maximum d\'articles correspondants qui peuvent être commandés pour que cette réduction soit appliquée. La valeur zéro indique que ce critère n\'est pas pris en compte.', - 'Maximum order quantity for this item is {num}.' => 'La quantité maximale de commande pour cet article est {num}.', - 'Message' => 'Message', - 'Meters (m)' => 'Mètres (m)', - 'Millimeters (mm)' => 'Millimètres (mm)', - 'Min Qty' => 'Qté min.', - 'Min quantity must be less than max.' => 'La quantité minimale doit être inférieure à max.', - 'Minimum Purchase Quantity' => 'Quantité minimum d\'achat', - 'Minimum Total Price Strategy' => 'Stratégie de prix minimum', - 'Minimum Total Shipping Cost' => 'Coût total minimum de la livraison', - 'Minimum allowed quantity' => 'Quantité minimale autorisée', - 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Nombre minimum d\'articles correspondants qui doivent être commandés pour que cette réduction puisse s\'appliquer.', - 'Minimum order quantity for this item is {num}.' => 'La quantité minimale de commande pour cet article est {num}.', - 'Missing Gateway' => 'Portail manquant', - 'Missing a default inventory location.' => 'Il manque un emplacement d\'inventaire par défaut.', - 'Move Inventory' => 'Déplacer l\'inventaire', - 'Move To' => 'Déplacer vers', - 'Move {qty} from {fromType} to {toType}' => 'Déplacer {qty} de {fromType} vers {toType}', - 'Move' => 'Déplacer', - 'Movement from deactivated inventory location' => 'Mouvement à partir d\'un emplacement d\'inventaire désactivé', - 'Movement' => 'Mouvement', - 'Must have at least one variant.' => 'Doit avoir au moins une variante.', - 'Name Field' => 'Champ de nom', - 'Name' => 'Nom', - 'New Customer' => 'Nouveau client', - 'New Customers' => 'Nouveaux clients', - 'New Order' => 'Nouvelle commande', - 'New PDF' => 'Nouveau PDF', - 'New address' => 'Nouvelle adresse', - 'New catalog pricing rule' => 'Nouvelle règle de tarification du catalogue', - 'New currency' => 'Nouvelle devise', - 'New discount' => 'Nouvelle remise', - 'New email' => 'Nouvel email', - 'New gateway' => 'Nouveau portail', - 'New line item status' => 'Nouveau statut d\'article', - 'New line items get this status by default when the order is completed' => 'Les nouveaux articles obtiennent ce statut par défaut lorsque la commande est terminée', - 'New location' => 'Nouvel emplacement', - 'New order status' => 'Nouveau statut de commande', - 'New orders get this status by default' => 'Statut par défaut des nouvelles commandes', - 'New product type' => 'Nouveau type de produit', - 'New product' => 'Nouveau produit', - 'New product, choose a type' => 'Nouveau produit, choisissez un type', - 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Les nouveaux produits passent par défaut dans la première catégorie fiscale disponible. Si aucune n\'est disponible, cette catégorie sera utilisée.', - 'New sale' => 'Nouvelle promotion', - 'New shipping category' => 'Nouvelle catégorie de livraison', - 'New shipping method' => 'Nouveau mode de livraison', - 'New shipping rule' => 'Nouvelle règle de livraison', - 'New shipping zone' => 'Nouvelle zone de livraison', - 'New subscription plan' => 'Nouvel abonnement', - 'New tax category' => 'Nouvelle catégorie de taxe', - 'New tax rate' => 'Nouveau taux de taxe', - 'New tax zone' => 'Nouvelle zone de taxe', - 'New transfer' => 'Nouveau transfert', - 'New {productType} product' => 'Nouveau produit {productType}', - 'New' => 'Nouveau', - 'Next payment' => 'Prochain paiement', - 'No Address' => 'Aucune adresse', - 'No PDFs exist yet.' => 'Il n\'existe pas encore de PDF.', - 'No access given to any specific store management features.' => 'Aucun accès n\'est donné à des fonctions spécifiques de gestion de magasin.', - 'No additional payment currencies exist yet.' => 'Il n\'existe pas encore de devise pour les paiements supplémentaires.', - 'No address' => 'Aucune adresse', - 'No billing address' => 'Aucune adresse de facturation', - 'No catalog pricing rule exists with the ID “{id}”' => 'Aucune règle de tarification du catalogue n\'existe avec l\'ID « {id} »', - 'No catalog pricing rules exist yet.' => 'Il n\'existe pas encore de règles de tarification du catalogue.', - 'No currency exists with the ID “{id}”' => 'Il n\'existe aucune devise avec l\'identifiant « {id} »', - 'No customer email address exists on this cart.' => 'Aucune adresse e-mail de client n\'existe dans ce panier.', - 'No description' => 'Aucune description', - 'No discount exists with the ID “{id}”' => 'Il n\'existe aucune remise avec l\'identifiant « {id} »', - 'No discounts exist yet.' => 'Il n\'existe pas encore de remise.', - 'No donation amount supplied.' => 'Aucun montant de don fourni.', - 'No emails exist yet.' => 'Il n\'existe pas encore d\'email.', - 'No inventory changes made.' => 'Aucune modification d\'inventaire n\'a été effectuée.', - 'No inventory found.' => 'Aucun inventaire n\'a été trouvé.', - 'No inventory movements made.' => 'Aucun mouvement d\'inventaire n\'a été effectué.', - 'No inventory transactions for this location.' => 'Aucune transaction d\'inventaire pour cet emplacement.', - 'No new customer selected.' => 'Aucun nouveau client sélectionné.', - 'No order history exists with the ID “{id}”' => 'Il n\'existe aucun historique de commande avec l\'identifiant « {id} »', - 'No order status history items will exist until the cart becomes an order.' => 'Il n’y aura aucun élément d’historique du statut de la commande jusqu’à ce que le panier devienne une commande.', - 'No payment source exists with the ID “{id}”' => 'Aucune source de paiement n’existe avec l’ID « {id} »', - 'No private Note.' => 'Aucune note privée.', - 'No product available.' => 'Aucun produit disponible.', - 'No product types exist yet.' => 'Il n\'existe pas encore de type de produit.', - 'No purchasable available.' => 'Aucun achetable disponible.', - 'No sale exists with the ID “{id}”' => 'Il n\'existe aucune promotion avec l\'identifiant « {id} »', - 'No sales exist yet.' => 'Il n\'existe pas encore de promotion.', - 'No shipping address' => 'Aucune adresse de livraison', - 'No shipping category exists with the ID “{id}”' => 'Il n\'existe pas de catégorie de livraison possédant l\'identifiant « {id} »', - 'No shipping method exists with the ID “{id}”' => 'Il n\'existe pas de mode de livraison avec l\'identifiant « {id} »', - 'No shipping rule exists with the ID “{id}”' => 'Il n\'existe pas de règle de livraison avec l\'identifiant « {id} »', - 'No shipping rules exist yet.' => 'Il n\'y a pas encore de règle de livraison.', - 'No shipping zone exists with the ID “{id}”' => 'Aucune zone de livraison ne porte l\'identifiant « {id} »', - 'No stats available.' => 'Aucune statistique disponible.', - 'No subscription plan exists with the ID “{id}”' => 'Aucun abonnement n’existe avec l’ID « {id} »', - 'No subscription plans exist yet.' => 'Il n’existe pas encore d’abonnement.', - 'No tax category exists with the ID “{id}”' => 'Il n\'existe aucune catégorie de taxe avec l\'identifiant « {id} »', - 'No tax rate exists with the ID “{id}”' => 'Il n\'existe pas de taux de taxe avec l\'identifiant « {id} »', - 'No tax zone exists with the ID “{id}”' => 'Il n\'existe pas de zone de taxe avec l\'identifiant « {id} »', - 'No transactions exist.' => 'Aucune transaction existante.', - 'No user authenticated.' => 'Aucun utilisateur authentifié.', - 'No' => 'Non', - 'None on hand' => 'Aucun à disposition', - 'None' => 'Aucun', - 'Not a valid address type' => 'N\'est pas un type d\'adresse valide', - 'Not a valid credit card number.' => 'Numéro de carte non valide.', - 'Not all SKUs are unique.' => 'Tous les numéros d’articles (SKU) ne sont pas uniques.', - 'Note' => 'Remarque', - 'Notes' => 'Notes', - 'Number of Coupons' => 'Nombre de coupons', - 'Number' => 'Numéro', - 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Parmi les sites activés ci-dessus, sur quels sites les produits de ce type de produit doivent-ils être enregistrées ?', - 'On Hand' => 'Disponible', - 'Only allow this gateway to be used for zero value orders?' => 'Autoriser l’utilisation de ce portail uniquement pour les commandes de valeur nulle ?', - 'Only match certain purchasables…' => 'Ne faire correspondre que certains produits achetables…', - 'Only match purchasables related to…' => 'Ne faire correspondre que les produits achetables liés à…', - 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Seules les commandes ayant les statuts suivants seront incluses. Laissez ce champ vide pour inclure tous les statuts.', - 'Only save product to the site they were created in' => 'N’enregistrer les produits que sur le site où ils ont été créés', - 'Options' => 'Options', - 'Order Condition Formula' => 'Formule de la condition de commande', - 'Order Description Format' => 'Format de la description de la commande', - 'Order Details' => 'Détails de la commande', - 'Order Fields' => 'Champs de commande', - 'Order PDF Download Link' => 'Lien de téléchargement du PDF de la commande', - 'Order PDF Filename Format' => 'Format de nom de fichier PDF de commandes', - 'Order Reference Number Format' => 'Format du numéro de référence de la commande', - 'Order Settings' => 'Paramètres de commande', - 'Order Site' => 'Site de commande', - 'Order Status description.' => 'Description du statut de la commande.', - 'Order Status' => 'Statut de commande', - 'Order Statuses' => 'Statuts de la commande', - 'Order can not be empty.' => 'La commande ne peut pas être vide.', - 'Order count' => 'Nombre de commandes', - 'Order customer data removed.' => 'Données des clients supprimées des commandes.', - 'Order deleted.' => 'Commande supprimée.', - 'Order fields saved.' => 'Champs commandes enregistrés.', - 'Order not found.' => 'Commande non trouvée.', - 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Le solde de paiement de la commande est {outstandingBalanceAsCurrency}. Il s\'agit du montant maximum facturé.', - 'Order recalculated.' => 'Commande recalculée.', - 'Order status saved.' => 'Statut de commande enregistré.', - 'Order statuses reordered.' => 'Statuts des commandes réordonnés.', - 'Order total shipping cost' => 'Coût total de livraison', - 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Prix total taxable de la commande (sous-total des articles individuels + total des remises + total des frais de livraison)', - 'Order' => 'Commande', - 'Orders (Legacy)' => 'Commandes (Legacy)', - 'Orders deleted.' => 'Commandes supprimées.', - 'Orders not restored.' => 'Commandes non restaurées.', - 'Orders restored.' => 'Commandes restaurées.', - 'Orders' => 'Commandes', - 'Organization Name' => 'Nom de l\'organisation', - 'Organization Tax ID' => 'ID fiscal de l\'organisation', - 'Origin and destination cannot be the same.' => 'L\'origine et la destination ne peuvent pas être les mêmes.', - 'Origin' => 'Origine', - 'Original Price' => 'Prix d’origine', - 'Original price' => 'Prix d\'origine', - 'Original promotional price' => 'Prix promotionnel d\'origine', - 'Other Languages' => 'Autres langues', - 'Other countries' => 'Autres pays', - 'Outgoing transfer from Transfer ID: ' => 'Transfert sortant à partir de l\'ID de transfert : ', - 'Overpaid' => 'Surpayé', - 'Overrides previous?' => 'Remplace la précédente ?', - 'PDF Attachment' => 'Pièce jointe PDF', - 'PDF Template Path' => 'Chemin du template PDF', - 'PDF saved.' => 'PDF enregistré.', - 'PDF' => 'PDF', - 'PDFs & Emails' => 'PDF et e-mails', - 'PDFs' => 'PDF', - 'Paid Amount' => 'Montant payé', - 'Paid Status' => 'Statut « Payé »', - 'Paid' => 'Payé', - 'Paper Orientation' => 'Orientation du papier', - 'Paper Size' => 'Format du papier', - 'Partial payment not allowed.' => 'Paiement partiel non autorisé.', - 'Partial' => 'Partiellement', - 'Past year' => 'L\'année dernière', - 'Past {num} days' => '{num} derniers jours', - 'Pay {amount} of {currency} on the order.' => 'Payer {amount} {currency} à la commande.', - 'Pay' => 'Payer', - 'Payment Amount' => 'Montant du paiement', - 'Payment Currencies' => 'Devises du paiement', - 'Payment Gateway' => 'Portail de paiement', - 'Payment Method' => 'Moyen de paiement', - 'Payment error: {message}' => 'Erreur de paiement : {message}', - 'Payment method issue' => 'Problème de moyen de paiement', - 'Payment source created.' => 'Source de paiement créée.', - 'Payment source deleted.' => 'Source de paiement supprimée.', - 'Payments' => 'Paiements', - 'Pending' => 'En cours', - 'Per Email Address Discount Limit' => 'Limite de réductions par adresse e-mail', - 'Per Item Amount Off' => 'Montant de réduction par article', - 'Per Item Discount' => 'Remise par article', - 'Per Item Percentage Off' => 'Montant de réduction par article', - 'Per Item Rate' => 'Frais par article', - 'Per User Discount Limit' => 'Limite de réductions par utilisateur', - 'Percentage Rate' => 'Frais en pourcentage', - 'Phone (Alt)' => 'Téléphone (Alt)', - 'Phone' => 'Téléphone', - 'Pick a plan' => 'Choisir un abonnement', - 'Plain Text Email Template Path' => 'Chemin du modèle d\'e-mail texte brut', - 'Plan' => 'Abonnement', - 'Plans reordered.' => 'Plans réordonnés.', - 'Portrait' => 'Portrait', - 'Post Date' => 'Date de publication', - 'Postal Code Formula' => 'Formule du code postal', - 'Pounds (lb)' => 'Livres (lb)', - 'Preview' => 'Aperçu', - 'Previous Status' => 'Statut précédent', - 'Price' => 'Prix', - 'Prices' => 'Prix', - 'Pricing Rules' => 'Règles de tarification', - 'Pricing jobs are currently running.' => 'Des travaux de tarification sont actuellement en cours.', - 'Pricing' => 'Tarification', - 'Primary Billing Address' => 'Adresse de facturation principale', - 'Primary Shipping Address' => 'Adresse de livraison principale', - 'Primary payment source updated.' => 'Source de paiement principale mise à jour.', - 'Primary' => 'Principal', - 'Private Note' => 'Note privée', - 'Product Fields' => 'Champs produit', - 'Product ID is required.' => 'Un ID produit est requis.', - 'Product Template' => 'Modèle du produit', - 'Product Title Format' => 'Format du titre de produit', - 'Product Type' => 'Type de produit', - 'Product Types' => 'Types de produits', - 'Product URI Format' => 'Format d\'URI produit', - 'Product Variant' => 'Variante du produit', - 'Product Variants' => 'Variantes du produit', - 'Product type saved.' => 'Type de produit enregistré.', - 'Product type settings' => 'Paramètres du type de produit', - 'Product' => 'Produit', - 'Products and Variants deleted.' => 'Produits et variantes supprimées.', - 'Products not restored.' => 'Produits non restaurés.', - 'Products restored.' => 'Produits restaurés.', - 'Products' => 'Produits', - 'Promotable' => 'Pouvant être promu', - 'Promotable?' => 'Peut-être mis en promotion ?', - 'Promotional Amount' => 'Montant promotionnel', - 'Promotional Price' => 'Prix promotionnel', - 'Purchasable Categories' => 'Catégories de produits achetables', - 'Purchasable ID and Sale ID are required.' => 'Un ID achetable et de vente est requis.', - 'Purchasable ID is required.' => 'Un ID achetable est requis.', - 'Purchasable Type' => 'Type de produit achetable', - 'Purchasable' => 'Produit achetable', - 'Purchase (Authorize and Capture Immediately)' => 'Acheter (autoriser et collecter immédiatement)', - 'Purchase Total' => 'Total des achats', - 'Qty' => 'Qté', - 'Quality Control' => 'Contrôle de la qualité', - 'Quantity' => 'Quantité', - 'Rate' => 'Taux', - 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Réaffecter {numOrders, plural, =1{la commande} other{les commandes}}', - 'Recalculate order' => 'Recalculer la commande', - 'Receive Inventory' => 'Recevoir l\'inventaire', - 'Receive Transfer' => 'Recevoir le transfert', - 'Receive' => 'Recevoir', - 'Received' => 'Reçu', - 'Recent Orders' => 'Commandes récentes', - 'Recipient' => 'Destinataire', - 'Recover Cart' => 'Récupérer le panier', - 'Reduce price' => 'Diminuer le prix', - 'Reduce the price by a fixed amount' => 'Diminuer le prix d’un montant fixe', - 'Reduce the price by a percentage of the original price' => 'Diminuer le prix d’un certain pourcentage du prix d’origine', - 'Reference' => 'Référence', - 'Refresh payment history' => 'Actualiser l\'historique de paiement', - 'Refund note' => 'Note de remboursement', - 'Refund payment' => 'Rembourser le paiement', - 'Refund' => 'Rembourser', - 'Reject' => 'Rejeter', - 'Rejected' => 'Rejeté', - 'Relationship Type' => 'Type de relation', - 'Removable included tax rates are only allowed for the default tax zone.' => 'Les taux de taxes inclus supprimables sont uniquement autorisés pour la zone de taxes par défaut.', - 'Remove address' => 'Supprimer l\'adresse', - 'Remove all shipping costs from the order' => 'Retirer tous les coûts de livraison de la commande', - 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Retirer l\'association au client et à l\'adresse e-mail {numOrders, plural, =1{de la commande} other{des commandes}}. Vous pouvez également sélectionner d\'autres données de clients à supprimer ci-dessous', - 'Remove customer data' => 'Supprimer les données de clients', - 'Remove from price?' => 'Retirer du prix ?', - 'Remove shipping costs for matching items only' => 'Retirer les coûts d\'expédition pour les articles correspondants seulement', - 'Remove the included tax when a valid organization tax ID is present?' => 'Supprimer la taxe incluse lorsqu\'un identifiant fiscal d\'organisation valide est présent ?', - 'Remove' => 'Supprimer', - 'Removed' => 'Supprimé', - 'Repeat Customers' => 'Clients réguliers', - 'Reply To' => 'Répondre à', - 'Require Billing Address At Checkout' => 'Exiger l\'adresse de facturation lors du passage de la commande', - 'Require Coupon Code' => 'Exiger un code promotionnel', - 'Require Shipping Address At Checkout' => 'Exiger l\'adresse d\'expédition lors du passage de la commande', - 'Require Shipping Method Selection At Checkout' => 'Exiger le choix du mode d\'expédition au moment du passage de la commande', - 'Require' => 'Demander', - 'Reserved' => 'Réservé', - 'Reset usage' => 'Réinitialiser l\'utilisation', - 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Restreignez la remise aux commandes pour lesquelles le client a acheté une valeur totale minimale d\'articles correspondants.', - 'Revenue Options' => 'Options de revenus', - 'Revenue' => 'Recettes', - 'Rule' => 'Règle', - 'Rules reordered.' => 'Règles réordonnées.', - 'SKU' => 'Code article interne (SKU)', - 'Safety' => 'Sécurité', - 'Sale Price' => 'Prix de vente', - 'Sale description.' => 'Description de la promotion.', - 'Sale reordered.' => 'Promotion réordonnée.', - 'Sale saved.' => 'Promotion enregistrée.', - 'Sale' => 'Promotion/solde', - 'Sales deleted.' => 'Ventes supprimées.', - 'Sales updated.' => 'Ventes mises à jour.', - 'Sales' => 'Promos & Soldes', - 'Save and continue editing' => 'Enregistrer et continuer l\'édition', - 'Save and return to all orders' => 'Enregistrer et revenir à toutes les commandes', - 'Save and set rules' => 'Enregistrer et définir les règles', - 'Save as a new rule' => 'Enregistrer en tant que nouvelle règle', - 'Save product to all sites enabled for this product type' => 'Enregistrer les produits sur tous les sites activés pour ce type de produit', - 'Save product to other sites in the same site group' => 'Enregistrer le produit sur les autres sites du même groupe de sites', - 'Save product to other sites with the same language' => 'Enregistrer le produit sur les autres sites ayant la même langue', - 'Save' => 'Enregistrer', - 'Search customer…' => 'Rechercher un client…', - 'Search inventory' => 'Rechercher dans l\'inventaire', - 'Search or enter customer email…' => 'Rechercher ou saisir un e-mail client…', - 'Search…' => 'Rechercher…', - 'See Orders' => 'Voir les commandes', - 'Select a gateway' => 'Sélectionner un portail', - 'Select a tax category.' => 'Choisir une catégorie de taxe.', - 'Select a tax zone. If empty, this rate will match anywhere.' => 'Sélectionner une zone fiscale. Si aucune zone n\'est sélectionnée, ce taux s\'appliquera partout.', - 'Select address' => 'Sélectionner l\'adresse', - 'Select an item' => 'Sélectionner un article', - 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Sélectionnez la façon dont la règle de tarification du catalogue sera appliquée aux produits achetables.', - 'Select how the sale will be applied to the purchasable(s).' => 'Indiquez comment la promotion sera appliquée aux articles en vente.', - 'Select product type' => 'Sélectionner le type de produit', - 'Select the emails that will be sent when transitioning to this status.' => 'Sélectionnez les emails qui seront envoyés lors du basculement sur ce statut.', - 'Select what this rate should be applied to.' => 'Sélectionner ce à quoi ce taux doit être appliqué.', - 'Send Email' => 'Envoyer l\'e-mail', - 'Send to custom recipient' => 'Envoyer à un destinataire spécifique', - 'Send to the customer' => 'Envoyer au client', - 'Set Quantity' => 'Définir la quantité', - 'Set default category' => 'Définir la catégorie par défaut', - 'Set default variant' => 'Définir la variante par défaut', - 'Set or Adjust' => 'Définir ou ajuster', - 'Set price' => 'Définir le prix', - 'Set status' => 'Définir le statut', - 'Set the price to a flat amount' => 'Définir le prix comme un montant fixe', - 'Set the price to a percentage of the original price' => 'Définir le prix comme un pourcentage du prix original', - 'Set the sale price to a flat amount' => 'Définir le prix soldé comme un montant fixe', - 'Set the sale price to a percentage of the original price' => 'Définir le prix soldé comme un pourcentage du prix original', - 'Set to' => 'Définir sur', - 'Settings saved.' => 'Paramètres enregistrés.', - 'Settings' => 'Paramètres', - 'Share cart…' => 'Partager le panier…', - 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Expédition - Le coût minimum est le coût d\'expédition, si le prix de la commande est inférieur au coût d\'expédition.', - 'Shipping Address Zone' => 'Zone d\'adresse de livraison', - 'Shipping Address' => 'Adresse de livraison', - 'Shipping Business Name' => 'Nom commercial pour la livraison', - 'Shipping Categories' => 'Catégories de livraison', - 'Shipping Category Conditions' => 'Conditions pour les différentes catégories de livraison', - 'Shipping Category' => 'Catégorie de livraison', - 'Shipping First Name' => 'Nom pour la livraison', - 'Shipping Full Name' => 'Nom complet pour la livraison', - 'Shipping Last Name' => 'Nom pour la livraison', - 'Shipping Method' => 'Mode de livraison', - 'Shipping Methods' => 'Modes de livraison', - 'Shipping Rule' => 'Règle de livraison', - 'Shipping Zones' => 'Zones de livraison', - 'Shipping address required.' => 'Adresse de livraison requise.', - 'Shipping categories deleted.' => 'Catégories de livraison supprimées.', - 'Shipping category saved.' => 'Catégorie de livraison enregistrée.', - 'Shipping category updated.' => 'Catégorie de livraison mise à jour.', - 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Les frais d\'expédition sont ajoutés à l\'ensemble de la commande avant l\'application des taux de pourcentage, d\'article et de poids. Réglez à zéro pour désactiver ce taux. Toute la règle, y compris ce taux de base, ne correspondra pas et ne s\'appliquera pas si le panier ne contient que des articles non expédiables comme des produits numériques.', - 'Shipping method saved.' => 'Mode de livraison enregistré.', - 'Shipping methods and rules deleted.' => 'Modes et règles de livraison supprimés.', - 'Shipping methods updated.' => 'Modes de livraison mis à jour.', - 'Shipping rule saved.' => 'Règle de livraison enregistrée.', - 'Shipping zone saved.' => 'Zone de livraison enregistrée.', - 'Shipping' => 'Livraison', - 'Short Number' => 'Numéro court', - 'Show Chart?' => 'Afficher le graphique ?', - 'Show Order Count?' => 'Afficher le nombre de commandes ?', - 'Show all prices' => 'Afficher tous les prix', - 'Show archived gateways' => 'Afficher les portails archivés', - 'Show order count line on chart.' => 'Afficher la ligne de décompte des commandes sur le graphique.', - 'Show related sales' => 'Afficher les ventes connexes', - 'Show rule details' => 'Afficher les détails de la règle', - 'Show the Dimensions and Weight fields for products of this type' => 'Afficher les champs Dimensions et Poids pour les produits de ce type', - 'Show the Title field for products' => 'Afficher le champ Titre pour les produits', - 'Show the Title field for variants' => 'Afficher le champ Titre pour les variantes', - 'Signed In' => 'Connexion effectuée', - 'Site Languages' => 'Langues du site', - 'Site store mapping saved.' => 'Mappage du site du magasin enregistré.', - 'Sites' => 'Sites', - 'Slug' => 'Identificateur', - 'Snapshot' => 'Instantané', - 'Snapshots' => 'Instantanés', - 'Some orders restored.' => 'Certaines commandes ont été restaurées.', - 'Some products restored.' => 'Certains produits ont été restaurés.', - 'Some variants restored.' => 'Certaines variantes ont été restaurées.', - 'Something changed with the order before payment, please review your order and submit payment again.' => 'Quelque chose a changé dans la commande avant le paiement, merci de la vérifier puis de soumettre de nouveau votre règlement.', - 'Sorry, no matching options.' => 'Désolé, aucune option ne correspond.', - 'Source - The purchasable relationship field is on the category' => 'Source - Le champ de relation du produit achetable est sur la catégorie', - 'Source' => 'Source', - 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Précisez une condition Twig déterminant si la réduction doit s\'appliquer à une commande donnée. (La commande peut être référencée via la variable `order`.)', - 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Précisez une condition Twig déterminant si la règle d\'expédition doit s\'appliquer à une commande donnée. (La commande peut être référencée via la variable `order`.)', - 'Start Date' => 'Date de début', - 'State' => 'État/Province', - 'Status Email Address' => 'Adresse email du statut', - 'Status Emails' => 'Emails de statut', - 'Status History' => 'Historique du statut', - 'Status Updated.' => 'Statut mis à jour.', - 'Status change message' => 'Message de changement de statut', - 'Status' => 'Statut', - 'Stock' => 'Stock', - 'Stops Processing?' => 'Cela arrête-t-il le traitement ?', - 'Stops subsequent?' => 'Cela arrête-t-il les éléments suivants ?', - 'Store Location' => 'Emplacement des magasins', - 'Store Management' => 'Gestion du magasin', - 'Store Markets' => 'Marchés de la boutique', - 'Store Rule' => 'Règle du magasin', - 'Store saved.' => 'Boutique enregistrée.', - 'Store' => 'Magasin', - 'Stores & Sites' => 'Magasins et sites', - 'Stores' => 'Magasins', - 'Strategy to apply when an order is free or has a zero balance.' => 'Stratégie à appliquer lorsqu\'une commande est gratuite ou a un solde nul.', - 'Strategy to apply when calculating the minimum order price.' => 'Stratégie à employer lors du calcul du prix de la commande minimale.', - 'Subject' => 'Objet', - 'Subscribing user' => 'Utilisateur abonné', - 'Subscription Fields' => 'Champs d’abonnement', - 'Subscription Plans' => 'Abonnements', - 'Subscription Settings' => 'Paramètres d\'abonnement', - 'Subscription cancelled.' => 'Abonnement annulé.', - 'Subscription date' => 'Date d’abonnement', - 'Subscription fields saved.' => 'Champs d’abonnement enregistrés.', - 'Subscription for {user} to {plan} prevented by a plugin.' => 'La souscription de l’utilisateur {user} à l’abonnement {plan} a été empêchée par un plug-in.', - 'Subscription plan saved.' => 'Abonnement enregistré.', - 'Subscription plan' => 'Abonnement', - 'Subscription plans' => 'Abonnements', - 'Subscription reactivated.' => 'Abonnement réactivé.', - 'Subscription reference' => 'Référence de l’abonnement', - 'Subscription started.' => 'Abonnement démarré.', - 'Subscription switched.' => 'Abonnement changé.', - 'Subscription to “{plan}”' => 'Abonnement à « {plan} »', - 'Subscription' => 'Abonnement', - 'Subscriptions on hold' => 'Abonnements en attente', - 'Subscriptions' => 'Abonnements', - 'Suppress emails' => 'Réduire les e-mails', - 'Switch plan' => 'Changer d’abonnement', - 'Switch' => 'Basculer', - 'System' => 'Système', - 'Table Columns' => 'Colonnes du tableau', - 'Target - The category relationship field is on the purchasable' => 'Cible - Le champ de relation de la catégorie est sur l\'achetable', - 'Tax & Shipping' => 'Taxes et expédition', - 'Tax (inc)' => 'Taxe (inc)', - 'Tax Categories' => 'Catégories de taxe', - 'Tax Category' => 'Catégorie de taxe', - 'Tax Rates' => 'Taux de taxe', - 'Tax Zone' => 'Zone de taxe', - 'Tax Zones' => 'Zones de taxe', - 'Tax categories deleted.' => 'Catégories de taxe supprimées.', - 'Tax category saved.' => 'Catégorie de taxe enregistrée.', - 'Tax category updated.' => 'Catégorie de taxe mise à jour.', - 'Tax rate saved.' => 'Taux de taxe enregistré.', - 'Tax rates updated.' => 'Taux de taxe mis à jour.', - 'Tax zone saved.' => 'Zone de taxe enregistrée.', - 'Tax' => 'Taxe', - 'Taxable Subject' => 'Ce qui doit être taxé', - 'Template Path' => 'Chemin du modèle', - 'That handle is already in use' => 'Cet identificateur est déjà utilisé', - 'That handle is already in use.' => 'Cet identificateur est déjà utilisé.', - 'The PDF to attach to this email.' => 'Le PDF à attacher à cet e-mail.', - 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'L\'URL de la page permettant de mettre à jour les détails de facturation d\'un abonnement, ainsi que de gérer l\'authentification 3DS.', - 'The address provided is outside the store’s market.' => 'L\'adresse fournie est en dehors du marché de la boutique.', - 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'Le montant de la remise qui est appliquée à l\'ensemble de la commande. Ce montant est réparti sur les objets dans l\'ordre du prix le plus élevé au prix le plus bas, jusqu\'à épuisement de la remise.', - 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'La remise de base ne peut réduire le prix des articles du panier à zéro, elle ne peut pas rendre la commande négative.', - 'The cart recovery link is invalid. Please request a new one.' => 'Le lien de récupération du panier n\'est pas valide. Veuillez en demander un nouveau.', - 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Le taux de conversion qui sera utilisé lorsqu\'un montant sera converti dans cette devise. Par exemple, si un article coûte {amount1}, vous obtiendrez le montant de {amount2} dans l\'autre devise si le taux de conversion est de {rate}.', - 'The countries that orders are allowed to be placed from.' => 'Les pays depuis lesquels il est possible de passer une commande.', - 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'Le coupon « {code} » a dépassé la limite de {limit} utilisations.', - 'The customer for this order has been deleted.' => 'Le client pour cette commande a été supprimé.', - 'The default shipping category is automatically available to all product types.' => 'La catégorie d\'expédition par défaut est automatiquement disponible pour tous les types de produits.', - 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'La réduction « {name} » a dépassé la limite de {limit} utilisations au total.', - 'The download link has expired. Please request a new one.' => 'Le lien de téléchargement a expiré. Veuillez en demander un nouveau.', - 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'L\'adresse à partir de laquelle les emails de statut de commande sont envoyés. Laisser vide pour utiliser l\'adresse email du système définie dans les Paramètres Généraux de Craft.', - 'The entry that contains the description for this subscription’s plan.' => 'Entrée contenant la description de cet abonnement.', - 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'La remise forfaitaire appliquée à chaque article. Par ex : « 3 » pour 3€ de réduction par article.', - 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'Le format utilisé pour générer de nouveaux coupons, par exemple {example}. Tout caractère « # » sera remplacé par une lettre aléatoire.', - 'The from and to inventory locations must be different.' => 'Les emplacements de départ et de retour de l\'inventaire doivent être différents.', - 'The inventory locations this store uses.' => 'Les emplacements d\'inventaire utilisés par ce magasin.', - 'The item is not enabled for sale.' => 'Cet article n’est pas autorisé à la vente.', - 'The language the order was made in.' => 'La langue dans laquelle la commande a été passée.', - 'The language to be used when this email is rendered.' => 'La langue à utiliser lors de l\'affichage de cet e-mail.', - 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Le nombre maximum de niveaux que ce type de produit peut avoir. Laissez vide si cela n\'est pas pertinent.', - 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Le maximum que le client devra payer pour la livraison. Mettre sur zéro pour désactiver.', - 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Le minimum que le client devra payer pour la livraison. Mettre sur zéro pour désactiver.', - 'The order is not valid.' => 'La commande est invalide.', - 'The payment gateway that will be used for the subscription plan.' => 'Indique quel portail de paiement sera utilisé pour l’abonnement.', - 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'La valeur du pourcentage de réduction qui doit être appliqué à chaque article, par exemple {ex1} pour une remise de {ex2}. Les pourcentages sont arrondis à 2 décimales.', - 'The previously-selected shipping method is no longer available.' => 'La méthode d\'expédition précédemment sélectionnée n\'est plus disponible.', - 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Le prix de {description} a augmenté, il est passé de {originalSalePriceAsCurrency} à {newSalePriceAsCurrency}', - 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Le prix de {description} a baissé, il est passé de {originalSalePriceAsCurrency} à {newSalePriceAsCurrency}', - 'The primary currency cannot be changed after orders are placed.' => 'La devise principale ne peut être modifiée après la validation des commandes.', - 'The purchasable defines the relationship' => 'Le produit achetable définit la relation', - 'The purchasable is related by another element' => 'Le produit achetable est lié par un autre élément', - 'The recipient of the email. Twig code can be used here.' => 'Le destinataire de l\'e-mail. Du code Twig peut être utilisé ici.', - 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'L\'adresse e-mail de réponse. Laissez vide pour la réponse normale à l\'expéditeur de l\'e-mail. Du code Twig peut être utilisé ici.', - 'The site the order was made in.' => 'Le site sur lequel la commande a été passée.', - 'The site to be used when this email is rendered.' => 'Le site à utiliser lors de l\'affichage de cet e-mail.', - 'The subject line of the email. Twig code can be used here.' => 'La ligne d\'objet de l\'e-mail. Du code Twig peut être utilisé ici.', - 'The template that the PDF should be generated from.' => 'Le modèle à partir duquel le PDF doit être généré.', - 'The template to be used for HTML emails.' => 'Le modèle utilisé pour les emails HTML', - 'The template to be used for plain text emails. Twig code can be used here.' => 'Le modèle à utiliser pour les e-mails en texte brut. Du code Twig peut être utilisé ici.', - 'The template to use when a product’s URL is requested.' => 'Le modèle à utiliser lorsque l\'URL d\'un produit est requise.', - 'The total number of order adjustments changed.' => 'Le nombre total de modifications de commandes qui ont été modifiées.', - 'The total price of the order changed.' => 'Le montant total de la commande qui a été modifié.', - 'The total quantity of items within the order changed.' => 'Le nombre total d\'articles dans la commande qui ont été modifiés.', - 'The unique SKU of the donation purchasable.' => 'L\'unique référence du don pouvant être acheté.', - 'The unit of measurement that should be used when specifying product dimensions.' => 'L\'unité de mesure qui doit être utilisée lorsque vous indiquez les dimensions du produits.', - 'The unit of measurement that should be used when specifying product weights.' => 'L\'unité de mesure qui doit être utilisée lorsque vous indiquez le poids du produit.', - 'The webhook URL for this gateway.' => 'L\'URL du webhook pour cette passerelle.', - 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'Le nom de l\'expéditeur utilisé lors de l\'envoi d\'emails de statut de commande. Laissez vide pour utiliser le nom de l\'expéditeur défini dans les Paramètres Généraux de Craft.', - 'There are errors on the order' => 'Il y a des erreurs dans la commande', - 'There are only {num} “{description}” items left in stock.' => 'Il reste seulement {num} « {description} » articles en stock.', - 'There aren’t any product types to select yet.' => 'Il n\'y a pas encore de types de produits à sélectionner.', - 'There is no gateway or payment source available for use with this order.' => 'Aucun portail ou source de paiement disponible pour cette commande.', - 'There is no gateway selected that supports payment sources.' => 'Aucun portail prenant en charge les sources de paiement n’a été sélectionné.', - 'There is no shipping method selected for this order.' => 'Aucun mode de livraison sélectionné pour cette commande.', - 'This URL will load the cart into the user’s session, making it the active cart.' => 'Cette URL chargera le panier dans la session de l\'utilisateur, ce qui en fera le panier actif.', - 'This action is not allowed for the current user.' => 'Cette action n\'est pas autorisée pour l\'utilisateur actuel.', - 'This category will be used as the default for all purchasables in this store.' => 'Cette catégorie sera utilisée par défaut pour tous les articles pouvant être achetés dans ce magasin.', - 'This coupon is for registered users and limited to {limit} uses.' => 'Ce coupon est limité à {limit} utilisation(s) pour les utilisateurs inscrits.', - 'This coupon is limited to {limit} uses.' => 'Ce coupon est limité à {limit} utilisation(s).', - 'This coupon requires an email address.' => 'Ce rabais nécessite une adresse e-mail.', - 'This gateway does not support that functionality.' => 'Ce portail ne prend pas en charge cette fonctionnalité.', - 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Cela est outrepassé par le paramètre de configuration {setting} dans `config/{file}.php`.', - 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Il s’agit de l’adresse physique de votre magasin. Elle peut être utilisée par différents plug-ins pour déterminer des éléments tels que le mode de livraison ou les taxes applicables. Elle peut également figurer sur les reçus au format PDF.', - 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Ceci est le PDF par défaut qui sera rendu lors de la demande du PDF de commande.', - 'This is the last location for the {store} store.' => 'Il s\'agit du dernier emplacement pour le magasin {store}.', - 'This month' => 'Ce mois-ci', - 'This order has unsaved changes.' => 'Cette commande a des modifications non enregistrées.', - 'This week' => 'Cette semaine', - 'This year' => 'Cette année', - 'Times Used' => 'Nombre de fois où elle a été utilisée', - 'Title' => 'Titre', - 'To' => 'À', - 'Today' => 'Aujourd’hui', - 'Too many variants for this product.' => 'Trop de variantes pour ce produit.', - 'Top Customers by Average Order' => 'Meilleurs clients par commande moyenne', - 'Top Customers by Total Revenue' => 'Meilleurs clients par recette totale', - 'Top Customers' => 'Meilleurs clients', - 'Top Product Types by Qty Sold' => 'Meilleurs types de produits par Qté vendue', - 'Top Product Types by Revenue' => 'Meilleurs types de produits par recettes', - 'Top Product Types' => 'Meilleurs type de produit', - 'Top Products by Qty Sold' => 'Meilleurs produits par Qté vendue', - 'Top Products by Revenue' => 'Meilleurs produits par recettes', - 'Top Products' => 'Meilleurs produits', - 'Top Purchasables by Qty Sold' => 'Meilleurs achetables par Qté vendue', - 'Top Purchasables by Revenue' => 'Meilleurs achetables par recettes', - 'Top Purchasables' => 'Meilleurs achetables', - 'Total ' => 'Total ', - 'Total Discount Use Limit' => 'Limite d\'utilisation totale des remises', - 'Total Discount' => 'Remise totale', - 'Total Included Tax' => 'Total TTC', - 'Total Orders by Billing Country' => 'Total des commandes par pays de facturation', - 'Total Orders by Country' => 'Total des commandes par pays', - 'Total Orders by Shipping Country' => 'Total des commandes par pays de livraison', - 'Total Orders' => 'Total des commandes', - 'Total Paid' => 'Total payé', - 'Total Price' => 'Montant total', - 'Total Qty' => 'Qté totale', - 'Total Revenue' => 'Total des recettes', - 'Total Shipping' => 'Total des frais de port', - 'Total Tax' => 'Total des taxes', - 'Total Weight' => 'Poids total', - 'Total' => 'Total', - 'Track Inventory' => 'Suivre l\'inventaire', - 'Transaction Hash' => 'Hash de la transaction', - 'Transaction ID' => 'ID de la transaction', - 'Transaction captured successfully: {message}' => 'Transaction capturée avec succès : {message}', - 'Transaction refunded successfully: {message}' => 'Transaction remboursée avec succès : {message}', - 'Transactions' => 'Transactions', - 'Transfer Fields' => 'Champs de transfert', - 'Transfer Items' => 'Articles de transfert', - 'Transfer Settings' => 'Paramètres de transfert', - 'Transfer Status' => 'Statut du transfert', - 'Transfer fields saved.' => 'Champs de transfert enregistrés.', - 'Transfer must have at least one item.' => 'Le transfert doit comporter au moins un article.', - 'Transfer' => 'Transférer', - 'Transfers' => 'Transferts', - 'Trial days credited' => 'Jours d’essai crédités', - 'Trial expiration' => 'Expiration de l\'essai', - 'Trial expiry date' => 'Date d’expiration de la version d’essai', - 'Type not in allowed options.' => 'Type non autorisé dans les options.', - 'Type' => 'Type', - 'URI' => 'URI', - 'Unable to cancel subscription at this time.' => 'Impossible d\'annuler l’abonnement actuellement.', - 'Unable to complete order: another request is already in progress.' => 'Impossible de terminer la commande : une autre demande est déjà en cours.', - 'Unable to find variant.' => 'Impossible de trouver la variante.', - 'Unable to generate coupon codes: {message}' => 'Impossible de générer des codes de réduction : {message}', - 'Unable to make payment at this time.' => 'Impossible d’effectuer le paiement actuellement.', - 'Unable to modify subscription at this time.' => 'Impossible de modifier l’abonnement actuellement.', - 'Unable to reactivate subscription at this time.' => 'Impossible de réactiver l’abonnement actuellement.', - 'Unable to reassign orders.' => 'Impossible de réattribuer les commandes.', - 'Unable to remove order data.' => 'Impossible de supprimer les données de commande.', - 'Unable to retrieve Sale and Purchasable.' => 'Impossible de récupérer les promotions et les achetables.', - 'Unable to retrieve cart.' => 'Impossible de récupérer le panier.', - 'Unable to retrieve customer.' => 'Impossible de récupérer le client.', - 'Unable to retrieve load cart URL' => 'Impossible de récupérer et charger l\'URL du panier', - 'Unable to retrieve payment source.' => 'Impossible de récupérer la source de paiement.', - 'Unable to set default shipping category.' => 'Impossible de définir la catégorie d\'expédition par défaut.', - 'Unable to set default tax category.' => 'Impossible de définir la catégorie de taxe par défaut.', - 'Unable to set primary payment source.' => 'Impossible de définir la source de paiement principale.', - 'Unable to start the subscription. Please check your payment details.' => 'Impossible de démarrer l’abonnement. Veuillez vérifier vos informations de paiement.', - 'Unable to subscribe at this time.' => 'Impossible de souscrire actuellement.', - 'Unable to update cart.' => 'Impossible de mettre à jour le panier.', - 'Unable to validate address.' => 'Impossible de valider l’adresse.', - 'Unit Price' => 'Prix unitaire', - 'Unit price (minus discounts)' => 'Prix unitaire (moins les remises)', - 'Units' => 'Unités', - 'Unpaid' => 'Non payés', - 'Unsubscribe' => 'Se désabonner', - 'Update Address' => 'Mettre à jour lʼadresse', - 'Update Order Status' => 'Mettre à jour le statut de la commande', - 'Update Order Status…' => 'Mise à jour du statut de commande…', - 'Update order' => 'Mettre à jour la commande', - 'Update subscription' => 'Mettre à jour l\'abonnement', - 'Update' => 'Mettre à jour', - 'Updated By' => 'Mis à jour par', - 'Updated committed stock successfully.' => 'Stock validé mis à jour avec succès.', - 'Updated' => 'Mis à jour', - 'Use Billing Address For Tax' => 'Utiliser l\'adresse de facturation pour les taxes', - 'Use as the primary billing address' => 'Utiliser comme adresse de facturation principale', - 'Use as the primary shipping address' => 'Utiliser comme adresse de livraison principale', - 'Used By Tax Rates' => 'Utilisé par le taux d\'imposition', - 'Used by Tax Rates' => 'Utilisé par le taux de taxe', - 'User Groups' => 'Groupes d\'utilisateurs', - 'User not found.' => 'Utilisateur non trouvé.', - 'User' => 'Utilisateur', - 'Uses' => 'Utilisations', - 'Validate Business Tax ID as Vat ID' => 'Valider l\'ID de taxe professionnelle en tant qu\'ID de TVA', - 'Validating condition syntax' => 'Validation de la syntaxe conditionnelle', - 'Validating formula syntax' => 'Validation de la syntaxe de la formule', - 'Variant Fields' => 'Champs variante', - 'Variant Has Untracked Stock' => 'La variante a un stock non suivi', - 'Variant Price' => 'Prix de la variante', - 'Variant SKU' => 'SKU de la variante', - 'Variant Search' => 'Rechercher une variante', - 'Variant Stock' => 'Stock de la variante', - 'Variant Title Format' => 'Format du titre de variante', - 'Variant Tracks Stock' => 'La variante suit le stock', - 'Variant UI Label Format' => 'Format des étiquettes de l\'interface de variantes', - 'Variant has no product.' => 'La variante n\'a pas de produit.', - 'Variants not restored.' => 'Variantes non restaurées.', - 'Variants restored.' => 'Variantes restaurées.', - 'Variants' => 'Variantes', - 'View customer' => 'Voir le client', - 'View order' => 'Afficher la commande', - 'View product type - {productType}' => 'Voir le type de produit - {productType}', - 'View user' => 'Voir l\'utilisateur', - 'View' => 'Voir', - 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Attention, la suppression de cette devise entraînera l\'arrêt de tous les paiements et remboursements dans cette devise, êtes-vous sûr de vouloir supprimer « {name} » ?', - 'Web' => 'Web', - 'Webhook URL' => 'URL du webhook', - 'Weight ({unit})' => 'Poids ({unit})', - 'Weight Rate' => 'Frais selon le poids', - 'Weight Unit' => 'Unité de poids', - 'Weight' => 'Poids', - 'What product URIs should look like for the site.' => 'À quoi les URIs de produits devraient ressembler pour le site.', - 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'À quoi devraient ressembler les titres de produits générés automatiquement. Vous pouvez inclure des balises qui produisent des propriétés des produits, comme {ex1} ou {ex2}. Tous les champs personnalisés utilisés doivent être obligatoires.', - 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'À quoi devraient ressembler les titres de variantes générés automatiquement. Vous pouvez inclure des balises qui produisent des propriétés des variantes, comme {ex1} ou {ex2}. Tous les champs personnalisés utilisés doivent être obligatoires.', - 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Ce à quoi le nom de fichier PDF de commandes devrait ressembler (sans extension). Vous pouvez inclure des étiquettes qui représentent certaines propriétés de la commande, telles que {ex1} ou {ex2}.', - 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'À quoi devraient ressembler les codes article internes (SKU) uniques générés automatiquement, lorsqu\'un champ SKU est soumis sans valeur. Vous pouvez inclure des balises qui produisent des propriétés, comme {ex1} ou {ex2}.', - 'What this PDF will be called in the control panel.' => 'Nom de ce PDF dans le panneau de contrôle.', - 'What this catalog pricing rule will be called in the control panel.' => 'Le nom de cette règle de tarification du catalogue dans le panneau de contrôle.', - 'What this discount will be called in the control panel.' => 'Nom de cette remise dans le panneau de contrôle.', - 'What this email will be called in the control panel.' => 'Nom de cet e-mail dans le panneau de configuration.', - 'What this product type will be called in the control panel.' => 'Nom de ce type de produit dans le panneau de contrôle.', - 'What this sale will be called in the control panel.' => 'Nom de cette promotion dans le panneau de contrôle.', - 'What this shipping category will be called in the control panel.' => 'Nom de cette catégorie de livraison dans le panneau de contrôle.', - 'What this shipping rule will be called in the control panel.' => 'Nom de cette règle de livraison dans le panneau de contrôle.', - 'What this shipping zone will be called in the control panel.' => 'Nom de cette zone de livraison dans le panneau de contrôle.', - 'What this status will be called in the control panel.' => 'Nom de ce statut dans le panneau de contrôle.', - 'What this subscription plan will be called in the control panel.' => 'Nom de cet abonnement dans le panneau de configuration.', - 'What this tax category will be called in the control panel.' => 'Nom de cette catégorie de taxe dans le panneau de contrôle.', - 'What this tax zone will be called in the control panel.' => 'Nom de cette zone de taxe dans le panneau de contrôle.', - 'When this discount is applied to an order, which line items should be discounted?' => 'Lorsque cette réduction est appliquée à une commande, quels postes doivent faire l\'objet d\'une réduction ?', - 'Whether the first available shipping method option should be set automatically on carts.' => 'Indique si la première option du mode d\'expédition disponible doit être définie automatiquement dans les paniers.', - 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Indique si la source de paiement principale de l\'utilisateur doit être définie automatiquement pour les nouveaux paniers.', - 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Indique si les adresses principales de livraison et de facturation de l\'utilisateur doivent être définies automatiquement pour les nouveaux paniers.', - 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Indique si cette règle de tarification du catalogue doit pouvoir être utilisée, indépendamment d\'autres conditions.', - 'Whether this sale should be available for use, regardless of other conditions.' => 'Indique si cette vente doit être utilisable, indépendamment des autres conditions.', - 'Which data to display in the name column in the results table.' => 'Les données à afficher dans la colonne des noms dans le tableau des résultats.', - 'Which product types should this category be available to?' => 'Dans quels types de produits cette catégorie devrait-elle être offerte ?', - 'Which template should be loaded when a product’s URL is requested.' => 'Quel template devrait être chargé quand l’URL d’un produit est demandée.', - 'Width ({unit})' => 'Largeur ({unit})', - 'Width' => 'Largeur', - 'YYYY' => 'AAAA', - 'Yes' => 'Oui', - 'You are not allowed to add a line item.' => 'Vous n\'avez pas le droit d’ajouter un article.', - 'You currently have no emails configured to select for this status.' => 'Vous n\'avez aucune adresse e-mail actuellement configurée à sélectionner pour ce statut.', - 'You do not have permission to load this cart.' => 'Vous n\'avez pas l\'autorisation de charger ce panier.', - 'You must set up at least one gateway that supports subscriptions first.' => 'Vous devez d’abord définir au moins un portail qui prend en charge les abonnements.', - 'You must be logged in or provide a valid token to load this cart.' => 'Vous devez être connecté ou fournir un jeton valide pour charger ce panier.', - 'You must be signed in to create a payment source.' => 'Vous devez être connecté(e) pour créer une source de paiement.', - 'You must be signed in to set a primary payment source.' => 'Vous devez être connecté(e) pour définir une source de paiement principale.', - 'You must make a payment to complete the order.' => 'Vous devez effectuer un paiement pour terminer la commande.', - 'Your Cart Recovery Link' => 'Lien de récupération de votre panier', - 'Your Order PDF Download Link' => 'Lien de téléchargement du PDF de votre commande', - 'Your order is empty' => 'Votre commande est vide', - 'ZIP file' => 'Fichier ZIP', - 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Zéro - Le prix minimum est zéro si les remises sont supérieures à la valeur de la commande.', - 'Zip Code' => 'code postal', - 'all' => 'tous', - 'any' => 'n\'importe quel', - 'average order total' => 'total de commande moyen', - 'billing address' => 'adresse de facturation', - 'donation' => 'don', - 'donations' => 'dons', - 'info' => 'infos', - 'inventory location' => 'emplacement de l\'inventaire', - 'new customers' => 'nouveaux clients', - 'on hand' => 'disponible', - 'only' => 'seulement', - 'order' => 'commande', - 'orders' => 'commandes', - 'price' => 'prix', - 'prices' => 'prix', - 'product variant' => 'variante du produit', - 'product variants' => 'variantes du produit', - 'product' => 'produit', - 'products' => 'produits', - 'repeat customers' => 'clients réguliers', - 'shipping address' => 'adresse de livraison', - 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'Impossible de définir à la fois shippingSameAsBilling et billingSameAsShipping.', - 'subscription' => 'abonnement', - 'subscriptions' => 'abonnements', - 'to' => 'à', - 'transfer' => 'transférer', - 'transfers' => 'transferts', - '{amount} included' => '{amount} inclut', - '{count} Unfulfilled Orders' => '{count} commandes non réalisées', - '{description} is no longer available.' => '{description} n\'est plus disponible.', - '{description} only has {stock} in stock.' => '{description} n\'a que {stock} unités en stock.', - '{from} to {to}' => '{from} à {to}', - '{name} (Primary)' => '{name} (Primaire)', - '{name} (Trashed)' => '{name} (mis à la corbeille)', - '{name} catalog price' => 'prix de catalogue {name}', - '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, one {}=1{commande mise à jour} other{commandes mises à jour}}.', - '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, =1{commande est associée} other{commandes sont associées}} {numUsers, plural, =1{à l\'utilisateur} other{aux utilisateurs}}.', - '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, =1{abonnement est activé} other{abonnements sont activés}} pour {numUsers, plural, =1{l\'utilisateur} other{les utilisateurs}}.', - '{number} more…' => '{number} de plus…', - '{pct} off the discounted item price' => '{pct} sur le prix remisé de l\'article', - '{pct} off the original item price' => '{pct} sur le prix original de l\'article', - '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, =1{n\'a pas été assigné} other{n\'ont pas été assignés}} à un site.', - '{total} in total revenue' => '{total} dans les recettes totales', - '{total} orders' => '{total} commandes', - '{total} saleable across {locationCount} location(s)' => '{total} vendables parmi {locationCount} emplacement(s)', - '{uses} uses across {emails} email addresses' => '{uses} utilisations sur {emails} adresses e-mail', - '{uses} uses across {users} users' => '{uses} utilisations pour {users} utilisateurs', - '“{description}” is currently out of stock.' => '« {description} » est actuellement épuisé.', - '“{key}” has invalid JSON' => '« {key} » possède du JSON invalide', -]; diff --git a/src/translations/it/commerce.php b/src/translations/it/commerce.php deleted file mode 100644 index 3aa41ed39e..0000000000 --- a/src/translations/it/commerce.php +++ /dev/null @@ -1,1428 +0,0 @@ - '(nuovo prezzo)', - '(of original price)' => '(del prezzo originale)', - '(off original price)' => '(in meno rispetto al prezzo originale)', - 'A cart number must be specified.' => 'È necessario specificare un numero di carrello.', - 'A cart recovery link has been sent to {email}.' => 'Un link per il recupero del carrello è stato inviato all\'indirizzo {email}.', - 'A cart recovery link will be sent to {email}.' => 'Un link per il recupero del carrello sarà inviato all\'indirizzo {email}.', - 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Un numero di riferimento di facile consultazione verrà generato in base a questo formato al completamento del carrello e alla sua trasformazione in ordine. Per esempio {ex1} o
{ex2}. Il risultato di questo formato deve essere univoco.', - 'A new download link has been sent to {email}' => 'Un nuovo link di download è stato inviato a {email}', - 'A new download link will be sent to {email}' => 'Un nuovo link di download sarà inviato a {email}', - 'A valid email is required to create a customer.' => 'Per creare un cliente è necessario fornire un\'e-mail valida.', - 'Accept' => 'Accetta', - 'Accepted' => 'Accettato', - 'Actions' => 'Azioni', - 'Active Carts' => 'Carrelli attivi', - 'Active subscriptions' => 'Sottoscrizioni attive', - 'Active' => 'Attivo', - 'Add Address' => 'Aggiungi indirizzo', - 'Add a coupon' => 'Aggiungi un coupon', - 'Add a custom line item' => 'Aggiungi una voce personalizzata', - 'Add a line item' => 'Aggiungi una voce', - 'Add a product' => 'Aggiungi prodotto', - 'Add a variant' => 'Aggiungi variante', - 'Add an adjustment' => 'Aggiungi una rettifica', - 'Add an item' => 'Aggiungi un articolo', - 'Add an option' => 'Aggiungi un\'opzione', - 'Add catalog price' => 'Aggiungi prezzo in catalogo', - 'Add' => 'Aggiungi', - 'Additional Actions' => 'Azioni aggiuntive', - 'Additional recipients that should receive this email. Twig code can be used here.' => 'Destinatari aggiuntivi che dovrebbero ricevere questa e-mail. Qui è possibile utilizzare il codice Twig.', - 'Address 1' => 'Indirizzo 1', - 'Address 2' => 'Indirizzo 2', - 'Address 3' => 'Indirizzo 3', - 'Address Line 1' => 'Indirizzo (linea 1)', - 'Address Line 2' => 'Indirizzo (linea 2)', - 'Address Updated.' => 'Indirizzo aggiornato.', - 'Address copied to user.' => 'Indirizzo copiato su utente.', - 'Address not found.' => 'Indirizzo non trovato.', - 'Adjust Quantity' => 'Rettifica quantità', - 'Adjust by' => 'Regola per', - 'Adjust price when included rate is disqualified?' => 'Rettificare il prezzo quando l\'aliquota inclusa non è valida?', - 'Adjustments' => 'Rettifiche', - 'Admin Notices' => 'Avvisi dell\'amministratore', - 'Administrative Area Code of Origin' => 'Codice dell\'area amministrativa di provenienza', - 'Advanced' => 'Impostazioni avanzate', - 'All Orders' => 'Tutti gli ordini', - 'All Totals' => 'Tutti i totali', - 'All Transfers' => 'Tutti i trasferimenti', - 'All active subscriptions' => 'Tutte le sottoscrizioni attive', - 'All customers' => 'Tutti i clienti', - 'All products' => 'Tutti i prodotti', - 'All variants must have a SKU.' => 'Tutte le varianti devono avere uno SKU.', - 'All' => 'Tutti', - 'Allow Checkout Without Payment' => 'Consenti il checkout senza pagamento', - 'Allow Empty Cart On Checkout' => 'Consenti il carrello vuoto al checkout', - 'Allow Partial Payment On Checkout' => 'Consenti il pagamento parziale al checkout', - 'Allow out of stock purchases' => 'Consenti acquisti fuori stock', - 'Allow' => 'Consenti', - 'Allowed Qty' => 'Qtà consentita', - 'Alternative Phone' => 'N. telefono alternativo', - 'Amount' => 'Importo', - 'An ID must be provided' => 'È necessario fornire un documento d\'identità', - 'An error occurred while generating this PDF.' => 'Si è verificato un errore durante la generazione di questo PDF.', - 'Any' => 'Qualsiasi', - 'Anywhere' => 'Dovunque', - 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Sei sicuro di voler archiviare il piano di sottoscrizione “{name}”? Questa operazione NON annullerà le sottoscrizioni esistenti.', - 'Are you sure you want to capture this transaction?' => 'Sei sicuro di voler acquisire questa transazione?', - 'Are you sure you want to complete this order?' => 'Sei sicuro di voler completare questo ordine?', - 'Are you sure you want to delete the selected orders?' => 'Sei sicuro di voler eliminare gli ordini selezionati?', - 'Are you sure you want to delete the selected product and its variants?' => 'Sei sicuro di voler eliminare il prodotto selezionato e le relative varianti?', - 'Are you sure you want to delete this shipping rule?' => 'Sei sicuro di voler eliminare questa regola di spedizione?', - 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Sei sicuro di voler eliminare “{name}” e tutti i relativi prodotti? Assicurati di avere un backup del tuo database prima di eseguire questa azione distruttiva.', - 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Sei sicuro di voler eliminare "{name}"? Tutte le voci con questo stato verranno impostate su nessuno stato.', - 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Contrassegnare questo trasferimento come in sospeso? Verrà visualizzato come in arrivo nella destinazione.', - 'Are you sure you want to overwrite the billing address?' => 'Sei sicuro di voler sovrascrivere l\'indirizzo di fatturazione?', - 'Are you sure you want to overwrite the shipping address?' => 'Sei sicuro di voler sovrascrivere l\'indirizzo di spedizione?', - 'Are you sure you want to permanently delete this store and everything in it?' => 'Sei sicuro di voler eliminare definitivamente questo store e tutto ciò che contiene?', - 'Are you sure you want to refund this transaction?' => 'Sei sicuro di voler rimborsare questa transazione?', - 'Are you sure you want to remove this customer?' => 'Sei sicuro di voler rimuovere questo cliente?', - 'Are you sure you want to save this as a new shipping rule?' => 'Sei sicuro di voler salvare questa immissione come nuova regola di spedizione?', - 'Are you sure you want to send email: {name}?' => 'Sei sicuro di voler inviare l\'email: {name}?', - 'At least one site must be enabled for the product type.' => 'Almeno un sito deve essere abilitato per il tipo di prodotto.', - 'Attempted Payments' => 'Tentativi di pagamento', - 'Attention' => 'Attenzione', - 'Authorize Only (Manually Capture)' => 'Autorizza solo (acquisizione manuale)', - 'Auto Set Cart Shipping Method Option' => 'Impostazione automatica del metodo di spedizione del carrello', - 'Auto Set New Cart Addresses' => 'Impostazione automatica dei nuovi indirizzi del carrello', - 'Auto Set Payment Source' => 'Impostazione automatica della fonte di pagamento', - 'Automatic SKU Format' => 'Formato SKU automatico', - 'Available Shipping Categories' => 'Categorie di spedizione disponibili', - 'Available Tax Categories' => 'Categorie fiscali disponibili', - 'Available for purchase' => 'Disponibile all’acquisto', - 'Available for purchase?' => 'Disponibile all’acquisto?', - 'Available inventory for "{description}" has gone below zero.' => 'Le scorte disponibili per “{description}” sono pari a zero.', - 'Available to Product Types' => 'Disponibile per tipi di prodotto', - 'Available' => 'Disponibile', - 'Available?' => 'Disponibile?', - 'Average Order Total' => 'Totale medio degli ordini', - 'Average' => 'Media', - 'BCC’d Recipient' => 'Destinatario in Ccn', - 'Bad Request' => 'Richiesta non valida', - 'Bad address ID.' => 'ID indirizzo non valido.', - 'Bad order ID.' => 'ID ordine non valido.', - 'Base Price' => 'Prezzo base', - 'Base Promotional Price' => 'Prezzo promozionale base', - 'Base Rate' => 'Tasso base', - 'Base' => 'Base', - 'Bcc' => 'Ccn', - 'Billing Address' => 'Indirizzo di fatturazione', - 'Billing Business Name' => 'Nome azienda per la fatturazione', - 'Billing First Name' => 'Nome per la fatturazione', - 'Billing Full Name' => 'Nome completo per la fatturazione', - 'Billing Last Name' => 'Cognome per la fatturazione', - 'Billing address required.' => 'Indirizzo di fatturazione obbligatorio.', - 'Billing detail update URL' => 'URL di aggiornamento dei dettagli di fatturazione', - 'Billing issues' => 'Problemi di fatturazione', - 'Billing' => 'Fatturazione', - 'Both (Line item price + Line item shipping costs)' => 'Entrambi (prezzo per voce + costi di spedizione per voce)', - 'Business ID' => 'ID azienda', - 'Business Name' => 'Nome azienda', - 'Business Tax ID' => 'Partita IVA aziendale', - 'CC’d Recipient' => 'Destinatario in Cc', - 'CVV' => 'CVV', - 'Can be used as an internal reference.' => 'Può essere utilizzato come riferimento interno.', - 'Can not complete payment for missing transaction.' => 'Impossibile completare il pagamento per transazione assente.', - 'Can not create a new order' => 'Impossibile creare un nuovo ordine', - 'Can not find an order to pay.' => 'Impossibile trovare un ordine da pagare.', - 'Can not find enabled email.' => 'Impossibile trovare l\'email abilitata.', - 'Can not find order' => 'Impossibile trovare l\'ordine', - 'Can not find order.' => 'Impossibile trovare l\'ordine.', - 'Can not find the transaction to refund' => 'Impossibile trovare la transazione da rimborsare', - 'Can not move between these inventory types.' => 'Impossibile spostarsi tra questi tipi di inventario.', - 'Can not refund amount greater than the remaining amount' => 'Impossibile rimborsare un importo maggiore rispetto all\'importo rimanente', - 'Cancel subscription' => 'Annulla sottoscrizione', - 'Cancel with gateway now' => 'Annulla ora tramite gateway', - 'Cancel' => 'Annulla', - 'Cancellation date' => 'Data annullamento', - 'Cancellation' => 'Annullamento', - 'Cannot switch plans for this subscription.' => 'Impossibile cambiare piano per questa sottoscrizione.', - 'Can’t preview this email.' => 'Impossibile visualizzare l\'anteprima di questa e-mail.', - 'Capture payment' => 'Acquisisci pagamento', - 'Capture' => 'Acquisisci', - 'Card Holder' => 'Proprietario della carta', - 'Card Number' => 'Numero di carta', - 'Card' => 'Carta', - 'Cart Recovery Link' => 'Link per il recupero del carrello', - 'Cart forgotten.' => 'Carrello dimenticato.', - 'Cart updated.' => 'Carrello aggiornato.', - 'Cart {number}' => 'Carrello {number}', - 'Catalog Pricing Rule' => 'Regola di prezzo del catalogo', - 'Catalog pricing rule description.' => 'Descrizione della regola di prezzo del catalogo.', - 'Catalog pricing rule saved.' => 'Regola di prezzo del catalogo salvata.', - 'Catalog pricing rules deleted.' => 'Regole di prezzo del catalogo eliminate.', - 'Catalog pricing rules updated.' => 'Regole di prezzo del catalogo aggiornate.', - 'Categories Relationship Type' => 'Tipo di rapporto delle categorie', - 'Categories' => 'Categorie', - 'Category Rate Overrides' => 'Ignora tariffa categoria', - 'Centimeters (cm)' => 'Centimetri (cm)', - 'Changing this value may affect your ability to refund existing transactions.' => 'Modificare questo valore può influenzare la tua capacità di rimborsare le transazioni esistenti.', - 'Choose a color to represent the order’s status' => 'Scegli un colore per rappresentare lo stato dell\'ordine', - 'Choose a new customer' => 'Scegli un nuovo cliente', - 'Choose adjustment values to include when calculating the product revenue total.' => 'Scegli i valori di rettifica da includere nel calcolo del ricavo totale del prodotto.', - 'Choose the currency’s ISO code.' => 'Scegliere il codice ISO della valuta.', - 'Choose the destination inventory location for the existing on hand stock.' => 'Scegli la sede di destinazione dell\'inventario per le scorte esistenti.', - 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Scegli i siti in cui questo tipo di prodotto deve essere disponibile e configura le impostazioni specifiche del sito.', - 'City' => 'Città', - 'Clear counter' => 'Azzera contatore', - 'Clear notices' => 'Cancella notifiche', - 'Close' => 'Chiudi', - 'Code' => 'Codice', - 'Collated PDF' => 'PDF fascicolati', - 'Color' => 'Colore', - 'Commerce Products' => 'Prodotti Commerce', - 'Commerce Settings' => 'Impostazioni Commerce', - 'Commerce Variants' => 'Varianti di Commerce', - 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Impossibile inviare l’email di Commerce “{email}” per l’ordine “{order}”.', - 'Commerce order exports' => 'Esportazioni ordini da Commerce', - 'Commerce' => 'Commerce', - 'Committed' => 'Impegnato', - 'Completed Email' => 'E-mail completa', - 'Completed' => 'Completato', - 'Completing order failed.' => 'Completamento dell\'ordine fallito.', - 'Condition' => 'Condizione', - 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Le condizioni vengono confrontate con un ordine prima di esaminare le regole. Ciò è utile se si desidera qualificare in anticipo la disponibilità di un metodo o se esistono condizioni comuni per tutte le regole per questo metodo.', - 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Le condizioni vengono confrontate con il cliente dell\'ordine prima di esaminare le regole. Ciò è utile se si desidera qualificare in anticipo la disponibilità di un metodo o se esistono condizioni comuni per tutte le regole per questo metodo.', - 'Conditions' => 'Condizioni', - 'Contains Purchasables' => 'Contiene prodotti disponibili all’acquisto', - 'Control Panel Settings' => 'Impostazioni del pannello di controllo', - 'Control panel' => 'Pannello di controllo', - 'Conversion Rate' => 'Tasso di conversione', - 'Converted Price' => 'Prezzo convertito', - 'Copied!' => 'Copiato!', - 'Copy the URL' => 'Copia l\'URL', - 'Copy to {location}' => 'Copia in {location}', - 'Copy' => 'Copia', - 'Costs' => 'Costi', - 'Could not archive gateway.' => 'Impossibile archiviare il gateway.', - 'Could not cancel “{reference}”.' => 'Impossibile cancellare “{reference}”.', - 'Could not create the payment source.' => 'Impossibile creare la fonte di pagamento.', - 'Could not delete shipping rule' => 'Impossibile eliminare le regole di spedizione', - 'Could not delete shipping zone' => 'Impossibile eliminare la zona di spedizione', - 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Impossibile eliminare {count, number} {count, plural, one{categoria} other{categorie}} di spedizione.', - 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Impossibile eliminare {count, number} {count, plural, one{metodo} other{metodi}} e regole di spedizione.', - 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Impossibile eliminare {count, number} {count, plural, one{categoria} other{categorie}} fiscale/i.', - 'Could not find the email or template.' => 'Non è stato possibile trovare l\'email o il template.', - 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Impossibile contrassegnare l’ordine {number} come completato. Il salvataggio non è andato a buon fine in fase di completamento dell’ordine con errori: {order}', - 'Could not reactivate “{reference}”.' => 'Impossibile riattivare “{reference}”.', - 'Could not send email' => 'Non è stato possibile inviare l’email', - 'Could not switch “{reference}” to “{plan}”.' => 'Impossibile passare da “{reference}” a “{plan}”.', - 'Could not update orders address.' => 'Non è stato possibile aggiornare gli indirizzi degli ordini.', - 'Couldn’t archive Line Item Status.' => 'Non è stato possibile salvare lo stato della voce.', - 'Couldn’t archive Order Status.' => 'Non è stato possibile archiviare lo stato dell’ordine.', - 'Couldn’t capture transaction.' => 'Non è stato possibile acquisire la transazione.', - 'Couldn’t capture transaction: {message}' => 'Non è stato possibile acquisire la transazione: {message}', - 'Couldn’t delete email.' => 'Impossibile eliminare l\'e-mail.', - 'Couldn’t delete the payment source.' => 'Impossibile eliminare la fonte di pagamento.', - 'Couldn’t get order.' => 'Non è stato possibile recuperare l’ordine.', - 'Couldn’t recalculate order.' => 'Non è stato possibile ricalcolare l’ordine.', - 'Couldn’t refund transaction.' => 'Non è stato possibile rimborsare la transazione.', - 'Couldn’t refund transaction: {message}' => 'Non è stato possibile rimborsare la transazione: {message}', - 'Couldn’t reorder Line Item Statuses.' => 'Non è stato possibile riordinare gli stati delle voci.', - 'Couldn’t reorder Order Statuses.' => 'Non è stato possibile riordinare gli stati ordine.', - 'Couldn’t reorder PDFs.' => 'Impossibile riordinare i PDF.', - 'Couldn’t reorder discounts.' => 'Non è stato possibile riordinare gli sconti.', - 'Couldn’t reorder gateways.' => 'Impossibile riordinare i gateway.', - 'Couldn’t reorder plans.' => 'Non è stato possibile riordinare i piani.', - 'Couldn’t reorder rules.' => 'Impossibile riordinare le regole.', - 'Couldn’t reorder sale.' => 'Impossibile riordinare la vendita promozionale.', - 'Couldn’t reorder sales.' => 'Impossibile riordinare le vendite.', - 'Couldn’t reorder statuses.' => 'Impossibile riordinare gli stati.', - 'Couldn’t reorder stores.' => 'Impossibile riordinare gli store.', - 'Couldn’t save PDF.' => 'Non è stato possibile salvare il PDF.', - 'Couldn’t save catalog pricing rule.' => 'Non è stato possibile salvare la regola di prezzo in catalogo.', - 'Couldn’t save currency.' => 'Non è stato possibile salvare la valuta.', - 'Couldn’t save discount.' => 'Non è stato possibile salvare lo sconto.', - 'Couldn’t save email.' => 'Non è stato possibile salvare l’email.', - 'Couldn’t save gateway.' => 'Impossibile salvare il gateway.', - 'Couldn’t save inventory location.' => 'Impossibile salvare la sede dell\'inventario.', - 'Couldn’t save line item status.' => 'Non è stato possibile salvare lo stato della voce.', - 'Couldn’t save order fields.' => 'Impossibile salvare i campi dell\'ordine.', - 'Couldn’t save order status.' => 'Non è stato possibile salvare lo stato dell’ordine.', - 'Couldn’t save order.' => 'Non è stato possibile salvare l’ordine.', - 'Couldn’t save product type.' => 'Non è stato possibile salvare il tipo di prodotto.', - 'Couldn’t save sale.' => 'Non è stato possibile salvare la vendita promozionale.', - 'Couldn’t save settings.' => 'Non è stato possibile salvare le impostazioni.', - 'Couldn’t save shipping category.' => 'Non è stato possibile salvare la categoria di spedizione.', - 'Couldn’t save shipping method.' => 'Non è stato possibile salvare il metodo di spedizione.', - 'Couldn’t save shipping rule.' => 'Non è stato possibile salvare la regola di spedizione.', - 'Couldn’t save shipping zone.' => 'Non è stato possibile salvare l’area di spedizione.', - 'Couldn’t save store.' => 'Impossibile salvare store.', - 'Couldn’t save subscription fields.' => 'Impossibile salvare i campi di sottoscrizione.', - 'Couldn’t save subscription plan.' => 'Impossibile salvare il piano di sottoscrizione.', - 'Couldn’t save subscription.' => 'Non è stato possibile salvare la sottoscrizione.', - 'Couldn’t save tax category.' => 'Non è stato possibile salvare la categoria fiscale.', - 'Couldn’t save tax rate.' => 'Non è stato possibile salvare l’aliquota fiscale.', - 'Couldn’t save tax zone.' => 'Non è stato possibile salvare la zona fiscale.', - 'Couldn’t save transfer fields.' => 'Impossibile salvare i campi del trasferimento.', - 'Couldn’t update catalog pricing rule statuses.' => 'Non è stato possibile aggiornare le regole di prezzo in catalogo.', - 'Couldn’t update status.' => 'Impossibile aggiornare lo stato.', - 'Couldn’t updated sales status.' => 'Non è stato possibile aggiornare lo stato della vendita promozionale.', - 'Country Code of Origin' => 'Codice del Paese di origine', - 'Country List' => 'Elenco di Paesi', - 'Country not allowed.' => 'Paese non consentito.', - 'Country' => 'Paese', - 'Coupon Code' => 'Codice promozionale', - 'Coupon can not apply discount to this order due to address mismatch.' => 'Impossibile applicare lo sconto a questo ordine tramite il codice promozionale a causa della mancata corrispondenza dell\'indirizzo.', - 'Coupon can not apply discount to this order due to customer mismatch.' => 'Impossibile applicare lo sconto a questo ordine tramite il codice promozionale a causa della mancata corrispondenza del cliente.', - 'Coupon can not apply discount to this order.' => 'Impossibile applicare lo sconto a questo ordine tramite il codice promozionale.', - 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Il codice coupon “{code}” è già utilizzato dallo sconto “{name}”.', - 'Coupon codes cannot be blank.' => 'I codici promozionali non possono essere vuoti.', - 'Coupon codes must be unique.' => 'I codici promozionali devono essere univoci.', - 'Coupon format is required and must contain at least one `#`.' => 'Il formato dei codici promozionali è obbligatorio e deve contenere almeno un `#`.', - 'Coupon not valid.' => 'Coupon non valido.', - 'Coupon removed: {explanation}' => 'Coupon rimosso: {explanation}', - 'Coupons' => 'Codici promozionali', - 'Craft Commerce - Administration' => 'Craft Commerce - Amministrazione', - 'Craft Commerce - Inventory' => 'Craft Commerce - Inventario', - 'Craft Commerce - Orders' => 'Craft Commerce - Ordini', - 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Tipo di prodotto - {name}', - 'Craft Commerce - Subscriptions' => 'Craft Commerce - Abbonamenti', - 'Create a Discount' => 'Crea uno sconto', - 'Create a Subscription Plan' => 'Crea un piano di sottoscrizione', - 'Create a new PDF' => 'Crea nuovo PDF', - 'Create a new catalog pricing rule' => 'Crea una nuova regola di prezzo in catalogo', - 'Create a new currency' => 'Crea una nuova valuta', - 'Create a new email' => 'Crea una nuova email', - 'Create a new gateway' => 'Crea un nuovo gateway', - 'Create a new line item status' => 'Crea un nuovo stato voce', - 'Create a new order status' => 'Crea un nuovo stato ordine', - 'Create a new product type' => 'Crea un nuovo tipo di prodotto', - 'Create a new sale' => 'Crea una nuova vendita promozionale', - 'Create a new shipping category' => 'Crea una nuova categoria di spedizione', - 'Create a new shipping method' => 'Crea un nuovo metodo di spedizione', - 'Create a new shipping rule' => 'Crea una nuova regola di spedizione', - 'Create a new tax category' => 'Crea una nuova categoria fiscale', - 'Create a new tax rate' => 'Crea una nuova aliquota fiscale', - 'Create a product type' => 'Crea un tipo di prodotto', - 'Create a shipping zone' => 'Crea un’area di spedizione', - 'Create a tax zone' => 'Crea una zona fiscale', - 'Create catalog pricing rules' => 'Crea regole di prezzo in catalogo', - 'Create customer: “{email}”' => 'Crea cliente: “{email}”', - 'Create discounts' => 'Crea sconti', - 'Create discount…' => 'Creazione sconto in corso...', - 'Create rules that allow this discount to match the order.' => 'Crea regole che permettono a questo sconto di corrispondere all\'ordine.', - 'Create rules that allow this discount to match the order’s billing address.' => 'Crea regole che permettono a questo sconto di corrispondere all\'indirizzo di fatturazione dell\'ordine.', - 'Create rules that allow this discount to match the order’s customer.' => 'Crea regole che permettono a questo sconto di corrispondere al cliente dell\'ordine.', - 'Create rules that allow this discount to match the order’s shipping address.' => 'Crea regole che permettono a questo sconto di corrispondere all\'indirizzo di spedizione dell\'ordine.', - 'Create rules that allow this gateway to match the billing address.' => 'Crea delle regole che consentano a questo gateway di corrispondere all\'indirizzo di fatturazione.', - 'Create rules that allow this gateway to match the order.' => 'Crea regole che permettono a questo gateway di corrispondere all\'ordine.', - 'Create rules that allow this gateway to match the shipping address.' => 'Crea delle regole che consentano a questo gateway di corrispondere all\'indirizzo di spedizione.', - 'Create sales' => 'Crea vendite promozionali', - 'Create sale…' => 'Creazione vendita promozionale in corso...', - 'Created' => 'Creato', - 'Credit Card Payment Type' => 'Tipo di pagamento con carta di credito', - 'Currency Code' => 'Codice valuta', - 'Currency saved.' => 'Valuta salvata.', - 'Currency' => 'Valuta', - 'Current' => 'Attuale', - 'Custom 1' => 'Personalizzato 1', - 'Custom 2' => 'Personalizzato 2', - 'Custom 3' => 'Personalizzato 3', - 'Custom 4' => 'Personalizzato 4', - 'Custom' => 'Personalizzato', - 'Customer Enabled?' => 'Cliente abilitato?', - 'Customer ID is required.' => 'È richiesto un ID cliente.', - 'Customer Note' => 'Nota del cliente', - 'Customer Notices' => 'Notifiche del cliente', - 'Customer data' => 'Dati dei clienti', - 'Customer' => 'Cliente', - 'Damaged' => 'Danneggiato', - 'Data shown might be outdated.' => 'I dati riportati potrebbero essere obsoleti.', - 'Date Authorized' => 'Data di autorizzazione', - 'Date Created' => 'Data creazione', - 'Date First Paid' => 'Data primo pagamento', - 'Date Ordered' => 'Data ordine', - 'Date Paid' => 'Data pagamento', - 'Date Updated' => 'Data aggiornamento', - 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Data in cui la regola di prezzo in catalogo verrà attivata. Lasciare vuoto per data di inizio illimitata', - 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Data in cui lo sconto verrà attivato. Lasciare vuoto per data di inizio illimitata', - 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Data in cui la vendita promozionale verrà attivata. Lasciare vuoto per data di inizio illimitata', - 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Data in cui la regola di prezzo in catalogo terminerà. Lasciare vuoto per data di fine illimitata', - 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Data in cui lo sconto terminerà. Lasciare vuoto per data di fine illimitata', - 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Data in cui la vendita promozionale terminerà. Lasciare vuoto per data di fine illimitata', - 'Date' => 'Data', - 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Predefinito - Consente un prezzo negativo se gli sconti sono di importo superiore al valore dell\'ordine.', - 'Default Category' => 'Categoria predefinita', - 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Visualizzazione predefinita del pannello di controllo di Commerce. Se non dispone del permesso, l\'utente tornerà a una posizione a cui può accedere.', - 'Default Order PDF' => 'PDF ordine predefinito', - 'Default Per Item Rate' => 'Tariffa in base ad articolo predefinita', - 'Default Percentage Rate' => 'Tariffa in base a percentuale predefinita', - 'Default Status?' => 'Stato predefinito?', - 'Default View' => 'Vista predefinita', - 'Default Weight Rate' => 'Tariffa in base al peso predefinita', - 'Default Zone' => 'Zona predefinita', - 'Default status?' => 'Stato predefinito?', - 'Default to this tax zone when no billing address is set' => 'Tasso di imposta predefinito se non viene impostato nessun indirizzo di fatturazione', - 'Default to this tax zone when no shipping address is set' => 'Zona fiscale predefinita se non viene impostato nessun indirizzo di spedizione', - 'Default variant updated.' => 'Variante predefinita aggiornata.', - 'Default' => 'Impostazione predefinita', - 'Default?' => 'Ripristinare le impostazioni predefinite?', - 'Delete catalog pricing rules' => 'Elimina regole di prezzo in catalogo', - 'Delete discounts' => 'Elimina sconti', - 'Delete orders' => 'Elimina ordini', - 'Delete sales' => 'Elimina vendite promozionali', - 'Delete' => 'Elimina', - 'Deleting the {location} location.' => 'Eliminazione della sede {location}.', - 'Describe this rule.' => 'Descrivere questa regola.', - 'Describe this shipping zone.' => 'Descrivere quest’area di spedizione.', - 'Describe this tax zone.' => 'Descrivere questa zona fiscale.', - 'Description' => 'Descrizione', - 'Destination Inventory Location' => 'Sede dell\'inventario di destinazione', - 'Destination' => 'Destinazione', - 'Details' => 'Dettagli', - 'Dimension Unit' => 'Unità dimensioni', - 'Dimensions' => 'Dimensioni', - 'Disabled' => 'Disabilitato', - 'Disallow' => 'Nega', - 'Discount all line items' => 'Sconta tutte le voci', - 'Discount description.' => 'Descrizione sconto.', - 'Discount is not allowed for the order' => 'Sconto non consentito per l\'ordine', - 'Discount is out of date.' => 'Sconto scaduto.', - 'Discount saved.' => 'Sconto salvato.', - 'Discount the matching items only' => 'Sconta solo gli articoli corrispondenti', - 'Discount use has reached its limit.' => 'Limite raggiunto per uso sconti.', - 'Discount' => 'Sconto', - 'Discounted Item Subtotal' => 'Totale parziale dell\'articolo scontato', - 'Discounted Items' => 'Articoli scontati', - 'Discounts deleted.' => 'Sconti eliminati.', - 'Discounts reordered.' => 'Sconti riordinati.', - 'Discounts updated.' => 'Sconti aggiornati.', - 'Discounts' => 'Sconti', - 'Disqualify with valid business tax ID?' => 'Annullare con partita IVA aziendale valida?', - 'Do not apply subsequent matching sales beyond applying this sale.' => 'Non applicare vendite corrispondenti successive oltre all’applicazione di questa vendita.', - 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Non applicare questa aliquota se l\'indirizzo dell\'ordine ha una delle partite IVA aziendali valide selezionate.', - 'Do not attach a PDF to this email' => 'Non allegare un PDF a questa email', - 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Non richiamare il ricalcolo dell\'ordine (Numero: {orderNumber}) in presenza di errori.', - 'Donation can not be zero.' => 'La donazione non può essere pari a zero.', - 'Donation needs to be an amount.' => 'La donazione deve essere un importo numerico.', - 'Donation settings saved.' => 'Impostazioni di donazione salvate.', - 'Donation' => 'Donazione', - 'Donations' => 'Donazioni', - 'Done' => 'Fine', - 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Non applicare sconti successivi a un ordine se viene applicato questo sconto', - 'Download PDF' => 'Scarica PDF', - 'Download PDF…' => 'Scarica PDF…', - 'Download Type' => 'Tipo di download', - 'Download' => 'Scarica', - 'Draft' => 'Bozza', - 'Dummy gateway payment failed.' => 'Pagamento gateway provvisorio non riuscito.', - 'Duplicate options exist' => 'Esistono opzioni duplicate', - 'Duration' => 'Durata', - 'EU VAT ID' => 'Partita IVA europea', - 'Edit address' => 'Modifica indirizzo', - 'Edit adjustments' => 'Modifica rettifiche', - 'Edit catalog pricing rules' => 'Modifica regole di prezzo in catalogo', - 'Edit discounts' => 'Modifica sconti', - 'Edit options' => 'Modifica opzioni', - 'Edit orders' => 'Modifica ordini', - 'Edit sales' => 'Modifica vendite promozionali', - 'Edit' => 'Modifica', - 'Effect' => 'Effetto', - 'Either (Default) - The relationship field is on the purchasable or the category' => 'Qualsiasi (predefinito) - Il campo relazione è sul campo disponibile all\'acquisto o categoria', - 'Either way' => 'Entrambi i casi', - 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Errore di generazione del PDF dell\'email per l\'email “{email}”. Ordine: “{order}”. Errore template PDF: “{message}” {file}:{line}', - 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'Il template PDF dell’email non esiste in “{templatePath}” per l’email “{email}”. Ordine: “{order}”.', - 'Email Subject' => 'Oggetto dell’email', - 'Email error. No email address found for order. Order: “{order}”' => 'Errore email. Nessun indirizzo email trovato per l’ordine. Ordine: “{order}”', - 'Email is not enabled.' => 'Email non abilitata.', - 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Il template dell’email di testo semplice non esiste in “{templatePath}” ed è risultato essere “{templateParsedPath}” per l’email “{email}”. Ordine: “{order}”.', - 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email di testo semplice per l\'email “{email}”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', - 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del percorso al template email di testo semplice per l\'email “{email}” in “Template Path”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', - 'Email required to make payments on a completed order.' => 'Email necessaria per effettuare i pagamenti per un ordine completato.', - 'Email saved.' => 'Email salvata.', - 'Email sent' => 'Email inviata', - 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'Il template dell’email non esiste in “{templatePath}” ed è risultato essere “{templateParsedPath}” per l’email “{email}”. Ordine: “{order}”.', - 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email per l\'email personalizzata “{email}” in “To:”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email per l\'email “{email}” in “BCC:”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email per l\'email “{email}” in “CC:”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email per l\'email “{email}” in “ReplyTo:”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email per l\'email “{email}” in “Subject:”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', - 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del template email per l\'email “{email}”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', - 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Errore di analisi del percorso al template email per l\'email “{email}” in “Template Path”. Ordine: “{order}”. Errore template: “{message}” {file}:{line}', - 'Email unavailable.' => 'E-mail non disponibile.', - 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'Non è stato possibile inviare l\'email “{email}” per l\'ordine “{order}”. Errore: {error} {file}:{line}', - 'Email “{email}” for order {order} was cancelled.' => 'L’email “{email}” per l’ordine {order} è stata cancellata.', - 'Email' => 'Email', - 'Emails' => 'Email', - 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Abilita l\'opzione se questa aliquota deve essere inclusa nel prezzo dell\'imponibile invece di aggiungere un costo all\'ordine.', - 'Enable structure for products of this type' => 'Abilita struttura per i prodotti di questo tipo', - 'Enable this discount' => 'Abilita questo sconto', - 'Enable this rule' => 'Abilita questa regola', - 'Enable this sale' => 'Abilita questa vendita promozionale', - 'Enable this shipping method on the front end' => 'Abilita questo metodo di spedizione in front end', - 'Enable this shipping rule' => 'Abilita questa regola di spedizione', - 'Enable this tax rate' => 'Abilita questa aliquota fiscale', - 'Enabled for customers to select during checkout?' => 'Abilitato per la selezione da parte dei clienti in fase di pagamento?', - 'Enabled for customers to select?' => 'Abilitato per la selezione da parte dei clienti?', - 'Enabled' => 'Pubblicato', - 'Enabled?' => 'Abilitato?', - 'End Date' => 'Data di fine', - 'Enter SKU' => 'Inserisci SKU', - 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Inserisci un nome facilmente comprensibile per questa aliquota fiscale da usare nel pannello di controllo.', - 'Enter a percentage like {ex1} or {ex2}.' => 'Inserisci una percentuale come {ex1} o {ex2}.', - 'Enter coupon code' => 'Inserisci il codice coupon', - 'Enter reference' => 'Inserisci riferimento', - 'Error refunding transaction: {transactionHash}' => 'Errore di rimborso della transazione: {transactionHash}', - 'Every new store must be assigned to at least one site.' => 'Ogni nuovo store deve essere assegnato ad almeno un sito.', - 'Everywhere' => 'Ovunque', - 'Example' => 'Esempio', - 'Exclude this discount for products that are already on promotion' => 'Escludi questo sconto per i prodotti che sono già in promozione', - 'Expired Link' => 'Link scaduto', - 'Expired' => 'Scaduto', - 'Expiry Date' => 'Data di scadenza', - 'Expiry date' => 'Data di scadenza', - 'Expiry' => 'Scadenza', - 'Failed to receive transfer: {error}' => 'Impossibile ricevere il trasferimento: {error}', - 'Failed to send email. Please try again.' => 'Impossibile inviare l\'email. Riprova.', - 'Failed to start' => 'Avvio fallito', - 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Impossibile aggiornare {num, plural, one {}=1{stato dell\'ordine} other{stati degli ordini}}.', - 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Impossibile aggiornare lo stato dell\'ordine su {num, plural, one {}=1{ordine} other{ordini}}.', - 'Feet (ft)' => 'Piedi (ft)', - 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Condizioni di applicazione filtri che descrivono a quali ordini si applica questa regola. Scrivere 0 per saltare una condizione.', - 'First Name' => 'Nome', - 'Flat Amount Off Order' => 'Importo forfettario di sconto sull\'ordine', - 'Flat Order Discount Amount Off' => 'Importo di sconto per ordine forfettario', - 'Free Order Payment Strategy' => 'Strategia di pagamento per ordini gratuiti', - 'Free Shipping' => 'Spedizione gratuita', - 'Free orders are processed by the payment gateway' => 'Gli ordini gratuiti vengono elaborati dal gateway di pagamento', - 'Free orders complete immediately' => 'Ordini gratuiti da completare immediatamente', - 'Free shipping can only be for whole order or matching items, not both.' => 'La spedizione gratuita può essere applicata solo all\'intero ordine o agli articoli abbinati, non a entrambi.', - 'From Name' => 'Nome provenienza', - 'Fulfill' => 'Evadi', - 'Fulfilled' => 'Evaso', - 'Fulfillment' => 'Evasione', - 'Full Name' => 'Nome completo', - 'Gateway Code' => 'Codice del gateway', - 'Gateway Message' => 'Messaggio del gateway', - 'Gateway Reference' => 'Riferimento del gateway', - 'Gateway Response' => 'Risposta del gateway', - 'Gateway doesn’t support authorize' => 'Il gateway non supporta l’autorizzazione', - 'Gateway doesn’t support partial refunds.' => 'Il gateway non supporta rimborsi parziali.', - 'Gateway doesn’t support purchase' => 'Il gateway non supporta l’acquisto', - 'Gateway doesn’t support refunds.' => 'Il gateway non supporta i rimborsi.', - 'Gateway saved.' => 'Gateway salvato.', - 'Gateway' => 'Gateway', - 'Gateways reordered.' => 'Gateway riordinati.', - 'Gateways' => 'Gateway', - 'General Settings' => 'Impostazioni generali', - 'General' => 'Generale', - 'Generate' => 'Genera', - 'Generated Coupon Format' => 'Formato codice promozionale generato', - 'Grams (g)' => 'Grammi (g)', - 'Groups for which this sale will be applicable to.' => 'Gruppi a cui questa vendita promozionale sarà applicabile.', - 'HTML Email Template Path' => 'Percorso template email HTML', - 'Handle' => 'Puntatore', - 'Harmonized System Code' => 'Codice sistema armonizzato', - 'Has Admin Notices' => 'Contiene avvisi dell\'amministratore', - 'Has Emails?' => 'Ha delle email?', - 'Has Free Shipping' => 'Spedizione gratuita disponibile', - 'Has Orders' => 'Ha ordini', - 'Has Purchasable' => 'Ha prodotti disponibili all’acquisto', - 'Has Variants?' => 'Ha delle varianti?', - 'Height ({unit})' => 'Altezza ({unit})', - 'Height' => 'Altezza', - 'Hide snapshot' => 'Nascondi snapshot', - 'History' => 'Cronologia', - 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Per quanto tempo (in secondi) un link di download PDF deve rimanere valido prima di scadere. Il valore predefinito è 86.400 (24 ore).', - 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Quante volte un indirizzo email può utilizzare questo sconto. Si applica a tutti gli ordini precedenti, sia di ospiti che di utenti. Impostare su zero per l’uso illimitato da parte di ospiti e di utenti.', - 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Quante volte un utente può usare questo sconto. Se questa opzione è impostata a un valore non nullo, lo sconto sarà disponibile solo agli utenti registrati.', - 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Quante volte gli ospiti o gli utenti registrati potranno utilizzare questo sconto in totale. Impostare su zero per l’uso illimitato.', - 'How products should be labeled within the control panel.' => 'Come devono essere etichettati i prodotti nel pannello di controllo.', - 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'Come sono correlati gli articolo disponibili all\'acquisto e le categorie, il che determina gli articoli corrispondenti. Vedi [Terminologia delle relazioni]({link}).', - 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'Come verrà descritto questo prodotto in una voce di un ordine. È possibile includere tag che producono proprietà, come {ex1} o {ex2}', - 'How this shipping method will be referred to in templates and forms.' => 'In che modo si farà riferimento a questo metodo di spedizione nei template e nei moduli.', - 'How variants should be labeled within the control panel.' => 'Come devono essere etichettate le varianti nel pannello di controllo.', - 'How you’ll refer to this PDF in the templates.' => 'Come farai riferimento a questo PDF nei template.', - 'How you’ll refer to this product type in the templates.' => 'Come farai riferimento a questo tipo di prodotto nei template.', - 'How you’ll refer to this shipping category in the templates.' => 'Come farai riferimento a questa categoria di spedizione nei template.', - 'How you’ll refer to this status in the templates.' => 'Come farai riferimento a questo stato nei template.', - 'How you’ll refer to this subscription plan in the templates.' => 'Come ti riferirai a questa sottoscrizione nei template.', - 'How you’ll refer to this tax category in the templates.' => 'Come farai riferimento a questa categoria fiscale nei template.', - 'ID' => 'ID', - 'IP Address' => 'Indirizzo IP', - 'If disabled, this PDF will not be available or sent with emails.' => 'Se disattivato, questo PDF non sarà disponibile o inviato con le e-mail.', - 'If disabled, this email will not send.' => 'Se la voce è disabilitata, questa email non verrà inviata.', - 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Se l\'opzione è abilitata e l\'aliquota non corrisponde all\'ordine, l\'importo dell\'aliquota sarà rimosso dal prezzo dell\'imponibile nel carrello.', - 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Se impostato su Autorizza solo, sarà necessario acquisire manualmente i pagamenti prima che i fondi vengano trasferiti sul proprio conto. Il gateway deve supportare l’opzione selezionata.', - 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Se selezioni la percentuale di "riduzione sul prezzo dell\'articolo scontato", essa includerà l\'"Importo per articolo" così come qualsiasi altro sconto applicato prima di questo.', - 'Ignore Promotions?' => 'Ignora promozioni?', - 'Ignore previous matching sales if this sale matches.' => 'Ignora vendite precedenti corrispondenti se questa vendita corrisponde.', - 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignora i prezzi promozionali quando questo sconto viene applicato agli articoli corrispondenti', - 'Inactive Carts' => 'Carrelli non attivi', - 'Inches (in)' => 'Pollici (in)', - 'Include built-in line item tax.' => 'Includi imposta integrata nelle voci.', - 'Include in price?' => 'Includere nel prezzo?', - 'Include line item discounts.' => 'Includi sconti delle voci.', - 'Include line item shipping costs.' => 'Includi costi di spedizione delle voci.', - 'Include separate line item tax.' => 'Includi imposta separata delle voci.', - 'Included in price?' => 'Incluso nel prezzo?', - 'Included' => 'Incluso', - 'Incoming transfer from Transfer ID: ' => 'Trasferimento in arrivo dall\'ID di trasferimento: ', - 'Incoming' => 'In arrivo', - 'Info' => 'Informazioni', - 'Information linked?' => 'Informazioni correlate?', - 'Information' => 'Informazioni', - 'Invalid JSON' => 'JSON non valido', - 'Invalid Order ID' => 'ID ordine non valido', - 'Invalid VAT ID.' => 'Partita IVA non valida.', - 'Invalid condition syntax' => 'Sintassi della condizione non valida', - 'Invalid email.' => 'Email non valida.', - 'Invalid formula syntax' => 'Sintassi della formula non valida', - 'Invalid gateway: {value}' => 'Gateway non valido: {value}', - 'Invalid inventory movements.' => 'Movimenti di inventario non validi.', - 'Invalid order condition syntax.' => 'Sintassi della condizione dell\'ordine non valida.', - 'Invalid payment or order. Please review.' => 'Ordine o pagamento non valido. Controlla i dati.', - 'Invalid payment source ID: {value}' => 'ID fonte di pagamento non valido: {value}', - 'Invalid store.' => 'Store non valido.', - 'Invalid user.' => 'Utente non valido.', - 'Inventory Item' => 'Articolo dell\'inventario', - 'Inventory Location' => 'Sede dell\'inventario', - 'Inventory Locations' => 'Sedi dell\'inventario', - 'Inventory Tracked' => 'Inventario tracciato', - 'Inventory Transfers' => 'Trasferimenti dell\'inventario', - 'Inventory could not be set.' => 'Non è stato possibile impostare l\'inventario.', - 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'La sede dell\'inventario ha impegnato lo stock, l\'ordine o gli ordini devono prima essere evasi.', - 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'La sede dell\'inventario ha stock in arrivo, il trasferimento o i trasferimenti devono prima essere completati.', - 'Inventory location is already deactivated.' => 'La sede dell\'inventario è già disattivata.', - 'Inventory location saved.' => 'Sede dell\'inventario salvata.', - 'Inventory locations not saved.' => 'Sedi dell\'inventario non salvate.', - 'Inventory movement could not be saved.' => 'Non è stato possibile salvare il movimento di inventario.', - 'Inventory movement saved.' => 'Movimento di inventario salvato.', - 'Inventory updated.' => 'Inventario aggiornato.', - 'Inventory was not updated.' => 'Inventario non aggiornato.', - 'Inventory' => 'Inventario', - 'Invoice amount' => 'Importo fattura', - 'Invoice date' => 'Data fattura', - 'Is Promotable' => 'Promozione applicabile', - 'Is Promotional Price?' => 'È il prezzo promozionale?', - 'Is Shippable' => 'Spedizione disponibile', - 'Is Taxable' => 'Tassazione disponibile', - 'Item Rates' => 'Tariffe per articolo', - 'Item Subtotal' => 'Subtotale articolo', - 'Item Total' => 'Totale articolo', - 'Item' => 'Elemento', - 'Items' => 'Articoli', - 'Kilograms (kg)' => 'Chilogrammi (kg)', - 'Label' => 'Etichetta', - 'Landscape' => 'Orizzontale', - 'Language' => 'Lingua', - 'Last Name' => 'Cognome', - 'Last Updated' => 'Ultimo aggiornamento', - 'Leave a category rate override blank to use the rate from above.' => 'Lascia vuota l\'opzione di override del tasso di categoria per utilizzare il tasso di cui sopra.', - 'Leave blank for unlimited uses.' => 'Lascia in bianco per indicare utilizzo illimitato.', - 'Leave blank if products don’t have URLs' => 'Lascia il campo vuoto se ai prodotti non sono associati URL', - 'Leave gateway subscription as-is' => 'Lascia invariato l\'abbonamento al gateway', - 'Length ({unit})' => 'Lunghezza ({unit})', - 'Length' => 'Lunghezza', - 'Let each product choose which sites it should be saved to' => 'Lascia scegliere a ogni prodotto su quali siti deve essere salvato', - 'Limit which orders this discount applies to based on its line items.' => 'Limita a quali ordini applicare questo sconto in base ai relativi articoli.', - 'Limit which purchasables this sale applies to.' => 'Limita a quali articoli disponibili all\'acquisto applicare questa offerta.', - 'Limit' => 'Limite', - 'Line Item Statuses' => 'Stati delle voci', - 'Line Item' => 'Voce', - 'Line Items' => 'Voci', - 'Line item price (minus discounts)' => 'Prezzo per articolo (meno gli sconti)', - 'Line item shipping cost' => 'Costo di spedizione per voce', - 'Line item statuses reordered.' => 'Stati delle voci riordinati.', - 'Link Duration' => 'Durata link', - 'Link Sent' => 'Link inviato', - 'Link to a product' => 'Collega a un prodotto', - 'Link to a variant' => 'Collega a una variante', - 'Link' => 'Collega', - 'Live' => 'Pubblicato', - 'Location' => 'Sede', - 'Locations that should be available for previewing products in this product type.' => 'Posizioni che dovrebbero essere disponibili per l\'anteprima dei prodotti in questo tipo di prodotto.', - 'MM' => 'MM', - 'Make a payment' => 'Effettua un pagamento', - 'Make this the primary store' => 'Renderlo lo store principale', - 'Manage Inventory' => 'Gestisci inventario', - 'Manage donation settings' => 'Gestisci impostazioni di donazione', - 'Manage general store settings' => 'Gestisci impostazioni dello store generale', - 'Manage inventory locations' => 'Gestisci sedi dell\'inventario', - 'Manage inventory stock levels' => 'Gestisci livelli di stock dell\'inventario', - 'Manage inventory transfers' => 'Gestisci trasferimenti dell\'inventario', - 'Manage orders' => 'Gestisci ordini', - 'Manage payment currencies' => 'Gestisci valute di pagamento', - 'Manage promotions' => 'Gestisci promozioni', - 'Manage shipping' => 'Gestisci spedizione', - 'Manage store settings' => 'Gestisci impostazioni dello store', - 'Manage subscription plans' => 'Gestisci piani di sottoscrizione', - 'Manage subscription' => 'Gestisci sottoscrizione', - 'Manage subscriptions' => 'Gestisci sottoscrizioni', - 'Manage taxes' => 'Gestisci imposte', - 'Manage' => 'Gestisci', - 'Mark as Pending' => 'Contrassegna come in attesa', - 'Mark as completed' => 'Contrassegna come completato', - 'Match Billing Address' => 'Sconto basato su indirizzo di fatturazione', - 'Match Customer' => 'Sconto basato sul cliente', - 'Match Order' => 'Sconto basato sull\'ordine', - 'Match Orders' => 'Corrispondenza ordini', - 'Match Product' => 'Abbina prodotto', - 'Match Purchasable' => 'Abbina articoli disponibili per l\'acquisto', - 'Match Shipping Address' => 'Sconto basato su indirizzo di spedizione', - 'Match Variant' => 'Abbina variante', - 'Matching Items' => 'Articoli corrispondenti', - 'Max Qty' => 'Quantità massima', - 'Max Uses' => 'Numero max. utilizzi', - 'Max Variants' => 'Varianti massime', - 'Max quantity must greater than min.' => 'La quantità massima deve essere maggiore di quella minima.', - 'Maximum Purchase Quantity' => 'Quantità massima acquistabile', - 'Maximum Total Shipping Cost' => 'Costi di spedizione totali massimi', - 'Maximum allowed quantity' => 'Quantità massima consentita', - 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Numero massimo di articoli corrispondenti ordinabili per applicare questo sconto. Se questo valore è pari a zero, questa condizione verrà saltata.', - 'Maximum order quantity for this item is {num}.' => 'La quantità massima ordinabile per questo articolo è pari a {num}.', - 'Message' => 'Messaggio', - 'Meters (m)' => 'Metri (m)', - 'Millimeters (mm)' => 'Millimetri (mm)', - 'Min Qty' => 'Quantità min.', - 'Min quantity must be less than max.' => 'La quantità minima deve essere minore di quella massima.', - 'Minimum Purchase Quantity' => 'Quantità minima acquistabile', - 'Minimum Total Price Strategy' => 'Strategia prezzo totale minimo', - 'Minimum Total Shipping Cost' => 'Costi di spedizione totali minimi', - 'Minimum allowed quantity' => 'Quantità minima consentita', - 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Numero minimo di articoli corrispondenti che devono essere ordinati per applicare questo sconto.', - 'Minimum order quantity for this item is {num}.' => 'La quantità minima ordinabile per questo articolo è pari a {num}.', - 'Missing Gateway' => 'Gateway mancante', - 'Missing a default inventory location.' => 'Sede predefinita dell\'inventario mancante.', - 'Move Inventory' => 'Sposta inventario', - 'Move To' => 'Sposta in', - 'Move {qty} from {fromType} to {toType}' => 'Sposta {qty} da {fromType} a {toType}', - 'Move' => 'Sposta', - 'Movement from deactivated inventory location' => 'Spostamento dalla sede dell\'inventario disattivata', - 'Movement' => 'Spostamento', - 'Must have at least one variant.' => 'Deve avere almeno una variante.', - 'Name Field' => 'Campo Nome', - 'Name' => 'Nome', - 'New Customer' => 'Nuovo cliente', - 'New Customers' => 'Nuovi clienti', - 'New Order' => 'Nuovo ordine', - 'New PDF' => 'Nuovo PDF', - 'New address' => 'Nuovo indirizzo', - 'New catalog pricing rule' => 'Nuova regola di prezzo in catalogo', - 'New currency' => 'Nuova valuta', - 'New discount' => 'Nuovo sconto', - 'New email' => 'Nuova email', - 'New gateway' => 'Nuovo gateway', - 'New line item status' => 'Nuovo stato voce', - 'New line items get this status by default when the order is completed' => 'Le nuove voci ricevono questo stato per impostazione predefinita al completamento dell\'ordine', - 'New location' => 'Nuova sede', - 'New order status' => 'Nuovo stato ordine', - 'New orders get this status by default' => 'I nuovi ordini passano a questo stato per impostazione predefinita', - 'New product type' => 'Nuovo tipo di prodotto', - 'New product' => 'Nuovo prodotto', - 'New product, choose a type' => 'Nuovo prodotto, scegli un tipo', - 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'I nuovi prodotti sono impostati come predefinito sulla prima categoria fiscale disponibile. Se nessuna è disponibile, è usata questa categoria.', - 'New sale' => 'Nuova vendita promozionale', - 'New shipping category' => 'Nuova categoria di spedizione', - 'New shipping method' => 'Nuovo metodo di spedizione', - 'New shipping rule' => 'Nuova regola di spedizione', - 'New shipping zone' => 'Nuova area di spedizione', - 'New subscription plan' => 'Nuovo piano di sottoscrizione', - 'New tax category' => 'Nuova categoria fiscale', - 'New tax rate' => 'Nuova aliquota fiscale', - 'New tax zone' => 'Nuova zona fiscale', - 'New transfer' => 'Nuovo trasferimento', - 'New {productType} product' => 'Nuovo prodotto {productType}', - 'New' => 'Nuovo', - 'Next payment' => 'Pagamento successivo', - 'No Address' => 'Nessun indirizzo', - 'No PDFs exist yet.' => 'Non esiste ancora nessun PDF.', - 'No access given to any specific store management features.' => 'Non è consentito l\'accesso ad alcuna funzione specifica di gestione dello store.', - 'No additional payment currencies exist yet.' => 'Non esiste ancora nessuna valuta di pagamento aggiuntiva.', - 'No address' => 'Nessun indirizzo', - 'No billing address' => 'Nessun indirizzo di fatturazione', - 'No catalog pricing rule exists with the ID “{id}”' => 'Nessuna regola di prezzo in catalogo esistente con ID “{id}”', - 'No catalog pricing rules exist yet.' => 'Non esiste ancora nessuna regola di prezzo in catalogo.', - 'No currency exists with the ID “{id}”' => 'Nessuna valuta esistente con ID “{id}”', - 'No customer email address exists on this cart.' => 'Non esiste nessun indirizzo email cliente in questo carrello.', - 'No description' => 'Nessuna descrizione', - 'No discount exists with the ID “{id}”' => 'Nessuno sconto esistente con ID “{id}”', - 'No discounts exist yet.' => 'Non esiste ancora nessuno sconto.', - 'No donation amount supplied.' => 'Nessun importo di donazione fornito.', - 'No emails exist yet.' => 'Non esiste ancora nessuna email.', - 'No inventory changes made.' => 'Non sono state apportate modifiche all\'inventario.', - 'No inventory found.' => 'Non è stato trovato alcun inventario.', - 'No inventory movements made.' => 'Non sono stati effettuati spostamenti in inventario.', - 'No inventory transactions for this location.' => 'Nessuna transazione di inventario per questa sede.', - 'No new customer selected.' => 'Non è stato selezionato alcun nuovo cliente.', - 'No order history exists with the ID “{id}”' => 'Nessuno storico ordini esistente con ID “{id}”', - 'No order status history items will exist until the cart becomes an order.' => 'Non esisterà alcuna cronologia di stato dell’ordine fino a quando il carrello non diventerà un ordine.', - 'No payment source exists with the ID “{id}”' => 'Nessuna fonte di pagamento esistente con ID “{id}”', - 'No private Note.' => 'Nessuna nota privata.', - 'No product available.' => 'Nessun prodotto disponibile.', - 'No product types exist yet.' => 'Non esiste ancora nessun tipo di prodotto.', - 'No purchasable available.' => 'Nessun prodotto disponibile all’acquisto.', - 'No sale exists with the ID “{id}”' => 'Nessuna vendita promozionale con ID “{id}”', - 'No sales exist yet.' => 'Non esiste ancora nessuna vendita promozionale.', - 'No shipping address' => 'Nessun indirizzo di spedizione', - 'No shipping category exists with the ID “{id}”' => 'Nessuna categoria di spedizione esistente con ID “{id}”', - 'No shipping method exists with the ID “{id}”' => 'Nessun metodo di spedizione esistente con ID “{id}”', - 'No shipping rule exists with the ID “{id}”' => 'Nessuna regola di spedizione esistente con ID “{id}”', - 'No shipping rules exist yet.' => 'Non esiste ancora nessuna regola di spedizione.', - 'No shipping zone exists with the ID “{id}”' => 'Nessuna area di spedizione esistente con ID “{id}”', - 'No stats available.' => 'Statistiche indisponibili.', - 'No subscription plan exists with the ID “{id}”' => 'Nessun piano di sottoscrizione esistente con ID “{id}”', - 'No subscription plans exist yet.' => 'Non esiste ancora nessun piano di sottoscrizione.', - 'No tax category exists with the ID “{id}”' => 'Nessuna categoria fiscale esistente con ID “{id}”', - 'No tax rate exists with the ID “{id}”' => 'Nessuna aliquota fiscale esistente con ID “{id}”', - 'No tax zone exists with the ID “{id}”' => 'Nessuna zona fiscale esistente con ID “{id}”', - 'No transactions exist.' => 'Nessuna transazione esistente.', - 'No user authenticated.' => 'Nessun utente autenticato.', - 'No' => 'No', - 'None on hand' => 'Nessuno a disposizione', - 'None' => 'Nessuno', - 'Not a valid address type' => 'Tipo di indirizzo non valido', - 'Not a valid credit card number.' => 'Numero di carta di credito non valido.', - 'Not all SKUs are unique.' => 'Non tutti gli SKU sono univoci.', - 'Note' => 'Nota', - 'Notes' => 'Note', - 'Number of Coupons' => 'Numero di codici promozionali', - 'Number' => 'Numero', - 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Dei siti abilitati riportati sopra, in quali siti devono essere salvati i prodotti in questo tipo di prodotto?', - 'On Hand' => 'Disponibile', - 'Only allow this gateway to be used for zero value orders?' => 'Consenti l’utilizzo di questo gateway solo per gli ordini di valore zero?', - 'Only match certain purchasables…' => 'Solo corrispondenza con determinati articoli disponibili all\'acquisto…', - 'Only match purchasables related to…' => 'Solo corrispondenza con articoli disponibili all\'acquisto relativi a…', - 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Solo gli ordini con il seguente stato saranno inclusi. Lasciare vuoto per consentire tutti gli stati.', - 'Only save product to the site they were created in' => 'Salva prodotti esclusivamente sul sito di creazione', - 'Options' => 'Opzioni', - 'Order Condition Formula' => 'Formula con condizione di ordine', - 'Order Description Format' => 'Formato descrizione ordine', - 'Order Details' => 'Dettagli ordine', - 'Order Fields' => 'Campi ordine', - 'Order PDF Download Link' => 'Link di download PDF ordine', - 'Order PDF Filename Format' => 'Formato nome file PDF ordine', - 'Order Reference Number Format' => 'Formato numero di riferimento ordine', - 'Order Settings' => 'Impostazioni ordine', - 'Order Site' => 'Sito dell\'ordine', - 'Order Status description.' => 'Descrizione stato ordine.', - 'Order Status' => 'Stato dell’ordine', - 'Order Statuses' => 'Stati ordine', - 'Order can not be empty.' => 'Il campo dell\'ordine non può essere vuoto.', - 'Order count' => 'Conteggio dell\'ordine', - 'Order customer data removed.' => 'Dati del cliente dell\'ordine rimossi.', - 'Order deleted.' => 'Ordine eliminato.', - 'Order fields saved.' => 'Campi dell\'ordine salvati.', - 'Order not found.' => 'Ordine non trovato.', - 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Il saldo del pagamento dell\'ordine è {outstandingBalanceAsCurrency}. Questo è il valore massimo che verrà addebitato.', - 'Order recalculated.' => 'Ordine ricalcolato.', - 'Order status saved.' => 'Stato dell’ordine salvato.', - 'Order statuses reordered.' => 'Stati degli ordini riordinati.', - 'Order total shipping cost' => 'Costi di spedizione totali dell’ordine', - 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Prezzo imponibile totale ordine (Subtotale articoli + Sconti totali + Spedizione totale)', - 'Order' => 'Ordine', - 'Orders (Legacy)' => 'Ordini (Legacy)', - 'Orders deleted.' => 'Ordini eliminati.', - 'Orders not restored.' => 'Ordini non ripristinati.', - 'Orders restored.' => 'Ordini ripristinati.', - 'Orders' => 'Ordini', - 'Organization Name' => 'Nome organizzazione', - 'Organization Tax ID' => 'Partita IVA organizzazione', - 'Origin and destination cannot be the same.' => 'L\'origine e la destinazione non possono essere identiche.', - 'Origin' => 'Origine', - 'Original Price' => 'Prezzo originale', - 'Original price' => 'Prezzo originale', - 'Original promotional price' => 'Prezzo promozionale originale', - 'Other Languages' => 'Altre lingue', - 'Other countries' => 'Altri Paesi', - 'Outgoing transfer from Transfer ID: ' => 'Trasferimento in uscita dall\'ID di trasferimento: ', - 'Overpaid' => 'Pagamento in eccesso', - 'Overrides previous?' => 'Sostituisce i dati precedenti?', - 'PDF Attachment' => 'Allegato PDF', - 'PDF Template Path' => 'Percorso al template PDF', - 'PDF saved.' => 'PDF salvato.', - 'PDF' => 'PDF', - 'PDFs & Emails' => 'PDF e e-mail', - 'PDFs' => 'PDF', - 'Paid Amount' => 'Importo pagato', - 'Paid Status' => 'Stato pagamenti effettuati', - 'Paid' => 'Pagato', - 'Paper Orientation' => 'Orientamento foglio', - 'Paper Size' => 'Dimensioni foglio', - 'Partial payment not allowed.' => 'Pagamento parziale non consentito.', - 'Partial' => 'Parziale', - 'Past year' => 'L\'anno scorso', - 'Past {num} days' => '{num} giorni trascorsi', - 'Pay {amount} of {currency} on the order.' => 'Paga {amount} in {currency} sull\'ordine.', - 'Pay' => 'Paga', - 'Payment Amount' => 'Importo pagato', - 'Payment Currencies' => 'Valute di pagamento', - 'Payment Gateway' => 'Gateway di pagamento', - 'Payment Method' => 'Metodo di pagamento', - 'Payment error: {message}' => 'Errore di pagamento: {message}', - 'Payment method issue' => 'Problema con il metodo di pagamento', - 'Payment source created.' => 'Fonte di pagamento creata.', - 'Payment source deleted.' => 'Fonte di pagamento eliminata.', - 'Payments' => 'Pagamenti', - 'Pending' => 'In attesa', - 'Per Email Address Discount Limit' => 'Limite di sconto per indirizzo email', - 'Per Item Amount Off' => 'Importo di sconto per articolo', - 'Per Item Discount' => 'Sconto per articolo', - 'Per Item Percentage Off' => 'Percentuale di sconto per articolo', - 'Per Item Rate' => 'Tariffa in base ad articolo', - 'Per User Discount Limit' => 'Limite di sconto per utente', - 'Percentage Rate' => 'Tariffa in base a percentuale', - 'Phone (Alt)' => 'N. telefono (alt)', - 'Phone' => 'N. telefono', - 'Pick a plan' => 'Scegli un piano', - 'Plain Text Email Template Path' => 'Percorso template email di testo semplice', - 'Plan' => 'Piano', - 'Plans reordered.' => 'Piani riordinati.', - 'Portrait' => 'Verticale', - 'Post Date' => 'Data di pubblicazione', - 'Postal Code Formula' => 'Formula codice postale', - 'Pounds (lb)' => 'Libbre (lb)', - 'Preview' => 'Anteprima', - 'Previous Status' => 'Stato precedente', - 'Price' => 'Prezzo', - 'Prices' => 'Prezzi', - 'Pricing Rules' => 'Regole di prezzo', - 'Pricing jobs are currently running.' => 'Le attività di prezzo sono attualmente in corso.', - 'Pricing' => 'Prezzo', - 'Primary Billing Address' => 'Indirizzo di fatturazione principale', - 'Primary Shipping Address' => 'Indirizzo di spedizione principale', - 'Primary payment source updated.' => 'Fonte di pagamento principale aggiornata.', - 'Primary' => 'Principale', - 'Private Note' => 'Nota privata', - 'Product Fields' => 'Campi prodotto', - 'Product ID is required.' => 'È richiesto un ID prodotto.', - 'Product Template' => 'Template prodotto', - 'Product Title Format' => 'Formato titolo prodotto', - 'Product Type' => 'Tipo di prodotto', - 'Product Types' => 'Tipi di prodotti', - 'Product URI Format' => 'Formato URL prodotto', - 'Product Variant' => 'Variante prodotto', - 'Product Variants' => 'Varianti prodotto', - 'Product type saved.' => 'Tipo di prodotto salvato.', - 'Product type settings' => 'Impostazioni tipo di prodotto', - 'Product' => 'Prodotto', - 'Products and Variants deleted.' => 'Prodotti e Varianti eliminati.', - 'Products not restored.' => 'Prodotti non ripristinati.', - 'Products restored.' => 'Prodotti ripristinati.', - 'Products' => 'Prodotti', - 'Promotable' => 'Promozione applicabile', - 'Promotable?' => 'È possibile applicare promozioni?', - 'Promotional Amount' => 'Importo promozionale', - 'Promotional Price' => 'Prezzo promozionale', - 'Purchasable Categories' => 'Categorie disponibili per l\'acquisto', - 'Purchasable ID and Sale ID are required.' => 'Sono richiesti un ID disponibile all’acquisto e un ID di vendita promozionale.', - 'Purchasable ID is required.' => 'È richiesto un ID disponibile all’acquisto.', - 'Purchasable Type' => 'Tipo disponibile per l’acquisto', - 'Purchasable' => 'Disponibile per l’acquisto', - 'Purchase (Authorize and Capture Immediately)' => 'Acquista (autorizza e acquisisci immediatamente)', - 'Purchase Total' => 'Totale acquisto', - 'Qty' => 'Qtà', - 'Quality Control' => 'Controllo qualità', - 'Quantity' => 'Quantità', - 'Rate' => 'Tasso', - 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Riassegna {numOrders, plural, one {}=1{ordine} other{ordini}}', - 'Recalculate order' => 'Ricalcola ordine', - 'Receive Inventory' => 'Ricevi inventario', - 'Receive Transfer' => 'Ricevi trasferimento', - 'Receive' => 'Ricevi', - 'Received' => 'Ricevuto', - 'Recent Orders' => 'Ordini recenti', - 'Recipient' => 'Destinatario', - 'Recover Cart' => 'Recupera carrello', - 'Reduce price' => 'Riduci prezzo', - 'Reduce the price by a fixed amount' => 'Riduci il prezzo di un importo fisso', - 'Reduce the price by a percentage of the original price' => 'Riduci il prezzo originale di una determinata percentuale', - 'Reference' => 'Riferimento', - 'Refresh payment history' => 'Aggiorna cronologia pagamenti', - 'Refund note' => 'Nota di rimborso', - 'Refund payment' => 'Rimborsa pagamento', - 'Refund' => 'Rimborso', - 'Reject' => 'Rifiuta', - 'Rejected' => 'Rifiutato', - 'Relationship Type' => 'Tipo di relazione', - 'Removable included tax rates are only allowed for the default tax zone.' => 'Le aliquote fiscali rimovibili incluse sono consentite solo per la zona fiscale predefinita.', - 'Remove address' => 'Rimuovi indirizzo', - 'Remove all shipping costs from the order' => 'Rimuovi tutti i costi di spedizione dall\'ordine', - 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Rimuovi l\'associazione al cliente e l\'indirizzo e-mail da {numOrders, plural, one {}=1{ordine} other{ordini}}. Puoi scegliere di selezionare ulteriori dati del cliente da rimuovere qui sotto', - 'Remove customer data' => 'Rimuovi dati dei clienti', - 'Remove from price?' => 'Rimuovere dal prezzo?', - 'Remove shipping costs for matching items only' => 'Rimuovi i costi di spedizione solo per gli articoli corrispondenti', - 'Remove the included tax when a valid organization tax ID is present?' => 'Rimuovere l\'aliquota inclusa quando una partita IVA aziendale valida è presente?', - 'Remove' => 'Rimuovi', - 'Removed' => 'Rimosso', - 'Repeat Customers' => 'Clienti abituali', - 'Reply To' => 'Rispondi a', - 'Require Billing Address At Checkout' => 'Richiedi l\'indirizzo di fatturazione al checkout', - 'Require Coupon Code' => 'Richiedi codice coupon', - 'Require Shipping Address At Checkout' => 'Richiedi l\'indirizzo di spedizione al checkout', - 'Require Shipping Method Selection At Checkout' => 'Richiedi la selezione del metodo di spedizione al checkout', - 'Require' => 'Richiedi', - 'Reserved' => 'Riservato', - 'Reset usage' => 'Ripristina utilizzo', - 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Limita questo sconto solo agli ordini in cui il cliente ha acquistato un valore totale minimo di articoli corrispondenti.', - 'Revenue Options' => 'Opzioni di ricavo', - 'Revenue' => 'Ricavo', - 'Rule' => 'Regola', - 'Rules reordered.' => 'Regole riordinate.', - 'SKU' => 'SKU', - 'Safety' => 'Sicurezza', - 'Sale Price' => 'Prezzo di vendita promozionale', - 'Sale description.' => 'Descrizione vendita promozionale.', - 'Sale reordered.' => 'Vendita promozionale riordinata.', - 'Sale saved.' => 'Vendita promozionale salvata.', - 'Sale' => 'Vendita promozionale', - 'Sales deleted.' => 'Vendite promozionali eliminate.', - 'Sales updated.' => 'Vendite promozionali aggiornate.', - 'Sales' => 'Vendite promozionali', - 'Save and continue editing' => 'Salva e continua modifiche', - 'Save and return to all orders' => 'Salva e torna a tutti gli ordini', - 'Save and set rules' => 'Salva e imposta regole', - 'Save as a new rule' => 'Salva come nuova regola', - 'Save product to all sites enabled for this product type' => 'Salva il prodotto in tutti i siti abilitati per questo tipo di prodotto', - 'Save product to other sites in the same site group' => 'Salva il prodotto su altri siti nello stesso gruppo di siti', - 'Save product to other sites with the same language' => 'Salva il prodotto su altri siti con la stessa lingua', - 'Save' => 'Salva', - 'Search customer…' => 'Cerca cliente…', - 'Search inventory' => 'Ricerca in inventario', - 'Search or enter customer email…' => 'Cerca o immetti l\'e-mail del cliente...', - 'Search…' => 'Ricerca...', - 'See Orders' => 'Visualizza ordini', - 'Select a gateway' => 'Seleziona un gateway', - 'Select a tax category.' => 'Selezionare una categoria fiscale.', - 'Select a tax zone. If empty, this rate will match anywhere.' => 'Seleziona una zona fiscale. Se l\'opzione è lasciata vuota, il tasso corrisponderà ovunque.', - 'Select address' => 'Seleziona indirizzo', - 'Select an item' => 'Seleziona un articolo', - 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Seleziona la modalità di applicazione della regola di prezzo in catalogo al/i prodotto/i disponibile/i per l\'acquisto.', - 'Select how the sale will be applied to the purchasable(s).' => 'Scegli come applicare la vendita all’oggetto acquistabile/agli oggetti acquistabili.', - 'Select product type' => 'Seleziona tipo di prodotto', - 'Select the emails that will be sent when transitioning to this status.' => 'Selezionare le email per l’invio durante la transizione a questo stato.', - 'Select what this rate should be applied to.' => 'Seleziona dove applicare questa aliquota.', - 'Send Email' => 'Invia email', - 'Send to custom recipient' => 'Invia a destinatario personalizzato', - 'Send to the customer' => 'Invia al cliente', - 'Set Quantity' => 'Imposta quantità', - 'Set default category' => 'Imposta categoria predefinita', - 'Set default variant' => 'Imposta variante predefinita', - 'Set or Adjust' => 'Imposta o rettifica', - 'Set price' => 'Imposta prezzo', - 'Set status' => 'Imposta stato', - 'Set the price to a flat amount' => 'Imposta il prezzo su un importo forfettario', - 'Set the price to a percentage of the original price' => 'Imposta il prezzo su una percentuale del prezzo originale', - 'Set the sale price to a flat amount' => 'Imposta il prezzo di vendita su un importo forfettario', - 'Set the sale price to a percentage of the original price' => 'Imposta il prezzo di vendita su una percentuale del prezzo originale', - 'Set to' => 'Imposta su', - 'Settings saved.' => 'Impostazioni salvate.', - 'Settings' => 'Impostazioni', - 'Share cart…' => 'Condividi carrello...', - 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Spedizione - Il costo minimo è il costo di spedizione applicato se il prezzo dell\'ordine è inferiore al costo di spedizione.', - 'Shipping Address Zone' => 'Zona dell\'indirizzo di spedizione', - 'Shipping Address' => 'Indirizzo di spedizione', - 'Shipping Business Name' => 'Nome azienda per la spedizione', - 'Shipping Categories' => 'Categorie di spedizione', - 'Shipping Category Conditions' => 'Condizioni della categoria di spedizione', - 'Shipping Category' => 'Categoria di spedizione', - 'Shipping First Name' => 'Nome di spedizione', - 'Shipping Full Name' => 'Nome completo di spedizione', - 'Shipping Last Name' => 'Cognome di spedizione', - 'Shipping Method' => 'Metodo di spedizione', - 'Shipping Methods' => 'Metodi di spedizione', - 'Shipping Rule' => 'Regola di spedizione', - 'Shipping Zones' => 'Aree di spedizione', - 'Shipping address required.' => 'Indirizzo di spedizione obbligatorio.', - 'Shipping categories deleted.' => 'Categorie di spedizione eliminate.', - 'Shipping category saved.' => 'Categoria di spedizione salvata.', - 'Shipping category updated.' => 'Categoria di spedizione aggiornata.', - 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Costi di spedizione aggiunti all\'ordine come totale prima dell\'applicazione delle percentuali, dell\'articolo e delle tariffe in base al peso. Imposta su zero per disabilitare questa tariffa. L\'intera regola, tasso base incluso, non è valida e applicata se il carrello contiene esclusivamente articoli non soggetti a spedizione, come ad esempio prodotti digitali.', - 'Shipping method saved.' => 'Metodo di spedizione salvato.', - 'Shipping methods and rules deleted.' => 'Metodi di spedizione e regole eliminati.', - 'Shipping methods updated.' => 'Metodi di spedizione aggiornati.', - 'Shipping rule saved.' => 'Regola di spedizione salvata.', - 'Shipping zone saved.' => 'Area di spedizione salvata.', - 'Shipping' => 'Spedizione', - 'Short Number' => 'Numero breve', - 'Show Chart?' => 'Mostrare il grafico?', - 'Show Order Count?' => 'Mostrare il conteggio dell\'ordine?', - 'Show all prices' => 'Mostra tutti i prezzi', - 'Show archived gateways' => 'Mostra gateway archiviati', - 'Show order count line on chart.' => 'Mostra la riga di conteggio dell\'ordine sul grafico.', - 'Show related sales' => 'Mostra vendite collegate', - 'Show rule details' => 'Mostra dettagli della regola', - 'Show the Dimensions and Weight fields for products of this type' => 'Mostra i campi Dimensioni e Peso per i prodotti di questo tipo', - 'Show the Title field for products' => 'Mostra il campo Titolo per i prodotti', - 'Show the Title field for variants' => 'Mostra il campo Titolo per le varianti', - 'Signed In' => 'Accesso riuscito', - 'Site Languages' => 'Lingue del sito', - 'Site store mapping saved.' => 'Mappatura dello store del sito salvata.', - 'Sites' => 'Siti', - 'Slug' => 'Slug', - 'Snapshot' => 'Snapshot', - 'Snapshots' => 'Snapshot', - 'Some orders restored.' => 'Alcuni ordini ripristinati.', - 'Some products restored.' => 'Alcuni prodotti ripristinati.', - 'Some variants restored.' => 'Alcune varianti ripristinate.', - 'Something changed with the order before payment, please review your order and submit payment again.' => 'È cambiato qualcosa nell’ordine prima del pagamento. Controllare l’ordine e inviare nuovamente il pagamento.', - 'Sorry, no matching options.' => 'Spiacente, nessuna opzione corrispondente.', - 'Source - The purchasable relationship field is on the category' => 'Fonte - Il campo relazione prodotti disponibili all\'acquisto si trova sulla categoria', - 'Source' => 'Fonte', - 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Specifica una condizione Twig che determina se lo sconto deve essere applicato a un determinato ordine. (L\'ordine può essere referenziato tramite una variabile `order`.)', - 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Specifica una condizione Twig che determina se le regole di spedizione devono essere applicate a un determinato ordine. (L\'ordine può essere referenziato tramite una variabile `order`.)', - 'Start Date' => 'Data di inizio', - 'State' => 'Stato', - 'Status Email Address' => 'Indirizzo email stato', - 'Status Emails' => 'Email stato', - 'Status History' => 'Cronologia stato', - 'Status Updated.' => 'Stato aggiornato.', - 'Status change message' => 'Messaggio modifica stato', - 'Status' => 'Stato', - 'Stock' => 'Stock', - 'Stops Processing?' => 'Interrompe l’elaborazione?', - 'Stops subsequent?' => 'Interrompe i dati successivi?', - 'Store Location' => 'Posizione store', - 'Store Management' => 'Gestione dello store', - 'Store Markets' => 'Paesi in cui lo store vende', - 'Store Rule' => 'Regola dello store', - 'Store saved.' => 'Store salvato.', - 'Store' => 'Store', - 'Stores & Sites' => 'Store e siti', - 'Stores' => 'Store', - 'Strategy to apply when an order is free or has a zero balance.' => 'Strategia da applicare quando un ordine è gratuito o ha un saldo pari a zero.', - 'Strategy to apply when calculating the minimum order price.' => 'Strategia da applicare durante il calcolo del prezzo d\'ordine minimo.', - 'Subject' => 'Oggetto', - 'Subscribing user' => 'Utente che effettua la sottoscrizione', - 'Subscription Fields' => 'Campi della sottoscrizione', - 'Subscription Plans' => 'Piani di sottoscrizione', - 'Subscription Settings' => 'Impostazioni di sottoscrizione', - 'Subscription cancelled.' => 'Sottoscrizione annullata.', - 'Subscription date' => 'Data sottoscrizione', - 'Subscription fields saved.' => 'Campi di sottoscrizione salvati.', - 'Subscription for {user} to {plan} prevented by a plugin.' => 'Sottoscrizione per {user} a {plan} impedita da un plug-in.', - 'Subscription plan saved.' => 'Piano di sottoscrizione salvato.', - 'Subscription plan' => 'Piano di sottoscrizione', - 'Subscription plans' => 'Piani di sottoscrizione', - 'Subscription reactivated.' => 'Sottoscrizione riattivata.', - 'Subscription reference' => 'Riferimento della sottoscrizione', - 'Subscription started.' => 'Sottoscrizione avviata.', - 'Subscription switched.' => 'Sottoscrizione modificata.', - 'Subscription to “{plan}”' => 'Sottoscrizione di “{plan}”', - 'Subscription' => 'Sottoscrizione', - 'Subscriptions on hold' => 'Sottoscrizioni in sospeso', - 'Subscriptions' => 'Sottoscrizioni', - 'Suppress emails' => 'Nascondi email', - 'Switch plan' => 'Cambia piano', - 'Switch' => 'Cambia', - 'System' => 'Sistema', - 'Table Columns' => 'Colonne tabella', - 'Target - The category relationship field is on the purchasable' => 'Obiettivo - Il campo della relazione di categoria è sul prodotto disponibile all\'acquisto', - 'Tax & Shipping' => 'Imposta e spedizione', - 'Tax (inc)' => 'Imposta (inc.)', - 'Tax Categories' => 'Categorie fiscali', - 'Tax Category' => 'Categoria fiscale', - 'Tax Rates' => 'Aliquote fiscali', - 'Tax Zone' => 'Zona fiscale', - 'Tax Zones' => 'Zone fiscali', - 'Tax categories deleted.' => 'Categorie fiscali eliminate.', - 'Tax category saved.' => 'Categoria fiscale salvata.', - 'Tax category updated.' => 'Categoria fiscale aggiornata.', - 'Tax rate saved.' => 'Aliquota fiscale salvata.', - 'Tax rates updated.' => 'Aliquote fiscali aggiornate.', - 'Tax zone saved.' => 'Zona fiscale salvata.', - 'Tax' => 'Imposta', - 'Taxable Subject' => 'Imponibile', - 'Template Path' => 'Percorso al template', - 'That handle is already in use' => 'Handle già in uso', - 'That handle is already in use.' => 'Handle già in uso.', - 'The PDF to attach to this email.' => 'Il PDF da allegare a questa e-mail.', - 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'L\'url della pagina contenente la sezione di aggiornamento dei dettagli di fatturazione di una sottoscrizione e di gestione dell\'autenticazione 3DS.', - 'The address provided is outside the store’s market.' => 'L\'indirizzo fornito è al di fuori del mercato dello store.', - 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'L\'importo dello sconto che viene applicato all\'intero ordine. Questo importo è ripartito tra le varie voci in ordine di prezzo da quello più elevato a quello più ridotto, fino ad esaurimento dello sconto.', - 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'Lo sconto di base può solo scontare fino a zero gli articoli nel carrello fino ad esaurimento; non può rendere negativo l\'ordine.', - 'The cart recovery link is invalid. Please request a new one.' => 'Il link per il recupero del carrello non è valido. Richiedine uno nuovo.', - 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Tasso di conversione che sarà usato per convertire un importo in questa valuta. Per esempio, se un articolo costa {amount1}, un tasso di conversione del {rate} darà come risultato {amount2} nella valuta alternativa.', - 'The countries that orders are allowed to be placed from.' => 'Paesi da cui è consentito inviare ordini.', - 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'Il coupon “{code}” ha superato il limite di utilizzo di {limit}.', - 'The customer for this order has been deleted.' => 'Il cliente associato a questo ordine è stato eliminato.', - 'The default shipping category is automatically available to all product types.' => 'La categoria di spedizione predefinita è automaticamente disponibile per tutti i tipi di prodotto.', - 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'Lo sconto “{name}” ha superato il limite totale di utilizzo di {limit}.', - 'The download link has expired. Please request a new one.' => 'Il link di download è scaduto. Richiedine uno nuovo.', - 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'Indirizzo email da cui vengono inviate le email di stato dell’ordine. Lasciare vuoto per utilizzare l’indirizzo email di sistema definito nelle Impostazioni generali di Craft.', - 'The entry that contains the description for this subscription’s plan.' => 'La Voce che contiene la descrizione di questo piano di sottoscrizione.', - 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'Importo forfettario corrispondente allo sconto applicabile a ciascun articolo, ovvero “3” per uno sconto di 3 $ su ciascun articolo.', - 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'Formato usato per generare nuovi codici promozionali, ad es., {example}. Eventuali caratteri `#` saranno sostituiti da lettere casuali.', - 'The from and to inventory locations must be different.' => 'Le sedi dell\'inventario da e verso devono essere diverse.', - 'The inventory locations this store uses.' => 'Le sedi dell\'inventario utilizzate da questo store.', - 'The item is not enabled for sale.' => 'L’articolo non è abilitato alla vendita.', - 'The language the order was made in.' => 'La lingua in cui è stato effettuato l\'ordine.', - 'The language to be used when this email is rendered.' => 'La lingua da utilizzare quando viene visualizzata questa email.', - 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Il numero massimo di livelli che questo tipo di prodotto può avere. Lasciare in bianco se non ha importanza.', - 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Importo massimo che il cliente dovrebbe spendere per la spedizione. Impostare su zero per disabilitare.', - 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Importo minimo che il cliente dovrebbe spendere per la spedizione. Impostare su zero per disabilitare.', - 'The order is not valid.' => 'L\'ordine non è valido.', - 'The payment gateway that will be used for the subscription plan.' => 'Quale gateway di pagamento verrà utilizzato per il piano di sottoscrizione.', - 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'Il valore percentile da scontare per ogni articolo, cioè {ex1} per il {ex2} di sconto. Le percentuali sono arrotondate a 2 decimali.', - 'The previously-selected shipping method is no longer available.' => 'Il metodo di spedizione selezionato in precedenza non è più disponibile.', - 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Il prezzo di {description} è aumentato da {originalSalePriceAsCurrency} a {newSalePriceAsCurrency}', - 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Il prezzo di {description} è diminuito da {originalSalePriceAsCurrency} a {newSalePriceAsCurrency}', - 'The primary currency cannot be changed after orders are placed.' => 'La valuta principale non può essere modificata dopo il completamento degli ordini.', - 'The purchasable defines the relationship' => 'L\'articolo disponibile all\'acquisto definisce la relazione', - 'The purchasable is related by another element' => 'L\'articolo disponibile all\'acquisto è correlato a un altro elemento', - 'The recipient of the email. Twig code can be used here.' => 'Il destinatario dell\'email. Qui è possibile utilizzare il codice Twig.', - 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'L\'indirizzo e-mail di risposta. Lasciare vuoto questo campo per rispondere con l\'indirizzo standard al mittente dell\'email. Qui è possibile utilizzare il codice Twig.', - 'The site the order was made in.' => 'Il sito in cui è stato effettuato l\'ordine.', - 'The site to be used when this email is rendered.' => 'Il sito da utilizzare quando viene visualizzata questa email.', - 'The subject line of the email. Twig code can be used here.' => 'L\'oggetto dell\'email. Qui è possibile utilizzare il codice Twig.', - 'The template that the PDF should be generated from.' => 'Il template da cui deve essere generato il PDF.', - 'The template to be used for HTML emails.' => 'Template da usare per le email HTML.', - 'The template to be used for plain text emails. Twig code can be used here.' => 'Il template da usare per le email di testo semplice. Qui è possibile utilizzare il codice Twig.', - 'The template to use when a product’s URL is requested.' => 'Template da usare quando è richiesto l’URL di un prodotto.', - 'The total number of order adjustments changed.' => 'Numero totale di rettifiche ordine modificate.', - 'The total price of the order changed.' => 'Prezzo totale dell’ordine modificato.', - 'The total quantity of items within the order changed.' => 'Quantità totale di articoli nell’ordine modificata.', - 'The unique SKU of the donation purchasable.' => 'SKU univoco della donazione disponibile all\'acquisto.', - 'The unit of measurement that should be used when specifying product dimensions.' => 'Unità di misura che dovrebbe essere utilizzata quando si specificano le dimensioni dei prodotti.', - 'The unit of measurement that should be used when specifying product weights.' => 'Unità di misura che dovrebbe essere utilizzata quando si specificano i pesi dei prodotti.', - 'The webhook URL for this gateway.' => 'L’URL del webhook per questo gateway.', - 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'Nome “Da” che verrà utilizzato per l’invio delle email di stato dell’ordine. Lasciare vuoto per usare il nome del mittente definito nelle Impostazioni generali di Craft.', - 'There are errors on the order' => 'L\'ordine contiene degli errori', - 'There are only {num} “{description}” items left in stock.' => 'Ci sono solo {num} articoli “{description}” in stock.', - 'There aren’t any product types to select yet.' => 'Non ci sono ancora tipi di prodotto da selezionare.', - 'There is no gateway or payment source available for use with this order.' => 'Non è disponibile un gateway o una fonte di pagamento utilizzabile con questo ordine.', - 'There is no gateway selected that supports payment sources.' => 'Non è stato selezionato nessun gateway che supporti le fonti di pagamento.', - 'There is no shipping method selected for this order.' => 'Non è stato selezionato alcun metodo di spedizione per questo ordine.', - 'This URL will load the cart into the user’s session, making it the active cart.' => 'Questo URL caricherà il carrello nella sessione dell\'utente, rendendolo il carrello attivo.', - 'This action is not allowed for the current user.' => 'Azione non consentita per l’utente corrente.', - 'This category will be used as the default for all purchasables in this store.' => 'Questa categoria verrà utilizzata come predefinita per tutti gli articoli disponibili all’acquisto in questo store.', - 'This coupon is for registered users and limited to {limit} uses.' => 'Questo coupon è riservato agli utenti registrati e limitato a {limit} utilizzi.', - 'This coupon is limited to {limit} uses.' => 'Questo coupon è limitato a {limit} utilizzi.', - 'This coupon requires an email address.' => 'Questo coupon richiede un indirizzo e-mail.', - 'This gateway does not support that functionality.' => 'Questo gateway non supporta tale funzionalità.', - 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Questo è stato escluso dalle impostazioni di configurazione {setting} in `config/{file}.php`.', - 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Questo è l’indirizzo del tuo store. Potrebbe essere utilizzato da vari plug-in per la determinazione, ad esempio, della spedizione e delle imposte. Potrebbe anche essere utilizzato nelle ricevute PDF.', - 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Questo è il PDF predefinito che verrà visualizzato quando si richiede il PDF dell\'ordine.', - 'This is the last location for the {store} store.' => 'Questa è l\'ultima sede dello store {store}.', - 'This month' => 'Questo mese', - 'This order has unsaved changes.' => 'Quest\'ordine ha modifiche non salvate.', - 'This week' => 'Questa settimana', - 'This year' => 'Quest\'anno', - 'Times Used' => 'Numero di utilizzi', - 'Title' => 'Titolo', - 'To' => 'A', - 'Today' => 'Oggi', - 'Too many variants for this product.' => 'Troppe varianti per questo prodotto.', - 'Top Customers by Average Order' => 'Principali clienti per ordine medio', - 'Top Customers by Total Revenue' => 'Principali clienti per ricavi totali', - 'Top Customers' => 'Clienti principali', - 'Top Product Types by Qty Sold' => 'Tipi di prodotti principali più venduti per qtà vendute', - 'Top Product Types by Revenue' => 'Principali tipi di prodotti per ricavo', - 'Top Product Types' => 'Principali tipi di prodotti', - 'Top Products by Qty Sold' => 'Prodotti principali per qtà vendute', - 'Top Products by Revenue' => 'Principali prodotti per ricavo', - 'Top Products' => 'Prodotti principali', - 'Top Purchasables by Qty Sold' => 'Principali prodotti disponibili all\'acquisto per qtà vendute', - 'Top Purchasables by Revenue' => 'Principali prodotti disponibili all\'acquisto per ricavo', - 'Top Purchasables' => 'Principali prodotti disponibili all’acquisto', - 'Total ' => 'Totale ', - 'Total Discount Use Limit' => 'Limite totale di utilizzo sconto', - 'Total Discount' => 'Sconto totale', - 'Total Included Tax' => 'Totale imposta inclusa', - 'Total Orders by Billing Country' => 'Totale ordini per Paese di spedizione', - 'Total Orders by Country' => 'Totale ordini per Paese', - 'Total Orders by Shipping Country' => 'Totale ordini per Paese di spedizione', - 'Total Orders' => 'Ordini totali', - 'Total Paid' => 'Totale pagato', - 'Total Price' => 'Prezzo totale', - 'Total Qty' => 'Quantità totale', - 'Total Revenue' => 'Ricavi totali', - 'Total Shipping' => 'Totale spedizione', - 'Total Tax' => 'Totale imposta', - 'Total Weight' => 'Peso totale', - 'Total' => 'Totale', - 'Track Inventory' => 'Traccia inventario', - 'Transaction Hash' => 'Hash di transazione', - 'Transaction ID' => 'ID transazione', - 'Transaction captured successfully: {message}' => 'Transazione acquisita correttamente: {message}', - 'Transaction refunded successfully: {message}' => 'Transazione rimborsata correttamente: {message}', - 'Transactions' => 'Transazioni', - 'Transfer Fields' => 'Campi di trasferimento', - 'Transfer Items' => 'Voci di trasferimento', - 'Transfer Settings' => 'Impostazioni di trasferimento', - 'Transfer Status' => 'Stato di trasferimento', - 'Transfer fields saved.' => 'Campi di trasferimento salvati.', - 'Transfer must have at least one item.' => 'Il trasferimento deve avere almeno una voce.', - 'Transfer' => 'Trasferimento', - 'Transfers' => 'Trasferimenti', - 'Trial days credited' => 'Giorni di prova concessi', - 'Trial expiration' => 'Scadenza prova', - 'Trial expiry date' => 'Data di scadenza del periodo di prova', - 'Type not in allowed options.' => 'Tipo non incluso nelle opzioni consentite.', - 'Type' => 'Tipo', - 'URI' => 'URI', - 'Unable to cancel subscription at this time.' => 'Al momento non è possibile annullare la sottoscrizione.', - 'Unable to complete order: another request is already in progress.' => 'Impossibile completare l\'ordine: è già in corso un\'altra richiesta.', - 'Unable to find variant.' => 'Impossibile trovare la variante.', - 'Unable to generate coupon codes: {message}' => 'Impossibile generare codici promozionali: {message}', - 'Unable to make payment at this time.' => 'Al momento non è possibile effettuare il pagamento.', - 'Unable to modify subscription at this time.' => 'Al momento non è possibile modificare la sottoscrizione.', - 'Unable to reactivate subscription at this time.' => 'Al momento non è possibile riattivare la sottoscrizione.', - 'Unable to reassign orders.' => 'Impossibile riassegnare gli ordini.', - 'Unable to remove order data.' => 'Impossibile eliminare i dati dell\'ordine.', - 'Unable to retrieve Sale and Purchasable.' => 'Impossibile recuperare Vendita promozionale e Disponibile all’acquisto.', - 'Unable to retrieve cart.' => 'Impossibile recuperare il carrello.', - 'Unable to retrieve customer.' => 'Impossibile recuperare il cliente.', - 'Unable to retrieve load cart URL' => 'Impossibile recuperare l\'URL del carrello caricato', - 'Unable to retrieve payment source.' => 'Impossibile recuperare fonte di pagamento.', - 'Unable to set default shipping category.' => 'Impossibile impostare la categoria di spedizione predefinita.', - 'Unable to set default tax category.' => 'Impossibile impostare la categoria d\'imposta predefinita.', - 'Unable to set primary payment source.' => 'Impossibile creare fonte di pagamento principale.', - 'Unable to start the subscription. Please check your payment details.' => 'Impossibile avviare la sottoscrizione. Controllare i dati di pagamento.', - 'Unable to subscribe at this time.' => 'Al momento non è possibile effettuare la sottoscrizione.', - 'Unable to update cart.' => 'Impossibile aggiornare il carrello.', - 'Unable to validate address.' => 'Impossibile convalidare l’indirizzo.', - 'Unit Price' => 'Prezzo unitario', - 'Unit price (minus discounts)' => 'Prezzo unitario (meno gli sconti)', - 'Units' => 'Unità', - 'Unpaid' => 'Non pagato', - 'Unsubscribe' => 'Annulla sottoscrizione', - 'Update Address' => 'Aggiorna indirizzo', - 'Update Order Status' => 'Aggiorna stato ordine', - 'Update Order Status…' => 'Aggiornamento stato ordine in corso...', - 'Update order' => 'Aggiorna ordine', - 'Update subscription' => 'Aggiorna sottoscrizione', - 'Update' => 'Aggiorna', - 'Updated By' => 'Aggiornato da', - 'Updated committed stock successfully.' => 'Aggiornamento dello stock impegnato completato correttamente.', - 'Updated' => 'Aggiornato', - 'Use Billing Address For Tax' => 'Usa l\'indirizzo di fatturazione per le imposte', - 'Use as the primary billing address' => 'Usa come indirizzo di fatturazione principale', - 'Use as the primary shipping address' => 'Usa come indirizzo di spedizione principale', - 'Used By Tax Rates' => 'Utilizzato per aliquote fiscali', - 'Used by Tax Rates' => 'Utilizzato per aliquote fiscali', - 'User Groups' => 'Gruppi di utenti', - 'User not found.' => 'Utente non trovato.', - 'User' => 'Utente', - 'Uses' => 'Utilizzi', - 'Validate Business Tax ID as Vat ID' => 'Convalida l\'ID fiscale dell\'azienda come ID IVA', - 'Validating condition syntax' => 'Convalida della sintassi della condizione', - 'Validating formula syntax' => 'Convalida della sintassi della formula', - 'Variant Fields' => 'Campi varianti', - 'Variant Has Untracked Stock' => 'La variante ha stock non tracciato', - 'Variant Price' => 'Prezzo variante', - 'Variant SKU' => 'SKU variante', - 'Variant Search' => 'Ricerca variante', - 'Variant Stock' => 'Stock variante', - 'Variant Title Format' => 'Formato titolo variante', - 'Variant Tracks Stock' => 'La variante traccia lo stock', - 'Variant UI Label Format' => 'Formato etichetta UI variante', - 'Variant has no product.' => 'La variante non ha prodotti.', - 'Variants not restored.' => 'Varianti non ripristinate.', - 'Variants restored.' => 'Varianti ripristinate.', - 'Variants' => 'Varianti', - 'View customer' => 'Visualizza cliente', - 'View order' => 'Visualizza ordine', - 'View product type - {productType}' => 'Visualizza il tipo di prodotto - {productType}', - 'View user' => 'Visualizza utente', - 'View' => 'Mostra', - 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Attenzione, eliminando questa valuta verranno bloccati tutti i pagamenti e i rimborsi in questa valuta, sei sicuro di voler eliminare "{name}"?', - 'Web' => 'Web', - 'Webhook URL' => 'URL webhook', - 'Weight ({unit})' => 'Peso ({unit})', - 'Weight Rate' => 'Tariffa in base al peso', - 'Weight Unit' => 'Unità di peso', - 'Weight' => 'Peso', - 'What product URIs should look like for the site.' => 'L\'aspetto degli URL del prodotto per il sito.', - 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Aspetto dei titoli dei prodotti auto-generati. È possibile includere tag che producono le proprietà dei prodotti, come {ex1} o {ex2}. Tutti i campi personalizzati utilizzati devono essere impostati come obbligatori.', - 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Aspetto dei titoli delle varianti auto-generati. È possibile includere tag che producono le proprietà delle varanti, come {ex1} o {ex2}. Tutti i campi personalizzati utilizzati devono essere impostati come obbligatori.', - 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Come dovrebbe apparire il nome del file PDF dell\'ordine (senza estensione). È possibile includere tag che producono le proprietà dell\'ordine, come {ex1} o {ex2}.', - 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Aspetto degli SKU univoci auto-generati, quando un campo SKU viene inviato senza un valore. È possibile includere tag che producono proprietà, come {ex1} o {ex2}.', - 'What this PDF will be called in the control panel.' => 'Nome di questo PDF nel pannello di controllo.', - 'What this catalog pricing rule will be called in the control panel.' => 'Come si chiamerà questa regola di prezzo in catalogo nel pannello di controllo.', - 'What this discount will be called in the control panel.' => 'Nome di questo sconto nel pannello di controllo.', - 'What this email will be called in the control panel.' => 'Nome di questa email nel pannello di controllo.', - 'What this product type will be called in the control panel.' => 'Nome di questo tipo di prodotto nel pannello di controllo.', - 'What this sale will be called in the control panel.' => 'Nome di questa vendita promozionale nel pannello di controllo.', - 'What this shipping category will be called in the control panel.' => 'Come si chiamerà questa categoria di spedizione nel pannello di controllo.', - 'What this shipping rule will be called in the control panel.' => 'Come si chiamerà questa regola di spedizione nel pannello di controllo.', - 'What this shipping zone will be called in the control panel.' => 'Come si chiamerà questa zona di spedizione nel pannello di controllo.', - 'What this status will be called in the control panel.' => 'Nome di questo stato nel pannello di controllo.', - 'What this subscription plan will be called in the control panel.' => 'Nome di questo piano di sottoscrizione nel pannello di controllo.', - 'What this tax category will be called in the control panel.' => 'Come si chiamerà questa categoria fiscale nel pannello di controllo.', - 'What this tax zone will be called in the control panel.' => 'Come si chiamerà questa zona fiscale nel pannello di controllo.', - 'When this discount is applied to an order, which line items should be discounted?' => 'Quando questo sconto è applicato a un ordine, quali articoli è necessario scontare?', - 'Whether the first available shipping method option should be set automatically on carts.' => 'Definisce se la prima opzione di metodo di spedizione disponibile debba essere impostata automaticamente sui carrelli.', - 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Definisce se il metodo di pagamento principale dell\'utente debba essere impostato automaticamente sui nuovi carrelli.', - 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Definisce se gli indirizzi principali di spedizione e fatturazione dell\'utente debbano essere impostati automaticamente sui nuovi carrelli.', - 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Se questa regola di prezzo in catalogo deve essere disponibile all’uso, indipendentemente da altre condizioni.', - 'Whether this sale should be available for use, regardless of other conditions.' => 'Se questa vendita deve essere disponibile all’uso, indipendentemente da altre condizioni.', - 'Which data to display in the name column in the results table.' => 'Quali dati visualizzare nella colonna del nome nella tabella dei risultati.', - 'Which product types should this category be available to?' => 'Per quali tipi di prodotto deve essere disponibile questa categoria?', - 'Which template should be loaded when a product’s URL is requested.' => 'Il template da caricare quando viene richiesto l\'URL di un prodotto.', - 'Width ({unit})' => 'Larghezza ({unit})', - 'Width' => 'Larghezza', - 'YYYY' => 'AAAA', - 'Yes' => 'Sì', - 'You are not allowed to add a line item.' => 'Non è consentito aggiungere una voce.', - 'You currently have no emails configured to select for this status.' => 'Attualmente non sono presenti e-mail configurate selezionabili per questo stato.', - 'You do not have permission to load this cart.' => 'Non disponi delle autorizzazioni necessarie per caricare questo carrello.', - 'You must set up at least one gateway that supports subscriptions first.' => 'Prima di tutto è necessario impostare almeno un gateway che supporti le sottoscrizioni.', - 'You must be logged in or provide a valid token to load this cart.' => 'Per caricare questo carrello è necessario effettuare l’accesso o presentare un token valido.', - 'You must be signed in to create a payment source.' => 'Per creare una fonte di pagamento è necessario effettuare l’accesso.', - 'You must be signed in to set a primary payment source.' => 'Per creare una fonte di pagamento principale è necessario effettuare l’accesso.', - 'You must make a payment to complete the order.' => 'È necessario effettuare un pagamento per completare l\'ordine.', - 'Your Cart Recovery Link' => 'Il tuo link per il recupero del carrello', - 'Your Order PDF Download Link' => 'Il tuo link di download PDF ordine', - 'Your order is empty' => 'Il tuo ordine è vuoto', - 'ZIP file' => 'File ZIP', - 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Zero - Il prezzo minimo è zero se gli sconti sono di importo superiore al valore dell\'ordine.', - 'Zip Code' => 'CAP', - 'all' => 'tutti', - 'any' => 'qualsiasi', - 'average order total' => 'totale medio degli ordini', - 'billing address' => 'indirizzo di fatturazione', - 'donation' => 'donazione', - 'donations' => 'donazioni', - 'info' => 'informazioni', - 'inventory location' => 'sede dell\'inventario', - 'new customers' => 'nuovi clienti', - 'on hand' => 'disponibile', - 'only' => 'solo', - 'order' => 'ordine', - 'orders' => 'ordini', - 'price' => 'prezzo', - 'prices' => 'prezzi', - 'product variant' => 'variante prodotto', - 'product variants' => 'varianti prodotto', - 'product' => 'prodotto', - 'products' => 'prodotti', - 'repeat customers' => 'clienti abituali', - 'shipping address' => 'indirizzo di spedizione', - 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'È impossibile impostare sia shippingSameAsBilling che billingSameAsShipping.', - 'subscription' => 'sottoscrizione', - 'subscriptions' => 'sottoscrizioni', - 'to' => 'a', - 'transfer' => 'trasferisci', - 'transfers' => 'trasferimenti', - '{amount} included' => '{amount} incluso', - '{count} Unfulfilled Orders' => '{count} ordini non evasi', - '{description} is no longer available.' => '{description} non è più disponibile.', - '{description} only has {stock} in stock.' => 'Sono disponibili solo {stock} {description} a magazzino.', - '{from} to {to}' => 'Da {from} a {to}', - '{name} (Primary)' => '{name} (Primario)', - '{name} (Trashed)' => '{name} (Spostato nel cestino)', - '{name} catalog price' => '{name} prezzo in catalogo', - '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, one {}=1{ordine aggiornato} other{ordini aggiornati}}.', - '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, one {}=1{ordine è} other{ordini sono}} associato/i {numUsers, plural, one {}=1{all\'utente} other{agli utenti}}.', - '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, one {}=1{abbonamento è} other{abbonamenti sono}} attivato/i per {numUsers, plural, one {}=1{l\'utente} other{gli utenti}}.', - '{number} more…' => '{number} in più…', - '{pct} off the discounted item price' => '{pct} di sconto sul prezzo scontato dell\'articolo', - '{pct} off the original item price' => '{pct} di sconto sul prezzo originale dell\'articolo', - '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, one {}=1{non è stato assegnato} other{non sono stati assegnati}} a un sito.', - '{total} in total revenue' => '{total} in ricavi totali', - '{total} orders' => '{total} ordini', - '{total} saleable across {locationCount} location(s)' => '{total} vendibile in {locationCount} sede/i', - '{uses} uses across {emails} email addresses' => '{uses} utilizzi su {emails} indirizzi email', - '{uses} uses across {users} users' => '{uses} utilizzi da parte dei {users} clienti', - '“{description}” is currently out of stock.' => '“{description}” è attualmente esaurito.', - '“{key}” has invalid JSON' => '“{key}” ha un JSON non valido', -]; diff --git a/src/translations/ja/commerce.php b/src/translations/ja/commerce.php deleted file mode 100644 index 516483efe5..0000000000 --- a/src/translations/ja/commerce.php +++ /dev/null @@ -1,1428 +0,0 @@ - '(新しい価格)', - '(of original price)' => '(値引き)', - '(off original price)' => '(元の価格からの割引)', - 'A cart number must be specified.' => 'カート番号を指定してください。', - 'A cart recovery link has been sent to {email}.' => 'カート復元リンクを{email}宛に送信しました。', - 'A cart recovery link will be sent to {email}.' => 'カート復元リンクを{email}宛に送信します。', - 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'カートが注文に変わると、このフォーマットに基づいて管理しやすい参照番号が生成されます。たとえば、{ex1}、または
{ex2}。このフォーマットの結果は一意でなければなりません。', - 'A new download link has been sent to {email}' => '新しいダウンロードリンクを {email} 宛に送信しました', - 'A new download link will be sent to {email}' => '新しいダウンロードリンクを {email} 宛に送信します', - 'A valid email is required to create a customer.' => '顧客を作成するために有効なメールが必要です。', - 'Accept' => '受け入れる', - 'Accepted' => '受け入れ済み', - 'Actions' => 'アクション', - 'Active Carts' => 'アクティブなカート', - 'Active subscriptions' => 'アクティブな定期支払い', - 'Active' => '有効', - 'Add Address' => '住所を追加', - 'Add a coupon' => 'クーポンを追加', - 'Add a custom line item' => 'カスタムラインアイテムを追加', - 'Add a line item' => 'ラインアイテムを追加', - 'Add a product' => '商品を追加', - 'Add a variant' => 'バリアントを追加', - 'Add an adjustment' => 'アジャストメントを追加', - 'Add an item' => 'アイテムを追加', - 'Add an option' => 'オプションを追加', - 'Add catalog price' => 'カテゴリ価格を追加', - 'Add' => '追加', - 'Additional Actions' => '追加のアクション', - 'Additional recipients that should receive this email. Twig code can be used here.' => 'このメールを受信するその他の受信者。ここに Twig コードを使用できます。', - 'Address 1' => '住所1', - 'Address 2' => '住所2', - 'Address 3' => '住所3', - 'Address Line 1' => '住所欄1', - 'Address Line 2' => '住所欄2', - 'Address Updated.' => '住所が更新されました。', - 'Address copied to user.' => 'アドレスをユーザーにコピーしました。', - 'Address not found.' => '住所が見つかりません。', - 'Adjust Quantity' => '数量を調整', - 'Adjust by' => '調整方法', - 'Adjust price when included rate is disqualified?' => '税込料金が不適格である場合、価格を調整しますか?', - 'Adjustments' => 'アジャストメント', - 'Admin Notices' => '管理者通知', - 'Administrative Area Code of Origin' => '原産地の行政区画のエリアコード', - 'Advanced' => '高度', - 'All Orders' => 'すべての注文', - 'All Totals' => 'すべての合計', - 'All Transfers' => 'すべての移動', - 'All active subscriptions' => 'すべての有効な定期支払い', - 'All customers' => 'すべての顧客', - 'All products' => 'すべての商品', - 'All variants must have a SKU.' => 'すべてのバリアントにはSKUが必要です。', - 'All' => 'すべて', - 'Allow Checkout Without Payment' => '支払いなしでのチェックアウトを許可する', - 'Allow Empty Cart On Checkout' => 'チェックアウト時の空のカートを許可する', - 'Allow Partial Payment On Checkout' => 'チェックアウトで一部支払いを許可する', - 'Allow out of stock purchases' => '在庫切れ商品の購入を許可する', - 'Allow' => '許可', - 'Allowed Qty' => '許可する数量', - 'Alternative Phone' => '予備の電話', - 'Amount' => '量', - 'An ID must be provided' => 'ID を指定してください', - 'An error occurred while generating this PDF.' => 'この PDF を生成中にエラーが発生しました。', - 'Any' => 'すべて', - 'Anywhere' => '指定なし', - 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => '定期支払いプラン「{name}」をアーカイブしてもよろしいですか?既存の定期支払いはキャンセルされません。', - 'Are you sure you want to capture this transaction?' => 'この取引をキャプチャしてもよろしいですか?', - 'Are you sure you want to complete this order?' => 'この注文を完了してもよろしいですか?', - 'Are you sure you want to delete the selected orders?' => '選択した注文を削除してもよろしいですか?', - 'Are you sure you want to delete the selected product and its variants?' => '選択した商品とそのバリアントを削除してもよろしいですか?', - 'Are you sure you want to delete this shipping rule?' => 'この配送ルールを削除してもよろしいですか?', - 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => '「{name}」とそのすべての商品を削除してもよろしいですか?この取り消しできない処理を実行する前に、データベースのバックアップがあることを確認してください。', - 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => '「{name}」を削除してよろしいですか? このステータスのあるすべてのラインアイテムがステータス無しに設定されます。', - 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'この移動を保留中としてマークしてもよろしいですか?これは移動先で入庫予定として表示されます。', - 'Are you sure you want to overwrite the billing address?' => 'この請求先住所を上書きしてよろしいですか?', - 'Are you sure you want to overwrite the shipping address?' => 'この配送先住所を上書きしてよろしいですか?', - 'Are you sure you want to permanently delete this store and everything in it?' => 'このストアとストアのすべてを完全に削除してもよろしいですか?', - 'Are you sure you want to refund this transaction?' => 'この取引を払い戻ししてもよろしいですか?', - 'Are you sure you want to remove this customer?' => 'この顧客を削除してもよろしいですか?', - 'Are you sure you want to save this as a new shipping rule?' => '新しい配送ルールとして保存してもよろしいですか?', - 'Are you sure you want to send email: {name}?' => '「{name}」メールを送信してもよろしいですか?', - 'At least one site must be enabled for the product type.' => '商品タイプには少なくとも1つのサイトを有効にする必要があります。', - 'Attempted Payments' => '試行された支払い', - 'Attention' => '部署名', - 'Authorize Only (Manually Capture)' => '承認のみ(手動キャプチャ)', - 'Auto Set Cart Shipping Method Option' => 'カート配送方法オプションを自動設定する', - 'Auto Set New Cart Addresses' => '新規カート住所を自動設定する', - 'Auto Set Payment Source' => '支払い元を自動設定する', - 'Automatic SKU Format' => 'SKUの自動フォーマット', - 'Available Shipping Categories' => '利用可能な配送カテゴリ', - 'Available Tax Categories' => '利用可能な税カテゴリ', - 'Available for purchase' => '購入可能', - 'Available for purchase?' => '購入可能にしますか?', - 'Available inventory for "{description}" has gone below zero.' => '「{description}」の利用可能在庫が0を下回っています。', - 'Available to Product Types' => '商品タイプで利用可能', - 'Available' => '利用可能', - 'Available?' => '購入可能?', - 'Average Order Total' => '注文合計平均', - 'Average' => '平均', - 'BCC’d Recipient' => 'BCC宛先', - 'Bad Request' => '不正なリクエスト', - 'Bad address ID.' => '不正な住所 ID です。', - 'Bad order ID.' => '不正な注文 ID です。', - 'Base Price' => '最安価格', - 'Base Promotional Price' => 'ベース販売促進価格', - 'Base Rate' => '基本料金', - 'Base' => '基本', - 'Bcc' => 'Bcc', - 'Billing Address' => '請求先住所', - 'Billing Business Name' => '請求先会社名', - 'Billing First Name' => '請求先の名', - 'Billing Full Name' => '請求先氏名', - 'Billing Last Name' => '請求先の姓', - 'Billing address required.' => '請求先住所が必要です。', - 'Billing detail update URL' => '課金詳細更新URL', - 'Billing issues' => '請求に関する問題', - 'Billing' => '請求先', - 'Both (Line item price + Line item shipping costs)' => '両方(ラインアイテム価格 + ラインアイテム配送料)', - 'Business ID' => '事業者ID', - 'Business Name' => '事業名', - 'Business Tax ID' => '事業税ID', - 'CC’d Recipient' => 'CC宛先', - 'CVV' => 'CVV', - 'Can be used as an internal reference.' => '内部で参照するために使用されます。', - 'Can not complete payment for missing transaction.' => '取引が見つからないため、支払いを完了できません。', - 'Can not create a new order' => '新しい注文を作成できません', - 'Can not find an order to pay.' => '支払う注文が見つかりません。', - 'Can not find enabled email.' => '有効なメールが見つかりません。', - 'Can not find order' => '注文が見つかりません', - 'Can not find order.' => '注文が見つかりません.', - 'Can not find the transaction to refund' => '払い戻しする取引が見つかりません', - 'Can not move between these inventory types.' => 'これらの在庫タイプ間は移動できません。', - 'Can not refund amount greater than the remaining amount' => '残金を上回る金額を払戻しできません', - 'Cancel subscription' => '定期支払いをキャンセルする', - 'Cancel with gateway now' => '今すぐゲートウェイでキャンセル', - 'Cancel' => 'キャンセル', - 'Cancellation date' => 'キャンセルした日付', - 'Cancellation' => 'キャンセル', - 'Cannot switch plans for this subscription.' => 'この定期支払いのプランを切り替えることはできません。', - 'Can’t preview this email.' => 'このメールはプレビューできません。', - 'Capture payment' => '支払いをキャプチャ', - 'Capture' => 'キャプチャ', - 'Card Holder' => 'カード名義人', - 'Card Number' => 'カード番号', - 'Card' => 'カード', - 'Cart Recovery Link' => 'カート復元リンク', - 'Cart forgotten.' => '忘れられたカート。', - 'Cart updated.' => 'カートが更新されました。', - 'Cart {number}' => 'カート{number}', - 'Catalog Pricing Rule' => 'カタログ価格ルール', - 'Catalog pricing rule description.' => 'カタログ価格ルールの説明。', - 'Catalog pricing rule saved.' => 'カタログ価格ルールを保存しました。', - 'Catalog pricing rules deleted.' => 'カタログ価格ルールを削除しました。', - 'Catalog pricing rules updated.' => 'カタログ価格ルールを更新しました。', - 'Categories Relationship Type' => 'カテゴリの関係付けタイプ', - 'Categories' => 'カテゴリ', - 'Category Rate Overrides' => 'カテゴリの料金上書き', - 'Centimeters (cm)' => 'センチメートル(cm)', - 'Changing this value may affect your ability to refund existing transactions.' => 'この値を変更すると、既存の取引の払い戻し機能に影響する場合があります。', - 'Choose a color to represent the order’s status' => '注文ステータスのカラーを選んでください', - 'Choose a new customer' => '新しい顧客を選択', - 'Choose adjustment values to include when calculating the product revenue total.' => '商品の合計収益を計算する際に含める調整値を選択してください。', - 'Choose the currency’s ISO code.' => 'この国のISOコードを選択してください。', - 'Choose the destination inventory location for the existing on hand stock.' => '宛先の既存の手持ち在庫の在庫場所を選択してください。', - 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'このセクションを表示可能にする商品タイプを選択して、サイト固有の設定を行ってください。', - 'City' => '市区町村', - 'Clear counter' => 'カウントをリセットする', - 'Clear notices' => '通知をクリア', - 'Close' => '閉じる', - 'Code' => 'コード', - 'Collated PDF' => '照合済みの PDF', - 'Color' => 'カラー', - 'Commerce Products' => 'Commerce 商品', - 'Commerce Settings' => 'コマース設定', - 'Commerce Variants' => 'Commerce バリアント', - 'Commerce email “{email}” could not be sent for order “{order}”.' => '注文「{order}」の Commerce メール「{email}」を送信できませんでした。', - 'Commerce order exports' => 'Commerce 注文のエクスポート', - 'Commerce' => 'Commerce', - 'Committed' => 'コミット済み', - 'Completed Email' => '完了したメール', - 'Completed' => '完了', - 'Completing order failed.' => '注文の完了に失敗しました。', - 'Condition' => '条件', - 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'ここでの条件は、ルールを調べる前に注文に対して照合されます。これは、前もってメソッドの可用性を評価する場合、またはこのメソッドに対してすべてのルールに共通の条件がある場合に役立ちます。', - 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'ここで設定した条件は、ルールを確認する前に注文の顧客情報と照合されます。これは、メソッドの利用可否を早い段階で判断したい場合や、このメソッドに適用されるすべてのルールに共通する条件を設けたい場合に便利です。', - 'Conditions' => '条件', - 'Contains Purchasables' => '購入可能商品を含む', - 'Control Panel Settings' => 'コントロールパネルの設定', - 'Control panel' => 'コントロールパネル', - 'Conversion Rate' => '換算レート', - 'Converted Price' => '換算価格', - 'Copied!' => 'コピーしました!', - 'Copy the URL' => 'URL をコピー', - 'Copy to {location}' => '{location}へコピー', - 'Copy' => 'コピー', - 'Costs' => '料金', - 'Could not archive gateway.' => 'ゲートウェイをアーカイブできませんでした。', - 'Could not cancel “{reference}”.' => '「{reference}」をキャンセルできませんでした。', - 'Could not create the payment source.' => '支払い元を作成できませんでした。', - 'Could not delete shipping rule' => '配送ルールを削除できませんでした', - 'Could not delete shipping zone' => '配送地域を削除できませんでした', - 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => '{count, number}件の配送{count, plural, one{カテゴリ} other{カテゴリ}}を削除できませんでした。', - 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => '{count, number}件の配送{count, plural, one{方法} other{方法}}とルールを削除できませんでした。', - 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => '{count, number}件の税{count, plural, one{カテゴリ} other{カテゴリ}}を削除できませんでした。', - 'Could not find the email or template.' => 'メールまたはテンプレートが見つかりませんでした。', - 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => '注文 {number} を完了としてマークできませんでした。注文完了中、注文の保存はエラーにより失敗しました: {order}', - 'Could not reactivate “{reference}”.' => '「{reference}」を再度有効にできませんでした。', - 'Could not send email' => 'メールを送信できませんでした', - 'Could not switch “{reference}” to “{plan}”.' => '「{reference}」を「{plan}」に切り替えることができませんでした。', - 'Could not update orders address.' => '注文の住所を更新できませんでした。', - 'Couldn’t archive Line Item Status.' => 'ラインアイテムのステータスをアーカイブできませんでした。', - 'Couldn’t archive Order Status.' => '注文ステータスをアーカイブできませんでした。', - 'Couldn’t capture transaction.' => '取引をキャプチャできませんでした。', - 'Couldn’t capture transaction: {message}' => '取引をキャプチャできませんでした: {message}', - 'Couldn’t delete email.' => 'メールを削除できませんでした。', - 'Couldn’t delete the payment source.' => '支払い元を削除できませんでした。', - 'Couldn’t get order.' => '注文を取得できませんでした。', - 'Couldn’t recalculate order.' => '注文を再計算できませんでした。', - 'Couldn’t refund transaction.' => '取引を払い戻しできませんでした。', - 'Couldn’t refund transaction: {message}' => '取引を払い戻しできませんでした: {message}', - 'Couldn’t reorder Line Item Statuses.' => 'ラインアイテムのステータスを並び替えできませんでした。', - 'Couldn’t reorder Order Statuses.' => '注文ステータスを並び替えできませんでした。', - 'Couldn’t reorder PDFs.' => 'PDF を並び替えできませんでした。', - 'Couldn’t reorder discounts.' => 'ディスカウントを並び替えできませんでした。', - 'Couldn’t reorder gateways.' => 'ゲートウェイを並び替えできませんでした。', - 'Couldn’t reorder plans.' => 'プランを並び替えできませんでした。', - 'Couldn’t reorder rules.' => 'ルールを並び替えできませんでした。', - 'Couldn’t reorder sale.' => 'セールを並び替えできませんでした。', - 'Couldn’t reorder sales.' => 'セールを並び替えできませんでした。', - 'Couldn’t reorder statuses.' => 'ステータスを並び替えできませんでした。', - 'Couldn’t reorder stores.' => 'ストアを並び替えできませんでした。', - 'Couldn’t save PDF.' => 'PDF を保存できませんでした。', - 'Couldn’t save catalog pricing rule.' => 'カタログ価格ルールを保存できませんでした。', - 'Couldn’t save currency.' => '通貨を保存できませんでした。', - 'Couldn’t save discount.' => 'ディスカウントを保存できませんでした。', - 'Couldn’t save email.' => 'メールを保存できませんでした。', - 'Couldn’t save gateway.' => 'ゲートウェイを保存できませんでした。', - 'Couldn’t save inventory location.' => '在庫場所を保存できませんでした。', - 'Couldn’t save line item status.' => 'ラインアイテムのステータスを保存できませんでした。', - 'Couldn’t save order fields.' => '注文フィールドを保存できませんでした。', - 'Couldn’t save order status.' => '注文ステータスを更新できませんでした。', - 'Couldn’t save order.' => '注文を保存できませんでした。', - 'Couldn’t save product type.' => '商品タイプを保存できませんでした。', - 'Couldn’t save sale.' => 'セールを保存できませんでした。', - 'Couldn’t save settings.' => '設定を保存できませんでした。', - 'Couldn’t save shipping category.' => '配送カテゴリを保存できませんでした。', - 'Couldn’t save shipping method.' => '配送方法を保存できませんでした。', - 'Couldn’t save shipping rule.' => '配送ルールを保存できませんでした。', - 'Couldn’t save shipping zone.' => '配送地域を保存できませんでした。', - 'Couldn’t save store.' => 'ストアを保存できませんでした。', - 'Couldn’t save subscription fields.' => '定期支払いフィールドを保存できませんでした。', - 'Couldn’t save subscription plan.' => '定期支払いプランを保存できませんでした。', - 'Couldn’t save subscription.' => '定期支払いを保存できませんでした。', - 'Couldn’t save tax category.' => '税カテゴリを保存できませんでした。', - 'Couldn’t save tax rate.' => '税率を保存できませんでした。', - 'Couldn’t save tax zone.' => '税対象地域を保存できませんでした。', - 'Couldn’t save transfer fields.' => '移動フィールドを保存できませんでした。', - 'Couldn’t update catalog pricing rule statuses.' => 'カタログ価格ルールのステータスを更新できませんでした。', - 'Couldn’t update status.' => 'ステータスを更新できませんでした。', - 'Couldn’t updated sales status.' => 'セールのステータスを更新できませんでした。', - 'Country Code of Origin' => '原産国コード', - 'Country List' => '国リスト', - 'Country not allowed.' => '許可されていない国です。', - 'Country' => '国', - 'Coupon Code' => 'クーポンコード', - 'Coupon can not apply discount to this order due to address mismatch.' => 'アドレスが一致しなかったため、この注文にはクーポンのディスカウントを適用できません。', - 'Coupon can not apply discount to this order due to customer mismatch.' => '顧客が一致しなかったため、この注文にはクーポンのディスカウントを適用できません。', - 'Coupon can not apply discount to this order.' => 'この注文にはクーポンのディスカウントを適用できません。', - 'Coupon code “{code}” is already in use by discount “{name}”.' => 'クーポンコード「{code}」はディスカウント「{name}」によってすでに使用されています。', - 'Coupon codes cannot be blank.' => 'クーポンコードは空白にできません。', - 'Coupon codes must be unique.' => 'クーポンコードは一意でなければなりません。', - 'Coupon format is required and must contain at least one `#`.' => 'クーポンフォーマットが必要で、少なくとも1つの「#」を含んでいなければなりません。', - 'Coupon not valid.' => 'クーポンが有効ではありません。', - 'Coupon removed: {explanation}' => 'クーポンが削除されました:{explanation}', - 'Coupons' => 'クーポン', - 'Craft Commerce - Administration' => 'Craft Commerce - 管理', - 'Craft Commerce - Inventory' => 'Craft Commerce - 在庫', - 'Craft Commerce - Orders' => 'Craft Commerce - 注文', - 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - 商品タイプ - {name}', - 'Craft Commerce - Subscriptions' => 'Craft Commerce - サブスクリプション', - 'Create a Discount' => 'ディスカウントを作成', - 'Create a Subscription Plan' => '定期支払いプランを作成', - 'Create a new PDF' => '新しい PDF を作成', - 'Create a new catalog pricing rule' => '新規カタログ価格ルールを作成', - 'Create a new currency' => '新しい通貨を作成', - 'Create a new email' => '新しいメールを作成', - 'Create a new gateway' => '新しいゲートウェイを作成', - 'Create a new line item status' => '新しいラインアイテムのステータスを作成', - 'Create a new order status' => '新しい注文ステータスを作成', - 'Create a new product type' => '新しい商品タイプを作成する', - 'Create a new sale' => '新しいセールを作成', - 'Create a new shipping category' => '新しい配送カテゴリを作成', - 'Create a new shipping method' => '新しい配送方法を作成', - 'Create a new shipping rule' => '新しい配送ルールを作成', - 'Create a new tax category' => '新しい税カテゴリを作成する', - 'Create a new tax rate' => '新しい税率を作成', - 'Create a product type' => '商品タイプを作成', - 'Create a shipping zone' => '配送地域を作成', - 'Create a tax zone' => '税対象地域を作成', - 'Create catalog pricing rules' => 'カタログ価格ルールを作成', - 'Create customer: “{email}”' => '顧客を作成: 「{email}」', - 'Create discounts' => 'ディスカウントを作成', - 'Create discount…' => 'ディスカウントを作成…', - 'Create rules that allow this discount to match the order.' => 'このディスカウントを注文に一致させるルールを作成します。', - 'Create rules that allow this discount to match the order’s billing address.' => 'このディスカウントを注文の請求先住所に一致させるルールを作成します。', - 'Create rules that allow this discount to match the order’s customer.' => 'このディスカウントを注文の顧客に一致させるルールを作成します。', - 'Create rules that allow this discount to match the order’s shipping address.' => 'このディスカウントを注文の配送先住所に一致させるルールを作成します。', - 'Create rules that allow this gateway to match the billing address.' => 'このゲートウェイを請求先住所と一致させるルールを作成します。', - 'Create rules that allow this gateway to match the order.' => 'このゲートウェイを注文に一致させるルールを作成します。', - 'Create rules that allow this gateway to match the shipping address.' => 'このゲートウェイを配送先住所と一致させるルールを作成します。', - 'Create sales' => 'セールを作成', - 'Create sale…' => 'セールを作成…', - 'Created' => '作成済み', - 'Credit Card Payment Type' => 'クレジットカード決済のタイプ', - 'Currency Code' => '通貨コード', - 'Currency saved.' => '通貨は保存されました。', - 'Currency' => '通貨', - 'Current' => '現在', - 'Custom 1' => 'カスタム1', - 'Custom 2' => 'カスタム2', - 'Custom 3' => 'カスタム3', - 'Custom 4' => 'カスタム4', - 'Custom' => 'カスタム', - 'Customer Enabled?' => '顧客が利用可能?', - 'Customer ID is required.' => '顧客 ID が必要です。', - 'Customer Note' => '顧客ノート', - 'Customer Notices' => '顧客の通知', - 'Customer data' => '顧客データ', - 'Customer' => '顧客', - 'Damaged' => '破損', - 'Data shown might be outdated.' => '表示データは古い可能性があります。', - 'Date Authorized' => 'オーソリ日', - 'Date Created' => '作成日', - 'Date First Paid' => '初回支払い日', - 'Date Ordered' => '注文日', - 'Date Paid' => '支払日', - 'Date Updated' => '更新日', - 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'カタログ価格ルールが有効になる日付。開始日を指定しない場合は空白のままにします。', - 'Date from which the discount will be active. Leave blank for unlimited start date' => 'ディスカウントが有効になる日付。開始日を指定しない場合は空白のままにします。', - 'Date from which the sale will be active. Leave blank for unlimited start date' => 'セールが有効になる日付。開始日を指定しない場合は空白のままにします。', - 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'カタログ価格ルールが終了する日付。終了日を指定しない場合は空白のままにします。', - 'Date when the discount will be finished. Leave blank for unlimited end date' => 'ディスカウントが終了する日付。終了日を指定しない場合は空白のままにします。', - 'Date when the sale will be finished. Leave blank for unlimited end date' => 'セールが終了する日付。終了日を指定しない場合は空白のままにします。', - 'Date' => '日時', - 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'デフォルト - ディスカウントが注文価格より上回る場合は価格を負にすることができます。', - 'Default Category' => 'デフォルトのカテゴリ', - 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'デフォルトの Commerce コントロールパネルビュー。ユーザーに権限がない場合、アクセスできる場所にフォールバックします。', - 'Default Order PDF' => 'デフォルトの注文PDF', - 'Default Per Item Rate' => 'デフォルトのアイテムごとの料金', - 'Default Percentage Rate' => 'デフォルトパーセント率', - 'Default Status?' => 'デフォルトのステータス?', - 'Default View' => 'デフォルトのビュー', - 'Default Weight Rate' => 'デフォルトの重量ごとの料金', - 'Default Zone' => 'デフォルトの地域', - 'Default status?' => 'デフォルトのステータス?', - 'Default to this tax zone when no billing address is set' => '請求先住所が設定されていない場合は、この税対象地域がデフォルトになります', - 'Default to this tax zone when no shipping address is set' => '配送先住所が設定されていない場合は、この税対象地域がデフォルトになります', - 'Default variant updated.' => 'デフォルトバリアントが更新されました。', - 'Default' => 'デフォルト', - 'Default?' => 'デフォルト?', - 'Delete catalog pricing rules' => 'カタログ価格ルールを削除', - 'Delete discounts' => 'ディスカウントを削除', - 'Delete orders' => '注文を削除', - 'Delete sales' => 'セールを削除', - 'Delete' => '削除', - 'Deleting the {location} location.' => '{location}の場所を削除しています。', - 'Describe this rule.' => 'このルールについて説明してください。', - 'Describe this shipping zone.' => 'この配送地域について説明してください。', - 'Describe this tax zone.' => 'この税対象地域について説明してください。', - 'Description' => '説明', - 'Destination Inventory Location' => '宛先の在庫場所', - 'Destination' => '宛先', - 'Details' => '詳細', - 'Dimension Unit' => '寸法の単位', - 'Dimensions' => '寸法', - 'Disabled' => '無効', - 'Disallow' => '許可しない', - 'Discount all line items' => 'すべてのラインアイテムをディスカウント', - 'Discount description.' => 'ディスカウントの説明。', - 'Discount is not allowed for the order' => '注文のディスカウントは許可されていません', - 'Discount is out of date.' => 'ディスカウントは期限切れです。', - 'Discount saved.' => 'ディスカウントは保存されました。', - 'Discount the matching items only' => '一致アイテムのみをディスカウント', - 'Discount use has reached its limit.' => 'ディスカウントの使用回数が限界に達しました。', - 'Discount' => 'ディスカウント', - 'Discounted Item Subtotal' => 'ディスカウント済みアイテムの小計', - 'Discounted Items' => 'ディスカウントされたアイテム', - 'Discounts deleted.' => 'ディスカウントを削除しました。', - 'Discounts reordered.' => 'ディスカウントを並び替えました。', - 'Discounts updated.' => 'ディスカウントが更新されました。', - 'Discounts' => 'ディスカウント', - 'Disqualify with valid business tax ID?' => '有効な事業税 ID で不適格にしますか?', - 'Do not apply subsequent matching sales beyond applying this sale.' => 'このセールを適用した場合、その後のセールを適用しない。', - 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => '注文の住所が選択された有効な事業税 ID のものである場合は、この税率を適用しないでください。', - 'Do not attach a PDF to this email' => 'このメールに PDF を添付しないでください', - 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'エラーがある場合、注文(番号: {orderNumber})の再計算を実行しないでください。', - 'Donation can not be zero.' => '寄付はゼロにできません。', - 'Donation needs to be an amount.' => '寄付は金額です。', - 'Donation settings saved.' => '寄付の設定が保存されました。', - 'Donation' => '寄付', - 'Donations' => '寄付', - 'Done' => '完了', - 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'このディスカウントが適用された場合、その後のディスカウントを注文に適用しない。', - 'Download PDF' => 'PDFをダウンロード', - 'Download PDF…' => 'PDF をダウンロード...', - 'Download Type' => 'ダウンロードタイプ', - 'Download' => 'ダウンロード', - 'Draft' => '下書き', - 'Dummy gateway payment failed.' => 'ダミーゲートウェアの支払いに失敗しました。', - 'Duplicate options exist' => '重複したオプションがあります', - 'Duration' => '期間', - 'EU VAT ID' => 'EU VAT ID', - 'Edit address' => '住所を編集する', - 'Edit adjustments' => '調整を編集', - 'Edit catalog pricing rules' => 'カタログ価格ルールを編集', - 'Edit discounts' => 'ディスカウントを編集', - 'Edit options' => 'オプションを編集する', - 'Edit orders' => '注文を編集', - 'Edit sales' => 'セールを編集', - 'Edit' => '編集', - 'Effect' => '効果', - 'Either (Default) - The relationship field is on the purchasable or the category' => 'いずれか(デフォルト)- 関連フィールドはパーチャサブルまたはカテゴリにあります', - 'Either way' => 'どちらでもいい', - 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'メール「{email}」に対し、メール PDF の生成エラーが発生しました。注文: “{order}”。PDF テンプレートエラー: 「{message}」{file}:{line}', - 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'メール「{email}」のメール PDF のテンプレートは「{templatePath}」に存在しません。注文:「{order}」。', - 'Email Subject' => 'メールの件名', - 'Email error. No email address found for order. Order: “{order}”' => 'メールエラー。注文のメールアドレスがありません。注文: “{order}”', - 'Email is not enabled.' => 'メールが無効です。', - 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'プレーンテキスト形式のメールテンプレートは「{templatePath}」に存在しません。メール「{email}」の「{templateParsedPath}」に解析されています。注文: 「{order}」。', - 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'メール「{email}」に対し、プレーンテキスト形式メールのテンプレートの解析エラーが発生しました。注文: “{order}”。テンプレートエラー: “{message}” {file}:{line}', - 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '「テンプレートパス」のメール「{email}」に対し、プレーンテキスト形式メールのテンプレートパスの解析エラーが発生しました。注文:「{order}」。テンプレートエラー:「{message}」{file}:{line}', - 'Email required to make payments on a completed order.' => '完了した注文に対する支払いに必要なメール。', - 'Email saved.' => 'メールが保存されました。', - 'Email sent' => 'メールが送信されました', - 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'メールテンプレートは「{templatePath}」に存在しません。メール「{email}」の「{templateParsedPath}」に解析されています。注文: 「{order}」。', - 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '「To:」 のカスタムメール 「{email}」に対し、メールテンプレートの解析エラーが発生しました。注文: 「{order}」。テンプレートエラー: 「{message}」{file}:{line}', - 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '「BCC:」 のメール 「{email}」に対し、メールテンプレートの解析エラーが発生しました。注文: 「{order}」。テンプレートエラー: 「{message}」{file}:{line}', - 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '「CC:」 のメール 「{email}」に対し、メールテンプレートの解析エラーが発生しました。注文: 「{order}」。テンプレートエラー: 「{message}」{file}:{line}', - 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '「ReplyTo:」 のメール 「{email}」に対し、メールテンプレートの解析エラーが発生しました。注文: 「{order}」。テンプレートエラー: 「{message}」{file}:{line}', - 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '「Subject:」 のメール 「{email}」に対し、メールテンプレートの解析エラーが発生しました。注文: 「{order}」。テンプレートエラー: 「{message}」{file}:{line}', - 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'メール「{email}」に対し、メールテンプレートの解析エラーが発生しました。注文: “{order}”。テンプレートエラー: “{message}” {file}:{line}', - 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => '“Template Path” のメール “{email}” に対し、メールテンプレートのパスの解析エラーが発生しました。注文: “{order}”。テンプレートエラー: “{message}” {file}:{line}', - 'Email unavailable.' => 'メールを利用できません。', - 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => '注文 “{order}” のメール “{email}” を送信できませんでした。エラー: {error} {file}:{line}', - 'Email “{email}” for order {order} was cancelled.' => '注文「{order}」のメール「{email}」はキャンセルされました。', - 'Email' => 'メール', - 'Emails' => 'メール', - 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'コストを注文に追加する代わりに、税率が課税対象価格に組み込まれるかどうかを有効にします。', - 'Enable structure for products of this type' => 'このタイプの商品の構造を有効にする', - 'Enable this discount' => 'このディスカウントを有効にする', - 'Enable this rule' => 'このルールを有効にする', - 'Enable this sale' => 'このセールを有効にする', - 'Enable this shipping method on the front end' => 'この配送方法をフロントエンドで有効にする', - 'Enable this shipping rule' => 'この配送ルールを有効にする', - 'Enable this tax rate' => 'この税率を有効にする', - 'Enabled for customers to select during checkout?' => '支払い時に顧客が選択できるようにしますか?', - 'Enabled for customers to select?' => '顧客が選択できるようにしますか?', - 'Enabled' => '有効', - 'Enabled?' => '有効?', - 'End Date' => '終了日', - 'Enter SKU' => 'SKUを入力してください', - 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'コントロールパネルで使用される、この税率の分かりやすい名前を入力してください。', - 'Enter a percentage like {ex1} or {ex2}.' => '{ex1} や {ex2} などのパーセンテージを入力してください。', - 'Enter coupon code' => 'クーポンコードを入力してください', - 'Enter reference' => '参照を入力してください', - 'Error refunding transaction: {transactionHash}' => '取引の払い戻し中にエラーが発生しました: {transactionHash}', - 'Every new store must be assigned to at least one site.' => 'すべての新規ストアに少なくとも1つのサイトを割り当てる必要があります。', - 'Everywhere' => 'すべての地域', - 'Example' => '実例', - 'Exclude this discount for products that are already on promotion' => 'すでに販売促進が適用された商品はディスカウント対象から除外します', - 'Expired Link' => '期限切れのリンク', - 'Expired' => '期限切れ', - 'Expiry Date' => '有効期限の日付', - 'Expiry date' => '有効期限', - 'Expiry' => '期日', - 'Failed to receive transfer: {error}' => '移動の受け入れに失敗しました: {error}', - 'Failed to send email. Please try again.' => 'メールの送信に失敗しました。もう一度お試しください。', - 'Failed to start' => '開始できませんでした', - 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => '{num, plural, =1{注文ステータス} other{注文ステータス}}の更新に失敗しました。', - 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => '{num, plural, =1{注文} other{注文}}の注文ステータスの更新に失敗しました。', - 'Feet (ft)' => 'フィート(ft)', - 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'このルールが適用される注文の抽出条件。条件をスキップする場合は0に設定します。', - 'First Name' => '名', - 'Flat Amount Off Order' => '定額を差し引いた注文', - 'Flat Order Discount Amount Off' => '注文を定額でディスカウントする', - 'Free Order Payment Strategy' => '無料注文の支払いの方法', - 'Free Shipping' => '無料配送', - 'Free orders are processed by the payment gateway' => '無料の注文はペイメントゲートウェイによって処理されます', - 'Free orders complete immediately' => '無料の注文を即時に完了する', - 'Free shipping can only be for whole order or matching items, not both.' => '配送無料は、注文全体あるいは一致するアイテムのどちらか一方にのみ適用されます。', - 'From Name' => '送信者名', - 'Fulfill' => '履行', - 'Fulfilled' => '履行済み', - 'Fulfillment' => 'フルフィルメント', - 'Full Name' => '氏名', - 'Gateway Code' => 'ゲートウェイコード', - 'Gateway Message' => 'ゲートウェイメッセージ', - 'Gateway Reference' => 'ゲートウェイ参照', - 'Gateway Response' => 'ゲートウェイレスポンス', - 'Gateway doesn’t support authorize' => 'ゲートウェイは認証をサポートしていません', - 'Gateway doesn’t support partial refunds.' => 'ゲートウェイは一部払い戻しをサポートしていません。', - 'Gateway doesn’t support purchase' => 'ゲートウェイは購入をサポートしていません', - 'Gateway doesn’t support refunds.' => 'ゲートウェイは払い戻しをサポートしていません。', - 'Gateway saved.' => 'ゲートウェイは保存されました。', - 'Gateway' => 'ゲートウェイ', - 'Gateways reordered.' => 'ゲートウェイを並び替えました。', - 'Gateways' => 'ゲートウェイ', - 'General Settings' => '一般設定', - 'General' => '一般', - 'Generate' => '生成', - 'Generated Coupon Format' => '生成されたクーポンフォーマット', - 'Grams (g)' => 'グラム(g)', - 'Groups for which this sale will be applicable to.' => 'このセールが適用されるグループ。', - 'HTML Email Template Path' => 'HTMLメールのテンプレートのパス', - 'Handle' => 'ハンドル', - 'Harmonized System Code' => '関税分類システムコード', - 'Has Admin Notices' => '管理者通知あり', - 'Has Emails?' => 'メールがある?', - 'Has Free Shipping' => '無料配送あり', - 'Has Orders' => '注文あり', - 'Has Purchasable' => 'パーチャサブルあり', - 'Has Variants?' => 'バリアントがある?', - 'Height ({unit})' => '高さ({unit})', - 'Height' => '高さ', - 'Hide snapshot' => 'スナップショットを隠す', - 'History' => '履歴', - 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'PDFダウンロードリンクが有効期限切れとなるまでの期間(秒単位)。デフォルトは 86400 秒(24 時間)。', - 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => '個別のメールアドレスがこのディスカウントを利用できる回数。これは、ゲストまたはユーザーに関係なく、以前のすべての注文に適用されます。ゲストまたはユーザーが無制限に利用するにはゼロに設定します。', - 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => '1 人のユーザーがこのディスカウントを使用できる回数。これがゼロ以外に設定されている場合、サインインしたユーザーのみがディスカウントを利用できます。', - 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'このディスカウントをゲストまたはサインインしたユーザーが合計で使用できる回数。無制限に使用するにはゼロを設定します。', - 'How products should be labeled within the control panel.' => 'コントロールパネル内での商品の表示方法。', - 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'マッチングアイテムを設定するための、パーチャサブルとカテゴリの関連付け方法。[リレーション 専門用語]({link}) をご覧ください。', - 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'この商品が注文のラインアイテムとしてどのように記述されるかを指定します。 {ex1}や{ex2}などのプロパティを出力するタグを含めることができます', - 'How this shipping method will be referred to in templates and forms.' => 'この配送方法をテンプレートとフォーム上で参照する方法。', - 'How variants should be labeled within the control panel.' => 'コントロールパネル内でのバリアントの表示方法。', - 'How you’ll refer to this PDF in the templates.' => 'テンプレートでこの PDF を参照する方法。', - 'How you’ll refer to this product type in the templates.' => 'この商品タイプをテンプレート上で参照する方法。', - 'How you’ll refer to this shipping category in the templates.' => 'この配送カテゴリをテンプレート上で参照する方法。', - 'How you’ll refer to this status in the templates.' => 'このステータスをテンプレート上で参照する方法。', - 'How you’ll refer to this subscription plan in the templates.' => 'テンプレート上で使う定期支払いプランの名前。', - 'How you’ll refer to this tax category in the templates.' => 'この税カテゴリをテンプレート上で参照する方法。', - 'ID' => 'ID', - 'IP Address' => 'IPアドレス', - 'If disabled, this PDF will not be available or sent with emails.' => '無効にした場合、このPDFは利用不可になり、メールで送信されません。', - 'If disabled, this email will not send.' => '無効な場合、このメールは送信されません。', - 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => '有効に設定されていて、この税率が注文と一致しない場合、税額はカートの対象価格から削除されます。', - 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'オーソリのみに設定した場合、資金がアカウントに送金される前に、支払いを手動でキャプチャする必要があります。ゲートウェイは選択したオプションをサポートする必要があります。', - 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => '「ディスカウントされたアイテム価格の割引」にパーセント率を選択した場合、「アイテムごとの金額」とこれが適用される前のすべてのディスカウントが含まれます。', - 'Ignore Promotions?' => 'プロモーションを無視しますか?', - 'Ignore previous matching sales if this sale matches.' => 'このセールがマッチする場合は、既存のマッチするセールを無視する。', - 'Ignore promotional prices when this discount is applied to matching line items' => 'このディスカウントがマッチングアイテムに適用される場合は販売促進価格を無視する', - 'Inactive Carts' => '非アクティブなカート', - 'Inches (in)' => 'インチ(in)', - 'Include built-in line item tax.' => '組み込まれているラインアイテムの税金を含む。', - 'Include in price?' => '価格に含めますか?', - 'Include line item discounts.' => 'ラインアイテムディスカウントを含む。', - 'Include line item shipping costs.' => 'ラインアイテムの配送料を含む。', - 'Include separate line item tax.' => '個別のラインアイテムの税金を含む。', - 'Included in price?' => '価格に含めますか?', - 'Included' => '込み', - 'Incoming transfer from Transfer ID: ' => '移動IDから入庫予定: ', - 'Incoming' => '入荷', - 'Info' => '情報', - 'Information linked?' => '情報はリンク済み?', - 'Information' => '詳細', - 'Invalid JSON' => 'JSONが無効です', - 'Invalid Order ID' => '無効な注文 ID', - 'Invalid VAT ID.' => 'VAT IDが無効です。', - 'Invalid condition syntax' => '無効な条件構文', - 'Invalid email.' => 'メールアドレスが無効です。', - 'Invalid formula syntax' => '無効な式構文', - 'Invalid gateway: {value}' => '無効なゲートウェイ: {value}', - 'Invalid inventory movements.' => '在庫の移動が無効です。', - 'Invalid order condition syntax.' => '注文条件の構文が無効です。', - 'Invalid payment or order. Please review.' => '無効な支払いまたは注文です。確認してください。', - 'Invalid payment source ID: {value}' => '無効な支払い元 ID: {value}', - 'Invalid store.' => 'ストアが無効です。', - 'Invalid user.' => '無効なユーザーです。', - 'Inventory Item' => '在庫アイテム', - 'Inventory Location' => '在庫場所', - 'Inventory Locations' => '在庫場所', - 'Inventory Tracked' => '追跡済み在庫', - 'Inventory Transfers' => '在庫移動', - 'Inventory could not be set.' => '在庫を設定できませんでした。', - 'Inventory location has committed stock, the order(s) must first be fulfilled.' => '在庫場所にコミット済みの在庫があります。最初に注文を履行する必要があります。', - 'Inventory location has incoming stock, the transfer(s) must first be completed.' => '在庫場所に入庫予定の在庫があります。最初に移動を完了する必要があります。', - 'Inventory location is already deactivated.' => '在庫場所はすでに無効化されています。', - 'Inventory location saved.' => '在庫場所が保存されました。', - 'Inventory locations not saved.' => '在庫場所は保存されていません。', - 'Inventory movement could not be saved.' => '在庫の移動を保存できませんでした。', - 'Inventory movement saved.' => '在庫の移動が保存されました。', - 'Inventory updated.' => '在庫が更新されました。', - 'Inventory was not updated.' => '在庫が更新されませんでした。', - 'Inventory' => '在庫', - 'Invoice amount' => '請求金額', - 'Invoice date' => '請求日', - 'Is Promotable' => '販売促進可能', - 'Is Promotional Price?' => '販売促進価格ですか?', - 'Is Shippable' => '出荷可能', - 'Is Taxable' => '課税対象', - 'Item Rates' => 'アイテムレート', - 'Item Subtotal' => 'アイテム小計', - 'Item Total' => 'アイテム合計', - 'Item' => 'アイテム', - 'Items' => 'アイテム', - 'Kilograms (kg)' => 'キログラム(kg)', - 'Label' => 'ラベル', - 'Landscape' => '横向き', - 'Language' => '言語', - 'Last Name' => '姓', - 'Last Updated' => '最終更新日', - 'Leave a category rate override blank to use the rate from above.' => 'カテゴリ料金の上書きを空白にすると、上記からの料金を使用します。', - 'Leave blank for unlimited uses.' => '無制限に使用するには空にしてください。', - 'Leave blank if products don’t have URLs' => '商品に URL がない場合は空にしてください。', - 'Leave gateway subscription as-is' => 'ゲートウェイのサブスクリプションを現状のままにします', - 'Length ({unit})' => '長さ({unit})', - 'Length' => '長さ', - 'Let each product choose which sites it should be saved to' => '各商品で保存先のサイトを選択する', - 'Limit which orders this discount applies to based on its line items.' => 'ラインアイテムに基づいてどの注文にこのディスカウントが適用されるかを制限します。', - 'Limit which purchasables this sale applies to.' => 'どのパーチャサブルにこのセールが適用されるかを制限します。', - 'Limit' => 'リミット', - 'Line Item Statuses' => 'ラインアイテムステータス', - 'Line Item' => 'ラインアイテム', - 'Line Items' => 'ラインアイテム', - 'Line item price (minus discounts)' => 'ラインアイテム価格(ディスカウントを差し引いた価格)', - 'Line item shipping cost' => 'ラインアイテムの配送料', - 'Line item statuses reordered.' => 'ラインアイテムステータスを並び替えました。', - 'Link Duration' => 'リンクの有効期限', - 'Link Sent' => 'リンクを送信しました', - 'Link to a product' => '商品にリンク', - 'Link to a variant' => 'バリアントにリンク', - 'Link' => 'リンク', - 'Live' => 'ライブ', - 'Location' => '場所', - 'Locations that should be available for previewing products in this product type.' => 'この商品タイプでプレビュー可能にすべき表示場所。', - 'MM' => 'MM', - 'Make a payment' => '支払いを行う', - 'Make this the primary store' => 'これをプライマリストアにする', - 'Manage Inventory' => '在庫を管理', - 'Manage donation settings' => '寄付設定を管理', - 'Manage general store settings' => '一般ストア設定を管理', - 'Manage inventory locations' => '在庫場所を管理', - 'Manage inventory stock levels' => '在庫レベルを管理', - 'Manage inventory transfers' => '在庫移動を管理', - 'Manage orders' => '注文を管理', - 'Manage payment currencies' => '支払い通貨を管理', - 'Manage promotions' => '販売促進を管理', - 'Manage shipping' => '配送を管理', - 'Manage store settings' => 'ストア設定を管理', - 'Manage subscription plans' => '定期支払いプランを管理', - 'Manage subscription' => '定期支払いを管理する', - 'Manage subscriptions' => '定期支払いを管理', - 'Manage taxes' => '税を管理', - 'Manage' => '管理', - 'Mark as Pending' => '保留中としてマーク', - 'Mark as completed' => '完了としてマーク', - 'Match Billing Address' => '請求先住所に一致', - 'Match Customer' => '顧客の一致', - 'Match Order' => '注文に一致', - 'Match Orders' => '注文に一致', - 'Match Product' => '商品を一致', - 'Match Purchasable' => 'パーチャサブルをマッチ', - 'Match Shipping Address' => '配送先住所に一致', - 'Match Variant' => 'バリアントを一致', - 'Matching Items' => 'マッチングアイテム', - 'Max Qty' => '最大数量', - 'Max Uses' => '最大使用回数', - 'Max Variants' => '最大バリアント', - 'Max quantity must greater than min.' => '最大数量は最小数量よりも大きくする必要があります。', - 'Maximum Purchase Quantity' => '最大購入数量', - 'Maximum Total Shipping Cost' => '最大合計送料', - 'Maximum allowed quantity' => '最大許容数量', - 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'このディスカウントの適用に可能となる、マッチングアイテムの最大数。ここでゼロの値を指定すると、この条件はスキップされます。', - 'Maximum order quantity for this item is {num}.' => 'このアイテムの最大注文数量は {num} です。', - 'Message' => 'メッセージ', - 'Meters (m)' => 'メートル(m)', - 'Millimeters (mm)' => 'ミリメートル(mm)', - 'Min Qty' => '最小数量', - 'Min quantity must be less than max.' => '最小数量は最大数量よりも小さくする必要があります。', - 'Minimum Purchase Quantity' => '最小購入数量', - 'Minimum Total Price Strategy' => '最低合計価格の算出方法', - 'Minimum Total Shipping Cost' => '最小合計送料', - 'Minimum allowed quantity' => '最小許容数量', - 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'このディスカウントの適用に必要となる、マッチングアイテムの最小数。', - 'Minimum order quantity for this item is {num}.' => 'このアイテムの最低注文数量は {num} です。', - 'Missing Gateway' => '見つからないゲートウェイ', - 'Missing a default inventory location.' => 'デフォルトの在庫場所がありません。', - 'Move Inventory' => '在庫を移動', - 'Move To' => '移動先', - 'Move {qty} from {fromType} to {toType}' => '{fromType}から{toType}へ{qty}件を移動', - 'Move' => '移動', - 'Movement from deactivated inventory location' => '無効化された在庫場所からの移動', - 'Movement' => '移動', - 'Must have at least one variant.' => '少なくとも 1 つのバリアントが必要です。', - 'Name Field' => '名前フィールド', - 'Name' => '名前', - 'New Customer' => '新規顧客', - 'New Customers' => '新規顧客', - 'New Order' => '新規注文', - 'New PDF' => '新規PDF', - 'New address' => '新規住所', - 'New catalog pricing rule' => '新規カタログ価格ルール', - 'New currency' => '新規通貨', - 'New discount' => '新規ディスカウント', - 'New email' => '新規メール', - 'New gateway' => '新規ゲートウェイ', - 'New line item status' => '新規ラインアイテムステータス', - 'New line items get this status by default when the order is completed' => '注文が完了すると、新しいラインアイテムにはデフォルトでこのステータスが適用されます', - 'New location' => '新しい場所', - 'New order status' => '新規注文ステータス', - 'New orders get this status by default' => '新しい注文はデフォルトでこのステータスが適用されます。', - 'New product type' => '新規商品タイプ', - 'New product' => '新規商品', - 'New product, choose a type' => '新しい商品、タイプを選択してください', - 'New products default to the first tax category available to them. If none are available, this category will be used.' => '新しい商品のデフォルトの税カテゴリになります。利用可能なカテゴリがない場合はこのカテゴリが使用されます。', - 'New sale' => '新規セール', - 'New shipping category' => '新規配送カテゴリ', - 'New shipping method' => '新規配送方法', - 'New shipping rule' => '新規配送ルール', - 'New shipping zone' => '新規配送地域', - 'New subscription plan' => '新規定期支払いプラン', - 'New tax category' => '新規税カテゴリ', - 'New tax rate' => '新規税率', - 'New tax zone' => '新規税対象地域', - 'New transfer' => '新しい移動', - 'New {productType} product' => '新規「{productType}」商品', - 'New' => '新規', - 'Next payment' => '次の支払い', - 'No Address' => '住所がありません', - 'No PDFs exist yet.' => 'PDFがまだありません。', - 'No access given to any specific store management features.' => '特定のストア管理機能へのアクセスがありません。', - 'No additional payment currencies exist yet.' => '追加の支払い通貨がまだありません。', - 'No address' => '住所がありません', - 'No billing address' => '請求先住所がありません', - 'No catalog pricing rule exists with the ID “{id}”' => 'ID「{id}」のカタログ価格ルールは存在しません', - 'No catalog pricing rules exist yet.' => 'カタログ価格ルールはまだ存在しません。', - 'No currency exists with the ID “{id}”' => 'ID「{id}」の通貨は存在しません', - 'No customer email address exists on this cart.' => 'このカートには顧客のメールアドレスは存在しません。', - 'No description' => '説明がありません', - 'No discount exists with the ID “{id}”' => 'ID「{id}」のディスカウントは存在しません', - 'No discounts exist yet.' => 'ディスカウントがまだありません。', - 'No donation amount supplied.' => '寄付の金額が指定されていません。', - 'No emails exist yet.' => 'メールがまだありません。', - 'No inventory changes made.' => '在庫に変更はありません。', - 'No inventory found.' => '在庫が見つかりません。', - 'No inventory movements made.' => '在庫の移動はありません。', - 'No inventory transactions for this location.' => 'この場所の在庫トランザクションはありません。', - 'No new customer selected.' => '新しい顧客が選択されていません。', - 'No order history exists with the ID “{id}”' => 'ID「{id}」の注文履歴は存在しません', - 'No order status history items will exist until the cart becomes an order.' => '注文ステータス履歴はカートが注文に変わった後に有効になります。', - 'No payment source exists with the ID “{id}”' => 'ID「{id}」の支払い元は存在しません', - 'No private Note.' => 'プライベートノートがありません。', - 'No product available.' => '利用できる商品はありません。', - 'No product types exist yet.' => '商品タイプがまだありません。', - 'No purchasable available.' => '利用できるパーチャサブルはありません。', - 'No sale exists with the ID “{id}”' => 'ID「{id}」のセールは存在しません', - 'No sales exist yet.' => 'セールがまだありません。', - 'No shipping address' => '配送先住所がありません', - 'No shipping category exists with the ID “{id}”' => 'ID「{id}」の配送カテゴリは存在しません', - 'No shipping method exists with the ID “{id}”' => 'ID「{id}」の配送方法は存在しません', - 'No shipping rule exists with the ID “{id}”' => 'ID「{id}」の配送ルールは存在しません', - 'No shipping rules exist yet.' => '配送ルールがまだ存在しません。', - 'No shipping zone exists with the ID “{id}”' => 'ID「{id}」の配送地域は存在しません', - 'No stats available.' => '統計情報はありません。', - 'No subscription plan exists with the ID “{id}”' => 'ID「{id}」の定期支払いプランは存在しません', - 'No subscription plans exist yet.' => 'まだ定期支払いプランがありません。', - 'No tax category exists with the ID “{id}”' => 'ID「{id}」の税カテゴリは存在しません', - 'No tax rate exists with the ID “{id}”' => 'ID「{id}」の税率は存在しません', - 'No tax zone exists with the ID “{id}”' => 'ID「{id}」の税対象地域は存在しません', - 'No transactions exist.' => '取引がありません。', - 'No user authenticated.' => '認証済みのユーザーがいません。', - 'No' => 'いいえ', - 'None on hand' => '在庫なし', - 'None' => 'なし', - 'Not a valid address type' => '有効な住所タイプではありません', - 'Not a valid credit card number.' => '有効なクレジットカード番号ではありません。', - 'Not all SKUs are unique.' => 'すべての SKU が一意とは限りません。', - 'Note' => 'ノート', - 'Notes' => 'ノート', - 'Number of Coupons' => 'クーポン数', - 'Number' => '番号', - 'Of the enabled sites above, which sites should products in this product type be saved to?' => '上記の有効なサイトのうち、この商品タイプの商品をどのサイトに保存しますか?', - 'On Hand' => '手持ち', - 'Only allow this gateway to be used for zero value orders?' => 'このゲートウェイを注文金額がゼロの場合にのみ使用できるようにしますか?', - 'Only match certain purchasables…' => '特定のパーチャサブルのみにマッチ…', - 'Only match purchasables related to…' => '次に関連するパーチャサブルのみにマッチ…', - 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => '次の注文ステータスの注文のみが含まれます。すべてのステータスを含めるには空白のままにします。', - 'Only save product to the site they were created in' => '作成したサイトにのみ商品を保存する', - 'Options' => 'オプション', - 'Order Condition Formula' => '注文の条件式', - 'Order Description Format' => '注文の表示フォーマット', - 'Order Details' => '注文の詳細', - 'Order Fields' => '注文フィールド', - 'Order PDF Download Link' => '注文PDFダウンロードリンク', - 'Order PDF Filename Format' => '注文PDFのファイル名フォーマット', - 'Order Reference Number Format' => '注文参照番号フォーマット', - 'Order Settings' => '注文設定', - 'Order Site' => '注文サイト', - 'Order Status description.' => '注文ステータスの説明。', - 'Order Status' => '注文ステータス', - 'Order Statuses' => '注文ステータス', - 'Order can not be empty.' => '注文を空にできません。', - 'Order count' => '注文数', - 'Order customer data removed.' => '注文の顧客データを削除しました。', - 'Order deleted.' => '注文は削除されました。', - 'Order fields saved.' => '注文フィールドは保存されました。', - 'Order not found.' => '注文が見つかりません。', - 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => '注文支払残高は {outstandingBalanceAsCurrency} です。これが最大請求額になります。', - 'Order recalculated.' => '注文が再計算されました。', - 'Order status saved.' => '注文ステータスは保存されました。', - 'Order statuses reordered.' => '注文ステータスを並び替えました。', - 'Order total shipping cost' => '注文合計の配送料', - 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => '注文合計の課税価格(ラインアイテム小計 + 合計ディスカウント + 合計配送料)', - 'Order' => '注文', - 'Orders (Legacy)' => '注文(レガシー)', - 'Orders deleted.' => '注文は削除されました。', - 'Orders not restored.' => '注文は復元されていません。', - 'Orders restored.' => '注文は復元されました。', - 'Orders' => '注文', - 'Organization Name' => '組織名', - 'Organization Tax ID' => '組織税 ID', - 'Origin and destination cannot be the same.' => '起点と宛先は同じにはできません。', - 'Origin' => '原因', - 'Original Price' => '元の価格', - 'Original price' => '元の価格', - 'Original promotional price' => '元の販売促進価格', - 'Other Languages' => 'その他の言語', - 'Other countries' => 'その他の国', - 'Outgoing transfer from Transfer ID: ' => '移動IDから出庫予定: ', - 'Overpaid' => '過払い', - 'Overrides previous?' => '既存を上書きする?', - 'PDF Attachment' => 'PDF添付', - 'PDF Template Path' => 'PDFテンプレートのパス', - 'PDF saved.' => 'PDF は保存されました。', - 'PDF' => 'PDF', - 'PDFs & Emails' => 'PDFとメール', - 'PDFs' => 'PDF', - 'Paid Amount' => '支払い総額', - 'Paid Status' => '支払いステータス', - 'Paid' => '決済完了', - 'Paper Orientation' => '用紙の向き', - 'Paper Size' => '用紙サイズ', - 'Partial payment not allowed.' => '一部支払はできません。', - 'Partial' => '一部', - 'Past year' => '去年', - 'Past {num} days' => '過去 {num} 日間', - 'Pay {amount} of {currency} on the order.' => '注文金額 {amount} {currency} を支払う。', - 'Pay' => '支払う', - 'Payment Amount' => '支払い金額', - 'Payment Currencies' => '支払い通貨', - 'Payment Gateway' => 'ペイメントゲートウェイ', - 'Payment Method' => '支払い方法', - 'Payment error: {message}' => '支払いエラー: {message}', - 'Payment method issue' => '支払い方法の問題', - 'Payment source created.' => '支払い元が作成されました。', - 'Payment source deleted.' => '支払い元は削除されました。', - 'Payments' => '支払い', - 'Pending' => '保留中', - 'Per Email Address Discount Limit' => 'メールアドレスごとのディスカウント限度', - 'Per Item Amount Off' => 'アイテムごとの料金割引', - 'Per Item Discount' => 'アイテムごとのディスカウント', - 'Per Item Percentage Off' => 'アイテムごとのパーセント率割引', - 'Per Item Rate' => 'アイテムごとの料金', - 'Per User Discount Limit' => 'ユーザーごとのディスカウント限度', - 'Percentage Rate' => 'パーセンテージ料金', - 'Phone (Alt)' => '電話 (予備)', - 'Phone' => '電話', - 'Pick a plan' => 'プランを選択', - 'Plain Text Email Template Path' => 'プレーンテキストメールのテンプレートのパス', - 'Plan' => 'プラン', - 'Plans reordered.' => 'プランを並び替えました。', - 'Portrait' => '縦向き', - 'Post Date' => '投稿日', - 'Postal Code Formula' => '郵便番号の式', - 'Pounds (lb)' => 'ポンド(lb)', - 'Preview' => 'プレビュー', - 'Previous Status' => '変更前のステータス', - 'Price' => '価格', - 'Prices' => '価格', - 'Pricing Rules' => '価格ルール', - 'Pricing jobs are currently running.' => '価格設定ジョブは現在実行中です。', - 'Pricing' => '価格設定', - 'Primary Billing Address' => '既定の請求先住所', - 'Primary Shipping Address' => '既定の配送先住所', - 'Primary payment source updated.' => '主な支払い元が更新されました。', - 'Primary' => 'プライマリ', - 'Private Note' => 'プライベートノート', - 'Product Fields' => '商品フィールド', - 'Product ID is required.' => '商品 ID が必要です。', - 'Product Template' => '商品のテンプレート', - 'Product Title Format' => '商品タイトルフォーマット', - 'Product Type' => '商品タイプ', - 'Product Types' => '商品タイプ', - 'Product URI Format' => '商品の URI フォーマット', - 'Product Variant' => '商品バリアント', - 'Product Variants' => '商品バリアント', - 'Product type saved.' => '商品タイプは保存されました。', - 'Product type settings' => '商品タイプ設定', - 'Product' => '商品', - 'Products and Variants deleted.' => '商品とバリアントは削除されました。', - 'Products not restored.' => '商品は復元されていません。', - 'Products restored.' => '商品は復元されました。', - 'Products' => '商品', - 'Promotable' => '販売促進可能', - 'Promotable?' => '販売促進可能?', - 'Promotional Amount' => '販売促進価格', - 'Promotional Price' => '販売促進価格', - 'Purchasable Categories' => 'パーチャサブルカテゴリ', - 'Purchasable ID and Sale ID are required.' => 'パーチャサブル ID とセール ID が必要です。', - 'Purchasable ID is required.' => 'パーチャサブル ID が必要です。', - 'Purchasable Type' => 'パーチャサブルタイプ', - 'Purchasable' => 'パーチャサブル', - 'Purchase (Authorize and Capture Immediately)' => '購入(即時承認とキャプチャ)', - 'Purchase Total' => '合計購入額', - 'Qty' => '数量', - 'Quality Control' => '品質コントロール', - 'Quantity' => '数量', - 'Rate' => '率', - 'Reassign {numOrders, plural, =1{order} other{orders}}' => '{numOrders, plural, =1{注文} other{注文}}を再割り当て', - 'Recalculate order' => '注文を再計算する', - 'Receive Inventory' => '在庫を受け入れ', - 'Receive Transfer' => '移動を受け入れ', - 'Receive' => '受け入れ', - 'Received' => '受け入れ済み', - 'Recent Orders' => '最近の注文', - 'Recipient' => '宛先', - 'Recover Cart' => 'カートを復元', - 'Reduce price' => '価格を下げる', - 'Reduce the price by a fixed amount' => '固定金額で価格を下げる', - 'Reduce the price by a percentage of the original price' => '元の価格のパーセンテージで値引く', - 'Reference' => '参照', - 'Refresh payment history' => '支払い履歴を更新する', - 'Refund note' => '払い戻しメモ', - 'Refund payment' => '払戻し', - 'Refund' => '返金', - 'Reject' => '拒否', - 'Rejected' => '拒否済み', - 'Relationship Type' => '関係付けタイプ', - 'Removable included tax rates are only allowed for the default tax zone.' => '削除可能な税込料金は、デフォルトの税対象地域にのみ使用できます。', - 'Remove address' => '注所を削除', - 'Remove all shipping costs from the order' => '注文のすべての送料を値引きする', - 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => '{numOrders, plural, =1{注文} other{注文}}から顧客の関連付けとメールアドレスを削除してください。以下から削除する追加の顧客データを任意で選択してください', - 'Remove customer data' => '顧客データを削除', - 'Remove from price?' => '価格から削除しますか?', - 'Remove shipping costs for matching items only' => 'マッチングアイテムの送料のみを値引きする', - 'Remove the included tax when a valid organization tax ID is present?' => '有効な組織税 ID が存在する場合、含まれている税金を削除しますか?', - 'Remove' => '削除', - 'Removed' => '削除済み', - 'Repeat Customers' => 'リピート顧客', - 'Reply To' => '返信先', - 'Require Billing Address At Checkout' => 'チェックアウトでは請求先住所が必要です', - 'Require Coupon Code' => 'クーポンコードが必要です', - 'Require Shipping Address At Checkout' => 'チェックアウトでは配送先住所が必要です', - 'Require Shipping Method Selection At Checkout' => 'チェックアウトでは配送方法の選択が必要です', - 'Require' => '必須', - 'Reserved' => '予約済み', - 'Reset usage' => '利用状況をリセットする', - 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'マッチングアイテムの合計購入額が最低額に達した注文のみにディスカウントを制限します。', - 'Revenue Options' => '収益オプション', - 'Revenue' => '収入', - 'Rule' => 'ルール', - 'Rules reordered.' => 'ルールを並び替えました。', - 'SKU' => 'SKU', - 'Safety' => '安全性', - 'Sale Price' => 'セール価格', - 'Sale description.' => 'セールの説明。', - 'Sale reordered.' => 'セールを並び替えました。', - 'Sale saved.' => 'セールが保存されました。', - 'Sale' => 'セール', - 'Sales deleted.' => 'セールを削除しました。', - 'Sales updated.' => 'セールは更新されました。', - 'Sales' => 'セール', - 'Save and continue editing' => '保存して編集を続く', - 'Save and return to all orders' => '保存してすべての注文に戻る', - 'Save and set rules' => '保存してルールを設定する', - 'Save as a new rule' => '新しいルールとして保存', - 'Save product to all sites enabled for this product type' => 'この商品タイプで有効なすべてのサイトに商品を保存する', - 'Save product to other sites in the same site group' => '同じサイトグループ内の他のサイトに商品を保存する', - 'Save product to other sites with the same language' => '同じ言語のサイトに商品を保存する', - 'Save' => '保存', - 'Search customer…' => '顧客を検索...', - 'Search inventory' => '在庫検索', - 'Search or enter customer email…' => '顧客のメールを検索、または入力してください...', - 'Search…' => '検索…', - 'See Orders' => '注文を表示', - 'Select a gateway' => 'ゲートウェイを選択してください', - 'Select a tax category.' => '税カテゴリを選択してください。', - 'Select a tax zone. If empty, this rate will match anywhere.' => '税対象地域を選択してください。空の場合、この税率はすべての地域に適用されます。', - 'Select address' => '住所を選択する', - 'Select an item' => 'アイテムを選択', - 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'パーチャサブルにカタログ価格ルールを適用する方法を選択します。', - 'Select how the sale will be applied to the purchasable(s).' => 'パーチャサブルにセールを適用する方法を選択します。', - 'Select product type' => '商品タイプを選択', - 'Select the emails that will be sent when transitioning to this status.' => 'このステータスに変化する時に送信されるメールを選択してください。', - 'Select what this rate should be applied to.' => 'この税率が適用されるべきものを選択してください。', - 'Send Email' => 'メールを送信する', - 'Send to custom recipient' => '任意の宛先に送信する', - 'Send to the customer' => '顧客に送信する', - 'Set Quantity' => '数量を設定', - 'Set default category' => 'デフォルトのカテゴリを設定', - 'Set default variant' => 'デフォルトバリアントを設定', - 'Set or Adjust' => '設定または調整', - 'Set price' => '価格を設定', - 'Set status' => 'ステータスの設定', - 'Set the price to a flat amount' => '定額に価格を設定する', - 'Set the price to a percentage of the original price' => '元の価格に対するパーセンテージで価格を設定する', - 'Set the sale price to a flat amount' => 'セール価格を定額に設定する', - 'Set the sale price to a percentage of the original price' => '元の価格に対するパーセンテージで価格を設定する', - 'Set to' => '設定する', - 'Settings saved.' => '設定が保存されました。', - 'Settings' => '設定', - 'Share cart…' => 'カートを共有…', - 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => '配送 - 注文価格が配送料を下回る場合、最低料金は配送料金です。', - 'Shipping Address Zone' => '配送先住所地域', - 'Shipping Address' => '配送先住所', - 'Shipping Business Name' => '配送先会社名', - 'Shipping Categories' => '配送カテゴリ', - 'Shipping Category Conditions' => '配送カテゴリの条件', - 'Shipping Category' => '配送カテゴリ', - 'Shipping First Name' => '配送先の名', - 'Shipping Full Name' => '配送先氏名', - 'Shipping Last Name' => '配送先の姓', - 'Shipping Method' => '配送方法', - 'Shipping Methods' => '配送方法', - 'Shipping Rule' => '配送ルール', - 'Shipping Zones' => '配送地域', - 'Shipping address required.' => '配送先住所が必要です。', - 'Shipping categories deleted.' => '配送カテゴリが削除されました。', - 'Shipping category saved.' => '配送カテゴリは保存されました。', - 'Shipping category updated.' => '配送カテゴリが更新されました。', - 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'パーセンテージ料金、アイテムごとの料金、および重量ごとの料金の適用前に、注文全体に追加される送料。この料金を無効にするには、0に設定します。この基本料金を含むルール全体は、カートにデジタル商品などの出荷不可アイテムのみが含まれる場合は一致せず、適用されません。', - 'Shipping method saved.' => '配送方法は保存されました。', - 'Shipping methods and rules deleted.' => '配送方法とルールが削除されました。', - 'Shipping methods updated.' => '配送方法が更新されました。', - 'Shipping rule saved.' => '配送ルールは保存されました。', - 'Shipping zone saved.' => '配送地域は保存されました。', - 'Shipping' => '配送', - 'Short Number' => '短縮番号', - 'Show Chart?' => 'チャートを表示?', - 'Show Order Count?' => '注文数を表示?', - 'Show all prices' => 'すべての価格を表示', - 'Show archived gateways' => 'アーカイブされたゲートウェイを表示', - 'Show order count line on chart.' => 'チャートに注文数の線を表示します。', - 'Show related sales' => '関連するセールを表示', - 'Show rule details' => 'ルールの詳細を表示', - 'Show the Dimensions and Weight fields for products of this type' => 'このタイプの商品に寸法と重量のフィールドを表示する', - 'Show the Title field for products' => '商品のタイトルフィールドを表示する', - 'Show the Title field for variants' => 'バリアントのタイトルフィールドを表示する', - 'Signed In' => 'サインイン済み', - 'Site Languages' => 'サイト言語', - 'Site store mapping saved.' => 'サイトストアマッピングが保存されました。', - 'Sites' => 'サイト', - 'Slug' => 'スラッグ', - 'Snapshot' => 'スナップショット', - 'Snapshots' => 'スナップショット', - 'Some orders restored.' => '一部の注文は復元されました。', - 'Some products restored.' => '一部の商品は復元されました。', - 'Some variants restored.' => 'バリアントの一部が復元されました。', - 'Something changed with the order before payment, please review your order and submit payment again.' => '支払い前に注文が変更されました。注文内容を確認してからもう一度支払いを行ってください。', - 'Sorry, no matching options.' => '申し訳ありません。一致するオプションはありません。', - 'Source - The purchasable relationship field is on the category' => 'ソース - パーチャサブルの関連フィールドはカテゴリにあります', - 'Source' => 'ソース', - 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'ディスカウントが特定の注文に適用される必要があるかどうかを判断する Twig 条件を指定します。(注文は `order` 変数で参照できます。)', - 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => '配送ルールが特定の注文に適用される必要があるかどうかを判断する Twig 条件を指定します。(注文は `order` 変数で参照できます。)', - 'Start Date' => '開始日', - 'State' => '州/都道府県', - 'Status Email Address' => 'ステータスメールアドレス', - 'Status Emails' => 'ステータスメール', - 'Status History' => 'ステータス履歴', - 'Status Updated.' => 'ステータスが更新されました。', - 'Status change message' => 'ステータス更新メッセージ', - 'Status' => 'ステータス', - 'Stock' => '在庫', - 'Stops Processing?' => '追加の設定を無効にする?', - 'Stops subsequent?' => '追加の設定を無効にする?', - 'Store Location' => 'ストアのロケーション', - 'Store Management' => 'ストア管理', - 'Store Markets' => 'ストア市場', - 'Store Rule' => 'ストアルール', - 'Store saved.' => 'ストアが保存されました。', - 'Store' => 'ストア', - 'Stores & Sites' => 'ストアとサイト', - 'Stores' => 'ストア', - 'Strategy to apply when an order is free or has a zero balance.' => '注文が無料、または合計金額がゼロの場合の支払い方法', - 'Strategy to apply when calculating the minimum order price.' => '最低の注文価格を算出する方法', - 'Subject' => '件名', - 'Subscribing user' => '定期支払い中のユーザー', - 'Subscription Fields' => '定期支払いフィールド', - 'Subscription Plans' => '定期支払いプラン', - 'Subscription Settings' => '定期支払い設定', - 'Subscription cancelled.' => '定期支払いをキャンセルしました。', - 'Subscription date' => '定期支払い作成日', - 'Subscription fields saved.' => '定期支払いフィールドは保存されました。', - 'Subscription for {user} to {plan} prevented by a plugin.' => '{user} の {plan} への定期支払いは、プラグインによって阻止されました。', - 'Subscription plan saved.' => '定期支払いプランは保存されました。', - 'Subscription plan' => '定期支払いプラン', - 'Subscription plans' => '定期支払いプラン', - 'Subscription reactivated.' => '定期支払いを再度有効にしました。', - 'Subscription reference' => '定期支払い参照番号', - 'Subscription started.' => '定期支払いを開始しました。', - 'Subscription switched.' => '定期支払いを切り替えました。', - 'Subscription to “{plan}”' => '「{plan}」の定期支払い', - 'Subscription' => '定期支払い', - 'Subscriptions on hold' => '保留中の定期支払い', - 'Subscriptions' => '定期支払い', - 'Suppress emails' => 'メールを表示しない', - 'Switch plan' => 'プランの切り替え', - 'Switch' => '切り替える', - 'System' => 'システム', - 'Table Columns' => 'テーブル列', - 'Target - The category relationship field is on the purchasable' => 'ターゲット - カテゴリ関連フィールドはパーチャサブルにあります', - 'Tax & Shipping' => '税と配送', - 'Tax (inc)' => '税(込み)', - 'Tax Categories' => '税カテゴリ', - 'Tax Category' => '税カテゴリ', - 'Tax Rates' => '税率', - 'Tax Zone' => '税対象地域', - 'Tax Zones' => '税対象地域', - 'Tax categories deleted.' => '税カテゴリが削除されました。', - 'Tax category saved.' => '税カテゴリは保存されました。', - 'Tax category updated.' => '税カテゴリが更新されました。', - 'Tax rate saved.' => '税率は保存されました。', - 'Tax rates updated.' => '税率が更新されました。', - 'Tax zone saved.' => '税対象地域は保存されました。', - 'Tax' => '税', - 'Taxable Subject' => '課税対象', - 'Template Path' => 'テンプレートのパス', - 'That handle is already in use' => 'このハンドルはすでに使用されています', - 'That handle is already in use.' => 'このハンドルはすでに使用されています。', - 'The PDF to attach to this email.' => 'このメールに添付するPDF。', - 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => '定期支払いの請求先情報を更新、および 3DS 認証を処理するページへの URL です。', - 'The address provided is outside the store’s market.' => '提供された住所はストア市場外です。', - 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => '注文全体に適用される割引額。この金額は、割引を使い果たすまで、最高価格から最低価格の順にラインアイテム間で適用されます。', - 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => '基本ディスカウントはカート内の商品の価格がゼロになるか、割引を使い果たすまで適用することができます。注文の金額をマイナスにすることはできません。', - 'The cart recovery link is invalid. Please request a new one.' => 'カート復元リンクが無効です。新しいリンクをリクエストしてください。', - 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => '金額をこの通貨に換算するときに使用される換算レート。例えば、アイテムの価格が{amount1}の場合、換算レートが{rate}であれば代替通貨では{amount2}になります。', - 'The countries that orders are allowed to be placed from.' => '注文が許可されていない国々', - 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'クーポン「{code}」は利用回数上限({limit})を超えています。', - 'The customer for this order has been deleted.' => 'この注文の顧客情報は削除されました。', - 'The default shipping category is automatically available to all product types.' => 'デフォルトの配送カテゴリは、すべての商品タイプで自動的に利用可能です。', - 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'ディスカウント「{name}」は累計利用回数上限({limit})を超えています。', - 'The download link has expired. Please request a new one.' => 'ダウンロードリンクの有効期限が切れました。新しいリンクをリクエストしてください。', - 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => '注文ステータスメールの送信元のメールアドレス。Craftの一般設定で定義されたシステムメールアドレスを使用するには、空白のままにします。', - 'The entry that contains the description for this subscription’s plan.' => 'この定期支払いプランの説明を記載しているエントリ。', - 'The flat value which should discount each item. i.e “3” for $3 off each item.' => '各アイテムを一律にディスカウントする値です。各アイテムから $3 割引であれば「3」とします。', - 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => '{example}など、新しいクーポンの生成で使用されるフォーマット。すべての「#」番号記号はランダムな文字に置き換えられます。', - 'The from and to inventory locations must be different.' => '在庫元と在庫先は異なる必要があります。', - 'The inventory locations this store uses.' => 'このストアが使用する在庫場所。', - 'The item is not enabled for sale.' => 'アイテムのセールは有効化されていません。', - 'The language the order was made in.' => '注文が行われた言語。', - 'The language to be used when this email is rendered.' => 'このメールをレンダリングする際に使用する言語。', - 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'この商品タイプが持つことのできる最大レベル数。いくつでも構わない場合は空白のままにしておいてください。', - 'The maximum the customer should spend on shipping. Set to zero to disable.' => '顧客が支払う送料の最大額。無効にするにはゼロに設定します', - 'The minimum the customer should spend on shipping. Set to zero to disable.' => '顧客が支払う送料の最小額。無効にするにはゼロに設定します', - 'The order is not valid.' => '注文は有効ではありません。', - 'The payment gateway that will be used for the subscription plan.' => '定期支払いプランに使用されるペイメントゲートウェイ。', - 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => '各アイテムからディスカウントされるパーセンタイル値で、{ex2} 割引の場合は{ex1}とします。パーセント率は小数点 2 桁に丸められます。', - 'The previously-selected shipping method is no longer available.' => '以前に選択した配送方法はご利用いただけません。', - 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => '{description}の価格が {originalSalePriceAsCurrency} から {newSalePriceAsCurrency} に上がりました', - 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => '{description}の金額が {originalSalePriceAsCurrency} から {newSalePriceAsCurrency} に下がりました', - 'The primary currency cannot be changed after orders are placed.' => '注文後に基本通貨を変更できません。', - 'The purchasable defines the relationship' => 'パーチャサブルは関係付けを定義します', - 'The purchasable is related by another element' => 'パーチャサブルが別のエレメントに関連しています', - 'The recipient of the email. Twig code can be used here.' => 'メールの受信者。ここに Twig コードを使用できます。', - 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => '返信先メールアドレス。通常のメール送信者への返信の場合は空白のままにします。ここに Twig コードを使用できます。', - 'The site the order was made in.' => '注文が行われたサイト。', - 'The site to be used when this email is rendered.' => 'このメールをレンダリングする際に使用するサイト。', - 'The subject line of the email. Twig code can be used here.' => 'メールの件名。ここに Twig コードを使用できます。', - 'The template that the PDF should be generated from.' => 'PDF を生成するテンプレート。', - 'The template to be used for HTML emails.' => 'HTML メールに使用されるテンプレート。', - 'The template to be used for plain text emails. Twig code can be used here.' => 'プレーンテキスト形式のメールに使用されるテンプレート。ここに Twig コードを使用できます。', - 'The template to use when a product’s URL is requested.' => '商品の URL が要求された場合に使用するテンプレート。', - 'The total number of order adjustments changed.' => '注文合計数のアジャストメントが変更されました。', - 'The total price of the order changed.' => '注文の合計金額が変更されました。', - 'The total quantity of items within the order changed.' => '注文内のアイテムの合計数量が変更されました。', - 'The unique SKU of the donation purchasable.' => '購入可能な寄付の一意のSKU。', - 'The unit of measurement that should be used when specifying product dimensions.' => '商品の寸法を示すために使用する単位。', - 'The unit of measurement that should be used when specifying product weights.' => '商品の重量を示すために使用する単位。', - 'The webhook URL for this gateway.' => 'このゲートウェイの webhook の URL。', - 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => '注文ステータスのメールを送信するときに使用される「差出人」の名前。 Craftの一般設定で定義された送信者名を使用するには、空白のままにします。', - 'There are errors on the order' => '注文にエラーがあります', - 'There are only {num} “{description}” items left in stock.' => '在庫には {num} 個の「{description}」アイテムしか残っていません。', - 'There aren’t any product types to select yet.' => '選択する商品タイプがまだありません。', - 'There is no gateway or payment source available for use with this order.' => 'この注文に利用できるゲートウェイまたは支払い元がありません。', - 'There is no gateway selected that supports payment sources.' => '支払い元をサポートするゲートウェイが選択されていません。', - 'There is no shipping method selected for this order.' => 'この注文に選択された配送方法はありません。', - 'This URL will load the cart into the user’s session, making it the active cart.' => 'この URL はカートをユーザーセッションにロードし、カートを有効にします。', - 'This action is not allowed for the current user.' => 'これは現在のユーザーに許可されていないアクションです。', - 'This category will be used as the default for all purchasables in this store.' => 'このカテゴリは、このストア内のすべてのパーチャサブルのデフォルトとして使用されます。', - 'This coupon is for registered users and limited to {limit} uses.' => 'このクーポンは登録済みユーザーを対象としており、使用回数は {limit} 回に制限されています。', - 'This coupon is limited to {limit} uses.' => 'このクーポンの使用は {limit} 回に制限されています。', - 'This coupon requires an email address.' => 'このクーポンにはメールアドレスが必要です。', - 'This gateway does not support that functionality.' => 'このゲートウェイはその機能をサポートしていません。', - 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'これは、`config/{file}.php` の {setting} 構成設定によって上書きされています。', - 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'これはあなたのストアの場所を示す住所です。配送料や税金などを決定するために、さまざまなプラグインによって使用される場合があります。PDF領収書でも使用できます。', - 'This is the default PDF that will be rendered when requesting the order PDF.' => 'これは、注文 PDF を要求する際に表示されるデフォルトの PDF です。', - 'This is the last location for the {store} store.' => 'これは{store}ストアの最後の場所です。', - 'This month' => '今月', - 'This order has unsaved changes.' => 'この注文には保存されていない変更があります。', - 'This week' => '今週', - 'This year' => '今年', - 'Times Used' => '利用された回数', - 'Title' => 'タイトル', - 'To' => '宛先', - 'Today' => '今日', - 'Too many variants for this product.' => 'この商品のバリアントが多すぎます。', - 'Top Customers by Average Order' => '平均注文別上位顧客', - 'Top Customers by Total Revenue' => '合計収益別上位顧客', - 'Top Customers' => '上位顧客', - 'Top Product Types by Qty Sold' => '販売数量別上位商品タイプ', - 'Top Product Types by Revenue' => '収益別上位商品タイプ', - 'Top Product Types' => '上位商品タイプ', - 'Top Products by Qty Sold' => '販売数量別上位商品', - 'Top Products by Revenue' => '収益別上位商品', - 'Top Products' => '上位商品', - 'Top Purchasables by Qty Sold' => '販売数量別上位パーチャサブル', - 'Top Purchasables by Revenue' => '収益別上位パーチャサブル', - 'Top Purchasables' => '上位パーチャサブル', - 'Total ' => '合計 ', - 'Total Discount Use Limit' => 'ディスカウントの合計使用制限数', - 'Total Discount' => '合計ディスカウント', - 'Total Included Tax' => '税込合計', - 'Total Orders by Billing Country' => '請求先国別合計注文数', - 'Total Orders by Country' => '国別合計注文数', - 'Total Orders by Shipping Country' => '配送先国別合計注文数', - 'Total Orders' => '合計注文数', - 'Total Paid' => '支払い済み合計', - 'Total Price' => '合計金額', - 'Total Qty' => '合計数量', - 'Total Revenue' => '合計収益', - 'Total Shipping' => '合計配送料', - 'Total Tax' => '税額合計', - 'Total Weight' => '合計重量', - 'Total' => '合計', - 'Track Inventory' => '在庫を追跡', - 'Transaction Hash' => '取引ハッシュ', - 'Transaction ID' => '取引 ID', - 'Transaction captured successfully: {message}' => '取引は正しくキャプチャされました: {message}', - 'Transaction refunded successfully: {message}' => '取引は正しく払い戻されました: {message}', - 'Transactions' => '取引', - 'Transfer Fields' => '転送項目', - 'Transfer Items' => '移動アイテム', - 'Transfer Settings' => '移動設定', - 'Transfer Status' => '移動ステータス', - 'Transfer fields saved.' => '移動フィールドは保存されました。', - 'Transfer must have at least one item.' => '移動には少なくとも1つのアイテムが必要です。', - 'Transfer' => '移動', - 'Transfers' => '移動', - 'Trial days credited' => '付与されたトライアル期間', - 'Trial expiration' => 'トライアル期限', - 'Trial expiry date' => 'トライアルの有効期限', - 'Type not in allowed options.' => 'タイプは許可されるオプションにありません。', - 'Type' => 'タイプ', - 'URI' => 'URI', - 'Unable to cancel subscription at this time.' => '現在、定期支払いをキャンセルできません。', - 'Unable to complete order: another request is already in progress.' => '注文を完了できません:別のリクエストが既に進行中です。', - 'Unable to find variant.' => 'バリアントが見つかりません。', - 'Unable to generate coupon codes: {message}' => 'クーポンコードを生成できません: {message}', - 'Unable to make payment at this time.' => '現在のところ、お支払いできません。', - 'Unable to modify subscription at this time.' => '現在、定期支払いを変更できません。', - 'Unable to reactivate subscription at this time.' => '現在、定期支払いを再開できません。', - 'Unable to reassign orders.' => '注文を再割り当てできません。', - 'Unable to remove order data.' => '注文データを削除できません。', - 'Unable to retrieve Sale and Purchasable.' => 'セールとパーチャサブルを取得できません。', - 'Unable to retrieve cart.' => 'カートを取得できません。', - 'Unable to retrieve customer.' => '顧客を取得できません。', - 'Unable to retrieve load cart URL' => 'ロードするカートの URL を取得できません', - 'Unable to retrieve payment source.' => '支払い元を取得できません。', - 'Unable to set default shipping category.' => 'デフォルトの配送カテゴリを設定できません。', - 'Unable to set default tax category.' => 'デフォルトの税カテゴリを設定できません。', - 'Unable to set primary payment source.' => '主な支払い元を設定できません。', - 'Unable to start the subscription. Please check your payment details.' => '定期支払いを開始できません。支払いの詳細を確認してください。', - 'Unable to subscribe at this time.' => '現在、定期支払いを設定できません。', - 'Unable to update cart.' => 'カートを更新できません。', - 'Unable to validate address.' => '住所を確認できません。', - 'Unit Price' => '単価', - 'Unit price (minus discounts)' => '単価(ディスカウントを差し引いた価格)', - 'Units' => '単位', - 'Unpaid' => '未決済', - 'Unsubscribe' => '定期支払い解除', - 'Update Address' => '住所を更新する', - 'Update Order Status' => '注文ステータスを更新する', - 'Update Order Status…' => '注文ステータスを更新…', - 'Update order' => '注文を更新する', - 'Update subscription' => '定期支払いを更新', - 'Update' => '更新', - 'Updated By' => '更新者', - 'Updated committed stock successfully.' => 'コミット済み在庫を正常に更新しました。', - 'Updated' => '更新済み', - 'Use Billing Address For Tax' => '税請求先住所を使用する', - 'Use as the primary billing address' => 'プライマリ請求先住所として使用', - 'Use as the primary shipping address' => 'プライマリ配送先住所として使用', - 'Used By Tax Rates' => '税率に使用されている', - 'Used by Tax Rates' => '税率に使用されている', - 'User Groups' => 'ユーザーグループ', - 'User not found.' => 'ユーザーが見つかりません。', - 'User' => 'ユーザー', - 'Uses' => '使用回数', - 'Validate Business Tax ID as Vat ID' => '事業税IDをVAT IDとして検証する', - 'Validating condition syntax' => '条件構文を検証中', - 'Validating formula syntax' => '式構文を検証中', - 'Variant Fields' => 'バリアントフィールド', - 'Variant Has Untracked Stock' => 'バリアントに追跡されていない在庫があります', - 'Variant Price' => 'バリアントの価格', - 'Variant SKU' => 'バリアントのSKU', - 'Variant Search' => 'バリアントの検索', - 'Variant Stock' => 'バリアントの在庫', - 'Variant Title Format' => 'バリアントのタイトルフォーマット', - 'Variant Tracks Stock' => 'バリアントは在庫を追跡します', - 'Variant UI Label Format' => 'バリアントUIラベルの表示形式', - 'Variant has no product.' => 'バリアントに商品がありません。', - 'Variants not restored.' => 'バリアントは復元されていません。', - 'Variants restored.' => 'バリアントが復元されました。', - 'Variants' => 'バリアント', - 'View customer' => '顧客を表示', - 'View order' => '注文を表示', - 'View product type - {productType}' => '商品タイプを表示 - {productType}', - 'View user' => 'ユーザーを表示', - 'View' => '表示', - 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => '警告。この通貨を削除すると、この通貨でのすべての支払いと返金が停止されます。「{name}」を削除してもよろしいですか?', - 'Web' => 'ウェブ', - 'Webhook URL' => 'Webhook URL', - 'Weight ({unit})' => '重量({unit})', - 'Weight Rate' => '重量ごとの料金', - 'Weight Unit' => '重量の単位', - 'Weight' => '重量', - 'What product URIs should look like for the site.' => 'サイト向けの商品 URI の形式', - 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => '自動生成されたバリアントタイトルの表示方法。{ex1} や {ex2} など、商品プロパティを出力するタグを含めることができます。使用されるすべてのカスタムフィールドは required に設定する必要があります。', - 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => '自動生成されたバリアントタイトルの表示方法。{ex1} や {ex2} など、バリアントプロパティを出力するタグを含めることができます。使用されるすべてのカスタムフィールドは required に設定する必要があります。', - 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => '注文 PDF ファイル名の表示方法(拡張子無し)。{ex1} または {ex2} など、注文プロパティを出力するタグを含めることができます。', - 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'SKU フィールドに値が指定されずに送信された場合に自動生成される一意の SKU の表示方法。{ex1} や {ex2} といった、プロパティを出力するタグを含めることができます。', - 'What this PDF will be called in the control panel.' => 'コントロールパネルに表示するこの PDF の名前。', - 'What this catalog pricing rule will be called in the control panel.' => 'コントロールパネルに表示するこのカタログ価格ルールの名前。', - 'What this discount will be called in the control panel.' => 'コントロールパネルに表示するこのディスカウントの名前。', - 'What this email will be called in the control panel.' => 'コントロールパネルに表示するこのメールの名前。', - 'What this product type will be called in the control panel.' => 'コントロールパネルに表示するこの商品タイプの名前。', - 'What this sale will be called in the control panel.' => 'コントロールパネルに表示するこのセールの名前。', - 'What this shipping category will be called in the control panel.' => 'コントロールパネルに表示するこの配送カテゴリの名前。', - 'What this shipping rule will be called in the control panel.' => 'コントロールパネルに表示するこの配送ルールの名前。', - 'What this shipping zone will be called in the control panel.' => 'コントロールパネルに表示するこの配送地域の名前。', - 'What this status will be called in the control panel.' => 'コントロールパネルに表示するこのステータスの名前。', - 'What this subscription plan will be called in the control panel.' => 'コントロールパネルに表示するこの定期支払いプランの名前。', - 'What this tax category will be called in the control panel.' => 'コントロールパネルに表示するこの税カテゴリの名前。', - 'What this tax zone will be called in the control panel.' => 'コントロールパネルに表示するこの税対象地域の名前。', - 'When this discount is applied to an order, which line items should be discounted?' => 'このディスカウントが注文に適用される場合、どのラインアイテムにディスカウントを適用しますか?', - 'Whether the first available shipping method option should be set automatically on carts.' => '最初に利用可能な配送方法オプションをカートに自動的に設定するかどうか。', - 'Whether the user’s primary payment source should be set automatically on new carts.' => 'ユーザーのプライマリの支払い元を自動的に新しいカートに設定するかどうか。', - 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'ユーザーのプライマリの配送先住所と請求先住所を自動的に新しいカートに設定するかどうか。', - 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => '他の条件に関係なく、このカタログ価格ルールを使用可能にしますか?', - 'Whether this sale should be available for use, regardless of other conditions.' => '他の条件に関係なく、このセールを使用可能にしますか?', - 'Which data to display in the name column in the results table.' => '結果テーブルの名前列に表示するデータ。', - 'Which product types should this category be available to?' => 'このカテゴリを使用できる商品タイプはどれですか?', - 'Which template should be loaded when a product’s URL is requested.' => '商品の URL がリクエストされた場合にロードするテンプレート。', - 'Width ({unit})' => '幅({unit})', - 'Width' => '幅', - 'YYYY' => 'YYYY', - 'Yes' => 'はい', - 'You are not allowed to add a line item.' => 'ラインアイテムを追加する権限がありません。', - 'You currently have no emails configured to select for this status.' => '現在、このステータスに選択できるメールが構成されていません。', - 'You do not have permission to load this cart.' => 'このカートを読み込む権限がありません。', - 'You must set up at least one gateway that supports subscriptions first.' => 'まず、定期支払いをサポートするゲートウェイを少なくとも 1 つセットアップする必要があります。', - 'You must be logged in or provide a valid token to load this cart.' => 'カートを読み込むにはログインするか、有効なトークンを入力してください。', - 'You must be signed in to create a payment source.' => '支払い元を作成するにはサインインする必要があります。', - 'You must be signed in to set a primary payment source.' => '主な支払い元を設定するにはサインインする必要があります。', - 'You must make a payment to complete the order.' => '注文を完了するには、支払いを行う必要があります。', - 'Your Cart Recovery Link' => 'カート復元リンク', - 'Your Order PDF Download Link' => 'お客様の注文PDFダウンロードリンク', - 'Your order is empty' => '注文内容がありません', - 'ZIP file' => 'ZIP ファイル', - 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'ゼロ - ディスカウントが注文価格を上回る場合、最低料金はゼロです。', - 'Zip Code' => '郵便番号', - 'all' => 'すべて', - 'any' => 'いずれか', - 'average order total' => '注文合計平均', - 'billing address' => '請求先住所', - 'donation' => '寄付', - 'donations' => '寄付', - 'info' => '情報', - 'inventory location' => '在庫場所', - 'new customers' => '新規顧客', - 'on hand' => '手持ち', - 'only' => 'のみ', - 'order' => '注文', - 'orders' => '注文', - 'price' => '価格', - 'prices' => '価格', - 'product variant' => '商品バリアント', - 'product variants' => '商品バリアント', - 'product' => '商品', - 'products' => '商品', - 'repeat customers' => 'リピート顧客', - 'shipping address' => '配送先住所', - 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'shippingSameAsBilling と billingSameAsShipping を同時に設定することはできません。', - 'subscription' => '定期支払い', - 'subscriptions' => '定期支払い', - 'to' => 'から', - 'transfer' => '移動', - 'transfers' => '移動', - '{amount} included' => '{amount} 込み', - '{count} Unfulfilled Orders' => '{count}件の履行されていない注文', - '{description} is no longer available.' => '{description}はご利用いただけません。', - '{description} only has {stock} in stock.' => '{description}の在庫数は {stock} のみです。', - '{from} to {to}' => '{from}から{to}', - '{name} (Primary)' => '{name} (既定)', - '{name} (Trashed)' => '{name} (破棄済み)', - '{name} catalog price' => '{name}カテゴリ価格', - '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, =1{注文} other{注文}}が更新されました。', - '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, =1{件の注文} other{件の注文}}が、{numUsers, plural, =1{ユーザー} other{ユーザー}}に関連付けられています。', - '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, =1{件のサブスクリプション} other{件のサブスクリプション}}が、{numUsers, plural, =1{ユーザー} other{ユーザー}}に対して有効化されています。', - '{number} more…' => 'その他 {number}...', - '{pct} off the discounted item price' => 'ディスカウントされたアイテム価格の {pct} 割引', - '{pct} off the original item price' => '元のアイテム価格の {pct} 割引', - '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, =1{は} other{は}}サイトにまだ割り当てられていません。', - '{total} in total revenue' => '合計収益の {total}', - '{total} orders' => '注文数 {total}', - '{total} saleable across {locationCount} location(s)' => '{locationCount}か所の場所全体での{total}件の販売可能品', - '{uses} uses across {emails} email addresses' => '{emails} 個のメールアドレスで {uses} 回使用', - '{uses} uses across {users} users' => '{users} 人のユーザーで {uses} 回使用', - '“{description}” is currently out of stock.' => '現在「{description}」の在庫はありません。', - '“{key}” has invalid JSON' => '「{key}」には無効なJSONがあります', -]; diff --git a/src/translations/nb/commerce.php b/src/translations/nb/commerce.php deleted file mode 100644 index 8230e1dd1d..0000000000 --- a/src/translations/nb/commerce.php +++ /dev/null @@ -1,1428 +0,0 @@ - '(ny pris)', - '(of original price)' => '(av opprinnelig pris)', - '(off original price)' => '(av opprinnelig pris)', - 'A cart number must be specified.' => 'Et handlekurvnummer må spesifiseres.', - 'A cart recovery link has been sent to {email}.' => 'En lenke for gjenoppretting av handlekurv ble sendt til {email}.', - 'A cart recovery link will be sent to {email}.' => 'En lenke for gjenoppretting av handlekurv blir sendt til {email}.', - 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Et vennlig referansenummer blir generert basert på dette formatet når en handlekurv er fullført og blir til en bestilling. F.eks. {ex1}, eller
{ex2}. Resultatet av dette formatet må være unikt.', - 'A new download link has been sent to {email}' => 'En ny lenke for nedlasting er sendt til {email}', - 'A new download link will be sent to {email}' => 'En ny lenke for nedlasting vil bli sendt til {email}', - 'A valid email is required to create a customer.' => 'For å opprette en kunde er det nødvendig med en gyldig e-postadresse.', - 'Accept' => 'Godta', - 'Accepted' => 'Godtatt', - 'Actions' => 'Handlinger', - 'Active Carts' => 'Aktive Handlevogner', - 'Active subscriptions' => 'Aktive abonnementer', - 'Active' => 'Aktiv', - 'Add Address' => 'Legg til adresse', - 'Add a coupon' => 'Legg til en kupong', - 'Add a custom line item' => 'Legg til et tilpasset linjeelement', - 'Add a line item' => 'Legg til en linjevare', - 'Add a product' => 'Legg til et produkt', - 'Add a variant' => 'Legg til en variasjon', - 'Add an adjustment' => 'Legg til en justering', - 'Add an item' => 'Legg til et element', - 'Add an option' => 'Legg til et valg', - 'Add catalog price' => 'Legg til katalogpris', - 'Add' => 'Legg til', - 'Additional Actions' => 'Flere handlinger', - 'Additional recipients that should receive this email. Twig code can be used here.' => 'Flere mottakere av e-posten. Twig-kode kan brukes.', - 'Address 1' => 'Adresse 1', - 'Address 2' => 'Adresse 2', - 'Address 3' => 'Adresse 3', - 'Address Line 1' => 'Adresselinje 1', - 'Address Line 2' => 'Adresselinje 2', - 'Address Updated.' => 'Adresse oppdatert.', - 'Address copied to user.' => 'Adresse kopiert til bruker.', - 'Address not found.' => 'Adresse ikke funnet.', - 'Adjust Quantity' => 'Juster antall', - 'Adjust by' => 'Juster etter', - 'Adjust price when included rate is disqualified?' => 'Justere pris når inkludert sats er diskvalifisert?', - 'Adjustments' => 'Justeringer', - 'Admin Notices' => 'Administratorvarsler', - 'Administrative Area Code of Origin' => 'Opprinnelseskode for administrativt område', - 'Advanced' => 'Avansert', - 'All Orders' => 'Alle ordrer', - 'All Totals' => 'Alle totaler', - 'All Transfers' => 'Alle overføringer', - 'All active subscriptions' => 'Alle aktive abonnementer', - 'All customers' => 'Alle kunder', - 'All products' => 'Alle produkter', - 'All variants must have a SKU.' => 'Alle varianter må ha en SKU.', - 'All' => 'Alle', - 'Allow Checkout Without Payment' => 'Tillat bestilling uten betaling', - 'Allow Empty Cart On Checkout' => 'Tillat tom handlevogn ved bestilling', - 'Allow Partial Payment On Checkout' => 'Tillat delbetaling ved bestilling', - 'Allow out of stock purchases' => 'Tillat kjøp som er utsolgt', - 'Allow' => 'Tillat', - 'Allowed Qty' => 'Tillatt Antall', - 'Alternative Phone' => 'Alternativt telefonnummer', - 'Amount' => 'Mengde', - 'An ID must be provided' => 'Oppgi en ID', - 'An error occurred while generating this PDF.' => 'En feil oppstod under generering av denne PDF-en.', - 'Any' => 'Noen', - 'Anywhere' => 'Overalt', - 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Er du sikker på at du vil arkivere “{name}” abonnementsplan? Den kansellerer IKKE eksisterende abonnementer.', - 'Are you sure you want to capture this transaction?' => 'Er du sikker på at du vil ta denne transaksjonen?', - 'Are you sure you want to complete this order?' => 'Er du sikker på at du vil fullføre ordren?', - 'Are you sure you want to delete the selected orders?' => 'Er du sikker på at du ønsker å slette de valgte ordrene?', - 'Are you sure you want to delete the selected product and its variants?' => 'Er du sikker på at du vil slette det valgte produktet og dets varianter?', - 'Are you sure you want to delete this shipping rule?' => 'Er du sikker på at du vil slette fraktregelen?', - 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Er du sikker på at du ønsker å slette “{name}” og alle tilhørende produkter? Vennligst vær sikker på at du har en sikkerhetskopi av databasen din før du utfører denne ødeleggende handlingen.', - 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Er du sikker på at du vil slette «{name}»? Alle linjevarene med denne statusen vil få ingen status.', - 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Er du sikker på at du vil merke denne overføring som Venter? Dette vil vises som innkommende på destinasjonen.', - 'Are you sure you want to overwrite the billing address?' => 'Er du sikker på at du vil overskrive faktureringsadressen?', - 'Are you sure you want to overwrite the shipping address?' => 'Er du sikker på at du vil overskrive leveringsadressen?', - 'Are you sure you want to permanently delete this store and everything in it?' => 'Er du sikker på at du vil slette denne butikken og alt i den for godt?', - 'Are you sure you want to refund this transaction?' => 'Er du sikker på at du ønsker å refundere denne transaksjonen?', - 'Are you sure you want to remove this customer?' => 'Er du sikker på at du vil fjerne kunden?', - 'Are you sure you want to save this as a new shipping rule?' => 'Er du sikker på at du vil lagre dette som en ny fraktregel?', - 'Are you sure you want to send email: {name}?' => 'Er du sikker på at du vil sende denne e-posten: {name}?', - 'At least one site must be enabled for the product type.' => 'Minst ett nettsted må aktiveres for produkttypen.', - 'Attempted Payments' => 'Forsøkte betalinger', - 'Attention' => 'Vær oppmerksom på', - 'Authorize Only (Manually Capture)' => 'Kun autoriser (ta opp manuelt)', - 'Auto Set Cart Shipping Method Option' => 'Angi fraktalternativ automatisk', - 'Auto Set New Cart Addresses' => 'Angi nye adresser for handlevogn automatisk', - 'Auto Set Payment Source' => 'Angi betalingskilde automatisk', - 'Automatic SKU Format' => 'Automatisk SKU-format', - 'Available Shipping Categories' => 'Tilgjengelige fraktkategorier', - 'Available Tax Categories' => 'Tilgjengelige skattekategorier', - 'Available for purchase' => 'Tilgjengelig for kjøp', - 'Available for purchase?' => 'Tilgjengelig for kjøp?', - 'Available inventory for "{description}" has gone below zero.' => 'Tilgjengelig inventar for «{description}» har gått under null.', - 'Available to Product Types' => 'Tilgjengelig for produkttyper', - 'Available' => 'Tilgjengelig', - 'Available?' => 'Tilgjengelig?', - 'Average Order Total' => 'Gjennomsnittlig ordretotal', - 'Average' => 'Gjennomsnitt', - 'BCC’d Recipient' => 'BCC\'d-mottaker', - 'Bad Request' => 'Problem med forespørsel', - 'Bad address ID.' => 'Feil adresse-ID.', - 'Bad order ID.' => 'Feil ordre-ID.', - 'Base Price' => 'Grunnpris', - 'Base Promotional Price' => 'Grunnpris kampanje', - 'Base Rate' => 'Grunnpris', - 'Base' => 'Base', - 'Bcc' => 'Bcc', - 'Billing Address' => 'Faktureringsadresse', - 'Billing Business Name' => 'Fakturering firmanavn', - 'Billing First Name' => 'Fakturering fornavn', - 'Billing Full Name' => 'Fakturering fullt navn', - 'Billing Last Name' => 'Fakturering etternavn', - 'Billing address required.' => 'Faktureringsadresse nødvendig.', - 'Billing detail update URL' => 'URL for oppdatering av faktureringsinformasjon', - 'Billing issues' => 'Faktureringsproblemer', - 'Billing' => 'Fakturering', - 'Both (Line item price + Line item shipping costs)' => 'Begge (linjepris + forsendelseskostnader for vare i varelinje)', - 'Business ID' => 'Forretnings-ID', - 'Business Name' => 'Forretningsnavn', - 'Business Tax ID' => 'Forretningsskatte-ID', - 'CC’d Recipient' => 'CC\'d-mottaker', - 'CVV' => 'CW', - 'Can be used as an internal reference.' => 'Kan brukes som intern referanse.', - 'Can not complete payment for missing transaction.' => 'Kan ikke fullføre betaling for manglende transaksjon.', - 'Can not create a new order' => 'Kan ikke opprette en ny ordre', - 'Can not find an order to pay.' => 'Kan ikke finne en ordre å betale.', - 'Can not find enabled email.' => 'Kan ikke finne deaktivert e-post.', - 'Can not find order' => 'Kan ikke finne ordre', - 'Can not find order.' => 'Kan ikke finne ordre.', - 'Can not find the transaction to refund' => 'Kan ikke finne transaksjonen som skal refunderes', - 'Can not move between these inventory types.' => 'Kan ikke flyttes mellom disse inventartypene.', - 'Can not refund amount greater than the remaining amount' => 'Kan ikke refundere beløp større enn det opprinnelige beløpet', - 'Cancel subscription' => 'Kansellere abonnement', - 'Cancel with gateway now' => 'Avbryt med gateway nå', - 'Cancel' => 'Kansellere', - 'Cancellation date' => 'Kanselleringsdato', - 'Cancellation' => 'Kansellering', - 'Cannot switch plans for this subscription.' => 'Kan ikke bytte planer for dette abonnementet.', - 'Can’t preview this email.' => 'Kan ikke forhåndsvise e-posten.', - 'Capture payment' => 'Fang betaling', - 'Capture' => 'Ta', - 'Card Holder' => 'Kortholder', - 'Card Number' => 'Kortnummer', - 'Card' => 'Kort', - 'Cart Recovery Link' => 'Lenke for gjenoppretting av handlekurv', - 'Cart forgotten.' => 'Handlevogn glemt.', - 'Cart updated.' => 'Handlevogn oppdatert.', - 'Cart {number}' => 'Handlevogn {number}', - 'Catalog Pricing Rule' => 'Prisregel for katalog', - 'Catalog pricing rule description.' => 'Beskrivelse av prisregel for katalog.', - 'Catalog pricing rule saved.' => 'Prisregel for katalog lagret.', - 'Catalog pricing rules deleted.' => 'Prisregel for katalog slettet.', - 'Catalog pricing rules updated.' => 'Prisregel for katalog oppdatert.', - 'Categories Relationship Type' => 'Kategorienes forholdstype', - 'Categories' => 'Kategorier', - 'Category Rate Overrides' => 'Overstyrer kategorirate', - 'Centimeters (cm)' => 'Centimeter (cm)', - 'Changing this value may affect your ability to refund existing transactions.' => 'Endring av verdien kan ha innvirkning på din mulighet til å tilbakebetale eksisterende transaksjoner.', - 'Choose a color to represent the order’s status' => 'Velg en farge som skal brukes på ordrens status', - 'Choose a new customer' => 'Velg en ny kunde', - 'Choose adjustment values to include when calculating the product revenue total.' => 'Velg justeringsverdier som skal inkluderes under beregning av samlet produktinntekt.', - 'Choose the currency’s ISO code.' => 'Velg ISO-koden for valutaen.', - 'Choose the destination inventory location for the existing on hand stock.' => 'Velg målsted i inventar for eksisterende tilgjengelige lagervarer.', - 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Velg hvilke nettsteder denne produkttypen skal være tilgjengelig på, og konfigurer innstillingene for hvert enkelt nettsted.', - 'City' => 'By', - 'Clear counter' => 'Fjern teller', - 'Clear notices' => 'Fjern merknader', - 'Close' => 'Lukk', - 'Code' => 'Kode', - 'Collated PDF' => 'Sortert PDF', - 'Color' => 'Farge', - 'Commerce Products' => 'Commerce-produkter', - 'Commerce Settings' => 'Commerce innstillinger', - 'Commerce Variants' => 'Salgsvarianter', - 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Salgs-e-post «{email}» kunne ikke sendes for bestilling «{order}».', - 'Commerce order exports' => 'Eksport av handelsordre', - 'Commerce' => 'Handel', - 'Committed' => 'Forpliktet', - 'Completed Email' => 'Fullført e-postadresse', - 'Completed' => 'Fullført', - 'Completing order failed.' => 'Fullføring av ordre mislyktes.', - 'Condition' => 'Tilstand', - 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Her matches betingelsene mot en ordre før reglene blir vurdert. Dette er nyttig hvis du ønsker å kvalifisere en metodes tilgjengelighet tidlig, eller hvis det finnes felles betingelser for alle reglene for denne metoden.', - 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Her matches betingelsene mot ordrens kunde før reglene blir vurdert. Dette er nyttig hvis du ønsker å kvalifisere en metodes tilgjengelighet tidlig, eller hvis det finnes felles betingelser for alle reglene for denne metoden.', - 'Conditions' => 'Betingelser', - 'Contains Purchasables' => 'Inneholder kjøpbare varer', - 'Control Panel Settings' => 'Kontrollpanelinnstillinger', - 'Control panel' => 'Kontrollpanel', - 'Conversion Rate' => 'Konverteringskurs', - 'Converted Price' => 'Konvertert pris', - 'Copied!' => 'Kopiert!', - 'Copy the URL' => 'Kopier URL', - 'Copy to {location}' => 'Kopier til {location}', - 'Copy' => 'Kopier', - 'Costs' => 'Kostnader', - 'Could not archive gateway.' => 'Kunne ikke arkivere portal.', - 'Could not cancel “{reference}”.' => 'Kunne ikke kansellere «{reference}».', - 'Could not create the payment source.' => 'Kunne ikke opprette betalingskilde.', - 'Could not delete shipping rule' => 'Kunne ikke slette regel for forsendelse', - 'Could not delete shipping zone' => 'Kunne ikke slette forsendelsessone', - 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Kunne ikke slette {count, number} frakt{count, plural, one{kategori} other{kategorier}}.', - 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Kunne ikke slette {count, number} frakt{count, plural, one{metode} other{metoder}} og regler.', - 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Kunne ikke slette {count, number} skatte{count, plural, one{kategori} other{kategorier}}.', - 'Could not find the email or template.' => 'Kunne ikke finne e-posten eller malen.', - 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Kunne ikke merke ordren {number} som komplett. Lagring av bestilling mislyktes under fullføring av ordren med feil: {order}', - 'Could not reactivate “{reference}”.' => 'Kunne ikke reaktivere «{reference}».', - 'Could not send email' => 'Kunne ikke sende e-post', - 'Could not switch “{reference}” to “{plan}”.' => 'Kunne ikke bytte «{reference}» til «{plan}».', - 'Could not update orders address.' => 'Kunne ikke oppdatere ordreadresser.', - 'Couldn’t archive Line Item Status.' => 'Kunne ikke arkivere linjevarestatus.', - 'Couldn’t archive Order Status.' => 'Kunne ikke arkivere ordrestatus.', - 'Couldn’t capture transaction.' => 'Kunne ikke fange transaksjon.', - 'Couldn’t capture transaction: {message}' => 'Kunne ikke fange transaksjon: {message}', - 'Couldn’t delete email.' => 'Kunne ikke slette e-post.', - 'Couldn’t delete the payment source.' => 'Kunne ikke slette betalingskilde.', - 'Couldn’t get order.' => 'Kunne ikke hente ordre.', - 'Couldn’t recalculate order.' => 'Kunne ikke beregne ordre på nytt.', - 'Couldn’t refund transaction.' => 'Kunne ikke refundere transaksjon.', - 'Couldn’t refund transaction: {message}' => 'Kunne ikke refundere transaksjon: {message}', - 'Couldn’t reorder Line Item Statuses.' => 'Kunne ikke endre rekkefølgen på linjevarestatuser.', - 'Couldn’t reorder Order Statuses.' => 'Kunne ikke endre rekkefølgen på ordrestatuser.', - 'Couldn’t reorder PDFs.' => 'Kunne ikke endre rekkefølgen på PDF-er.', - 'Couldn’t reorder discounts.' => 'Kunne ikke endre rekkefølgen på rabatter.', - 'Couldn’t reorder gateways.' => 'Kunne ikke bestille portaler på nytt.', - 'Couldn’t reorder plans.' => 'Kunne ikke bestille planer på nytt.', - 'Couldn’t reorder rules.' => 'Kunne ikke endre rekkefølgen på regler.', - 'Couldn’t reorder sale.' => 'Kunne ikke endre rekkefølgen på salg.', - 'Couldn’t reorder sales.' => 'Kunne ikke endre rekkefølgen på salg.', - 'Couldn’t reorder statuses.' => 'Kunne ikke endre rekkefølgen på statuser.', - 'Couldn’t reorder stores.' => 'Kunne ikke endre rekkefølgen på butikker.', - 'Couldn’t save PDF.' => 'Kunne ikke lagre PDF.', - 'Couldn’t save catalog pricing rule.' => 'Kunne ikke lagre prisregel for katalog.', - 'Couldn’t save currency.' => 'Kunne ikke lagre valuta.', - 'Couldn’t save discount.' => 'Kunne ikke lagre rabatt.', - 'Couldn’t save email.' => 'Kunne ikke lagre e-post.', - 'Couldn’t save gateway.' => 'Kunne ikke lagre portal.', - 'Couldn’t save inventory location.' => 'Kunne ikke lagre inventarsted.', - 'Couldn’t save line item status.' => 'Kunne ikke lagre linjevarestatus.', - 'Couldn’t save order fields.' => 'Kunne ikke lagre ordrefelt.', - 'Couldn’t save order status.' => 'Kunne ikke lagre ordrestatus.', - 'Couldn’t save order.' => 'Kunne ikke lagre ordre.', - 'Couldn’t save product type.' => 'Kunne ikke lagre produkttype.', - 'Couldn’t save sale.' => 'Kunne ikke lagre salg.', - 'Couldn’t save settings.' => 'Kunne ikke lagre innstillinger.', - 'Couldn’t save shipping category.' => 'Kunne ikke lagre fraktkategori.', - 'Couldn’t save shipping method.' => 'Kunne ikke lagre fraktmetode.', - 'Couldn’t save shipping rule.' => 'Kunne ikke lagre fraktregel.', - 'Couldn’t save shipping zone.' => 'Kunne ikke lagre fraktsone.', - 'Couldn’t save store.' => 'Kunne ikke lagre butikk.', - 'Couldn’t save subscription fields.' => 'Kunne ikke lagre abonnementsfelter.', - 'Couldn’t save subscription plan.' => 'Kunne ikke lagre abonnementsplan.', - 'Couldn’t save subscription.' => 'Kunne ikke lagre abonnement.', - 'Couldn’t save tax category.' => 'Kunne ikke lagre avgiftskategori.', - 'Couldn’t save tax rate.' => 'Kunne ikke lagre skattesats.', - 'Couldn’t save tax zone.' => 'Kunne ikke lagre avgiftssone.', - 'Couldn’t save transfer fields.' => 'Kunne ikke lagre overføringsfelt.', - 'Couldn’t update catalog pricing rule statuses.' => 'Kunne ikke oppdatere status på prisregel for katalog.', - 'Couldn’t update status.' => 'Kunne ikke oppdatere status.', - 'Couldn’t updated sales status.' => 'Kunne ikke oppdatere salgstatus.', - 'Country Code of Origin' => 'Opprinnelseskode for land', - 'Country List' => 'Landliste', - 'Country not allowed.' => 'Land ikke tillatt.', - 'Country' => 'Land', - 'Coupon Code' => 'Rabattkode', - 'Coupon can not apply discount to this order due to address mismatch.' => 'Kupongen kan ikke brukes til å gi rabatt på denne ordren på grunn av adresseavvik.', - 'Coupon can not apply discount to this order due to customer mismatch.' => 'Kupongen kan ikke brukes til å gi rabatt på denne ordren på grunn av kundeavvik.', - 'Coupon can not apply discount to this order.' => 'Kupongen kan ikke brukes til å gi rabatt på denne ordren.', - 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Kupongkoden «{code}» er allerede i bruk av rabatten «{name}».', - 'Coupon codes cannot be blank.' => 'Kupongkoder kan ikke være blank.', - 'Coupon codes must be unique.' => 'Kupongkoder må være unik.', - 'Coupon format is required and must contain at least one `#`.' => 'Kupongformat kreves og må inneholde minst én \'#\'.', - 'Coupon not valid.' => 'Kupongen er ikke gyldig.', - 'Coupon removed: {explanation}' => 'Kupong fjernet: {explanation}', - 'Coupons' => 'Kuponger', - 'Craft Commerce - Administration' => 'Craft Commerce – Administrasjon', - 'Craft Commerce - Inventory' => 'Craft Commerce – Inventar', - 'Craft Commerce - Orders' => 'Craft Commerce – Ordrer', - 'Craft Commerce - Product Type - {name}' => 'Craft Commerce – Produkttype – {name}', - 'Craft Commerce - Subscriptions' => 'Craft Commerce – Abonnementer', - 'Create a Discount' => 'Opprett en rabatt', - 'Create a Subscription Plan' => 'Opprett en abonnementsplan', - 'Create a new PDF' => 'Opprett en ny PDF', - 'Create a new catalog pricing rule' => 'Opprett en ny prisregel for katalog', - 'Create a new currency' => 'Lag en ny valuta', - 'Create a new email' => 'Opprett en ny e-post', - 'Create a new gateway' => 'Opprett en ny portal', - 'Create a new line item status' => 'Opprett en ny linjevarestatus', - 'Create a new order status' => 'Opprett en ny ordrestatus', - 'Create a new product type' => 'Opprett ny produkttype', - 'Create a new sale' => 'Opprett et nytt salg', - 'Create a new shipping category' => 'Opprett en ny fraktkategori', - 'Create a new shipping method' => 'Opprett en ny fraktmetode', - 'Create a new shipping rule' => 'Opprett en ny fraktregel', - 'Create a new tax category' => 'Opprett en ny avgiftskategori', - 'Create a new tax rate' => 'Opprett en ny skattesats', - 'Create a product type' => 'Opprett en produkttype', - 'Create a shipping zone' => 'Opprett en fraktsone', - 'Create a tax zone' => 'Opprett en avgiftssone', - 'Create catalog pricing rules' => 'Opprett prisregler for katalog', - 'Create customer: “{email}”' => 'Opprett kunde: «{email}»', - 'Create discounts' => 'Opprett rabatter', - 'Create discount…' => 'Opprett rabatt ...', - 'Create rules that allow this discount to match the order.' => 'Opprett regler som lar denne rabatten matche bestillingen.', - 'Create rules that allow this discount to match the order’s billing address.' => 'Opprett regler som lar denne rabatten matche faktureringsadressen.', - 'Create rules that allow this discount to match the order’s customer.' => 'Opprett regler som lar denne rabatten matche kunden.', - 'Create rules that allow this discount to match the order’s shipping address.' => 'Opprett regler som lar denne rabatten matche leveringsadressen.', - 'Create rules that allow this gateway to match the billing address.' => 'Opprett regler som lar denne portalen matche faktureringsadressen.', - 'Create rules that allow this gateway to match the order.' => 'Opprett regler som lar denne portalen matche ordren.', - 'Create rules that allow this gateway to match the shipping address.' => 'Opprett regler som lar denne portalen matche faktureringsadressen.', - 'Create sales' => 'Opprett salg', - 'Create sale…' => 'Opprett salg …', - 'Created' => 'Opprettet', - 'Credit Card Payment Type' => 'Type kredittkortbetaling', - 'Currency Code' => 'Valutakode', - 'Currency saved.' => 'Valuta lagret.', - 'Currency' => 'Valuta', - 'Current' => 'Gjeldende', - 'Custom 1' => 'Kunde 1', - 'Custom 2' => 'Kunde 2', - 'Custom 3' => 'Kunde 3', - 'Custom 4' => 'Kunde 4', - 'Custom' => 'Egendefinert', - 'Customer Enabled?' => 'Kunde aktivert?', - 'Customer ID is required.' => 'Kunde-ID er nødvendig.', - 'Customer Note' => 'Kundemerknad', - 'Customer Notices' => 'Kundemerknader', - 'Customer data' => 'Kundedata', - 'Customer' => 'Kunde', - 'Damaged' => 'Skadet', - 'Data shown might be outdated.' => 'Vist data kan være utdatert.', - 'Date Authorized' => 'Dato for autorisasjon', - 'Date Created' => 'Dato Opprettet', - 'Date First Paid' => 'Første betalingsdato', - 'Date Ordered' => 'Dato Bestilt', - 'Date Paid' => 'Dato Betalt', - 'Date Updated' => 'Dato Oppdatert', - 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Dato når prisregel for katalog vil aktiveres. La stå tomt for ubegrenset startdato', - 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Datoen som rabatten vil aktiveres. La stå tom for ubegrenset startdato', - 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Dato når salget vil aktiveres. La stå tomt for ubegrenset startdato', - 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Dato når prisregel for katalog avsluttes. La stå tom for ubegrenset sluttdato', - 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Dato når rabatten avsluttes. La stå tom for ubegrenset sluttdato', - 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Dato når salget avsluttes. La stå tom for ubegrenset sluttdato', - 'Date' => 'Dato', - 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Standard – la prisen være negativ hvis rabatter er større en ordreverdien.', - 'Default Category' => 'Standardkategori', - 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Standard visning av Commerce-kontrollpanelet. Hvis brukeren ikke har tillatelse, vil de bli tatt tilbake til et sted de har tilgang til.', - 'Default Order PDF' => 'Standard ordre-PDF', - 'Default Per Item Rate' => 'Standard enhetspris', - 'Default Percentage Rate' => 'Standard prosentpris', - 'Default Status?' => 'Standardstatus?', - 'Default View' => 'Standardvisning', - 'Default Weight Rate' => 'Standard vektpris', - 'Default Zone' => 'Standard sone', - 'Default status?' => 'Standardstatus?', - 'Default to this tax zone when no billing address is set' => 'Sett dette som standard skattesone når ingen fakturaadresse er satt', - 'Default to this tax zone when no shipping address is set' => 'Standard til denne avgiftssonen når ingen leveringsadresse er satt', - 'Default variant updated.' => 'Standardvariant oppdatert.', - 'Default' => 'Standard', - 'Default?' => 'Standard?', - 'Delete catalog pricing rules' => 'Slett prisregler for katalog', - 'Delete discounts' => 'Slett rabatter', - 'Delete orders' => 'Slett ordrer', - 'Delete sales' => 'Slett salg', - 'Delete' => 'Slett', - 'Deleting the {location} location.' => 'Sletter stedet {location}.', - 'Describe this rule.' => 'Beskriv denne regelen.', - 'Describe this shipping zone.' => 'Beskriv denne fraktsonen.', - 'Describe this tax zone.' => 'Beskriv denne avgiftssonen.', - 'Description' => 'Beskrivelse', - 'Destination Inventory Location' => 'Målsted i inventar', - 'Destination' => 'Destinasjon', - 'Details' => 'Detaljer', - 'Dimension Unit' => 'Dimensjonsenhet', - 'Dimensions' => 'Dimensjoner', - 'Disabled' => 'Deaktivert', - 'Disallow' => 'Ikke tillat', - 'Discount all line items' => 'Rabatt på alle linjevarer', - 'Discount description.' => 'Beskrivelse av rabatt', - 'Discount is not allowed for the order' => 'Rabatter er ikke tillat for ordren', - 'Discount is out of date.' => 'Rabatt er utdatert.', - 'Discount saved.' => 'Rabatt lagret.', - 'Discount the matching items only' => 'Gi rabatt bare på matchende varer', - 'Discount use has reached its limit.' => 'Rabattbruk har nådd grensen.', - 'Discount' => 'Rabatt', - 'Discounted Item Subtotal' => 'Rabbatert delsum for vare', - 'Discounted Items' => 'Rabatterte artikler', - 'Discounts deleted.' => 'Rabatter slettet.', - 'Discounts reordered.' => 'Rabatter har ny rekkefølge.', - 'Discounts updated.' => 'Rabatter oppdatert.', - 'Discounts' => 'Rabatter', - 'Disqualify with valid business tax ID?' => 'Diskvalifisere med gyldig ID for virksomhetsskatt?', - 'Do not apply subsequent matching sales beyond applying this sale.' => 'Benytt ikke påfølgende overensstemmende salg utover dette salget.', - 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Ikke bruk denne satsen hvis ordreadressen har noen av de valgte gyldige ID-ene for virksomhetsskatt.', - 'Do not attach a PDF to this email' => 'Ikke legg ved en PDF til denne e-posten', - 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Ikke beregn ordre på nytt (nummer: {orderNumber}) hvis det er problemer.', - 'Donation can not be zero.' => 'Donasjon kan ikke være null.', - 'Donation needs to be an amount.' => 'Donasjon må være et beløp.', - 'Donation settings saved.' => 'Innstillinger for donasjon er lagret.', - 'Donation' => 'Donasjon', - 'Donations' => 'Donasjoner', - 'Done' => 'Ferdig', - 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Ikke bruk påfølgende rabatter på en ordre hvis denne rabatten er brukt', - 'Download PDF' => 'Last ned PDF', - 'Download PDF…' => 'Last ned PDF …', - 'Download Type' => 'Type nedlastning', - 'Download' => 'Last ned', - 'Draft' => 'Utkast', - 'Dummy gateway payment failed.' => 'Dummy gateway-betaling mislyktes.', - 'Duplicate options exist' => 'Identiske valg eksisterer', - 'Duration' => 'Varighet', - 'EU VAT ID' => 'EU VAT-ID', - 'Edit address' => 'Rediger adresse', - 'Edit adjustments' => 'Rediger justeringer', - 'Edit catalog pricing rules' => 'Rediger prisregler for katalog', - 'Edit discounts' => 'Rediger rabatter', - 'Edit options' => 'Rediger valg', - 'Edit orders' => 'Rediger ordrer', - 'Edit sales' => 'Rediger salg', - 'Edit' => 'Rediger', - 'Effect' => 'Effekt', - 'Either (Default) - The relationship field is on the purchasable or the category' => 'Både (standard) – forholdsfeltet er på Kan kjøpes-artikkelen eller kategorien', - 'Either way' => 'Uansett', - 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Generering av PDF i e-post parse error for e-post «{email}». Ordre: «{order}». PDF-malfeil: «{message}» {file}:{line}', - 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'E-post PDF-mal finnes ikke på «{templatePath}» for e-post «{email}». Ordre: «{order}».', - 'Email Subject' => 'E-postens emne', - 'Email error. No email address found for order. Order: “{order}”' => 'Feil med e-post. Ingen e-postadresse funnet for bestilling. Bestilling: «{order}»', - 'Email is not enabled.' => 'E-post er ikke aktivert.', - 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'E-post-mal for tekst finnes ikke i «{templatePath}» og resulterte i «{templateParsedPath}» for e-post «{email}». Ordre: «{order}».', - 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal for tekst parse error for e-post «{email}». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', - 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmalbane for tekst parse error for e-post «{email}» i «Malbane». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', - 'Email required to make payments on a completed order.' => 'E-post påkrevet for å foreta betalinger for en fullført bestilling.', - 'Email saved.' => 'E-post lagret.', - 'Email sent' => 'E-post sendt', - 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'E-post-mal finnes ikke i «{templatePath}» og resulterte i «{templateParsedPath}» for e-post «{email}». Ordre: «{order}».', - 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal parse error for tilpasset e-post «{email}» i «Til:». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', - 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal parse error for e-post «{email}» i «BCC:». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', - 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal parse error for e-post «{email}» i «CC:». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', - 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal parse error for e-post «{email}» i «ReplyTo:». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', - 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal parse error for e-post «{email}» i «Emne:». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', - 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmal parse error for e-post «{email}». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', - 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'E-postmalbane parse error for e-post «{email}» i «Malbane:». Ordre: «{order}». Malfeil: «{message}» {file}:{line}', - 'Email unavailable.' => 'E-post utilgjengelig.', - 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'E-post «{email}» kunne ikke sendes for ordre «{order}». Error: {error} {file}:{line}', - 'Email “{email}” for order {order} was cancelled.' => 'E-post «{email}» for ordre {order} ble kansellert.', - 'Email' => 'Epost', - 'Emails' => 'E-poster', - 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Aktiver hvis denne satsen skal implementeres i den skattbare produktprisen i stedet for å legge et påslag til ordren.', - 'Enable structure for products of this type' => 'Aktiver struktur for produkter av denne typen', - 'Enable this discount' => 'Aktiver denne rabatten', - 'Enable this rule' => 'Deaktiver denne regelen', - 'Enable this sale' => 'Aktiver dette salget', - 'Enable this shipping method on the front end' => 'Aktiver denne leveringsmetoden på frontsiden', - 'Enable this shipping rule' => 'Aktiver denne fraktregelen', - 'Enable this tax rate' => 'Rediger denne skattesatsen', - 'Enabled for customers to select during checkout?' => 'Aktivert slik at kunder kan velge det under utsjekking?', - 'Enabled for customers to select?' => 'Aktivert for å kunne velges av kunder?', - 'Enabled' => 'Aktivert', - 'Enabled?' => 'Aktivert', - 'End Date' => 'Sluttdato', - 'Enter SKU' => 'Angi SKU', - 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Skriv et menneskevennlig navn for denne skattesatsen som skal brukes i kontrollpanelet.', - 'Enter a percentage like {ex1} or {ex2}.' => 'Skriv inn en prosent, som {ex1} eller {ex2}.', - 'Enter coupon code' => 'Skriv inn kupongkode', - 'Enter reference' => 'Skriv inn referanse', - 'Error refunding transaction: {transactionHash}' => 'En feil oppstod under tilbakebetalingstransaksjonen: {transactionHash}', - 'Every new store must be assigned to at least one site.' => 'Alle nye butikker må tilordnes minst ett nettsted.', - 'Everywhere' => 'Overalt', - 'Example' => 'Eksempel', - 'Exclude this discount for products that are already on promotion' => 'Ekskluder denne rabatten for produkter som allerede er med i en kampanje', - 'Expired Link' => 'Utløpt lenke', - 'Expired' => 'Utløpt', - 'Expiry Date' => 'Utløpsdato', - 'Expiry date' => 'Utløpsdato', - 'Expiry' => 'Utløpsdato', - 'Failed to receive transfer: {error}' => 'Kunne ikke hente overføring: {error}', - 'Failed to send email. Please try again.' => 'Kunne ikke sende e-post. Prøv igjen.', - 'Failed to start' => 'Kunne ikke starte', - 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Kunne ikke oppdatere {num, plural, =1{ordrestatus} other{ordrestatuser}}.', - 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Kunne ikke oppdatere ordrestatus for {num, plural, =1{ordre} other{ordrer}}.', - 'Feet (ft)' => 'Fot (ft)', - 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Filterbetingelser som beskriver hvilke ordre denne regelen gjelder. Skriv 0 for å hoppe over en betingelse.', - 'First Name' => 'Fornavn', - 'Flat Amount Off Order' => 'Flatt rabattbeløp på ordre', - 'Flat Order Discount Amount Off' => 'Beløp på flat ordrerabattsats', - 'Free Order Payment Strategy' => 'Betalingsstrategi for gratis ordre', - 'Free Shipping' => 'Gratis levering', - 'Free orders are processed by the payment gateway' => 'Gratisordrer behandles av betalingsportalen', - 'Free orders complete immediately' => 'Gratisordrer fullføres umiddelbart', - 'Free shipping can only be for whole order or matching items, not both.' => 'Gratis frakt kan bare gjelde for hele ordrer eller matchende varer, ikke begge deler.', - 'From Name' => 'Fra Navn', - 'Fulfill' => 'Fullfør', - 'Fulfilled' => 'Fullført', - 'Fulfillment' => 'Fullbyrdelse', - 'Full Name' => 'Fullt navn', - 'Gateway Code' => 'Portalkode', - 'Gateway Message' => 'Portalmelding', - 'Gateway Reference' => 'Portalreferanse', - 'Gateway Response' => 'Portalrespons', - 'Gateway doesn’t support authorize' => 'Systemport støtter ikke autorisering', - 'Gateway doesn’t support partial refunds.' => 'Portalen støtter ikke delvise tilbakebetalinger.', - 'Gateway doesn’t support purchase' => 'Portalen støtter ikke kjøp', - 'Gateway doesn’t support refunds.' => 'Portalen støtter ikke tilbakebetalinger.', - 'Gateway saved.' => 'Portal lagret.', - 'Gateway' => 'Port', - 'Gateways reordered.' => 'Portaler har ny rekkefølge.', - 'Gateways' => 'Portaler', - 'General Settings' => 'Generelle innstillinger', - 'General' => 'Generelt', - 'Generate' => 'Genrerer', - 'Generated Coupon Format' => 'Generert kupongformat', - 'Grams (g)' => 'Gram (g)', - 'Groups for which this sale will be applicable to.' => 'Grupper som dette salget gjelder for.', - 'HTML Email Template Path' => 'HTML E-postmal Vei', - 'Handle' => 'Håndter', - 'Harmonized System Code' => 'Harmonisert systemkode', - 'Has Admin Notices' => 'Har administratorvarsler', - 'Has Emails?' => 'Har e-poster?', - 'Has Free Shipping' => 'Har gratis frakt', - 'Has Orders' => 'Har ordre', - 'Has Purchasable' => 'Har Kan kjøpes-artikler', - 'Has Variants?' => 'Har varianter?', - 'Height ({unit})' => 'Høyde ({unit})', - 'Height' => 'Høyde', - 'Hide snapshot' => 'Skjul snapshot', - 'History' => 'Historikk', - 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Hvor lenge (i sekunder) en PDF-nedlastingslenke skal være gyldig før den utløper. Standard er 86400 sekunder (24 timer).', - 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Hvor mange ganger en e-postadresse har lov til å bruke denne rabatten. Dette gjelder alle tidligere bestillinger, enten for gjest eller bruker. Sett til null for ubegrenset bruk av gjester eller brukere.', - 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Antall ganger en bruker har lov til å bruke denne rabatten. Hvis dette er satt til noe annet enn null, vil rabatten kun være tilgjengelig for påloggede brukere.', - 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Antall ganger denne rabatten kan bli brukt av gjester eller påloggede brukere. Sett til null for ubegrenset bruk.', - 'How products should be labeled within the control panel.' => 'Slik skal oppføringer merkes i kontrollpanelet.', - 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'Måten kjøpbare artikler og kategorier er tilknyttet, noe som fastlegger samsvarende varer. Se [Relasjonsterminologi]({link}).', - 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'Hvordan dette produktet vil bli beskrevet på varelinjen i en bestilling. Du kan inkludere koder som utgangsegeneskaper, slik som {ex1} eller {ex2}', - 'How this shipping method will be referred to in templates and forms.' => 'Hvordan denne leveringsmetoden vil refereres til i malene og skjemaene.', - 'How variants should be labeled within the control panel.' => 'Slik skal varianter merkes i kontrollpanelet.', - 'How you’ll refer to this PDF in the templates.' => 'Måten du henviser til denne PDF-en i malen.', - 'How you’ll refer to this product type in the templates.' => 'Hvordan du henviser til denne produkttypen i malene.', - 'How you’ll refer to this shipping category in the templates.' => 'Hvordan du refererer til denne fraktkategorien i malene.', - 'How you’ll refer to this status in the templates.' => 'Hvordan du henviser til denne statusen i malene.', - 'How you’ll refer to this subscription plan in the templates.' => 'Hvordan du refererer til denne abonnementsplanen i malene.', - 'How you’ll refer to this tax category in the templates.' => 'Hvordan du henviser til denne skattekategorien i malene.', - 'ID' => 'ID', - 'IP Address' => 'IP-adresse', - 'If disabled, this PDF will not be available or sent with emails.' => 'Hvis deaktivert, vil denne PDF-en verken være tilgjengelig eller sendes med e-poster.', - 'If disabled, this email will not send.' => 'Hvis deaktivert, blir denne e-posten ikke sendt.', - 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Hvis aktivert, og denne skattesatsen ikke samsvarer med bestillingen, blir satsbeløpet fjernet fra produktprisen i handlekurven.', - 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Hvis den settes til "Kun ved autorisasjon", må du manuelt hente inn betalinger før beløpet overføres til kontoen din. Gateway må støtte det valgte alternativet.', - 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Hvis du velger at prosenten skal være «avslag på rabattert pris», vil det inkludere «Beløp per vare» i tillegg til andre rabatter som er lagt til tidligere.', - 'Ignore Promotions?' => 'Ignorere kampanjer?', - 'Ignore previous matching sales if this sale matches.' => 'Ignorere forrige samsvarende salg hvis dette salget samsvarer.', - 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignorer kampanjepriser når rabatten er brukt på samsvarende linjevarer', - 'Inactive Carts' => 'Inaktive vogner', - 'Inches (in)' => 'Tommer (")', - 'Include built-in line item tax.' => 'Inkluder innebygd linjevareavgift.', - 'Include in price?' => 'Inkluder i pris?', - 'Include line item discounts.' => 'Inkluder linjevarerabatt.', - 'Include line item shipping costs.' => 'Inkluder forsendelseskostnader for linjevare.', - 'Include separate line item tax.' => 'Inkluder separat linjevareavgift.', - 'Included in price?' => 'Inkludert i pris?', - 'Included' => 'Inkludert', - 'Incoming transfer from Transfer ID: ' => 'Innkommende overføring fra overførings-ID: ', - 'Incoming' => 'Innkommende', - 'Info' => 'Info', - 'Information linked?' => 'Informasjon lenket?', - 'Information' => 'Informasjon', - 'Invalid JSON' => 'Ugyldig JSON', - 'Invalid Order ID' => 'Ugyldig ordre-ID', - 'Invalid VAT ID.' => 'Ugyldig MVA-ID.', - 'Invalid condition syntax' => 'Ugyldig tilstandssyntaks', - 'Invalid email.' => 'Ugyldig e-post.', - 'Invalid formula syntax' => 'Ugyldig formelsyntaks', - 'Invalid gateway: {value}' => 'Ugyldig portal: {value}', - 'Invalid inventory movements.' => 'Ugyldig inventarforflytninger.', - 'Invalid order condition syntax.' => 'Ugyldig tilstandssyntaks for ordre.', - 'Invalid payment or order. Please review.' => 'Ugyldig betaling eller bestilling. Må endres.', - 'Invalid payment source ID: {value}' => 'Ugyldig betalingskilde-ID: {value}', - 'Invalid store.' => 'Ugyldig butikk.', - 'Invalid user.' => 'Ugyldig bruker.', - 'Inventory Item' => 'Lagervare', - 'Inventory Location' => 'Inventarsted', - 'Inventory Locations' => 'Inventarsteder', - 'Inventory Tracked' => 'Inventar sporet', - 'Inventory Transfers' => 'Lageroverføringer', - 'Inventory could not be set.' => 'Inventar kunne ikke angis.', - 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'Inventarsted har forpliktet lagerbeholdning, ordre(ne) må først fullføres.', - 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Inventarsted har innkommende lagerbeholdning, overføringen(e) må først fullføres.', - 'Inventory location is already deactivated.' => 'Inventarsted er allerede deaktivert.', - 'Inventory location saved.' => 'Inventarsted lagret.', - 'Inventory locations not saved.' => 'Inventarsteder ikke lagret.', - 'Inventory movement could not be saved.' => 'Inventarforflytninger kunne ikke bli lagret.', - 'Inventory movement saved.' => 'Inventarforflytninger lagret.', - 'Inventory updated.' => 'Inventar oppdatert.', - 'Inventory was not updated.' => 'Inventar ble ikke oppdatert.', - 'Inventory' => 'Inventar', - 'Invoice amount' => 'Fakturabeløp', - 'Invoice date' => 'Fakturadato', - 'Is Promotable' => 'Kan promoteres', - 'Is Promotional Price?' => 'Kampanjepris?', - 'Is Shippable' => 'Kan sendes', - 'Is Taxable' => 'Kan skattes', - 'Item Rates' => 'Varepriser', - 'Item Subtotal' => 'Delsum for vare', - 'Item Total' => 'Varetotal', - 'Item' => 'Vare', - 'Items' => 'Elementer', - 'Kilograms (kg)' => 'Kilogram (kg)', - 'Label' => 'Merkelapp', - 'Landscape' => 'Landskap', - 'Language' => 'Språk', - 'Last Name' => 'Etternavn', - 'Last Updated' => 'Sist oppdatert', - 'Leave a category rate override blank to use the rate from above.' => 'La en overstyring av kategorirate stå tom for å bruke en pris ovenfra.', - 'Leave blank for unlimited uses.' => 'La stå tomt for ubegrenset bruk.', - 'Leave blank if products don’t have URLs' => 'Skal stå tomt hvis produktene ikke har nettadresser (URL)', - 'Leave gateway subscription as-is' => 'La gateway-abonnementet være uendret', - 'Length ({unit})' => 'Lengde ({unit})', - 'Length' => 'Lengde', - 'Let each product choose which sites it should be saved to' => 'La hvert produkt velge hvilket nettsted det skal lagres til', - 'Limit which orders this discount applies to based on its line items.' => 'Begrens hvilke ordre denne rabatten gjelder for basert på linjevarer.', - 'Limit which purchasables this sale applies to.' => 'Begrens hvilke «Kan kjøpes»-varer dette salget gjelder for.', - 'Limit' => 'Grense', - 'Line Item Statuses' => 'Linjevarestatuser', - 'Line Item' => 'Linjevare', - 'Line Items' => 'Linjevarer', - 'Line item price (minus discounts)' => 'Varelinjepris (minus rabatter)', - 'Line item shipping cost' => 'Forsendelseskostnader for vare i varelinje', - 'Line item statuses reordered.' => 'Linjevarestatuser har ny rekkefølge.', - 'Link Duration' => 'Lenkens varighet', - 'Link Sent' => 'Lenke sendt', - 'Link to a product' => 'Lag lenke til et produkt', - 'Link to a variant' => 'Lag lenke til en variant', - 'Link' => 'Lenke', - 'Live' => 'Direkte', - 'Location' => 'Sted', - 'Locations that should be available for previewing products in this product type.' => 'Plasseringer som bør være tilgjengelig for forhåndsvisning av produkter av denne produkttypen.', - 'MM' => 'MM', - 'Make a payment' => 'Betal', - 'Make this the primary store' => 'Vil du gjøre dette til primærbutikk', - 'Manage Inventory' => 'Behandle inventar', - 'Manage donation settings' => 'Administrer donasjonsinnstillinger', - 'Manage general store settings' => 'Administrer generelle butikkinnstillinger', - 'Manage inventory locations' => 'Behandle inventarsteder', - 'Manage inventory stock levels' => 'Behandle inventarnivåer', - 'Manage inventory transfers' => 'Administrer lageroverføringer', - 'Manage orders' => 'Administrer ordrer', - 'Manage payment currencies' => 'Administrer betalingsvalutaer', - 'Manage promotions' => 'Administrer kampanjer', - 'Manage shipping' => 'Administrer frakt', - 'Manage store settings' => 'Administrer butikkinnstillinger', - 'Manage subscription plans' => 'Administrer abonnementsplaner', - 'Manage subscription' => 'Administrer abonnement', - 'Manage subscriptions' => 'Administrer abonnementer', - 'Manage taxes' => 'Administrer avgifter', - 'Manage' => 'Administrer', - 'Mark as Pending' => 'Merk som Venter', - 'Mark as completed' => 'Merk som ferdig', - 'Match Billing Address' => 'Match faktureringsadresse', - 'Match Customer' => 'Match kunde', - 'Match Order' => 'Match ordre', - 'Match Orders' => 'Match ordrer', - 'Match Product' => 'Match produkt', - 'Match Purchasable' => 'Sammenlign kan kjøpes-artikler', - 'Match Shipping Address' => 'Match fraktadresse', - 'Match Variant' => 'Match variant', - 'Matching Items' => 'Samsvarende varer', - 'Max Qty' => 'Maks. antall', - 'Max Uses' => 'Maks. antall brukere', - 'Max Variants' => 'Maks. varianter', - 'Max quantity must greater than min.' => 'Maksimal mengde må være større enn min.', - 'Maximum Purchase Quantity' => 'Maksimumskjøp kvantitet', - 'Maximum Total Shipping Cost' => 'Maksimal total fraktkostnad', - 'Maximum allowed quantity' => 'Maksimalt tillatt antall', - 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Maksimalt antall samsvarende varer som kan bestilles for at denne rabatten trer i kraft. Null i verdi vil her hoppe over denne betingelsen.', - 'Maximum order quantity for this item is {num}.' => 'Maksimumsantall som kan bestilles av denne artikkelen er {num}.', - 'Message' => 'Beskjed', - 'Meters (m)' => 'Meter (m)', - 'Millimeters (mm)' => 'Millimeter (mm)', - 'Min Qty' => 'Min. antall', - 'Min quantity must be less than max.' => 'Minimum mengde må være mindre enn maks.', - 'Minimum Purchase Quantity' => 'Minimumskjøp kvantitet', - 'Minimum Total Price Strategy' => 'Strategi for minimum totalpris', - 'Minimum Total Shipping Cost' => 'Minimum på total fraktkostnad', - 'Minimum allowed quantity' => 'Minimum tillatt antall', - 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Minimalt antall samsvarende varer som må bestilles for at denne rabatten skal gjelde.', - 'Minimum order quantity for this item is {num}.' => 'Minimumsantall for denne artikkelen er {num}.', - 'Missing Gateway' => 'Mangler portal', - 'Missing a default inventory location.' => 'Mangler et standard inventarsted.', - 'Move Inventory' => 'Flytt inventar', - 'Move To' => 'Flytt til', - 'Move {qty} from {fromType} to {toType}' => 'Flytt {qty} fra {fromType} til {toType}', - 'Move' => 'Flytt', - 'Movement from deactivated inventory location' => 'Forflytning fra deaktivert inventarsted', - 'Movement' => 'Forflytning', - 'Must have at least one variant.' => 'Må ha minst én variant.', - 'Name Field' => 'Navnfelt', - 'Name' => 'Navn', - 'New Customer' => 'Nyt kunde', - 'New Customers' => 'Nye kunder', - 'New Order' => 'Ny ordre', - 'New PDF' => 'Ny PDF', - 'New address' => 'Ny adresse', - 'New catalog pricing rule' => 'Ny prisregler for katalog', - 'New currency' => 'Ny valuta', - 'New discount' => 'Ny rabatt', - 'New email' => 'Ny e-post', - 'New gateway' => 'Ny portal', - 'New line item status' => 'Ny linjevarestatus', - 'New line items get this status by default when the order is completed' => 'Ny linjevarer får denne statusen som standard når ordren er fullført', - 'New location' => 'Nytt sted', - 'New order status' => 'Ny bestillingsstatus', - 'New orders get this status by default' => 'Nye bestillinger får denne statusen som standard', - 'New product type' => 'Ny betalingsmetode', - 'New product' => 'Nytt produkt', - 'New product, choose a type' => 'Nytt produkt, velg type', - 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Nye produkter oppføres som standard med den første tilgjengelige skattekategorien. Hvis ingen er tilgjengelige, brukes denne kategorien.', - 'New sale' => 'Nytt salg', - 'New shipping category' => 'Ny fraktkategori', - 'New shipping method' => 'Ny fraktmetode', - 'New shipping rule' => 'Ny fraktregel', - 'New shipping zone' => 'Ny fraktsone', - 'New subscription plan' => 'Ny abonnementsplan', - 'New tax category' => 'Ny skattekategori', - 'New tax rate' => 'Ny skattehyppighet', - 'New tax zone' => 'Ny skattesone', - 'New transfer' => 'Ny overføring', - 'New {productType} product' => 'Nytt {productType}-produkt', - 'New' => 'Ny', - 'Next payment' => 'Neste betaling', - 'No Address' => 'Ingen adresse', - 'No PDFs exist yet.' => 'Ingen PDF-er eksisterer ennå.', - 'No access given to any specific store management features.' => 'Ingen tilgang gitt til noen spesifikke butikkadministrasjonsfunksjoner.', - 'No additional payment currencies exist yet.' => 'Ingen ekstra betalingsvaluta finnes ennå', - 'No address' => 'Ingen adresse', - 'No billing address' => 'Ingen faktureringsadresse', - 'No catalog pricing rule exists with the ID “{id}”' => 'Ingen prisregel for katalog eksisterer med ID-en «{id}»', - 'No catalog pricing rules exist yet.' => 'Ingen prisregler for katalog eksisterer ennå.', - 'No currency exists with the ID “{id}”' => 'Ingen valuta finnes med denne ID-en «{id}»', - 'No customer email address exists on this cart.' => 'Det eksisterer ingen kunde-e-postadresse på denne handlekurven.', - 'No description' => 'Ingen beskrivelse', - 'No discount exists with the ID “{id}”' => 'Ingen rabatt eksisterer med ID-en «{id}»', - 'No discounts exist yet.' => 'Ingen rabatter finnes ennå.', - 'No donation amount supplied.' => 'Ingen donasjonsbeløp levert.', - 'No emails exist yet.' => 'Ingen e-poster finnes ennå.', - 'No inventory changes made.' => 'Ingen inventarendringer gjort.', - 'No inventory found.' => 'Ingen inventar funnet.', - 'No inventory movements made.' => 'Ingen inventarforflytninger gjort.', - 'No inventory transactions for this location.' => 'Ingen inventaroverføringer for dette stedet.', - 'No new customer selected.' => 'Ingen ny kunde valgt.', - 'No order history exists with the ID “{id}”' => 'Ingen ordrehistorikk eksisterer med ID-en «{id}»', - 'No order status history items will exist until the cart becomes an order.' => 'Ingen historikk-elementer for ordrestatus eksisterer før handlekurven blir en ordre.', - 'No payment source exists with the ID “{id}”' => 'Det finnes ingen betalingskilde med ID-en «{id}»', - 'No private Note.' => 'Ingen privat merknad.', - 'No product available.' => 'Ingen produkt tilgjengelig.', - 'No product types exist yet.' => 'Ingen produkttyper finnes ennå.', - 'No purchasable available.' => 'Ingen Kan kjøpes-artikler tilgjengelig.', - 'No sale exists with the ID “{id}”' => 'Ingen salg eksisterer med ID-en «{id}»', - 'No sales exist yet.' => 'Ingen salg finnes ennå.', - 'No shipping address' => 'Ingen fraktadresse', - 'No shipping category exists with the ID “{id}”' => 'Ingen fraktkategori finnes med den ID-en «{id}»', - 'No shipping method exists with the ID “{id}”' => 'Ingen fraktmetode eksisterer med ID-en «{id}»', - 'No shipping rule exists with the ID “{id}”' => 'Ingen fraktregel eksisterer med ID-en «{id}»', - 'No shipping rules exist yet.' => 'Ingen fraktregler finnes ennå.', - 'No shipping zone exists with the ID “{id}”' => 'Ingen fraktsone eksisterer med ID-en «{id}»', - 'No stats available.' => 'Ingen statistikk tilgjengelig.', - 'No subscription plan exists with the ID “{id}”' => 'Det finnes ingen abonnementsplan med ID-en «{id}»', - 'No subscription plans exist yet.' => 'Det finnes ingen abonnementsplan ennå.', - 'No tax category exists with the ID “{id}”' => 'Ingen skattekategori eksisterer med ID-en «{id}»', - 'No tax rate exists with the ID “{id}”' => 'Ingen skattesatsen eksisterer med ID-en «{id}»', - 'No tax zone exists with the ID “{id}”' => 'Ingen skattesone eksisterer med ID-en «{id}»', - 'No transactions exist.' => 'Ingen transaksjoner eksisterer.', - 'No user authenticated.' => 'Ingen bruker autentisert.', - 'No' => 'Nei', - 'None on hand' => 'Ingen for hånden', - 'None' => 'Ingen', - 'Not a valid address type' => 'Ikke en gyldig adressetype', - 'Not a valid credit card number.' => 'Ikke et gyldig kredittkortnummer.', - 'Not all SKUs are unique.' => 'Ikke alle produktkoder (SKU) er unike.', - 'Note' => 'Merk', - 'Notes' => 'Merknader', - 'Number of Coupons' => 'Antall kuponger', - 'Number' => 'Nummer', - 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Av de aktiverte sidene ovenfor, til hvilke sider skal produktene i denne produkttypen lagres?', - 'On Hand' => 'Tilgjengelig', - 'Only allow this gateway to be used for zero value orders?' => 'Kun tillate at denne portalen brukes for nullverdi-bestillinger?', - 'Only match certain purchasables…' => 'Kombiner bare bestemte «Kan kjøpes»-artikler …', - 'Only match purchasables related to…' => 'Kombiner bare «Kan kjøpes»-artikler knyttet til …', - 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Kun ordrer med følgende ordrestatuser vil bli inkludert. La stå tomt for å inkludere alle statuser.', - 'Only save product to the site they were created in' => 'Lagre produkter kun til siden de ble laget på', - 'Options' => 'Valg', - 'Order Condition Formula' => 'Formel for ordretilstand', - 'Order Description Format' => 'Ordrebeskrivelse format', - 'Order Details' => 'Ordredetaljer', - 'Order Fields' => 'Bestillingsfelt', - 'Order PDF Download Link' => 'Nedlastingslenke for PDF-ordre', - 'Order PDF Filename Format' => 'Bestillings PDF-filnavn format', - 'Order Reference Number Format' => 'Format for bestillingsreferanse', - 'Order Settings' => 'Ordreinnstillinger', - 'Order Site' => 'Ordreside', - 'Order Status description.' => 'Beskrivelse av ordrestatus.', - 'Order Status' => 'Ordrestatus', - 'Order Statuses' => 'Bestillingsstatuser', - 'Order can not be empty.' => 'Ordre kan ikke stå tomt.', - 'Order count' => 'Antall ordre', - 'Order customer data removed.' => 'Kundedata bestilt fjernet.', - 'Order deleted.' => 'Ordre slettet.', - 'Order fields saved.' => 'Ordrefelt lagret.', - 'Order not found.' => 'Ordre ikke funnet.', - 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Saldoen for ordren er {outstandingBalanceAsCurrency}. Dette er det høyeste beløpet som vil bli belastet.', - 'Order recalculated.' => 'Ordre beregnet på nytt.', - 'Order status saved.' => 'Ordrestatus lagret.', - 'Order statuses reordered.' => 'Ordrestatuser har ny rekkefølge.', - 'Order total shipping cost' => 'Total forsendelseskostnad for ordren', - 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Bestillingens totale avgiftspliktige pris (linjepris delsum + totale rabatter + totale forsendelseskostnader)', - 'Order' => 'Ordre', - 'Orders (Legacy)' => 'Ordrer (eldre)', - 'Orders deleted.' => 'Ordrer slettet.', - 'Orders not restored.' => 'Ordre ble ikke gjenopprettet.', - 'Orders restored.' => 'Bestillinger gjenopprettet.', - 'Orders' => 'Ordre', - 'Organization Name' => 'Organisasjonsnavn', - 'Organization Tax ID' => 'Skatte-ID for organisasjon', - 'Origin and destination cannot be the same.' => 'Opprinnelse og destinasjon kan ikke være det samme.', - 'Origin' => 'Opprinnelse', - 'Original Price' => 'Opprinnelig pris', - 'Original price' => 'Opprinnelig pris', - 'Original promotional price' => 'Opprinnelig kampanjepris', - 'Other Languages' => 'Andre språk', - 'Other countries' => 'Andre land', - 'Outgoing transfer from Transfer ID: ' => 'Utgående overføring fra overførings-ID: ', - 'Overpaid' => 'Overbetalt', - 'Overrides previous?' => 'Overstyrer forrige?', - 'PDF Attachment' => 'PDF-vedlegg', - 'PDF Template Path' => 'Bane for PDF-mal', - 'PDF saved.' => 'PDF lagret.', - 'PDF' => 'PDF', - 'PDFs & Emails' => 'PDF-er og e-poster', - 'PDFs' => 'PDF-er', - 'Paid Amount' => 'Betalt beløp', - 'Paid Status' => 'Betalingsstatus', - 'Paid' => 'Betalt', - 'Paper Orientation' => 'Papirretning', - 'Paper Size' => 'Papirstørrelse', - 'Partial payment not allowed.' => 'Delbetaling ikke tillatt.', - 'Partial' => 'Delvis', - 'Past year' => 'Siste år', - 'Past {num} days' => 'Siste {num} dager', - 'Pay {amount} of {currency} on the order.' => 'Betal {amount} i {currency} på ordren.', - 'Pay' => 'Betal', - 'Payment Amount' => 'Betalingssum', - 'Payment Currencies' => 'Betalingsvalutaer', - 'Payment Gateway' => 'Betalingsportal', - 'Payment Method' => 'Betalingsmetode', - 'Payment error: {message}' => 'Betalingsfeil: {message}', - 'Payment method issue' => 'Problem med betalingsmetode', - 'Payment source created.' => 'Betalingskilde opprettet.', - 'Payment source deleted.' => 'Betalingskilde slettet.', - 'Payments' => 'Betalinger', - 'Pending' => 'Venter', - 'Per Email Address Discount Limit' => 'Rabattgrense per e-postadresse', - 'Per Item Amount Off' => 'Rabattbeløp per vare', - 'Per Item Discount' => 'Rabatt per enhet', - 'Per Item Percentage Off' => 'Rabatt per vare', - 'Per Item Rate' => 'Enhetspris', - 'Per User Discount Limit' => 'Rabattgrense per bruker', - 'Percentage Rate' => 'Prosentpris', - 'Phone (Alt)' => 'Telefonnummer (alt.)', - 'Phone' => 'Telefon', - 'Pick a plan' => 'Velg en plan', - 'Plain Text Email Template Path' => 'Malbane for tekst-e-post', - 'Plan' => 'Plan', - 'Plans reordered.' => 'Planer har ny rekkefølge.', - 'Portrait' => 'Portrett', - 'Post Date' => 'Postdato', - 'Postal Code Formula' => 'Postnummer-formel', - 'Pounds (lb)' => 'Pund (lb)', - 'Preview' => 'Forhåndsvis', - 'Previous Status' => 'Tidligere status', - 'Price' => 'Pris', - 'Prices' => 'Priser', - 'Pricing Rules' => 'Prisregler', - 'Pricing jobs are currently running.' => 'Prisjobber pågår.', - 'Pricing' => 'Prissetting', - 'Primary Billing Address' => 'Primær faktureringsadresse', - 'Primary Shipping Address' => 'Primær fraktadresse', - 'Primary payment source updated.' => 'Primær betalingskilde oppdatert.', - 'Primary' => 'Primær', - 'Private Note' => 'Privat merknad', - 'Product Fields' => 'Produktfelt', - 'Product ID is required.' => 'Produkt-ID nødvendig.', - 'Product Template' => 'Produktmal', - 'Product Title Format' => 'Tittelformat for produkt', - 'Product Type' => 'Produkttype', - 'Product Types' => 'Produkttyper', - 'Product URI Format' => 'URI-format for produkt', - 'Product Variant' => 'Produktvariant', - 'Product Variants' => 'Produktvarianter', - 'Product type saved.' => 'Produkttype lagret.', - 'Product type settings' => 'Innstillinger for produkttype', - 'Product' => 'Produkt', - 'Products and Variants deleted.' => 'Produkter og varianter slettet.', - 'Products not restored.' => 'Produkter ble ikke gjenopprettet.', - 'Products restored.' => 'Produkter gjenopprettet.', - 'Products' => 'Produkter', - 'Promotable' => 'Promoterbar', - 'Promotable?' => 'Promoterbar?', - 'Promotional Amount' => 'Kampanjebeløp', - 'Promotional Price' => 'Kampanjepris', - 'Purchasable Categories' => 'Kategorier som kan kjøpes', - 'Purchasable ID and Sale ID are required.' => 'Kjøpbar- og salg-ID nødvendig.', - 'Purchasable ID is required.' => 'Kjøpbar-ID nødvendig.', - 'Purchasable Type' => 'Typer som kan kjøpes', - 'Purchasable' => 'Kan kjøpes', - 'Purchase (Authorize and Capture Immediately)' => 'Kjøp (autoriser og fang umiddelbart)', - 'Purchase Total' => 'Kjøpstotal', - 'Qty' => 'Ant.', - 'Quality Control' => 'Kvalitetskontroll', - 'Quantity' => 'Kvantitet', - 'Rate' => 'Pris', - 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Tildel {numOrders, plural, =1{ordre} other{ordrer}}', - 'Recalculate order' => 'Beregn ordre på nytt', - 'Receive Inventory' => 'Motta lager', - 'Receive Transfer' => 'Motta overføring', - 'Receive' => 'Motta', - 'Received' => 'Mottatt', - 'Recent Orders' => 'Nylige ordrer', - 'Recipient' => 'Mottaker', - 'Recover Cart' => 'Gjenopprett handlekurv', - 'Reduce price' => 'Reduser pris', - 'Reduce the price by a fixed amount' => 'Reduser pris med et fast beløp', - 'Reduce the price by a percentage of the original price' => 'Reduser prisen med en prosentdel av den opprinnelige prisen', - 'Reference' => 'Referanse', - 'Refresh payment history' => 'Oppdater betalingshistorikk', - 'Refund note' => 'Merknad til tilbakebetaling', - 'Refund payment' => 'Refunder betaling', - 'Refund' => 'Refusjon', - 'Reject' => 'Avslå', - 'Rejected' => 'Avslått', - 'Relationship Type' => 'Forholdstype', - 'Removable included tax rates are only allowed for the default tax zone.' => 'Inkluderte skatter og avgifter som kan fjernes, er kun tillatt for standard skattesone.', - 'Remove address' => 'Fjern adresse', - 'Remove all shipping costs from the order' => 'Fjern alle fraktkostnader fra bestillingen', - 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Fjern kundetilknytning og e-post fra {numOrders, plural, =1{ordre} other{ordrer}}. Valgfritt: velg ytterligere kundedata som skal fjernes nedenfor', - 'Remove customer data' => 'Fjern kundedata', - 'Remove from price?' => 'Fjern fra pris?', - 'Remove shipping costs for matching items only' => 'Fjern fraktkostnader kun for identiske varer', - 'Remove the included tax when a valid organization tax ID is present?' => 'Fjerne inkludert skatt når det er en gyldig organisasjonsskatt?', - 'Remove' => 'Fjern', - 'Removed' => 'Fjernet', - 'Repeat Customers' => 'Tilbakevendende kunder', - 'Reply To' => 'Svar til', - 'Require Billing Address At Checkout' => 'Krev faktureringsadresse ved bestilling', - 'Require Coupon Code' => 'Krever kupongkode', - 'Require Shipping Address At Checkout' => 'Krev fraktadresse ved bestilling', - 'Require Shipping Method Selection At Checkout' => 'Krev valg av leveringsmetode ved bestilling', - 'Require' => 'Krever', - 'Reserved' => 'Reservert', - 'Reset usage' => 'Tilbakestill bruk', - 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Begrens rabatten til kun bestillinger hvor kunden har kjøpt produkter til et minimumsbeløp med matchende varer.', - 'Revenue Options' => 'Alternativer for inntekt', - 'Revenue' => 'Inntekt', - 'Rule' => 'Regel', - 'Rules reordered.' => 'Regler har ny rekkefølge.', - 'SKU' => 'SKU', - 'Safety' => 'Sikkerhet', - 'Sale Price' => 'Salgspris', - 'Sale description.' => 'Salgsbeskrivelse.', - 'Sale reordered.' => 'Salg har ny rekkefølge.', - 'Sale saved.' => 'Salg lagret.', - 'Sale' => 'Salg', - 'Sales deleted.' => 'Salg slettet.', - 'Sales updated.' => 'Salg er oppdatert.', - 'Sales' => 'Salg', - 'Save and continue editing' => 'Lagre og fortsett redigering', - 'Save and return to all orders' => 'Lagre og gå tilbake til alle ordre', - 'Save and set rules' => 'Lagre og fastsett regler', - 'Save as a new rule' => 'Lagre som en ny regel', - 'Save product to all sites enabled for this product type' => 'Lagre produkt på alle sider som er aktivert for denne produkttypen', - 'Save product to other sites in the same site group' => 'Lagre produkt på andre sider i samme sidegruppe', - 'Save product to other sites with the same language' => 'Lagre produkt på andre sider med samme språk', - 'Save' => 'Lagre', - 'Search customer…' => 'Søk på kunde…', - 'Search inventory' => 'Søk i inventar', - 'Search or enter customer email…' => 'Søk eller skriv inn e-post til kunden …', - 'Search…' => 'Søk…', - 'See Orders' => 'Se ordre', - 'Select a gateway' => 'Velg en ny portal', - 'Select a tax category.' => 'Velg en skattekategori.', - 'Select a tax zone. If empty, this rate will match anywhere.' => 'Velg en skattesone. Hvis tomt, vil satsen samsvare med hvor som helst.', - 'Select address' => 'Velg adresse', - 'Select an item' => 'Velg et element', - 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Velg hvordan prisregel for katalog skal anvendes på varen(e).', - 'Select how the sale will be applied to the purchasable(s).' => 'Velg hvordan salget skal anvendes på varen(e).', - 'Select product type' => 'Velg produkttype', - 'Select the emails that will be sent when transitioning to this status.' => 'Selv e-postene som vil sendes ved overgang til denne statusen.', - 'Select what this rate should be applied to.' => 'Velg hva denne satsen skal gjelde for.', - 'Send Email' => 'Send e-post', - 'Send to custom recipient' => 'Send til tilpasset mottaker', - 'Send to the customer' => 'Send til kunden', - 'Set Quantity' => 'Angi antall', - 'Set default category' => 'Angi standardkategori', - 'Set default variant' => 'Angi standardvariant', - 'Set or Adjust' => 'Angi eller juster', - 'Set price' => 'Angi pris', - 'Set status' => 'Sett status', - 'Set the price to a flat amount' => 'Sett prisen til et flatt beløp', - 'Set the price to a percentage of the original price' => 'Sett prisen til en prosentdel av den opprinnelige prisen', - 'Set the sale price to a flat amount' => 'Angi salgsprisen til et flatt beløp', - 'Set the sale price to a percentage of the original price' => 'Angi salgsprisen til en prosentdel av den opprinnelige prisen', - 'Set to' => 'Angi som', - 'Settings saved.' => 'Innstillinger lagret.', - 'Settings' => 'Innstillinger', - 'Share cart…' => 'Del handlekurv …', - 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Frakt – minimumskostnad er fraktkostnaden, hvis ordreprisen er mindre enn fraktkostnaden.', - 'Shipping Address Zone' => 'Fraktadressesone', - 'Shipping Address' => 'Fraktadresse', - 'Shipping Business Name' => 'Frakt firmanavn', - 'Shipping Categories' => 'Fraktkategorier', - 'Shipping Category Conditions' => 'Betingelser for fraktkategori', - 'Shipping Category' => 'Fraktkategori', - 'Shipping First Name' => 'Frakt fornavn', - 'Shipping Full Name' => 'Frakt fullt navn', - 'Shipping Last Name' => 'Frakt etternavn', - 'Shipping Method' => 'Fraktmetode', - 'Shipping Methods' => 'Leveringsmetoder', - 'Shipping Rule' => 'Fraktregel', - 'Shipping Zones' => 'Fraktsoner', - 'Shipping address required.' => 'Forsendelsesadresse nødvendig.', - 'Shipping categories deleted.' => 'Fraktkategorier slettet.', - 'Shipping category saved.' => 'Fraktkategori er lagret.', - 'Shipping category updated.' => 'Fraktkategori oppdatert.', - 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Fraktkostnader lagt til ordren som en helhet før prosent-, vare-, og vektsatser blir brukt. Sett til null for å deaktivere denne satsen. Hele regelen, inkludert denne grunnsatsen, vil ikke samsvare og gjelde hvis handlekurven inneholder elementer som ikke kan sendes, for eksempel digitale produkter.', - 'Shipping method saved.' => 'Fraktmetode lagret.', - 'Shipping methods and rules deleted.' => 'Fraktmetoder og regler slettet.', - 'Shipping methods updated.' => 'Leveringsmetoder oppdatert.', - 'Shipping rule saved.' => 'Fraktregel lagret.', - 'Shipping zone saved.' => 'Fraktsone lagret.', - 'Shipping' => 'Levering', - 'Short Number' => 'Kort nummer', - 'Show Chart?' => 'Vise handlekurv?', - 'Show Order Count?' => 'Vise antall ordre?', - 'Show all prices' => 'Vis alle priser', - 'Show archived gateways' => 'Vis arkiverte portaler', - 'Show order count line on chart.' => 'Vis linje for antall ordre i tabell.', - 'Show related sales' => 'Vis relaterte salg', - 'Show rule details' => 'Vis detaljer om regel', - 'Show the Dimensions and Weight fields for products of this type' => 'Vis mål- og vektfelt for slike produkter', - 'Show the Title field for products' => 'Vis tittelfeltet for produkter', - 'Show the Title field for variants' => 'Vis tittelfeltet for varianter', - 'Signed In' => 'Logget inn', - 'Site Languages' => 'Nettsidespråk', - 'Site store mapping saved.' => 'Kartleggingen av butikk lagret.', - 'Sites' => 'Nettsteder', - 'Slug' => 'Lenke', - 'Snapshot' => 'Snapshot', - 'Snapshots' => 'Snapshots', - 'Some orders restored.' => 'Enkelte ordrer ble gjenopprettet.', - 'Some products restored.' => 'Enkelte produkter ble gjenopprettet.', - 'Some variants restored.' => 'Noen varianter ble gjenopprettet.', - 'Something changed with the order before payment, please review your order and submit payment again.' => 'Noe endret seg ved bestillingen før betaling, vennligst se over bestillingen og send betalingen igjen.', - 'Sorry, no matching options.' => 'Beklager, ingen tilsvarende valg.', - 'Source - The purchasable relationship field is on the category' => 'Kile – forholdsfeltet for Kan kjøpes-artikler er på kategorien', - 'Source' => 'Kilde', - 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Spesifiser en Twig-tilstand som bestemmer om rabatten skal anvendes på en gitt ordre. (Ordren kan henvises til via en `order`-variabel.)', - 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Spesifiser en Twig-tilstand som bestemmer om fraktregelen skal anvendes på en gitt ordre. (Ordren kan henvises til via en `order`-variabel.)', - 'Start Date' => 'Startdato', - 'State' => 'Stat', - 'Status Email Address' => 'E-postadresse-status', - 'Status Emails' => 'E-postadresse-statuser', - 'Status History' => 'Statushistorikk', - 'Status Updated.' => 'Status oppdatert.', - 'Status change message' => 'Melding ved statusendring', - 'Status' => 'Status', - 'Stock' => 'Beholdning', - 'Stops Processing?' => 'Stoppe behandling?', - 'Stops subsequent?' => 'Stopper påfølgende?', - 'Store Location' => 'Stedet for butikk', - 'Store Management' => 'Butikkadministrasjon', - 'Store Markets' => 'Butikkmarkeder', - 'Store Rule' => 'Butikkregel', - 'Store saved.' => 'Butikk lagret.', - 'Store' => 'Butikk', - 'Stores & Sites' => 'Butikker og nettsteder', - 'Stores' => 'Butikker', - 'Strategy to apply when an order is free or has a zero balance.' => 'Strategien som skal brukes, når en ordre er gratis eller har en saldo som står i null.', - 'Strategy to apply when calculating the minimum order price.' => 'Strategi å bruke når du beregner minimumspris for ordre.', - 'Subject' => 'Emne', - 'Subscribing user' => 'Abonnerende bruker', - 'Subscription Fields' => 'Abonnementsfelt', - 'Subscription Plans' => 'Abonnementsplaner', - 'Subscription Settings' => 'Abonnementsinnstillinger', - 'Subscription cancelled.' => 'Abonnement kansellert.', - 'Subscription date' => 'Abonnementsdato', - 'Subscription fields saved.' => 'Abonnementsfelt er lagret.', - 'Subscription for {user} to {plan} prevented by a plugin.' => 'Abonnement for {user} til {plan} forhindret av en programutvidelse.', - 'Subscription plan saved.' => 'Abonnementsplan er lagret.', - 'Subscription plan' => 'Abonnementsplan', - 'Subscription plans' => 'Abonnementsplaner', - 'Subscription reactivated.' => 'Abonnement reaktivert.', - 'Subscription reference' => 'Referanse for abonnement', - 'Subscription started.' => 'Abonnement startet.', - 'Subscription switched.' => 'Abonnement byttet.', - 'Subscription to “{plan}”' => 'Abonnement på «{plan}»', - 'Subscription' => 'Abonnement', - 'Subscriptions on hold' => 'Abonnement på vent', - 'Subscriptions' => 'Abonnementer', - 'Suppress emails' => 'Undertrykk e-poster', - 'Switch plan' => 'Bytte plan', - 'Switch' => 'Bytte', - 'System' => 'System', - 'Table Columns' => 'Tabellkolonner', - 'Target - The category relationship field is on the purchasable' => 'Mål – feltet for kategoriforhold er på Kan kjøpes-artikkelen', - 'Tax & Shipping' => 'Avgift og frakt', - 'Tax (inc)' => 'Avgift (inkludert)', - 'Tax Categories' => 'Skattekategorier', - 'Tax Category' => 'Skattekategori', - 'Tax Rates' => 'Skatteavgifter', - 'Tax Zone' => 'Skattesone', - 'Tax Zones' => 'Skattesoner', - 'Tax categories deleted.' => 'Skattekategorier slettet.', - 'Tax category saved.' => 'Skattekategori lagret.', - 'Tax category updated.' => 'Avgiftskategori oppdatert.', - 'Tax rate saved.' => 'Skattesats lagret.', - 'Tax rates updated.' => 'Skattesatser ble oppdatert.', - 'Tax zone saved.' => 'Skattesone lagret.', - 'Tax' => 'Avgift', - 'Taxable Subject' => 'Skattepliktig enhet', - 'Template Path' => 'Mal-vei', - 'That handle is already in use' => 'Etiketten er allerede i bruk', - 'That handle is already in use.' => 'Etiketten er allerede i bruk.', - 'The PDF to attach to this email.' => 'PDF-en som skal legges til e-posten.', - 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'Lenken til sidenfor oppdatering av faktureringsinformasjon for et abonnement, samt administrering av 3DS-autentisering.', - 'The address provided is outside the store’s market.' => 'Adressen som er oppgitt er utenfor butikkens marked.', - 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'Rabattbeløpet som er brukt på hele ordren. Beløpet er spredt over linjevarene i rekkefølgen fra høyest til lavest pris til rabatten er brukt opp.', - 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'Grunnrabatten gjelder bare for varer i handlekurven ned til null inntil den er brukt opp. Ordren kan ikke bli negativ.', - 'The cart recovery link is invalid. Please request a new one.' => 'Lenken for gjenoppretting av handlekurv er ugyldig. Be om en ny.', - 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Konverteringskursen som vil bli brukt når man konverterer en sum til denne valutaen. For eksempel, hvis en vare koster {amount1}, vil en konverteringskurs på {rate} resultere i {amount2} i den alternative valutaen.', - 'The countries that orders are allowed to be placed from.' => 'Landene det er tillatt å legge inn bestillinger fra.', - 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'Kupongen «{code}» har overskredet bruksgrensen på {limit}.', - 'The customer for this order has been deleted.' => 'Kunden for denne ordren ble slettet.', - 'The default shipping category is automatically available to all product types.' => 'Standard fraktkategori er automatisk tilgjengelig for alle produkttyper.', - 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'Rabatten «{name}» har overskredet bruksgrensen på {limit}.', - 'The download link has expired. Please request a new one.' => 'Nedlastingslenken er utløpt. Be om en ny.', - 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'E-postadressen som e-poster med bestillingsstatus sendes fra. La stå tomt for å bruke systemets e-postadresse som er definert i Crafts generelle innstillinger.', - 'The entry that contains the description for this subscription’s plan.' => 'Oppføringen som inneholder beskrivelse for denne abonnementsplanen.', - 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'Den flate verdien som gir avslag på hver vare, f.eks. «3» for $3 avslag på hver vare.', - 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'Formatet som brukes til å generere nye kuponger, f.eks. {example}. Alle \'#\'-tegn blir byttet ut med en tilfeldig bokstav.', - 'The from and to inventory locations must be different.' => 'Inventarstedene til og fra må være forskjellige.', - 'The inventory locations this store uses.' => 'Inventarstedene denne butikken bruker.', - 'The item is not enabled for sale.' => 'Denne artikkelen er ikke aktivert for salg.', - 'The language the order was made in.' => 'Språket som ordren ble laget i.', - 'The language to be used when this email is rendered.' => 'Språk som skal brukes i e-posten som genereres.', - 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Maksimalt antall nivåer tillatt i produkttypen. La stå tomt hvis det ikke spiller noen rolle.', - 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Det maksimale en kunde skal betale i frakt. Sett til null for å deaktivere.', - 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Det minste kunden skal betale i frakt. Sett til null for å deaktivere.', - 'The order is not valid.' => 'Ugyldig ordre.', - 'The payment gateway that will be used for the subscription plan.' => 'Hvilken betalingsportal skal brukes for abonnementsplanen.', - 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'Den prosentvise verdien som skal brukes som rabatt på hver vare. F.eks. {ex1} for en rabatt på {ex2}. Prosenten rundes av til to desimaler.', - 'The previously-selected shipping method is no longer available.' => 'Tidligere valgt leveringsmetode er ikke lenger tilgjengelig.', - 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Prisen på {description} har økt fra {originalSalePriceAsCurrency} til {newSalePriceAsCurrency}', - 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Prisen på {description} har blitt redusert fra {originalSalePriceAsCurrency} til {newSalePriceAsCurrency}', - 'The primary currency cannot be changed after orders are placed.' => 'Hovedvalutaen kan ikke endres etter at ordren er sendt inn.', - 'The purchasable defines the relationship' => '«Kan kjøpes»-artikkelen definerer forholdet', - 'The purchasable is related by another element' => '«Kan kjøpes»-artikkelen er relatert til et annet element', - 'The recipient of the email. Twig code can be used here.' => 'Mottakeren av e-posten. Twig-kode kan brukes.', - 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'E-postadresser som svar skal sendes til. La stå tomt for vanlig "svar til" fra e-postavsender. Twig-kode kan brukes.', - 'The site the order was made in.' => 'Nettstedet som ordren ble laget på.', - 'The site to be used when this email is rendered.' => 'Nettstedet som skal brukes i e-posten som genereres.', - 'The subject line of the email. Twig code can be used here.' => 'Emnefeltet til e-posten. Twig-kode kan brukes.', - 'The template that the PDF should be generated from.' => 'Malen som PDF-en skal genereres fra.', - 'The template to be used for HTML emails.' => 'Malen som skal brukes for HTML-e-poster.', - 'The template to be used for plain text emails. Twig code can be used here.' => 'Malen som skal brukes for enkle e-poster med tekst. Twig-kode kan brukes.', - 'The template to use when a product’s URL is requested.' => 'Malen som skal brukes når URL-en til et produkt blir forespurt om.', - 'The total number of order adjustments changed.' => 'Det totale antallet ordrejusteringer er endret.', - 'The total price of the order changed.' => 'Totalprisen på bestillingen er endret.', - 'The total quantity of items within the order changed.' => 'Den totale størrelsen på varer i ordren er endret.', - 'The unique SKU of the donation purchasable.' => 'Den unike SKU-en for donasjonen som kan kjøpes.', - 'The unit of measurement that should be used when specifying product dimensions.' => 'Måleenheten som skal brukes ved spesifikasjon av produktmål.', - 'The unit of measurement that should be used when specifying product weights.' => 'Måleenheten som skal brukes ved spesifikasjon av produktets vekt.', - 'The webhook URL for this gateway.' => 'Webhook-URL for gatewayen.', - 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => '“Fra”-navnet som skal brukes ved sending av e-poster med bestillingsstatuser. La stå tomt for å bruke avsendernavnet som er definert i Crafts generelle innstillinger.', - 'There are errors on the order' => 'Det er feil i ordren', - 'There are only {num} “{description}” items left in stock.' => 'Det er kun {num} «{description}»-artikler igjen på lager.', - 'There aren’t any product types to select yet.' => 'Det er ingen produkttyper å velge ennå.', - 'There is no gateway or payment source available for use with this order.' => 'Det er ingen portal eller betalingskilde tilgjengelig for denne ordren.', - 'There is no gateway selected that supports payment sources.' => 'Det er ikke valgt noen portal som støtter betalingskilder.', - 'There is no shipping method selected for this order.' => 'Det er ikke valgt noen leveringsmetode for denne ordren.', - 'This URL will load the cart into the user’s session, making it the active cart.' => 'Denne URL-en laster handlekurven inn i brukerens økt, slik at den blir den aktive handlekurven.', - 'This action is not allowed for the current user.' => 'Denne handlingen er ikke tillatt for nåværende bruker.', - 'This category will be used as the default for all purchasables in this store.' => 'Denne kategorien vil bli brukt som standard for alle varer som kan kjøpes i denne butikken.', - 'This coupon is for registered users and limited to {limit} uses.' => 'Denne kupongen er for registrerte brukere og begrenset til {limit} bruk.', - 'This coupon is limited to {limit} uses.' => 'Denne kupongen er begrenset til {limit} bruk.', - 'This coupon requires an email address.' => 'En e-postadresse er nødvendig for å bruke kupongen.', - 'This gateway does not support that functionality.' => 'Denne portalen støtter ikke den funksjonaliteten.', - 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Dette bli overstyrt av konfigurasjonsinnstillingen {setting} i `config/{file}.php`.', - 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Dette er adressen hvor butikken din ligger. Det kan bli brukt av ulike programutvidelser for å bestemme ting som frakt og skatt. Den kan også brukes i PDF-kvitteringer.', - 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Dette er standard-PDF-en som lages nårman henter fram ordre-PDF-en.', - 'This is the last location for the {store} store.' => 'Dette er siste sted for butikken {store}.', - 'This month' => 'Denne måneden', - 'This order has unsaved changes.' => 'Ordren har endringer som ikke er lagret.', - 'This week' => 'Denne uken', - 'This year' => 'Dette året', - 'Times Used' => 'Tid brukt', - 'Title' => 'Tittel', - 'To' => 'Til', - 'Today' => 'I dag', - 'Too many variants for this product.' => 'For mange varianter for dette produktet.', - 'Top Customers by Average Order' => 'Topp kunder etter gjennomsnittssordre', - 'Top Customers by Total Revenue' => 'Topp kunder etter total inntekt', - 'Top Customers' => 'Topp kunder', - 'Top Product Types by Qty Sold' => 'Topp produkttyper etter antall solgt', - 'Top Product Types by Revenue' => 'Topp produkttyper etter inntekt', - 'Top Product Types' => 'Topp produkttyper', - 'Top Products by Qty Sold' => 'Topp produkter etter antall solgt', - 'Top Products by Revenue' => 'Topp produkttyper etter inntekt', - 'Top Products' => 'Topp produkter', - 'Top Purchasables by Qty Sold' => 'Topp Kan kjøpes-artikler etter antall solgt', - 'Top Purchasables by Revenue' => 'Topp Kan kjøpes-artikler etter inntekt', - 'Top Purchasables' => 'De beste Kan kjøpes-artikler', - 'Total ' => 'Sum ', - 'Total Discount Use Limit' => 'Grense på samlet rabattbruk', - 'Total Discount' => 'Total rabatt', - 'Total Included Tax' => 'Sum inkludert avgift', - 'Total Orders by Billing Country' => 'Totalt antall ordrer etter faktureringsland', - 'Total Orders by Country' => 'Totalt antall ordrer etter land', - 'Total Orders by Shipping Country' => 'Totalt antall ordrer etter fraktland', - 'Total Orders' => 'Totalt antall ordre', - 'Total Paid' => 'Total betalt', - 'Total Price' => 'Totalpris', - 'Total Qty' => 'Totalt antall', - 'Total Revenue' => 'Total inntekt', - 'Total Shipping' => 'Total frakt', - 'Total Tax' => 'Total avgift', - 'Total Weight' => 'Samlet vekt', - 'Total' => 'Sum', - 'Track Inventory' => 'Spor inventar', - 'Transaction Hash' => 'Transaksjons-Hash', - 'Transaction ID' => 'Transaksjons-ID', - 'Transaction captured successfully: {message}' => 'Vellykket fanging av transaksjoner: {message}', - 'Transaction refunded successfully: {message}' => 'Vellykket refusjon av transaksjon: {message}', - 'Transactions' => 'Transaksjoner', - 'Transfer Fields' => 'Overføringsfelt', - 'Transfer Items' => 'Overføringselement', - 'Transfer Settings' => 'Overføringsinnstillinger', - 'Transfer Status' => 'Overføringsstatus', - 'Transfer fields saved.' => 'Overføringsfelt lagret.', - 'Transfer must have at least one item.' => 'Overføringer må ha minst ett element.', - 'Transfer' => 'Overføring', - 'Transfers' => 'Overføringer', - 'Trial days credited' => 'Prøvedager kreditert', - 'Trial expiration' => 'Utløpstid for prøveperiode', - 'Trial expiry date' => 'Utløpsdato for utprøving', - 'Type not in allowed options.' => 'Type ikke med blant tillatte alternativer.', - 'Type' => 'Type', - 'URI' => 'URI', - 'Unable to cancel subscription at this time.' => 'Kan ikke avbryte abonnementet nå.', - 'Unable to complete order: another request is already in progress.' => 'Kunne ikke fullføre ordren: En annen forespørsel er allerede i gang.', - 'Unable to find variant.' => 'Kan ikke finne variant.', - 'Unable to generate coupon codes: {message}' => 'Kan ikke generere kupongkoder: {message}', - 'Unable to make payment at this time.' => 'Kan ikke utføre betaling nå.', - 'Unable to modify subscription at this time.' => 'Kan ikke endre abonnementet nå.', - 'Unable to reactivate subscription at this time.' => 'Kan ikke reaktivere abonnementet nå.', - 'Unable to reassign orders.' => 'Kunne ikke tilordne ordrer på nytt.', - 'Unable to remove order data.' => 'Kunne ikke fjerne ordredata.', - 'Unable to retrieve Sale and Purchasable.' => 'Kan ikke hente salg eller Kan kjøpes-artikler.', - 'Unable to retrieve cart.' => 'Kan ikke hente handlekurv.', - 'Unable to retrieve customer.' => 'Kan ikke hente kunde.', - 'Unable to retrieve load cart URL' => 'Kan ikke hente URL for innlasting av handlekurv', - 'Unable to retrieve payment source.' => 'Kunne ikke hente betalingskilde.', - 'Unable to set default shipping category.' => 'Kan ikke angi standard fraktkategori.', - 'Unable to set default tax category.' => 'Kan ikke angi standard avgiftskategori.', - 'Unable to set primary payment source.' => 'Kunne ikke angi primær betalingskilde.', - 'Unable to start the subscription. Please check your payment details.' => 'Kan ikke starte abonnement. Kontroller dine betalingsdetaljer.', - 'Unable to subscribe at this time.' => 'Kan ikke abonnere nå.', - 'Unable to update cart.' => 'Kan ikke oppdatere handlekurv.', - 'Unable to validate address.' => 'Kan ikke validere adresser.', - 'Unit Price' => 'Enhetspris', - 'Unit price (minus discounts)' => 'Enhetspris (minus rabatter)', - 'Units' => 'Enheter', - 'Unpaid' => 'Ubetalt', - 'Unsubscribe' => 'Avslutte abonnement', - 'Update Address' => 'Oppdater adressen', - 'Update Order Status' => 'Oppdater bestillingsstatus', - 'Update Order Status…' => 'Oppdater ordrestatus ...', - 'Update order' => 'Oppdater ordre', - 'Update subscription' => 'Oppdater abonnement', - 'Update' => 'Oppdater', - 'Updated By' => 'Oppdatert av', - 'Updated committed stock successfully.' => 'Forpliktet beholdning oppdatert.', - 'Updated' => 'Oppdatert', - 'Use Billing Address For Tax' => 'Bruk faktureringsadresse for avgift', - 'Use as the primary billing address' => 'Bruk som den primære faktureringsadressen', - 'Use as the primary shipping address' => 'Bruk som den primære leveringsadressen', - 'Used By Tax Rates' => 'Bruk av skattesatser', - 'Used by Tax Rates' => 'Brukt av skattesats', - 'User Groups' => 'Brukergrupper', - 'User not found.' => 'Bruker ikke funnet.', - 'User' => 'Bruker', - 'Uses' => 'Bruksområder', - 'Validate Business Tax ID as Vat ID' => 'Bekreft ID for virksomhetsskatt som MVA-ID', - 'Validating condition syntax' => 'Validerer tilstandssyntaks', - 'Validating formula syntax' => 'Validerer formelsyntaks', - 'Variant Fields' => 'Variantfelt', - 'Variant Has Untracked Stock' => 'Varianten har lagerbeholdning som ikke er sporet', - 'Variant Price' => 'Variantpris', - 'Variant SKU' => 'Variant-SKU', - 'Variant Search' => 'Variantsøk', - 'Variant Stock' => 'Varantlager', - 'Variant Title Format' => 'Tittelformat for varianter', - 'Variant Tracks Stock' => 'Varianten sporer lagerbeholdning', - 'Variant UI Label Format' => 'Etikettformat for grensesnitt for variant', - 'Variant has no product.' => 'Variant har ingen produkt.', - 'Variants not restored.' => 'Varianter ble ikke gjenopprettet.', - 'Variants restored.' => 'Varianter ble gjenopprettet.', - 'Variants' => 'Varianter', - 'View customer' => 'Vis kunder', - 'View order' => 'Vis ordre', - 'View product type - {productType}' => 'Vis produkttype – {productType}', - 'View user' => 'Vis bruker', - 'View' => 'Vis', - 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Advarsel. Hvis du sletter valutaen, vil det stoppe alle betalinger og tilbakebetalinger i valutaen. Er du sikker på at du vil slette «{name}»?', - 'Web' => 'Web', - 'Webhook URL' => 'Webhook-URL', - 'Weight ({unit})' => 'Vekt ({unit})', - 'Weight Rate' => 'Vektpris', - 'Weight Unit' => 'Vektenhet', - 'Weight' => 'Vekt', - 'What product URIs should look like for the site.' => 'Hvordan produkt-URI-er skal se ut på dette nettstedet.', - 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Hvordan den automatisk genererte produkttittelen skal se ut. Du kan inkludere tags som gir egenskaper til de ulike produktene, slik som {ex1} eller {ex2}. Alle spesialfelt som brukes må settes til påkrevd.', - 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Hvordan den automatisk genererte varianttittelen skal se ut. Du kan inkludere tags som gir egenskaper til de ulike variantene, slik som {ex1} eller {ex2}. Alle spesialfelt som brukes må settes til påkrevd.', - 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Hvordan PDF-filnavnet til ordrene skal se ut (uten utvidelse). Du kan inkludere stikkord med utgående ordreegenskaper, slik som {ex1} eller {ex2}.', - 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Hvordan de unike, autogenererte SKU-ene skal se ut når et SKU-felt sendes inn uten en verdi. Du kan inkludere tags som gir egenskaper, slik som {ex1} eller {ex2}.', - 'What this PDF will be called in the control panel.' => 'Hva denne PDF-en skal hete i kontrollpanelet.', - 'What this catalog pricing rule will be called in the control panel.' => 'Hva denne prisregelen for katalog skal hete i kontrollpanelet.', - 'What this discount will be called in the control panel.' => 'Hva denne rabatten skal hete i kontrollpanelet.', - 'What this email will be called in the control panel.' => 'Hva denne e-posten skal hete i kontrollpanelet.', - 'What this product type will be called in the control panel.' => 'Hva denne produkttypen skal hete i kontrollpanelet.', - 'What this sale will be called in the control panel.' => 'Hva dette salget skal hete i kontrollpanelet.', - 'What this shipping category will be called in the control panel.' => 'Hva denne fraktkategorien skal hete i kontrollpanelet.', - 'What this shipping rule will be called in the control panel.' => 'Hva denne fraktregelen skal hete i kontrollpanelet.', - 'What this shipping zone will be called in the control panel.' => 'Hva denne fraktsonen skal hete i kontrollpanelet.', - 'What this status will be called in the control panel.' => 'Hva denne statusen skal hete i kontrollpanelet.', - 'What this subscription plan will be called in the control panel.' => 'Hva dette abonnementet skal hete i kontrollpanelet.', - 'What this tax category will be called in the control panel.' => 'Hva denne avgiftskategorien skal hete i kontrollpanelet.', - 'What this tax zone will be called in the control panel.' => 'Hva denne skattesonen skal hete i kontrollpanelet.', - 'When this discount is applied to an order, which line items should be discounted?' => 'Når denne rabatten blir brukt på en ordre, hvilke linjevarer skal få rabatt?', - 'Whether the first available shipping method option should be set automatically on carts.' => 'Om det første tilgjengelige fraktalternativet skal angis automatisk i handlekurver.', - 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Om brukerens primære betalingskilde skal angis automatisk i nye handlekurver.', - 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Om brukerens primære frakt- og faktureringsadresse betalingskilde skal angis automatisk i nye handlekurver.', - 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Om denne prisregelen for katalog skal være tilgjengelig for bruk, uansett andre forhold.', - 'Whether this sale should be available for use, regardless of other conditions.' => 'Om dette salget skal være tilgjengelig for bruk, uansett andre forhold.', - 'Which data to display in the name column in the results table.' => 'Hvilken data som skal vises i kolonnen med navn i resultattabellen.', - 'Which product types should this category be available to?' => 'Hvilke produkttyper skal denne kategorien være tilgjengelig for?', - 'Which template should be loaded when a product’s URL is requested.' => 'Hvilken mal som skal lastes når en forespørsel sendes for et produkts URL.', - 'Width ({unit})' => 'Bredde ({unit})', - 'Width' => 'Bredde', - 'YYYY' => 'ÅÅÅÅ', - 'Yes' => 'Ja', - 'You are not allowed to add a line item.' => 'Du har ikke tillatelse til å legge til en linjevare.', - 'You currently have no emails configured to select for this status.' => 'Du har ikke en e-post du kan velge for denne statusen.', - 'You do not have permission to load this cart.' => 'Du har ikke tillatelse til å laste inn denne handlekurven.', - 'You must set up at least one gateway that supports subscriptions first.' => 'Du må sette opp minst en portal som støtter abonnementer først.', - 'You must be logged in or provide a valid token to load this cart.' => 'Du må være innlogget eller oppgi en gyldig nøkkel for å laste inn denne handlekurven.', - 'You must be signed in to create a payment source.' => 'Du må være innlogget for å opprette en betalingskilde.', - 'You must be signed in to set a primary payment source.' => 'Du må være innlogget for å angi en primær betalingskilde.', - 'You must make a payment to complete the order.' => 'Du må betale for å fullføre ordren.', - 'Your Cart Recovery Link' => 'Lenken din for gjenoppretting av handlekurv', - 'Your Order PDF Download Link' => 'Din nedlastingslenke for PDF-ordre', - 'Your order is empty' => 'Ordren er tom', - 'ZIP file' => 'ZIP-fil', - 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Null – minimumspris er null hvis rabattene er større enn ordreverdien.', - 'Zip Code' => 'Postnummer', - 'all' => 'alle', - 'any' => 'noen', - 'average order total' => 'gjennomsnittlig ordrebeløp', - 'billing address' => 'faktureringsadresse', - 'donation' => 'donasjon', - 'donations' => 'donasjoner', - 'info' => 'info', - 'inventory location' => 'inventarsted', - 'new customers' => 'nye kunder', - 'on hand' => 'for hånden', - 'only' => 'kun', - 'order' => 'ordre', - 'orders' => 'ordre', - 'price' => 'pris', - 'prices' => 'priser', - 'product variant' => 'produktvariant', - 'product variants' => 'produktvarianter', - 'product' => 'produkt', - 'products' => 'produkter', - 'repeat customers' => 'tilbakevendende kunder', - 'shipping address' => 'fraktadresse', - 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'Både shippingSameAsBilling og billingSameAsShipping kan ikke settes.', - 'subscription' => 'abonnement', - 'subscriptions' => 'abonnementer', - 'to' => 'til', - 'transfer' => 'overføring', - 'transfers' => 'overføringer', - '{amount} included' => '{amount} inkludert', - '{count} Unfulfilled Orders' => '{count} ufullførte ordrer', - '{description} is no longer available.' => '{description} er ikke tilgjengelig lenger.', - '{description} only has {stock} in stock.' => '{description} har bare {stock} på lager.', - '{from} to {to}' => '{from} til {to}', - '{name} (Primary)' => '{name} (Primære)', - '{name} (Trashed)' => '{name} (forkastet)', - '{name} catalog price' => 'Katalogpris {name}', - '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, one {}=1{ordre} other{ordrer}} oppdatert.', - '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, =1{ordre er} other{ordrer er}} assosiert med {numUsers, plural, =1{bruker} other{brukere}}.', - '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, =1{abonnement er} other{abonnementer er}} aktivert for {numUsers, plural, =1{bruker} other{brukere}}.', - '{number} more…' => '{number} mer…', - '{pct} off the discounted item price' => '{pct} avslag på rabattert pris', - '{pct} off the original item price' => '{pct} avslag på opprinnelig pris', - '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, one {}=1{har} other{har}} ikke blitt tilordnet et nettsted.', - '{total} in total revenue' => '{total} i total inntekt', - '{total} orders' => '{total} ordrer', - '{total} saleable across {locationCount} location(s)' => '{total} salgbare på tvers av {locationCount} sted(er)', - '{uses} uses across {emails} email addresses' => '{uses} antall bruk for {emails} e-postadresser', - '{uses} uses across {users} users' => '{uses} antall bruk for {users} brukere', - '“{description}” is currently out of stock.' => 'Det er for øyeblikket tomt på lageret for «{description}».', - '“{key}” has invalid JSON' => '«{key}» har ugyldig JSON', -]; diff --git a/src/translations/nl/commerce.php b/src/translations/nl/commerce.php deleted file mode 100644 index d43925fdcd..0000000000 --- a/src/translations/nl/commerce.php +++ /dev/null @@ -1,1428 +0,0 @@ - '(nieuwe prijs)', - '(of original price)' => '(van originele prijs)', - '(off original price)' => '(van originele prijs)', - 'A cart number must be specified.' => 'Gelieve een winkelwagennummer op te geven.', - 'A cart recovery link has been sent to {email}.' => 'Er is een link voor het herstellen van de winkelwagen verstuurd naar {email}.', - 'A cart recovery link will be sent to {email}.' => 'Er wordt een link voor het herstellen van de winkelwagen verstuurd naar {email}.', - 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Er wordt een gebruiksvriendelijk referentienummer met dit formaat gegenereerd wanneer een winkelwagen is voltooid en in een bestelling wordt omgezet. Bijvoorbeeld {ex1} of
{ex2}. Het resultaat van dit formaat moet uniek zijn.', - 'A new download link has been sent to {email}' => 'Er is een nieuwe downloadlink verstuurd naar {email}', - 'A new download link will be sent to {email}' => 'Er wordt een nieuwe downloadlink verstuurd naar {email}', - 'A valid email is required to create a customer.' => 'Om een klant aan te maken, is een geldig e-mailadres nodig.', - 'Accept' => 'Accepteren', - 'Accepted' => 'Geaccepteerd', - 'Actions' => 'Acties', - 'Active Carts' => 'Actieve Winkelwagens', - 'Active subscriptions' => 'Actieve abonnementen', - 'Active' => 'Actief', - 'Add Address' => 'Adres toevoegen', - 'Add a coupon' => 'Voeg een coupon toe', - 'Add a custom line item' => 'Een aangepast lijnitem toevoegen', - 'Add a line item' => 'Lijnitem toevoegen', - 'Add a product' => 'Voeg een product toe', - 'Add a variant' => 'Voeg een variant toe', - 'Add an adjustment' => 'Aanpassing toevoegen', - 'Add an item' => 'Een item toevoegen', - 'Add an option' => 'Voeg een optie toe', - 'Add catalog price' => 'Catalogusprijs toevoegen', - 'Add' => 'Toevoegen', - 'Additional Actions' => 'Bijkomende acties', - 'Additional recipients that should receive this email. Twig code can be used here.' => 'Bijkomende ontvangers die deze e-mail moeten ontvangen. Hier kunt u Twig-code gebruiken.', - 'Address 1' => 'Adres 1', - 'Address 2' => 'Adres 2', - 'Address 3' => 'Adres 3', - 'Address Line 1' => 'Adresregel 1', - 'Address Line 2' => 'Adresregel 2', - 'Address Updated.' => 'Adres bijgewerkt.', - 'Address copied to user.' => 'Adres gekopieerd naar gebruiker.', - 'Address not found.' => 'Adres niet gevonden.', - 'Adjust Quantity' => 'Hoeveelheid aanpassen', - 'Adjust by' => 'Aanpassen met', - 'Adjust price when included rate is disqualified?' => 'Prijs aanpassen wanneer belastingtarief onjuist is?', - 'Adjustments' => 'Aanpassingen', - 'Admin Notices' => 'Beheerderskennisgevingen', - 'Administrative Area Code of Origin' => 'Oorsprongscode administratief gebied', - 'Advanced' => 'Geavanceerd', - 'All Orders' => 'Alle bestellingen', - 'All Totals' => 'Alle totalen', - 'All Transfers' => 'Alle overdrachten', - 'All active subscriptions' => 'Alle actieve abonnementen', - 'All customers' => 'Alle klanten', - 'All products' => 'Alle producten', - 'All variants must have a SKU.' => 'Alle varianten moeten een SKU hebben.', - 'All' => 'Alle', - 'Allow Checkout Without Payment' => 'Afrekenen zonder betaling toestaan', - 'Allow Empty Cart On Checkout' => 'Lege winkelwagen bij afrekenen toestaan', - 'Allow Partial Payment On Checkout' => 'Gedeeltelijke betaling bij afrekenen toestaan', - 'Allow out of stock purchases' => 'Aankopen niet op voorraad toestaan', - 'Allow' => 'Toestaan', - 'Allowed Qty' => 'Toegestaan ​​aantal', - 'Alternative Phone' => 'Alternatief telefoonnummer', - 'Amount' => 'Aantal', - 'An ID must be provided' => 'Er moet een ID worden opgegeven', - 'An error occurred while generating this PDF.' => 'Er is een fout opgetreden bij het genereren van deze PDF.', - 'Any' => 'Ieder', - 'Anywhere' => 'Eender waar', - 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Weet u zeker dat u het abonnementsplan “{name}” wilt archiveren? Hierdoor worden uw bestaande abonnementen NIET geannuleerd.', - 'Are you sure you want to capture this transaction?' => 'Weet u zeker dat u deze transactie wilt vastleggen?', - 'Are you sure you want to complete this order?' => 'Weet u zeker dat u deze bestelling wilt voltooien?', - 'Are you sure you want to delete the selected orders?' => 'Weet u zeker dat u de geselecteerde bestellingen wilt verwijderen?', - 'Are you sure you want to delete the selected product and its variants?' => 'Weet u zeker dat u het geselecteerde product en de varianten wilt verwijderen?', - 'Are you sure you want to delete this shipping rule?' => 'Weet u zeker dat u deze verzendingsregel wilt verwijderen?', - 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Weet u zeker dat u “{name}” wilt verwijderen en al haar producten? Zorg ervoor dat u een back-up van uw database heeft voordat u deze destructieve actie uitvoert.', - 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Weet u zeker dat u “{name}” wilt verwijderen? Dit zal alle lijnitems met deze status resetten naar geen status.', - 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Weet u zeker dat u deze overdracht als in afwachting wilt markeren? Deze wordt als inkomend weergegeven op de bestemming.', - 'Are you sure you want to overwrite the billing address?' => 'Weet u zeker dat u het factuuradres wilt overschrijven?', - 'Are you sure you want to overwrite the shipping address?' => 'Weet u zeker dat u het verzendadres wilt overschrijven?', - 'Are you sure you want to permanently delete this store and everything in it?' => 'Weet u zeker dat u deze winkel en alles erin permanent wilt verwijderen?', - 'Are you sure you want to refund this transaction?' => 'Weet u zeker dat u deze transactie wilt terugbetalen?', - 'Are you sure you want to remove this customer?' => 'Weet u zeker dat u deze klant wilt verwijderen?', - 'Are you sure you want to save this as a new shipping rule?' => 'Weet u zeker dat u deze nieuwe verzendingsregel wilt opslaan?', - 'Are you sure you want to send email: {name}?' => 'Weet u zeker dat de e-mail: {name} wilt verzenden?', - 'At least one site must be enabled for the product type.' => 'Er moet minimaal één site zijn ingeschakeld voor het producttype.', - 'Attempted Payments' => 'Betaalpogingen', - 'Attention' => 'Ter attentie van', - 'Authorize Only (Manually Capture)' => 'Alleen autoriseren (Handmatig registreren)', - 'Auto Set Cart Shipping Method Option' => 'Verzendmethodeoptie winkelwagen automatisch instellen', - 'Auto Set New Cart Addresses' => 'Nieuw winkelwagenadres automatisch instellen', - 'Auto Set Payment Source' => 'Betaalbron automatisch instellen', - 'Automatic SKU Format' => 'Automatisch Artikelnummer Formaat', - 'Available Shipping Categories' => 'Beschikbare verzendcategorieën', - 'Available Tax Categories' => 'Beschikbare belastingcategorieën', - 'Available for purchase' => 'Beschikbaar voor aankoop', - 'Available for purchase?' => 'Beschikbaar voor aankoop?', - 'Available inventory for "{description}" has gone below zero.' => 'De beschikbare voorraad voor "{description}" is lager dan nul.', - 'Available to Product Types' => 'Beschikbaar voor producttypen', - 'Available' => 'Beschikbaar', - 'Available?' => 'Beschikbaar?', - 'Average Order Total' => 'Gemiddelde totaalprijs bestellingen', - 'Average' => 'Gemiddelde', - 'BCC’d Recipient' => 'BCC’de ontvanger', - 'Bad Request' => 'Foutieve aanvraag', - 'Bad address ID.' => 'Onjuiste adres-ID.', - 'Bad order ID.' => 'Verkeerde bestelling-ID.', - 'Base Price' => 'Basisprijs', - 'Base Promotional Price' => 'Promotiebasisprijs', - 'Base Rate' => 'Basistarief', - 'Base' => 'Basis', - 'Bcc' => 'Bcc', - 'Billing Address' => 'Factuuradres', - 'Billing Business Name' => 'Bedrijfsnaam voor facturering', - 'Billing First Name' => 'Voornaam voor facturering', - 'Billing Full Name' => 'Volledige naam voor facturering', - 'Billing Last Name' => 'Achternaam voor facturering', - 'Billing address required.' => 'Factuuradres verplicht.', - 'Billing detail update URL' => 'URL om factuurgegevens bij te werken', - 'Billing issues' => 'Factureringsprobleem', - 'Billing' => 'Facturering', - 'Both (Line item price + Line item shipping costs)' => 'Beide (prijs regelartikel + verzendkosten regelartikel)', - 'Business ID' => 'Bedrijfs-id', - 'Business Name' => 'Bedrijfsnaam', - 'Business Tax ID' => 'BTW nummer', - 'CC’d Recipient' => 'Ontvanger in cc', - 'CVV' => 'CVC', - 'Can be used as an internal reference.' => 'Kan worden gebruikt als interne referentie.', - 'Can not complete payment for missing transaction.' => 'Betaling voor ontbrekende transactie voltooien niet mogelijk.', - 'Can not create a new order' => 'Nieuwe bestelling aanmaken niet mogelijk', - 'Can not find an order to pay.' => 'Geen bestelling gevonden om te betalen.', - 'Can not find enabled email.' => 'Kan ingeschakeld e-mail niet vinden.', - 'Can not find order' => 'Bestelling niet gevonden', - 'Can not find order.' => 'Bestelling niet gevonden.', - 'Can not find the transaction to refund' => 'Terug te betalen transactie onvindbaar', - 'Can not move between these inventory types.' => 'Kan niet verplaatsen tussen deze voorraadtypen.', - 'Can not refund amount greater than the remaining amount' => 'Het is niet mogelijk om een bedrag terug te betalen dat hoger is dan het resterend bedrag', - 'Cancel subscription' => 'Abonnement annuleren', - 'Cancel with gateway now' => 'Nu annuleren met gateway', - 'Cancel' => 'Annuleer', - 'Cancellation date' => 'Annuleringsdatum', - 'Cancellation' => 'Annulering', - 'Cannot switch plans for this subscription.' => 'Kan niet overstappen op een ander plan voor dit abonnement.', - 'Can’t preview this email.' => 'Er is geen voorbeeldweergave van deze e-mail beschikbaar.', - 'Capture payment' => 'Betaling registreren', - 'Capture' => 'Vastleggen', - 'Card Holder' => 'Kaarthouder', - 'Card Number' => 'Kaartnummer', - 'Card' => 'Kaart', - 'Cart Recovery Link' => 'Link voor het herstellen van de winkelwagen', - 'Cart forgotten.' => 'Winkelwagen vergeten.', - 'Cart updated.' => 'Winkelwagen bijgewerkt.', - 'Cart {number}' => 'Winkelwagen {number}', - 'Catalog Pricing Rule' => 'Catalogusprijsregel', - 'Catalog pricing rule description.' => 'Beschrijving van catalogusprijsregel.', - 'Catalog pricing rule saved.' => 'Catalogusprijsregel opgeslagen.', - 'Catalog pricing rules deleted.' => 'Catalogusprijsregels verwijderd.', - 'Catalog pricing rules updated.' => 'Catalogusprijsregels bijgewerkt.', - 'Categories Relationship Type' => 'Relatietype categorieën', - 'Categories' => 'Categorieën', - 'Category Rate Overrides' => 'Categorietarief overschrijft', - 'Centimeters (cm)' => 'Centimeter (cm)', - 'Changing this value may affect your ability to refund existing transactions.' => 'Het wijzigen van deze waarde kan gevolgen hebben voor de mogelijkheid om bestaande transacties terug te betalen.', - 'Choose a color to represent the order’s status' => 'Kies een kleur om de status van de bestelling weer te geven', - 'Choose a new customer' => 'Kies een nieuwe klant', - 'Choose adjustment values to include when calculating the product revenue total.' => 'Kies de te gebruiken aanpasswingswaarden bij het berekenen van de totale omzet van het product.', - 'Choose the currency’s ISO code.' => 'Kies de ISO code van de valuta.', - 'Choose the destination inventory location for the existing on hand stock.' => 'Kies de bestemmingsvoorraadlocatie voor de bestaande aanwezige voorraad.', - 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Kies op welke sites dit producttype beschikbaar moet zijn en configureer de sitespecifieke instellingen.', - 'City' => 'Plaats', - 'Clear counter' => 'Teller op nul zetten', - 'Clear notices' => 'Kennisgevingen wissen', - 'Close' => 'Sluiten', - 'Code' => 'Code', - 'Collated PDF' => 'Samengevoegde PDF', - 'Color' => 'Kleur', - 'Commerce Products' => 'Commerce-producten', - 'Commerce Settings' => 'Commerce instellingen', - 'Commerce Variants' => 'Commerce-varianten', - 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Commerce-e-mail “{email}” kan niet worden verzonden voor bestelling “{order}”.', - 'Commerce order exports' => 'Exports van commerce-bestellingen', - 'Commerce' => 'Commerce', - 'Committed' => 'Toegezegde voorraad', - 'Completed Email' => 'Ingevuld e-mailadres', - 'Completed' => 'Voltooid', - 'Completing order failed.' => 'Uitvoering bestelling mislukt.', - 'Condition' => 'Conditie', - 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Voorwaarden worden hier vergeleken met een bestelling voordat de regels worden bekeken. Dit is handig als u de beschikbaarheid van een methode vroegtijdig wilt kwalificeren of als er gemeenschappelijke voorwaarden zijn voor alle regels voor deze methode.', - 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Voorwaarden hier worden vergeleken met de klant van de bestelling voordat de regels worden bekeken. Dit is handig als u de beschikbaarheid van een methode vroegtijdig wilt kwalificeren of als er gemeenschappelijke voorwaarden zijn voor alle regels voor deze methode.', - 'Conditions' => 'Voorwaarden', - 'Contains Purchasables' => 'Bevat koopbare artikelen', - 'Control Panel Settings' => 'Instellingen configuratiescherm', - 'Control panel' => 'Configuratiescherm', - 'Conversion Rate' => 'Conversietarief', - 'Converted Price' => 'Omgezette prijs', - 'Copied!' => 'Gekopieerd!', - 'Copy the URL' => 'Kopieer de URL', - 'Copy to {location}' => 'Kopiëren naar {location}', - 'Copy' => 'Kopiëren', - 'Costs' => 'Kosten', - 'Could not archive gateway.' => 'Gateway archiveren niet mogelijk.', - 'Could not cancel “{reference}”.' => 'Kan niet annuleren “{reference}”.', - 'Could not create the payment source.' => 'Betaalbron aanmaken niet mogelijk.', - 'Could not delete shipping rule' => 'Verzendingsregel verwijderen niet mogelijk', - 'Could not delete shipping zone' => 'Verzendingszone verwijderen niet mogelijk', - 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Kan {count, number} verzend{count, plural, one{categorie} other{categorieën}} niet verwijderen.', - 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Kan {count, number} verzend{count, plural, one{methode} other{methoden}} en regels niet verwijderen.', - 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Kan {count, number} belasting{count, plural, one{categorie} other{categorieën}} niet verwijderen.', - 'Could not find the email or template.' => 'E-mail of sjabloon niet gevonden.', - 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Bestelling {number} kan niet als voltooid worden gemarkeerd. Opslaan bestelling mislukt vanwege fouten tijdens het voltooien van de bestelling: {order}', - 'Could not reactivate “{reference}”.' => 'Kan niet opnieuw activeren “{reference}”.', - 'Could not send email' => 'E-mail verzenden niet mogelijk', - 'Could not switch “{reference}” to “{plan}”.' => 'Kan “{reference}” niet omschakelen naar “{plan}”.', - 'Could not update orders address.' => 'Bijwerken adres bestelling niet mogelijk.', - 'Couldn’t archive Line Item Status.' => 'Status regelartikel archiveren niet mogelijk.', - 'Couldn’t archive Order Status.' => 'Status bestelling archiveren niet mogelijk.', - 'Couldn’t capture transaction.' => 'Transactie registreren niet mogelijk.', - 'Couldn’t capture transaction: {message}' => 'Transactie registreren niet mogelijk: {message}', - 'Couldn’t delete email.' => 'Kon e-mailadres niet verwijderen.', - 'Couldn’t delete the payment source.' => 'Betaalbron verwijderen niet mogelijk.', - 'Couldn’t get order.' => 'Bestelling ophalen lukt niet.', - 'Couldn’t recalculate order.' => 'Bestelling herberekenen was niet mogelijk.', - 'Couldn’t refund transaction.' => 'Transactie terugbetalen niet mogelijk.', - 'Couldn’t refund transaction: {message}' => 'Transactie terugbetalen niet mogelijk: {message}', - 'Couldn’t reorder Line Item Statuses.' => 'Regelartikelstatussen herschikken niet mogelijk.', - 'Couldn’t reorder Order Statuses.' => 'Bestellingsstatussen herschikken niet mogelijk.', - 'Couldn’t reorder PDFs.' => 'PDF\'s herschikken niet mogelijk.', - 'Couldn’t reorder discounts.' => 'Kan kortingen niet opnieuw rangschikken.', - 'Couldn’t reorder gateways.' => 'Kan gateways niet opnieuw bestellen.', - 'Couldn’t reorder plans.' => 'Kan plannen niet opnieuw rangschikken.', - 'Couldn’t reorder rules.' => 'Regels herschikken niet mogelijk.', - 'Couldn’t reorder sale.' => 'Aanbieding herschikken lukt niet.', - 'Couldn’t reorder sales.' => 'Aanbiedingen herschikken niet mogelijk.', - 'Couldn’t reorder statuses.' => 'De statussen herschikken was niet mogelijk.', - 'Couldn’t reorder stores.' => 'Kon winkels niet herordenen.', - 'Couldn’t save PDF.' => 'PDF opslaan niet mogelijk.', - 'Couldn’t save catalog pricing rule.' => 'Kon catalogusprijsregel niet opslaan.', - 'Couldn’t save currency.' => 'Valuta opslaan niet mogelijk.', - 'Couldn’t save discount.' => 'Korting opslaan niet mogelijk.', - 'Couldn’t save email.' => 'E-mail opslaan niet mogelijk.', - 'Couldn’t save gateway.' => 'Gateway opslaan niet mogelijk.', - 'Couldn’t save inventory location.' => 'Kon voorraadlocatie niet opslaan.', - 'Couldn’t save line item status.' => 'Status regelartikel opslaan niet mogelijk.', - 'Couldn’t save order fields.' => 'Kon bestellingsvelden niet opslaan.', - 'Couldn’t save order status.' => 'Status bestelling opslaan niet mogelijk.', - 'Couldn’t save order.' => 'Bestelling opslaan niet mogelijk.', - 'Couldn’t save product type.' => 'Producttype opslaan niet mogelijk.', - 'Couldn’t save sale.' => 'Aanbieding opslaan niet mogelijk.', - 'Couldn’t save settings.' => 'Instellingen opslaan niet mogelijk.', - 'Couldn’t save shipping category.' => 'Verzendcategorie opslaan niet mogelijk.', - 'Couldn’t save shipping method.' => 'Verzendmethode opslaan niet mogelijk.', - 'Couldn’t save shipping rule.' => 'Verzendingsregel opslaan niet mogelijk.', - 'Couldn’t save shipping zone.' => 'Verzendingszone opslaan niet mogelijk.', - 'Couldn’t save store.' => 'Kon winkel niet opslaan.', - 'Couldn’t save subscription fields.' => 'Kon abonnementsvelden niet opslaan.', - 'Couldn’t save subscription plan.' => 'Abonnementsplan opslaan niet mogelijk.', - 'Couldn’t save subscription.' => 'Abonnement opslaan niet mogelijk.', - 'Couldn’t save tax category.' => 'Belastingcategorie opslaan niet mogelijk.', - 'Couldn’t save tax rate.' => 'Belastingtarief opslaan niet mogelijk.', - 'Couldn’t save tax zone.' => 'Belastingzone opslaan niet mogelijk.', - 'Couldn’t save transfer fields.' => 'Kan overdrachtsvelden niet opslaan.', - 'Couldn’t update catalog pricing rule statuses.' => 'Kon status catalogusprijsregel niet bijwerken.', - 'Couldn’t update status.' => 'Kan status niet bijwerken.', - 'Couldn’t updated sales status.' => 'Status aanbieding bijwerken niet mogelijk.', - 'Country Code of Origin' => 'Oorsprongscode land', - 'Country List' => 'Landenlijst', - 'Country not allowed.' => 'Land niet toegestaan.', - 'Country' => 'Land', - 'Coupon Code' => 'Kortingscode', - 'Coupon can not apply discount to this order due to address mismatch.' => 'Met deze coupon kan geen korting worden toegepast op deze bestelling omdat het adres niet overeenkomt.', - 'Coupon can not apply discount to this order due to customer mismatch.' => 'Met deze coupon kan geen korting worden toegepast op deze bestelling omdat de klant niet overeenkomt.', - 'Coupon can not apply discount to this order.' => 'Met deze coupon kan geen korting worden toegepast op deze bestelling.', - 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Couponcode "{code}" wordt al gebruikt door korting "{name}".', - 'Coupon codes cannot be blank.' => 'Couponcodes mogen niet leeg zijn.', - 'Coupon codes must be unique.' => 'Couponcodes moeten uniek zijn.', - 'Coupon format is required and must contain at least one `#`.' => 'Couponnotatie is vereist en moet minimaal één \'#\' bevatten.', - 'Coupon not valid.' => 'Ongeldige coupon.', - 'Coupon removed: {explanation}' => 'Coupon verwijderd: {explanation}', - 'Coupons' => 'Coupons', - 'Craft Commerce - Administration' => 'Craft Commerce - Administratie', - 'Craft Commerce - Inventory' => 'Craft Commerce - Voorraad', - 'Craft Commerce - Orders' => 'Craft Commerce - Bestellingen', - 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Producttype - {name}', - 'Craft Commerce - Subscriptions' => 'Craft Commerce - Abonnementen', - 'Create a Discount' => 'Maak een korting aan', - 'Create a Subscription Plan' => 'Een abonnementsplan aanmaken', - 'Create a new PDF' => 'Maak een nieuw PDF-bestand aan', - 'Create a new catalog pricing rule' => 'Maak een nieuwe catalogusprijsregel', - 'Create a new currency' => 'Maak nieuwe valuta aan', - 'Create a new email' => 'Maak een nieuw e-mailadres aan', - 'Create a new gateway' => 'Een nieuwe gateway aanmaken', - 'Create a new line item status' => 'Maak een regelartikelstatus aan', - 'Create a new order status' => 'Maak een bestellingstatus aan', - 'Create a new product type' => 'Maak een nieuw producttype', - 'Create a new sale' => 'Maak een nieuwe aanbieding aan', - 'Create a new shipping category' => 'Maak een nieuwe verzendcategorie aan', - 'Create a new shipping method' => 'Maak een nieuwe verzendmethode aan', - 'Create a new shipping rule' => 'Maak een nieuwe verzendingsregel', - 'Create a new tax category' => 'Maak een nieuwe fiscale categorie aan', - 'Create a new tax rate' => 'Maak een nieuw belastingtarief aan', - 'Create a product type' => 'Maak een producttype aan', - 'Create a shipping zone' => 'Maak een verzendingszone aan', - 'Create a tax zone' => 'Maak een belastingzone aan', - 'Create catalog pricing rules' => 'Catalogusprijsregels maken', - 'Create customer: “{email}”' => 'Klant aanmaken: “{email}”', - 'Create discounts' => 'Kortingen aanmaken', - 'Create discount…' => 'Korting aanmaken ...', - 'Create rules that allow this discount to match the order.' => 'Maak regels waardoor deze korting bij de bestelling past.', - 'Create rules that allow this discount to match the order’s billing address.' => 'Maak regels waardoor deze korting bij het factuuradres van de bestelling past.', - 'Create rules that allow this discount to match the order’s customer.' => 'Maak regels waardoor deze korting bij de klant van de bestelling past.', - 'Create rules that allow this discount to match the order’s shipping address.' => 'Maak regels waardoor deze korting bij het verzendadres van de bestelling past.', - 'Create rules that allow this gateway to match the billing address.' => 'Maak regels waardoor deze gateway bij het factuuradres past.', - 'Create rules that allow this gateway to match the order.' => 'Maak regels waardoor deze gateway overeenkomt met de bestelling.', - 'Create rules that allow this gateway to match the shipping address.' => 'Maak regels waardoor deze gateway bij het verzendadres past.', - 'Create sales' => 'Verkopen aanmaken', - 'Create sale…' => 'Aanbieding aanmaken ...', - 'Created' => 'Gemaakt', - 'Credit Card Payment Type' => 'Creditcard betalingstype', - 'Currency Code' => 'Valutacode', - 'Currency saved.' => 'Valuta opgeslagen.', - 'Currency' => 'Muntsoort', - 'Current' => 'Huidige', - 'Custom 1' => 'Aangepast 1', - 'Custom 2' => 'Aangepast 2', - 'Custom 3' => 'Aangepast 3', - 'Custom 4' => 'Aangepast 4', - 'Custom' => 'Aangepast', - 'Customer Enabled?' => 'Klant ingeschakeld?', - 'Customer ID is required.' => 'Klant-ID is verplicht.', - 'Customer Note' => 'Notitie van klant', - 'Customer Notices' => 'Kennisgevingen aan klanten', - 'Customer data' => 'Klantgegevens', - 'Customer' => 'Klant', - 'Damaged' => 'Beschadigd', - 'Data shown might be outdated.' => 'De weergegeven gegevens zijn mogelijk verouderd.', - 'Date Authorized' => 'Datum geautoriseerd', - 'Date Created' => 'Datum aangemaakt', - 'Date First Paid' => 'Datum eerste betaling', - 'Date Ordered' => 'Datum besteld', - 'Date Paid' => 'Datum van betaling', - 'Date Updated' => 'Datum van update', - 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Datum vanaf wanneer de catalogusprijsregel actief zal zijn. Laat leeg voor onbeperkte startdatum', - 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Datum vanaf wanneer de korting actief zal zijn. Laat leeg voor onbeperkte startdatum', - 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Datum vanaf wanneer de verkoop actief zal zijn. Laat leeg voor onbeperkte startdatum', - 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Datum waarop de catalogusprijsregel zal stoppen. Laat leeg voor onbeperkte einddatum', - 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Datum waarop de korting zal stoppen. Laat leeg voor onbeperkte einddatum', - 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Datum waarop de verkoop zal stoppen. Laat leeg voor onbeperkte einddatum', - 'Date' => 'Datum', - 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Standaard - de prijs mag negatief zijn als kortingen hoger zijn dan de bestelwaarde.', - 'Default Category' => 'Standaardcategorie', - 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'De standaard weergave van het Commerce-configuratiescherm. Als de gebruiker geen rechten heeft, wordt een voor de gebruiker toegankelijke locatie gebruikt.', - 'Default Order PDF' => 'Standaard bestel-pdf', - 'Default Per Item Rate' => 'Standaardkost per item', - 'Default Percentage Rate' => 'Standaardpercentage', - 'Default Status?' => 'Standaard status?', - 'Default View' => 'Standaardweergave', - 'Default Weight Rate' => 'Standaard gewichttarief', - 'Default Zone' => 'Standaard zone', - 'Default status?' => 'Standaardstatus?', - 'Default to this tax zone when no billing address is set' => 'Stel standaard deze belastingzone in als er geen factuuradres is ingesteld', - 'Default to this tax zone when no shipping address is set' => 'Standaard BTW zone met dit verzendadres is ingesteld', - 'Default variant updated.' => 'Standaardvariant bijgewerkt.', - 'Default' => 'Standaard', - 'Default?' => 'Standaard?', - 'Delete catalog pricing rules' => 'Catalogusprijsregels verwijderen', - 'Delete discounts' => 'Kortingen verwijderen', - 'Delete orders' => 'Bestellingen verwijderen', - 'Delete sales' => 'Verkopen verwijderen', - 'Delete' => 'Verwijder', - 'Deleting the {location} location.' => 'Verwijder de locatie {location}.', - 'Describe this rule.' => 'Beschrijf deze regel.', - 'Describe this shipping zone.' => 'Geef een beschrijving voor de verzendingszone', - 'Describe this tax zone.' => 'Beschrijf deze fiscale zone.', - 'Description' => 'Beschrijving', - 'Destination Inventory Location' => 'Bestemmingsvoorraadlocatie', - 'Destination' => 'Bestemming', - 'Details' => 'Details', - 'Dimension Unit' => 'Afmetingen eenheid', - 'Dimensions' => 'Afmeting', - 'Disabled' => 'Uitgeschakeld', - 'Disallow' => 'Niet toestaan', - 'Discount all line items' => 'Korting toepassen op alle regelartikelen', - 'Discount description.' => 'Kortingsbeschrijving.', - 'Discount is not allowed for the order' => 'Korting is niet toegestaan voor de bestelling', - 'Discount is out of date.' => 'De korting is verouderd.', - 'Discount saved.' => 'Korting opgeslagen.', - 'Discount the matching items only' => 'Korting alleen toepassen op overeenstemmende artikelen', - 'Discount use has reached its limit.' => 'De limiet voor het kortinggebruik is bereikt.', - 'Discount' => 'Korting', - 'Discounted Item Subtotal' => 'Subtotaal kortingsartikel', - 'Discounted Items' => 'Artikelen met korting', - 'Discounts deleted.' => 'Kortingen verwijderd.', - 'Discounts reordered.' => 'Kortingen herschikt.', - 'Discounts updated.' => 'Kortingen bijgewerkt.', - 'Discounts' => 'Kortingen', - 'Disqualify with valid business tax ID?' => 'Diskwalificeren met geldig btw-nummer?', - 'Do not apply subsequent matching sales beyond applying this sale.' => 'Pas geen opeenvolgende overeenkomende verkopen toe na het toepassen van deze verkoop.', - 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Pas dit tarief niet toe als het besteladres een of meer van de geselecteerde geldige btw-nummers bevat.', - 'Do not attach a PDF to this email' => 'Voeg geen PDF toe in bijlage bij deze e-mail', - 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Geen herberekening vragen voor bestelling (Nummer: {orderNumber}) als er fouten aanwezig zijn.', - 'Donation can not be zero.' => 'Een donatie mag niet gelijk zijn aan nul.', - 'Donation needs to be an amount.' => 'De donatie moet een bedrag zijn.', - 'Donation settings saved.' => 'Donatie-instellingen opgeslagen.', - 'Donation' => 'Donatie', - 'Donations' => 'Donaties', - 'Done' => 'Klaar', - 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Geen verdere kortingen toepassen voor een bestelling als deze korting wordt toegepast', - 'Download PDF' => 'PDF downloaden', - 'Download PDF…' => 'PDF downloaden…', - 'Download Type' => 'Downloadtype', - 'Download' => 'Downloaden', - 'Draft' => 'Concept', - 'Dummy gateway payment failed.' => 'Dummygatewaybetaling mislukt.', - 'Duplicate options exist' => 'Er zijn dubbele opties', - 'Duration' => 'Duur', - 'EU VAT ID' => 'EU-btw-nummer', - 'Edit address' => 'Adres bewerken', - 'Edit adjustments' => 'Aanpassingen bewerken', - 'Edit catalog pricing rules' => 'Catalogusprijsregels bewerken', - 'Edit discounts' => 'Kortingen bewerken', - 'Edit options' => 'Opties bewerken', - 'Edit orders' => 'Bestellingen bewerken', - 'Edit sales' => 'Verkopen bewerken', - 'Edit' => 'Bewerken', - 'Effect' => 'Effect', - 'Either (Default) - The relationship field is on the purchasable or the category' => 'Eender (standaard) - Het relatieveld bevindt zich op het koopbare artikel of de categorie', - 'Either way' => 'Allebei', - 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Fout bij genereren van e-mail-PDF voor e-mail “{email}”. Bestelling: “{order}”. PDF-sjabloonfout: “{message}” {file}:{line}', - 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'PDF-sjabloon voor e-mail bestaat niet in “{templatePath}” voor e-mail “{email}”. Bestelling: “{order}”.', - 'Email Subject' => 'E-mail onderwerp', - 'Email error. No email address found for order. Order: “{order}”' => 'E-mailfout. Geen e-mailadres gevonden voor bestelling. Bestelling: "{order}"', - 'Email is not enabled.' => 'E-mail is niet ingeschakeld.', - 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'E-mailsjabloon platte tekst bestaat niet in “{templatePath}”. Dit heeft geresulteerd in “{templateParsedPath}” voor e-mail “{email}”. Bestelling: “{order}”.', - 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in platte tekst e-mailsjabloon voor e-mail “{email}”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', - 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in platte tekst e-mailsjabloonpad voor e-mail “{email}” in “Sjabloonpad:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', - 'Email required to make payments on a completed order.' => 'Het e-mailadres is verplicht voor betaling van een voltooide bestelling.', - 'Email saved.' => 'E-mail opgeslagen.', - 'Email sent' => 'E-mail verzonden', - 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'De e-mailsjabloon bestaat niet in “{templatePath}”. Dit heeft geresulteerd in “{templateParsedPath}” voor e-mail “{email}”. Bestelling: “{order}”.', - 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloon voor aangepaste e-mail “{email}” in “Aan:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloon voor e-mail “{email}” in “Bcc:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloon voor e-mail “{email}” in “Cc:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloon voor e-mail “{email}” in “Antwoorden aan:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloon voor e-mail “{email}” in “Onderwerp:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', - 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloon voor e-mail “{email}”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', - 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Parseerfout in e-mailsjabloonpad voor e-mail “{email}” in “Sjabloonpad:”. Bestelling: “{order}”. Sjabloonfout: “{message}” {file}:{line}', - 'Email unavailable.' => 'E-mailadres niet beschikbaar.', - 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'E-mail “{email}” kon niet worden verzonden voor bestelling “{order}”. Fout: {error} {file}:{line}', - 'Email “{email}” for order {order} was cancelled.' => 'E-mail "{email}" voor bestelling "{order}" is geannuleerd.', - 'Email' => 'E-mail', - 'Emails' => 'E-mails', - 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Schakel dit in als dit tarief moet worden opgenomen in de belastbare prijs in plaats van het toevoegen van een kostenpost aan de bestelling.', - 'Enable structure for products of this type' => 'Structuur inschakelen voor producten van dit type', - 'Enable this discount' => 'Schakel deze korting in', - 'Enable this rule' => 'Schakel deze regel in', - 'Enable this sale' => 'Schakel deze korting in', - 'Enable this shipping method on the front end' => 'Activeer deze verzendmethode aan de voorkant', - 'Enable this shipping rule' => 'Deze verzending regel inschakelen', - 'Enable this tax rate' => 'Schakel dit belastingtarief in', - 'Enabled for customers to select during checkout?' => 'Ingeschakeld voor klanten om te selecteren tijdens het afrekenen?', - 'Enabled for customers to select?' => 'Ingeschakeld voor selectie door klanten?', - 'Enabled' => 'Ingeschakeld', - 'Enabled?' => 'Ingeschakeld?', - 'End Date' => 'Einddatum', - 'Enter SKU' => 'Voer Artikelnummer in', - 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Voeg een mensvriendelijke naam in voor dit belastingtarief voor gebruik in het configuratiescherm.', - 'Enter a percentage like {ex1} or {ex2}.' => 'Voer een percentage in, bijvoorbeeld {ex1} of {ex2}.', - 'Enter coupon code' => 'Couponcode invoeren', - 'Enter reference' => 'Referentie invoeren', - 'Error refunding transaction: {transactionHash}' => 'Fout bij terugbetaling transactie: {transactionHash}', - 'Every new store must be assigned to at least one site.' => 'Elke nieuwe winkel moet aan minimaal één site worden toegewezen.', - 'Everywhere' => 'Overal', - 'Example' => 'Voorbeeld', - 'Exclude this discount for products that are already on promotion' => 'Sluit deze korting uit voor producten die al in de promotie zijn', - 'Expired Link' => 'Link verlopen', - 'Expired' => 'Verlopen', - 'Expiry Date' => 'Houdbaarheidsdatum', - 'Expiry date' => 'Vervaldatum', - 'Expiry' => 'Verloop', - 'Failed to receive transfer: {error}' => 'Overdracht ontvangen mislukt: {error}', - 'Failed to send email. Please try again.' => 'E-mail versturen mislukt. Probeer het opnieuw.', - 'Failed to start' => 'Start mislukt', - 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Bijwerken van {num, plural, =1{bestelstatus} other{bestelstatussen}} mislukt.', - 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Bijwerken van bestelstatus mislukt voor {num, plural, =1{bestelling} other{bestellingen}}.', - 'Feet (ft)' => 'Voet (ft)', - 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Aan het filteren op voorwaarden die beschrijven op welke bestelling deze regel van toepassing is. Voer 0 in om een voorwaarde over te slaan.', - 'First Name' => 'Voornaam', - 'Flat Amount Off Order' => 'Vast kortingbedrag voor bestelling', - 'Flat Order Discount Amount Off' => 'Afgetrokken vast kortingbedrag bestelling', - 'Free Order Payment Strategy' => 'Betaalstrategie gratis bestelling', - 'Free Shipping' => 'Gratis verzending', - 'Free orders are processed by the payment gateway' => 'Gratis bestellingen worden verwerkt door de betalingsgateway', - 'Free orders complete immediately' => 'Gratis bestellingen worden onmiddellijk uitgevoerd', - 'Free shipping can only be for whole order or matching items, not both.' => 'Gratis verzending is alleen van toepassing voor de hele bestelling of overeenstemmende artikelen, niet voor beide.', - 'From Name' => 'Van naam', - 'Fulfill' => 'Vervullen', - 'Fulfilled' => 'Vervuld', - 'Fulfillment' => 'Vervulling', - 'Full Name' => 'Volledige naam', - 'Gateway Code' => 'Gatewaycode', - 'Gateway Message' => 'Gatewaybericht', - 'Gateway Reference' => 'Gatewayreferentie', - 'Gateway Response' => 'Antwoord van de gateway', - 'Gateway doesn’t support authorize' => 'De gateway ondersteunt geen autorisatie', - 'Gateway doesn’t support partial refunds.' => 'De gateway ondersteunt geen gedeeltelijke terugbetalingen.', - 'Gateway doesn’t support purchase' => 'De gateway ondersteunt geen aankoop', - 'Gateway doesn’t support refunds.' => 'De gateway ondersteunt geen terugbetalingen.', - 'Gateway saved.' => 'Gateway opgeslagen.', - 'Gateway' => 'Gateway', - 'Gateways reordered.' => 'Gateways herschikt.', - 'Gateways' => 'Gateways', - 'General Settings' => 'Algemene instellingen', - 'General' => 'Algemeen', - 'Generate' => 'Genereren', - 'Generated Coupon Format' => 'Couponnotatie gegenereerd', - 'Grams (g)' => 'Gram (g)', - 'Groups for which this sale will be applicable to.' => 'Groepen waarop deze verkoop van toepassing is.', - 'HTML Email Template Path' => 'HTML e-mail sjabloonpad', - 'Handle' => 'Ingang', - 'Harmonized System Code' => 'Geharmoniseerde systeemcode', - 'Has Admin Notices' => 'Heeft beheerderskennisgevingen', - 'Has Emails?' => 'Heeft e-mails?', - 'Has Free Shipping' => 'Heeft gratis verzending', - 'Has Orders' => 'Heeft bestellingen', - 'Has Purchasable' => 'Heeft koopbare', - 'Has Variants?' => 'Heeft varianten?', - 'Height ({unit})' => 'Hoogte ({unit})', - 'Height' => 'Hoogte', - 'Hide snapshot' => 'Snapshot verbergen', - 'History' => 'Geschiedenis', - 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'De tijd (in seconden) die een pdf-downloadlink geldig moet blijven voordat hij verloopt. Standaard is dit 86.400 (24 uur).', - 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Hoe vaak één e-mailadres is toegestaan om deze korting te gebruiken. Dit geldt voor alle voorgaande bestellingen, voor zowel gast als gebruiker. Voor een nul in voor onbeperkt gebruik door gasten en gebruikers.', - 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Hoe vaak een gebruiker deze korting mag gebruiken. Als dit is ingesteld op iets anders dan nul, is de korting alleen beschikbaar voor aangemelde gebruikers.', - 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Hoe vaak deze korting in totaal kan worden gebruikt door gasten of aangemelde gebruikers. Voer een nul in voor onbeperkt gebruik.', - 'How products should be labeled within the control panel.' => 'Hoe producten op het configuratiescherm gelabeld moeten worden.', - 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'De wijze waarop de aankopen en categorieën met elkaar gerelateerd zijn. Dit bepaalt de overeenstemmende items. Zie [Relatieterminologie]({link}).', - 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'Hoe dit product wordt omschreven bij een lijnitem in een order. Je kunt tags met eigenschappen toevoegen, zoals {ex1} of {ex2}.', - 'How this shipping method will be referred to in templates and forms.' => 'Hoe naar deze verzendmethode in sjablonen en formulieren wordt verwezen.', - 'How variants should be labeled within the control panel.' => 'Hoe varianten op het configuratiescherm gelabeld moeten worden.', - 'How you’ll refer to this PDF in the templates.' => 'Hoe u naar deze pdf verwijst in de sjablonen.', - 'How you’ll refer to this product type in the templates.' => 'Hoe u zult verwijzen naar dit type product in de templates.', - 'How you’ll refer to this shipping category in the templates.' => 'Hoe deze verzendcategorie wordt genoemd in de sjablonen.', - 'How you’ll refer to this status in the templates.' => 'Hoe zult u verwijzen naar deze status in de templates.', - 'How you’ll refer to this subscription plan in the templates.' => 'Manier waarop u in sjablonen naar dit abonnementsplan verwijst.', - 'How you’ll refer to this tax category in the templates.' => 'Hoe zult u verwijzen naar deze fiscale categorie in de sjablonen.', - 'ID' => 'ID', - 'IP Address' => 'IP-adres', - 'If disabled, this PDF will not be available or sent with emails.' => 'Indien dit niet is geselecteerd, zal deze pdf niet beschikbaar zijn of niet verzonden worden met e-mails.', - 'If disabled, this email will not send.' => 'Bij uitschakeling wordt deze e-mail niet verzonden.', - 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Als dit is ingeschakeld en dit tarief niet overeen komt met de bestelling, wordt het tariefbedrag in mindering gebracht op de prijs in de winkelwagen.', - 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Indien ingesteld op \'Alleen met toestemming\', moet u handmatig de betalingen vastleggen voordat het geld zal worden overgemaakt op uw rekening. De Gateway moet de geselecteerde optie ondersteunen.', - 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Als u het percentage selecteert als “aftrekken van de itemprijs met korting”, omvat dit het “Bedrag per item” en alle andere eerder toegepaste kortingen.', - 'Ignore Promotions?' => 'Promoties negeren?', - 'Ignore previous matching sales if this sale matches.' => 'Eerdere overeenkomende verkopen negeren als deze verkoop overeenkomt.', - 'Ignore promotional prices when this discount is applied to matching line items' => 'Promotieprijzen negeren als deze korting is toegepast op overeenstemmende lijnitems', - 'Inactive Carts' => 'Inactieve Winkelwagens', - 'Inches (in)' => 'Inch (in)', - 'Include built-in line item tax.' => 'Ingebouwd lijnitembelasting opnemen.', - 'Include in price?' => 'Opnemen in prijs?', - 'Include line item discounts.' => 'Lijnitemkortingen opnemen.', - 'Include line item shipping costs.' => 'Verzendkosten voor lijnitem opnemen.', - 'Include separate line item tax.' => 'Afzonderlijke lijnitembelasting opnemen.', - 'Included in price?' => 'Opgenomen in prijs?', - 'Included' => 'Inbegrepen', - 'Incoming transfer from Transfer ID: ' => 'Inkomende overdracht van overdracht-ID: ', - 'Incoming' => 'Inkomend', - 'Info' => 'Info', - 'Information linked?' => 'Informatie gekoppeld?', - 'Information' => 'Informatie', - 'Invalid JSON' => 'JSON ongeldig', - 'Invalid Order ID' => 'Ongeldige bestelling-ID', - 'Invalid VAT ID.' => 'Ongeldig btw-nr.', - 'Invalid condition syntax' => 'Ongeldige voorwaardesyntaxis', - 'Invalid email.' => 'Ongeldig e-mailadres.', - 'Invalid formula syntax' => 'Ongeldige formulesyntaxis', - 'Invalid gateway: {value}' => 'Ongeldige gateway: {value}', - 'Invalid inventory movements.' => 'Ongeldige voorraadverplaatsingen.', - 'Invalid order condition syntax.' => 'De voorwaardesyntaxis van de bestelling is ongeldig.', - 'Invalid payment or order. Please review.' => 'Ongeldige betaling of bestelling. Controleer de gegevens.', - 'Invalid payment source ID: {value}' => 'Ongeldige betaalbron-ID: {value}', - 'Invalid store.' => 'Ongeldige winkel.', - 'Invalid user.' => 'Ongeldige gebruiker.', - 'Inventory Item' => 'Voorraaditem', - 'Inventory Location' => 'Voorraadlocatie', - 'Inventory Locations' => 'Voorraadlocaties', - 'Inventory Tracked' => 'Voorraad bijgehouden', - 'Inventory Transfers' => 'Voorraadoverdrachten', - 'Inventory could not be set.' => 'Voorraad kon niet worden ingesteld.', - 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'Voorraadlocatie heeft toegezegde voorraad, de bestelling(en) moet(en) eerst worden vervuld.', - 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Voorraadlocatie heeft inkomende voorraad, de overdracht(en) moet(en) eerst worden vervuld.', - 'Inventory location is already deactivated.' => 'Voorraadlocatie is al gedeactiveerd.', - 'Inventory location saved.' => 'Voorraadlocatie opgeslagen.', - 'Inventory locations not saved.' => 'Voorraadlocaties niet opgeslagen.', - 'Inventory movement could not be saved.' => 'Voorraadverplaatsing kon niet worden opgeslagen.', - 'Inventory movement saved.' => 'Voorraadverplaatsing opgeslagen.', - 'Inventory updated.' => 'Voorraad bijgewerkt.', - 'Inventory was not updated.' => 'Voorraad niet bijgewerkt.', - 'Inventory' => 'Voorraad', - 'Invoice amount' => 'Factuurbedrag', - 'Invoice date' => 'Factuurdatum', - 'Is Promotable' => 'Is promootbaar', - 'Is Promotional Price?' => 'Is promotieprijs?', - 'Is Shippable' => 'Is verzendbaar', - 'Is Taxable' => 'Is belastbaar', - 'Item Rates' => 'Itemtarieven', - 'Item Subtotal' => 'Subtotaal artikel', - 'Item Total' => 'Totaal artikel', - 'Item' => 'Item', - 'Items' => 'Artikelen', - 'Kilograms (kg)' => 'Kilogram (kg)', - 'Label' => 'Label', - 'Landscape' => 'Liggend', - 'Language' => 'Taal', - 'Last Name' => 'Achternaam', - 'Last Updated' => 'Laatst bijgewerkt', - 'Leave a category rate override blank to use the rate from above.' => 'Laat een categorietariefoverschrijving leeg om het tarief van hierboven te gebruiken.', - 'Leave blank for unlimited uses.' => 'Laat leeg voor onbeperkt gebruik.', - 'Leave blank if products don’t have URLs' => 'Leeg laten als de producten geen URL hebben', - 'Leave gateway subscription as-is' => 'Gatewayabonnement ongewijzigd laten', - 'Length ({unit})' => 'Lengte ({unit})', - 'Length' => 'Lengte', - 'Let each product choose which sites it should be saved to' => 'Laat elk product kiezen op welke sites het wordt opgeslagen', - 'Limit which orders this discount applies to based on its line items.' => 'Beperk op welke bestellingen deze korting van toepassing is op basis van de artikelen.', - 'Limit which purchasables this sale applies to.' => 'Beperk op welke koopbare artikelen deze aanbieding van toepassing is.', - 'Limit' => 'Limiet', - 'Line Item Statuses' => 'Lijnitemstatussen', - 'Line Item' => 'Lijnitem', - 'Line Items' => 'Regelartikel', - 'Line item price (minus discounts)' => 'Lijnitemprijs (min kortingen)', - 'Line item shipping cost' => 'Verzendkosten regelartikel', - 'Line item statuses reordered.' => 'Statussen lijnitems herschikt.', - 'Link Duration' => 'Duur van de link', - 'Link Sent' => 'Link verstuurd', - 'Link to a product' => 'Link naar een product', - 'Link to a variant' => 'Link naar een variant', - 'Link' => 'Link', - 'Live' => 'Live', - 'Location' => 'Locatie', - 'Locations that should be available for previewing products in this product type.' => 'Locaties die beschikbaar moeten zijn voor het bekijken van een voorbeeld van producten in dit producttype.', - 'MM' => 'MM', - 'Make a payment' => 'Verricht een betaling', - 'Make this the primary store' => 'Dit de primaire winkel maken', - 'Manage Inventory' => 'Voorraad beheren', - 'Manage donation settings' => 'Donatie-instellingen beheren', - 'Manage general store settings' => 'Algemene winkelinstellingen beheren', - 'Manage inventory locations' => 'Voorraadlocaties beheren', - 'Manage inventory stock levels' => 'Voorraadniveaus beheren', - 'Manage inventory transfers' => 'Voorraadoverdrachten beheren', - 'Manage orders' => 'Bestellingen beheren', - 'Manage payment currencies' => 'Betaalvaluta\'s beheren', - 'Manage promotions' => 'Promoties beheren', - 'Manage shipping' => 'Verzending beheren', - 'Manage store settings' => 'Winkelinstellingen beheren', - 'Manage subscription plans' => 'Abonnementsplannen beheren', - 'Manage subscription' => 'Abonnement beheren', - 'Manage subscriptions' => 'Abonnementen beheren', - 'Manage taxes' => 'Belastingen beheren', - 'Manage' => 'Beheren', - 'Mark as Pending' => 'Markeren als in afwachting', - 'Mark as completed' => 'Markeren als voltooid', - 'Match Billing Address' => 'Factuuradres matchen', - 'Match Customer' => 'Klant matchen', - 'Match Order' => 'Bestelling matchen', - 'Match Orders' => 'Bestellingen matchen', - 'Match Product' => 'Product matchen', - 'Match Purchasable' => 'Koopbare matchen', - 'Match Shipping Address' => 'Verzendadres matchen', - 'Match Variant' => 'Variant matchen', - 'Matching Items' => 'Overeenstemmende items', - 'Max Qty' => 'Maximumhoeveelheid', - 'Max Uses' => 'Max. aantal gebruiken', - 'Max Variants' => 'Max. aantal varianten', - 'Max quantity must greater than min.' => 'Maximumhoeveelheid moet groter zijn dan minimum.', - 'Maximum Purchase Quantity' => 'Maximale orderhoeveelheid', - 'Maximum Total Shipping Cost' => 'Maximale totale verzendkosten', - 'Maximum allowed quantity' => 'Maximaal toegestane hoeveelheid', - 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Maximaal aantal bijpassende items dat kan worden besteld zodat deze account van toepassing is. Met een waarde van nul wordt deze voorwaarde overgeslagen.', - 'Maximum order quantity for this item is {num}.' => 'De maximale bestelhoeveelheid voor dit artikel is {num}.', - 'Message' => 'Bericht', - 'Meters (m)' => 'Meter (m)', - 'Millimeters (mm)' => 'Millimeter (mm)', - 'Min Qty' => 'Minimumhoeveelheid', - 'Min quantity must be less than max.' => 'Minimumhoeveelheid moet keiner zijn dan maximum.', - 'Minimum Purchase Quantity' => 'Minimale bestelhoeveelheid', - 'Minimum Total Price Strategy' => 'Strategie voor minimale totaalprijs', - 'Minimum Total Shipping Cost' => 'Minimale totale verzendkosten', - 'Minimum allowed quantity' => 'Minimaal toegestane hoeveelheid', - 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Minimale aantal bijpassende items dat moet worden besteld om deze korting te krijgen.', - 'Minimum order quantity for this item is {num}.' => 'De minimale bestelhoeveelheid voor dit artikel is {num}.', - 'Missing Gateway' => 'Ontbrekende gateway', - 'Missing a default inventory location.' => 'Standaard voorraadlocatie ontbreekt.', - 'Move Inventory' => 'Voorraad verplaatsen', - 'Move To' => 'Verplaatsen naar', - 'Move {qty} from {fromType} to {toType}' => '{qty} verplaatsen van {fromType} naar {toType}', - 'Move' => 'Verplaatsen', - 'Movement from deactivated inventory location' => 'Verplaatsing van gedeactiveerde voorraadlocatie', - 'Movement' => 'Verplaatsing', - 'Must have at least one variant.' => 'Moet minstens één variant hebben.', - 'Name Field' => 'Naamveld', - 'Name' => 'Naam', - 'New Customer' => 'Nieuwe klant', - 'New Customers' => 'Nieuwe klanten', - 'New Order' => 'Nieuwe bestelling', - 'New PDF' => 'Nieuwe pdf', - 'New address' => 'Nieuw adres', - 'New catalog pricing rule' => 'Nieuwe catalogusprijsregel', - 'New currency' => 'Nieuwe valuta', - 'New discount' => 'Nieuwe korting', - 'New email' => 'Nieuwe e-mail', - 'New gateway' => 'Nieuwe gateway', - 'New line item status' => 'Nieuwe lijnitemstatus', - 'New line items get this status by default when the order is completed' => 'Nieuwe regelartikelen krijgen standaard deze status wanneer de bestelling voltooid is', - 'New location' => 'Nieuwe locatie', - 'New order status' => 'Status van nieuwe order', - 'New orders get this status by default' => 'Nieuwe bestellingen krijgen deze status standaard', - 'New product type' => 'Nieuw producttype', - 'New product' => 'Nieuw product', - 'New product, choose a type' => 'Nieuw product, kies een type', - 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Nieuwe producten worden standaard in de eerste beschikbare belastingcategorie geplaatst. Als er geen beschikbaar is, wordt deze categorie gebruikt.', - 'New sale' => 'Nieuwe korting', - 'New shipping category' => 'Nieuwe verzendcategorie', - 'New shipping method' => 'Nieuwe verzendmethode', - 'New shipping rule' => 'Nieuwe verzendingsregel', - 'New shipping zone' => 'Nieuw verzendingszone', - 'New subscription plan' => 'Nieuw abonnementsplan', - 'New tax category' => 'Nieuwe fiscale categorie', - 'New tax rate' => 'Nieuw BTW tarief', - 'New tax zone' => 'Nieuwe fiscale zone', - 'New transfer' => 'Nieuwe overdracht', - 'New {productType} product' => 'Nieuw {productType}-product', - 'New' => 'Nieuw', - 'Next payment' => 'Volgende betaling', - 'No Address' => 'Geen adres', - 'No PDFs exist yet.' => 'Er bestaan ​​nog geen pdf\'s.', - 'No access given to any specific store management features.' => 'Geen toegang gegeven tot specifieke winkelbeheerfuncties.', - 'No additional payment currencies exist yet.' => 'Er zijn geen aanvullende betaalvaluta\'s.', - 'No address' => 'Geen adres', - 'No billing address' => 'Geen factuuradres', - 'No catalog pricing rule exists with the ID “{id}”' => 'Er bestaat geen catalogusprijsregel met ID “{id}”', - 'No catalog pricing rules exist yet.' => 'Er bestaan nog geen catalogusprijsregels.', - 'No currency exists with the ID “{id}”' => 'Er bestaan geen valuta met ID "{id}"', - 'No customer email address exists on this cart.' => 'Er bestaat geen e-mailadres van een klant voor deze winkelwagen.', - 'No description' => 'Geen beschrijving', - 'No discount exists with the ID “{id}”' => 'Er bestaat geen korting met ID "{id}"', - 'No discounts exist yet.' => 'Er bestaan nog geen kortingen.', - 'No donation amount supplied.' => 'Geen donatiebedrag opgegeven.', - 'No emails exist yet.' => 'Er bestaan nog geen e-mails.', - 'No inventory changes made.' => 'Geen voorraadwijzigingen aangebracht.', - 'No inventory found.' => 'Geen voorraad gevonden.', - 'No inventory movements made.' => 'Geen voorraadverplaatsingen gedaan.', - 'No inventory transactions for this location.' => 'Geen voorraadtransacties voor deze locatie.', - 'No new customer selected.' => 'Geen nieuwe klant geselecteerd.', - 'No order history exists with the ID “{id}”' => 'Er bestaat geen bestegeschiedenis met ID “{id}”', - 'No order status history items will exist until the cart becomes an order.' => 'Er worden pas items aan de geschiedenis van de orderstatus toegevoegd nadat de winkelwagen een order is geworden.', - 'No payment source exists with the ID “{id}”' => 'Er bestaat geen betaalbron met ID “{id}”', - 'No private Note.' => 'Geen privénotitie.', - 'No product available.' => 'Geen product beschikbaar.', - 'No product types exist yet.' => 'Er bestaan nog geen producttypen.', - 'No purchasable available.' => 'Geen koopbare artikelen beschikbaar.', - 'No sale exists with the ID “{id}”' => 'Er bestaat geen aanbieding met ID “{id}”', - 'No sales exist yet.' => 'Er bestaan ​​nog geen verkopen.', - 'No shipping address' => 'Geen verzendadres', - 'No shipping category exists with the ID “{id}”' => 'Er bestaat geen verzendcategorie met ID "{id}"', - 'No shipping method exists with the ID “{id}”' => 'Er bestaat geen verzendmethode met ID “{id}”', - 'No shipping rule exists with the ID “{id}”' => 'Er bestaat geen verzendingsregel met ID “{id}”', - 'No shipping rules exist yet.' => 'Er bestaan nog geen verzendingsregels.', - 'No shipping zone exists with the ID “{id}”' => 'Verzendingszone met ID “{id}” bestaat niet', - 'No stats available.' => 'Geen statistieken beschikbaar.', - 'No subscription plan exists with the ID “{id}”' => 'Er bestaat geen abonnementsplan met ID “{id}”', - 'No subscription plans exist yet.' => 'Er bestaan nog geen abonnementsplannen.', - 'No tax category exists with the ID “{id}”' => 'Er bestaat geen belastingcategorie met ID “{id}”', - 'No tax rate exists with the ID “{id}”' => 'Er bestaat geen belastingtarief met ID “{id}”', - 'No tax zone exists with the ID “{id}”' => 'Er bestaat geen belastingzone met ID “{id}”', - 'No transactions exist.' => 'Er zijn geen transacties.', - 'No user authenticated.' => 'Geen gebruiker geauthenticeerd.', - 'No' => 'Nee', - 'None on hand' => 'Geen aanwezig', - 'None' => 'Geen', - 'Not a valid address type' => 'Geen geldig adrestype', - 'Not a valid credit card number.' => 'Geen geldig creditcardnummer.', - 'Not all SKUs are unique.' => 'Niet alle SKU\'s zijn uniek.', - 'Note' => 'Opmerking', - 'Notes' => 'Notities', - 'Number of Coupons' => 'Aantal coupons', - 'Number' => 'Aantal', - 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Op welke van de bovenstaande ingeschakelde sites moeten producten van dit producttype worden opgeslagen?', - 'On Hand' => 'Aanwezig', - 'Only allow this gateway to be used for zero value orders?' => 'Toestaan dat deze gateway alleen wordt gebruikt voor orders met waarde nul?', - 'Only match certain purchasables…' => 'Alleen bepaalde koopbare artikelen vergelijken…', - 'Only match purchasables related to…' => 'Alleen koopbare artikelen vergelijken gerelateerd aan…', - 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Alleen bestellingen met de volgende bestelstatussen worden meegenomen. Laat dit leeg voor alle statussen.', - 'Only save product to the site they were created in' => 'Product alleen opslaan op de site waarop het is gemaakt', - 'Options' => 'Opties', - 'Order Condition Formula' => 'Formule bestelvoorwaarde', - 'Order Description Format' => 'Bestellingbeschrijvingsformaat', - 'Order Details' => 'Bestellingsgegevens', - 'Order Fields' => 'Bestellingsvelden', - 'Order PDF Download Link' => 'Pdf-downloadlink van de bestelling', - 'Order PDF Filename Format' => 'Bestelling-PDF-Bestandsnaamformaat', - 'Order Reference Number Format' => 'Notatie orderreferentienummer', - 'Order Settings' => 'Bestellingsinstellingen', - 'Order Site' => 'Bestelsite', - 'Order Status description.' => 'Beschrijving bestellingsstatus.', - 'Order Status' => 'Bestelstatus', - 'Order Statuses' => 'Orderstatussen', - 'Order can not be empty.' => 'De bestelling mag niet leeg zijn.', - 'Order count' => 'Aantal bestellingen', - 'Order customer data removed.' => 'Klantgegevens bestellingen verwijderd.', - 'Order deleted.' => 'Bestelling verwijderd.', - 'Order fields saved.' => 'Bestellingsvelden opgeslagen.', - 'Order not found.' => 'Bestelling niet gevonden.', - 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Het saldo voor betaling van de bestelling is {outstandingBalanceAsCurrency}. Dit is de maximumwaarde die wordt aangerekend.', - 'Order recalculated.' => 'Bestelling herberekend.', - 'Order status saved.' => 'Bestelstatus opgeslagen.', - 'Order statuses reordered.' => 'Bestelstatussen herschikt.', - 'Order total shipping cost' => 'Totale verzendkosten bestelling', - 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Totale belastbare prijs bestelling (subtotaal van regelartikel + totale kortingen + totale verzendkosten)', - 'Order' => 'Bestelling', - 'Orders (Legacy)' => 'Bestellingen (Verouders)', - 'Orders deleted.' => 'Bestellingen verwijderd.', - 'Orders not restored.' => 'Bestellingen niet hersteld.', - 'Orders restored.' => 'Bestellingen hersteld.', - 'Orders' => 'Bestellingen', - 'Organization Name' => 'Organisatienaam', - 'Organization Tax ID' => 'Fiscaal nummer organisatie', - 'Origin and destination cannot be the same.' => 'Oorsprong en bestemming kunnen niet hetzelfde zijn.', - 'Origin' => 'Oorsprong', - 'Original Price' => 'Oorspronkelijke prijs', - 'Original price' => 'Oorspronkelijke prijs', - 'Original promotional price' => 'Oorspronkelijke promotieprijs', - 'Other Languages' => 'Andere talen', - 'Other countries' => 'Andere landen', - 'Outgoing transfer from Transfer ID: ' => 'Uitgaande overdracht van overdracht-ID: ', - 'Overpaid' => 'Teveel betaald', - 'Overrides previous?' => 'Vorige overschrijven?', - 'PDF Attachment' => 'Pdf-bijlage', - 'PDF Template Path' => 'Pad van PDF-sjabloon', - 'PDF saved.' => 'PDF opgeslagen.', - 'PDF' => 'PDF', - 'PDFs & Emails' => 'Pdf\'s en e-mails', - 'PDFs' => 'Pdf\'s', - 'Paid Amount' => 'Betaald bedrag', - 'Paid Status' => 'Status Betaald', - 'Paid' => 'Betaald', - 'Paper Orientation' => 'Papierstand', - 'Paper Size' => 'Papierformaat', - 'Partial payment not allowed.' => 'Gedeeltelijke betaling niet toegestaan.', - 'Partial' => 'Gedeeltelijk', - 'Past year' => 'Vorig jaar', - 'Past {num} days' => 'Afgelopen {num} dagen', - 'Pay {amount} of {currency} on the order.' => '{amount} {currency} betalen voor de bestelling.', - 'Pay' => 'Betalen', - 'Payment Amount' => 'Betaalbedrag', - 'Payment Currencies' => 'Betaalvaluta\'s', - 'Payment Gateway' => 'Betalingsgateway', - 'Payment Method' => 'Betaalmethode', - 'Payment error: {message}' => 'Betalingsfout: {message}', - 'Payment method issue' => 'Probleem met betaalmethode', - 'Payment source created.' => 'Betaalbron aangemaakt.', - 'Payment source deleted.' => 'Betaalbron verwijderd.', - 'Payments' => 'Betalingen', - 'Pending' => 'In afwachting', - 'Per Email Address Discount Limit' => 'Kortingslimiet per e-mailadres', - 'Per Item Amount Off' => 'Kortingbedrag per item', - 'Per Item Discount' => 'Korting per artikel', - 'Per Item Percentage Off' => 'Kortingpercentage per item', - 'Per Item Rate' => 'Per stuk prijs', - 'Per User Discount Limit' => 'Kortingslimiet per gebruiker', - 'Percentage Rate' => 'Percentage Tarief', - 'Phone (Alt)' => 'Telefoon (alt)', - 'Phone' => 'Telefoon', - 'Pick a plan' => 'Een plan selecteren', - 'Plain Text Email Template Path' => 'Sjabloonpad e-mail met platte tekst', - 'Plan' => 'Plan', - 'Plans reordered.' => 'Plannen herschikt.', - 'Portrait' => 'Staand', - 'Post Date' => 'Post datum', - 'Postal Code Formula' => 'Postcodeformule', - 'Pounds (lb)' => 'Pond (lb)', - 'Preview' => 'Preview', - 'Previous Status' => 'Vorige status', - 'Price' => 'Prijs', - 'Prices' => 'Prijzen', - 'Pricing Rules' => 'Prijsregels', - 'Pricing jobs are currently running.' => 'Er worden momenteel prijstaken uitgevoerd.', - 'Pricing' => 'Prijzen', - 'Primary Billing Address' => 'Primair factuuradres', - 'Primary Shipping Address' => 'Primair verzendadres', - 'Primary payment source updated.' => 'Primaire betaalbron bijgewerkt.', - 'Primary' => 'Primair', - 'Private Note' => 'Privénotitie', - 'Product Fields' => 'Productvelden', - 'Product ID is required.' => 'De product-ID is verplicht.', - 'Product Template' => 'Product Sjabloon', - 'Product Title Format' => 'Titelformaat product', - 'Product Type' => 'Producttype', - 'Product Types' => 'Producttypen', - 'Product URI Format' => 'URI-formaat product', - 'Product Variant' => 'Productvariant', - 'Product Variants' => 'Productvarianten', - 'Product type saved.' => 'Producttype opgeslagen.', - 'Product type settings' => 'Instellingen voor het producttype', - 'Product' => 'Product', - 'Products and Variants deleted.' => 'Producten en varianten verwijderd.', - 'Products not restored.' => 'Producten niet hersteld.', - 'Products restored.' => 'Producten hersteld.', - 'Products' => 'Producten', - 'Promotable' => 'Promootbaar', - 'Promotable?' => 'Promotie mogelijk?', - 'Promotional Amount' => 'Promotiebedrag', - 'Promotional Price' => 'Promotieprijs', - 'Purchasable Categories' => 'Koopbare categorieën', - 'Purchasable ID and Sale ID are required.' => 'ID koopbaar artikel en ID aanbieding zijn verplicht.', - 'Purchasable ID is required.' => 'ID koopbaar artikel is verplicht.', - 'Purchasable Type' => 'Koopbaar type', - 'Purchasable' => 'Koopbaar', - 'Purchase (Authorize and Capture Immediately)' => 'Kopen (onmiddellijk autoriseren en registreren)', - 'Purchase Total' => 'Ordertotaal', - 'Qty' => 'Aant.', - 'Quality Control' => 'Kwaliteitscontrole', - 'Quantity' => 'Hoeveelheid', - 'Rate' => 'Score', - 'Reassign {numOrders, plural, =1{order} other{orders}}' => '{numOrders, plural, one {}=1{bestelling} other{bestellingen}} opnieuw toewijzen', - 'Recalculate order' => 'Bestelling herberekenen', - 'Receive Inventory' => 'Voorraad ontvangen', - 'Receive Transfer' => 'Overdracht ontvangen', - 'Receive' => 'Ontvangen', - 'Received' => 'Ontvangen', - 'Recent Orders' => 'Recente bestellingen', - 'Recipient' => 'Ontvanger', - 'Recover Cart' => 'Winkelwagen herstellen', - 'Reduce price' => 'Prijs verminderen', - 'Reduce the price by a fixed amount' => 'De prijs verminderen met een vast bedrag', - 'Reduce the price by a percentage of the original price' => 'Verlaag de prijs met een percentage van de originele prijs', - 'Reference' => 'Verwijzing', - 'Refresh payment history' => 'Betalingsgeschiedenis vernieuwen', - 'Refund note' => 'Terugbetalingsnotitie', - 'Refund payment' => 'Betaling terugbetalen', - 'Refund' => 'Terugbetaling', - 'Reject' => 'Weigeren', - 'Rejected' => 'Geweigerd', - 'Relationship Type' => 'Relatietype', - 'Removable included tax rates are only allowed for the default tax zone.' => 'Verwijderbare opgenomen belastingtarieven zijn alleen toegestaan voor de standaard belastingzone.', - 'Remove address' => 'Adres verwijderen', - 'Remove all shipping costs from the order' => 'Alle verzendkosten verwijderen van de bestelling', - 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Verwijder klantassociatie en e-mailadres uit de {numOrders, plural, one {}=1{bestelling} other{bestellingen}}. Selecteer optioneel extra klantgegevens hieronder om te verwijderen', - 'Remove customer data' => 'Klantgegevens verwijderen', - 'Remove from price?' => 'Verwijderen van prijs?', - 'Remove shipping costs for matching items only' => 'Verzendkosten alleen verwijderen voor overeenstemmende items', - 'Remove the included tax when a valid organization tax ID is present?' => 'Inbegrepen belasting verwijderen als er een geldig fiscaal nummer aanwezig is voor een organisatie?', - 'Remove' => 'Verwijderen', - 'Removed' => 'Verwijderd', - 'Repeat Customers' => 'Terugkerende klanten', - 'Reply To' => 'Antwoorden aan', - 'Require Billing Address At Checkout' => 'Factuuradres vereisen bij afrekenen', - 'Require Coupon Code' => 'Couponcode vereisen', - 'Require Shipping Address At Checkout' => 'Verzendadres vereisen bij afrekenen', - 'Require Shipping Method Selection At Checkout' => 'Selectie van verzendmethode vereisen bij afrekenen', - 'Require' => 'Vereisen', - 'Reserved' => 'Gereserveerd', - 'Reset usage' => 'Gebruik resetten', - 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Beperk de korting tot de bestellingen waarbij de klant een minimum aantal waarde aan bijbehorende items heeft gekocht.', - 'Revenue Options' => 'Omzetopties', - 'Revenue' => 'Inkomsten', - 'Rule' => 'Regel', - 'Rules reordered.' => 'Regels herschikt.', - 'SKU' => 'Artikelnummer', - 'Safety' => 'Veiligheid', - 'Sale Price' => 'Prijs aanbieding', - 'Sale description.' => 'Aanbieding omschrijving.', - 'Sale reordered.' => 'Aanbieding herschikt.', - 'Sale saved.' => 'Aanbieding opgeslagen.', - 'Sale' => 'Aanbieding', - 'Sales deleted.' => 'Verkopen verwijderd.', - 'Sales updated.' => 'Aanbiedingen bijgewerkt.', - 'Sales' => 'Aanbiedingen', - 'Save and continue editing' => 'Opslaan en doorgaan met aanpassen', - 'Save and return to all orders' => 'Opslaan en terugkeren naar alle bestellingen', - 'Save and set rules' => 'Opslaan en voer regels in', - 'Save as a new rule' => 'Opslaan als nieuwe regel', - 'Save product to all sites enabled for this product type' => 'Product opslaan op alle sites die zijn ingeschakeld voor dit producttype', - 'Save product to other sites in the same site group' => 'Product opslaan op andere sites in dezelfde sitegroep', - 'Save product to other sites with the same language' => 'Product opslaan op andere sites met dezelfde taal', - 'Save' => 'Opslaan', - 'Search customer…' => 'Klant zoeken ...', - 'Search inventory' => 'Voorraad zoeken', - 'Search or enter customer email…' => 'E-mailadres klant zoeken of invoeren ...', - 'Search…' => 'Zoeken …', - 'See Orders' => 'Bestellingen weergeven', - 'Select a gateway' => 'Een gateway selecteren', - 'Select a tax category.' => 'Selecteer een BTW categorie.', - 'Select a tax zone. If empty, this rate will match anywhere.' => 'Selecteer een belastingzone. Als deze leeg is, is dit tarief overal van toepassing.', - 'Select address' => 'Adres selecteren', - 'Select an item' => 'Selecteer een item', - 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Selecteer hoe de catalogusprijsregel wordt toegepast op de koopbare artikelen.', - 'Select how the sale will be applied to the purchasable(s).' => 'Selecteer hoe de aanbieding wordt toegepast op de te koop aangeboden artikelen.', - 'Select product type' => 'Selecteer producttype', - 'Select the emails that will be sent when transitioning to this status.' => 'Selecteer de e-mails die zullen worden verstuurd wanneer er wordt overgegaan naar deze status.', - 'Select what this rate should be applied to.' => 'Selecteer waarop dit tarief moet worden toegepast.', - 'Send Email' => 'E-mail verzenden', - 'Send to custom recipient' => 'Verzenden naar aangepaste ontvanger', - 'Send to the customer' => 'Verstuur naar de klant', - 'Set Quantity' => 'Hoeveelheid instellen', - 'Set default category' => 'Standaardcategorie instellen', - 'Set default variant' => 'Standaardvariant instellen', - 'Set or Adjust' => 'Instellen of aanpassen', - 'Set price' => 'Prijs instellen', - 'Set status' => 'Status instellen', - 'Set the price to a flat amount' => 'Prijs instellen op vast bedrag', - 'Set the price to a percentage of the original price' => 'Stel de prijs in op een percentage van de oorspronkelijke prijs', - 'Set the sale price to a flat amount' => 'De prijs instellen op een vast bedrag', - 'Set the sale price to a percentage of the original price' => 'De prijs van de aanbieding instellen als een percentage van de oorspronkelijke prijs', - 'Set to' => 'Instellen op', - 'Settings saved.' => 'Instellingen opgeslagen.', - 'Settings' => 'Instellingen', - 'Share cart…' => 'Winkelwagen delen ...', - 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Verzending - de minimumkosten zijn de verzendkosten als de prijs van de bestelling lager is dan de verzendkosten.', - 'Shipping Address Zone' => 'Verzendadreszone', - 'Shipping Address' => 'Verzendadres', - 'Shipping Business Name' => 'Bedrijfsnaam voor verzending', - 'Shipping Categories' => 'Verzendcategorieën', - 'Shipping Category Conditions' => 'Voorwaarden verzendcategorie', - 'Shipping Category' => 'Verzendcategorie', - 'Shipping First Name' => 'Voornaam voor verzending', - 'Shipping Full Name' => 'Volledige naam voor verzending', - 'Shipping Last Name' => 'Achternaam voor verzending', - 'Shipping Method' => 'Verzendmethode', - 'Shipping Methods' => 'Verzend methoden', - 'Shipping Rule' => 'Verzendingsregel', - 'Shipping Zones' => 'Verzendingszones', - 'Shipping address required.' => 'Verzendadres verplicht.', - 'Shipping categories deleted.' => 'Verzendcategorieën verwijderd.', - 'Shipping category saved.' => 'Verzendcategorie opgeslagen.', - 'Shipping category updated.' => 'Verzendcategorie bijgewerkt.', - 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Verzendkosten die aan de bestelling worden toegevoegd als geheel voordat percentage-, item- en gewichtstarieven worden toegepast. Zet dit op nul om dit tarief uit te schakelen. De gehele regel, inclusief dit basistarief, wordt niet toegepast als de winkelwagen alleen niet-verzendbare items zoals digitale producten bevat.', - 'Shipping method saved.' => 'Verzendmethode opgeslagen.', - 'Shipping methods and rules deleted.' => 'Verzendcategorieën en regels verwijderd.', - 'Shipping methods updated.' => 'Verzendmethoden bijgewerkt.', - 'Shipping rule saved.' => 'Verzendingsregel opgeslagen.', - 'Shipping zone saved.' => 'Verzendingszone opgeslagen.', - 'Shipping' => 'Verzending', - 'Short Number' => 'Kort nummer', - 'Show Chart?' => 'Grafiek tonen?', - 'Show Order Count?' => 'Aantal bestellingen tonen?', - 'Show all prices' => 'Alle prijzen weergeven', - 'Show archived gateways' => 'Gearchiveerde gateways weergeven', - 'Show order count line on chart.' => 'Lijn met aantal bestellingen tonen in grafiek.', - 'Show related sales' => 'Gerelateerde verkopen weergeven', - 'Show rule details' => 'Regeldetails tonen', - 'Show the Dimensions and Weight fields for products of this type' => 'Toon de dimensies en gewicht velden voor producten van dit type', - 'Show the Title field for products' => 'Het titelveld tonen voor producten', - 'Show the Title field for variants' => 'Toon het titel veld voor varianten', - 'Signed In' => 'Aangemeld', - 'Site Languages' => 'Talen van de site', - 'Site store mapping saved.' => 'Site-winkelkoppeling opgeslagen.', - 'Sites' => 'Websites', - 'Slug' => 'Slug', - 'Snapshot' => 'Snapshot', - 'Snapshots' => 'Snapshots', - 'Some orders restored.' => 'Sommige bestellingen zijn hersteld.', - 'Some products restored.' => 'Sommige producten zijn hersteld.', - 'Some variants restored.' => 'Sommige verianten zijn hersteld.', - 'Something changed with the order before payment, please review your order and submit payment again.' => 'De bestelling is gewijzigd voorafgaand aan de betaling. Controleer uw bestelling en verricht de betaling opnieuw.', - 'Sorry, no matching options.' => 'Er zijn helaas geen overeenstemmende opties.', - 'Source - The purchasable relationship field is on the category' => 'Bron - het relatieveld van het koopbare artikel bevindt zich in de categorie', - 'Source' => 'Bron', - 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Geef een Twig-voorwaarde op die bepaalt of de korting van toepassing is voor een specifieke bestelling. (Via een `order` variabele kan worden verwezen naar de bestelling.)', - 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Geef een Twig-voorwaarde op die bepaalt of de verzendingsregel van toepassing is voor een specifieke bestelling. (Via een `order` variabele kan worden verwezen naar de bestelling.)', - 'Start Date' => 'Startdatum', - 'State' => 'Provincie', - 'Status Email Address' => 'E-mailadres status', - 'Status Emails' => 'Status e-mail', - 'Status History' => 'Statusgeschiedenis', - 'Status Updated.' => 'Status bijgewerkt.', - 'Status change message' => 'Melding van statuswijziging', - 'Status' => 'Status', - 'Stock' => 'Voorraad', - 'Stops Processing?' => 'Verwerking stoppen?', - 'Stops subsequent?' => 'Volgende stoppen?', - 'Store Location' => 'Winkellocatie', - 'Store Management' => 'Winkelbeheer', - 'Store Markets' => 'Winkelmarkten', - 'Store Rule' => 'Winkelregel', - 'Store saved.' => 'Winkel opgeslagen.', - 'Store' => 'Winkel', - 'Stores & Sites' => 'Winkels en sites', - 'Stores' => 'Winkels', - 'Strategy to apply when an order is free or has a zero balance.' => 'De strategie die moet worden toegepast voor een gratis bestelling of een bestelling met een nulsaldo.', - 'Strategy to apply when calculating the minimum order price.' => 'Toe te passen strategie bij het berekenen van de minimale bestelprijs.', - 'Subject' => 'Onderwerp', - 'Subscribing user' => 'Geabonneerde gebruiker', - 'Subscription Fields' => 'Abonnementsvelden', - 'Subscription Plans' => 'Abonnementsplannen', - 'Subscription Settings' => 'Abonnementsinstellingen', - 'Subscription cancelled.' => 'Abonnement geannuleerd.', - 'Subscription date' => 'Datum van abonnement', - 'Subscription fields saved.' => 'Abonnementsvelden opgeslagen.', - 'Subscription for {user} to {plan} prevented by a plugin.' => 'Abonnement van {user} voor {plan} is door een plug-in voorkomen.', - 'Subscription plan saved.' => 'Abonnementsplan opgeslagen.', - 'Subscription plan' => 'Abonnementsplan', - 'Subscription plans' => 'Abonnementsplannen', - 'Subscription reactivated.' => 'Abonnement opnieuw geactiveerd.', - 'Subscription reference' => 'Abonnementsreferentie', - 'Subscription started.' => 'Abonnement gestart.', - 'Subscription switched.' => 'Abonnement omgeschakeld.', - 'Subscription to “{plan}”' => 'Abonnement op “{plan}”', - 'Subscription' => 'Abonnement', - 'Subscriptions on hold' => 'Abonnementen in wachtstand', - 'Subscriptions' => 'Abonnementen', - 'Suppress emails' => 'E-mails onderdrukken', - 'Switch plan' => 'Ander plan selecteren', - 'Switch' => 'Wisselen', - 'System' => 'Systeem', - 'Table Columns' => 'Tabelkolommen', - 'Target - The category relationship field is on the purchasable' => 'Doel - het relatieveld categorie bevindt zich op het koopbare artikel', - 'Tax & Shipping' => 'Belasting en verzending', - 'Tax (inc)' => 'Belasting (incl.)', - 'Tax Categories' => 'BTW categorieën', - 'Tax Category' => 'Fiscale Categorie', - 'Tax Rates' => 'Belastingtarieven', - 'Tax Zone' => 'Fiscale Zone', - 'Tax Zones' => 'Fiscale zones', - 'Tax categories deleted.' => 'Belastingcategorieën verwijderd.', - 'Tax category saved.' => 'Belastingcategorie opgeslagen.', - 'Tax category updated.' => 'Belastingcategorie bijgewerkt.', - 'Tax rate saved.' => 'Belastingtarief opgeslagen.', - 'Tax rates updated.' => 'Belastingtarieven bijgewerkt.', - 'Tax zone saved.' => 'Belastingzone opgeslagen.', - 'Tax' => 'Belasting', - 'Taxable Subject' => 'Belastingplichtig persoon', - 'Template Path' => 'Template pad', - 'That handle is already in use' => 'Deze ingang is al in gebruik', - 'That handle is already in use.' => 'Deze ingang is al in gebruik.', - 'The PDF to attach to this email.' => 'De pdf die in bijlage wordt toegevoegd aan deze e-mail.', - 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'De URL van de pagina voor het bijwerken van factuurgegevens voor een abonnement en voor 3DS-authenticatie.', - 'The address provided is outside the store’s market.' => 'Het opgegeven adres is buiten het marktgebied van de winkel.', - 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'Het kortingbedrag dat wordt toegepast op de hele bestelling. Dit bedrag is verdeeld over de lijnitems van de hoogste tot de laagste prijs, totdat de korting is opgebruikt.', - 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'De basiskorting kan maar tot nul korting toepassen voor items in de winkelwagen en kan de bestelling niet negatief maken.', - 'The cart recovery link is invalid. Please request a new one.' => 'De link voor het herstellen van de winkelwagen is ongeldig. Vraag een nieuwe aan.', - 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Het conversietarief dat wordt gebruikt bij het omrekenen van een bedrag naar deze valuta. Als een item bijvoorbeeld {amount1} kost, dan is dit met een conversietarief van {rate} een bedrag van {amount2} in de andere valuta.', - 'The countries that orders are allowed to be placed from.' => 'De landen van waaruit bestellingen mogen worden geplaatst.', - 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'De gebruikslimiet van {limit} van de coupon "{code}" is overschreden.', - 'The customer for this order has been deleted.' => 'De klant voor deze bestelling is verwijderd.', - 'The default shipping category is automatically available to all product types.' => 'De standaardverzendcategorie is automatisch beschikbaar voor alle producttypes.', - 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'De gebruikslimiet van {limit} van de korting "{name}" is overschreden.', - 'The download link has expired. Please request a new one.' => 'De downloadlink is verlopen. Vraag een nieuwe aan.', - 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'Het e-mailadres waarvan bestelling status e-mails worden verzonden. Laat leeg om het Systeem E-mailadres vastgesteld in de algemene instellingen van Craft te gebruiken.', - 'The entry that contains the description for this subscription’s plan.' => 'Het item met een beschrijving voor dit abonnementsplan.', - 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'De vaste waarde van de korting voor elk item. Bijv. “3” voor $ 3 korting op elk item.', - 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'De gebruikte notatie om nieuwe coupons te genereren, bijvoorbeeld {example}. \'#\'-tekens worden vervangen door een willekeurige letter.', - 'The from and to inventory locations must be different.' => 'De voorraadlocatie van en naar moeten verschillend zijn.', - 'The inventory locations this store uses.' => 'De inventarisatielocaties die deze winkel gebruikt.', - 'The item is not enabled for sale.' => 'Het artikel is niet ingeschakeld voor verkoop.', - 'The language the order was made in.' => 'De taal waarin de bestelling is geplaatst.', - 'The language to be used when this email is rendered.' => 'De taal die moet worden gebruikt bij het renderen van deze e-mail.', - 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Het maximum aantal niveau\'s dat dit producttype kan hebben. Laat leeg als dit niet uitmaakt.', - 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Het maximumbedrag dat de klant zou moeten uitgeven aan verzending. Stel in op nul om uit te schakelen.', - 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Het minimumbedrag dat de klant zou moeten uitgeven aan verzending. Stel in op nul om uit te schakelen.', - 'The order is not valid.' => 'De bestelling is ongeldig.', - 'The payment gateway that will be used for the subscription plan.' => 'De betaalgateway die voor dit abonnementsplan wordt gebruikt.', - 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'De percentielwaarde van de korting voor elk item. Bijv. {ex1} voor {ex2} korting. Percentages worden afgerond tot op 2 decimalen.', - 'The previously-selected shipping method is no longer available.' => 'De eerder geselecteerde verzendmethode is niet meer beschikbaar.', - 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'De prijs van {description} is verhoogd van {originalSalePriceAsCurrency} naar {newSalePriceAsCurrency}', - 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'De prijs van {description} is gedaald van {originalSalePriceAsCurrency} naar {newSalePriceAsCurrency}', - 'The primary currency cannot be changed after orders are placed.' => 'De primaire valuta kan niet worden gewijzigd na het plaatsen van bestellingen.', - 'The purchasable defines the relationship' => 'Het koopbare artikel bepaalt de relatie', - 'The purchasable is related by another element' => 'Het koopbare artikel is gerelateerd aan een ander element', - 'The recipient of the email. Twig code can be used here.' => 'De ontvanger van de e-mail. Hier kan Twig-code worden gebruikt.', - 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'Het e-mailadres om te antwoorden. Laat dit leeg om het gewone e-mailadres van de afzender te gebruiken. Hier kunt u Twig-code gebruiken.', - 'The site the order was made in.' => 'De site waarop de bestelling is geplaatst.', - 'The site to be used when this email is rendered.' => 'De site die moet worden gebruikt bij het renderen van deze e-mail.', - 'The subject line of the email. Twig code can be used here.' => 'Het onderwerp van de e-mail. Hier kunt u Twig-code gebruiken.', - 'The template that the PDF should be generated from.' => 'De sjabloon op basis waarvan de pdf wordt gemaakt.', - 'The template to be used for HTML emails.' => 'De template die gebruikt moet worden voor HTML e-mail.', - 'The template to be used for plain text emails. Twig code can be used here.' => 'De sjabloon voor e-mails met platte tekst. Hier kunt u Twig-code gebruiken.', - 'The template to use when a product’s URL is requested.' => 'Het sjabloon om te gebruiken wanneer een URL van een product wordt opgevraagd.', - 'The total number of order adjustments changed.' => 'Het totale aantal bestellingsaanpassingen is veranderd.', - 'The total price of the order changed.' => 'De totale prijs van de bestelling is veranderd.', - 'The total quantity of items within the order changed.' => 'De totale hoeveelheid artikelen binnen de bestelling is veranderd.', - 'The unique SKU of the donation purchasable.' => 'De unieke SKU van de te kopen donatie.', - 'The unit of measurement that should be used when specifying product dimensions.' => 'De meet eenheid die gebruikt moet worden wanneer dimensies van een product worden gespecificeerd.', - 'The unit of measurement that should be used when specifying product weights.' => 'De meet eenheid die gebruikt moet worden wanneer gewichten van een product worden gespecificeerd.', - 'The webhook URL for this gateway.' => 'De webhook-URL voor deze gateway.', - 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'De “Van” naam die gebruikt zal worden bij het verzenden van bestelling status e-mails. Laat leeg om de Verzender Naam vastgesteld in de algemene instellingen van Craft te gebruiken.', - 'There are errors on the order' => 'De bestelling bevat fouten', - 'There are only {num} “{description}” items left in stock.' => 'Er zijn nog maar {num} “{description}”-artikelen op voorraad.', - 'There aren’t any product types to select yet.' => 'Er zijn nog geen producttypen om te selecteren.', - 'There is no gateway or payment source available for use with this order.' => 'Er is geen gateway of betalingsbron beschikbaar om te gebruiken bij deze bestelling.', - 'There is no gateway selected that supports payment sources.' => 'Er is geen gateway geselecteerd die betaalbronnen ondersteunt.', - 'There is no shipping method selected for this order.' => 'Voor deze bestelling is geen verzendmethode geselecteerd.', - 'This URL will load the cart into the user’s session, making it the active cart.' => 'Deze URL zal de winkelwagen laden in de sessie van de gebruiker en er de actieve winkelwagen van maken.', - 'This action is not allowed for the current user.' => 'Deze is actie is niet toegestaan voor de huidige gebruiker.', - 'This category will be used as the default for all purchasables in this store.' => 'Deze categorie wordt gebruikt als standaardcategorie voor alle koopbare artikelen in deze winkel.', - 'This coupon is for registered users and limited to {limit} uses.' => 'Deze coupon is voor geregistreerde gebruikers en maximaal {limit} keer bruikbaar.', - 'This coupon is limited to {limit} uses.' => 'Deze coupon mag {limit} keer gebruikt worden.', - 'This coupon requires an email address.' => 'Deze coupon vereist een e-mailadres.', - 'This gateway does not support that functionality.' => 'Deze gateway ondersteunt deze functionaliteit niet.', - 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Dit wordt overschreven door de configuratie-instelling {setting} in `config/{file}.php`.', - 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Dit is het adres waar uw winkel is gevestigd. Dit adres kan door verschillende invoegtoepassingen worden gebruikt, onder andere voor verzending en belastingen. Het kan ook in PDF-bonnen worden gebruikt.', - 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Dit is de standaard pdf die wordt weergegeven bij het opvragen van de bestel-pdf.', - 'This is the last location for the {store} store.' => 'Dit is de laatste locatie voor de winkel {store}.', - 'This month' => 'Deze maand', - 'This order has unsaved changes.' => 'Deze bestelling heeft niet-opgeslagen wijzigingen.', - 'This week' => 'Deze week', - 'This year' => 'Dit jaar', - 'Times Used' => 'Aantal keren gebruikt', - 'Title' => 'Titel', - 'To' => 'Aan', - 'Today' => 'Vandaag', - 'Too many variants for this product.' => 'Te veel varianten voor dit product.', - 'Top Customers by Average Order' => 'Topklanten per gemiddelde bestelling', - 'Top Customers by Total Revenue' => 'Topklanten volgens totale omzet', - 'Top Customers' => 'Topklanten', - 'Top Product Types by Qty Sold' => 'Topproducttypen volgens verkochte hoeveelheid', - 'Top Product Types by Revenue' => 'Top-producttypen volgens omzet', - 'Top Product Types' => 'Top-producttypen', - 'Top Products by Qty Sold' => 'Topproducten volgens verkochte hoeveelheid', - 'Top Products by Revenue' => 'Topproducten volgens omzet', - 'Top Products' => 'Topproducten', - 'Top Purchasables by Qty Sold' => 'Top koopbare artikelen volgens verkochte hoeveelheid', - 'Top Purchasables by Revenue' => 'Top koopbare artikelen volgens omzet', - 'Top Purchasables' => 'Top van de koopbare artikelen', - 'Total ' => 'Totaal ', - 'Total Discount Use Limit' => 'Totale gebruikslimiet korting', - 'Total Discount' => 'Totale korting', - 'Total Included Tax' => 'Totaal inclusief belasting', - 'Total Orders by Billing Country' => 'Totaal aantal bestellingen per land van facturering', - 'Total Orders by Country' => 'Totaal aantal bestellingen per land', - 'Total Orders by Shipping Country' => 'Totaal aantal bestellingen per land van verzending', - 'Total Orders' => 'Totaal aantal bestellingen', - 'Total Paid' => 'Totaal betaald', - 'Total Price' => 'Totale prijs', - 'Total Qty' => 'Totaal aantal', - 'Total Revenue' => 'Totale omzet', - 'Total Shipping' => 'Totale verzendkosten', - 'Total Tax' => 'Totaal belasting', - 'Total Weight' => 'Totaal gewicht', - 'Total' => 'Totaal', - 'Track Inventory' => 'Voorraad bijhouden', - 'Transaction Hash' => 'Transactiehash', - 'Transaction ID' => 'Transactie-ID', - 'Transaction captured successfully: {message}' => 'Transactie succesvol vastgelegd: {message}', - 'Transaction refunded successfully: {message}' => 'Transactie succesvol terugbetaald: {message}', - 'Transactions' => 'Transacties', - 'Transfer Fields' => 'Overdrachtvelden', - 'Transfer Items' => 'Overdrachtsitems', - 'Transfer Settings' => 'Overdrachtsinstellingen', - 'Transfer Status' => 'Overdrachtsstatus', - 'Transfer fields saved.' => 'Overdrachtsvelden opgeslagen.', - 'Transfer must have at least one item.' => 'Overdracht moet minimaal één item bevatten.', - 'Transfer' => 'Overdracht', - 'Transfers' => 'Overdrachten', - 'Trial days credited' => 'Dagen voor proefversie gecrediteerd', - 'Trial expiration' => 'Proefperiode verlopen', - 'Trial expiry date' => 'Vervaldatum proefperiode', - 'Type not in allowed options.' => 'Type maakt geen deel uit van de toegestane opties.', - 'Type' => 'Type', - 'URI' => 'URI', - 'Unable to cancel subscription at this time.' => 'Annulering van het abonnement is momenteel niet mogelijk.', - 'Unable to complete order: another request is already in progress.' => 'Kan bestelling niet voltooien: er loopt al een andere aanvraag.', - 'Unable to find variant.' => 'Kan variant niet vinden.', - 'Unable to generate coupon codes: {message}' => 'Kan couponcodes niet genereren: {message}', - 'Unable to make payment at this time.' => 'Betalen is momenteel niet mogelijk.', - 'Unable to modify subscription at this time.' => 'Wijzigen van het abonnement is momenteel niet mogelijk.', - 'Unable to reactivate subscription at this time.' => 'Opnieuw activeren van het abonnement is momenteel niet mogelijk.', - 'Unable to reassign orders.' => 'Kan bestellingen niet opnieuw toewijzen.', - 'Unable to remove order data.' => 'Kan bestelgegevens niet verwijderen.', - 'Unable to retrieve Sale and Purchasable.' => 'Aanbieding en koopbaar artikel ophalen niet mogelijk.', - 'Unable to retrieve cart.' => 'Winkelwagen ophalen is niet mogelijk.', - 'Unable to retrieve customer.' => 'Klant ophalen niet mogelijk.', - 'Unable to retrieve load cart URL' => 'Kan URL om de winkelwagen te laden niet ophalen', - 'Unable to retrieve payment source.' => 'Kan betaalbron niet ophalen.', - 'Unable to set default shipping category.' => 'Kan de standaard verzendcategorie niet instellen.', - 'Unable to set default tax category.' => 'Kan de standaard belastingcategorie niet instellen.', - 'Unable to set primary payment source.' => 'Kan primaire betaalbron niet instellen.', - 'Unable to start the subscription. Please check your payment details.' => 'Starten van het abonnement is niet mogelijk. Controleer uw betalingsgegevens.', - 'Unable to subscribe at this time.' => 'Abonneren is op dit moment niet mogelijk.', - 'Unable to update cart.' => 'Winkelwagen bijwerken is niet mogelijk.', - 'Unable to validate address.' => 'Kan adres niet valideren.', - 'Unit Price' => 'Eenheidsprijs', - 'Unit price (minus discounts)' => 'Eenheidsprijs (min kortingen)', - 'Units' => 'Eenheden', - 'Unpaid' => 'Niet-betaald', - 'Unsubscribe' => 'Afmelden', - 'Update Address' => 'Adres bijwerken', - 'Update Order Status' => 'Orderstatus bijwerken', - 'Update Order Status…' => 'Bestellingsstatus bijwerken ...', - 'Update order' => 'Bestelling bijwerken', - 'Update subscription' => 'Abonnement bijwerken', - 'Update' => 'Updaten', - 'Updated By' => 'Bijgewerkt door', - 'Updated committed stock successfully.' => 'Toegezegde voorraad bijgewerkt.', - 'Updated' => 'Bijgewerkt', - 'Use Billing Address For Tax' => 'Factuuradres gebruiken voor belasting', - 'Use as the primary billing address' => 'Gebruiken als primair factuuradres', - 'Use as the primary shipping address' => 'Gebruiken als primair verzendadres', - 'Used By Tax Rates' => 'Gebruikt door belastingtarieven', - 'Used by Tax Rates' => 'Gebruikt door belastingtarieven', - 'User Groups' => 'Gebruikersgroepen', - 'User not found.' => 'Gebruiker niet gevonden.', - 'User' => 'Gebruiker', - 'Uses' => 'Aantal gebruiken', - 'Validate Business Tax ID as Vat ID' => 'Fiscaal ondernemingsnummer valideren als btw-nummer', - 'Validating condition syntax' => 'Validatie voorwaardesyntaxis', - 'Validating formula syntax' => 'Validatie formulesyntaxis', - 'Variant Fields' => 'Variantvelden', - 'Variant Has Untracked Stock' => 'Variant heeft niet-bijgehouden voorraad', - 'Variant Price' => 'Variantprijs', - 'Variant SKU' => 'Variant-SKU', - 'Variant Search' => 'Variant zoeken', - 'Variant Stock' => 'Variantvoorraad', - 'Variant Title Format' => 'Variant titel formaat', - 'Variant Tracks Stock' => 'Voorraad van variant wordt bijgehouden', - 'Variant UI Label Format' => 'Variant UI-labelformaat', - 'Variant has no product.' => 'Variant heeft geen product.', - 'Variants not restored.' => 'Varianten niet hersteld.', - 'Variants restored.' => 'Varianten hersteld.', - 'Variants' => 'Varianten', - 'View customer' => 'Klant bekijken', - 'View order' => 'Bestelling bekijken', - 'View product type - {productType}' => 'Producttype bekijken - {productType}', - 'View user' => 'Gebruiker bekijken', - 'View' => 'Weergeven', - 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Waarschuwing: door deze valuta te verwijderen worden alle betalingen en terugbetalingen in deze valuta gestopt. Weet u zeker dat u “{name}” wilt verwijderen?', - 'Web' => 'Web', - 'Webhook URL' => 'Webhook-URL', - 'Weight ({unit})' => 'Gewicht ({unit})', - 'Weight Rate' => 'Gewicht percentage', - 'Weight Unit' => 'Eenheid gewicht', - 'Weight' => 'Gewicht', - 'What product URIs should look like for the site.' => 'Hoe URI\'s van producten eruit moeten zien voor de website.', - 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Hoe de automatisch gegenereerde producttitels eruit moeten zien. U kunt tags bijvoegen die producteigenschappen uitvoeren, zoals {ex1} of {ex2}. Alle gebruikte aangepaste velden moeten worden ingesteld als verplicht.', - 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Hoe de automatisch gegenereerde variant titels eruit moeten zien. U kunt tags bijvoegen die eigenschappen van de variant uitzetten, zoals {ex1} of {ex2}. Alle aangepaste velden die gebruikt worden moeten worden ingesteld als vereist.', - 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Hoe de pdf-bestandsnaam van de bestelling eruit moet zien (zonder extensie). U kunt tags toevoegen die eigenschappen van de bestelling weergeven, zoals {ex1} of {ex2}.', - 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Hoe de unieke automatisch gegenereerde SKU\'s eruit moeten zien, wanneer een SKU veld wordt ingevoerd zonder waarde. U kunt tags bijvoegen die eigenschappen uitzetten, zoals {ex1} of {ex2}.', - 'What this PDF will be called in the control panel.' => 'De naam van deze pdf in het configuratiescherm.', - 'What this catalog pricing rule will be called in the control panel.' => 'De naam van deze catalogusprijsregel in het configuratiescherm.', - 'What this discount will be called in the control panel.' => 'Naam van deze korting in het configuratiescherm.', - 'What this email will be called in the control panel.' => 'De naam van dit e-mailbericht in het configuratiescherm.', - 'What this product type will be called in the control panel.' => 'Naam van dit producttype in het configuratiescherm.', - 'What this sale will be called in the control panel.' => 'De naam van deze verkoop in het configuratiescherm.', - 'What this shipping category will be called in the control panel.' => 'De naam van deze verzendcategorie in het configuratiescherm.', - 'What this shipping rule will be called in the control panel.' => 'De naam van deze verzendingsregel in het configuratiescherm.', - 'What this shipping zone will be called in the control panel.' => 'De naam van deze verzendingszone in het configuratiescherm.', - 'What this status will be called in the control panel.' => 'Naam van deze status in het configuratiescherm.', - 'What this subscription plan will be called in the control panel.' => 'De naam van dit abonnement in het configuratiescherm.', - 'What this tax category will be called in the control panel.' => 'De naam van deze belastingcategorie in het configuratiescherm.', - 'What this tax zone will be called in the control panel.' => 'De naam van deze belastingzone in het configuratiescherm.', - 'When this discount is applied to an order, which line items should be discounted?' => 'Wanneer deze korting wordt toegepast op een bestelling, op welke artikelen moet deze dan worden toegepast?', - 'Whether the first available shipping method option should be set automatically on carts.' => 'Of de eerste beschikbare verzendmethode automatisch moet worden ingesteld voor winkelwagens.', - 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Of de primaire betaalbron van de gebruiker automatisch moet worden ingesteld voor nieuwe winkelwagens.', - 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Of het primaire verzend- en factuuradres automatisch moeten worden ingesteld voor nieuwe winkelwagens.', - 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Aanduiding of deze catalogusprijsregel beschikbaar moet zijn voor gebruik, ongeacht andere voorwaarden.', - 'Whether this sale should be available for use, regardless of other conditions.' => 'Aanduiding of deze verkoop beschikbaar moet zijn voor gebruik, ongeacht andere voorwaarden.', - 'Which data to display in the name column in the results table.' => 'Welke gegevens worden weergegeven in de naamkolom van de resultatentabel.', - 'Which product types should this category be available to?' => 'Voor welke producttypen moet deze categorie beschikbaar zijn?', - 'Which template should be loaded when a product’s URL is requested.' => 'De sjabloon die moet worden geladen bij het opvragen van de URL van een product.', - 'Width ({unit})' => 'Breedte ({unit})', - 'Width' => 'Breedte', - 'YYYY' => 'YYYY', - 'Yes' => 'Ja', - 'You are not allowed to add a line item.' => 'Het is niet toegestaan om een lijnitem toe te voegen.', - 'You currently have no emails configured to select for this status.' => 'U heeft momenteel geen e-mails geconfigureerd die u voor deze status kunt selecteren.', - 'You do not have permission to load this cart.' => 'U heeft geen rechten om deze winkelwagen te laden.', - 'You must set up at least one gateway that supports subscriptions first.' => 'U moet eerst minimaal één gateway instellen die abonnementen ondersteunt.', - 'You must be logged in or provide a valid token to load this cart.' => 'U moet ingelogd zijn of een geldig token verstrekken om deze winkelwagen te laden.', - 'You must be signed in to create a payment source.' => 'U moet zijn aangemeld om een betaalbron aan te maken.', - 'You must be signed in to set a primary payment source.' => 'U moet zijn aangemeld om een primaire betaalbron in te stellen.', - 'You must make a payment to complete the order.' => 'U moet een betaling verrichten om de bestelling te voltooien.', - 'Your Cart Recovery Link' => 'Uw link voor het herstellen van de winkelwagen', - 'Your Order PDF Download Link' => 'De pdf-downloadlink van uw bestelling', - 'Your order is empty' => 'Uw bestelling is leeg', - 'ZIP file' => 'ZIP-bestand', - 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Nul - de minimumprijs is nul als kortingen hoger zijn dan de bestelwaarde.', - 'Zip Code' => 'Postcode', - 'all' => 'alle', - 'any' => 'elke', - 'average order total' => 'gemiddelde totaalprijs bestellingen', - 'billing address' => 'factuuradres', - 'donation' => 'donatie', - 'donations' => 'donaties', - 'info' => 'info', - 'inventory location' => 'voorraadlocatie', - 'new customers' => 'nieuwe klanten', - 'on hand' => 'aanwezig', - 'only' => 'enkel', - 'order' => 'bestelling', - 'orders' => 'bestellingen', - 'price' => 'prijs', - 'prices' => 'prijzen', - 'product variant' => 'productvariant', - 'product variants' => 'productvarianten', - 'product' => 'product', - 'products' => 'producten', - 'repeat customers' => 'terugkerende klanten', - 'shipping address' => 'verzendadres', - 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'shippingSameAsBilling en billingSameAsShipping kunnen niet beide worden ingesteld.', - 'subscription' => 'abonnement', - 'subscriptions' => 'abonnementen', - 'to' => 'aan', - 'transfer' => 'overdracht', - 'transfers' => 'overdrachten', - '{amount} included' => '{amount} inbegrepen', - '{count} Unfulfilled Orders' => '{count} onvervulde bestellingen', - '{description} is no longer available.' => '{description} is niet meer beschikbaar.', - '{description} only has {stock} in stock.' => '{description} heeft maar {stock} op voorraad.', - '{from} to {to}' => '{from} naar {to}', - '{name} (Primary)' => '{name} (primair)', - '{name} (Trashed)' => '{name} (verwijderd)', - '{name} catalog price' => 'Catalogusprijs {name}', - '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, =1{bestelling} other{bestellingen}} bijgewerkt.', - '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, one {}=1{bestelling is} other{bestellingen zijn}} geassocieerd met de {numUsers, plural, one {}=1{gebruiker} other{gebruikers}}.', - '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, one {}=1{abonnement is} other{abonnementen zijn}} geactiveerd voor de {numUsers, plural, one {}=1{gebruiker} other{gebruikers}}.', - '{number} more…' => '{number} extra...', - '{pct} off the discounted item price' => '{pct} korting op de itemprijs met korting', - '{pct} off the original item price' => '{pct} korting op de oorspronkelijke itemprijs', - '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, one {}=1{is} other{zijn}} niet toegevoegd aan een site.', - '{total} in total revenue' => '{total} aan totale omzet', - '{total} orders' => '{total} bestellingen', - '{total} saleable across {locationCount} location(s)' => '{total} verkoopbaar op {locationCount} locatie(s)', - '{uses} uses across {emails} email addresses' => '{uses} gebruiksinstanties voor {emails} e-mailadressen', - '{uses} uses across {users} users' => '{uses} gebruiken voor {users} gebruikers', - '“{description}” is currently out of stock.' => '“{description}” is momenteel niet voorradig.', - '“{key}” has invalid JSON' => '“{key}” bevat ongeldige JSON', -]; diff --git a/src/translations/pt/commerce.php b/src/translations/pt/commerce.php deleted file mode 100644 index fe7b2f0990..0000000000 --- a/src/translations/pt/commerce.php +++ /dev/null @@ -1,1428 +0,0 @@ - '(novo preço)', - '(of original price)' => '(do preço original)', - '(off original price)' => '(de desconto no preço original)', - 'A cart number must be specified.' => 'Deve ser especificado um número de carrinho.', - 'A cart recovery link has been sent to {email}.' => 'Foi enviado um link para recuperar o carrinho para {email}.', - 'A cart recovery link will be sent to {email}.' => 'Será enviado um link para recuperar o carrinho para {email}.', - 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Um número de referência amigável será gerado com base neste formato quando o carrinho for convertido num pedido. Por exemplo {ex1}, ou
{ex2}. O resultado deste formato deve ser único.', - 'A new download link has been sent to {email}' => 'Um novo link para download foi enviado para {email}', - 'A new download link will be sent to {email}' => 'Um novo link para download será enviado para {email}', - 'A valid email is required to create a customer.' => 'É necessário um email válido para criar um cliente.', - 'Accept' => 'Aceitar', - 'Accepted' => 'Aceite', - 'Actions' => 'Ações', - 'Active Carts' => 'Carrinhos Ativos', - 'Active subscriptions' => 'Subscrições ativas', - 'Active' => 'Ativo', - 'Add Address' => 'Adicionar endereço', - 'Add a coupon' => 'Adicionar um cupão', - 'Add a custom line item' => 'Adicionar um item de linha personalizado', - 'Add a line item' => 'Adicionar um item de linha', - 'Add a product' => 'Adicionar um produto', - 'Add a variant' => 'Adicionar um variante', - 'Add an adjustment' => 'Adicionar um ajuste', - 'Add an item' => 'Adicionar um item', - 'Add an option' => 'Adicionar uma opção', - 'Add catalog price' => 'Adicionar preço de catálogo', - 'Add' => 'Adicionar', - 'Additional Actions' => 'Ações adicionais', - 'Additional recipients that should receive this email. Twig code can be used here.' => 'Destinatários adicionais que devem receber este email. O código Twig pode ser usado aqui.', - 'Address 1' => 'Endereço 1', - 'Address 2' => 'Endereço 2', - 'Address 3' => 'Endereço 3', - 'Address Line 1' => 'Linha de Endereço 1', - 'Address Line 2' => 'Endereço linha 2', - 'Address Updated.' => 'Endereço atualizado.', - 'Address copied to user.' => 'Endereço copiado para o utilizador.', - 'Address not found.' => 'Endereço não encontrado.', - 'Adjust Quantity' => 'Ajustar a quantidade', - 'Adjust by' => 'Ajustar por', - 'Adjust price when included rate is disqualified?' => 'Ajustar o preço quando a tarifa incluída for desqualificada?', - 'Adjustments' => 'Ajustes', - 'Admin Notices' => 'Avisos da administração', - 'Administrative Area Code of Origin' => 'Código da área administrativa de origem', - 'Advanced' => 'Avançado', - 'All Orders' => 'Todos os Pedidos', - 'All Totals' => 'Todos os totais', - 'All Transfers' => 'Todas as transferências', - 'All active subscriptions' => 'Todas as subscrições ativas', - 'All customers' => 'Todos os clientes', - 'All products' => 'Todos os produtos', - 'All variants must have a SKU.' => 'Todas as variantes devem ter um SKU.', - 'All' => 'Tudo', - 'Allow Checkout Without Payment' => 'Permitir checkout sem pagamento', - 'Allow Empty Cart On Checkout' => 'Permitir carrinho vazio no checkout', - 'Allow Partial Payment On Checkout' => 'Permitir pagamento parcial no checkout', - 'Allow out of stock purchases' => 'Permitir compras fora de stock', - 'Allow' => 'Permitir', - 'Allowed Qty' => 'Qntd Permitida', - 'Alternative Phone' => 'Telefone alternativo', - 'Amount' => 'Valor', - 'An ID must be provided' => 'Deve ser fornecida uma identificação', - 'An error occurred while generating this PDF.' => 'Ocorreu um erro ao gerar este PDF.', - 'Any' => 'Qualquer', - 'Anywhere' => 'Qualquer sítio', - 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Tem a certeza de que pretende arquivar o plano de subscrição “{name}”? NÃO IRÁ cancelar as subscrições existentes.', - 'Are you sure you want to capture this transaction?' => 'Tem certeza que deseja capturar essa transação?', - 'Are you sure you want to complete this order?' => 'Tem certeza de que deseja concluir este pedido?', - 'Are you sure you want to delete the selected orders?' => 'Tem a certeza que quer eliminar os pedidos selecionados?', - 'Are you sure you want to delete the selected product and its variants?' => 'Tem a certeza que quer eliminar o produto selecionado e seus variantes?', - 'Are you sure you want to delete this shipping rule?' => 'Tem a certeza de que pretende eliminar esta regra de transporte?', - 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Tem certeza que você quer deletar “{name}” e todos os seus produtos? Por favor, faça um backup do seu banco de dados antes de realizar essa ação destrutiva.', - 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Tem a certeza que quer eliminar "{name}"? Isto definirá todos os itens de linha com este estado para sem estado.', - 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Tem a certeza de que deseja marcar esta transferência como pendente? Isto será mostrado como a entrar no destino.', - 'Are you sure you want to overwrite the billing address?' => 'Tem a certeza que deseja substituir este endereço de faturação?', - 'Are you sure you want to overwrite the shipping address?' => 'Tem a certeza que deseja substituir este endereço de envio?', - 'Are you sure you want to permanently delete this store and everything in it?' => 'Tem a certeza de que quer eliminar permanentemente esta loja e tudo o que nela se encontra?', - 'Are you sure you want to refund this transaction?' => 'Tem certeza que você deseja estornar essa transação?', - 'Are you sure you want to remove this customer?' => 'Tem a certeza que deseja eliminar este cliente?', - 'Are you sure you want to save this as a new shipping rule?' => 'Tem a certeza de que pretende guardar esta nova regra de transporte?', - 'Are you sure you want to send email: {name}?' => 'Tem a certeza de que deseja enviar o email: {name}?', - 'At least one site must be enabled for the product type.' => 'Pelo menos um site deve estar ativo para o tipo de produto.', - 'Attempted Payments' => 'Tentativas de pagamento efetuadas', - 'Attention' => 'Atenção', - 'Authorize Only (Manually Capture)' => 'Apenas Autorizar (Capturar Manualmente)', - 'Auto Set Cart Shipping Method Option' => 'Opção de método de envio do carrinho de compras definido automaticamente', - 'Auto Set New Cart Addresses' => 'Definir automaticamente novos endereços de carrinho de compras', - 'Auto Set Payment Source' => 'Definição automática da fonte de pagamento', - 'Automatic SKU Format' => 'Formato SKU Automático', - 'Available Shipping Categories' => 'Categorias de Envio Disponíveis', - 'Available Tax Categories' => 'Categorias de Tributos Disponíveis', - 'Available for purchase' => 'Disponível para compra', - 'Available for purchase?' => 'Disponível para compra?', - 'Available inventory for "{description}" has gone below zero.' => 'O stock disponível de "{description}" ficou abaixo de zero.', - 'Available to Product Types' => 'Disponível para Tipos de Produtos', - 'Available' => 'Disponível', - 'Available?' => 'Disponível?', - 'Average Order Total' => 'Total médio de pedidos', - 'Average' => 'Média', - 'BCC’d Recipient' => 'Cópia oculta para', - 'Bad Request' => 'Pedido inválido', - 'Bad address ID.' => 'ID do endereço inválida.', - 'Bad order ID.' => 'ID de pedido inválido.', - 'Base Price' => 'Preço de base', - 'Base Promotional Price' => 'Preço promocional de base', - 'Base Rate' => 'Custo Base', - 'Base' => 'Base', - 'Bcc' => 'Cco', - 'Billing Address' => 'Endereço de Cobrança', - 'Billing Business Name' => 'Nome da Empresa para Cobrança', - 'Billing First Name' => 'Primeiro nome de Cobrança', - 'Billing Full Name' => 'Nome Completo de Cobrança', - 'Billing Last Name' => 'Último nome de Cobrança', - 'Billing address required.' => 'É necessário o endereço de faturação.', - 'Billing detail update URL' => 'URL de atualização de detalhes de faturação', - 'Billing issues' => 'Questões de faturação', - 'Billing' => 'Faturação', - 'Both (Line item price + Line item shipping costs)' => 'Ambos (preço do item individual + custos de envio do item individual)', - 'Business ID' => 'CNPJ', - 'Business Name' => 'Nome da Empresa', - 'Business Tax ID' => 'CNPJ da Empresa', - 'CC’d Recipient' => 'Destinatário de CC', - 'CVV' => 'CVV', - 'Can be used as an internal reference.' => 'Pode ser utilizado como referência de intervalo.', - 'Can not complete payment for missing transaction.' => 'Não é possível completar o pagamento para a transação não encontrada.', - 'Can not create a new order' => 'Não é possível criar um novo pedido', - 'Can not find an order to pay.' => 'Não foi possível encontrar um pedido por pagar.', - 'Can not find enabled email.' => 'Não foi possível encontrar o email ativo.', - 'Can not find order' => 'Não foi possível encontrar o pedido', - 'Can not find order.' => 'Não foi possível encontrar o pedido.', - 'Can not find the transaction to refund' => 'Não foi possível encontrar a transação a reembolsar', - 'Can not move between these inventory types.' => 'Não é possível mover entre estes tipos de inventário.', - 'Can not refund amount greater than the remaining amount' => 'Não é possível reembolsar uma quantia superior ao valor restante', - 'Cancel subscription' => 'Cancelar subscrição', - 'Cancel with gateway now' => 'Cancelar agora através do gateway', - 'Cancel' => 'Cancelar', - 'Cancellation date' => 'Data de cancelamento', - 'Cancellation' => 'Cancelamento', - 'Cannot switch plans for this subscription.' => 'Não foi possível mudar de plano para esta subscrição.', - 'Can’t preview this email.' => 'Não é possível pré-visualizar este email.', - 'Capture payment' => 'Capturar pagamento', - 'Capture' => 'Capturar', - 'Card Holder' => 'Titular do cartão', - 'Card Number' => 'Número do Cartão', - 'Card' => 'Cartão', - 'Cart Recovery Link' => 'Link para recuperar o carrinho', - 'Cart forgotten.' => 'Carrinho esquecido.', - 'Cart updated.' => 'Carrinho atualizado.', - 'Cart {number}' => 'Carrinho {number}', - 'Catalog Pricing Rule' => 'Regra de determinação de preços de catálogo', - 'Catalog pricing rule description.' => 'Descrição da regra de cálculo do preço de catálogo.', - 'Catalog pricing rule saved.' => 'Regra de determinação de preços de catálogo guardada.', - 'Catalog pricing rules deleted.' => 'Regras de fixação de preços de catálogo suprimidas.', - 'Catalog pricing rules updated.' => 'Regras de fixação de preços do catálogo atualizadas.', - 'Categories Relationship Type' => 'Tipo de relação das categorias', - 'Categories' => 'Categorias', - 'Category Rate Overrides' => 'Sobreposições à Categoria de Tributo', - 'Centimeters (cm)' => 'Centímetros (cm)', - 'Changing this value may affect your ability to refund existing transactions.' => 'Alterar este valor pode afetar a sua capacidade de reembolsar transações existentes.', - 'Choose a color to represent the order’s status' => 'Escolha uma cor para representar o estado do pedido', - 'Choose a new customer' => 'Escolha um novo cliente', - 'Choose adjustment values to include when calculating the product revenue total.' => 'Escolha os valores de ajuste a serem incluídos no cálculo da receita total do produto.', - 'Choose the currency’s ISO code.' => 'Escolha o código ISO da moeda.', - 'Choose the destination inventory location for the existing on hand stock.' => 'Selecione o local de destino do inventário para o stock disponível existente.', - 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Escolha em que sites este tipo de produto deverá estar disponível e configure as definições específicas do site.', - 'City' => 'Cidade', - 'Clear counter' => 'Limpar contador', - 'Clear notices' => 'Limpar os avisos', - 'Close' => 'Fechar', - 'Code' => 'Código', - 'Collated PDF' => 'PDF agrupado', - 'Color' => 'Cor', - 'Commerce Products' => 'Produtos do Commerce', - 'Commerce Settings' => 'Configurações do Commerce', - 'Commerce Variants' => 'Variantes comerciais', - 'Commerce email “{email}” could not be sent for order “{order}”.' => 'Não foi possível enviar o email comercial “{email}” para o pedido “{order}”.', - 'Commerce order exports' => 'Exportações de encomendas comerciais', - 'Commerce' => 'Commerce', - 'Committed' => 'Comprometido', - 'Completed Email' => 'E-mail completo', - 'Completed' => 'Concluído', - 'Completing order failed.' => 'Erro ao concluir o pedido.', - 'Condition' => 'Condição', - 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'As condições são comparadas com uma ordem antes de examinar as regras. É útil para qualificar antecipadamente a disponibilidade de um método ou se houver condições comuns a todas as regras para esse método.', - 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'As condições aqui são comparadas com o cliente do pedido antes de consultar as regras. Isso é útil se quiser qualificar a disponibilidade de um método antecipadamente, ou se houver condições comuns a todas as regras para esse método.', - 'Conditions' => 'Condições', - 'Contains Purchasables' => 'Contém artigos de compra', - 'Control Panel Settings' => 'Definições do painel de controlo', - 'Control panel' => 'Painel de controlo', - 'Conversion Rate' => 'Taxa de Conversão', - 'Converted Price' => 'Preço convertido', - 'Copied!' => 'Copiado!', - 'Copy the URL' => 'Copiar a URL', - 'Copy to {location}' => 'Copiar para {location}', - 'Copy' => 'Copiar', - 'Costs' => 'Custos', - 'Could not archive gateway.' => 'Não foi possível arquivar o gateway.', - 'Could not cancel “{reference}”.' => 'Não foi possível cancelar “{reference}”.', - 'Could not create the payment source.' => 'Não foi possível criar a fonte de pagamento.', - 'Could not delete shipping rule' => 'Não foi possível eliminar a regra de envio', - 'Could not delete shipping zone' => 'Não foi possível eliminar a zona de envio', - 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Não foi possível eliminar {count, number} {count, plural, one{categoria} other{categorias}} de envio.', - 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Não foi possível eliminar {count, number} {count, plural, one{método} other{métodos}} e regras de envio.', - 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Não foi possível eliminar {count, number} {count, plural, one{categoria} other{categorias}} de imposto.', - 'Could not find the email or template.' => 'Não foi possível encontrar o email ou o modelo.', - 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Não foi possível assinalar a encomenda {number} como concluída. Erro ao guardar a encomenda durante a conclusão da encomenda com erros: {order}', - 'Could not reactivate “{reference}”.' => 'Não foi possível reativar “{reference}”.', - 'Could not send email' => 'Não foi possível enviar o email', - 'Could not switch “{reference}” to “{plan}”.' => 'Não foi possível mudar “{reference}” para “{plan}”.', - 'Could not update orders address.' => 'Não foi possível atualizar o endereço dos pedidos.', - 'Couldn’t archive Line Item Status.' => 'Não foi possível arquivar o status do item de linha.', - 'Couldn’t archive Order Status.' => 'Não foi possível arquivar o estado do pedido.', - 'Couldn’t capture transaction.' => 'Não foi possível efetuar a transação.', - 'Couldn’t capture transaction: {message}' => 'Não foi possível efetuar a transação: {message}', - 'Couldn’t delete email.' => 'Não foi possível apagar o e-mail.', - 'Couldn’t delete the payment source.' => 'Não foi possível eliminar a fonte de pagamento.', - 'Couldn’t get order.' => 'Não foi possível obter o pedido.', - 'Couldn’t recalculate order.' => 'Não foi possível recalcular o pedido.', - 'Couldn’t refund transaction.' => 'Não foi possível reembolsar a transação.', - 'Couldn’t refund transaction: {message}' => 'Não foi possível reembolsar a transação: {message}', - 'Couldn’t reorder Line Item Statuses.' => 'Não foi possível reordenar os status de itens de linha.', - 'Couldn’t reorder Order Statuses.' => 'Não foi possível reordenar os Status de Pedido.', - 'Couldn’t reorder PDFs.' => 'Não foi possível reordenar os PDFs.', - 'Couldn’t reorder discounts.' => 'Não foi possível reordenar os descontos.', - 'Couldn’t reorder gateways.' => 'Não foi possível reordenar os gateways.', - 'Couldn’t reorder plans.' => 'Não foi possível pedir novamente os planos.', - 'Couldn’t reorder rules.' => 'Não foi possível reordenar as regras.', - 'Couldn’t reorder sale.' => 'Não foi possível voltar a encomendar a promoção.', - 'Couldn’t reorder sales.' => 'Não foi possível reordenar as ofertas.', - 'Couldn’t reorder statuses.' => 'Não foi possível reordenar os estados.', - 'Couldn’t reorder stores.' => 'Não foi possível voltar a pedir de lojas.', - 'Couldn’t save PDF.' => 'Não foi possível guardar o PDF.', - 'Couldn’t save catalog pricing rule.' => 'Não foi possível guardar a regra de fixação de preços do catálogo.', - 'Couldn’t save currency.' => 'Não foi possível guardar a moeda.', - 'Couldn’t save discount.' => 'Não foi possível guardar o desconto.', - 'Couldn’t save email.' => 'Não foi possível guardar o e-mail.', - 'Couldn’t save gateway.' => 'Não foi possível guardar o gateway.', - 'Couldn’t save inventory location.' => 'Não foi possível guardar a localização do inventário.', - 'Couldn’t save line item status.' => 'Não foi possível guardar o status do item de linha.', - 'Couldn’t save order fields.' => 'Não foi possível guardar os campos do pedido.', - 'Couldn’t save order status.' => 'Não foi possível guardar o status do pedido.', - 'Couldn’t save order.' => 'Não foi possível guardar o pedido.', - 'Couldn’t save product type.' => 'Não foi possível guardar o tipo de produto.', - 'Couldn’t save sale.' => 'Não foi possível guardar a oferta.', - 'Couldn’t save settings.' => 'Não foi possível guardar as definições.', - 'Couldn’t save shipping category.' => 'Não foi possível guardar a categoria de envio.', - 'Couldn’t save shipping method.' => 'Não foi possível guardar o método de envio.', - 'Couldn’t save shipping rule.' => 'Não foi possível guardar a regra de envio.', - 'Couldn’t save shipping zone.' => 'Não foi possível guardar a zona de envio.', - 'Couldn’t save store.' => 'Não foi possível guardar a loja.', - 'Couldn’t save subscription fields.' => 'Não foi possível guardar os campos de subscrição.', - 'Couldn’t save subscription plan.' => 'Não foi possível guardar o plano de subscrição.', - 'Couldn’t save subscription.' => 'Não foi possível guardar a subscrição.', - 'Couldn’t save tax category.' => 'Não foi possível guardar esta categoria de imposto.', - 'Couldn’t save tax rate.' => 'Não foi possível guardar a taxa de imposto.', - 'Couldn’t save tax zone.' => 'Não foi possível guardar a zona fiscal.', - 'Couldn’t save transfer fields.' => 'Não foi possível guardar os campos da transferência.', - 'Couldn’t update catalog pricing rule statuses.' => 'Não foi possível atualizar o estado das regras de fixação de preços do catálogo.', - 'Couldn’t update status.' => 'Não foi possível atualizar o estado.', - 'Couldn’t updated sales status.' => 'Não foi possível atualizar o status das ofertas.', - 'Country Code of Origin' => 'Código do país de origem', - 'Country List' => 'Lista de países', - 'Country not allowed.' => 'O país não é permitido.', - 'Country' => 'País', - 'Coupon Code' => 'Cupom de Desconto', - 'Coupon can not apply discount to this order due to address mismatch.' => 'O cupão não consegue aplicar desconto a este pedido devido a incompatibilidade de endereço.', - 'Coupon can not apply discount to this order due to customer mismatch.' => 'O cupão não consegue aplicar desconto a este pedido devido a incompatibilidade com o cliente.', - 'Coupon can not apply discount to this order.' => 'O cupão não consegue aplicar desconto a este pedido.', - 'Coupon code “{code}” is already in use by discount “{name}”.' => 'O código de cupão “{code}” já está a ser utilizado pelo desconto “{name}”.', - 'Coupon codes cannot be blank.' => 'Os códigos do cupão não podem ficar em branco.', - 'Coupon codes must be unique.' => 'Os códigos de cupão devem ser únicos.', - 'Coupon format is required and must contain at least one `#`.' => 'O formato do cupão é obrigatório e deve conter pelo menos um `#`.', - 'Coupon not valid.' => 'Cupão inválido.', - 'Coupon removed: {explanation}' => 'Cupão removido: {explanation}', - 'Coupons' => 'Cupões', - 'Craft Commerce - Administration' => 'Craft Commerce - Administração', - 'Craft Commerce - Inventory' => 'Craft Commerce - Inventário', - 'Craft Commerce - Orders' => 'Craft Commerce - Pedidos', - 'Craft Commerce - Product Type - {name}' => 'Craft Commerce - Tipo de produto - {name}', - 'Craft Commerce - Subscriptions' => 'Craft Commerce - Subscrições', - 'Create a Discount' => 'Criar um Desconto', - 'Create a Subscription Plan' => 'Criar um plano de subscrição', - 'Create a new PDF' => 'Criar um novo PDF', - 'Create a new catalog pricing rule' => 'Criar uma nova regra de determinação de preços do catálogo', - 'Create a new currency' => 'Criar nova moeda', - 'Create a new email' => 'Criar um novo e-mail', - 'Create a new gateway' => 'Criar um novo gateway', - 'Create a new line item status' => 'Criar um novo status de item de linha', - 'Create a new order status' => 'Criar um novo status de pedido', - 'Create a new product type' => 'Criar um novo tipo de produto', - 'Create a new sale' => 'Criar nova oferta', - 'Create a new shipping category' => 'Criar uma nova categoria de envio', - 'Create a new shipping method' => 'Criar um novo método de envio', - 'Create a new shipping rule' => 'Criar uma nova regra de envio', - 'Create a new tax category' => 'Criar uma nova categoria de tributos', - 'Create a new tax rate' => 'Criar uma nova taxa do imposto', - 'Create a product type' => 'Criar um tipo de produto', - 'Create a shipping zone' => 'Criar nova zona de envio', - 'Create a tax zone' => 'Criar uma zona fiscal', - 'Create catalog pricing rules' => 'Criar regras de determinação do preço do catálogo', - 'Create customer: “{email}”' => 'Criar cliente: "{email}"', - 'Create discounts' => 'Criar descontos', - 'Create discount…' => 'Criar desconto…', - 'Create rules that allow this discount to match the order.' => 'Criar regras que permitam que este desconto corresponda ao pedido.', - 'Create rules that allow this discount to match the order’s billing address.' => 'Criar regras que permitam que este desconto corresponda ao endereço de cobrança do pedido.', - 'Create rules that allow this discount to match the order’s customer.' => 'Criar regras que permitam que este desconto corresponda ao pedido do cliente.', - 'Create rules that allow this discount to match the order’s shipping address.' => 'Criar regras que permitam que este desconto corresponda ao endereço de envio do pedido.', - 'Create rules that allow this gateway to match the billing address.' => 'Crie regras que permitam que este gateway corresponda ao endereço de faturação.', - 'Create rules that allow this gateway to match the order.' => 'Crie regras que permitam que este gateway corresponda ao pedido.', - 'Create rules that allow this gateway to match the shipping address.' => 'Crie regras que permitam que este gateway corresponda à morada de entrega.', - 'Create sales' => 'Criar ofertas', - 'Create sale…' => 'Criar oferta…', - 'Created' => 'Criados', - 'Credit Card Payment Type' => 'Tipo de Modalidade do Cartão de Crédito', - 'Currency Code' => 'Código da Moeda', - 'Currency saved.' => 'Moeda guardada.', - 'Currency' => 'Moeda', - 'Current' => 'Atual', - 'Custom 1' => 'Personalizado 1', - 'Custom 2' => 'Personalizado 2', - 'Custom 3' => 'Personalizado 3', - 'Custom 4' => 'Personalizado 4', - 'Custom' => 'Personalizado', - 'Customer Enabled?' => 'Habilitado para Clientes?', - 'Customer ID is required.' => 'A ID do cliente é obrigatória.', - 'Customer Note' => 'Nota de cliente', - 'Customer Notices' => 'Avisos do cliente', - 'Customer data' => 'Dados dos clientes', - 'Customer' => 'Cliente', - 'Damaged' => 'Danificado', - 'Data shown might be outdated.' => 'Os dados apresentados podem estar desatualizados.', - 'Date Authorized' => 'Data da autorização', - 'Date Created' => 'Data de Criação', - 'Date First Paid' => 'Data do primeiro pagamento', - 'Date Ordered' => 'Data do Pedido', - 'Date Paid' => 'Data de Pagamento', - 'Date Updated' => 'Data Atualizada', - 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Data a partir da qual a regra de determinação do preço do catálogo estará ativa. Deixe em branco para uma data de início ilimitada', - 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Data a partir da qual o desconto estará ativo. Deixe em branco para data de início não limitada', - 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Data a partir da qual essa oferta estará ativa. Deixe em branco para data de início não limitada', - 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Data em que a regra de determinação do preço do catálogo será concluída. Deixe em branco para uma data final ilimitada', - 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Data na qual o desconto terminará. Deixe em branco para data final indefinida', - 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Data em que a oferta termina. Deixe em branco para prazo final indeterminado.', - 'Date' => 'Data', - 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Padrão - permite que o preço seja negativo se os descontos forem maiores do que o valor do pedido.', - 'Default Category' => 'Categoria padrão', - 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Visualização padrão do painel de controlo do Commerce. Se o utilizador não tiver permissão, voltará a um local a que possa aceder.', - 'Default Order PDF' => 'PDF de pedido padrão', - 'Default Per Item Rate' => 'Taxa padrão por item', - 'Default Percentage Rate' => 'Taxa percentual padrão', - 'Default Status?' => 'Status Padrão?', - 'Default View' => 'Visão padrão', - 'Default Weight Rate' => 'Taxa padrão de peso', - 'Default Zone' => 'Zona Padrão', - 'Default status?' => 'Estado padrão?', - 'Default to this tax zone when no billing address is set' => 'Predefinir esta zona de imposto quando não for definido um endereço de faturação', - 'Default to this tax zone when no shipping address is set' => 'Zona de tributos padrão para quando nenhum endereço de entrega esteja definido', - 'Default variant updated.' => 'Variante predefinida atualizada.', - 'Default' => 'Padrão', - 'Default?' => 'Padrão?', - 'Delete catalog pricing rules' => 'Eliminar regras de determinação de preços do catálogo', - 'Delete discounts' => 'Eliminar descontos', - 'Delete orders' => 'Eliminar pedidos', - 'Delete sales' => 'Eliminar ofertas', - 'Delete' => 'Deletar', - 'Deleting the {location} location.' => 'Eliminar a localização {location}.', - 'Describe this rule.' => 'Descreva essa regra.', - 'Describe this shipping zone.' => 'Descreva sua zona de envio.', - 'Describe this tax zone.' => 'Descreva essa zona de tributos.', - 'Description' => 'Descrição', - 'Destination Inventory Location' => 'Local de destino do inventário', - 'Destination' => 'Destino', - 'Details' => 'Detalhes', - 'Dimension Unit' => 'Unidade de Dimensão', - 'Dimensions' => 'Dimensões', - 'Disabled' => 'Desabilitado', - 'Disallow' => 'Proibir', - 'Discount all line items' => 'Descontar todos os itens de linha', - 'Discount description.' => 'Descrição do desconto.', - 'Discount is not allowed for the order' => 'Desconto não permitido para o pedido', - 'Discount is out of date.' => 'O desconto está desatualizado.', - 'Discount saved.' => 'Desconto guardado.', - 'Discount the matching items only' => 'Descontar apenas os itens correspondentes', - 'Discount use has reached its limit.' => 'A utilização do desconto atingiu o limite.', - 'Discount' => 'Desconto', - 'Discounted Item Subtotal' => 'Subtotal do artigo com desconto', - 'Discounted Items' => 'Artigos com desconto', - 'Discounts deleted.' => 'Descontos eliminados.', - 'Discounts reordered.' => 'Descontos reordenados.', - 'Discounts updated.' => 'Descontos atualizados.', - 'Discounts' => 'Descontos', - 'Disqualify with valid business tax ID?' => 'Desqualificar com ID fiscal comercial válida?', - 'Do not apply subsequent matching sales beyond applying this sale.' => 'Não aplicar vendas correspondentes subsequentes além da aplicação desta venda.', - 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Não aplicar esta taxa se o endereço do pedido tiver qualquer um dos IDs de imposto comerciais válidos selecionados.', - 'Do not attach a PDF to this email' => 'Não anexe um PDF a este email', - 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'Não chame o recálculo no pedido (Número: {orderNumber}) se houver erros.', - 'Donation can not be zero.' => 'A doação não pode ser zero.', - 'Donation needs to be an amount.' => 'A doação tem de ser uma quantia.', - 'Donation settings saved.' => 'Definições de doações guardadas.', - 'Donation' => 'Doação', - 'Donations' => 'Doações', - 'Done' => 'Concluído', - 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Não aplicar nenhum desconto subsequente a um pedido caso esse desconto seja aplicado', - 'Download PDF' => 'Baixar PDF', - 'Download PDF…' => 'Transferir PDF…', - 'Download Type' => 'Tipo de transferência', - 'Download' => 'Transferência', - 'Draft' => 'Rascunho', - 'Dummy gateway payment failed.' => 'O pagamento do gateway fictício falhou.', - 'Duplicate options exist' => 'Existem opções duplicadas', - 'Duration' => 'Duração', - 'EU VAT ID' => 'ID de NIF da UE', - 'Edit address' => 'Editar Endereço', - 'Edit adjustments' => 'Editar ajustes', - 'Edit catalog pricing rules' => 'Editar regras de determinação de preços do catálogo', - 'Edit discounts' => 'Editar descontos', - 'Edit options' => 'Editar opções', - 'Edit orders' => 'Editar pedidos', - 'Edit sales' => 'Editar ofertas', - 'Edit' => 'Editar', - 'Effect' => 'Efeito', - 'Either (Default) - The relationship field is on the purchasable or the category' => 'Qualquer (Padrão) - O campo de relação está no produto para compra ou na categoria', - 'Either way' => 'Ambas', - 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Erro de geração de email com PDF para o email “{email}”. Pedido: “{order}”. Erro de modelo de PDF: “{message}” {file}:{line}', - 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'O modelo do PDF de email não existe em “{templatePath}” para o email “{email}”. Encomenda: “{order}”.', - 'Email Subject' => 'Assunto do E-mail', - 'Email error. No email address found for order. Order: “{order}”' => 'Erro de e-mail. Não foi encontrado um endereço de e-mail para o pedido. Pedido: “{order}”', - 'Email is not enabled.' => 'O email não está ativo.', - 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'O modelo de email de texto simples não existe em “{templatePath}” o que resultou em “{templateParsedPath}” para o email “{email}”. Pedido: “{order}”.', - 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de modelo de email de texto simples para o email “{email}”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', - 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de caminho de modelo de email de texto simples para o email “{email}” em “Caminho de modelo:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', - 'Email required to make payments on a completed order.' => 'Email necessário para fazer pagamentos num pedido concluído.', - 'Email saved.' => 'Email guardado.', - 'Email sent' => 'Email enviado', - 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'O modelo de email não existe em “{templatePath}” o que resultou em “{templateParsedPath}” para o email “{email}”. Encomenda: “{order}”.', - 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de email personalizado para o email “{email}” em “Para:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de modelo de email para o email “{email}” em “BCC:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de modelo de email para o email “{email}” em “CC:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de modelo de email para o email “{email}” em “ReplyTo:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', - 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de modelo de email para o email “{email}” em “Assunto:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', - 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de modelo de email para o email “{email}”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', - 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Erro de análise de caminho de modelo de email para o email “{email}” em “Caminho de modelo:”. Pedido: “{order}”. Erro do modelo: “{message}” {file}:{line}', - 'Email unavailable.' => 'E-mail indisponível.', - 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'Não foi possível enviar o email “{email}” para o pedido “{order}”. Erro: {error} {file}:{line}', - 'Email “{email}” for order {order} was cancelled.' => 'O email “{email}” do pedido {order} foi cancelado.', - 'Email' => 'E-mail', - 'Emails' => 'E-mails', - 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Ativar se esta taxa deve ser incorporada ao preço do assunto tributável em vez de adicionar um custo ao pedido.', - 'Enable structure for products of this type' => 'Ativar estrutura para produtos deste tipo', - 'Enable this discount' => 'Habilitar esse desconto', - 'Enable this rule' => 'Ativar esta regra', - 'Enable this sale' => 'Habilitar essa oferta', - 'Enable this shipping method on the front end' => 'Habilitar esse método de envio de publicamente', - 'Enable this shipping rule' => 'Habilitar essa regra de envio', - 'Enable this tax rate' => 'Ativar esta taxa de imposto', - 'Enabled for customers to select during checkout?' => 'Habilitar para seleção por clientes durante a compra?', - 'Enabled for customers to select?' => 'Ativado para seleção por parte dos clientes?', - 'Enabled' => 'Habilitado', - 'Enabled?' => 'Habilitado?', - 'End Date' => 'Data Final', - 'Enter SKU' => 'Insira o SKU', - 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Insira um nome simples para esta taxa de imposto para ser usada no painel de controlo.', - 'Enter a percentage like {ex1} or {ex2}.' => 'Insira uma percentagem como {ex1} ou {ex2}.', - 'Enter coupon code' => 'Inserir o código do cupão', - 'Enter reference' => 'Inserir referência', - 'Error refunding transaction: {transactionHash}' => 'Erro ao reembolsar a transação: {transactionHash}', - 'Every new store must be assigned to at least one site.' => 'Cada nova loja deve ser atribuída a pelo menos um local.', - 'Everywhere' => 'Em todo o lado', - 'Example' => 'Exemplo', - 'Exclude this discount for products that are already on promotion' => 'Excluir este desconto para produtos que já estejam em promoção', - 'Expired Link' => 'Link expirado', - 'Expired' => 'Expirado', - 'Expiry Date' => 'Data de Expiração', - 'Expiry date' => 'Data de validade', - 'Expiry' => 'Validade', - 'Failed to receive transfer: {error}' => 'Falha ao receber a transferência: {error}', - 'Failed to send email. Please try again.' => 'Erro ao enviar e-mail. Tente novamente.', - 'Failed to start' => 'Erro ao iniciar', - 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Erro ao atualizar o {num, plural, =1{estado do pedido} other{estado dos pedidos}}.', - 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Erro ao atualizar o estado do pedido em {num, plural, =1{pedido} other{pedidos}}.', - 'Feet (ft)' => 'Pés (ft)', - 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Condições de filtragem que descrevem em quais pedidos essa regra se aplica. Escreva 0 para pular essa condição.', - 'First Name' => 'Nome', - 'Flat Amount Off Order' => 'Valor fixo de desconto no pedido', - 'Flat Order Discount Amount Off' => 'Valor do desconto de pedido fixo', - 'Free Order Payment Strategy' => 'Estratégia de pagamento de pedidos gratuitos', - 'Free Shipping' => 'Envio Grátis', - 'Free orders are processed by the payment gateway' => 'Os pedidos gratuitos são processados pelo gateway de pagamento', - 'Free orders complete immediately' => 'Os pedidos gratuitos completam-se imediatamente', - 'Free shipping can only be for whole order or matching items, not both.' => 'O envio gratuito só pode ser feito no pedido total ou em itens correspondentes, não em ambos.', - 'From Name' => 'Remetente', - 'Fulfill' => 'Cumprir', - 'Fulfilled' => 'Cumprido', - 'Fulfillment' => 'Cumprimento', - 'Full Name' => 'Nome completo', - 'Gateway Code' => 'Código de gateway', - 'Gateway Message' => 'Mensagem do gateway', - 'Gateway Reference' => 'Referência do gateway', - 'Gateway Response' => 'Resposta do gateway', - 'Gateway doesn’t support authorize' => 'O gateway não suporta a autorização', - 'Gateway doesn’t support partial refunds.' => 'O gateway não suporta reembolsos parciais.', - 'Gateway doesn’t support purchase' => 'O Gateway não suporta a compra', - 'Gateway doesn’t support refunds.' => 'O gateway não suporta reembolsos.', - 'Gateway saved.' => 'Gateway guardado.', - 'Gateway' => 'Gateway', - 'Gateways reordered.' => 'Gateways reordenados.', - 'Gateways' => 'Gateways', - 'General Settings' => 'Configurações Gerais', - 'General' => 'Geral', - 'Generate' => 'Gerar', - 'Generated Coupon Format' => 'Formato de cupão gerado', - 'Grams (g)' => 'Gramas (g)', - 'Groups for which this sale will be applicable to.' => 'Grupos aos quais esta venda será aplicável.', - 'HTML Email Template Path' => 'Caminho para Template de E-mail HTML', - 'Handle' => 'Identificador', - 'Harmonized System Code' => 'Código do Sistema Harmonizado', - 'Has Admin Notices' => 'Tem avisos da administração', - 'Has Emails?' => 'Possuí E-mails?', - 'Has Free Shipping' => 'Tem Envio Grátis', - 'Has Orders' => 'Tem pedidos', - 'Has Purchasable' => 'Tem produtos para compra', - 'Has Variants?' => 'Possuí Variantes?', - 'Height ({unit})' => 'Altura ({unit})', - 'Height' => 'Altura', - 'Hide snapshot' => 'Ocultar snapshot', - 'History' => 'Histórico', - 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Quanto tempo (em segundos) deve um link de transferência de PDF permanecer válido antes de expirar. O padrão é 86400 (24 horas).', - 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Quantas vezes um único e-mail pode usar esse desconto. Isso se aplica a todos os pedidos passados, quer seja usuário ou convidado. Defina como zero para uso ilimitado por usuários e convidados.', - 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Quantas vezes é permitido ao utilizador usar este desconto. Se estiver definido para algo além de zero, o desconto estará disponível apenas para utilizadores com sessão iniciada.', - 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Quantas vezes este desconto pode ser usado no total por convidados e utilizadores com sessão iniciada. Defina como zero para uso ilimitado.', - 'How products should be labeled within the control panel.' => 'Como os produtos devem ser rotulados no painel de controlo.', - 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'Como as Compras e Categorias estão relacionadas, o que determina os itens correspondentes. Consulte [Terminologia de relações]({link}).', - 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'Como esse produto será descrito em um item em um pedido. Você pode incluir tags que deem como resposta propriedades, assim como {ex1} ou {ex2}', - 'How this shipping method will be referred to in templates and forms.' => 'Como você vai se referir a esse método de envio nos templates e formulários.', - 'How variants should be labeled within the control panel.' => 'Como as variantes devem ser rotuladas no painel de controlo.', - 'How you’ll refer to this PDF in the templates.' => 'Como se irá referir a este PDF nos modelos.', - 'How you’ll refer to this product type in the templates.' => 'Como você vai se referir a esse tipo de produto nos template', - 'How you’ll refer to this shipping category in the templates.' => 'Como você vai se referir a essa categoria de envio nos templates e formulários.', - 'How you’ll refer to this status in the templates.' => 'Como você vai se referir a esse status nos templates.', - 'How you’ll refer to this subscription plan in the templates.' => 'Como se irá referir a este plano de subscrição nos modelos.', - 'How you’ll refer to this tax category in the templates.' => 'Como você vai se referir a essa categoria de tributos nos templates.', - 'ID' => 'ID', - 'IP Address' => 'Endereço de IP', - 'If disabled, this PDF will not be available or sent with emails.' => 'Se desativado, este PDF não ficará disponível nem será enviado nos emails.', - 'If disabled, this email will not send.' => 'Se desativado, este email não será enviado.', - 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Se ativado e esta taxa não corresponder ao pedido, o valor da taxa será removido do preço em questão no carrinho.', - 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Se definido para Apenas Autorizar, você precisará capturar manualmente os pagamentos antes, para então os fundos serem transferidos para sua conta. O Gateway precisa suportar a opção selecionada.', - 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Se selecionar a percentagem que vai estar "fora do preço do item com desconto", esta vai incluir o "Valor por item" assim como qualquer outro desconto aplicado antes deste.', - 'Ignore Promotions?' => 'Ignorar promoções?', - 'Ignore previous matching sales if this sale matches.' => 'Ignorar vendas correspondentes anteriores se esta venda for correspondente.', - 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignorar preços promocionais quando este desconto é aplicado a artigos de linha correspondentes', - 'Inactive Carts' => 'Carrinhos Inativos', - 'Inches (in)' => 'Polegadas (in)', - 'Include built-in line item tax.' => 'Incluir imposto de item de linha integrado.', - 'Include in price?' => 'Incluir no preço?', - 'Include line item discounts.' => 'Incluir descontos de item de linha.', - 'Include line item shipping costs.' => 'Incluir os custos de envio do item de linha.', - 'Include separate line item tax.' => 'Incluir imposto de item de linha em separado.', - 'Included in price?' => 'Incluído no preço?', - 'Included' => 'Incluído', - 'Incoming transfer from Transfer ID: ' => 'Transferência a entrar do ID de transferência: ', - 'Incoming' => 'A chegar', - 'Info' => 'Informações', - 'Information linked?' => 'Informação disponível numa ligação?', - 'Information' => 'Informação', - 'Invalid JSON' => 'JSON inválido', - 'Invalid Order ID' => 'A ID de pedido não é válida', - 'Invalid VAT ID.' => 'ID de IVA inválido.', - 'Invalid condition syntax' => 'Sintaxe da condição inválida', - 'Invalid email.' => 'Email inválido.', - 'Invalid formula syntax' => 'A sintaxe da fórmula não é válida', - 'Invalid gateway: {value}' => 'Gateway inválido: {value}', - 'Invalid inventory movements.' => 'Movimentos de stock inválidos.', - 'Invalid order condition syntax.' => 'Sintaxe inválida da condição do pedido.', - 'Invalid payment or order. Please review.' => 'Pagamento ou encomenda inválidos. Reveja.', - 'Invalid payment source ID: {value}' => 'ID de fonte de pagamento inválida: {value}', - 'Invalid store.' => 'Loja inválida.', - 'Invalid user.' => 'Utilizador inválido.', - 'Inventory Item' => 'Item de inventário', - 'Inventory Location' => 'Localização do inventário', - 'Inventory Locations' => 'Locais de inventário', - 'Inventory Tracked' => 'Inventário monitorizado', - 'Inventory Transfers' => 'Transferências de inventário', - 'Inventory could not be set.' => 'Não foi possível definir o inventário.', - 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'O local do stock tem stock comprometido, a(s) ordem(ns) deve(m) ser atendida(s) primeiro.', - 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Se o local do inventário tiver entradas de stock, a(s) transferência(s) deve(m) ser concluída(s) primeiro.', - 'Inventory location is already deactivated.' => 'A localização do inventário já está desativada.', - 'Inventory location saved.' => 'Local do inventário guardado.', - 'Inventory locations not saved.' => 'Locais de inventário não guardados.', - 'Inventory movement could not be saved.' => 'Não foi possível guardar o movimento de inventário.', - 'Inventory movement saved.' => 'Movimento de inventário guardado.', - 'Inventory updated.' => 'Inventário atualizado.', - 'Inventory was not updated.' => 'O inventário não foi atualizado.', - 'Inventory' => 'Inventário', - 'Invoice amount' => 'Quantia da fatura', - 'Invoice date' => 'Data da fatura', - 'Is Promotable' => 'É promovível', - 'Is Promotional Price?' => 'É preço promocional?', - 'Is Shippable' => 'É expedível', - 'Is Taxable' => 'É taxável', - 'Item Rates' => 'Custo por Item', - 'Item Subtotal' => 'Subtotal do item', - 'Item Total' => 'Total do artigo', - 'Item' => 'Item', - 'Items' => 'Itens', - 'Kilograms (kg)' => 'Quilogramas (kg)', - 'Label' => 'Etiqueta', - 'Landscape' => 'Paisagem', - 'Language' => 'Idioma', - 'Last Name' => 'Sobrenome', - 'Last Updated' => 'Última atualização', - 'Leave a category rate override blank to use the rate from above.' => 'Deixar em branco a substituição de uma taxa de categoria para utilizar a taxa acima.', - 'Leave blank for unlimited uses.' => 'Deixar em branco para usos ilimitados.', - 'Leave blank if products don’t have URLs' => 'Deixar em branco se os produtos não tiverem URL', - 'Leave gateway subscription as-is' => 'Deixar a subscrição do gateway tal como está', - 'Length ({unit})' => 'Comprimento ({unit})', - 'Length' => 'Comprimento', - 'Let each product choose which sites it should be saved to' => 'Deixar que cada produto escolha os sites em que deve ser guardado', - 'Limit which orders this discount applies to based on its line items.' => 'Limitar a quais pedidos este desconto é aplicável com base nos respetivos itens de linha.', - 'Limit which purchasables this sale applies to.' => 'Limitar a quais produtos para compra se aplica esta oferta.', - 'Limit' => 'Limite', - 'Line Item Statuses' => 'Estados de item de linha', - 'Line Item' => 'Item de linha', - 'Line Items' => 'Itens de linha', - 'Line item price (minus discounts)' => 'Preço do item de linha (menos descontos)', - 'Line item shipping cost' => 'Custo de envio do item individual', - 'Line item statuses reordered.' => 'Estados do item de linha reordenados.', - 'Link Duration' => 'Duração do link', - 'Link Sent' => 'Link enviado', - 'Link to a product' => 'Link para um produto', - 'Link to a variant' => 'Ligar a uma variante', - 'Link' => 'Ligação', - 'Live' => 'Publicado', - 'Location' => 'Local', - 'Locations that should be available for previewing products in this product type.' => 'Locais que devem estar disponíveis para pré-visualização de produtos deste tipo.', - 'MM' => 'MM', - 'Make a payment' => 'Fazer um pagamento', - 'Make this the primary store' => 'Tornar esta a loja principal', - 'Manage Inventory' => 'Gerir inventário', - 'Manage donation settings' => 'Gerir definições de doações', - 'Manage general store settings' => 'Gerir definições gerais da loja', - 'Manage inventory locations' => 'Gerir locais de inventário', - 'Manage inventory stock levels' => 'Gerir os níveis de stock do inventário', - 'Manage inventory transfers' => 'Gerir transferências de inventário', - 'Manage orders' => 'Gerir pedidos', - 'Manage payment currencies' => 'Gerir moedas de pagamento', - 'Manage promotions' => 'Gerir promoções', - 'Manage shipping' => 'Gerir envios', - 'Manage store settings' => 'Gerir definições da loja', - 'Manage subscription plans' => 'Gerir planos de subscrição', - 'Manage subscription' => 'Gerir subscrição', - 'Manage subscriptions' => 'Gerir subscrições', - 'Manage taxes' => 'Gerir impostos', - 'Manage' => 'Gerir', - 'Mark as Pending' => 'Assinalar como pendente', - 'Mark as completed' => 'Assinalar como concluído', - 'Match Billing Address' => 'Fazer corresponder ao endereço de cobrança', - 'Match Customer' => 'Fazer corresponder ao cliente', - 'Match Order' => 'Fazer corresponder ao pedido', - 'Match Orders' => 'Fazer corresponder aos pedidos', - 'Match Product' => 'Corresponder produto', - 'Match Purchasable' => 'Produto para compra correspondente', - 'Match Shipping Address' => 'Fazer corresponder ao endereço de envio', - 'Match Variant' => 'Corresponder variante', - 'Matching Items' => 'Itens correspondentes', - 'Max Qty' => 'Quantidade máxima', - 'Max Uses' => 'Máximo de utilizações', - 'Max Variants' => 'Variantes máximas', - 'Max quantity must greater than min.' => 'A quantidade máxima deve ser superior à mínima.', - 'Maximum Purchase Quantity' => 'Quantidade Máxima por Compra', - 'Maximum Total Shipping Cost' => 'Custo Total Máximo do Envio', - 'Maximum allowed quantity' => 'Quantidade máxima autorizada', - 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'O número máximo de itens correspondentes que podem ser comprados para a aplicação do desconto. Um valor zero aqui ignorará essa condição.', - 'Maximum order quantity for this item is {num}.' => 'A quantidade máxima de pedido para este item é {num}.', - 'Message' => 'Mensagem', - 'Meters (m)' => 'Metros (m)', - 'Millimeters (mm)' => 'Milímetros (mm)', - 'Min Qty' => 'Quantidade mínima', - 'Min quantity must be less than max.' => 'A quantidade mínima deve ser inferior à máxima.', - 'Minimum Purchase Quantity' => 'Quantidade Mínima por Pedido', - 'Minimum Total Price Strategy' => 'Estratégia Mínima de Preço Total', - 'Minimum Total Shipping Cost' => 'Custo Total Mínimo do Envio', - 'Minimum allowed quantity' => 'Quantidade mínima autorizada', - 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Número mínimo de items válidos que precisam ser pedidos para que esse desconto seja aplicado.', - 'Minimum order quantity for this item is {num}.' => 'A quantidade mínima para pedido deste item é {num}.', - 'Missing Gateway' => 'Gateway em falta', - 'Missing a default inventory location.' => 'Falta uma localização de inventário padrão.', - 'Move Inventory' => 'Mover inventário', - 'Move To' => 'Mover para', - 'Move {qty} from {fromType} to {toType}' => 'Mover {qty} de {fromType} para {toType}', - 'Move' => 'Mover', - 'Movement from deactivated inventory location' => 'Movimento a partir do local de inventário desativado', - 'Movement' => 'Movimento', - 'Must have at least one variant.' => 'Deve ter pelo menos uma variante.', - 'Name Field' => 'Campo do nome', - 'Name' => 'Nome', - 'New Customer' => 'Novo cliente', - 'New Customers' => 'Novos clientes', - 'New Order' => 'Novo pedido', - 'New PDF' => 'Novo PDF', - 'New address' => 'Novo endereço', - 'New catalog pricing rule' => 'Nova regra de fixação de preços por catálogo', - 'New currency' => 'Nova moeda', - 'New discount' => 'Novo Desconto', - 'New email' => 'Novo e-mail', - 'New gateway' => 'Novo gateway', - 'New line item status' => 'Novo estado de item de linha', - 'New line items get this status by default when the order is completed' => 'Novos itens de linha obtêm este status por defeito quando o pedido é concluído', - 'New location' => 'Novo local', - 'New order status' => 'Novo status de pedido', - 'New orders get this status by default' => 'Novos pedidos recebem esse status automaticamente', - 'New product type' => 'Novo tipo de produto', - 'New product' => 'Novo produto', - 'New product, choose a type' => 'Novo produto, escolher um tipo', - 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Os novos produtos assumem como padrão a primeira categoria de imposto disponível para eles. Se não houver nenhuma disponível, será usada esta categoria.', - 'New sale' => 'Nova oferta', - 'New shipping category' => 'Nova categoria de envio', - 'New shipping method' => 'Novo método de envio', - 'New shipping rule' => 'Nova regra de envio', - 'New shipping zone' => 'Nova zona de envio', - 'New subscription plan' => 'Novo plano de subscrição', - 'New tax category' => 'Nova categoria de tributos', - 'New tax rate' => 'Novo tributo', - 'New tax zone' => 'Nova zona de tributos', - 'New transfer' => 'Nova transferência', - 'New {productType} product' => 'Novo produto {productType}', - 'New' => 'Novo', - 'Next payment' => 'Próximo pagamento', - 'No Address' => 'Sem endereço', - 'No PDFs exist yet.' => 'Não existem PDFs ainda.', - 'No access given to any specific store management features.' => 'Sem acesso atribuído a quaisquer funcionalidades específicas de gestão de lojas.', - 'No additional payment currencies exist yet.' => 'Ainda não há moedas de pagamento adicionais.', - 'No address' => 'Sem endereço', - 'No billing address' => 'Sem endereço de faturação', - 'No catalog pricing rule exists with the ID “{id}”' => 'Não existe nenhuma regra de fixação de preços do catálogo com o ID "{id}"', - 'No catalog pricing rules exist yet.' => 'Ainda não existem regras de fixação de preços de catálogo.', - 'No currency exists with the ID “{id}”' => 'Não existe moeda com a ID “{id}”', - 'No customer email address exists on this cart.' => 'Não existe nenhum e-mail de cliente neste carrinho.', - 'No description' => 'Sem descrição', - 'No discount exists with the ID “{id}”' => 'Não existe desconto com a ID “{id}”', - 'No discounts exist yet.' => 'Não existem descontos ainda.', - 'No donation amount supplied.' => 'Não foi fornecido nenhum valor de doação.', - 'No emails exist yet.' => 'Nenhum e-mail existe ainda.', - 'No inventory changes made.' => 'Não foram feitas alterações ao inventário.', - 'No inventory found.' => 'Não foi encontrado nenhum inventário.', - 'No inventory movements made.' => 'Não foram feitos movimentos de inventário.', - 'No inventory transactions for this location.' => 'Não há transações de inventário para este local.', - 'No new customer selected.' => 'Não foi selecionado nenhum novo cliente.', - 'No order history exists with the ID “{id}”' => 'Não existe histórico de pedidos com o ID “{id}”', - 'No order status history items will exist until the cart becomes an order.' => 'Não existirão itens do histórico de estado de encomenda até que o carrinho seja convertido numa encomenda.', - 'No payment source exists with the ID “{id}”' => 'Não existem fontes de pagamento com a ID “{id}”', - 'No private Note.' => 'Sem nota privada.', - 'No product available.' => 'Nenhum produto disponível.', - 'No product types exist yet.' => 'Não existe tipos de produto ainda.', - 'No purchasable available.' => 'Não está disponível nenhum produto para compra.', - 'No sale exists with the ID “{id}”' => 'Não existe oferta com a ID “{id}”', - 'No sales exist yet.' => 'Não existem ofertas ainda.', - 'No shipping address' => 'Sem endereço de Envio', - 'No shipping category exists with the ID “{id}”' => 'Não existe categoria de envio com a ID “{id}”', - 'No shipping method exists with the ID “{id}”' => 'Não existe método de envio com a ID “{id}”', - 'No shipping rule exists with the ID “{id}”' => 'Não existe regra de envio com o ID “{id}”', - 'No shipping rules exist yet.' => 'Ainda não existem regras de envio.', - 'No shipping zone exists with the ID “{id}”' => 'Não existe zona de envio com a ID “{id}”', - 'No stats available.' => 'Não há estatísticas disponíveis.', - 'No subscription plan exists with the ID “{id}”' => 'Não existe um plano de subscrição com a ID “{id}”', - 'No subscription plans exist yet.' => 'Ainda não existem planos de subscrição.', - 'No tax category exists with the ID “{id}”' => 'Não existem categorias de tributos com o ID “{id}”', - 'No tax rate exists with the ID “{id}”' => 'Não existem impostos com a ID “{id}”', - 'No tax zone exists with the ID “{id}”' => 'Não existe uma zona fiscal com a ID “{id}”', - 'No transactions exist.' => 'Não há nenhuma transação.', - 'No user authenticated.' => 'Nenhum utilizador autenticado.', - 'No' => 'Não', - 'None on hand' => 'Nenhum disponível', - 'None' => 'Nenhum', - 'Not a valid address type' => 'Tipo de endereço inválido', - 'Not a valid credit card number.' => 'Não é um número de cartão de crédito válido.', - 'Not all SKUs are unique.' => 'Nem todos os SKU são únicos.', - 'Note' => 'Observação', - 'Notes' => 'Notas', - 'Number of Coupons' => 'Número de cupões', - 'Number' => 'Número', - 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Dos sites habilitados acima, em que sites é que os produtos deste tipo de produto devem ser guardados?', - 'On Hand' => 'Disponível', - 'Only allow this gateway to be used for zero value orders?' => 'Permitir que apenas este gateway seja utilizado para encomendas de valor zero?', - 'Only match certain purchasables…' => 'Apenas corresponder a determinados produtos para compra…', - 'Only match purchasables related to…' => 'Apenas corresponder produtos para compra relacionados com…', - 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Só serão incluídos pedidos com os seguintes estados de pedido. Deixe em branco para incluir todos os estados.', - 'Only save product to the site they were created in' => 'Guardar produtos apenas no site em que foram criados', - 'Options' => 'Opções', - 'Order Condition Formula' => 'Formula de condição de pedido', - 'Order Description Format' => 'Formato de Descrição do Pedido', - 'Order Details' => 'Detalhes do pedido', - 'Order Fields' => 'Campos de Pedido', - 'Order PDF Download Link' => 'Link de pedido de transferência de PDF', - 'Order PDF Filename Format' => 'Formato do Nome do Arquivo PDF do Pedido', - 'Order Reference Number Format' => 'Formato do número de referência da encomenda', - 'Order Settings' => 'Configurações de Pedido', - 'Order Site' => 'Site do pedido', - 'Order Status description.' => 'Descrição do estado do pedido.', - 'Order Status' => 'Status do Pedido', - 'Order Statuses' => 'Status de Pedido', - 'Order can not be empty.' => 'O pedido não pode ficar vazio.', - 'Order count' => 'Contagem de pedidos', - 'Order customer data removed.' => 'Solicitar a remoção dos dados do cliente.', - 'Order deleted.' => 'Pedido eliminado.', - 'Order fields saved.' => 'Campos de pedido guardados.', - 'Order not found.' => 'O pedido não foi encontrado.', - 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'O saldo do pagamento do pedido é {outstandingBalanceAsCurrency}. Este é o valor máximo que será cobrado.', - 'Order recalculated.' => 'Pedido recalculado.', - 'Order status saved.' => 'Status do pedido guardado.', - 'Order statuses reordered.' => 'Estados de pedidos reordenados.', - 'Order total shipping cost' => 'Custo de envio total da encomenda', - 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Preço total da encomenda passível de aplicação de impostos (subtotal do item individual + total de descontos + total do envio)', - 'Order' => 'Pedido', - 'Orders (Legacy)' => 'Pedidos (Legacy)', - 'Orders deleted.' => 'Pedidos eliminados.', - 'Orders not restored.' => 'Pedidos não restaurados.', - 'Orders restored.' => 'Pedidos restaurados.', - 'Orders' => 'Pedidos', - 'Organization Name' => 'Nome da empresa', - 'Organization Tax ID' => 'NIF da Empresa', - 'Origin and destination cannot be the same.' => 'A origem e o destino não podem ser o mesmo.', - 'Origin' => 'Origem', - 'Original Price' => 'Preço original', - 'Original price' => 'Preço original', - 'Original promotional price' => 'Preço promocional original', - 'Other Languages' => 'Outros idiomas', - 'Other countries' => 'Outros países', - 'Outgoing transfer from Transfer ID: ' => 'Transferência a sair do ID de transferência: ', - 'Overpaid' => 'Pagamento excessivo', - 'Overrides previous?' => 'Substitui o anterior?', - 'PDF Attachment' => 'Anexo PDF', - 'PDF Template Path' => 'Caminho do modelo de PDF', - 'PDF saved.' => 'PDF guardado.', - 'PDF' => 'PDF', - 'PDFs & Emails' => 'PDFs e E-mails', - 'PDFs' => 'PDFs', - 'Paid Amount' => 'Quantia paga', - 'Paid Status' => 'Estado de pagamento efetuado', - 'Paid' => 'Pago', - 'Paper Orientation' => 'Orientação do papel', - 'Paper Size' => 'Tamanho do papel', - 'Partial payment not allowed.' => 'Não é permitido o pagamento parcial.', - 'Partial' => 'Parcial', - 'Past year' => 'Último ano', - 'Past {num} days' => 'Últimos {num} dias', - 'Pay {amount} of {currency} on the order.' => 'Pagar {amount} de {currency} no pedido.', - 'Pay' => 'Pagar', - 'Payment Amount' => 'Valor do Pagamento', - 'Payment Currencies' => 'Moedas de Pagamento', - 'Payment Gateway' => 'Gateway de pagamento', - 'Payment Method' => 'Método de Pagamento', - 'Payment error: {message}' => 'Erro no pagamento: {message}', - 'Payment method issue' => 'Problema no método de pagamento', - 'Payment source created.' => 'Fonte de pagamento criada.', - 'Payment source deleted.' => 'Fonte de pagamento eliminada.', - 'Payments' => 'Pagamentos', - 'Pending' => 'Pendente', - 'Per Email Address Discount Limit' => 'Limite de desconto por endereço de email', - 'Per Item Amount Off' => 'Desconto por item', - 'Per Item Discount' => 'Desconto por item', - 'Per Item Percentage Off' => 'Percentagem de desconto por item', - 'Per Item Rate' => 'Custo Por Item', - 'Per User Discount Limit' => 'Limite de desconto por utilizador', - 'Percentage Rate' => 'Custo Proporcional', - 'Phone (Alt)' => 'Telefone (Alt)', - 'Phone' => 'Telefone', - 'Pick a plan' => 'Escolher um plano', - 'Plain Text Email Template Path' => 'Caminho para Template de E-mail Simples', - 'Plan' => 'Plano', - 'Plans reordered.' => 'Planos reordenados.', - 'Portrait' => 'Retrato', - 'Post Date' => 'Data de Envio', - 'Postal Code Formula' => 'Fórmula do código postal', - 'Pounds (lb)' => 'Libras (lb)', - 'Preview' => 'Pré-visualização', - 'Previous Status' => 'Status Anterior', - 'Price' => 'Preço', - 'Prices' => 'Preços', - 'Pricing Rules' => 'Regras de preços', - 'Pricing jobs are currently running.' => 'As atividades de fixação de preços estão atualmente em curso.', - 'Pricing' => 'Preços', - 'Primary Billing Address' => 'Endereço de faturação principal', - 'Primary Shipping Address' => 'Endereço de envio principal', - 'Primary payment source updated.' => 'Fonte de pagamento principal atualizada.', - 'Primary' => 'Principal', - 'Private Note' => 'Nota privada', - 'Product Fields' => 'Campos de Produto', - 'Product ID is required.' => 'É necessária a ID do produto.', - 'Product Template' => 'Template do Produto', - 'Product Title Format' => 'Formato do Título do Produto', - 'Product Type' => 'Tipo de produto', - 'Product Types' => 'Tipos de Produto', - 'Product URI Format' => 'Formato do URI do Produto', - 'Product Variant' => 'Variante de produto', - 'Product Variants' => 'Variantes de produto', - 'Product type saved.' => 'Tipo de produto guardado.', - 'Product type settings' => 'Definições do tipo de produto', - 'Product' => 'Produto', - 'Products and Variants deleted.' => 'Produtos e Variantes eliminados.', - 'Products not restored.' => 'Produtos não restaurados.', - 'Products restored.' => 'Produtos restaurados.', - 'Products' => 'Produtos', - 'Promotable' => 'Promovível', - 'Promotable?' => 'Promovível?', - 'Promotional Amount' => 'Montante promocional', - 'Promotional Price' => 'Preço promocional', - 'Purchasable Categories' => 'Categorias para compra', - 'Purchasable ID and Sale ID are required.' => 'É necessária a ID do produto para compra e da oferta.', - 'Purchasable ID is required.' => 'É necessária a ID do produto para compra.', - 'Purchasable Type' => 'Tipo de artigo para compra', - 'Purchasable' => 'Para compra', - 'Purchase (Authorize and Capture Immediately)' => 'Comprar (Autorizar e cobrar imediatamente)', - 'Purchase Total' => 'Total da Compra', - 'Qty' => 'Qtd', - 'Quality Control' => 'Controlo de qualidade', - 'Quantity' => 'Quantidade', - 'Rate' => 'Taxa', - 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Reatribuir {numOrders, plural, one {}=1{pedido} other{pedidos}}', - 'Recalculate order' => 'Recalcular pedido', - 'Receive Inventory' => 'Receber inventário', - 'Receive Transfer' => 'Receber transferência', - 'Receive' => 'Receber', - 'Received' => 'Recebido', - 'Recent Orders' => 'Pedidos Recentes', - 'Recipient' => 'Destinatário', - 'Recover Cart' => 'Recuperar carrinho', - 'Reduce price' => 'Reduzir preço', - 'Reduce the price by a fixed amount' => 'Reduzir um valor fixo no preço', - 'Reduce the price by a percentage of the original price' => 'Reduzir o preço segundo uma percentagem do preço original', - 'Reference' => 'Referência', - 'Refresh payment history' => 'Atualizar histórico de pagamentos', - 'Refund note' => 'Nota de reembolso', - 'Refund payment' => 'Reembolsar pagamento', - 'Refund' => 'Estornar', - 'Reject' => 'Rejeitar', - 'Rejected' => 'Rejeitado', - 'Relationship Type' => 'Tipo de relação', - 'Removable included tax rates are only allowed for the default tax zone.' => 'As taxas de imposto removíveis incluídas só são permitidas na zona de imposto predefinida.', - 'Remove address' => 'Remover endereço', - 'Remove all shipping costs from the order' => 'Remover todos os custos de envio do pedido', - 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Remover a associação do cliente e o endereço de e-mail {numOrders, plural, one {}=1{do pedido} other{dos pedidos}}. Se desejar, selecione abaixo os dados adicionais do cliente que pretende eliminar', - 'Remove customer data' => 'Remover dados dos clientes', - 'Remove from price?' => 'Remover do preço?', - 'Remove shipping costs for matching items only' => 'Remove os custos de envio apenas nos itens correspondentes', - 'Remove the included tax when a valid organization tax ID is present?' => 'Remover o imposto incluído quando está presente um NIF empresarial válido?', - 'Remove' => 'Remover', - 'Removed' => 'Removido', - 'Repeat Customers' => 'Repetir clientes', - 'Reply To' => 'Responder a', - 'Require Billing Address At Checkout' => 'Exigir endereço de faturação no checkout', - 'Require Coupon Code' => 'Requer Código de Cupão', - 'Require Shipping Address At Checkout' => 'Exigir endereço de envio no checkout', - 'Require Shipping Method Selection At Checkout' => 'Exigir a seleção do método de envio no checkout', - 'Require' => 'Exigir', - 'Reserved' => 'Reservado', - 'Reset usage' => 'Restaurar a utilização', - 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Restringir o desconto apenas aos pedidos de clientes que tenham comprado o valor mínimo total em itens válidos.', - 'Revenue Options' => 'Opções de receita', - 'Revenue' => 'Receita', - 'Rule' => 'Regra', - 'Rules reordered.' => 'Regras reordenadas.', - 'SKU' => 'SKU', - 'Safety' => 'Segurança', - 'Sale Price' => 'Preço de venda', - 'Sale description.' => 'Descrição da Oferta.', - 'Sale reordered.' => 'Venda reencomendada.', - 'Sale saved.' => 'Oferta guardada.', - 'Sale' => 'Oferta', - 'Sales deleted.' => 'Promoções eliminadas.', - 'Sales updated.' => 'Ofertas atualizadas.', - 'Sales' => 'Ofertas', - 'Save and continue editing' => 'Salvar e continuar editando', - 'Save and return to all orders' => 'Guardar e devolver a todos os pedidos', - 'Save and set rules' => 'Salvar e definir regras', - 'Save as a new rule' => 'Guardar como nova regra', - 'Save product to all sites enabled for this product type' => 'Guardar produto em todos os sites ativos para este tipo de produto', - 'Save product to other sites in the same site group' => 'Guardar produto noutros sites do mesmo grupo de sites', - 'Save product to other sites with the same language' => 'Guardar produto para outros sites com o mesmo idioma', - 'Save' => 'Salvar', - 'Search customer…' => 'Pesquisar cliente…', - 'Search inventory' => 'Pesquisar inventário', - 'Search or enter customer email…' => 'Pesquisar ou introduzir email do cliente…', - 'Search…' => 'Pesquisar…', - 'See Orders' => 'Ver pedidos', - 'Select a gateway' => 'Selecionar um gateway', - 'Select a tax category.' => 'Selecionar uma categoria de tributos.', - 'Select a tax zone. If empty, this rate will match anywhere.' => 'Selecione uma zona fiscal. Se estiver em branco, esta taxa corresponderá a qualquer lugar.', - 'Select address' => 'Selecionar endereço', - 'Select an item' => 'Selecionar um item', - 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Selecione como a regra de determinação do preço do catálogo será aplicada ao(s) artigo(s) para compra.', - 'Select how the sale will be applied to the purchasable(s).' => 'Selecione como o desconto será aplicado aos itens que podem ser comprados.', - 'Select product type' => 'Selecionar o tipo de produto', - 'Select the emails that will be sent when transitioning to this status.' => 'Selecione os e-mails que serão enviados ao transicionar para esse status.', - 'Select what this rate should be applied to.' => 'Selecione onde aplicar esta taxa.', - 'Send Email' => 'Enviar email', - 'Send to custom recipient' => 'Enviar para um recipiente personalizado', - 'Send to the customer' => 'Enviar para o cliente', - 'Set Quantity' => 'Definir quantidade', - 'Set default category' => 'Definir categoria padrão', - 'Set default variant' => 'Definir variante predefinida', - 'Set or Adjust' => 'Definir ou ajustar', - 'Set price' => 'Definir preço', - 'Set status' => 'Definir status', - 'Set the price to a flat amount' => 'Definir o preço para um montante fixo', - 'Set the price to a percentage of the original price' => 'Definir o preço para uma percentagem do preço original', - 'Set the sale price to a flat amount' => 'Definir uma quantia fixa no preço de venda', - 'Set the sale price to a percentage of the original price' => 'Definir o preço de venda como uma percentagem do preço original', - 'Set to' => 'Definir para', - 'Settings saved.' => 'Definições guardadas.', - 'Settings' => 'Configurações', - 'Share cart…' => 'Partilhar carrinho…', - 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Envio - O custo mínimo é o custo de envio, caso o preço do pedido seja inferior ao custo de envio.', - 'Shipping Address Zone' => 'Zona do endereço de envio', - 'Shipping Address' => 'Endereço de Envio', - 'Shipping Business Name' => 'Nome da Empresa para Envio', - 'Shipping Categories' => 'Categorias de Envio', - 'Shipping Category Conditions' => 'Condições da Categoria de Envio', - 'Shipping Category' => 'Categoria de Envio', - 'Shipping First Name' => 'Primeiro nome de envio', - 'Shipping Full Name' => 'Nome Completo de Envio', - 'Shipping Last Name' => 'Último nome de envio', - 'Shipping Method' => 'Método de Envio', - 'Shipping Methods' => 'Métodos de Envio', - 'Shipping Rule' => 'Regra de Envio', - 'Shipping Zones' => 'Zonas de Envio', - 'Shipping address required.' => 'É necessário um endereço de envio.', - 'Shipping categories deleted.' => 'Categorias de envio eliminadas.', - 'Shipping category saved.' => 'Categoria de envio guardada.', - 'Shipping category updated.' => 'Categoria de envio atualizada.', - 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Custos de envio adicionados ao pedido como um todo antes que as taxas de percentagem, o item e o peso sejam aplicados. Defina como zero para desativar esta taxa. A regra completa, incluindo esta taxa básica, não será igual e só é aplicável se o carrinho contiver apenas itens que não precisam de ser enviados, tais como produtos digitais.', - 'Shipping method saved.' => 'Método de envio guardado.', - 'Shipping methods and rules deleted.' => 'Métodos e regras de envio eliminados.', - 'Shipping methods updated.' => 'Métodos de envio atualizados.', - 'Shipping rule saved.' => 'Regra de envio guardada.', - 'Shipping zone saved.' => 'Zona de envio guardada.', - 'Shipping' => 'Envio', - 'Short Number' => 'Número curto', - 'Show Chart?' => 'Mostrar carrinho?', - 'Show Order Count?' => 'Mostrar contagem de pedidos?', - 'Show all prices' => 'Mostrar todos os preços', - 'Show archived gateways' => 'Mostrar gateways arquivados', - 'Show order count line on chart.' => 'Mostrar a linha de contagem de pedidos no gráfico.', - 'Show related sales' => 'Mostrar vendas relacionadas', - 'Show rule details' => 'Mostrar os detalhes da regra', - 'Show the Dimensions and Weight fields for products of this type' => 'Mostrar os campos de Dimensões e Peso para produtos desse tipo', - 'Show the Title field for products' => 'Mostra o campo Título para produtos', - 'Show the Title field for variants' => 'Mostra o campo Título para variantes', - 'Signed In' => 'Iniciou sessão', - 'Site Languages' => 'Idioma do site', - 'Site store mapping saved.' => 'Mapeamento da loja do site guardado.', - 'Sites' => 'Sites', - 'Slug' => 'Slug', - 'Snapshot' => 'Snapshot', - 'Snapshots' => 'Snapshots', - 'Some orders restored.' => 'Alguns pedidos restaurados.', - 'Some products restored.' => 'Alguns produtos restaurados.', - 'Some variants restored.' => 'Algumas variantes restauradas.', - 'Something changed with the order before payment, please review your order and submit payment again.' => 'Algo mudou no pedido antes do pagamento, por favor reveja-o e faça novamente o pagamento.', - 'Sorry, no matching options.' => 'Lamentamos mas não há opções correspondentes.', - 'Source - The purchasable relationship field is on the category' => 'Fonte - O campo de relação de compra está na categoria', - 'Source' => 'Fonte', - 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Especifique uma condição Twig que determina se o desconto deve ser aplicado a um pedido específico. (O pedido pode ser referenciado através de uma variável `order`.)', - 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Especifique uma condição Twig que determina se a regra de envio deve ser aplicada a um pedido específico. (O pedido pode ser referenciado através de uma variável `order`.)', - 'Start Date' => 'Data Inicial', - 'State' => 'Estado', - 'Status Email Address' => 'Endereço de E-mail de Status', - 'Status Emails' => 'E-mails de Status', - 'Status History' => 'Histórico de estado', - 'Status Updated.' => 'Estado atualizado.', - 'Status change message' => 'Mensagem de alteração de estado', - 'Status' => 'Estado', - 'Stock' => 'Estoque', - 'Stops Processing?' => 'Para o processamento?', - 'Stops subsequent?' => 'Para o subsequente?', - 'Store Location' => 'Localização da loja', - 'Store Management' => 'Gestão de loja', - 'Store Markets' => 'Mercados da loja', - 'Store Rule' => 'Regra da loja', - 'Store saved.' => 'Loja guardada.', - 'Store' => 'Loja', - 'Stores & Sites' => 'Lojas e sites', - 'Stores' => 'Lojas', - 'Strategy to apply when an order is free or has a zero balance.' => 'Estratégia a aplicar quando um pedido é grátis ou tem saldo zero.', - 'Strategy to apply when calculating the minimum order price.' => 'Estratégia a aplicar ao calcular o preço mínimo do pedido.', - 'Subject' => 'Assunto', - 'Subscribing user' => 'Utilizador com subscrição ativa', - 'Subscription Fields' => 'Campos de subscrição', - 'Subscription Plans' => 'Planos de subscrição', - 'Subscription Settings' => 'Definições da subscrição', - 'Subscription cancelled.' => 'Subscrição cancelada.', - 'Subscription date' => 'Data de subscrição', - 'Subscription fields saved.' => 'Campos de subscrição guardados.', - 'Subscription for {user} to {plan} prevented by a plugin.' => 'Subscrição de {user} para {plan} impedida por um plugin.', - 'Subscription plan saved.' => 'Plano de subscrição guardado.', - 'Subscription plan' => 'Plano de subscrição', - 'Subscription plans' => 'Planos de subscrição', - 'Subscription reactivated.' => 'Subscrição reativada.', - 'Subscription reference' => 'Referência da subscrição', - 'Subscription started.' => 'Subscrição iniciada.', - 'Subscription switched.' => 'Subscrição trocada.', - 'Subscription to “{plan}”' => 'Subscrição de “{plan}”', - 'Subscription' => 'Subscrição', - 'Subscriptions on hold' => 'Subscrições em pausa', - 'Subscriptions' => 'Subscrições', - 'Suppress emails' => 'Suprimir emails', - 'Switch plan' => 'Mudar de plano', - 'Switch' => 'Mudar', - 'System' => 'Sistema', - 'Table Columns' => 'Colunas de tabela', - 'Target - The category relationship field is on the purchasable' => 'Destino - O campo da relação da categoria está no produto para compra', - 'Tax & Shipping' => 'Imposto e Envio', - 'Tax (inc)' => 'Taxa (inc)', - 'Tax Categories' => 'Categorias de Tributos', - 'Tax Category' => 'Categorias de Tributos', - 'Tax Rates' => 'Tributos', - 'Tax Zone' => 'Zona de Tributo', - 'Tax Zones' => 'Zonas de Tributos', - 'Tax categories deleted.' => 'Categorias de impostos eliminadas.', - 'Tax category saved.' => 'Categoria de imposto guardada.', - 'Tax category updated.' => 'Categoria de taxa atualizada.', - 'Tax rate saved.' => 'Taxa do imposto guardada.', - 'Tax rates updated.' => 'Taxas de imposto atualizadas.', - 'Tax zone saved.' => 'Zona fiscal guardada.', - 'Tax' => 'Imposto', - 'Taxable Subject' => 'Alvo do Tributo', - 'Template Path' => 'Caminho do Template', - 'That handle is already in use' => 'Essa pega já está a ser usada', - 'That handle is already in use.' => 'Esta pega já está a ser usada.', - 'The PDF to attach to this email.' => 'O PDF a anexar a este email.', - 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'A URL para a página para atualizar os detalhes de cobrança de uma assinatura, bem como gerir a autenticação 3DS.', - 'The address provided is outside the store’s market.' => 'O endereço fornecido está fora do mercado da loja.', - 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'O valor de desconto aplicado a todo o pedido. Esse valor é distribuído pelos itens de linha na ordem do preço mais alto para o preço mais baixo, até que o desconto esteja esgotado.', - 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'O desconto base só pode descontar itens do carrinho para zero até que seja gasto, e não pode tornar o pedido negativo.', - 'The cart recovery link is invalid. Please request a new one.' => 'O link de recuperação do carrinho não é válido. Solicite um novo.', - 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'A taxa de conversão que será aplicada ao converter o valor para essa moeda. Por exemplo, se o item custa {amount1}, uma taxa de conversão de {rate} resultaria em {amount2} na outra moeda.', - 'The countries that orders are allowed to be placed from.' => 'Os países a partir dos quais podem ser feitos pedidos.', - 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'O cupão "{code}" ultrapassou o seu limite de utilização de {limit}.', - 'The customer for this order has been deleted.' => 'O cliente associado a este pedido foi eliminado.', - 'The default shipping category is automatically available to all product types.' => 'A categoria de envio padrão está automaticamente disponível para todos os tipos de produtos.', - 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'O desconto "{name}" ultrapassou o seu limite total de utilização de {limit}.', - 'The download link has expired. Please request a new one.' => 'O link de download expirou. Por favor, solicite um novo.', - 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'O endereço de e-mail do qual os e-mails de status de pedidos são enviados. Deixe em branco para usar o Endereço de E-mail do Sistema definido nas Configurações Gerais do Craft.', - 'The entry that contains the description for this subscription’s plan.' => 'A entrada que contém a descrição deste plano de subscrições.', - 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'O valor fixo que deve ser descontado em cada item. Ex: “3” para $3 de desconto.', - 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'O formato usado para gerar novos cupões, por exemplo {example}. Quaisquer caracteres `#` serão substituídos por uma letra aleatória.', - 'The from and to inventory locations must be different.' => 'Os locais de inventário de origem e destino devem ser diferentes.', - 'The inventory locations this store uses.' => 'Os locais de inventário que esta loja utiliza.', - 'The item is not enabled for sale.' => 'O item não está disponível para venda.', - 'The language the order was made in.' => 'O idioma em que foi feito o pedido.', - 'The language to be used when this email is rendered.' => 'O idioma a usar quando este e-mail for renderizado.', - 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'O número máximo de níveis que este tipo de produto pode ter. Deixe em branco se você não se importa.', - 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'O máximo que o cliente deve gastar com o envio. Defina o valor como zero para desabilitar.', - 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'O mínimo que o cliente deve gastar com o envio. Defina o valor como zero para desabilitar.', - 'The order is not valid.' => 'O pedido não é válido.', - 'The payment gateway that will be used for the subscription plan.' => 'Que gateway de pagamento será utilizado para o plano de subscrição.', - 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'O valor percentual que deve descontar em cada item, ou seja, {ex1} para {ex2} de desconto. As percentagens são arredondadas para duas casas decimais.', - 'The previously-selected shipping method is no longer available.' => 'Já não está disponível o método de envio selecionado anteriormente.', - 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'O preço de {description} subiu de {originalSalePriceAsCurrency} para {newSalePriceAsCurrency}', - 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'O preço de {description} baixou de {originalSalePriceAsCurrency} para {newSalePriceAsCurrency}', - 'The primary currency cannot be changed after orders are placed.' => 'A moeda principal não pode ser alterada depois dos pedidos terem sido feitos.', - 'The purchasable defines the relationship' => 'Os produtos para compra definem a relação', - 'The purchasable is related by another element' => 'Os produtos para compra estão relacionados por outro elemento', - 'The recipient of the email. Twig code can be used here.' => 'O destinatário do email. O código twig pode ser utilizado aqui.', - 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'A resposta ao endereço de email. Deixar em branco para resposta normal de remetente de email. O código Twig pode ser usado aqui.', - 'The site the order was made in.' => 'O site onde o pedido foi feito.', - 'The site to be used when this email is rendered.' => 'O site a utilizar quando este e-mail for apresentado.', - 'The subject line of the email. Twig code can be used here.' => 'A linha de assunto do email. O código twig pode ser utilizado aqui.', - 'The template that the PDF should be generated from.' => 'O modelo a partir do qual o PDF deve ser gerado.', - 'The template to be used for HTML emails.' => 'O template a ser usado para e-mails HTML.', - 'The template to be used for plain text emails. Twig code can be used here.' => 'O modelo a ser utilizado nos emails de texto simples. O código twig pode ser utilizado aqui.', - 'The template to use when a product’s URL is requested.' => 'O template a ser usado quando a URL desse produto for requisitada.', - 'The total number of order adjustments changed.' => 'O número total de ajustes no pedido mudou.', - 'The total price of the order changed.' => 'O preço total do pedido mudou.', - 'The total quantity of items within the order changed.' => 'A quantidade total de itens no pedido mudou.', - 'The unique SKU of the donation purchasable.' => 'O SKU único da doação para compra.', - 'The unit of measurement that should be used when specifying product dimensions.' => 'A unidade de medida que deve ser usada ao especificar as dimensões do produto.', - 'The unit of measurement that should be used when specifying product weights.' => 'A unidade de medida que deve ser usada para especificar o peso do produto.', - 'The webhook URL for this gateway.' => 'O webhook URL para este gateway.', - 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'O nome “de” que será usado quando forem enviados e-mails de status de pedido. Deixe em branco para usar o Nome do Remetente definido nas Configurações Gerais do Craft.', - 'There are errors on the order' => 'Há erros no pedido', - 'There are only {num} “{description}” items left in stock.' => 'Existem apenas {num} “{description}” itens em stock.', - 'There aren’t any product types to select yet.' => 'Ainda não há nenhum tipo de produto para selecionar.', - 'There is no gateway or payment source available for use with this order.' => 'Não há gateway ou fonte de pagamento disponível para usar neste pedido.', - 'There is no gateway selected that supports payment sources.' => 'Não foi selecionado um gateway que suporte as fontes de pagamento.', - 'There is no shipping method selected for this order.' => 'Não foi selecionado um método de envio para este pedido.', - 'This URL will load the cart into the user’s session, making it the active cart.' => 'Este URL irá carregar o carrinho para a sessão do utilizador, transformando-o no carrinho ativo.', - 'This action is not allowed for the current user.' => 'Essa ação não é permitida ao utilizador atual.', - 'This category will be used as the default for all purchasables in this store.' => 'Esta categoria será utilizada como padrão para todas as compras nesta loja.', - 'This coupon is for registered users and limited to {limit} uses.' => 'Este cupão é para utilizadores registados e está limitado a {limit} utilizações.', - 'This coupon is limited to {limit} uses.' => 'Este cupão está limitado a {limit} utilizações.', - 'This coupon requires an email address.' => 'Este cupão requer um endereço de e-mail.', - 'This gateway does not support that functionality.' => 'Este gateway não suporta essa funcionalidade.', - 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Isto está a ser sobreposto pela definição de configuração {setting} em `config/{file}.php`.', - 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Este é o endereço da localização da sua loja. Pode ser utilizado por vários plugins para determinar aspetos como o envio e os impostos. Também pode ser utilizado em recibos PDF.', - 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Este é o PDF padrão que será renderizado ao pedir o PDF do pedido.', - 'This is the last location for the {store} store.' => 'Esta é a última localização da loja {store}.', - 'This month' => 'Este mês', - 'This order has unsaved changes.' => 'Este pedido tem alterações não guardadas.', - 'This week' => 'Esta semana', - 'This year' => 'Este ano', - 'Times Used' => 'Vezes Usado', - 'Title' => 'Título', - 'To' => 'Para', - 'Today' => 'Hoje', - 'Too many variants for this product.' => 'Demasiadas variantes para este produto.', - 'Top Customers by Average Order' => 'Clientes principais por média de pedido', - 'Top Customers by Total Revenue' => 'Clientes principais por rendimento total', - 'Top Customers' => 'Clientes Principais', - 'Top Product Types by Qty Sold' => 'Principais tipos de produtos por quantidade vendida', - 'Top Product Types by Revenue' => 'Principais tipos de produtos por receita', - 'Top Product Types' => 'Principais Tipos de Produtos', - 'Top Products by Qty Sold' => 'Principais produtos por quantidade vendida', - 'Top Products by Revenue' => 'Principais produtos por receita', - 'Top Products' => 'Produtos Principais', - 'Top Purchasables by Qty Sold' => 'Principais produtos para compra por quantidade vendida', - 'Top Purchasables by Revenue' => 'Principais produtos para compra por receita', - 'Top Purchasables' => 'Principais produtos para compra', - 'Total ' => 'Total ', - 'Total Discount Use Limit' => 'Limite Total de Uso do desconto', - 'Total Discount' => 'Desconto Total', - 'Total Included Tax' => 'Imposto total incluído', - 'Total Orders by Billing Country' => 'Total de pedidos por país de faturação', - 'Total Orders by Country' => 'Total de pedidos por país', - 'Total Orders by Shipping Country' => 'Total de pedidos por país de envio', - 'Total Orders' => 'Encomendas totais', - 'Total Paid' => 'Total Pago', - 'Total Price' => 'Preço Total', - 'Total Qty' => 'Quantidade total', - 'Total Revenue' => 'Total de receitas', - 'Total Shipping' => 'Total de Envio', - 'Total Tax' => 'Imposto Total', - 'Total Weight' => 'Peso total', - 'Total' => 'Total', - 'Track Inventory' => 'Monitorizar inventário', - 'Transaction Hash' => 'Hash da transação', - 'Transaction ID' => 'ID da transação', - 'Transaction captured successfully: {message}' => 'Transação realizada com sucesso: {message}', - 'Transaction refunded successfully: {message}' => 'Transação reembolsada com sucesso: {message}', - 'Transactions' => 'Transações', - 'Transfer Fields' => 'Campos de transferência', - 'Transfer Items' => 'Itens da transferência', - 'Transfer Settings' => 'Definições da transferência', - 'Transfer Status' => 'Estado da transferência', - 'Transfer fields saved.' => 'Campos de transferência guardados.', - 'Transfer must have at least one item.' => 'A transferência deve ter pelo menos um item.', - 'Transfer' => 'Transferência', - 'Transfers' => 'Transferências', - 'Trial days credited' => 'Dias de teste creditados', - 'Trial expiration' => 'Data de validade do teste', - 'Trial expiry date' => 'Data de validade do período experimental', - 'Type not in allowed options.' => 'O tipo não está nas opções permitidas.', - 'Type' => 'Tipo', - 'URI' => 'URI', - 'Unable to cancel subscription at this time.' => 'De momento, não é possível cancelar a subscrição.', - 'Unable to complete order: another request is already in progress.' => 'Não foi possível concluir o pedido: já existe outro pedido em curso.', - 'Unable to find variant.' => 'Não foi possível encontrar a variante.', - 'Unable to generate coupon codes: {message}' => 'Não foi possível gerar códigos de cupão: {message}', - 'Unable to make payment at this time.' => 'De momento, não é possível efetuar o pagamento.', - 'Unable to modify subscription at this time.' => 'De momento, não é possível modificar a subscrição.', - 'Unable to reactivate subscription at this time.' => 'De momento, não é possível reativar a subscrição.', - 'Unable to reassign orders.' => 'Não é possível reatribuir pedidos.', - 'Unable to remove order data.' => 'Não foi possível eliminar os dados do pedido.', - 'Unable to retrieve Sale and Purchasable.' => 'Não foi possível encontrar a Oferta e o Produto para compra.', - 'Unable to retrieve cart.' => 'Não foi possível encontrar o carrinho.', - 'Unable to retrieve customer.' => 'Não foi possível encontrar o cliente.', - 'Unable to retrieve load cart URL' => 'Não foi possível carregar o URL do carrinho', - 'Unable to retrieve payment source.' => 'Não foi possível encontrar a fonte de pagamento.', - 'Unable to set default shipping category.' => 'Não foi possível definir a categoria de envio padrão.', - 'Unable to set default tax category.' => 'Não foi possível definir a categoria de taxas padrão.', - 'Unable to set primary payment source.' => 'Não foi possível definir a fonte principal de pagamento.', - 'Unable to start the subscription. Please check your payment details.' => 'Não foi possível iniciar a subscrição. Verifique os seus dados de pagamento.', - 'Unable to subscribe at this time.' => 'De momento, não é possível subscrever.', - 'Unable to update cart.' => 'Não foi possível atualizar o carrinho.', - 'Unable to validate address.' => 'Não foi possível validar a morada.', - 'Unit Price' => 'Preço unitário', - 'Unit price (minus discounts)' => 'Preço unitário (menos descontos)', - 'Units' => 'Unidades', - 'Unpaid' => 'Não pago', - 'Unsubscribe' => 'Anular subscrição', - 'Update Address' => 'Atualizar endereço', - 'Update Order Status' => 'Atualizar o estado da encomenda', - 'Update Order Status…' => 'Atualizar Status do Pedido…', - 'Update order' => 'Atualizar pedido', - 'Update subscription' => 'Atualizar subscrição', - 'Update' => 'Atualizar', - 'Updated By' => 'Atualizado por', - 'Updated committed stock successfully.' => 'Atualizado o stock comprometido com sucesso.', - 'Updated' => 'Atualizado', - 'Use Billing Address For Tax' => 'Utilizar o endereço de faturação para o imposto', - 'Use as the primary billing address' => 'Utilizar como endereço de faturação principal', - 'Use as the primary shipping address' => 'Utilizar como endereço de envio principal', - 'Used By Tax Rates' => 'Usado pelos Tributos', - 'Used by Tax Rates' => 'Usado pelos Tributos', - 'User Groups' => 'Grupos de Usuário', - 'User not found.' => 'O utilizador não foi encontrado.', - 'User' => 'Utilizador', - 'Uses' => 'Utilizações', - 'Validate Business Tax ID as Vat ID' => 'Validar o NIF da empresa como ID do IVA', - 'Validating condition syntax' => 'A validar a sintaxe da condição', - 'Validating formula syntax' => 'A validar a sintaxe da fórmula', - 'Variant Fields' => 'Campos de Variante', - 'Variant Has Untracked Stock' => 'A variante tem stock sem acompanhamento', - 'Variant Price' => 'Preço da variante', - 'Variant SKU' => 'SKU da variante', - 'Variant Search' => 'Pesquisa de variantes', - 'Variant Stock' => 'Stock da variante', - 'Variant Title Format' => 'Formato do Título de Variante', - 'Variant Tracks Stock' => 'Acompanhamento de stock de variantes', - 'Variant UI Label Format' => 'Formato da etiqueta de interface variante', - 'Variant has no product.' => 'A variante não tem produto.', - 'Variants not restored.' => 'Variantes não restauradas.', - 'Variants restored.' => 'Variantes restauradas.', - 'Variants' => 'Variantes', - 'View customer' => 'Ver cliente', - 'View order' => 'Ver pedido', - 'View product type - {productType}' => 'Ver tipo de produto - {productType}', - 'View user' => 'Ver utilizador', - 'View' => 'Ver', - 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Aviso, apagar esta moeda irá parar todos os pagamentos e reembolsos nesta moeda, tem a certeza que quer apagar "{name}"?', - 'Web' => 'Web', - 'Webhook URL' => 'Webhook URL', - 'Weight ({unit})' => 'Peso ({unit})', - 'Weight Rate' => 'Fator Multiplicador por Peso', - 'Weight Unit' => 'Unidade de Peso', - 'Weight' => 'Peso', - 'What product URIs should look like for the site.' => 'Como deverá ser o aspeto dos URI de produtos do site.', - 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Como os títulos de produtos gerados automaticamente devem ser. Pode incluir tags que indiquem propriedades do produto, tais como {ex1} ou {ex2}. Todos os campos personalizados devem ser definidos como obrigatórios.', - 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Como os títulos de variante gerado automaticamente devem ser. Você pode incluir tags que deem como resultado propriedades do variante, como {ex1} ou {ex2}. Todos os campos personalizados devem ser definidos como obrigatórios.', - 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Como os nomes dos ficheiros PDF do pedido devem parecer (sem extensão). Pode incluir tags que emitam propriedades do pedido, como {ex1} ou {ex2}.', - 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Como o SKU único gerado automaticamente deve ser, quando um campo SKU é enviado sem um valor. Você pode incluir tags que deem como resposta propriedades, como {ex1} ou {ex2}', - 'What this PDF will be called in the control panel.' => 'Qual o nome deste PDF no Painel de Controlo.', - 'What this catalog pricing rule will be called in the control panel.' => 'Como será designada esta regra de fixação de preços de catálogo no painel de controlo.', - 'What this discount will be called in the control panel.' => 'Qual o nome deste desconto no Painel de Controlo.', - 'What this email will be called in the control panel.' => 'Nome deste email no Painel de Controlo.', - 'What this product type will be called in the control panel.' => 'Como este tipo de produto será chamado no Painel de Controlo.', - 'What this sale will be called in the control panel.' => 'Qual o nome desta venda no Painel de Controlo.', - 'What this shipping category will be called in the control panel.' => 'Qual o nome desta categoria no Painel de Controlo.', - 'What this shipping rule will be called in the control panel.' => 'Qual o nome desta regra de envio no Painel de Controlo.', - 'What this shipping zone will be called in the control panel.' => 'Qual o nome desta zona de envio no Painel de Controlo.', - 'What this status will be called in the control panel.' => 'Qual o nome deste estado no Painel de Controlo.', - 'What this subscription plan will be called in the control panel.' => 'Qual o nome deste plano de subscrição no Painel de Controlo.', - 'What this tax category will be called in the control panel.' => 'Qual o nome desta categoria de taxa no painel de controlo.', - 'What this tax zone will be called in the control panel.' => 'Qual o nome desta zona de taxa no painel de controlo.', - 'When this discount is applied to an order, which line items should be discounted?' => 'Quando este desconto é aplicado a um pedido, que itens de linha devem ter desconto?', - 'Whether the first available shipping method option should be set automatically on carts.' => 'Se a primeira opção de método de envio disponível deve ser definida automaticamente nos carrinhos.', - 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Se a fonte de pagamento principal do utilizador deve ser definida automaticamente nos novos carrinhos.', - 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Se os endereços principais de envio e faturação do utilizador devem ser definidos automaticamente nos novos carrinhos.', - 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Se esta regra de determinação de preços do catálogo deve estar disponível para utilização, independentemente de outras condições.', - 'Whether this sale should be available for use, regardless of other conditions.' => 'Se a venda deve ou não estar disponível para utilização, independentemente das outras condições.', - 'Which data to display in the name column in the results table.' => 'Qual a data a indicar na coluna de nome na tabela de resultados.', - 'Which product types should this category be available to?' => 'Para que tipos de produto é que esta categoria deve ficar disponível?', - 'Which template should be loaded when a product’s URL is requested.' => 'Que modelo deverá ser carregado quando é solicitado o URL de um produto.', - 'Width ({unit})' => 'Largura ({unit})', - 'Width' => 'Largura', - 'YYYY' => 'AAAA', - 'Yes' => 'Sim', - 'You are not allowed to add a line item.' => 'Não tem permissão para adicionar um item de linha.', - 'You currently have no emails configured to select for this status.' => 'Atualmente não tem emails configurados a selecionar para este estado.', - 'You do not have permission to load this cart.' => 'Não tem permissão para carregar este carrinho.', - 'You must set up at least one gateway that supports subscriptions first.' => 'Primeiro, deve configurar, no mínimo, um gateway que suporte subscrições.', - 'You must be logged in or provide a valid token to load this cart.' => 'Tem de iniciar sessão ou fornecer um token válido para carregar este carrinho.', - 'You must be signed in to create a payment source.' => 'Deve ter sessão iniciada para criar uma fonte de pagamento.', - 'You must be signed in to set a primary payment source.' => 'Deve ter sessão iniciada para definir uma fonte de pagamento principal.', - 'You must make a payment to complete the order.' => 'Tem de fazer o pagamento para concluir o pedido.', - 'Your Cart Recovery Link' => 'Link para recuperar o seu carrinho', - 'Your Order PDF Download Link' => 'O seu link de pedido de transferência de PDF', - 'Your order is empty' => 'O seu pedido está vazio', - 'ZIP file' => 'Ficheiro ZIP', - 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Zero - O preço mínimo é zero caso os descontos sejam maiores do que o valor do pedido.', - 'Zip Code' => 'CEP', - 'all' => 'tudo', - 'any' => 'qualquer', - 'average order total' => 'total médio de pedidos', - 'billing address' => 'endereço de faturação', - 'donation' => 'doação', - 'donations' => 'doações', - 'info' => 'informações', - 'inventory location' => 'localização do inventário', - 'new customers' => 'novos clientes', - 'on hand' => 'disponível', - 'only' => 'apenas', - 'order' => 'pedido', - 'orders' => 'pedidos', - 'price' => 'preço', - 'prices' => 'preços', - 'product variant' => 'variante de produto', - 'product variants' => 'variantes de produto', - 'product' => 'produto', - 'products' => 'produtos', - 'repeat customers' => 'repetir clientes', - 'shipping address' => 'endereço de Envio', - 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'shippingSameAsBilling e billingSameAsShipping não podem ser definidos em simultâneo.', - 'subscription' => 'subscrição', - 'subscriptions' => 'subscrições', - 'to' => 'para', - 'transfer' => 'transferência', - 'transfers' => 'transferências', - '{amount} included' => '{amount} incluído', - '{count} Unfulfilled Orders' => '{count} Ordens não cumpridas', - '{description} is no longer available.' => '{description} já não está disponível.', - '{description} only has {stock} in stock.' => '{description} só tem {stock} em stock.', - '{from} to {to}' => '{from} para {to}', - '{name} (Primary)' => '{name} (Principal)', - '{name} (Trashed)' => '{name} (Movido para a reciclagem)', - '{name} catalog price' => '{name} preço de catálogo', - '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, =1{pedido} other{pedidos}} atualizado(s).', - '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, one {}=1{pedido é} other{pedidos são}} associated with the {numUsers, plural, one {}=1{utilizador} other{utilizadores}}.', - '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, one {}=1{subscrição está} other{subscrições estão}} ativada para {numUsers, plural, one {}=1{utilizador} other{utilizadores}}.', - '{number} more…' => '{number} mais…', - '{pct} off the discounted item price' => '{pct} de desconto no preço do item', - '{pct} off the original item price' => '{pct} de desconto no preço original do item', - '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, one {}=1{não foi atribuído} other{não foram atribuídos}} a um site.', - '{total} in total revenue' => '{total} em receitas totais', - '{total} orders' => '{total} pedidos', - '{total} saleable across {locationCount} location(s)' => '{total} vendável através de {locationCount} local(ais)', - '{uses} uses across {emails} email addresses' => '{uses} utilizações em {emails} endereços de email', - '{uses} uses across {users} users' => '{uses} utilizações em {users} utilizadores', - '“{description}” is currently out of stock.' => '“{description}” está fora de stock de momento.', - '“{key}” has invalid JSON' => '“{key}” tem JSON inválido', -]; diff --git a/src/translations/sk/commerce.php b/src/translations/sk/commerce.php deleted file mode 100644 index a82e9788e2..0000000000 --- a/src/translations/sk/commerce.php +++ /dev/null @@ -1,1428 +0,0 @@ - '(nová cena)', - '(of original price)' => '(z pôvodnej ceny)', - '(off original price)' => '(z pôvodnej ceny)', - 'A cart number must be specified.' => 'Musíte uviesť číslo košíka.', - 'A cart recovery link has been sent to {email}.' => 'Odkaz na obnovenie košíka bol odoslaný na adresu {email}.', - 'A cart recovery link will be sent to {email}.' => 'Odkaz na obnovenie košíka odošleme na adresu {email}.', - 'A friendly reference number will be generated based on this format when a cart is completed and becomes an order. For example {ex1}, or
{ex2}. The result of this format must be unique.' => 'Po naplnení košíka a jeho premene na objednávku sa na základe tohto formátu vytvorí zrozumiteľné referenčné číslo. Napríklad {ex1} alebo
{ex2}. Výsledok tohto formátovania musí byť jedinečný.', - 'A new download link has been sent to {email}' => 'Nový odkaz na stiahnutie bol odoslaný na adresu {email}', - 'A new download link will be sent to {email}' => 'Nový odkaz na stiahnutie bude zaslaný na adresu {email}', - 'A valid email is required to create a customer.' => 'Pre vytvorenie zákazníka sa vyžaduje platná e-mailová adresa.', - 'Accept' => 'Prijať', - 'Accepted' => 'Prijaté', - 'Actions' => 'Akcie', - 'Active Carts' => 'Aktívne Košíky', - 'Active subscriptions' => 'Aktívne prihlásenia na odber', - 'Active' => 'Aktívny', - 'Add Address' => 'Pridať adresu', - 'Add a coupon' => 'Pridať kupón', - 'Add a custom line item' => 'Pridať vlastnú položku', - 'Add a line item' => 'Pridať riadkovú položku', - 'Add a product' => 'Pridať produkt', - 'Add a variant' => 'Pridať variantu', - 'Add an adjustment' => 'Pridať nastavenie', - 'Add an item' => 'Pridať položku', - 'Add an option' => 'Pridať možnosť', - 'Add catalog price' => 'Pridať katalógovú cenu', - 'Add' => 'Pridať', - 'Additional Actions' => 'Dodatočné úkony', - 'Additional recipients that should receive this email. Twig code can be used here.' => 'Dodatoční príjemcovia, ktorí by mali obdržať tento e-mail. Môže byť použitý Twig kód.', - 'Address 1' => 'Adresa 1', - 'Address 2' => 'Adresa 2', - 'Address 3' => 'Adresa 3', - 'Address Line 1' => 'Adresa, riadok 1', - 'Address Line 2' => 'Adresa, riadok 2', - 'Address Updated.' => 'Adresa aktualizovaná.', - 'Address copied to user.' => 'Adresa skopírovaná pre používateľa.', - 'Address not found.' => 'Adresa nebola nájdená.', - 'Adjust Quantity' => 'Upraviť množstvo', - 'Adjust by' => 'Upraviť podľa', - 'Adjust price when included rate is disqualified?' => 'Upraviť cenu, keď je zahrnutá sadzba diskvalifikovaná?', - 'Adjustments' => 'Nastavenia', - 'Admin Notices' => 'Oznámenia správcu', - 'Administrative Area Code of Origin' => 'Kód administratívnej oblasti pôvodu', - 'Advanced' => 'Pokročilé', - 'All Orders' => 'Všetky objednávky', - 'All Totals' => 'Všetko celkom', - 'All Transfers' => 'Všetky prevody', - 'All active subscriptions' => 'Všetky aktívne prihlásenia na odber', - 'All customers' => 'Všetci zákazníci', - 'All products' => 'Všetky produkty', - 'All variants must have a SKU.' => 'Všetky varianty musia mať SKU.', - 'All' => 'Všetky', - 'Allow Checkout Without Payment' => 'Povoliť kontrolu bez platby', - 'Allow Empty Cart On Checkout' => 'Povoliť prázdny košík pri pokladni', - 'Allow Partial Payment On Checkout' => 'Povoliť čiastočnú platbu pri pokladni', - 'Allow out of stock purchases' => 'Umožniť nákupy tovaru, ktorý nie je na sklade', - 'Allow' => 'Povoliť', - 'Allowed Qty' => 'Povolené Množstvo', - 'Alternative Phone' => 'Alternatívne telefónne číslo', - 'Amount' => 'Množstvo', - 'An ID must be provided' => 'Musí byť poskytnutá identifikácia', - 'An error occurred while generating this PDF.' => 'Pri vytváraní tohto súboru PDF sa vyskytla chyba.', - 'Any' => 'Akékoľvek', - 'Anywhere' => 'Kdekoľvek', - 'Are you sure you want to archive the “{name}” subscription plan? It WILL NOT cancel the existing subscriptions.' => 'Naozaj chcete archivovať plán prihlásení na odber {name}? Táto akcia nezruší existujúce prihlásenia na odber.', - 'Are you sure you want to capture this transaction?' => 'Určite zachytiť túto transakciu?', - 'Are you sure you want to complete this order?' => 'Naozaj chcete dokončiť túto objednávku?', - 'Are you sure you want to delete the selected orders?' => 'Určite zmazať vybrané objednávky?', - 'Are you sure you want to delete the selected product and its variants?' => 'Ste si istí, že chcete vymazať vybraný výrobok a jeho varianty?', - 'Are you sure you want to delete this shipping rule?' => 'Naozaj chcete odstrániť toto pravidlo dopravy?', - 'Are you sure you want to delete “{name}” and all its products? Please make sure you have a backup of your database before performing this destructive action.' => 'Určite zmazať „{name}“ a všetky prislúchajúce produkty? Uisti sa prosím, že je spravená záloha databázy pred vykonaním tejto deštruktívnej akcie.', - 'Are you sure you want to delete “{name}”, this will set all line items with this status to no status?' => 'Určite zmazať „{name}“? Všetky riadkové položky s týmto stavom budú zmenené na „žiadny stav“.', - 'Are you sure you want to mark this transfer as pending? This will show as incoming at the destination.' => 'Naozaj chcete tento prevod označiť ako čakajúci? V cieli sa zobrazí ako prichádzajúci.', - 'Are you sure you want to overwrite the billing address?' => 'Naozaj chcete prepísať fakturačnú adresu?', - 'Are you sure you want to overwrite the shipping address?' => 'Naozaj chcete prepísať dodaciu adresu?', - 'Are you sure you want to permanently delete this store and everything in it?' => 'Naozaj chcete natrvalo odstrániť tento obchod a všetko, čo obsahuje?', - 'Are you sure you want to refund this transaction?' => 'Určite vrátiť túto transakciu?', - 'Are you sure you want to remove this customer?' => 'Naozaj chcete odstrániť tohto zákazníka?', - 'Are you sure you want to save this as a new shipping rule?' => 'Naozaj chcete uložiť ako nové pravidlo dopravy?', - 'Are you sure you want to send email: {name}?' => 'Naozaj chcete poslať e-mail: {name}?', - 'At least one site must be enabled for the product type.' => 'Pre daný typ produktu musí byť povolená aspoň jedna stránka.', - 'Attempted Payments' => 'Pokusy o platbu', - 'Attention' => 'Pozor', - 'Authorize Only (Manually Capture)' => 'Len autorizovať (ručne zachytiť)', - 'Auto Set Cart Shipping Method Option' => 'Automatické nastavenie spôsobu dopravy v košíku', - 'Auto Set New Cart Addresses' => 'Automatické nastavenie nových adries košíka', - 'Auto Set Payment Source' => 'Automatické nastavenie zdroja platby', - 'Automatic SKU Format' => 'Automatický SKU Formát', - 'Available Shipping Categories' => 'Dostupné kategórie dopravy', - 'Available Tax Categories' => 'Dostupné kategórie daní', - 'Available for purchase' => 'Dostupné na nákup', - 'Available for purchase?' => 'Dostupné na nákup?', - 'Available inventory for "{description}" has gone below zero.' => 'Dostupné zásoby pre položky „{description}“ klesli pod nulu.', - 'Available to Product Types' => 'Dostupné pre typy produktov', - 'Available' => 'Dostupné', - 'Available?' => 'Dostupné?', - 'Average Order Total' => 'Priemerná cena objednávky', - 'Average' => 'Priemerný', - 'BCC’d Recipient' => 'Príjemca Skrytej kópie (BCC)', - 'Bad Request' => 'Zlá požiadavka', - 'Bad address ID.' => 'Zlé ID adresy.', - 'Bad order ID.' => 'Zlé ID objednávky.', - 'Base Price' => 'Základná cena', - 'Base Promotional Price' => 'Základná propagačná cena', - 'Base Rate' => 'Základná Sadzba', - 'Base' => 'Základ', - 'Bcc' => 'Skrytá kópia (Bcc)', - 'Billing Address' => 'Fakturačná adresa', - 'Billing Business Name' => 'Fakturačný obchodný názov', - 'Billing First Name' => 'Meno na fakturácii', - 'Billing Full Name' => 'Celý fakturačný názov', - 'Billing Last Name' => 'Priezvisko na fakturácii', - 'Billing address required.' => 'Požaduje sa fakturačná adresa.', - 'Billing detail update URL' => 'URL pre aktualizáciu platobných údajov', - 'Billing issues' => 'Problémy s platbou', - 'Billing' => 'Účtovanie', - 'Both (Line item price + Line item shipping costs)' => 'Obidve (cena riadkovej položky + náklady na dodanie riadkovej položky)', - 'Business ID' => 'IČO', - 'Business Name' => 'Obchodné Meno', - 'Business Tax ID' => 'DIČ', - 'CC’d Recipient' => 'Príjemca kópie (CC)', - 'CVV' => 'CVV', - 'Can be used as an internal reference.' => 'Je možné použiť ako internú referenciu.', - 'Can not complete payment for missing transaction.' => 'Nemožno dokončiť platbu pre chýbajúcu transakciu.', - 'Can not create a new order' => 'Nepodarilo sa vytvoriť novú objednávku', - 'Can not find an order to pay.' => 'Nie je možné nájsť objednávku na zaplatenie.', - 'Can not find enabled email.' => 'Nebolo možné nájsť povolený e-mail.', - 'Can not find order' => 'Objednávku nebolo možné nájsť', - 'Can not find order.' => 'Objednávku nebolo možné nájsť.', - 'Can not find the transaction to refund' => 'Transakcia na refundáciu sa nenašla', - 'Can not move between these inventory types.' => 'Medzi týmito typmi zásob sa nedá pohybovať.', - 'Can not refund amount greater than the remaining amount' => 'Nie je možné refundovať vyššiu sumu ako je zvyšná suma', - 'Cancel subscription' => 'Zrušiť prihlásenie na odber', - 'Cancel with gateway now' => 'Zrušiť cez platobnú bránu teraz', - 'Cancel' => 'Zrušiť', - 'Cancellation date' => 'Dátum zrušenia', - 'Cancellation' => 'Zrušenie', - 'Cannot switch plans for this subscription.' => 'Nie je možné prepnúť plány pre toto prihlásenie na odber.', - 'Can’t preview this email.' => 'Nie je možné zobraziť náhľad tohto e-mailu.', - 'Capture payment' => 'Zachytiť platbu', - 'Capture' => 'Zachytiť', - 'Card Holder' => 'Držiteľ karty', - 'Card Number' => 'Číslo karty', - 'Card' => 'Karta', - 'Cart Recovery Link' => 'Odkaz na obnovenie košíka', - 'Cart forgotten.' => 'Zabudnutý košík.', - 'Cart updated.' => 'Košík je aktualizovaný.', - 'Cart {number}' => 'Košík {number}', - 'Catalog Pricing Rule' => 'Pravidlo stanovovania cien podľa katalógu', - 'Catalog pricing rule description.' => 'Popis pravidla stanovovania cien podľa katalógu.', - 'Catalog pricing rule saved.' => 'Pravidlo stanovovania cien podľa katalógu bolo uložené.', - 'Catalog pricing rules deleted.' => 'Pravidlá stanovovania cien podľa katalógu boli odstránené.', - 'Catalog pricing rules updated.' => 'Pravidlá stanovovania cien podľa katalógu boli aktualizované.', - 'Categories Relationship Type' => 'Kategórie typov vzťahov', - 'Categories' => 'Kategórie', - 'Category Rate Overrides' => 'Nahradenia sadzieb kategórií', - 'Centimeters (cm)' => 'Centimetre (cm)', - 'Changing this value may affect your ability to refund existing transactions.' => 'Zmena tejto hodnoty môže ovplyvniť možnosť refundácie existujúcich transakcií.', - 'Choose a color to represent the order’s status' => 'Vyberte farbu, ktorá má predstavovať stav objednávky', - 'Choose a new customer' => 'Vyberte nového zákazníka', - 'Choose adjustment values to include when calculating the product revenue total.' => 'Vyberte hodnoty úprav, ktoré sa majú zahrnúť pri výpočte celkových príjmov z produktu.', - 'Choose the currency’s ISO code.' => 'Vyberte k mene kód ISO.', - 'Choose the destination inventory location for the existing on hand stock.' => 'Vyberte cieľové miesto zásob pre existujúce zásoby na sklade.', - 'Choose which sites this product type should be available in, and configure the site-specific settings.' => 'Vyberte, na ktorých weboch by mala byť tento typ produktu k dispozícii, a nakonfigurujte nastavenia pre konkrétne weby.', - 'City' => 'Mesto', - 'Clear counter' => 'Vynulovať počítadlo', - 'Clear notices' => 'Vymazať upozornenia', - 'Close' => 'Zavrieť', - 'Code' => 'Kód', - 'Collated PDF' => 'Zosumarizované PDF', - 'Color' => 'Farba', - 'Commerce Products' => 'Commerce produkty', - 'Commerce Settings' => 'Commerce Nastavenia', - 'Commerce Variants' => 'Varianty systému Commerce', - 'Commerce email “{email}” could not be sent for order “{order}”.' => 'E-mail systému Commerce „{email}“ pre objednávku „{order}“ sa nedá odoslať.', - 'Commerce order exports' => 'Exporty objednávok systému Commerce', - 'Commerce' => 'Commerce', - 'Committed' => 'Odovzdané', - 'Completed Email' => 'Vyplnený e-mail', - 'Completed' => 'Dokončené', - 'Completing order failed.' => 'Nepodarilo sa dokončiť objednávku.', - 'Condition' => 'Stav', - 'Conditions here are matched against an order before looking through the rules. This is useful if you want to qualify a method’s availability early, or if there are common conditions to all rules for this method.' => 'Podmienky sa tu porovnávajú s príkazom pred vyhľadaním pravidiel. To je užitočné, ak chcete predčasne overiť dostupnosť spôsobu alebo ak existujú spoločné podmienky pre všetky pravidlá pre tento spôsob.', - 'Conditions here are matched against the order’s customer before looking through the rules. This is useful if you want qualify a method’s availability early or if there are common conditions to all rules for this method.' => 'Podmienky sa tu porovnávajú so zákazníkom objednávky pred vyhľadaním pravidiel. To je užitočné, ak chcete predčasne overiť dostupnosť metódy alebo ak existujú spoločné podmienky pre všetky pravidlá pre túto metódu.', - 'Conditions' => 'Podmienky', - 'Contains Purchasables' => 'Obsahuje položky na predaj', - 'Control Panel Settings' => 'Nastavenia ovládacieho panela', - 'Control panel' => 'Ovládací panel', - 'Conversion Rate' => 'Konverzný kurz', - 'Converted Price' => 'Konvertovaná cena', - 'Copied!' => 'Skopírované!', - 'Copy the URL' => 'Kopírovať URL', - 'Copy to {location}' => 'Kopírovať do {location}', - 'Copy' => 'Kopírovať', - 'Costs' => 'Náklady', - 'Could not archive gateway.' => 'Brána sa nedá archivovať.', - 'Could not cancel “{reference}”.' => 'Nebolo možné zrušiť „{reference}“.', - 'Could not create the payment source.' => 'Zdroj platby sa nedá vytvoriť.', - 'Could not delete shipping rule' => 'Pravidlo dodania sa nedá odstrániť', - 'Could not delete shipping zone' => 'Dodacia oblasť sa nedá odstrániť', - 'Could not delete {count, number} shipping {count, plural, one{category} other{categories}}.' => 'Nepodarilo sa vymazať {count, number} {count, plural, one{kategóriu dopravy} few {kategórie dopravy} many {kategórie dopravy} other{kategórií dopravy}}.', - 'Could not delete {count, number} shipping {count, plural, one{method} other{methods}} and rules.' => 'Nepodarilo sa vymazať {count, number} {count, plural, one{spôsob dopravy} few {spôsoby dopravy} many {spôsobu dopravy} other{spôsobov dopravy}} a pravidlá.', - 'Could not delete {count, number} tax {count, plural, one{category} other{categories}}.' => 'Nepodarilo sa vymazať {count, number} {count, plural, one{daňovú kategóriu} few {daňové kategórie} many {daňovej kategórie} other{daňových kategórií}}.', - 'Could not find the email or template.' => 'E-mail alebo šablónu sa nepodarilo nájsť.', - 'Could not mark order {number} as complete. Order save failed during order completion with errors: {order}' => 'Objednávku {number} nebolo možné označiť ako dokončenú. Pri dokončovaní nasledujúcej objednávky zlyhalo jej uloženie s chybami: {order}', - 'Could not reactivate “{reference}”.' => 'Nebolo možné znovu aktivovať „{reference}“.', - 'Could not send email' => 'E-mail nebolo možné odoslať', - 'Could not switch “{reference}” to “{plan}”.' => 'Nebolo možné zmeniť „{reference}“ na „{plan}“.', - 'Could not update orders address.' => 'Adresy objednávok sa nedajú aktualizovať.', - 'Couldn’t archive Line Item Status.' => 'Nebolo možné archivovať stav riadkovej položky.', - 'Couldn’t archive Order Status.' => 'Nebolo možné archivovať stav objednávky.', - 'Couldn’t capture transaction.' => 'Nemožno zachytiť transakciu.', - 'Couldn’t capture transaction: {message}' => 'Nemožno zachytiť transakciu: {message}', - 'Couldn’t delete email.' => 'Nepodarilo sa odstrániť e-mail.', - 'Couldn’t delete the payment source.' => 'Zdroj platby sa nedá odstrániť.', - 'Couldn’t get order.' => 'Nemožno získať objednávku.', - 'Couldn’t recalculate order.' => 'Nemožno prepočítať objednávku.', - 'Couldn’t refund transaction.' => 'Nemožno vrátiť transakciu.', - 'Couldn’t refund transaction: {message}' => 'Transakciu nie je možné vrátiť: {message}', - 'Couldn’t reorder Line Item Statuses.' => 'Nedalo sa zmeniť usporiadanie stavov riadkových položiek.', - 'Couldn’t reorder Order Statuses.' => 'Nedalo sa zmeniť usporiadanie stavov objednávok.', - 'Couldn’t reorder PDFs.' => 'Poradie súborov PDF sa nedá zmeniť.', - 'Couldn’t reorder discounts.' => 'Nedalo sa zmeniť usporiadanie zliav.', - 'Couldn’t reorder gateways.' => 'Poradie brán sa nedá zmeniť.', - 'Couldn’t reorder plans.' => 'Opätovná objednávka plánov sa nepodarila.', - 'Couldn’t reorder rules.' => 'Poradie pravidiel sa nedá zmeniť.', - 'Couldn’t reorder sale.' => 'Poradie výpredaja nebolo možné zmeniť.', - 'Couldn’t reorder sales.' => 'Poradie predajných akcií sa nedá zmeniť.', - 'Couldn’t reorder statuses.' => 'Poradie stavov sa nedá zmeniť.', - 'Couldn’t reorder stores.' => 'Nepodarilo sa zmeniť poradie obchodov.', - 'Couldn’t save PDF.' => 'Nemožno uložiť súbor PDF.', - 'Couldn’t save catalog pricing rule.' => 'Nepodarilo sa uložiť pravidlo stanovovania cien podľa katalógu.', - 'Couldn’t save currency.' => 'Mena sa nedala uložiť.', - 'Couldn’t save discount.' => 'Nemožno uložiť zľavu.', - 'Couldn’t save email.' => 'Nemožno uložiť e-mail.', - 'Couldn’t save gateway.' => 'Brána sa nedá uložiť.', - 'Couldn’t save inventory location.' => 'Nepodarilo sa uložiť umiestnenie zásob.', - 'Couldn’t save line item status.' => 'Nemožno uložiť stav riadkovej položky.', - 'Couldn’t save order fields.' => 'Nemožno uložiť polia objednávky.', - 'Couldn’t save order status.' => 'Nemožno uložiť stav objednávky.', - 'Couldn’t save order.' => 'Nemožno uložiť objednávku.', - 'Couldn’t save product type.' => 'Nemožno uložiť typ produktu.', - 'Couldn’t save sale.' => 'Nemožno uložiť výpredaj.', - 'Couldn’t save settings.' => 'Nemožno uložiť nastavenia.', - 'Couldn’t save shipping category.' => 'Kategória dopravy sa nedala uložiť.', - 'Couldn’t save shipping method.' => 'Nemožno uložiť spôsob dodania.', - 'Couldn’t save shipping rule.' => 'Nemožno uložiť pravidlo dodania.', - 'Couldn’t save shipping zone.' => 'Nie je možné vybrať zónu doručenia.', - 'Couldn’t save store.' => 'Nepodarilo sa uložiť obchod.', - 'Couldn’t save subscription fields.' => 'Polia predplatného sa nepodarilo uložiť.', - 'Couldn’t save subscription plan.' => 'Plán prihlásení na odber sa nedá uložiť.', - 'Couldn’t save subscription.' => 'Predplatné nebolo možné uložiť.', - 'Couldn’t save tax category.' => 'Nemožno uložiť daňovú kategóriu.', - 'Couldn’t save tax rate.' => 'Nemožno uložiť daňovú sadzbu.', - 'Couldn’t save tax zone.' => 'Nemožno uložiť daňovú zónu.', - 'Couldn’t save transfer fields.' => 'Nemožno uložiť polia prevodu.', - 'Couldn’t update catalog pricing rule statuses.' => 'Nepodarilo sa aktualizovať stav pravidla stanovovania cien podľa katalógu.', - 'Couldn’t update status.' => 'Stav sa nepodarilo aktualizovať.', - 'Couldn’t updated sales status.' => 'Nebolo možné aktualizovať stav výpredajov.', - 'Country Code of Origin' => 'Kód krajiny pôvodu', - 'Country List' => 'Zoznam krajín', - 'Country not allowed.' => 'Krajina nie je povolená.', - 'Country' => 'Krajina', - 'Coupon Code' => 'Kupónový Kód', - 'Coupon can not apply discount to this order due to address mismatch.' => 'Kupón nie je možné uplatniť na túto objednávku z dôvodu nesúladu adries.', - 'Coupon can not apply discount to this order due to customer mismatch.' => 'Kupón nie je možné uplatniť na túto objednávku z dôvodu nesúladu zákazníka.', - 'Coupon can not apply discount to this order.' => 'Kupón nie je možné uplatniť na túto objednávku.', - 'Coupon code “{code}” is already in use by discount “{name}”.' => 'Kód kupónu „{code}“ sa už používa pri zľave „{name}“.', - 'Coupon codes cannot be blank.' => 'Kódy kupónov nemôžu byť prázdne.', - 'Coupon codes must be unique.' => 'Kódy kupónov musia byť jedinečné.', - 'Coupon format is required and must contain at least one `#`.' => 'Formát kupónu je povinný a musí obsahovať aspoň jeden znak „#“.', - 'Coupon not valid.' => 'Kupón je neplatný.', - 'Coupon removed: {explanation}' => 'Kupón odstránený: {explanation}', - 'Coupons' => 'Kupóny', - 'Craft Commerce - Administration' => 'Craft Commerce – Správa', - 'Craft Commerce - Inventory' => 'Craft Commerce – Zásoby', - 'Craft Commerce - Orders' => 'Craft Commerce – Objednávky', - 'Craft Commerce - Product Type - {name}' => 'Craft Commerce – Typ výrobku – {name}', - 'Craft Commerce - Subscriptions' => 'Craft Commerce – Predplatné', - 'Create a Discount' => 'Vytvoriť zľavu', - 'Create a Subscription Plan' => 'Vytvorte plán prihlásení na odber', - 'Create a new PDF' => 'Vytvoriť nový súbor PDF', - 'Create a new catalog pricing rule' => 'Vytvoriť nové pravidlo stanovovania cien podľa katalógu', - 'Create a new currency' => 'Vytvoriť novú menu', - 'Create a new email' => 'Vytvoriť nový e-mail', - 'Create a new gateway' => 'Vytvoriť novú bránu', - 'Create a new line item status' => 'Vytvoriť nový stav riadkovej položky', - 'Create a new order status' => 'Vytvoriť nový stav objednávky', - 'Create a new product type' => 'Vytvoriť nový typ produktu', - 'Create a new sale' => 'Vytvoriť nový výpredaj', - 'Create a new shipping category' => 'Vytvoriť novú kategóriu dopravy', - 'Create a new shipping method' => 'Vytvoriť nový spôsob dodania', - 'Create a new shipping rule' => 'Vytvoriť nové pravidlo dodania', - 'Create a new tax category' => 'Vytvoriť novú daňovú kategóriu', - 'Create a new tax rate' => 'Vytvoriť novú daňovú sadzbu', - 'Create a product type' => 'Vytvoriť typ produktu', - 'Create a shipping zone' => 'Vytvoriť zónu doručenia', - 'Create a tax zone' => 'Vytvoriť daňovú zónu', - 'Create catalog pricing rules' => 'Vytvoriť pravidlá stanovovania cien podľa katalógu', - 'Create customer: “{email}”' => 'Vytvoriť zákazníka: „{email}“', - 'Create discounts' => 'Vytvoriť zľavy', - 'Create discount…' => 'Vytvoriť zľavu…', - 'Create rules that allow this discount to match the order.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto zľava zodpovedala objednávke.', - 'Create rules that allow this discount to match the order’s billing address.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto zľava zodpovedala fakturačnej adrese objednávky.', - 'Create rules that allow this discount to match the order’s customer.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto zľava zodpovedala zákazníkovi objednávky.', - 'Create rules that allow this discount to match the order’s shipping address.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto zľava zodpovedala dodacej adrese objednávky.', - 'Create rules that allow this gateway to match the billing address.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto brána zodpovedala fakturačnej adrese.', - 'Create rules that allow this gateway to match the order.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto platobná brána zodpovedala objednávke.', - 'Create rules that allow this gateway to match the shipping address.' => 'Vytvorte pravidlá, ktoré umožnia, aby táto brána zodpovedala dodacej adrese.', - 'Create sales' => 'Vytvoriť výpredaj', - 'Create sale…' => 'Vytvoriť výpredaj…', - 'Created' => 'Vytvorené', - 'Credit Card Payment Type' => 'Typ Platby Kreditnou Kartou', - 'Currency Code' => 'Kód meny', - 'Currency saved.' => 'Mena sa uložila.', - 'Currency' => 'Mena', - 'Current' => 'Súčasný', - 'Custom 1' => 'Vlastný 1', - 'Custom 2' => 'Vlastný 2', - 'Custom 3' => 'Vlastný 3', - 'Custom 4' => 'Vlastný 4', - 'Custom' => 'Vlastný', - 'Customer Enabled?' => 'Povolené pre zákazníkov?', - 'Customer ID is required.' => 'Vyžaduje sa ID zákazníka.', - 'Customer Note' => 'Poznámka zákazníka', - 'Customer Notices' => 'Zákaznícke upozornenia', - 'Customer data' => 'Údaje o zákazníkoch', - 'Customer' => 'Zákazník', - 'Damaged' => 'Poškodené', - 'Data shown might be outdated.' => 'Uvedené údaje môžu byť zastarané.', - 'Date Authorized' => 'Dátum schválenia', - 'Date Created' => 'Dátum Vytvorenia', - 'Date First Paid' => 'Dátum prvej platby', - 'Date Ordered' => 'Dátum Objednania', - 'Date Paid' => 'Dátum Úhrady', - 'Date Updated' => 'Dátum Aktualizácie', - 'Date from which the catalog pricing rule will be active. Leave blank for unlimited start date' => 'Dátum, od ktorého bude pravidlo stanovovania cien podľa katalógu aktívne. Prázdne pre neobmedzený dátum začiatku', - 'Date from which the discount will be active. Leave blank for unlimited start date' => 'Dátum, od ktorého bude zľava aktívna. Prázdne pre neobmedzený dátum začiatku', - 'Date from which the sale will be active. Leave blank for unlimited start date' => 'Dátum, od ktorého bude výpredaj aktívny. Prázdne pre neobmedzený dátum začiatku', - 'Date when the catalog pricing rule will be finished. Leave blank for unlimited end date' => 'Dátum, od ktorého bude pravidlo stanovovania cien podľa katalógu ukončené. Prázdne pre neobmedzený dátum začiatku', - 'Date when the discount will be finished. Leave blank for unlimited end date' => 'Dátum, kedy bude zľava ukončená. Prázdne pre neobmedzený dátum ukončenia', - 'Date when the sale will be finished. Leave blank for unlimited end date' => 'Dátum, kedy bude výpredaj ukončený. Prázdne pre neobmedzený dátum ukončenia', - 'Date' => 'Dátum', - 'Default - Allow the price to be negative if discounts are greater than the order value.' => 'Predvolené – Povoliť zápornú cenu v prípade, že zľavy sú vyššie ako hodnota objednávky.', - 'Default Category' => 'Predvolená kategória', - 'Default Commerce control panel view. If the user does not have permission it will fall back to a location they can access.' => 'Predvolené zobrazenie ovládacieho panela Commerce. Ak používateľ nemá oprávnenie, vráti sa na miesto, ku ktorému má prístup.', - 'Default Order PDF' => 'Predvolená objednávka PDF', - 'Default Per Item Rate' => 'Predvolená sadzba na položku', - 'Default Percentage Rate' => 'Predvolená percentuálna sadzba', - 'Default Status?' => 'Predvolený Stav?', - 'Default View' => 'Predvolené zobrazenie', - 'Default Weight Rate' => 'Predvolená váhová sadzba', - 'Default Zone' => 'Predvolená Zóna', - 'Default status?' => 'Predvolený stav?', - 'Default to this tax zone when no billing address is set' => 'Nastaviť predvolenú hodnotu na túto daňovú oblasť, ak nie je nastavená fakturačná adresa', - 'Default to this tax zone when no shipping address is set' => 'Predvoliť túto daňovú zónu, ak nie je nastavená žiadna dodacia adresa', - 'Default variant updated.' => 'Predvolený variant aktualizovaný.', - 'Default' => 'Predvolené', - 'Default?' => 'Predvolené?', - 'Delete catalog pricing rules' => 'Odstrániť pravidlá stanovovania cien podľa katalógu', - 'Delete discounts' => 'Súvisiace zľavy', - 'Delete orders' => 'Zmazať objednávky', - 'Delete sales' => 'Odstrániť výpredaj', - 'Delete' => 'Zmazať', - 'Deleting the {location} location.' => 'Odstránenie polohy {location}.', - 'Describe this rule.' => 'Popíš toto pravidlo.', - 'Describe this shipping zone.' => 'Popíšte túto zónu doručenia.', - 'Describe this tax zone.' => 'Popis tejto daňovej zóny.', - 'Description' => 'Popis', - 'Destination Inventory Location' => 'Inventarizácia miesta určenia', - 'Destination' => 'Cieľ', - 'Details' => 'Podrobnosti', - 'Dimension Unit' => 'Jednotka Rozmeru', - 'Dimensions' => 'Rozmery', - 'Disabled' => 'Deaktivované', - 'Disallow' => 'Zakázať', - 'Discount all line items' => 'Zľaviť všetky riadkové položky', - 'Discount description.' => 'Popis zľavy.', - 'Discount is not allowed for the order' => 'Na túto objednávku nie je možné uplatniť zľavu', - 'Discount is out of date.' => 'Zľava je premlčaná.', - 'Discount saved.' => 'Zľava uložená.', - 'Discount the matching items only' => 'Zľaviť iba zhodujúce sa položky', - 'Discount use has reached its limit.' => 'Použitie zliav dosiahlo limit.', - 'Discount' => 'Zľava', - 'Discounted Item Subtotal' => 'Medzisúčet zľavnenej položky', - 'Discounted Items' => 'Zľavnené položky', - 'Discounts deleted.' => 'Zľavy odstránené.', - 'Discounts reordered.' => 'Poradie zliav bolo zmenené.', - 'Discounts updated.' => 'Zľavy aktualizované.', - 'Discounts' => 'Zľavy', - 'Disqualify with valid business tax ID?' => 'Diskvalifikovať sa platným daňovým identifikačným číslom?', - 'Do not apply subsequent matching sales beyond applying this sale.' => 'Nepoužívať následné zodpovedajúce predajné akcie nad rámec použitia tejto predajnej akcie.', - 'Do not apply this rate if the order address has any of the selected valid business tax IDs.' => 'Túto sadzbu neuplatňujte, ak má adresa objednávky niektoré z vybraných platných DIČ pre podnikateľov.', - 'Do not attach a PDF to this email' => 'K tomuto e-mailu neprikladajte súbor PDF', - 'Do not call recalculate on the order (Number: {orderNumber}) if errors are present.' => 'V prípade chýb nevolať prepočítanie objednávky (Číslo: {orderNumber}).', - 'Donation can not be zero.' => 'Hodnota daru nemôže byť nula.', - 'Donation needs to be an amount.' => 'Dar musí mať uvedenú hodnotu.', - 'Donation settings saved.' => 'Nastavenia darovania uložené.', - 'Donation' => 'Darovanie', - 'Donations' => 'Dary', - 'Done' => 'Hotovo', - 'Don’t apply any subsequent discounts to an order if this discount is applied' => 'Ak sa uplatňuje táto zľava, neuplatňovať žiadne ďalšie zľavy', - 'Download PDF' => 'Stiahnuť PDF', - 'Download PDF…' => 'Stiahnuť PDF…', - 'Download Type' => 'Stiahnuť typ', - 'Download' => 'Stiahnuť', - 'Draft' => 'Koncept', - 'Dummy gateway payment failed.' => 'Platba cez prázdnu bránu zlyhala.', - 'Duplicate options exist' => 'Existuje duplicitná možnosť', - 'Duration' => 'Trvanie', - 'EU VAT ID' => 'DIČ pre EÚ', - 'Edit address' => 'Upraviť adresu', - 'Edit adjustments' => 'Upraviť nastavenia', - 'Edit catalog pricing rules' => 'Upraviť pravidlá stanovovania cien podľa katalógu', - 'Edit discounts' => 'Upraviť zľavy', - 'Edit options' => 'Upraviť možnosti', - 'Edit orders' => 'Upraviť objednávky', - 'Edit sales' => 'Upraviť výpredaj', - 'Edit' => 'Upraviť', - 'Effect' => 'Efekt', - 'Either (Default) - The relationship field is on the purchasable or the category' => 'Oboje (predvolené) - Pole vzťahov je na položke na predaj alebo kategórii', - 'Either way' => 'Obojsmerne', - 'Email PDF generation error for email “{email}”. Order: “{order}”. PDF Template error: “{message}” {file}:{line}' => 'Chyba generovania PDF e-mailu pre e-mail „{email}“. Objednávka: „{order}“. Chyba šablóny PDF: „{message}“ {file}:{line}', - 'Email PDF template does not exist at “{templatePath}” for email “{email}”. Order: “{order}”.' => 'V ceste „{templatePath}“ neexistuje e-mailová šablóna vo formáte PDF pre e-mail „{email}“. Objednávka: „{order}“.', - 'Email Subject' => 'Predmet Emailu', - 'Email error. No email address found for order. Order: “{order}”' => 'Chyba e-mailu. K objednávke nie je priradená žiadna e-mailová adresa. Objednávka:„{order}“', - 'Email is not enabled.' => 'E-mail nie je povolený.', - 'Email plain text template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'V ceste „{templatePath}“ neexistuje e-mailová šablóna s obyčajným textom. Výsledkom je cesta „{templateParsedPath}“ pre e-mail „{email}“. Objednávka: „{order}“.', - 'Email plain text template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny s obyčajným textom pre e-mail „{email}“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', - 'Email plain text template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny s obyčajným textom pre e-mail „{email}“ v „Cesta šablóny“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', - 'Email required to make payments on a completed order.' => 'Na uskutočnenie platieb na základe vyplnenej objednávky sa požaduje e-mail.', - 'Email saved.' => 'E-mail uložený.', - 'Email sent' => 'E-mail odoslaný', - 'Email template does not exist at “{templatePath}” which resulted in “{templateParsedPath}” for email “{email}”. Order: “{order}”.' => 'V ceste „{templatePath}“ neexistuje e-mailová šablóna. Výsledkom je cesta „{templateParsedPath}“ pre e-mail „{email}“. Objednávka: „{order}“.', - 'Email template parse error for custom email “{email}” in “To:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny pre vlastný e-mail „{email}“ v „Adresát:“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', - 'Email template parse error for email “{email}” in “BCC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny pre e-mail „{email}“ v „Skrytá kópia:“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', - 'Email template parse error for email “{email}” in “CC:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny pre e-mail „{email}“ v „Kópia:“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', - 'Email template parse error for email “{email}” in “ReplyTo:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny pre e-mail „{email}“ v „Odpoveď:“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', - 'Email template parse error for email “{email}” in “Subject:”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny pre e-mail „{email}“ v „Predmet:“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', - 'Email template parse error for email “{email}”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy e-mailovej šablóny pre e-mail „{email}“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', - 'Email template path parse error for email “{email}” in “Template Path”. Order: “{order}”. Template error: “{message}” {file}:{line}' => 'Chyba analýzy cesty e-mailovej šablóny pre e-mail „{email}“ v „Cesta šablóny:“. Objednávka: „{order}“. Chyba šablóny: „{message}“ {file}:{line}', - 'Email unavailable.' => 'E-mail nie je k dispozícii.', - 'Email “{email}” could not be sent for order “{order}”. Error: {error} {file}:{line}' => 'Pre objednávku „{order}“ nebolo možné odoslať e-mail „{email}“. Chyba: {error} {file}:{line}', - 'Email “{email}” for order {order} was cancelled.' => 'E-mail „{email}“ k objednávke „{order}“ bol zrušený.', - 'Email' => 'E-mail', - 'Emails' => 'Emaily', - 'Enable if this rate should be built into the taxable subject price instead of adding a cost to the order.' => 'Povoľte, ak sa má táto sadzba započítať do ceny zdaniteľného predmetu namiesto pridania nákladov k objednávke.', - 'Enable structure for products of this type' => 'Povoľte štruktúru pre produkty tohto typu', - 'Enable this discount' => 'Povoliť túto zľavu', - 'Enable this rule' => 'Povoliť toto pravidlo', - 'Enable this sale' => 'Povoliť tento výpredaj', - 'Enable this shipping method on the front end' => 'Povoliť tento spôsob dodania na front-ende', - 'Enable this shipping rule' => 'Povoliť toto pravidlo dodania', - 'Enable this tax rate' => 'Povoliť túto daňovú sadzbu', - 'Enabled for customers to select during checkout?' => 'Umožniť zákazníkom vybrať počas overovania?', - 'Enabled for customers to select?' => 'Majú zákazníci povolený výber?', - 'Enabled' => 'Povolené', - 'Enabled?' => 'Povolené?', - 'End Date' => 'Dátum ukončenia', - 'Enter SKU' => 'Zadať SKU', - 'Enter a human-friendly name for this tax rate to be used in the control panel.' => 'Zadajte názov tejto daňovej sadzby, ktorý sa bude používať v ovládacom paneli.', - 'Enter a percentage like {ex1} or {ex2}.' => 'Zadajte percentuálnu hodnotu, ako napr. {ex1} alebo {ex2}.', - 'Enter coupon code' => 'Zadajte kód kupónu', - 'Enter reference' => 'Zadajte referenciu', - 'Error refunding transaction: {transactionHash}' => 'Chyba pri refundácii transakcie: {transactionHash}', - 'Every new store must be assigned to at least one site.' => 'Každý nový obchod musí byť priradený aspoň k jednej lokalite.', - 'Everywhere' => 'Všade', - 'Example' => 'Príklad', - 'Exclude this discount for products that are already on promotion' => 'Nepoužiť túto zľavu na výrobky, ktoré sú už v akcii', - 'Expired Link' => 'Neplatný odkaz', - 'Expired' => 'Vypršaný', - 'Expiry Date' => 'Dátum vypršania platnosti', - 'Expiry date' => 'Dátum exspirácie', - 'Expiry' => 'Platnosť', - 'Failed to receive transfer: {error}' => 'Nepodarilo sa prijať prevod: {error}', - 'Failed to send email. Please try again.' => 'Odoslanie e-mailu sa nepodarilo. Skúste to znovu.', - 'Failed to start' => 'Nebolo možné spustiť', - 'Failed to update {num, plural, =1{order status} other{order statuses}}.' => 'Nepodarilo sa aktualizovať {num, plural, one {} few {stavy objednávky} many {stavov objednávky}=1{stav objednávky} other{stavov objednávky}}.', - 'Failed updating order status on {num, plural, =1{order} other{orders}}.' => 'Nepodarilo sa aktualizovať stav pre {num, plural, one {} few {objednávky} many {objednávok}=1{objednávku} other{objednávok}}.', - 'Feet (ft)' => 'Stopy (ft)', - 'Filtering conditions which describe to which orders this rule is applicable to. Write 0 to skip a condition.' => 'Filtrovanie podmienok, ktoré popisujú, na ktoré objednávky sa toto pravidlo vzťahuje. Nula pre preskočenie podmienky.', - 'First Name' => 'Krstné Meno', - 'Flat Amount Off Order' => 'Pevná cena z objednávky', - 'Flat Order Discount Amount Off' => 'Fixná čiastka zľavy objednávky', - 'Free Order Payment Strategy' => 'Platobná stratégia pre objednávku zdarma', - 'Free Shipping' => 'Doprava Zdarma', - 'Free orders are processed by the payment gateway' => 'Objednávky zdarma sú spracované platobnou bránou', - 'Free orders complete immediately' => 'Objednávky zdarma sa dokončia ihneď', - 'Free shipping can only be for whole order or matching items, not both.' => 'Doprava zdarma je dostupná len pre celú objednávku alebo zhodné položky, avšak nie pre oboje.', - 'From Name' => 'Meno Od', - 'Fulfill' => 'Splniť', - 'Fulfilled' => 'Splnené', - 'Fulfillment' => 'Splnenie', - 'Full Name' => 'Celé meno', - 'Gateway Code' => 'Kód brány', - 'Gateway Message' => 'Správa z brány', - 'Gateway Reference' => 'Referencia brány', - 'Gateway Response' => 'Odpoveď brány', - 'Gateway doesn’t support authorize' => 'Brána nepodporuje autorizáciu', - 'Gateway doesn’t support partial refunds.' => 'Brána nepodporuje čiastočné refundácie.', - 'Gateway doesn’t support purchase' => 'Brána nepodporuje nákup', - 'Gateway doesn’t support refunds.' => 'Brána nepodporuje refundácie.', - 'Gateway saved.' => 'Brána bola uložená.', - 'Gateway' => 'Brána', - 'Gateways reordered.' => 'Poradie brán bolo zmenené.', - 'Gateways' => 'Brány', - 'General Settings' => 'Všeobecné Nastavenia', - 'General' => 'Všeobecné', - 'Generate' => 'Generovať', - 'Generated Coupon Format' => 'Formát generovaného kupónu', - 'Grams (g)' => 'Gramy (g)', - 'Groups for which this sale will be applicable to.' => 'Skupiny, na ktoré sa tento výpredaj vzťahuje.', - 'HTML Email Template Path' => 'Cesta HTML Emailovej Šablóny', - 'Handle' => 'Identifikátor', - 'Harmonized System Code' => 'Kód harmonizovaného systému', - 'Has Admin Notices' => 'Obsahuje oznámenia správcu', - 'Has Emails?' => 'Má emaily?', - 'Has Free Shipping' => 'Má dopravu zadarmo', - 'Has Orders' => 'Má objednávky', - 'Has Purchasable' => 'Má položky na predaj', - 'Has Variants?' => 'Má Varianty?', - 'Height ({unit})' => 'Výška ({unit})', - 'Height' => 'Výška', - 'Hide snapshot' => 'Skryť snímku', - 'History' => 'História', - 'How long (in seconds) a PDF download link should remain valid before expiring. Default is 86400 (24 hours).' => 'Ako dlho (v sekundách) má odkaz na stiahnutie PDF zostať platný pred vypršaním platnosti. Predvolená hodnota je 86400 (24 hodín).', - 'How many times one email address is allowed to use this discount. This applies to all previous orders, whether guest or user. Set to zero for unlimited use by guests or users.' => 'Koľkokrát môže byť táto zľava využitá z jednej emailovej adresy. Toto sa týka všetkých predošlých objednávok vykonaných návštevníkmi aj užívateľmi. Pre neobmedzené použitie návštevníkmi alebo užívateľmi nastavte na nula.', - 'How many times one user is allowed to use this discount. If this is set to something besides zero, the discount will only be available to signed in users.' => 'Koľkokrát môže jeden používateľ využiť túto zľavu. Ak je táto hodnota nastavená na inú hodnotu ako nula, zľava bude k dispozícii len prihláseným používateľom.', - 'How many times this discount can be used in total by guests or signed in users. Set zero for unlimited use.' => 'Koľkokrát celkovo môžu túto zľavu využiť hostia alebo prihlásení používatelia. Nastavte nulu pre neobmedzené použitie.', - 'How products should be labeled within the control panel.' => 'Ako by mali byť produkty označené v ovládacom paneli.', - 'How the Purchasables and Categories are related, which determines the matching items. See [Relations Terminology]({link}).' => 'Aký vzťah majú medzi sebou položky na zakúpenie a kategórie. Toto určuje položky zhody. Viď [Terminológia vzťahov]({link}).', - 'How this product will be described on a line item in an order. You can include tags that output properties, such as {ex1} or {ex2}' => 'Ako bude výrobok opísaný v riadkovej položke objednávky. Môžete pridať štítky, ktorých výstupom sú vlastnosti, ako napríklad {ex1} alebo {ex2}', - 'How this shipping method will be referred to in templates and forms.' => 'Ako sa bude na tento spôsob dodania odkazovať v šablónach a formulároch.', - 'How variants should be labeled within the control panel.' => 'Ako by mali byť varianty označené v ovládacom paneli.', - 'How you’ll refer to this PDF in the templates.' => 'Názov, akým budete tento PDF súbor nazývať v šablónach.', - 'How you’ll refer to this product type in the templates.' => 'Ako sa bude na tento typ produktu odkazovať v šablónach.', - 'How you’ll refer to this shipping category in the templates.' => 'Ako sa bude táto kategória dopravy označovať v šablónach.', - 'How you’ll refer to this status in the templates.' => 'Ako sa bude na tento stav odkazovať v šablónach.', - 'How you’ll refer to this subscription plan in the templates.' => 'Spôsob, akým sa v šablónach odkazuje na tento plán prihlásení na odber.', - 'How you’ll refer to this tax category in the templates.' => 'Ako sa bude na túto daňovú kategóriu odkazovať v šablónach.', - 'ID' => 'ID', - 'IP Address' => 'Adresa IP', - 'If disabled, this PDF will not be available or sent with emails.' => 'Ak bude zakázaný, nebude tento PDF súbor k dispozícii a nebude možné ho posielať ako prílohu v e-mailoch.', - 'If disabled, this email will not send.' => 'Ak je táto položka zakázaná, tento e-mail sa neodošle.', - 'If enabled and this rate does not match the order, the rate amount will be removed from the subject price in the cart.' => 'Ak je táto možnosť povolená a táto sadzba nesúhlasí s objednávkou, bude táto zahrnutá suma sadzby odstránená z ceny predmetu v košíku.', - 'If set to Authorize Only, you will need to manually capture payments before the funds will be transferred to your account. The Gateway needs to support the selected option.' => 'Ak je nastavené na Len Autorizovať, bude treba platby ručne zachytiť než budú finančné prostriedky prevedené na účet. Brána musí vybranú voľbu podporovať.', - 'If you select the percentage to be “off the discounted item price”, this will include the “Per Item Amount” as well as any other discounts that applied before this one.' => 'Ak vyberiete percentuálnu sadzbu „zo zľavnenej ceny položky“, bude obsahovať ako „zľavnenú čiastku na položku“, tak aj akékoľvek iné zľavy, ktoré boli použité pred touto.', - 'Ignore Promotions?' => 'Ignorovať akcie?', - 'Ignore previous matching sales if this sale matches.' => 'Ignorovať predchádzajúce zodpovedajúce predajné akcie, ak táto predajná akcia súhlasí.', - 'Ignore promotional prices when this discount is applied to matching line items' => 'Ignorovať akčné ceny keď je táto zľava použitá na zhodujúce sa riadkové položky', - 'Inactive Carts' => 'Neaktívne Košíky', - 'Inches (in)' => 'Palce (in)', - 'Include built-in line item tax.' => 'Zahrnúť zabudovanú daňovú položku.', - 'Include in price?' => 'Zahrnúť do ceny?', - 'Include line item discounts.' => 'Zahrnúť zľavy z položiek.', - 'Include line item shipping costs.' => 'Zahrnúť náklady na dopravu v jednotlivých položkách.', - 'Include separate line item tax.' => 'Zahrnúť samostatnú daňovú položku.', - 'Included in price?' => 'Zahrnuté v cene?', - 'Included' => 'Vrátane', - 'Incoming transfer from Transfer ID: ' => 'Prichádzajúci prevod z Transfer ID: ', - 'Incoming' => 'Prichádzajúce', - 'Info' => 'Informácie', - 'Information linked?' => 'Je informácia prepojená?', - 'Information' => 'Informácia', - 'Invalid JSON' => 'Neplatné JSON', - 'Invalid Order ID' => 'Neplatné ID objednávky', - 'Invalid VAT ID.' => 'Neplatné IČ DPH.', - 'Invalid condition syntax' => 'Neplatná podmienka syntaxe', - 'Invalid email.' => 'Neplatná e-mailová adresa.', - 'Invalid formula syntax' => 'Neplatná syntax vzorca', - 'Invalid gateway: {value}' => 'Neplatná brána: {value}', - 'Invalid inventory movements.' => 'Neplatné pohyby zásob.', - 'Invalid order condition syntax.' => 'Neplatná syntax podmienky objednávky.', - 'Invalid payment or order. Please review.' => 'Neplatná platba alebo objednávka. Skontrolujte, prosím.', - 'Invalid payment source ID: {value}' => 'Neplatné ID zdroja platby: {value}', - 'Invalid store.' => 'Neplatný obchod.', - 'Invalid user.' => 'Neplatný používateľ.', - 'Inventory Item' => 'Položka inventára', - 'Inventory Location' => 'Umiestnenie zásob', - 'Inventory Locations' => 'Umiestnenia zásob', - 'Inventory Tracked' => 'Sledovanie zásob', - 'Inventory Transfers' => 'Prevody zásob', - 'Inventory could not be set.' => 'Zásoby sa nepodarilo nastaviť.', - 'Inventory location has committed stock, the order(s) must first be fulfilled.' => 'Inventárne miesto má viazané zásoby, objednávka musí byť najprv splnená.', - 'Inventory location has incoming stock, the transfer(s) must first be completed.' => 'Inventúrne miesto má zásoby na prijatie, musí sa najprv dokončiť prevod.', - 'Inventory location is already deactivated.' => 'Umiestnenie zásob je už deaktivované.', - 'Inventory location saved.' => 'Uložené umiestnenie zásob.', - 'Inventory locations not saved.' => 'Umiestnenie zásob nebolo uložené.', - 'Inventory movement could not be saved.' => 'Pohyb zásob nebolo možné uložiť.', - 'Inventory movement saved.' => 'Uložený pohyb zásob.', - 'Inventory updated.' => 'Inventár aktualizovaný.', - 'Inventory was not updated.' => 'Inventár nebol aktualizovaný.', - 'Inventory' => 'Inventár', - 'Invoice amount' => 'Suma faktúry', - 'Invoice date' => 'Dátum faktúry', - 'Is Promotable' => 'Je možné propagovať', - 'Is Promotional Price?' => 'Je propagačná cena?', - 'Is Shippable' => 'Je možné odoslať', - 'Is Taxable' => 'Je zdaniteľné', - 'Item Rates' => 'Sadzby za položky', - 'Item Subtotal' => 'Medzisúčet položky', - 'Item Total' => 'Položka celkom', - 'Item' => 'Položka', - 'Items' => 'Položky', - 'Kilograms (kg)' => 'Kilogramy (kg)', - 'Label' => 'Štítok', - 'Landscape' => 'Krajina', - 'Language' => 'Jazyk', - 'Last Name' => 'Priezvisko', - 'Last Updated' => 'Naposledy aktualizované', - 'Leave a category rate override blank to use the rate from above.' => 'Ak chcete použiť sadzbu z vyššie uvedenej kategórie, ponechajte prázdne miesto.', - 'Leave blank for unlimited uses.' => 'Pre neobmedzené použitie nechajte prázdne.', - 'Leave blank if products don’t have URLs' => 'Ak produkty nemajú adresy URL, ponechajte prázdne', - 'Leave gateway subscription as-is' => 'Pnoechať predplatné brány bez zmien', - 'Length ({unit})' => 'Dĺžka ({unit})', - 'Length' => 'Dĺžka', - 'Let each product choose which sites it should be saved to' => 'Nechať každý produkt zvoliť, na ktoré weby sa má uložiť', - 'Limit which orders this discount applies to based on its line items.' => 'Obmedziť, na ktoré objednávky sa táto zľava vzťahuje na základe ich položiek.', - 'Limit which purchasables this sale applies to.' => 'Obmedzte, na ktorý nákupný tovar sa tento predaj vzťahuje.', - 'Limit' => 'Limit', - 'Line Item Statuses' => 'Stavy riadkových položiek', - 'Line Item' => 'Riadková položka', - 'Line Items' => 'Riadkové položky', - 'Line item price (minus discounts)' => 'Cena položky (znížená o zľavy)', - 'Line item shipping cost' => 'Náklady na dodanie riadkovej položky', - 'Line item statuses reordered.' => 'Stavy riadkových položiek boli zmenené.', - 'Link Duration' => 'Trvanie odkazu', - 'Link Sent' => 'Odkaz bol odoslaný', - 'Link to a product' => 'Odkaz na produkt', - 'Link to a variant' => 'Odkaz na variant', - 'Link' => 'Odkaz', - 'Live' => 'Publikované', - 'Location' => 'Poloha', - 'Locations that should be available for previewing products in this product type.' => 'Umiestnenia, ktoré majú byť dostupné pre náhľad produktov v tomto type produktu.', - 'MM' => 'MM', - 'Make a payment' => 'Vykonať platbu', - 'Make this the primary store' => 'Nastaviť ako hlavný obchod', - 'Manage Inventory' => 'Správa zásob', - 'Manage donation settings' => 'Spravovať nastavenia darovania', - 'Manage general store settings' => 'Spravovať všeobecné nastavenia obchodu', - 'Manage inventory locations' => 'Správa skladových miest', - 'Manage inventory stock levels' => 'Správa úrovne skladových zásob', - 'Manage inventory transfers' => 'Spravovať presuny zásob', - 'Manage orders' => 'Správa objednávok', - 'Manage payment currencies' => 'Spravovať platobné meny', - 'Manage promotions' => 'Spravovať akcie', - 'Manage shipping' => 'Správa prepravy', - 'Manage store settings' => 'Spravovať nastavenia obchodu', - 'Manage subscription plans' => 'Spravovať plány predplatného', - 'Manage subscription' => 'Správa prihlásení na odber', - 'Manage subscriptions' => 'Správa prihlásení na odber', - 'Manage taxes' => 'Správa daní', - 'Manage' => 'Spravovať', - 'Mark as Pending' => 'Označiť ako čakajúce', - 'Mark as completed' => 'Označiť ako dokončené', - 'Match Billing Address' => 'Zhoda fakturačnej adresy', - 'Match Customer' => 'Zhoda zákazníka', - 'Match Order' => 'Zhoda objednávky', - 'Match Orders' => 'Zhoda objednávok', - 'Match Product' => 'Zodpovedajúci produkt', - 'Match Purchasable' => 'Zhoda položiek na predaj', - 'Match Shipping Address' => 'Zhoda dodacej adresy', - 'Match Variant' => 'Zodpovedajúci variant', - 'Matching Items' => 'Položky so zhodou', - 'Max Qty' => 'Maximálne množstvo', - 'Max Uses' => 'Maximálny počet použití', - 'Max Variants' => 'Maximálne varianty', - 'Max quantity must greater than min.' => 'Max. množstvo musí byť väčšie ako min.', - 'Maximum Purchase Quantity' => 'Maximálny počet položiek v nákupe', - 'Maximum Total Shipping Cost' => 'Maximálna celková cena dopravy', - 'Maximum allowed quantity' => 'Maximálne povolené množstvo', - 'Maximum number of matching items that can be ordered for this discount to apply. A zero value here will skip this condition.' => 'Maximálny počet zhodných položiek, ktoré treba objednať, aby zľava platila. Pri zadanej nulovej hodnote sa podmienka preskakuje.', - 'Maximum order quantity for this item is {num}.' => 'Maximálne množstvo objednávky tejto položky je {num}.', - 'Message' => 'Správa', - 'Meters (m)' => 'Metre (m)', - 'Millimeters (mm)' => 'Milimetre (mm)', - 'Min Qty' => 'Minimálne množstvo', - 'Min quantity must be less than max.' => 'Min. množstvo musí byť menšie ako max.', - 'Minimum Purchase Quantity' => 'Minimálny počet položiek v nákupe', - 'Minimum Total Price Strategy' => 'Stratégia pre minimálnu celkovú cenu', - 'Minimum Total Shipping Cost' => 'Minimálna celková cena dopravy', - 'Minimum allowed quantity' => 'Minimálne povolené množstvo', - 'Minimum number of matching items that need to be ordered for this discount to apply.' => 'Minimálny počet zhodných položiek, ktoré treba objednať, aby zľava nadobudla platnosť.', - 'Minimum order quantity for this item is {num}.' => 'Minimálne množstvo objednávky tejto položky je {num}.', - 'Missing Gateway' => 'Chýba brána', - 'Missing a default inventory location.' => 'Chýba predvolené umiestnenie inventára.', - 'Move Inventory' => 'Presun inventára', - 'Move To' => 'Presunúť do', - 'Move {qty} from {fromType} to {toType}' => 'Presunúť {qty} z {fromType} do {toType}', - 'Move' => 'Presunúť', - 'Movement from deactivated inventory location' => 'Presun z deaktivovaného inventárneho miesta', - 'Movement' => 'Presun', - 'Must have at least one variant.' => 'Musí mať aspoň jeden variant.', - 'Name Field' => 'Pole s názvom', - 'Name' => 'Meno', - 'New Customer' => 'Nový zákazník', - 'New Customers' => 'Noví zákazníci', - 'New Order' => 'Nová objednávka', - 'New PDF' => 'Nové PDF', - 'New address' => 'Nová adresa', - 'New catalog pricing rule' => 'Nové pravidlo stanovovania cien podľa katalógu', - 'New currency' => 'Nová mena', - 'New discount' => 'Nová zľava', - 'New email' => 'Nový email', - 'New gateway' => 'Nová brána', - 'New line item status' => 'Nový stav riadkovej položky', - 'New line items get this status by default when the order is completed' => 'Keď sa objednávka dokončí, nové riadkové položky budú mať tento stav predvolený', - 'New location' => 'Nové umiestnenie', - 'New order status' => 'Nový stav objednávky', - 'New orders get this status by default' => 'Nové objednávky budú mať tento stav predvolený', - 'New product type' => 'Nový typ produktu', - 'New product' => 'Nový produkt', - 'New product, choose a type' => 'Nový produkt, vyberte typ', - 'New products default to the first tax category available to them. If none are available, this category will be used.' => 'Pre nové produkty bude predvolená prvá dostupná daňová kategória. Ak nebude žiadna dostupná, použije sa táto kategória.', - 'New sale' => 'Nový výpredaj', - 'New shipping category' => 'Nová kategória dopravy', - 'New shipping method' => 'Nový spôsob dodania', - 'New shipping rule' => 'Nové pravidlo dodania', - 'New shipping zone' => 'Nová zóna doručenia', - 'New subscription plan' => 'Nový plán prihlásení na odber', - 'New tax category' => 'Nová daňová kategória', - 'New tax rate' => 'Nová daňová sadzba', - 'New tax zone' => 'Nová daňová zóna', - 'New transfer' => 'Nový prevod', - 'New {productType} product' => 'Nový produkt {productType}', - 'New' => 'Nové', - 'Next payment' => 'Ďalšia platba', - 'No Address' => 'Žiadna adresa', - 'No PDFs exist yet.' => 'Neexistuje zatiaľ žiadne PDF.', - 'No access given to any specific store management features.' => 'Nemáte prístup k žiadnym špecifickým funkciám správy obchodu.', - 'No additional payment currencies exist yet.' => 'Zatiaľ neexistujú žiadne doplnkové platobné meny.', - 'No address' => 'Žiadna adresa', - 'No billing address' => 'Žiadna fakturačná adresa', - 'No catalog pricing rule exists with the ID “{id}”' => 'Žiadne pravidlo stanovovania cien podľa katalógu s ID „{id}“ neexistuje', - 'No catalog pricing rules exist yet.' => 'Zatiaľ neexistujú žiadne pravidlá stanovovania cien podľa katalógu.', - 'No currency exists with the ID “{id}”' => 'Mena s identifikátorom „{id}“ neexistuje', - 'No customer email address exists on this cart.' => 'Pre tento košík chýba e-mailová adresa zákazníka.', - 'No description' => 'Žiadny popis', - 'No discount exists with the ID “{id}”' => 'Žiadna zľava s ID „{id}“ neexistuje', - 'No discounts exist yet.' => 'Žiadne zľavy zatiaľ neexistujú.', - 'No donation amount supplied.' => 'Nebola zadaná žiadna hodnota pre darovanie.', - 'No emails exist yet.' => 'Žiadne emaily zatiaľ neexistujú.', - 'No inventory changes made.' => 'Neboli vykonané žiadne zmeny v inventári.', - 'No inventory found.' => 'Nenašiel sa žiadny inventár.', - 'No inventory movements made.' => 'Nevykonali sa žiadne inventúrne pohyby.', - 'No inventory transactions for this location.' => 'Na tomto mieste sa nevykonávajú žiadne inventúrne operácie.', - 'No new customer selected.' => 'Nebol vybraný žiadny nový zákazník.', - 'No order history exists with the ID “{id}”' => 'Žiadna história objednávky s ID „{id}“ neexistuje', - 'No order status history items will exist until the cart becomes an order.' => 'Kým sa obsah košíku nepremení na objednávku, nebudú v histórii stavov objednávky žiadne položky.', - 'No payment source exists with the ID “{id}”' => 'Neexistuje zdroj platby s ID „{id}“', - 'No private Note.' => 'Žiadna súkromná poznámka.', - 'No product available.' => 'Žiadny produkt k dispozícii.', - 'No product types exist yet.' => 'Žiadne typy produktov zatiaľ neexistujú.', - 'No purchasable available.' => 'Žiadne položky na predaj k dispozícii.', - 'No sale exists with the ID “{id}”' => 'Žiadny výpredaj s ID „{id}“ neexistuje', - 'No sales exist yet.' => 'Žiadne zľavy zatiaľ neexistujú.', - 'No shipping address' => 'Žiadna dodacia adresa', - 'No shipping category exists with the ID “{id}”' => 'Kategória dopravy s identifikátorom „{id}“ neexistuje', - 'No shipping method exists with the ID “{id}”' => 'Žiadny spôsob dodania s ID „{id}“ neexistuje', - 'No shipping rule exists with the ID “{id}”' => 'Žiadne pravidlo dodania s ID „{id}“ neexistuje', - 'No shipping rules exist yet.' => 'Žiadne pravidlá dodania zatiaľ neexistujú.', - 'No shipping zone exists with the ID “{id}”' => 'Neexistuje žiadna zóna doručenia s ID „{id}“', - 'No stats available.' => 'Nie sú k dispozícii žiadne štatistiky.', - 'No subscription plan exists with the ID “{id}”' => 'Neexistuje žiadny plán prihlásení na odber s ID „{id}“', - 'No subscription plans exist yet.' => 'Doposiaľ neexistujú žiadne plány prihlásení na odber.', - 'No tax category exists with the ID “{id}”' => 'Žiadna daňová kategória s ID „{id}“ neexistuje', - 'No tax rate exists with the ID “{id}”' => 'Žiadna daňová sadzba s ID „{id}“ neexistuje', - 'No tax zone exists with the ID “{id}”' => 'Žiadna daňová zóna s ID „{id}“ neexistuje', - 'No transactions exist.' => 'Neexistujú žiadne transakcie.', - 'No user authenticated.' => 'Žiadny overený používateľ.', - 'No' => 'Nie', - 'None on hand' => 'Žiadne nie sú k dispozícii', - 'None' => 'Žiadne', - 'Not a valid address type' => 'Typ adresy nie je platný', - 'Not a valid credit card number.' => 'Toto nie je platné číslo platobnej karty.', - 'Not all SKUs are unique.' => 'Nie všetky jednotky SKU sú jedinečné.', - 'Note' => 'Poznámka', - 'Notes' => 'Poznámky', - 'Number of Coupons' => 'Počet kupónov', - 'Number' => 'Číslo', - 'Of the enabled sites above, which sites should products in this product type be saved to?' => 'Na ktoré z vyššie uvedených povolených webov by sa mali ukladať produkty tohto typu produktu?', - 'On Hand' => 'Dostupné', - 'Only allow this gateway to be used for zero value orders?' => 'Povoliť použitie tejto brány pre objednávky s nulovou hodnotou?', - 'Only match certain purchasables…' => 'Zhodujú sa len niektoré položky na predaj…', - 'Only match purchasables related to…' => 'Zhodujte sa len s položkami na predaj súvisiacimi s…', - 'Only orders with the following order statuses will be included. Leave blank to include all statuses.' => 'Zahrnuté budú len objednávky s nasledujúcimi stavmi objednávok. Ak chcete zahrnúť všetky stavy, nechajte prázdne.', - 'Only save product to the site they were created in' => 'Produkty ukladať len do webov, v ktorých boli vytvorené', - 'Options' => 'Možnosti', - 'Order Condition Formula' => 'Vzorec podmienky objednávky', - 'Order Description Format' => 'Formát opisu objednávky', - 'Order Details' => 'Informácie o objednávke', - 'Order Fields' => 'Polia Objednávky', - 'Order PDF Download Link' => 'Odkaz na stiahnutie PDF objednávky', - 'Order PDF Filename Format' => 'Formát názvu PDF súboru k objednávke', - 'Order Reference Number Format' => 'Formát referenčného čísla objednávky', - 'Order Settings' => 'Nastavenia Objednávky', - 'Order Site' => 'Web objednávok', - 'Order Status description.' => 'Popis stavu výpredaja.', - 'Order Status' => 'Stav Objednávky', - 'Order Statuses' => 'Stavy Objednávok', - 'Order can not be empty.' => 'Objednávka nemôže byť prázdna.', - 'Order count' => 'Počet objednávok', - 'Order customer data removed.' => 'Nariadiť odstránenie údajov o zákazníkoch.', - 'Order deleted.' => 'Objednávka zmazaná.', - 'Order fields saved.' => 'Polia objednávky uložené.', - 'Order not found.' => 'Objednávka sa nenašla.', - 'Order payment balance is {outstandingBalanceAsCurrency}. This is the maximum value that will be charged.' => 'Čiastka platby za objednávku je {outstandingBalanceAsCurrency}. Toto je maximálna hodnota, ktorá bude účtovaná.', - 'Order recalculated.' => 'Objednávka prepočítaná.', - 'Order status saved.' => 'Stav objednávky uložený.', - 'Order statuses reordered.' => 'Poradie stavu objednávok bolo zmenené.', - 'Order total shipping cost' => 'Celkové náklady na dodanie objednávky', - 'Order total taxable price (Line item subtotal + Total discounts + Total shipping)' => 'Celková zdaniteľná cena objednávky (medzisúčet riadkovej položky + suma zliav + celkové náklady na dodanie)', - 'Order' => 'Objednávka', - 'Orders (Legacy)' => 'Objednávky (staršia verzia)', - 'Orders deleted.' => 'Objednávky zmazané.', - 'Orders not restored.' => 'Objednávky neboli obnovené.', - 'Orders restored.' => 'Objednávky obnovené.', - 'Orders' => 'Objednávky', - 'Organization Name' => 'Názov organizácie', - 'Organization Tax ID' => 'Daňové identifikačné číslo organizácie', - 'Origin and destination cannot be the same.' => 'Pôvodné a cieľové miesto nemôžu byť rovnaké.', - 'Origin' => 'Pôvod', - 'Original Price' => 'Pôvodná cena', - 'Original price' => 'Pôvodná cena', - 'Original promotional price' => 'Pôvodná propagačná cena', - 'Other Languages' => 'Ostatné jazyky', - 'Other countries' => 'Ostatné krajiny', - 'Outgoing transfer from Transfer ID: ' => 'Odchádzajúci prevod z Transfer ID: ', - 'Overpaid' => 'Preplatené', - 'Overrides previous?' => 'Nahradiť predchádzajúce?', - 'PDF Attachment' => 'Príloha PDF', - 'PDF Template Path' => 'Cesta k šablóne PDF', - 'PDF saved.' => 'Súbor PDF uložený.', - 'PDF' => 'PDF', - 'PDFs & Emails' => 'PDF súbory a e-maily', - 'PDFs' => 'Súbory PDF', - 'Paid Amount' => 'Zaplatená suma', - 'Paid Status' => 'Stav zaplatenia', - 'Paid' => 'Zaplatené', - 'Paper Orientation' => 'Orientácia papiera', - 'Paper Size' => 'Veľkosť papiera', - 'Partial payment not allowed.' => 'Čiastočné platby nie sú povolené.', - 'Partial' => 'Čiastočný', - 'Past year' => 'Minulý rok', - 'Past {num} days' => 'Posledných {num} dní', - 'Pay {amount} of {currency} on the order.' => 'Zaplaťte čiastku {amount} v {currency} za objednávku.', - 'Pay' => 'Zaplatiť', - 'Payment Amount' => 'Suma platby', - 'Payment Currencies' => 'Platobné meny', - 'Payment Gateway' => 'Platobná brána', - 'Payment Method' => 'Spôsob Platby', - 'Payment error: {message}' => 'Chyba platby: {message}', - 'Payment method issue' => 'Problém so spôsobom platby', - 'Payment source created.' => 'Zdroj platby bol vytvorený.', - 'Payment source deleted.' => 'Zdroj platby bol odstránený.', - 'Payments' => 'Platby', - 'Pending' => 'Nevyriešené', - 'Per Email Address Discount Limit' => 'Obmedzenie počtu zliav na e-mailovú adresu', - 'Per Item Amount Off' => 'Zľavnená čiastka na položku', - 'Per Item Discount' => 'Zľava na položku', - 'Per Item Percentage Off' => 'Percentuálna zľava na položku', - 'Per Item Rate' => 'Sadzba Na Položku', - 'Per User Discount Limit' => 'Obmedzenie počtu zliav na osobu', - 'Percentage Rate' => 'Percentuálna Sadzba', - 'Phone (Alt)' => 'Telefón (alt.)', - 'Phone' => 'Telefón', - 'Pick a plan' => 'Zvoľte plán', - 'Plain Text Email Template Path' => 'Cesta k šablóne pre e-mail s obyčajným textom', - 'Plan' => 'Plán', - 'Plans reordered.' => 'Poradie plánov bolo zmenené.', - 'Portrait' => 'Portrét', - 'Post Date' => 'Dátum Príspevku', - 'Postal Code Formula' => 'Vzorec poštového smerovacieho čísla', - 'Pounds (lb)' => 'Libry (lb)', - 'Preview' => 'Náhľad', - 'Previous Status' => 'Predchádzajúci stav', - 'Price' => 'Cena', - 'Prices' => 'Ceny', - 'Pricing Rules' => 'Pravidlá stanovovania cien', - 'Pricing jobs are currently running.' => 'V súčasnosti prebiehajú cenové úlohy.', - 'Pricing' => 'Ceny', - 'Primary Billing Address' => 'Hlavná fakturačná adresa', - 'Primary Shipping Address' => 'Hlavná dodacia adresa', - 'Primary payment source updated.' => 'Primárny zdroj platby aktualizovaný.', - 'Primary' => 'Hlavný', - 'Private Note' => 'Súkromná poznámka', - 'Product Fields' => 'Polia produktu', - 'Product ID is required.' => 'Vyžaduje sa ID produktu.', - 'Product Template' => 'Šablóna Produktu', - 'Product Title Format' => 'Formát Názvu produktu', - 'Product Type' => 'Typ produktu', - 'Product Types' => 'Typy Produktov', - 'Product URI Format' => 'Formát URI produktu', - 'Product Variant' => 'Variant produktu', - 'Product Variants' => 'Varianty produktu', - 'Product type saved.' => 'Typ produktu uložený.', - 'Product type settings' => 'Nastavenia typu produktu', - 'Product' => 'Produkt', - 'Products and Variants deleted.' => 'Produkty a Varianty odstránené.', - 'Products not restored.' => 'Produkty neboli obnovené.', - 'Products restored.' => 'Produkty obnovené.', - 'Products' => 'Produkty', - 'Promotable' => 'Akciový', - 'Promotable?' => 'Akciový?', - 'Promotional Amount' => 'Propagačná čiastka', - 'Promotional Price' => 'Propagačná cena', - 'Purchasable Categories' => 'Zakúpiteľné kategórie', - 'Purchasable ID and Sale ID are required.' => 'Vyžaduje sa ID položky na predaj a ID zľavy.', - 'Purchasable ID is required.' => 'Vyžaduje sa ID položky na predaj.', - 'Purchasable Type' => 'Zakúpiteľný typ', - 'Purchasable' => 'Na predaj', - 'Purchase (Authorize and Capture Immediately)' => 'Nákup (Okamžitá Autorizácia a Zachytenie)', - 'Purchase Total' => 'Nákup Celkom', - 'Qty' => 'Množstvo', - 'Quality Control' => 'Kontrola kvality', - 'Quantity' => 'Množstvo', - 'Rate' => 'Sadzba', - 'Reassign {numOrders, plural, =1{order} other{orders}}' => 'Znovu prideliť {numOrders, plural, one {} few {objednávky} many {objednávok}=1{objednávku} other{objednávok}}', - 'Recalculate order' => 'Prepočítať objednávku', - 'Receive Inventory' => 'Prijatie zásob', - 'Receive Transfer' => 'Prijatie prevodu', - 'Receive' => 'Prijať', - 'Received' => 'Prijaté', - 'Recent Orders' => 'Posledné objednávky', - 'Recipient' => 'Príjemca', - 'Recover Cart' => 'Obnovenie košíka', - 'Reduce price' => 'Znížiť cenu', - 'Reduce the price by a fixed amount' => 'Znížiť cenu o pevnú sumu', - 'Reduce the price by a percentage of the original price' => 'Znížiť cenu o percentuálnu hodnotu pôvodnej ceny', - 'Reference' => 'Referencia', - 'Refresh payment history' => 'Obnoviť históriu platieb', - 'Refund note' => 'Poznámka k refundácii', - 'Refund payment' => 'Refundácia platby', - 'Refund' => 'Vrátenie peňazí', - 'Reject' => 'Zamietnuť', - 'Rejected' => 'Zamietnuté', - 'Relationship Type' => 'Typ vzťahu', - 'Removable included tax rates are only allowed for the default tax zone.' => 'Odnímateľné zahrnutie daňových sadzieb je povolené iba pre predvolenú daňovú oblasť.', - 'Remove address' => 'Odstrániť adresu', - 'Remove all shipping costs from the order' => 'Odstrániť všetky náklady na dopravu z objednávky', - 'Remove customer association and email from the {numOrders, plural, =1{order} other{orders}}. Optionally select additional customer data to remove below' => 'Odstrániť prepojenie so zákazníkom a e-mailovú adresu z {numOrders, plural, one {} few {objednávok} many {objednávok}=1{objednávky} other{objednávok}}. Podľa potreby nižšie vyberte ďalšie údaje o zákazníkoch, ktoré chcete odstrániť', - 'Remove customer data' => 'Odstrániť údaje o zákazníkoch', - 'Remove from price?' => 'Odstrániť z ceny?', - 'Remove shipping costs for matching items only' => 'Odstrániť náklady za dopravu len pre položky, ktoré sa zhodujú', - 'Remove the included tax when a valid organization tax ID is present?' => 'Odstrániť zahrnutú daň, ak je k dispozícii platné DIČ organizácie?', - 'Remove' => 'Odstrániť', - 'Removed' => 'Odstránené', - 'Repeat Customers' => 'Opakovaní zákazníci', - 'Reply To' => 'Odpovedať', - 'Require Billing Address At Checkout' => 'Vyžadovať fakturačnú adresu pri pokladni', - 'Require Coupon Code' => 'Vyžiadať kód kupónu', - 'Require Shipping Address At Checkout' => 'Vyžadovať adresu prepravy pri pokladni', - 'Require Shipping Method Selection At Checkout' => 'Vyžadovať výber spôsobu dopravy pri pokladni', - 'Require' => 'Vyžadovať', - 'Reserved' => 'Rezervované', - 'Reset usage' => 'Vynulovať počítadlo použitia', - 'Restrict the discount to only those orders where the customer has purchased a minimum total value of matching items.' => 'Obmedziť zľavu len na tie objednávky, kde zákazník nakúpil za minimálnu celkovú hodnotu zodpovedajúcich prvkov.', - 'Revenue Options' => 'Možnosti príjmov', - 'Revenue' => 'Výnos', - 'Rule' => 'Pravidlo', - 'Rules reordered.' => 'Poradie pravidiel upravené.', - 'SKU' => 'SKU', - 'Safety' => 'Bezpečnosť', - 'Sale Price' => 'Výpredajová cena', - 'Sale description.' => 'Popis výpredaja.', - 'Sale reordered.' => 'Poradie výpredaja bolo zmenené.', - 'Sale saved.' => 'Výpredaj uložený.', - 'Sale' => 'Výpredaj', - 'Sales deleted.' => 'Výpredaje odstránené.', - 'Sales updated.' => 'Zľavy aktualizované.', - 'Sales' => 'Výpredaje', - 'Save and continue editing' => 'Uložiť a pokračovať v úpravách', - 'Save and return to all orders' => 'Uložiť a vrátiť sa na všetky objednávky', - 'Save and set rules' => 'Uložiť a nastaviť pravidlá', - 'Save as a new rule' => 'Uložiť ako nové pravidlo', - 'Save product to all sites enabled for this product type' => 'Uložiť produkt na všetky stránky povolené pre tento typ produktu', - 'Save product to other sites in the same site group' => 'Uložiť produkt do iných webov v rovnakej skupine webov', - 'Save product to other sites with the same language' => 'Uložiť produkt do iných webov s rovnakým jazykom', - 'Save' => 'Uložiť', - 'Search customer…' => 'Vyhľadať zákazníka…', - 'Search inventory' => 'Vyhľadávanie v inventári', - 'Search or enter customer email…' => 'Vyhľadajte alebo zadajte e-mail zákazníka…', - 'Search…' => 'Hľadať…', - 'See Orders' => 'Zobraziť objednávky', - 'Select a gateway' => 'Vyberte bránu', - 'Select a tax category.' => 'Vybrať daňovú kategóriu.', - 'Select a tax zone. If empty, this rate will match anywhere.' => 'Vyberte daňovú zónu. Ak bude prázdna, použije sa táto hodnota všade.', - 'Select address' => 'Vybrať adresu', - 'Select an item' => 'Vyberte položku', - 'Select how the catalog pricing rule will be applied to the purchasable(s).' => 'Vyberte spôsob aplikovania pravidla stanovovania cien podľa katalógu na položku(y) na zakúpenie.', - 'Select how the sale will be applied to the purchasable(s).' => 'Vyberte spôsob aplikovania predajnej akcie na položku(y) na zakúpenie.', - 'Select product type' => 'Vyberte typ produktu', - 'Select the emails that will be sent when transitioning to this status.' => 'Vybrať emaily, ktoré budú poslané pri prechode na tento stav.', - 'Select what this rate should be applied to.' => 'Vyberte, na čo sa má táto sadzba uplatňovať.', - 'Send Email' => 'Poslať e-mail', - 'Send to custom recipient' => 'Odoslať vlastnému príjemcovi', - 'Send to the customer' => 'Poslať zákazníkovi', - 'Set Quantity' => 'Nastaviť množstvo', - 'Set default category' => 'Nastaviť predvolenú kategóriu', - 'Set default variant' => 'Nastaviť predvolený variant', - 'Set or Adjust' => 'Nastaviť alebo upraviť', - 'Set price' => 'Nastaviť cenu', - 'Set status' => 'Nastaviť stav', - 'Set the price to a flat amount' => 'Nastaviť cenu na pevnú sumu', - 'Set the price to a percentage of the original price' => 'Nastaviť cenu na percentuálnu hodnotu pôvodnej ceny', - 'Set the sale price to a flat amount' => 'Nastaviť cenu výpredaja na pevnú sumu', - 'Set the sale price to a percentage of the original price' => 'Nastaviť cenu výpredaja na percentuálnu hodnotu pôvodnej ceny', - 'Set to' => 'Nastaviť na', - 'Settings saved.' => 'Nastavenia uložené.', - 'Settings' => 'Nastavenia', - 'Share cart…' => 'Zdieľať košík…', - 'Shipping - Minimum cost is the shipping cost, if the order price is less than the shipping cost.' => 'Doprava – Minimálna cena za dopravu v prípade, že je hodnota objednávky nižšia ako cena za dopravu.', - 'Shipping Address Zone' => 'Zóna dodacej adresy', - 'Shipping Address' => 'Dodacia Adresa', - 'Shipping Business Name' => 'Expedičný obchodný názov', - 'Shipping Categories' => 'Kategórie dopravy', - 'Shipping Category Conditions' => 'Podmienky kategórie dopravy', - 'Shipping Category' => 'Kategória dopravy', - 'Shipping First Name' => 'Meno na spôsobe dopravy', - 'Shipping Full Name' => 'Celý expedičný názov', - 'Shipping Last Name' => 'Priezvisko na spôsobe dopravy', - 'Shipping Method' => 'Spôsob Dodania', - 'Shipping Methods' => 'Spôsoby Dodania', - 'Shipping Rule' => 'Pravidlo dopravy', - 'Shipping Zones' => 'Zóny doručenia', - 'Shipping address required.' => 'Požaduje sa dodacia adresa.', - 'Shipping categories deleted.' => 'Kategórie dopravy boli vymazané.', - 'Shipping category saved.' => 'Kategória dopravy sa uložila.', - 'Shipping category updated.' => 'Kategória dopravy bola aktualizovaná.', - 'Shipping costs added to the order as a whole before percentage, item, and weight rates are applied. Set to zero to disable this rate. The whole rule, including this base rate, will not match and apply if the cart only contains non-shippable items like digital products.' => 'Cena za dopravu pridaná k objednávke ako celok ešte predtým, ako sa použijú percentuálne sadzby, sadzby za položku a hmotnosť. Nastavte na nulu a táto sadzba sa nepoužije. Toto pravidlo, vrátane základnej sadzby, sa nepoužije v prípade, že košík obsahuje položky, ktoré nie je možné dopraviť, napríklad digitálne produkty.', - 'Shipping method saved.' => 'Spôsob dodania uložený.', - 'Shipping methods and rules deleted.' => 'Spôsoby a pravidlá dopravy boli odstránené.', - 'Shipping methods updated.' => 'Spôsob dopravy bol aktualizovaný.', - 'Shipping rule saved.' => 'Pravidlo dodania uložené.', - 'Shipping zone saved.' => 'Zóna doručenia uložená.', - 'Shipping' => 'Dodanie', - 'Short Number' => 'Krátke číslo', - 'Show Chart?' => 'Zobraziť graf?', - 'Show Order Count?' => 'Zobraziť počet objednávok?', - 'Show all prices' => 'Zobraziť všetky ceny', - 'Show archived gateways' => 'Zobraziť archivované brány', - 'Show order count line on chart.' => 'Zobraziť v grafe riadok s počtom objednávok.', - 'Show related sales' => 'Zobraziť súvisiaci výpredaj', - 'Show rule details' => 'Zobraziť podrobnosti pravidla', - 'Show the Dimensions and Weight fields for products of this type' => 'Zobraziť pole Rozmerov a Hmotnosti pre produkty tohto typu', - 'Show the Title field for products' => 'Zobraziť pole Názvu pre produkty', - 'Show the Title field for variants' => 'Zobraziť pole Názvu pre varianty', - 'Signed In' => 'Prihlásený', - 'Site Languages' => 'Jazyky webu', - 'Site store mapping saved.' => 'Mapovanie úložiska je uložené.', - 'Sites' => 'Lokality', - 'Slug' => 'Slug', - 'Snapshot' => 'Snímka', - 'Snapshots' => 'Snímky', - 'Some orders restored.' => 'Niektoré objednávky boli obnovené.', - 'Some products restored.' => 'Niektoré produkty boli obnovené.', - 'Some variants restored.' => 'Niektoré varianty boli obnovené.', - 'Something changed with the order before payment, please review your order and submit payment again.' => 'Pred platbou došlo k zmene v objednávke, skontrolujte, prosím, svoju objednávku a znova vykonajte platbu.', - 'Sorry, no matching options.' => 'Ľutujeme, nenašli sa žiadne zhody.', - 'Source - The purchasable relationship field is on the category' => 'Zdroj – pole nákupného vzťahu sa nachádza v kategórii', - 'Source' => 'Zdroj', - 'Specify a Twig condition that determines whether the discount should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Nastavte podmienku Twig, ktorá určí, či sa má pre danú objednávku uplatniť zľava. (Objednávku je možné volať pomocou premennej `order`.)', - 'Specify a Twig condition that determines whether the shipping rule should apply to a given order. (The order can be referenced via an `order` variable.)' => 'Nastavte podmienku Twig, ktorá určí, či sa má pre danú objednávku uplatniť pravidlo dopravy. (Objednávku je možné volať pomocou premennej `order`.)', - 'Start Date' => 'Dátum Začiatku', - 'State' => 'Štát', - 'Status Email Address' => 'Emailová Adresa pre Stav', - 'Status Emails' => 'Emaily pre Stavy', - 'Status History' => 'História stavov', - 'Status Updated.' => 'Stav bol aktualizovaný.', - 'Status change message' => 'Správa o zmene stavu', - 'Status' => 'Stav', - 'Stock' => 'Sklad', - 'Stops Processing?' => 'Zastaví spracovanie?', - 'Stops subsequent?' => 'Zastaví následné?', - 'Store Location' => 'Umiestnenie obchodu', - 'Store Management' => 'Manažment obchodu', - 'Store Markets' => 'Obchodné trhy', - 'Store Rule' => 'Pravidlo obchodu', - 'Store saved.' => 'Uložený obchod.', - 'Store' => 'Obchod', - 'Stores & Sites' => 'Obchody a lokality', - 'Stores' => 'Obchody', - 'Strategy to apply when an order is free or has a zero balance.' => 'Ktorú stratégiu použiť v prípade, že je objednávka zdarma alebo má nulový zostatok.', - 'Strategy to apply when calculating the minimum order price.' => 'Ktorú stratégiu použiť pri počítaní minimálnej ceny objednávky.', - 'Subject' => 'Predmet', - 'Subscribing user' => 'Používateľ prihlasujúci odber', - 'Subscription Fields' => 'Polia prihlásenia na odber', - 'Subscription Plans' => 'Plány prihlásení na odber', - 'Subscription Settings' => 'Nastavenie predplatného', - 'Subscription cancelled.' => 'Predplatné zrušené.', - 'Subscription date' => 'Dátum prihlásenia na odber', - 'Subscription fields saved.' => 'Polia prihlásenia na odber uložené.', - 'Subscription for {user} to {plan} prevented by a plugin.' => 'Doplnok zabránil prihláseniu na odber plánu {plan} pre používateľa {user}.', - 'Subscription plan saved.' => 'Plán prihlásení na odber bol uložený.', - 'Subscription plan' => 'Plán prihlásení na odber', - 'Subscription plans' => 'Plány prihlásení na odber', - 'Subscription reactivated.' => 'Predplatné opäť aktivované.', - 'Subscription reference' => 'Referenčné číslo prihlásenia na odber', - 'Subscription started.' => 'Predplatné spustené.', - 'Subscription switched.' => 'Predplatné zmenené.', - 'Subscription to “{plan}”' => 'Prihlásenie na odber plánu „{plan}“', - 'Subscription' => 'Prihlásenie na odber', - 'Subscriptions on hold' => 'Predplatné pozastavené', - 'Subscriptions' => 'Prihlásenia na odber', - 'Suppress emails' => 'Odstrániť e-maily', - 'Switch plan' => 'Prepnúť plán', - 'Switch' => 'Prepnúť', - 'System' => 'Systém', - 'Table Columns' => 'Stĺpce tabuľky', - 'Target - The category relationship field is on the purchasable' => 'Cieľ – Pole kategórie vzťahov je na položke na predaj', - 'Tax & Shipping' => 'Daň a doprava', - 'Tax (inc)' => 'Daň (vr.)', - 'Tax Categories' => 'Daňové Kategórie', - 'Tax Category' => 'Daňová Kategória', - 'Tax Rates' => 'Daňové Sadzby', - 'Tax Zone' => 'Daňová Zóna', - 'Tax Zones' => 'Daňové Zóny', - 'Tax categories deleted.' => 'Daňové kategórie boli vymazané.', - 'Tax category saved.' => 'Daňová kategória uložená.', - 'Tax category updated.' => 'Daňová kategória bola aktualizovaná.', - 'Tax rate saved.' => 'Daňová sadzba uložená.', - 'Tax rates updated.' => 'Daňové sadzby boli aktualizované.', - 'Tax zone saved.' => 'Daňová zóna uložená.', - 'Tax' => 'Daň', - 'Taxable Subject' => 'Zdaniteľný Subjekt', - 'Template Path' => 'Cesta Šablóny', - 'That handle is already in use' => 'Tento popisovač sa už používa', - 'That handle is already in use.' => 'Tento popisovač sa už používa.', - 'The PDF to attach to this email.' => 'PDF pre priloženie do e-mailu.', - 'The URL to the page for updating billing details for a subscription, as well as handling 3DS authentication.' => 'Adresa URL stránky na aktualizáciu fakturačných údajov predplatného, ako aj na spracovanie overovania 3DS.', - 'The address provided is outside the store’s market.' => 'Uvedená adresa sa nachádza mimo trhu obchodu.', - 'The amount of discount that is applied to the whole order. This amount is spread across line items in order of highest price to lowest price, until the discount is used up.' => 'Čiastka zľavy, ktorá je použitá na celú objednávku. Táto čiastka sa rozdelí medzi riadkové položky v poradí od najvyššej ceny po najnižšiu, až kým sa nevyužije celá zľava.', - 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'Základná zľava môže zľaviť položky v košíku maximálne na hodnotu nula, až kým nebude celá využitá. Nie je možné dostať objednávku do záporného čísla.', - 'The cart recovery link is invalid. Please request a new one.' => 'Odkaz na obnovenie košíka je neplatný. Požiadajte o nový.', - 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'Konverzný kurz, ktorý sa použije pri konverzii nejakej sumy do tejto meny. Napríklad, ak nejaká položka stojí {amount1}, na základe konverzného kurzu {rate} by v druhej mene stála {amount2}.', - 'The countries that orders are allowed to be placed from.' => 'Krajiny, z ktorých je povolené zadávať objednávky.', - 'The coupon "{code}" has exceeded its usage limit of {limit}.' => 'Kupón „{code}“ prekročil limit použitia {limit}.', - 'The customer for this order has been deleted.' => 'Zákazník, na ktorého sa táto objednávka vzťahuje, bol odstránený.', - 'The default shipping category is automatically available to all product types.' => 'Predvolená kategória dopravy je automaticky dostupná pre všetky typy produktov.', - 'The discount "{name}" has exceeded its total usage limit of {limit}.' => 'Zľava „{name}“ prekročila limit použitia {limit}.', - 'The download link has expired. Please request a new one.' => 'Platnosť odkazu na stiahnutie vypršala. Požiadajte o nový odkaz.', - 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'Emailová adresa, z ktorej sú odosielané emaily o stave objednávky. Prázdne, ak sa má použiť systémová emailová adresa definovaná vo všeobecných nastaveniach Craft-u.', - 'The entry that contains the description for this subscription’s plan.' => 'Záznam, ktorý obsahuje popis tohto plánu prihlásení na odber.', - 'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'Fixná hodnota, ktorá má zľaviť každú položku, t.j. „3“ pre zľavu €3 pre každú položku.', - 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'Formát použitý na generovanie nových kupónov, napr. {example}. Akékoľvek znaky „#“ budú nahradené náhodným písmenom.', - 'The from and to inventory locations must be different.' => 'Miesta inventúry z a do sa musia líšiť.', - 'The inventory locations this store uses.' => 'Inventárne miesta, ktoré tento obchod používa.', - 'The item is not enabled for sale.' => 'Túto položku nie je povolené zahrnúť do predaja.', - 'The language the order was made in.' => 'Jazyk, v ktorom bola objednávka uskutočnená.', - 'The language to be used when this email is rendered.' => 'Jazyk, ktorý sa má použiť pri zobrazení tohto e-mailu.', - 'The maximum number of levels this product type can have. Leave blank if you don’t care.' => 'Maximálny počet úrovní, ktoré môže tento typ produktu obsahovať. Ak na tom nezáleží, ponechajte políčko prázdne.', - 'The maximum the customer should spend on shipping. Set to zero to disable.' => 'Maximálna suma, ktorú by mal zákazník minúť za dopravu. Vypnete zadaním nuly.', - 'The minimum the customer should spend on shipping. Set to zero to disable.' => 'Minimálna suma, ktorú by mal zákazník minúť za dopravu. Vypnete zadaním nuly.', - 'The order is not valid.' => 'Neplatná objednávka.', - 'The payment gateway that will be used for the subscription plan.' => 'Platobná brána, ktorá sa použije pre plán prihlásení na odber.', - 'The percentile value which should discount each item. i.e. {ex1} for {ex2} off. Percentages are rounded to 2 decimal places.' => 'Percentuálna hodnota, ktorá má zľaviť každú položku, t.j. {ex1} pre zľavu {ex2} pre každú položku. Percentá sú zaokrúhlené na 2 desatinné miesta.', - 'The previously-selected shipping method is no longer available.' => 'Predtým zvolený spôsob dopravy už nie je dostupný.', - 'The price of {description} increased from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Cena {description} sa zvýšila z {originalSalePriceAsCurrency} na {newSalePriceAsCurrency}', - 'The price of {description} was reduced from {originalSalePriceAsCurrency} to {newSalePriceAsCurrency}' => 'Cena {description} sa znížila z {originalSalePriceAsCurrency} na {newSalePriceAsCurrency}', - 'The primary currency cannot be changed after orders are placed.' => 'Hlavná mena nemôže byť po zadaní objednávok zmenená.', - 'The purchasable defines the relationship' => 'Položka na predaj definuje vzťah', - 'The purchasable is related by another element' => 'Položka na predaj je spojená s ďalším prvkom', - 'The recipient of the email. Twig code can be used here.' => 'Príjemca e-mailu. Môže byť použitý Twig kód.', - 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'E-mailová adresa pre odpoveď. Ak chcete použiť normálnu adresu pre odpoveď, ponechajte toto pole prázdne. Môže byť použitý Twig kód.', - 'The site the order was made in.' => 'Miesto, kde bola objednávka zadaná.', - 'The site to be used when this email is rendered.' => 'Miesto, ktoré sa má použiť pri zobrazení tohto e-mailu.', - 'The subject line of the email. Twig code can be used here.' => 'Riadok predmetu e-mailu. Môže byť použitý Twig kód.', - 'The template that the PDF should be generated from.' => 'Šablóna, z ktorej sa má generovať PDF súbor.', - 'The template to be used for HTML emails.' => 'Šablóna ktorá sa má použiť pre HTML emaily.', - 'The template to be used for plain text emails. Twig code can be used here.' => 'Šablóna, ktorá sa má použiť pre e-maily s obyčajným textom. Môže byť použitý Twig kód.', - 'The template to use when a product’s URL is requested.' => 'Šablóna, ktorá sa má použiť pri požiadavke URL adresy produktu.', - 'The total number of order adjustments changed.' => 'Celkový počet úprav objednávky sa zmenil.', - 'The total price of the order changed.' => 'Celková cena objednávky sa zmenila.', - 'The total quantity of items within the order changed.' => 'Celkové množstvo položiek v objednávke sa zmenilo.', - 'The unique SKU of the donation purchasable.' => 'Jedinečné SKU pre predajný dar.', - 'The unit of measurement that should be used when specifying product dimensions.' => 'Merná jednotka, ktorá má byť použitá pri určovaní rozmerov produktu.', - 'The unit of measurement that should be used when specifying product weights.' => 'Merná jednotka, ktorá má byť použitá pri určovaní váhy produktu.', - 'The webhook URL for this gateway.' => 'URL pre webhook pre túto bránu.', - 'The “From” name that will be used when sending order status emails. Leave blank to use the Sender Name defined in Craft’s General Settings.' => 'Meno „Od“, ktoré bude použité pri odosielaní emailov o stave objednávky. Prázdne, ak sa má použiť meno odosielateľa definované vo všeobecných nastaveniach Craft-u.', - 'There are errors on the order' => 'Objednávka obsahuje chyby', - 'There are only {num} “{description}” items left in stock.' => 'Počet zvyšných položiek „{description}“ na sklade je {num}.', - 'There aren’t any product types to select yet.' => 'Zatiaľ nie je možné vybrať žiadne typy produktov.', - 'There is no gateway or payment source available for use with this order.' => 'Pre túto objednávku nie je k dispozícii žiadna použiteľná brána alebo zdroj platby.', - 'There is no gateway selected that supports payment sources.' => 'Nie je vybratá žiadna brána, ktorá podporuje zdroje platby.', - 'There is no shipping method selected for this order.' => 'Pre túto objednávku nie je vybraný žiadny spôsob doručenia.', - 'This URL will load the cart into the user’s session, making it the active cart.' => 'Tento odkaz URL nahrá košík do relácie používateľa a aktivuje ho.', - 'This action is not allowed for the current user.' => 'Táto akcia nie je pre aktuálneho používateľa povolená.', - 'This category will be used as the default for all purchasables in this store.' => 'Táto kategória sa bude používať ako predvolená pre všetky nákupy v tomto obchode.', - 'This coupon is for registered users and limited to {limit} uses.' => 'Tento kupón je pre registrovaných používateľov a je obmedzený na {limit} použití.', - 'This coupon is limited to {limit} uses.' => 'Tento kupón je obmedzený na {limit} použití.', - 'This coupon requires an email address.' => 'Tento kupón vyžaduje e-mailovú adresu.', - 'This gateway does not support that functionality.' => 'Táto brána nepodporuje danú funkciu.', - 'This is being overridden by the {setting} config setting in `config/{file}.php`.' => 'Toto je prepísané nastavením konfigurácie {setting} v `config/{file}.php`.', - 'This is the address where your store is located. It may be used by various plugins to determine things like shipping and taxes. It could also be used in PDF receipts.' => 'Toto je adresa umiestnenia obchodu. Pomocou nej môžu rôzne doplnky určiť napríklad dodacie podmienky a dane. Používa sa tiež v potvrdenkách vo formáte PDF.', - 'This is the default PDF that will be rendered when requesting the order PDF.' => 'Toto je predvolený súbor PDF, ktorý sa vykreslí pri žiadosti o súbor PDF objednávky.', - 'This is the last location for the {store} store.' => 'Toto je posledné miesto pre obchod {store}.', - 'This month' => 'Tento mesiac', - 'This order has unsaved changes.' => 'Táto objednávka obsahuje neuložené zmeny.', - 'This week' => 'Tento týždeň', - 'This year' => 'Tento rok', - 'Times Used' => 'Počet Použití', - 'Title' => 'Názov', - 'To' => 'Komu', - 'Today' => 'Dnes', - 'Too many variants for this product.' => 'Príliš veľa variantov pre tento produkt.', - 'Top Customers by Average Order' => 'Top zákazníci podľa priemernej objednávky', - 'Top Customers by Total Revenue' => 'Top zákazníci podľa celkového príjmu', - 'Top Customers' => 'Top zákazníci', - 'Top Product Types by Qty Sold' => 'Top typy produktov podľa počtu predaných kusov', - 'Top Product Types by Revenue' => 'Top typy produktov podľa príjmu', - 'Top Product Types' => 'Top typy produktov', - 'Top Products by Qty Sold' => 'Top produkty podľa počtu predaných kusov', - 'Top Products by Revenue' => 'Top produkty podľa príjmu', - 'Top Products' => 'Top produkty', - 'Top Purchasables by Qty Sold' => 'Top položky na predaj podľa počtu predaných kusov', - 'Top Purchasables by Revenue' => 'Top položky na predaj podľa príjmu', - 'Top Purchasables' => 'Top položky na predaj', - 'Total ' => 'Celkom ', - 'Total Discount Use Limit' => 'Celkový limit na využitie zľavy', - 'Total Discount' => 'Celková zľava', - 'Total Included Tax' => 'Celková započítaná daň', - 'Total Orders by Billing Country' => 'Celkový počet objednávok podľa krajiny fakturácie', - 'Total Orders by Country' => 'Celkový počet objednávok podľa krajiny', - 'Total Orders by Shipping Country' => 'Celkový počet objednávok podľa krajiny dopravy', - 'Total Orders' => 'Celkový počet objednávok', - 'Total Paid' => 'Celkom Uhradené', - 'Total Price' => 'Celková Cena', - 'Total Qty' => 'Celkové množstvo', - 'Total Revenue' => 'Celkový príjem', - 'Total Shipping' => 'Celkové náklady na dopravu', - 'Total Tax' => 'Celková daň', - 'Total Weight' => 'Celková hmotnosť', - 'Total' => 'Celkom', - 'Track Inventory' => 'Sledovanie zásob', - 'Transaction Hash' => 'Hodnota hash transakcie', - 'Transaction ID' => 'ID transakcie', - 'Transaction captured successfully: {message}' => 'Transakcia úspešne zachytená: {message}', - 'Transaction refunded successfully: {message}' => 'Transakcia úspešne vrátená: {message}', - 'Transactions' => 'Transakcie', - 'Transfer Fields' => 'Polia na prevod', - 'Transfer Items' => 'Položky na prevod', - 'Transfer Settings' => 'Nastavenia prevodu', - 'Transfer Status' => 'Stav prevodu', - 'Transfer fields saved.' => 'Uložené polia na prevod.', - 'Transfer must have at least one item.' => 'Prevod musí obsahovať aspoň jednu položku.', - 'Transfer' => 'Prevod', - 'Transfers' => 'Prevody', - 'Trial days credited' => 'Priznané dni skúšobnej verzie', - 'Trial expiration' => 'Vypršanie platnosti skúšobnej verzie', - 'Trial expiry date' => 'Dátum exspirácie skúšobnej verzie', - 'Type not in allowed options.' => 'Typ nie je možné použiť v možnostiach.', - 'Type' => 'Typ', - 'URI' => 'URI', - 'Unable to cancel subscription at this time.' => 'Prihlásenie na odber sa momentálne nedá zrušiť.', - 'Unable to complete order: another request is already in progress.' => 'Objednávku nie je možné dokončiť: práve prebieha iná požiadavka.', - 'Unable to find variant.' => 'Nie je možné nájsť variant.', - 'Unable to generate coupon codes: {message}' => 'Nie je možné generovať kódy kupónov: {message}', - 'Unable to make payment at this time.' => 'Platba sa nedá aktuálne uskutočniť.', - 'Unable to modify subscription at this time.' => 'Prihlásenie na odber sa aktuálne nedá upraviť.', - 'Unable to reactivate subscription at this time.' => 'Prihlásenie na odber sa aktuálne nedá znova aktivovať.', - 'Unable to reassign orders.' => 'Objednávky nie je možné prerozdeliť.', - 'Unable to remove order data.' => 'Údaje o objednávke sa nepodarilo odstrániť.', - 'Unable to retrieve Sale and Purchasable.' => 'Nebolo možné obnoviť zľavu a položky na predaj.', - 'Unable to retrieve cart.' => 'Košík sa nedá obnoviť.', - 'Unable to retrieve customer.' => 'Nebolo možné obnoviť zákazníka.', - 'Unable to retrieve load cart URL' => 'Nebolo možné získať odkaz URL pre nahranie košíka', - 'Unable to retrieve payment source.' => 'Nie je možné načítať zdroj platby.', - 'Unable to set default shipping category.' => 'Predvolená kategória dopravy sa nedala nastaviť.', - 'Unable to set default tax category.' => 'Predvolená daňová kategória sa nedala nastaviť.', - 'Unable to set primary payment source.' => 'Nie je možné nastaviť primárny zdroj platby.', - 'Unable to start the subscription. Please check your payment details.' => 'Prihlásenie na odber nie je možné spustiť. Skontrolujte platobné údaje.', - 'Unable to subscribe at this time.' => 'Aktuálne nie je možné prihlásiť odber.', - 'Unable to update cart.' => 'Košík sa nedá aktualizovať.', - 'Unable to validate address.' => 'Nie je možné overiť adresu.', - 'Unit Price' => 'Jednotková cena', - 'Unit price (minus discounts)' => 'Jednotková cena (znížená o zľavy)', - 'Units' => 'Jednotky', - 'Unpaid' => 'Nezaplatené', - 'Unsubscribe' => 'Odhlásiť odber', - 'Update Address' => 'Aktualizovať adresu', - 'Update Order Status' => 'Aktualizovať stav objednávky', - 'Update Order Status…' => 'Aktualizovať stav objednávky…', - 'Update order' => 'Aktualizovať objednávku', - 'Update subscription' => 'Aktualizovať predplatné', - 'Update' => 'Aktualizovať', - 'Updated By' => 'Aktualizoval(a)', - 'Updated committed stock successfully.' => 'Úspešne aktualizované viazané zásoby.', - 'Updated' => 'Aktualizované', - 'Use Billing Address For Tax' => 'Použitie fakturačnej adresy pre daň', - 'Use as the primary billing address' => 'Používajte ako hlavnú fakturačnú adresu', - 'Use as the primary shipping address' => 'Používajte ako hlavnú dodaciu adresu', - 'Used By Tax Rates' => 'Používané daňovými sadzbami', - 'Used by Tax Rates' => 'Používané daňovými sadzbami', - 'User Groups' => 'Skupiny užívateľov', - 'User not found.' => 'Používateľ sa nenašiel.', - 'User' => 'Používateľ', - 'Uses' => 'Používa', - 'Validate Business Tax ID as Vat ID' => 'Overenie daňového identifikačného čísla podniku ako identifikačného čísla DPH', - 'Validating condition syntax' => 'Overuje sa platnosť syntaxe', - 'Validating formula syntax' => 'Overuje sa syntax vzorca', - 'Variant Fields' => 'Polia variantu', - 'Variant Has Untracked Stock' => 'Variant má nesledované zásoby', - 'Variant Price' => 'Cena variantu', - 'Variant SKU' => 'SKU variantu', - 'Variant Search' => 'Vyhľadávanie variantov', - 'Variant Stock' => 'Zásoby variantu', - 'Variant Title Format' => 'Formát Názvu Varianty', - 'Variant Tracks Stock' => 'Variant skladových zásob', - 'Variant UI Label Format' => 'Formát označenia variantu používateľského rozhrania', - 'Variant has no product.' => 'Variant nemá žiadny produkt.', - 'Variants not restored.' => 'Varianty neboli obnovené.', - 'Variants restored.' => 'Varianty boli obnovené.', - 'Variants' => 'Varianty', - 'View customer' => 'Zobraziť zákazníka', - 'View order' => 'Zobraziť objednávku', - 'View product type - {productType}' => 'Zobraziť typ produktu - {productType}', - 'View user' => 'Zobraziť používateľa', - 'View' => 'Zobraziť', - 'Warning, deleting this currency will stop all payments and refunds in this currency, are you sure you want to delete “{name}”?' => 'Upozornenie. Zmazanie tejto meny pozastaví všetky platby a vrátenie platieb v tejto mene. Naozaj chcete zmazať „{name}“?', - 'Web' => 'Web', - 'Webhook URL' => 'URL pre webhook', - 'Weight ({unit})' => 'Hmotnosť ({unit})', - 'Weight Rate' => 'Váhová Sadzba', - 'Weight Unit' => 'Jednotka Hmotnosti', - 'Weight' => 'Váha', - 'What product URIs should look like for the site.' => 'Ako by mali vyzerať identifikátory URI produktov pre daný web.', - 'What the auto-generated product titles should look like. You can include tags that output product properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Ako majú vyzerať automaticky generované názvy produktov. Možno zahrnúť značky, ktoré zobrazia vlastnosti produktov, ako je {ex1} alebo {ex2}. Všetky vlastné polia musia byť nastavené na povinné.', - 'What the auto-generated variant titles should look like. You can include tags that output variant properties, such as {ex1} or {ex2}. All custom fields used must be set to required.' => 'Ako majú vyzerať automaticky generované názvy variant. Možno zahrnúť značky, ktoré zobrazia vlastnosti variant, ako je {ex1} alebo {ex2}. Všetky vlastné polia musia byť nastavené na povinné.', - 'What the order PDF filename should look like (sans extension). You can include tags that output order properties, such as {ex1} or {ex2}.' => 'Ako by mal vyzerať názov PDF súboru k objednávkam (bez prípony). Môžete pridať štítky, ktorých výstupom sú vlastnosti objednávky, ako napríklad {ex1} alebo {ex2}.', - 'What the unique auto-generated SKUs should look like, when a SKU field is submitted without a value. You can include tags that output properties, such as {ex1} or {ex2}' => 'Ako má vyzerať jedinečné automaticky generované SKU, keď je pole SKU odoslané prázdne. Možno zahrnúť značky, ktoré zobrazia vlastnosti, ako je {ex1} alebo {ex2}', - 'What this PDF will be called in the control panel.' => 'Ako sa bude tento PDF súbor volať v ovládacom paneli.', - 'What this catalog pricing rule will be called in the control panel.' => 'Ako sa bude toto pravidlo stanovovania cien podľa katalógu volať v ovládacom paneli.', - 'What this discount will be called in the control panel.' => 'Ako sa bude táto zľava volať v ovládacom paneli.', - 'What this email will be called in the control panel.' => 'Ako sa bude tento e-mail volať v ovládacom paneli.', - 'What this product type will be called in the control panel.' => 'Ako sa bude tento typ produktu volať v ovládacom paneli.', - 'What this sale will be called in the control panel.' => 'Ako sa bude tento výpredaj volať v ovládacom paneli.', - 'What this shipping category will be called in the control panel.' => 'Ako sa bude táto kategória dopravy volať v ovládacom paneli.', - 'What this shipping rule will be called in the control panel.' => 'Ako sa bude toto pravidlo dopravy volať v ovládacom paneli.', - 'What this shipping zone will be called in the control panel.' => 'Ako sa bude táto zóna doručenia volať v ovládacom paneli.', - 'What this status will be called in the control panel.' => 'Ako sa bude tento stav volať v ovládacom paneli.', - 'What this subscription plan will be called in the control panel.' => 'Ako sa bude toto predplatné volať v ovládacom paneli.', - 'What this tax category will be called in the control panel.' => 'Ako sa bude táto daňová kategória volať v ovládacom paneli.', - 'What this tax zone will be called in the control panel.' => 'Ako sa bude táto daňová zóna volať v ovládacom paneli.', - 'When this discount is applied to an order, which line items should be discounted?' => 'Keď sa táto zľava uplatní na objednávku, ktoré položky by mali byť zľavnené?', - 'Whether the first available shipping method option should be set automatically on carts.' => 'Či sa má v košíkoch automaticky nastaviť prvá dostupná možnosť spôsobu dopravy.', - 'Whether the user’s primary payment source should be set automatically on new carts.' => 'Či sa má pri nových košíkoch automaticky nastaviť primárny zdroj platby používateľa.', - 'Whether the user’s primary shipping and billing addresses should be set automatically on new carts.' => 'Či sa má v nových košíkoch automaticky nastaviť primárna dodacia a fakturačná adresa používateľa.', - 'Whether this catalog pricing rule should be available for use, regardless of other conditions.' => 'Či by toto pravidlo stanovovania cien podľa katalógu malo byť k dispozícii na použitie bez ohľadu na ostatné podmienky.', - 'Whether this sale should be available for use, regardless of other conditions.' => 'Určuje, či má byť táto predajná akcia dostupná na použitie bez ohľadu na ostatné podmienky.', - 'Which data to display in the name column in the results table.' => 'Ktoré údaje zobrazovať v stĺpci s názvom v tabuľke výsledkov.', - 'Which product types should this category be available to?' => 'Pre ktoré typy produktov má byť táto kategória dostupná?', - 'Which template should be loaded when a product’s URL is requested.' => 'Šablóna, ktorá sa ma načítať, keď je vyžiadaná adresa URL produktu.', - 'Width ({unit})' => 'Šírka ({unit})', - 'Width' => 'Šírka', - 'YYYY' => 'RRRR', - 'Yes' => 'Áno', - 'You are not allowed to add a line item.' => 'Nemáte oprávnenie pridať riadkovú položku.', - 'You currently have no emails configured to select for this status.' => 'Pre výber tohto stavu nemáte momentálne nastavený žiadny e-mail.', - 'You do not have permission to load this cart.' => 'Nemáte oprávnenie na načítanie tohto košíka.', - 'You must set up at least one gateway that supports subscriptions first.' => 'Najprv musíte nastaviť aspoň jednu bránu, ktorá podporuje prihlásenia na odber.', - 'You must be logged in or provide a valid token to load this cart.' => 'Na načítanie tohto košíka sa musíte prihlásiť alebo zadať platný token.', - 'You must be signed in to create a payment source.' => 'Ak chcete vytvoriť zdroj platby, musíte byť prihlásení.', - 'You must be signed in to set a primary payment source.' => 'Ak chcete nastaviť primárny zdroj platby, musíte byť prihlásení.', - 'You must make a payment to complete the order.' => 'Ak chcete dokončiť túto objednávku, musíte ju zaplatiť.', - 'Your Cart Recovery Link' => 'Váš odkaz na obnovenie košíka', - 'Your Order PDF Download Link' => 'Odkaz na stiahnutie PDF vašej objednávky', - 'Your order is empty' => 'Vaša objednávka je prázdna', - 'ZIP file' => 'Súbor ZIP', - 'Zero - Minimum price is zero if discounts are greater than the order value.' => 'Nula – Minimálna cena je nula v prípade, že sú zľavy vyššie ako hodnota objednávky.', - 'Zip Code' => 'PSČ', - 'all' => 'všetky', - 'any' => 'akékoľvek', - 'average order total' => 'priemerná cena objednávky', - 'billing address' => 'fakturačná adresa', - 'donation' => 'darovanie', - 'donations' => 'dary', - 'info' => 'informácie', - 'inventory location' => 'umiestnenie zásob', - 'new customers' => 'noví zákazníci', - 'on hand' => 'dostupné', - 'only' => 'len', - 'order' => 'objednať', - 'orders' => 'objednávky', - 'price' => 'cena', - 'prices' => 'ceny', - 'product variant' => 'variant produktu', - 'product variants' => 'varianty produktu', - 'product' => 'produkt', - 'products' => 'produkty', - 'repeat customers' => 'opakovaní zákazníci', - 'shipping address' => 'dodacia adresa', - 'shippingSameAsBilling and billingSameAsShipping can’t both be set.' => 'Nie je možné nastaviť súčasne položku shippingSameAsBilling aj položku billingSameAsShipping.', - 'subscription' => 'predplatné', - 'subscriptions' => 'predplatné', - 'to' => 'na', - 'transfer' => 'prevod', - 'transfers' => 'prevody', - '{amount} included' => 'vrátane {amount}', - '{count} Unfulfilled Orders' => 'Nesplnené objednávky: {count}', - '{description} is no longer available.' => '{description} už nie je dostupný.', - '{description} only has {stock} in stock.' => 'Počet kusov {description} na sklade je už len {stock}.', - '{from} to {to}' => '{from} až {to}', - '{name} (Primary)' => '{name} (Hlavná)', - '{name} (Trashed)' => '{name} (Zahodené do koša)', - '{name} catalog price' => '{name} katalógová cena', - '{num, plural, =1{Order} other{Orders}} updated.' => '{num, plural, one {} few {objednávky} many {objednávok}=1{objednávka} other{objednávok}} aktualizovaná/aktualizovaných.', - '{numOrders, number} {numOrders, plural, =1{order is} other{orders are}} associated with the {numUsers, plural, =1{user} other{users}}.' => '{numOrders, number} {numOrders, plural, one {} few {objednávky sú priradené} many {objednávok je priradených}=1{objednávka je priradená} other{objednávok je priradených}} k {numUsers, plural, one {} few {používateľom} many {používateľom}=1{používateľovi} other{používateľom}}.', - '{numSubscriptions, number} {numSubscriptions, plural, =1{subscription is} other{subscriptions are}} activated for the {numUsers, plural, =1{user} other{users}}.' => '{numSubscriptions, number} {numSubscriptions, plural, one {} few {predplatné sú aktivované} many {predplatných je aktivovaných}=1{predplatné je aktivované} other{predplatných je aktivovaných}} pre {numUsers, plural, one {} few {používateľov} many {používateľov}=1{používateľa} other{používateľov}}.', - '{number} more…' => 'Ešte {number}…', - '{pct} off the discounted item price' => '{pct} zo zľavnenej ceny položky', - '{pct} off the original item price' => '{pct} z pôvodnej ceny položky', - '{storeNames} {num, plural, =1{has} other{have}} not been assigned to a site.' => '{storeNames} {num, plural, one {} few {nemajú} many {nemá}=1{nemá} other{nemá}} priradenú lokalitu.', - '{total} in total revenue' => 'celkový príjem {total}', - '{total} orders' => '{total} objednávok', - '{total} saleable across {locationCount} location(s)' => '{total} predajné na {locationCount} mieste (miestach)', - '{uses} uses across {emails} email addresses' => '{uses} použití pre e-mailové adresy {emails}', - '{uses} uses across {users} users' => '{uses} použití pre použivateľov {users}', - '“{description}” is currently out of stock.' => 'Produkt „{description}“ je momentálne vypredaný.', - '“{key}” has invalid JSON' => '„{key}“ obsahuje neplatný JSON', -]; diff --git a/src/validators/CouponsValidator.php b/src/validators/CouponsValidator.php deleted file mode 100644 index 07a3dae441..0000000000 --- a/src/validators/CouponsValidator.php +++ /dev/null @@ -1,69 +0,0 @@ - - * @since 4.0.0 - */ -class CouponsValidator extends Validator -{ - /** - * @param \craft\commerce\models\Coupon $model the coupon model to be validated - * @inheritdoc - */ - public function validateAttribute($model, $attribute): void - { - $codes = ArrayHelper::getColumn($model->$attribute, 'code'); - - // Make sure there aren't any blank lines - if (array_filter($codes) !== $codes) { - $this->addError($model, $attribute, Craft::t('commerce', 'Coupon codes cannot be blank.')); - } - - // Case-insensitive check for duplicates in the same set of codes - if (array_intersect_key($codes, array_unique(array_map('strtolower', $codes))) !== $codes) { - $this->addError($model, $attribute, Craft::t('commerce', 'Coupon codes must be unique.')); - return; - } - - // Check other codes in the DB - $query = (new Query()) - ->select([ - 'coupons.code', - 'discounts.name', - ]) - ->from(Table::COUPONS . ' coupons') - ->leftJoin(Table::DISCOUNTS . ' discounts', '[[discounts.id]] = [[coupons.discountId]]') - ->where(['in', 'code', $codes]); - - if ($model->id) { - $query->andWhere(['not', ['discountId' => $model->id]]); - } - - $existingDiscounts = $query->all(); - - if (count($existingDiscounts)) { - foreach ($existingDiscounts as $existingDiscount) { - $this->addError($model, $attribute, Craft::t('commerce', 'Coupon code “{code}” is already in use by discount “{name}”.', [ - 'code' => $existingDiscount['code'], - 'name' => $existingDiscount['name'], - ])); - } - } - } -} diff --git a/src/views/debug/commerce/detail.php b/src/views/debug/commerce/detail.php deleted file mode 100644 index 4815f8b3e4..0000000000 --- a/src/views/debug/commerce/detail.php +++ /dev/null @@ -1,35 +0,0 @@ - -

Commerce Info

- -
- data['content'] as $k => $item) { - echo Html::tag('div', $item, [ - 'class' => $k === 0 ? 'tab-pane fade active show' : 'tab-pane fade', - 'id' => 'comdebug-tab-' . $k, - ]); - } - ?> -
\ No newline at end of file diff --git a/src/views/debug/commerce/model.php b/src/views/debug/commerce/model.php deleted file mode 100644 index 75be0eae29..0000000000 --- a/src/views/debug/commerce/model.php +++ /dev/null @@ -1,22 +0,0 @@ - -

-
- - - toArray($fields ?? array_keys($model->fields()), $extraFields ?? $model->extraFields()) as $attr => $value): ?> - 0): ?> - $val): ?> - - - - - - - -
-
- diff --git a/src/views/debug/commerce/summary.php b/src/views/debug/commerce/summary.php deleted file mode 100644 index e8ecb2e0a9..0000000000 --- a/src/views/debug/commerce/summary.php +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/src/web/assets/commercecp/src/js/CommerceSubscriptionIndex.js b/src/web/assets/commercecp/src/js/CommerceSubscriptionIndex.js deleted file mode 100644 index 9685d64729..0000000000 --- a/src/web/assets/commercecp/src/js/CommerceSubscriptionIndex.js +++ /dev/null @@ -1,14 +0,0 @@ -if (typeof Craft.Commerce === typeof undefined) { - Craft.Commerce = {}; -} - -/** - * Class Craft.Commerce.SubscriptionIndex - */ -Craft.Commerce.SubscriptionsIndex = Craft.BaseElementIndex.extend({}); - -// Register the Commerce order index class -Craft.registerElementIndexClass( - 'craft\\commerce\\elements\\Subscription', - Craft.Commerce.SubscriptionsIndex -); diff --git a/src/web/assets/commercecp/src/scss/subscriptions.scss b/src/web/assets/commercecp/src/scss/subscriptions.scss deleted file mode 100644 index 0eace9a2dd..0000000000 --- a/src/web/assets/commercecp/src/scss/subscriptions.scss +++ /dev/null @@ -1,7 +0,0 @@ -.payment-status-unpaid { - color: #d0021b; -} - -.payment-status-paid { - color: #27ae60; -} diff --git a/src/web/assets/inventory/InventoryAsset.php b/src/web/assets/inventory/InventoryAsset.php deleted file mode 100644 index 3eddfbe674..0000000000 --- a/src/web/assets/inventory/InventoryAsset.php +++ /dev/null @@ -1,72 +0,0 @@ - - * @since 5.0 - */ -class InventoryAsset extends AssetBundle -{ - /** - * @inheritdoc - */ - public function init(): void - { - $this->sourcePath = __DIR__ . '/dist'; - - $this->depends = [ - CpAsset::class, - HtmxAsset::class, - AdminTableAsset::class, - ]; - - $this->css[] = 'css/inventory.css'; - - $this->js[] = 'inventory.js'; - - parent::init(); - } - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('commerce', [ - 'Item', - 'No inventory found.', - 'Search inventory', - 'No inventory found.', - 'Reserved', - 'Damaged', - 'Safety', - 'Quality Control', - 'Committed', - 'Available', - 'On Hand', - 'Incoming', - 'View', - 'Table Columns', - 'Purchasable', - 'SKU', - ]); - } - } -} diff --git a/src/web/assets/purchasablepricefield/PurchasablePriceFieldAsset.php b/src/web/assets/purchasablepricefield/PurchasablePriceFieldAsset.php deleted file mode 100644 index 190e94bee3..0000000000 --- a/src/web/assets/purchasablepricefield/PurchasablePriceFieldAsset.php +++ /dev/null @@ -1,51 +0,0 @@ - - * @since 5.0.0 - */ -class PurchasablePriceFieldAsset extends AssetBundle -{ - /** - * @inheritdoc - */ - public function init(): void - { - $this->sourcePath = __DIR__ . '/dist'; - - $this->depends = [ - CpAsset::class, - ]; - - $this->js[] = 'purchasablepricefield.js'; - - parent::init(); - } - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('commerce', [ - // @TODO Register translation keys for the user-facing strings used by purchasablepricefield.js (e.g. via Craft::t('commerce', '...')) - ]); - } - } -} diff --git a/src/web/assets/transfers/TransfersAsset.php b/src/web/assets/transfers/TransfersAsset.php deleted file mode 100644 index e83d3efc33..0000000000 --- a/src/web/assets/transfers/TransfersAsset.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @since 5.0 - */ -class TransfersAsset extends AssetBundle -{ - /** - * @inheritdoc - */ - public function init(): void - { - $this->sourcePath = __DIR__ . '/dist'; - - $this->depends = [ - CpAsset::class, - HtmxAsset::class, - ]; - - $this->css[] = 'css/transfers.css'; - - $this->js[] = 'transfers.js'; - - parent::init(); - } - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('commerce', [ - - ]); - } - } -} diff --git a/src/web/twig/CraftVariableBehavior.php b/src/web/twig/CraftVariableBehavior.php deleted file mode 100644 index 2101f66b20..0000000000 --- a/src/web/twig/CraftVariableBehavior.php +++ /dev/null @@ -1,94 +0,0 @@ - - * @since 2.0 - */ -class CraftVariableBehavior extends Behavior -{ - /** - * @var Plugin - */ - public Plugin $commerce; - - public function init(): void - { - parent::init(); - - // Point `craft.commerce` to the craft\commerce\Plugin instance - $this->commerce = Plugin::getInstance(); - } - - /** - * Returns a new OrderQuery instance. - * - * @param array $criteria - * @return OrderQuery - */ - public function orders(array $criteria = []): OrderQuery - { - $query = Order::find(); - Craft::configure($query, $criteria); - return $query; - } - - /** - * Returns a new SubscriptionQuery instance. - * - * @param array $criteria - * @return SubscriptionQuery - */ - public function subscriptions(array $criteria = []): SubscriptionQuery - { - $query = Subscription::find(); - Craft::configure($query, $criteria); - return $query; - } - - /** - * Returns a new ProductQuery instance. - * - * @param array $criteria - * @return ProductQuery - */ - public function products(array $criteria = []): ProductQuery - { - $query = Product::find(); - Craft::configure($query, $criteria); - return $query; - } - - /** - * Returns a new VariantQuery instance. - * - * @param array $criteria - * @return VariantQuery - */ - public function variants(array $criteria = []): VariantQuery - { - $query = Variant::find(); - Craft::configure($query, $criteria); - return $query; - } -} diff --git a/src/web/twig/Extension.php b/src/web/twig/Extension.php deleted file mode 100644 index 2a48532769..0000000000 --- a/src/web/twig/Extension.php +++ /dev/null @@ -1,65 +0,0 @@ - - * @since 2.0 - */ -class Extension extends AbstractExtension implements GlobalsInterface -{ - public function getName(): string - { - return 'Craft Commerce Twig Extension'; - } - - /** - * @inheritdoc - */ - public function getFilters(): array - { - return [ - new TwigFilter('commerceCurrency', [Currency::class, 'formatAsCurrency']), - new TwigFilter('commercePaymentFormNamespace', [PaymentForm::class, 'getPaymentFormNamespace']), - ]; - } - - /** - * @return null[] - * @throws SiteNotFoundException - * @throws InvalidConfigException - * @since 5.0.0 - */ - public function getGlobals(): array - { - $currentStore = null; - - /** @var Site|StoreBehavior $currentSite */ - $currentSite = Craft::$app->getSites()->getCurrentSite(); - if ($currentSite->getBehavior('commerce:store') !== null) { - $currentStore = $currentSite->getStore(); - } - - return [ - 'currentStore' => $currentStore, - ]; - } -} diff --git a/src/widgets/AverageOrderTotal.php b/src/widgets/AverageOrderTotal.php deleted file mode 100644 index 054ff820a3..0000000000 --- a/src/widgets/AverageOrderTotal.php +++ /dev/null @@ -1,137 +0,0 @@ - - * @since 3.0 - */ -class AverageOrderTotal extends Widget -{ - use StatWidgetTrait; - - /** - * @var null|AverageOrderTotalStat - */ - private ?AverageOrderTotalStat $_stat = null; - - /** - * @inheritDoc - */ - public function init(): void - { - parent::init(); - - if (!(isset($this->storeId)) || !$this->storeId) { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $this->storeId = $site?->getStore()->id ?? Plugin::getInstance()->getStores()->getPrimaryStore()->id; - } - - $this->_stat = new AverageOrderTotalStat( - $this->dateRange, - DateTimeHelper::toDateTime($this->startDate, true), - DateTimeHelper::toDateTime($this->endDate, true), - $this->storeId - ); - - if (!empty($this->orderStatuses)) { - $this->_stat->setOrderStatuses($this->orderStatuses); - } - } - - /** - * @inheritdoc - */ - public static function isSelectable(): bool - { - return Craft::$app->getUser()->checkPermission('commerce-manageOrders'); - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Average Order Total'); - } - - /** - * @inheritdoc - */ - public static function icon(): ?string - { - return Craft::getAlias('@craft/commerce/icon-mask.svg'); - } - - /** - * @inheritdoc - */ - public function getTitle(): ?string - { - return ''; - } - - /** - * @inheritdoc - */ - public function getBodyHtml(): ?string - { - $number = $this->_stat->get(); - $timeFrame = $this->_stat->getDateRangeWording(); - - $view = Craft::$app->getView(); - $view->registerAssetBundle(StatWidgetsAsset::class); - - return $view->renderTemplate('commerce/_components/widgets/orders/average/body', compact('number', 'timeFrame')); - } - - /** - * @inheritDoc - */ - public static function maxColspan(): ?int - { - return 1; - } - - /** - * @inheritdoc - */ - public function getSettingsHtml(): ?string - { - $id = 'average-order-total' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - Craft::$app->getView()->registerAssetBundle(CommerceWidgetsAsset::class); - - return Craft::$app->getView()->renderTemplate('commerce/_components/widgets/orders/average/settings', [ - 'id' => $id, - 'namespaceId' => $namespaceId, - 'orderStatuses' => $this->getOrderStatusOptions(), - 'widget' => $this, - ]); - } -} diff --git a/src/widgets/NewCustomers.php b/src/widgets/NewCustomers.php deleted file mode 100644 index c9583d0047..0000000000 --- a/src/widgets/NewCustomers.php +++ /dev/null @@ -1,139 +0,0 @@ - - * @since 3.0 - */ -class NewCustomers extends Widget -{ - use StatWidgetTrait; - - /** - * @var null|NewCustomersStat - */ - private ?NewCustomersStat $_stat = null; - - /** - * @inheritDoc - * @throws Exception - */ - public function init(): void - { - parent::init(); - - if (!(isset($this->storeId)) || !$this->storeId) { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $this->storeId = $site?->getStore()->id ?? Plugin::getInstance()->getStores()->getPrimaryStore()->id; - } - - $this->_stat = new NewCustomersStat( - $this->dateRange, - DateTimeHelper::toDateTime($this->startDate, true), - DateTimeHelper::toDateTime($this->endDate, true), - $this->storeId - ); - - if (!empty($this->orderStatuses)) { - $this->_stat->setOrderStatuses($this->orderStatuses); - } - } - - /** - * @inheritdoc - */ - public static function isSelectable(): bool - { - return Craft::$app->getUser()->checkPermission('commerce-manageCustomers'); - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'New Customers'); - } - - /** - * @inheritdoc - */ - public static function icon(): ?string - { - return Craft::getAlias('@craft/commerce/icon-mask.svg'); - } - - /** - * @inheritdoc - */ - public function getTitle(): ?string - { - return ''; - } - - /** - * @inheritdoc - */ - public function getBodyHtml(): ?string - { - $number = $this->_stat->get(); - $timeFrame = $this->_stat->getDateRangeWording(); - - $view = Craft::$app->getView(); - $view->registerAssetBundle(StatWidgetsAsset::class); - - return $view->renderTemplate('commerce/_components/widgets/customers/new/body', compact('number', 'timeFrame')); - } - - /** - * @inheritDoc - */ - public static function maxColspan(): ?int - { - return 1; - } - - /** - * @inheritdoc - */ - public function getSettingsHtml(): ?string - { - $id = 'new-customers' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - Craft::$app->getView()->registerAssetBundle(CommerceWidgetsAsset::class); - - return Craft::$app->getView()->renderTemplate('commerce/_components/widgets/customers/new/settings', [ - 'id' => $id, - 'namespaceId' => $namespaceId, - 'orderStatuses' => $this->getOrderStatusOptions(), - 'widget' => $this, - ]); - } -} diff --git a/src/widgets/Orders.php b/src/widgets/Orders.php deleted file mode 100644 index dfcacc2790..0000000000 --- a/src/widgets/Orders.php +++ /dev/null @@ -1,155 +0,0 @@ - - * @since 2.0 - */ -class Orders extends Widget -{ - use StatWidgetTrait; - - /** - * @var int - */ - public int $limit = 10; - - public function init(): void - { - parent::init(); - - if (!(isset($this->storeId)) || !$this->storeId) { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $this->storeId = $site?->getStore()->id ?? Plugin::getInstance()->getStores()->getPrimaryStore()->id; - } - } - - /** - * @inheritdoc - */ - public static function isSelectable(): bool - { - return Craft::$app->getUser()->checkPermission('commerce-manageOrders'); - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Recent Orders'); - } - - /** - * @inheritdoc - */ - public static function icon(): ?string - { - return Craft::getAlias('@craft/commerce/icon-mask.svg'); - } - - /** - * @inheritdoc - */ - public function getTitle(): ?string - { - if (!empty($this->orderStatuses) && count($this->orderStatuses) === 1) { - $orderStatus = Plugin::getInstance()->getOrderStatuses()->getOrderStatusByUid(ArrayHelper::firstValue($this->orderStatuses), $this->storeId); - - if ($orderStatus) { - return Craft::t('commerce', 'Recent Orders') . ' – ' . Craft::t('commerce', $orderStatus->name); - } - } - - return parent::getTitle(); - } - - /** - * @inheritdoc - */ - public function getBodyHtml(): ?string - { - $orders = $this->_getOrders(); - - $id = 'recent-orders-settings-' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - - return Craft::$app->getView()->renderTemplate('commerce/_components/widgets/orders/recent/body', [ - 'orders' => $orders, - 'showStatuses' => !empty($this->orderStatuses) && count($this->orderStatuses) > 1, - 'id' => $id, - 'namespaceId' => $namespaceId, - ]); - } - - /** - * @inheritdoc - */ - public function getSettingsHtml(): ?string - { - Craft::$app->getView()->registerAssetBundle(OrdersWidgetAsset::class); - Craft::$app->getView()->registerAssetBundle(CommerceWidgetsAsset::class); - - $id = 'recent-orders-settings-' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - return Craft::$app->getView()->renderTemplate('commerce/_components/widgets/orders/recent/settings', [ - 'id' => $id, - 'widget' => $this, - 'orderStatuses' => $this->getOrderStatusOptions(), - 'namespaceId' => $namespaceId, - ]); - } - - - /** - * Returns the recent entries, based on the widget settings and user permissions. - * - * @return Order[] - */ - private function _getOrders(): array - { - $limit = $this->limit; - - $query = Order::find(); - $query->isCompleted(true); - $query->dateOrdered(':notempty:'); - $query->limit($limit); - $query->storeId($this->storeId); - $query->orderBy('dateOrdered DESC'); - - if (!empty($this->orderStatuses)) { - $orderStatusIds = Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses($this->storeId) - ->filter(fn($orderStatus) => in_array($orderStatus->uid, $this->orderStatuses))->map(fn($os) => $os->id)->all(); - $query->orderStatusId($orderStatusIds); - } - - return $query->all(); - } -} diff --git a/src/widgets/RepeatCustomers.php b/src/widgets/RepeatCustomers.php deleted file mode 100644 index 03ce914a5e..0000000000 --- a/src/widgets/RepeatCustomers.php +++ /dev/null @@ -1,139 +0,0 @@ - - * @since 3.0 - */ -class RepeatCustomers extends Widget -{ - use StatWidgetTrait; - - /** - * @var null|RepeatingCustomersStat - */ - private ?RepeatingCustomersStat $_stat = null; - - /** - * @inheritDoc - */ - public function init(): void - { - parent::init(); - - if (!(isset($this->storeId)) || !$this->storeId) { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $this->storeId = $site?->getStore()->id ?? Plugin::getInstance()->getStores()->getPrimaryStore()->id; - } - - $this->dateRange = !isset($this->dateRange) || !$this->dateRange ? RepeatingCustomersStat::DATE_RANGE_TODAY : $this->dateRange; - - $this->_stat = new RepeatingCustomersStat( - $this->dateRange, - DateTimeHelper::toDateTime($this->startDate, true), - DateTimeHelper::toDateTime($this->endDate, true), - $this->storeId - ); - - if (!empty($this->orderStatuses)) { - $this->_stat->setOrderStatuses($this->orderStatuses); - } - } - - /** - * @inheritdoc - */ - public static function isSelectable(): bool - { - return Craft::$app->getUser()->checkPermission('commerce-manageCustomers'); - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Repeat Customers'); - } - - /** - * @inheritdoc - */ - public static function icon(): ?string - { - return Craft::getAlias('@craft/commerce/icon-mask.svg'); - } - - /** - * @inheritdoc - */ - public function getTitle(): ?string - { - return ''; - } - - /** - * @inheritdoc - */ - public function getBodyHtml(): ?string - { - $numbers = $this->_stat->get(); - $timeFrame = $this->_stat->getDateRangeWording(); - - $view = Craft::$app->getView(); - $view->registerAssetBundle(StatWidgetsAsset::class); - - return $view->renderTemplate('commerce/_components/widgets/customers/repeat/body', compact('numbers', 'timeFrame')); - } - - /** - * @inheritDoc - */ - public static function maxColspan(): ?int - { - return 1; - } - - /** - * @inheritdoc - */ - public function getSettingsHtml(): ?string - { - $id = 'repeat' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - Craft::$app->getView()->registerAssetBundle(CommerceWidgetsAsset::class); - - return Craft::$app->getView()->renderTemplate('commerce/_components/widgets/customers/repeat/settings', [ - 'id' => $id, - 'namespaceId' => $namespaceId, - 'orderStatuses' => $this->getOrderStatusOptions(), - 'widget' => $this, - ]); - } -} diff --git a/src/widgets/TopCustomers.php b/src/widgets/TopCustomers.php deleted file mode 100644 index 2eacdb3681..0000000000 --- a/src/widgets/TopCustomers.php +++ /dev/null @@ -1,177 +0,0 @@ - - * @since 3.0 - */ -class TopCustomers extends Widget -{ - use StatWidgetTrait; - - /** - * @var string|null Options 'total', 'average'. - */ - public ?string $type = null; - - /** - * @var TopCustomersStat - */ - private TopCustomersStat $_stat; - - /** - * @var string - */ - private string $_title; - - /** - * @var array - */ - private array $_typeOptions; - - /** - * @inheritDoc - */ - public function init(): void - { - if (!(isset($this->storeId)) || !$this->storeId) { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $this->storeId = $site?->getStore()->id ?? Plugin::getInstance()->getStores()->getPrimaryStore()->id; - } - - $this->_typeOptions = [ - 'total' => Craft::t('commerce', 'Total'), - 'average' => Craft::t('commerce', 'Average'), - ]; - - $this->_title = match ($this->type) { - 'average' => Craft::t('commerce', 'Top Customers by Average Order'), - 'total' => Craft::t('commerce', 'Top Customers by Total Revenue'), - default => Craft::t('commerce', 'Top Customers'), - }; - $this->dateRange = !isset($this->dateRange) || !$this->dateRange ? TopCustomersStat::DATE_RANGE_TODAY : $this->dateRange; - - $this->_stat = new TopCustomersStat( - $this->dateRange, - $this->type, - DateTimeHelper::toDateTime($this->startDate, true), - DateTimeHelper::toDateTime($this->endDate, true), - $this->storeId - ); - - if (!empty($this->orderStatuses)) { - $this->_stat->setOrderStatuses($this->orderStatuses); - } - - parent::init(); - } - - /** - * @inheritdoc - */ - public static function isSelectable(): bool - { - return Craft::$app->getUser()->checkPermission('commerce-manageOrders') && Craft::$app->getUser()->checkPermission('commerce-manageCustomers'); - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Top Customers'); - } - - /** - * @inheritdoc - */ - public static function icon(): ?string - { - return Craft::getAlias('@craft/commerce/icon-mask.svg'); - } - - /** - * @inheritdoc - */ - public function getTitle(): ?string - { - return $this->_title; - } - - /** - * @inheritDoc - */ - public function getSubtitle(): ?string - { - return $this->_stat->getDateRangeWording(); - } - - /** - * @inheritdoc - */ - public function getBodyHtml(): ?string - { - $stats = $this->_stat->get(); - - if (empty($stats)) { - return Html::tag('p', Craft::t('commerce', 'No stats available.'), ['class' => 'zilch']); - } - - $view = Craft::$app->getView(); - $view->registerAssetBundle(StatWidgetsAsset::class); - $view->registerAssetBundle(AdminTableAsset::class); - - return $view->renderTemplate('commerce/_components/widgets/customers/top/body', [ - 'stats' => $stats, - 'type' => $this->type, - 'typeLabel' => $this->_typeOptions[$this->type] ?? '', - 'id' => 'top-products' . StringHelper::randomString(), - ]); - } - - /** - * @inheritdoc - */ - public function getSettingsHtml(): ?string - { - $id = 'top-products' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - Craft::$app->getView()->registerAssetBundle(CommerceWidgetsAsset::class); - - return Craft::$app->getView()->renderTemplate('commerce/_components/widgets/customers/top/settings', [ - 'id' => $id, - 'namespaceId' => $namespaceId, - 'orderStatuses' => $this->getOrderStatusOptions(), - 'widget' => $this, - 'typeOptions' => $this->_typeOptions, - ]); - } -} diff --git a/src/widgets/TopProductTypes.php b/src/widgets/TopProductTypes.php deleted file mode 100644 index f7093e0680..0000000000 --- a/src/widgets/TopProductTypes.php +++ /dev/null @@ -1,178 +0,0 @@ - - * @since 3.0 - */ -class TopProductTypes extends Widget -{ - use StatWidgetTrait; - - /** - * @var string|null Options 'revenue', 'qty'. - */ - public ?string $type = null; - - /** - * @var TopProductTypesStat - */ - private TopProductTypesStat $_stat; - - /** - * @var string - */ - private string $_title; - - /** - * @var array - */ - private array $_typeOptions; - - /** - * @inheritDoc - */ - public function init(): void - { - if (!(isset($this->storeId)) || !$this->storeId) { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $this->storeId = $site?->getStore()->id ?? Plugin::getInstance()->getStores()->getPrimaryStore()->id; - } - - $this->_typeOptions = [ - 'qty' => Craft::t('commerce', 'Qty'), - 'revenue' => Craft::t('commerce', 'Revenue'), - ]; - - $this->_title = match ($this->type) { - 'revenue' => Craft::t('commerce', 'Top Product Types by Revenue'), - 'qty' => Craft::t('commerce', 'Top Product Types by Qty Sold'), - default => Craft::t('commerce', 'Top Product Types'), - }; - - $this->dateRange = !isset($this->dateRange) || !$this->dateRange ? TopProductTypesStat::DATE_RANGE_TODAY : $this->dateRange; - - $this->_stat = new TopProductTypesStat( - $this->dateRange, - $this->type, - DateTimeHelper::toDateTime($this->startDate, true), - DateTimeHelper::toDateTime($this->endDate, true), - $this->storeId - ); - - if (!empty($this->orderStatuses)) { - $this->_stat->setOrderStatuses($this->orderStatuses); - } - - parent::init(); - } - - /** - * @inheritdoc - */ - public static function isSelectable(): bool - { - return Craft::$app->getUser()->checkPermission('commerce-manageOrders'); - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Top Product Types'); - } - - /** - * @inheritdoc - */ - public static function icon(): ?string - { - return Craft::getAlias('@craft/commerce/icon-mask.svg'); - } - - /** - * @inheritdoc - */ - public function getTitle(): ?string - { - return $this->_title; - } - - /** - * @inheritDoc - */ - public function getSubtitle(): ?string - { - return $this->_stat->getDateRangeWording(); - } - - /** - * @inheritdoc - */ - public function getBodyHtml(): ?string - { - $stats = $this->_stat->get(); - - if (empty($stats)) { - return Html::tag('p', Craft::t('commerce', 'No stats available.'), ['class' => 'zilch']); - } - - $view = Craft::$app->getView(); - $view->registerAssetBundle(StatWidgetsAsset::class); - $view->registerAssetBundle(AdminTableAsset::class); - - return $view->renderTemplate('commerce/_components/widgets/producttypes/top/body', [ - 'stats' => $stats, - 'type' => $this->type, - 'typeLabel' => $this->_typeOptions[$this->type] ?? '', - 'id' => 'top-products' . StringHelper::randomString(), - ]); - } - - /** - * @inheritdoc - */ - public function getSettingsHtml(): ?string - { - $id = 'top-products' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - Craft::$app->getView()->registerAssetBundle(CommerceWidgetsAsset::class); - - return Craft::$app->getView()->renderTemplate('commerce/_components/widgets/producttypes/top/settings', [ - 'id' => $id, - 'namespaceId' => $namespaceId, - 'orderStatuses' => $this->getOrderStatusOptions(), - 'widget' => $this, - 'typeOptions' => $this->_typeOptions, - ]); - } -} diff --git a/src/widgets/TopProducts.php b/src/widgets/TopProducts.php deleted file mode 100644 index 68a82fc641..0000000000 --- a/src/widgets/TopProducts.php +++ /dev/null @@ -1,235 +0,0 @@ - - * @since 3.0 - */ -class TopProducts extends Widget -{ - use StatWidgetTrait; - - /** - * @var string|null Options 'revenue', 'qty'. - */ - public ?string $type = null; - - /** - * @var array|null - */ - public ?array $revenueOptions = [ - TopProductsStat::REVENUE_OPTION_DISCOUNT, - TopProductsStat::REVENUE_OPTION_TAX_INCLUDED, - TopProductsStat::REVENUE_OPTION_TAX, - TopProductsStat::REVENUE_OPTION_SHIPPING, - ]; - - /** - * @var TopProductsStat - */ - private TopProductsStat $_stat; - - /** - * @var string - */ - private string $_title; - - /** - * @var array - */ - private array $_typeOptions; - - /** - * @var array - */ - private array $_revenueCheckboxOptions; - - /** - * @inheritDoc - */ - public function init(): void - { - parent::init(); - - if (!(isset($this->storeId)) || !$this->storeId) { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $this->storeId = $site?->getStore()->id ?? Plugin::getInstance()->getStores()->getPrimaryStore()->id; - } - - $this->_typeOptions = [ - TopProductsStat::TYPE_QTY => Craft::t('commerce', 'Qty'), - TopProductsStat::TYPE_REVENUE => Craft::t('commerce', 'Revenue'), - ]; - - $this->_revenueCheckboxOptions = [ - [ - 'value' => TopProductsStat::REVENUE_OPTION_DISCOUNT, - 'label' => Craft::t('commerce', 'Discount'), - 'checked' => in_array(TopProductsStat::REVENUE_OPTION_DISCOUNT, $this->revenueOptions, true), - 'instructions' => Craft::t('commerce', 'Include line item discounts.'), - ], - [ - 'value' => TopProductsStat::REVENUE_OPTION_TAX_INCLUDED, - 'label' => Craft::t('commerce', 'Tax (inc)'), - 'checked' => in_array(TopProductsStat::REVENUE_OPTION_TAX_INCLUDED, $this->revenueOptions, true), - 'instructions' => Craft::t('commerce', 'Include built-in line item tax.'), - ], - [ - 'value' => TopProductsStat::REVENUE_OPTION_TAX, - 'label' => Craft::t('commerce', 'Tax'), - 'checked' => in_array(TopProductsStat::REVENUE_OPTION_TAX, $this->revenueOptions, true), - 'instructions' => Craft::t('commerce', 'Include separate line item tax.'), - ], - [ - 'value' => TopProductsStat::REVENUE_OPTION_SHIPPING, - 'label' => Craft::t('commerce', 'Shipping'), - 'checked' => in_array(TopProductsStat::REVENUE_OPTION_SHIPPING, $this->revenueOptions, true), - 'instructions' => Craft::t('commerce', 'Include line item shipping costs.'), - ], - ]; - - $this->_title = match ($this->type) { - 'revenue' => Craft::t('commerce', 'Top Products by Revenue'), - 'qty' => Craft::t('commerce', 'Top Products by Qty Sold'), - default => Craft::t('commerce', 'Top Products'), - }; - - $this->dateRange = !isset($this->dateRange) || !$this->dateRange ? TopProductsStat::DATE_RANGE_TODAY : $this->dateRange; - - $this->_stat = new TopProductsStat( - $this->dateRange, - $this->type, - DateTimeHelper::toDateTime($this->startDate, true), - DateTimeHelper::toDateTime($this->endDate, true), - $this->revenueOptions, - $this->storeId - ); - - if (!empty($this->orderStatuses)) { - $this->_stat->setOrderStatuses($this->orderStatuses); - } - } - - /** - * @inheritdoc - */ - public static function isSelectable(): bool - { - return Craft::$app->getUser()->checkPermission('commerce-manageOrders'); - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Top Products'); - } - - /** - * @inheritdoc - */ - public static function icon(): ?string - { - return Craft::getAlias('@craft/commerce/icon-mask.svg'); - } - - /** - * @inheritdoc - */ - public function getTitle(): ?string - { - return $this->_title; - } - - /** - * @inheritDoc - */ - public function getSubtitle(): ?string - { - return $this->_stat->getDateRangeWording(); - } - - /** - * @inheritdoc - */ - public function getBodyHtml(): ?string - { - $stats = $this->_stat->get(); - - if (empty($stats)) { - return Html::tag('p', Craft::t('commerce', 'No stats available.'), ['class' => 'zilch']); - } - - $view = Craft::$app->getView(); - $view->registerAssetBundle(StatWidgetsAsset::class); - $view->registerAssetBundle(AdminTableAsset::class); - - $revenueOptions = [ - TopProductsStat::REVENUE_OPTION_DISCOUNT, - TopProductsStat::REVENUE_OPTION_TAX_INCLUDED, - TopProductsStat::REVENUE_OPTION_TAX, - TopProductsStat::REVENUE_OPTION_SHIPPING, - ]; - $revenueColumnHandle = 'revenue'; - if ($this->type === TopProductsStat::TYPE_REVENUE && count(array_intersect($revenueOptions, $this->revenueOptions)) !== count($revenueOptions)) { - $revenueColumnHandle = 'revenue_custom'; - } - - return $view->renderTemplate('commerce/_components/widgets/products/top/body', [ - 'stats' => $stats, - 'revenueColumnHandle' => $revenueColumnHandle, - 'type' => $this->type, - 'typeLabel' => $this->_typeOptions[$this->type] ?? '', - 'id' => 'top-products' . StringHelper::randomString(), - ]); - } - - /** - * @inheritdoc - */ - public function getSettingsHtml(): ?string - { - $id = 'top-products' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - Craft::$app->getView()->registerAssetBundle(CommerceWidgetsAsset::class); - - return Craft::$app->getView()->renderTemplate('commerce/_components/widgets/products/top/settings', [ - 'id' => $id, - 'namespaceId' => $namespaceId, - 'widget' => $this, - 'typeOptions' => $this->_typeOptions, - 'revenueOptions' => $this->_revenueCheckboxOptions, - 'orderStatuses' => $this->getOrderStatusOptions(), - 'isRevenueOptionsEnabled' => $this->type === TopProductsStat::TYPE_REVENUE, - ]); - } -} diff --git a/src/widgets/TopPurchasables.php b/src/widgets/TopPurchasables.php deleted file mode 100644 index 06eaba3c62..0000000000 --- a/src/widgets/TopPurchasables.php +++ /dev/null @@ -1,198 +0,0 @@ - - * @since 3.0 - */ -class TopPurchasables extends Widget -{ - use StatWidgetTrait; - - /** - * @var string|null Options 'revenue', 'qty'. - */ - public ?string $type = null; - - /** - * @var string options 'description', 'sku'. - */ - public string $nameField; - - /** - * @var TopPurchasablesStat - */ - private TopPurchasablesStat $_stat; - - /** - * @var string - */ - private string $_title; - - /** - * @var array - */ - private array $_typeOptions; - - /** - * @var array - */ - private array $_nameFieldOptions; - - /** - * @inheritDoc - */ - public function init(): void - { - if (!(isset($this->storeId)) || !$this->storeId) { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $this->storeId = $site?->getStore()->id ?? Plugin::getInstance()->getStores()->getPrimaryStore()->id; - } - - $this->nameField = isset($this->nameField) ?: 'description'; - - $this->_nameFieldOptions = [ - 'description' => Craft::t('commerce', 'Description'), - 'sku' => Craft::t('commerce', 'SKU'), - ]; - - $this->_typeOptions = [ - 'qty' => Craft::t('commerce', 'Qty'), - 'revenue' => Craft::t('commerce', 'Revenue'), - ]; - - $this->_title = match ($this->type) { - 'revenue' => Craft::t('commerce', 'Top Purchasables by Revenue'), - 'qty' => Craft::t('commerce', 'Top Purchasables by Qty Sold'), - default => Craft::t('commerce', 'Top Purchasables'), - }; - - $this->dateRange = !isset($this->dateRange) || !$this->dateRange ? TopPurchasablesStat::DATE_RANGE_TODAY : $this->dateRange; - - $this->_stat = new TopPurchasablesStat( - $this->dateRange, - $this->type, - DateTimeHelper::toDateTime($this->startDate, true), - DateTimeHelper::toDateTime($this->endDate, true), - $this->storeId - ); - - if (!empty($this->orderStatuses)) { - $this->_stat->setOrderStatuses($this->orderStatuses); - } - - parent::init(); - } - - /** - * @inheritdoc - */ - public static function isSelectable(): bool - { - return Craft::$app->getUser()->checkPermission('commerce-manageOrders'); - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Top Purchasables'); - } - - /** - * @inheritdoc - */ - public static function icon(): ?string - { - return Craft::getAlias('@craft/commerce/icon-mask.svg'); - } - - /** - * @inheritdoc - */ - public function getTitle(): ?string - { - return $this->_title; - } - - /** - * @inheritDoc - */ - public function getSubtitle(): ?string - { - return $this->_stat->getDateRangeWording(); - } - - /** - * @inheritdoc - */ - public function getBodyHtml(): ?string - { - $stats = $this->_stat->get(); - - if (empty($stats)) { - return Html::tag('p', Craft::t('commerce', 'No stats available.'), ['class' => 'zilch']); - } - - $view = Craft::$app->getView(); - $view->registerAssetBundle(StatWidgetsAsset::class); - $view->registerAssetBundle(AdminTableAsset::class); - - return $view->renderTemplate('commerce/_components/widgets/purchasables/top/body', [ - 'stats' => $stats, - 'type' => $this->type, - 'nameField' => $this->nameField, - 'nameFieldLabel' => $this->_nameFieldOptions[$this->nameField] ?? '', - 'typeLabel' => $this->_typeOptions[$this->type] ?? '', - 'id' => 'top-purchasables' . StringHelper::randomString(), - ]); - } - - /** - * @inheritdoc - */ - public function getSettingsHtml(): ?string - { - $id = 'top-purchasables' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - Craft::$app->getView()->registerAssetBundle(CommerceWidgetsAsset::class); - - return Craft::$app->getView()->renderTemplate('commerce/_components/widgets/purchasables/top/settings', [ - 'id' => $id, - 'namespaceId' => $namespaceId, - 'widget' => $this, - 'orderStatuses' => $this->getOrderStatusOptions(), - 'typeOptions' => $this->_typeOptions, - 'nameFieldOptions' => $this->_nameFieldOptions, - ]); - } -} diff --git a/src/widgets/TotalOrders.php b/src/widgets/TotalOrders.php deleted file mode 100644 index b316ea26cb..0000000000 --- a/src/widgets/TotalOrders.php +++ /dev/null @@ -1,183 +0,0 @@ - - * @since 3.0 - */ -class TotalOrders extends Widget -{ - use StatWidgetTrait; - - /** - * @var int|bool - */ - public mixed $showChart = null; - - /** - * @var null|TotalOrdersStat - */ - private ?TotalOrdersStat $_stat = null; - - public function init(): void - { - parent::init(); - - if (!(isset($this->storeId)) || !$this->storeId) { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $this->storeId = $site?->getStore()->id ?? Plugin::getInstance()->getStores()->getPrimaryStore()->id; - } - - $this->dateRange = !isset($this->dateRange) || !$this->dateRange ? TotalOrdersStat::DATE_RANGE_TODAY : $this->dateRange; - - $this->_stat = new TotalOrdersStat( - $this->dateRange, - DateTimeHelper::toDateTime($this->startDate, true), - DateTimeHelper::toDateTime($this->endDate, true), - $this->storeId - ); - - if (!empty($this->orderStatuses)) { - $this->_stat->setOrderStatuses($this->orderStatuses); - } - } - - /** - * @inheritdoc - */ - public static function isSelectable(): bool - { - return Craft::$app->getUser()->checkPermission('commerce-manageOrders'); - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Total Orders'); - } - - /** - * @inheritdoc - */ - public static function icon(): ?string - { - return Craft::getAlias('@craft/commerce/icon-mask.svg'); - } - - /** - * @inheritdoc - */ - public function getTitle(): ?string - { - if (!$this->showChart) { - return ''; - } - - $stats = $this->_stat->get(); - $total = $stats['total'] ?? 0; - $total = Craft::$app->getFormatter()->asInteger($total); - - return Craft::t('commerce', '{total} orders', ['total' => $total]); - } - - public function getSubtitle(): ?string - { - if (!$this->showChart) { - return ''; - } - - return $this->_stat->getDateRangeWording(); - } - - /** - * @inheritdoc - */ - public function getBodyHtml(): ?string - { - $showChart = $this->showChart; - $stats = $this->_stat->get(); - - if (empty($stats)) { - return Html::tag('p', Craft::t('commerce', 'No stats available.'), ['class' => 'zilch']); - } - - $number = $stats['total'] ?? 0; - $chart = $stats['chart'] ?? []; - - $labels = ArrayHelper::getColumn($chart, 'datekey', false); - $data = ArrayHelper::getColumn($chart, 'total', false); - - $timeFrame = $this->_stat->getDateRangeWording(); - $number = Craft::$app->getFormatter()->asInteger($number); - - $id = 'total-orders' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - $view = Craft::$app->getView(); - $view->registerAssetBundle(StatWidgetsAsset::class); - - return $view->renderTemplate('commerce/_components/widgets/orders/total/body', compact( - 'namespaceId', - 'number', - 'timeFrame', - 'labels', - 'data', - 'showChart' - )); - } - - /** - * @inheritDoc - */ - public static function maxColspan(): ?int - { - return 1; - } - - /** - * @inheritdoc - */ - public function getSettingsHtml(): ?string - { - $id = 'total-orders' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - Craft::$app->getView()->registerAssetBundle(CommerceWidgetsAsset::class); - - return Craft::$app->getView()->renderTemplate('commerce/_components/widgets/orders/total/settings', [ - 'id' => $id, - 'namespaceId' => $namespaceId, - 'orderStatuses' => $this->getOrderStatusOptions(), - 'widget' => $this, - ]); - } -} diff --git a/src/widgets/TotalOrdersByCountry.php b/src/widgets/TotalOrdersByCountry.php deleted file mode 100644 index 2931cded64..0000000000 --- a/src/widgets/TotalOrdersByCountry.php +++ /dev/null @@ -1,187 +0,0 @@ - - * @since 3.0 - */ -class TotalOrdersByCountry extends Widget -{ - use StatWidgetTrait; - - /** - * @var string Options 'billing', 'shipping'. - */ - public string $type; - - /** - * @var TotalOrdersByCountryStat - */ - private TotalOrdersByCountryStat $_stat; - - /** - * @var string - */ - private string $_title; - - /** - * @var array - */ - private array $_typeOptions; - - /** - * @inheritDoc - */ - public function init(): void - { - parent::init(); - - if (!(isset($this->storeId)) || !$this->storeId) { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $this->storeId = $site?->getStore()->id ?? Plugin::getInstance()->getStores()->getPrimaryStore()->id; - } - - $this->_typeOptions = [ - 'billing' => Craft::t('commerce', 'Billing'), - 'shipping' => Craft::t('commerce', 'Shipping'), - ]; - - if (isset($this->type) && $this->type == 'billing') { - $this->_title = Craft::t('commerce', 'Total Orders by Billing Country'); - } else { - $this->_title = Craft::t('commerce', 'Total Orders by Shipping Country'); - $this->type = 'shipping'; - } - - $this->dateRange = !isset($this->dateRange) || !$this->dateRange ? TotalOrdersByCountryStat::DATE_RANGE_TODAY : $this->dateRange; - - $this->_stat = new TotalOrdersByCountryStat( - $this->dateRange, - $this->type, - DateTimeHelper::toDateTime($this->startDate, true), - DateTimeHelper::toDateTime($this->endDate, true), - $this->storeId - ); - - if (!empty($this->orderStatuses)) { - $this->_stat->setOrderStatuses($this->orderStatuses); - } - } - - /** - * @inheritDoc - */ - public function getTitle(): ?string - { - return $this->_title; - } - - /** - * @inheritDoc - */ - public function getSubtitle(): ?string - { - return $this->_stat->getDateRangeWording(); - } - - /** - * @inheritdoc - */ - public static function isSelectable(): bool - { - return Craft::$app->getUser()->checkPermission('commerce-manageOrders'); - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Total Orders by Country'); - } - - /** - * @inheritdoc - */ - public static function icon(): ?string - { - return Craft::getAlias('@craft/commerce/icon-mask.svg'); - } - - /** - * @inheritdoc - */ - public function getBodyHtml(): ?string - { - $stats = $this->_stat->get(); - - if (empty($stats)) { - return Html::tag('p', Craft::t('commerce', 'No stats available.'), ['class' => 'zilch']); - } - - $view = Craft::$app->getView(); - $view->registerAssetBundle(StatWidgetsAsset::class); - - $id = 'total-revenue' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - $labels = ArrayHelper::getColumn($stats, 'name', false); - $totalOrders = ArrayHelper::getColumn($stats, 'total', false); - - return $view->renderTemplate('commerce/_components/widgets/orders/country/body', - compact( - 'stats', - 'namespaceId', - 'labels', - 'totalOrders' - ) - ); - } - - /** - * @inheritdoc - */ - public function getSettingsHtml(): ?string - { - $id = 'total-orders' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - Craft::$app->getView()->registerAssetBundle(CommerceWidgetsAsset::class); - - return Craft::$app->getView()->renderTemplate('commerce/_components/widgets/orders/country/settings', [ - 'id' => $id, - 'namespaceId' => $namespaceId, - 'orderStatuses' => $this->getOrderStatusOptions(), - 'widget' => $this, - 'typeOptions' => $this->_typeOptions, - ]); - } -} diff --git a/src/widgets/TotalRevenue.php b/src/widgets/TotalRevenue.php deleted file mode 100644 index a3af34f05e..0000000000 --- a/src/widgets/TotalRevenue.php +++ /dev/null @@ -1,215 +0,0 @@ - - * @since 3.0 - */ -class TotalRevenue extends Widget -{ - use StatWidgetTrait; - - /** - * @var string - * @since 4.1.0 - */ - public string $type = TotalRevenueStat::TYPE_TOTAL; - - /** - * @var bool - */ - public bool $showOrderCount = false; - - /** - * @var TotalRevenueStat - */ - private TotalRevenueStat $_stat; - - /** - * @inheritdoc - */ - protected function defineRules(): array - { - $rules = parent::defineRules(); - $rules[] = [['type'], 'in', 'range' => [TotalRevenueStat::TYPE_TOTAL, TotalRevenueStat::TYPE_TOTAL_PAID]]; - - return $rules; - } - - /** - * @inheritDoc - */ - public function init(): void - { - parent::init(); - - if (!(isset($this->storeId)) || !$this->storeId) { - /** @var Site|StoreBehavior|null $site */ - $site = Cp::requestedSite(); - $this->storeId = $site?->getStore()->id ?? Plugin::getInstance()->getStores()->getPrimaryStore()->id; - } - - $this->dateRange = !isset($this->dateRange) || !$this->dateRange ? TotalRevenueStat::DATE_RANGE_TODAY : $this->dateRange; - - $this->_stat = new TotalRevenueStat( - $this->dateRange, - DateTimeHelper::toDateTime($this->startDate, true), - DateTimeHelper::toDateTime($this->endDate, true), - $this->storeId - ); - - if (!empty($this->orderStatuses)) { - $this->_stat->setOrderStatuses($this->orderStatuses); - } - - $this->_stat->type = $this->type; - } - - /** - * @inheritdoc - */ - public static function isSelectable(): bool - { - return Craft::$app->getUser()->checkPermission('commerce-manageOrders'); - } - - /** - * @inheritdoc - */ - public static function displayName(): string - { - return Craft::t('commerce', 'Total Revenue'); - } - - /** - * @inheritdoc - */ - public function getTitle(): ?string - { - $stats = $this->_stat->get(); - $revenue = ArrayHelper::getColumn($stats, 'revenue', false); - $total = round(array_sum($revenue), 0, PHP_ROUND_HALF_DOWN); - - $formattedTotal = Currency::formatAsCurrency($total, $this->getStore()->getCurrency()->getCode(), false, true, true); - - return Craft::t('commerce', '{total} in total revenue', ['total' => $formattedTotal]); - } - - /** - * @inheritDoc - */ - public function getSubtitle(): ?string - { - return $this->_stat->getDateRangeWording(); - } - - /** - * @inheritdoc - */ - public static function icon(): ?string - { - return Craft::getAlias('@craft/commerce/icon-mask.svg'); - } - - /** - * @inheritdoc - */ - public function getBodyHtml(): ?string - { - $stats = $this->_stat->get(); - $timeFrame = $this->_stat->getDateRangeWording(); - $chartInterval = $this->_stat->getDateRangeInterval(); - - $view = Craft::$app->getView(); - $view->registerAssetBundle(StatWidgetsAsset::class); - - $id = 'total-revenue' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - if (empty($stats)) { - return Html::tag('p', Craft::t('commerce', 'No stats available.'), ['class' => 'zilch']); - } - - $labels = ArrayHelper::getColumn($stats, 'datekey', false); - if ($this->_stat->getDateRangeInterval() == 'month') { - $labels = array_map(static function($label) { - [$year, $month] = explode('-', $label); - $month = $month < 10 ? '0' . $month : $month; - return implode('-', [$year, $month, '01']); - }, $labels); - } elseif ($this->_stat->getDateRangeInterval() == 'week') { - $labels = array_map(static function($label) { - $year = substr($label, 0, 4); - $week = substr($label, -2); - return $year . 'W' . $week; - }, $labels); - } - - $revenue = ArrayHelper::getColumn($stats, 'revenue', false); - $orderCount = ArrayHelper::getColumn($stats, 'count', false); - $widget = $this; - - return $view->renderTemplate('commerce/_components/widgets/orders/revenue/body', - compact( - 'widget', - 'stats', - 'timeFrame', - 'namespaceId', - 'labels', - 'revenue', - 'orderCount', - 'chartInterval' - ) - ); - } - - /** - * @inheritdoc - */ - public function getSettingsHtml(): ?string - { - $id = 'total-revenue' . StringHelper::randomString(); - $namespaceId = Craft::$app->getView()->namespaceInputId($id); - - Craft::$app->getView()->registerAssetBundle(CommerceWidgetsAsset::class); - - return Craft::$app->getView()->renderTemplate('commerce/_components/widgets/orders/revenue/settings', [ - 'id' => $id, - 'namespaceId' => $namespaceId, - 'widget' => $this, - 'orderStatuses' => $this->getOrderStatusOptions(), - 'types' => [ - TotalRevenueStat::TYPE_TOTAL => Craft::t('commerce', 'Total'), - TotalRevenueStat::TYPE_TOTAL_PAID => Craft::t('commerce', 'Total Paid'), - ], - ]); - } -} diff --git a/testbench.yaml b/testbench.yaml new file mode 100644 index 0000000000..c1c8b93955 --- /dev/null +++ b/testbench.yaml @@ -0,0 +1,6 @@ +providers: + - Inertia\ServiceProvider + - CraftCms\Aliases\AliasesServiceProvider + - CraftCms\DependencyAwareCache\CacheServiceProvider + - CraftCms\Cms\Providers\CraftServiceProvider + - CraftCms\Yii2Adapter\Yii2ServiceProvider diff --git a/tests/fixtures/BaseModelFixture.php b/tests-yii2/fixtures/BaseModelFixture.php similarity index 100% rename from tests/fixtures/BaseModelFixture.php rename to tests-yii2/fixtures/BaseModelFixture.php diff --git a/tests/fixtures/CategoriesFixture.php b/tests-yii2/fixtures/CategoriesFixture.php similarity index 100% rename from tests/fixtures/CategoriesFixture.php rename to tests-yii2/fixtures/CategoriesFixture.php diff --git a/tests/fixtures/CustomerAddressFixture.php b/tests-yii2/fixtures/CustomerAddressFixture.php similarity index 100% rename from tests/fixtures/CustomerAddressFixture.php rename to tests-yii2/fixtures/CustomerAddressFixture.php diff --git a/tests/fixtures/CustomerFixture.php b/tests-yii2/fixtures/CustomerFixture.php similarity index 100% rename from tests/fixtures/CustomerFixture.php rename to tests-yii2/fixtures/CustomerFixture.php diff --git a/tests-yii2/fixtures/DiscountsFixture.php b/tests-yii2/fixtures/DiscountsFixture.php new file mode 100644 index 0000000000..4b7f7d0de4 --- /dev/null +++ b/tests-yii2/fixtures/DiscountsFixture.php @@ -0,0 +1,102 @@ + + * @author Global Network Group | Giel Tettelaar + * @since 2.1 + */ +class DiscountsFixture extends BaseModelFixture +{ + /** + * @inheritdoc + */ + public $dataFile = __DIR__ . '/data/discounts.php'; + + /** + * @inheritdoc + */ + public $modelClass = Discount::class; + + /** + * @inheritDoc + */ + public string $saveMethod = 'saveDiscount'; + + /** + * @inheritDoc + */ + public string $deleteMethod = 'deleteDiscountById'; + + /** + * @inheritDoc + */ + public $service = 'discounts'; + + /** + * @inheritDoc + */ + public function init(): void + { + $this->service = Plugin::getInstance()->get($this->service); + + parent::init(); + } + + /** + * @inheritdoc + */ + protected function prepData($data) + { + if (empty($data['_coupons'])) { + unset($data['_coupons']); + return $data; + } + + $data['coupons'] = []; + foreach ($data['_coupons'] as $c) { + $data['coupons'][] = \Craft::createObject(Coupon::class, ['config' => [ + 'attributes' => $c, + ]]); + } + + unset($data['_coupons']); + return $data; + } + + /** + * @inheritdoc + */ + public function unload(): void + { + // @TODO Investigate why the FK cascade delete on coupons does not fire during fixture unload, then remove this manual cleanup + if (isset($this->data) && !empty($this->data)) { + foreach ($this->data as $discount) { + $coupons = CouponRecord::where('discountId', $discount['id'])->get(); + + if (empty($coupons)) { + continue; + } + + foreach ($coupons as $coupon) { + $coupon->delete(); + } + } + } + + parent::unload(); + } +} diff --git a/tests/fixtures/EmailsFixture.php b/tests-yii2/fixtures/EmailsFixture.php similarity index 100% rename from tests/fixtures/EmailsFixture.php rename to tests-yii2/fixtures/EmailsFixture.php diff --git a/tests/fixtures/FieldLayoutFixture.php b/tests-yii2/fixtures/FieldLayoutFixture.php similarity index 100% rename from tests/fixtures/FieldLayoutFixture.php rename to tests-yii2/fixtures/FieldLayoutFixture.php diff --git a/tests/fixtures/OrderStatusesFixture.php b/tests-yii2/fixtures/OrderStatusesFixture.php similarity index 100% rename from tests/fixtures/OrderStatusesFixture.php rename to tests-yii2/fixtures/OrderStatusesFixture.php diff --git a/tests/fixtures/OrdersFixture.php b/tests-yii2/fixtures/OrdersFixture.php similarity index 100% rename from tests/fixtures/OrdersFixture.php rename to tests-yii2/fixtures/OrdersFixture.php diff --git a/tests/fixtures/PaymentCurrenciesFixture.php b/tests-yii2/fixtures/PaymentCurrenciesFixture.php similarity index 100% rename from tests/fixtures/PaymentCurrenciesFixture.php rename to tests-yii2/fixtures/PaymentCurrenciesFixture.php diff --git a/tests-yii2/fixtures/ProductFixture.php b/tests-yii2/fixtures/ProductFixture.php new file mode 100644 index 0000000000..fb3e23de34 --- /dev/null +++ b/tests-yii2/fixtures/ProductFixture.php @@ -0,0 +1,31 @@ + + * @since 3.1.4 + * @method Product getElement(string $key) + */ +class ProductFixture extends BaseProductFixture +{ + /** + * @inheritdoc + */ + public $dataFile = __DIR__ . '/data/products.php'; + + /** + * @inheritdoc + */ + public $depends = [ProductTypeFixture::class]; +} diff --git a/tests/fixtures/ProductTypeFixture.php b/tests-yii2/fixtures/ProductTypeFixture.php similarity index 100% rename from tests/fixtures/ProductTypeFixture.php rename to tests-yii2/fixtures/ProductTypeFixture.php diff --git a/tests/fixtures/ProductTypeSitesFixture.php b/tests-yii2/fixtures/ProductTypeSitesFixture.php similarity index 100% rename from tests/fixtures/ProductTypeSitesFixture.php rename to tests-yii2/fixtures/ProductTypeSitesFixture.php diff --git a/tests-yii2/fixtures/ProductTypesShippingCategoriesFixture.php b/tests-yii2/fixtures/ProductTypesShippingCategoriesFixture.php new file mode 100644 index 0000000000..ec40c922bd --- /dev/null +++ b/tests-yii2/fixtures/ProductTypesShippingCategoriesFixture.php @@ -0,0 +1,44 @@ +data = $this->loadData($this->dataFile, false); + + foreach ($this->data as $row) { + DB::table(Table::PRODUCTTYPES_SHIPPINGCATEGORIES)->insert($row); + } + } + + #[\Override] + public function unload(): void + { + foreach ($this->data as $row) { + DB::table(Table::PRODUCTTYPES_SHIPPINGCATEGORIES)->where('id', $row['id'])->delete(); + } + + $this->data = []; + } +} diff --git a/tests-yii2/fixtures/ProductTypesTaxCategoriesFixture.php b/tests-yii2/fixtures/ProductTypesTaxCategoriesFixture.php new file mode 100644 index 0000000000..1e48b5dc0a --- /dev/null +++ b/tests-yii2/fixtures/ProductTypesTaxCategoriesFixture.php @@ -0,0 +1,44 @@ +data = $this->loadData($this->dataFile, false); + + foreach ($this->data as $row) { + DB::table(Table::PRODUCTTYPES_TAXCATEGORIES)->insert($row); + } + } + + #[\Override] + public function unload(): void + { + foreach ($this->data as $row) { + DB::table(Table::PRODUCTTYPES_TAXCATEGORIES)->where('id', $row['id'])->delete(); + } + + $this->data = []; + } +} diff --git a/tests/fixtures/SalesFixture.php b/tests-yii2/fixtures/SalesFixture.php similarity index 100% rename from tests/fixtures/SalesFixture.php rename to tests-yii2/fixtures/SalesFixture.php diff --git a/tests/fixtures/ShippingCategoryFixture.php b/tests-yii2/fixtures/ShippingCategoryFixture.php similarity index 100% rename from tests/fixtures/ShippingCategoryFixture.php rename to tests-yii2/fixtures/ShippingCategoryFixture.php diff --git a/tests/fixtures/ShippingFixture.php b/tests-yii2/fixtures/ShippingFixture.php similarity index 100% rename from tests/fixtures/ShippingFixture.php rename to tests-yii2/fixtures/ShippingFixture.php diff --git a/tests/fixtures/ShippingMethodsFixture.php b/tests-yii2/fixtures/ShippingMethodsFixture.php similarity index 100% rename from tests/fixtures/ShippingMethodsFixture.php rename to tests-yii2/fixtures/ShippingMethodsFixture.php diff --git a/tests/fixtures/ShippingZonesFixture.php b/tests-yii2/fixtures/ShippingZonesFixture.php similarity index 100% rename from tests/fixtures/ShippingZonesFixture.php rename to tests-yii2/fixtures/ShippingZonesFixture.php diff --git a/tests/fixtures/SitesFixture.php b/tests-yii2/fixtures/SitesFixture.php similarity index 100% rename from tests/fixtures/SitesFixture.php rename to tests-yii2/fixtures/SitesFixture.php diff --git a/tests/fixtures/StoreFixture.php b/tests-yii2/fixtures/StoreFixture.php similarity index 100% rename from tests/fixtures/StoreFixture.php rename to tests-yii2/fixtures/StoreFixture.php diff --git a/tests-yii2/fixtures/TaxCategoryFixture.php b/tests-yii2/fixtures/TaxCategoryFixture.php new file mode 100644 index 0000000000..11f467b5e1 --- /dev/null +++ b/tests-yii2/fixtures/TaxCategoryFixture.php @@ -0,0 +1,50 @@ +service = Plugin::getInstance()->get($this->service); + + parent::init(); + } +} diff --git a/tests/fixtures/UserGroupsFixture.php b/tests-yii2/fixtures/UserGroupsFixture.php similarity index 100% rename from tests/fixtures/UserGroupsFixture.php rename to tests-yii2/fixtures/UserGroupsFixture.php diff --git a/tests/fixtures/data/categories.php b/tests-yii2/fixtures/data/categories.php similarity index 100% rename from tests/fixtures/data/categories.php rename to tests-yii2/fixtures/data/categories.php diff --git a/tests/fixtures/data/customer-addresses.php b/tests-yii2/fixtures/data/customer-addresses.php similarity index 100% rename from tests/fixtures/data/customer-addresses.php rename to tests-yii2/fixtures/data/customer-addresses.php diff --git a/tests/fixtures/data/customers.php b/tests-yii2/fixtures/data/customers.php similarity index 100% rename from tests/fixtures/data/customers.php rename to tests-yii2/fixtures/data/customers.php diff --git a/tests/fixtures/data/discounts.php b/tests-yii2/fixtures/data/discounts.php similarity index 100% rename from tests/fixtures/data/discounts.php rename to tests-yii2/fixtures/data/discounts.php diff --git a/tests/fixtures/data/emails.php b/tests-yii2/fixtures/data/emails.php similarity index 100% rename from tests/fixtures/data/emails.php rename to tests-yii2/fixtures/data/emails.php diff --git a/tests/fixtures/data/field-layout.php b/tests-yii2/fixtures/data/field-layout.php similarity index 100% rename from tests/fixtures/data/field-layout.php rename to tests-yii2/fixtures/data/field-layout.php diff --git a/tests/fixtures/data/inventory-items.php b/tests-yii2/fixtures/data/inventory-items.php similarity index 100% rename from tests/fixtures/data/inventory-items.php rename to tests-yii2/fixtures/data/inventory-items.php diff --git a/tests/fixtures/data/order-statuses.php b/tests-yii2/fixtures/data/order-statuses.php similarity index 90% rename from tests/fixtures/data/order-statuses.php rename to tests-yii2/fixtures/data/order-statuses.php index b91977b054..32305a40f9 100644 --- a/tests/fixtures/data/order-statuses.php +++ b/tests-yii2/fixtures/data/order-statuses.php @@ -11,7 +11,7 @@ 'sortOrder' => 1, 'default' => 1, // Because this is already in the DB, retrieve the `uid` - 'uid' => \craft\commerce\records\OrderStatus::find()->where(['id' => '1'])->one()->uid, + 'uid' => \CraftCms\Commerce\Order\Models\OrderStatus::where('id', '1')->first()->uid, ], [ 'storeId' => 1, // Primary diff --git a/tests/fixtures/data/orders.php b/tests-yii2/fixtures/data/orders.php similarity index 96% rename from tests/fixtures/data/orders.php rename to tests-yii2/fixtures/data/orders.php index 40594aadb1..e1ec748b35 100644 --- a/tests/fixtures/data/orders.php +++ b/tests-yii2/fixtures/data/orders.php @@ -7,7 +7,7 @@ use craft\commerce\elements\Variant; use craft\commerce\Plugin; -use craft\commerce\records\OrderStatus; +use CraftCms\Commerce\Order\Models\OrderStatus; $variants = Variant::find()->indexBy('sku')->all(); @@ -25,7 +25,7 @@ 'note' => '', 'taxCategoryId' => 1, ]; -$orderStatuses = OrderStatus::find()->select(['id', 'handle'])->indexBy('handle')->column(); +$orderStatuses = OrderStatus::pluck('id', 'handle'); $yesterday = new DateTime(); $yesterday->setTimezone(new DateTimeZone('America/Los_Angeles')); diff --git a/tests/fixtures/data/payment-currencies.php b/tests-yii2/fixtures/data/payment-currencies.php similarity index 100% rename from tests/fixtures/data/payment-currencies.php rename to tests-yii2/fixtures/data/payment-currencies.php diff --git a/tests/fixtures/data/product-types-shipping-categories.php b/tests-yii2/fixtures/data/product-types-shipping-categories.php similarity index 100% rename from tests/fixtures/data/product-types-shipping-categories.php rename to tests-yii2/fixtures/data/product-types-shipping-categories.php diff --git a/tests/fixtures/data/product-types-sites.php b/tests-yii2/fixtures/data/product-types-sites.php similarity index 100% rename from tests/fixtures/data/product-types-sites.php rename to tests-yii2/fixtures/data/product-types-sites.php diff --git a/tests/fixtures/data/product-types-tax-categories.php b/tests-yii2/fixtures/data/product-types-tax-categories.php similarity index 100% rename from tests/fixtures/data/product-types-tax-categories.php rename to tests-yii2/fixtures/data/product-types-tax-categories.php diff --git a/tests/fixtures/data/product-types.php b/tests-yii2/fixtures/data/product-types.php similarity index 100% rename from tests/fixtures/data/product-types.php rename to tests-yii2/fixtures/data/product-types.php diff --git a/tests/fixtures/data/products.php b/tests-yii2/fixtures/data/products.php similarity index 100% rename from tests/fixtures/data/products.php rename to tests-yii2/fixtures/data/products.php diff --git a/tests/fixtures/data/sales.php b/tests-yii2/fixtures/data/sales.php similarity index 100% rename from tests/fixtures/data/sales.php rename to tests-yii2/fixtures/data/sales.php diff --git a/tests/fixtures/data/shipping-category.php b/tests-yii2/fixtures/data/shipping-category.php similarity index 100% rename from tests/fixtures/data/shipping-category.php rename to tests-yii2/fixtures/data/shipping-category.php diff --git a/tests/fixtures/data/shipping-methods.php b/tests-yii2/fixtures/data/shipping-methods.php similarity index 100% rename from tests/fixtures/data/shipping-methods.php rename to tests-yii2/fixtures/data/shipping-methods.php diff --git a/tests/fixtures/data/shipping-rules.php b/tests-yii2/fixtures/data/shipping-rules.php similarity index 100% rename from tests/fixtures/data/shipping-rules.php rename to tests-yii2/fixtures/data/shipping-rules.php diff --git a/tests/fixtures/data/shipping-zones.php b/tests-yii2/fixtures/data/shipping-zones.php similarity index 100% rename from tests/fixtures/data/shipping-zones.php rename to tests-yii2/fixtures/data/shipping-zones.php diff --git a/tests/fixtures/data/sites.php b/tests-yii2/fixtures/data/sites.php similarity index 100% rename from tests/fixtures/data/sites.php rename to tests-yii2/fixtures/data/sites.php diff --git a/tests/fixtures/data/stores.php b/tests-yii2/fixtures/data/stores.php similarity index 100% rename from tests/fixtures/data/stores.php rename to tests-yii2/fixtures/data/stores.php diff --git a/tests/fixtures/data/tax-category.php b/tests-yii2/fixtures/data/tax-category.php similarity index 100% rename from tests/fixtures/data/tax-category.php rename to tests-yii2/fixtures/data/tax-category.php diff --git a/tests/fixtures/data/user-addresses.php b/tests-yii2/fixtures/data/user-addresses.php similarity index 100% rename from tests/fixtures/data/user-addresses.php rename to tests-yii2/fixtures/data/user-addresses.php diff --git a/tests/fixtures/data/user-groups.php b/tests-yii2/fixtures/data/user-groups.php similarity index 100% rename from tests/fixtures/data/user-groups.php rename to tests-yii2/fixtures/data/user-groups.php diff --git a/tests-yii2/fixtures/elements/ProductFixture.php b/tests-yii2/fixtures/elements/ProductFixture.php new file mode 100644 index 0000000000..9eddb3a0de --- /dev/null +++ b/tests-yii2/fixtures/elements/ProductFixture.php @@ -0,0 +1,141 @@ + + * @author Robuust digital | Bob Olde Hampsink + * @author Global Network Group | Giel Tettelaar + * @since 2.1 + */ +class ProductFixture extends BaseElementFixture +{ + /** + * @var array + */ + protected array $productTypeIds = []; + + private ?VariantCollection $_variants = null; + + /** + * {@inheritdoc} + */ + public function init(): void + { + parent::init(); + + // Ensure loaded + $commerce = Plugin::getInstance(); + if (!$commerce) { + throw new InvalidArgumentException('Commerce plugin needs to be loaded before using the ProductFixture'); + } + + // Get all product type id's + $this->productTypeIds = $this->_getProductTypeIds(); + } + + public function afterLoad(): void + { + $this->productTypeIds = $this->_getProductTypeIds(); + + // Generate catalog pricing + Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); + } + + protected function createElement(): ElementInterface + { + return new Product(); + } + + /** + * Get array of product type IDs indexed by handle. + * This uses a raw query to avoid service level caching/memoization. + * + * @todo Review whether this raw-query workaround for service-level memoization is still needed in Commerce 6.0 #COM-54 + */ + private function _getProductTypeIds(): array + { + return new Query() + ->select([ + 'productTypes.id', + 'productTypes.handle', + ]) + ->from([Table::PRODUCTTYPES . ' productTypes']) + ->indexBy('handle') + ->column(); + } + + /** + * @inheritdoc + * @param Product $element + */ + protected function populateElement(ElementInterface $element, array $attributes): void + { + foreach ($attributes as $name => $value) { + if ($name !== '_variants') { + $element->$name = $value; + } else { + $this->_variants = VariantCollection::make($value); + $element->setVariants($value); + } + } + } + + protected function saveElement(ElementInterface $element): bool + { + $return = parent::saveElement($element); + + // Save the variants + $this->_variants->each(function(Variant $v) use ($element) { + if (new Query() + ->from(Table::VARIANTS . ' v') + ->leftJoin(Table::PURCHASABLES . ' p', '[[p.id]] = [[v.id]]') + ->where(['primaryOwnerId' => $element->id]) + ->andWhere(['p.sku' => $v->getSku()]) + ->exists() + ) { + return; + } + + $v->setPrimaryOwnerId($element->id); + $v->setOwnerId($element->id); + \Craft::$app->getElements()->saveElement($v,false); + }); + + $this->_variants = null; + + return $return; + } + + protected function deleteElement(ElementInterface $element): bool + { + /** @var Product $element */ + $variants = $element->getVariants(true); + + foreach ($variants as $variant) { + Craft::$app->getElements()->deleteElement($variant, true); + } + + return parent::deleteElement($element); + } +} diff --git a/tests-yii2/mockclasses/Purchasable.php b/tests-yii2/mockclasses/Purchasable.php new file mode 100644 index 0000000000..975df68bd9 --- /dev/null +++ b/tests-yii2/mockclasses/Purchasable.php @@ -0,0 +1,40 @@ + + * @author Global Network Group | Giel Tettelaar + * @since 2.1 + */ +class Purchasable extends BasePurchasable +{ + public bool $isPromotable = true; + + public float $price = 25.10; + + public function getIsPromotable(): bool + { + return $this->isPromotable; + } + + public function getPrice(string|Store|null $store = null): ?float + { + return 25.10; + } + + public function getSku(): string + { + return 'commerce_testing_unique_sku'; + } +} diff --git a/tests/.env.example.mysql b/tests/.env.example.mysql deleted file mode 100644 index 2be345a8a1..0000000000 --- a/tests/.env.example.mysql +++ /dev/null @@ -1,15 +0,0 @@ -APP_ID=CraftCMS -SECURITY_KEY=UPzGqJJMCTM4n07jkqaFNaVoof6j_Xgo - -DB_DRIVER=mysql -DB_SERVER=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=craft_test -DB_USER=root -DB_PASSWORD= -DB_SCHEMA="public" - -# Set this to the `entryUrl` param in the `codeception.yml` file. -DEFAULT_SITE_URL="https://test.craftcms.test/index.php" -FROM_EMAIL_NAME="Craft CMS" -FROM_EMAIL_ADDRESS="info@craftcms.com" \ No newline at end of file diff --git a/tests/.env.example.pgsql b/tests/.env.example.pgsql deleted file mode 100644 index aa67c08cc4..0000000000 --- a/tests/.env.example.pgsql +++ /dev/null @@ -1,15 +0,0 @@ -APP_ID=CraftCMS -SECURITY_KEY=UPzGqJJMCTM4n07jkqaFNaVoof6j_Xgo - -DB_DRIVER=pgsql -DB_SERVER=127.0.0.1 -DB_PORT=5432 -DB_DATABASE=craft_test -DB_USER=root -DB_PASSWORD= -DB_SCHEMA="public" - -# Set this to the `entryUrl` param in the `codeception.yml` file. -DEFAULT_SITE_URL="https://test.craftcms.test/index.php" -FROM_EMAIL_NAME="Craft CMS" -FROM_EMAIL_ADDRESS="info@craftcms.com" \ No newline at end of file diff --git a/tests/.gitignore b/tests/.gitignore deleted file mode 100644 index 4c49bd78f1..0000000000 --- a/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.env diff --git a/tests/_data/.gitkeep b/tests/Arch/.gitkeep similarity index 100% rename from tests/_data/.gitkeep rename to tests/Arch/.gitkeep diff --git a/tests/Arch/ArchTest.php b/tests/Arch/ArchTest.php new file mode 100644 index 0000000000..8995fbc8bb --- /dev/null +++ b/tests/Arch/ArchTest.php @@ -0,0 +1,12 @@ +expect('src') + ->not->toUse(['die', 'dd', 'dump', 'env']); + +arch('src/ should not reference legacy Craft core classes (craft\commerce is allowed during the migration)') + ->expect('src') + ->not->toUse('craft') + ->ignoring('craft\commerce'); diff --git a/tests/Feature/CatalogPricing/CatalogPricingQueueTest.php b/tests/Feature/CatalogPricing/CatalogPricingQueueTest.php new file mode 100644 index 0000000000..1e726fd6d9 --- /dev/null +++ b/tests/Feature/CatalogPricing/CatalogPricingQueueTest.php @@ -0,0 +1,324 @@ +fixture = StoresFixture::seed(); +}); + +test('createCatalogPricingJob creates a new queue row for a single purchasable ID', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + $rows = CatalogPricingQueueRecord::all(); + expect($rows)->toHaveCount(1); + + $row = $rows[0]; + expect($row->type)->toBe(CatalogPricingQueueRecord::TYPE_PURCHASABLE); + expect($row->storeId)->toBe($this->fixture->primaryStore->id); + expect($row->ids)->toBe([1]); + expect($row->reserved)->toBeFalse(); + + Queue::assertPushed(CatalogPricingJob::class); +}); + +test('createCatalogPricingJob creates a new queue row for a single rule ID', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'catalogPricingRuleIds' => [5], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + $rows = CatalogPricingQueueRecord::all(); + expect($rows)->toHaveCount(1); + + $row = $rows[0]; + expect($row->type)->toBe(CatalogPricingQueueRecord::TYPE_RULE); + expect($row->storeId)->toBe($this->fixture->primaryStore->id); + expect($row->ids)->toBe([5]); + expect($row->reserved)->toBeFalse(); +}); + +test('purchasable and rule types are queued as separate rows', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1, 2], + 'catalogPricingRuleIds' => [5, 6], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + $rows = CatalogPricingQueueRecord::orderBy('type')->get(); + expect($rows)->toHaveCount(2); + + expect($rows[0]->type)->toBe(CatalogPricingQueueRecord::TYPE_PURCHASABLE); + expect($rows[0]->ids)->toBe([1, 2]); + + expect($rows[1]->type)->toBe(CatalogPricingQueueRecord::TYPE_RULE); + expect($rows[1]->ids)->toBe([5, 6]); +}); + +test('different stores create separate queue rows for the same type', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1], + 'storeId' => $this->fixture->ukStore->id, + ]); + + $rows = CatalogPricingQueueRecord::orderBy('storeId')->get(); + expect($rows)->toHaveCount(2); + expect($rows[0]->storeId)->toBe($this->fixture->primaryStore->id); + expect($rows[1]->storeId)->toBe($this->fixture->ukStore->id); +}); + +test('multiple queue calls for the same store and type merge into one row', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1, 2], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [3, 4], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + $rows = CatalogPricingQueueRecord::all(); + expect($rows)->toHaveCount(1); + expect($rows[0]->ids)->toBe([1, 2, 3, 4]); +}); + +test('duplicate IDs across merged rows are deduplicated', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1, 2, 3], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [2, 3, 4], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + $row = CatalogPricingQueueRecord::where('storeId', $this->fixture->primaryStore->id) + ->where('type', CatalogPricingQueueRecord::TYPE_PURCHASABLE) + ->first(); + + expect($row->ids)->toBe([1, 2, 3, 4]); +}); + +test('a null storeId represents all stores', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1], + 'storeId' => null, + ]); + + $row = CatalogPricingQueueRecord::where('type', CatalogPricingQueueRecord::TYPE_PURCHASABLE)->first(); + expect($row->storeId)->toBeNull(); + expect($row->ids)->toBe([1]); +}); + +test('merging specific IDs with null IDs expands the scope to null', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1, 2], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => null, + 'storeId' => $this->fixture->primaryStore->id, + ]); + + $row = CatalogPricingQueueRecord::where('storeId', $this->fixture->primaryStore->id) + ->where('type', CatalogPricingQueueRecord::TYPE_PURCHASABLE) + ->first(); + + expect($row->ids)->toBeNull(); +}); + +test('a reserved row is not merged into, and a new row is created instead', function() { + $record = new CatalogPricingQueueRecord(); + $record->storeId = $this->fixture->primaryStore->id; + $record->type = CatalogPricingQueueRecord::TYPE_PURCHASABLE; + $record->ids = [1]; + $record->reserved = true; + $record->save(); + + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [2], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + $rows = CatalogPricingQueueRecord::where('storeId', $this->fixture->primaryStore->id) + ->where('type', CatalogPricingQueueRecord::TYPE_PURCHASABLE) + ->orderBy('reserved', 'desc') + ->get(); + + expect($rows)->toHaveCount(2); + expect($rows[0]->ids)->toBe([1]); + expect($rows[1]->ids)->toBe([2]); +}); + +test('IDs are sorted numerically', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [100, 5, 50, 1], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + $row = CatalogPricingQueueRecord::where('storeId', $this->fixture->primaryStore->id)->first(); + expect($row->ids)->toBe([1, 5, 50, 100]); +}); + +test('zero and negative IDs are filtered out', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1, 0, -5, 2], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + $row = CatalogPricingQueueRecord::where('storeId', $this->fixture->primaryStore->id)->first(); + expect($row->ids)->toBe([1, 2]); +}); + +test('areCatalogPricingJobsRunning reflects whether the queue has pending rows', function() { + expect(app(CatalogPricing::class)->areCatalogPricingJobsRunning())->toBeFalse(); + + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + expect(app(CatalogPricing::class)->areCatalogPricingJobsRunning())->toBeTrue(); +}); + +test('reserveCatalogPricingQueueRow marks a pending row as reserved', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + expect(CatalogPricingQueueRecord::where('reserved', true)->count())->toBe(0); + + $reserved = app(CatalogPricing::class)->reserveCatalogPricingQueueRow(); + + expect($reserved)->not->toBeNull(); + expect($reserved->reserved)->toBeTrue(); + expect($reserved->ids)->toBe([1]); + expect(CatalogPricingQueueRecord::find($reserved->id)->reserved)->toBeTrue(); +}); + +test('pending rows are reserved one at a time, in order, until none remain', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [2], + 'storeId' => $this->fixture->ukStore->id, + ]); + + $first = app(CatalogPricing::class)->reserveCatalogPricingQueueRow(); + expect($first->ids)->toBe([1]); + + $second = app(CatalogPricing::class)->reserveCatalogPricingQueueRow(); + expect($second->ids)->toBe([2]); + + expect(app(CatalogPricing::class)->reserveCatalogPricingQueueRow())->toBeNull(); +}); + +test('releaseCatalogPricingQueueRowById marks a reserved row as unreserved', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + $reserved = app(CatalogPricing::class)->reserveCatalogPricingQueueRow(); + expect($reserved->reserved)->toBeTrue(); + + app(CatalogPricing::class)->releaseCatalogPricingQueueRowById($reserved->id); + + expect(CatalogPricingQueueRecord::find($reserved->id)->reserved)->toBeFalse(); +}); + +test('deleteCatalogPricingQueueRowById removes a row from the queue', function() { + app(CatalogPricing::class)->createCatalogPricingJob([ + 'purchasableIds' => [1], + 'storeId' => $this->fixture->primaryStore->id, + ]); + + $row = CatalogPricingQueueRecord::where('storeId', $this->fixture->primaryStore->id)->first(); + expect($row)->not->toBeNull(); + + app(CatalogPricing::class)->deleteCatalogPricingQueueRowById($row->id); + + expect(CatalogPricingQueueRecord::find($row->id))->toBeNull(); +}); + +test('a complex sequence of queue calls across stores and types produces the expected rows', function(array $queueCalls, array $expectedRows) { + $storeIdByHandle = fn(?string $handle) => match ($handle) { + 'primary' => $this->fixture->primaryStore->id, + 'ukStore' => $this->fixture->ukStore->id, + null => null, + }; + + foreach ($queueCalls as $call) { + $call['storeId'] = $storeIdByHandle($call['storeId']); + app(CatalogPricing::class)->createCatalogPricingJob($call); + } + + $rows = CatalogPricingQueueRecord::all(); + expect($rows)->toHaveCount(count($expectedRows)); + + foreach ($expectedRows as $index => $expected) { + $row = $rows[$index]; + expect($row->storeId)->toBe($storeIdByHandle($expected['storeId'])); + expect($row->type)->toBe($expected['type']); + expect($row->ids)->toBe($expected['ids']); + } +})->with([ + 'a single store queues purchasable and rule work as separate rows' => [ + [ + ['purchasableIds' => [1, 2], 'storeId' => 'primary'], + ['catalogPricingRuleIds' => [10, 11], 'storeId' => 'primary'], + ], + [ + ['storeId' => 'primary', 'type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE, 'ids' => [1, 2]], + ['storeId' => 'primary', 'type' => CatalogPricingQueueRecord::TYPE_RULE, 'ids' => [10, 11]], + ], + ], + 'the same type merges within a store but stays separate across stores' => [ + [ + ['purchasableIds' => [1], 'storeId' => 'primary'], + ['purchasableIds' => [2], 'storeId' => 'primary'], + ['purchasableIds' => [1], 'storeId' => 'ukStore'], + ], + [ + ['storeId' => 'primary', 'type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE, 'ids' => [1, 2]], + ['storeId' => 'ukStore', 'type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE, 'ids' => [1]], + ], + ], + 'a null storeId (all stores) stays separate from a specific store' => [ + [ + ['purchasableIds' => [1, 2], 'storeId' => 'primary'], + ['purchasableIds' => [3], 'storeId' => null], + ], + [ + ['storeId' => 'primary', 'type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE, 'ids' => [1, 2]], + ['storeId' => null, 'type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE, 'ids' => [3]], + ], + ], +]); diff --git a/tests/Feature/CatalogPricing/CatalogPricingTest.php b/tests/Feature/CatalogPricing/CatalogPricingTest.php new file mode 100644 index 0000000000..bb67d304b0 --- /dev/null +++ b/tests/Feature/CatalogPricing/CatalogPricingTest.php @@ -0,0 +1,250 @@ + $conditionRules Keyed by condition property name + * ("purchasableCondition", "variantCondition", "productCondition"), each a single + * pre-configured condition rule instance to add to that condition. + */ +function createCatalogPricingRule(array $attributes, array $conditionRules = []): CatalogPricingRuleData +{ + $rule = new CatalogPricingRuleData($attributes); + + foreach ($conditionRules as $property => $conditionRule) { + $getter = 'get' . ucfirst($property); + $setter = 'set' . ucfirst($property); + $condition = $rule->$getter(); + $condition->addConditionRule($conditionRule); + $rule->$setter($condition); + } + + if (!app(CatalogPricingRules::class)->saveCatalogPricingRule($rule)) { + throw new RuntimeException('Could not save catalog pricing rule: ' . json_encode($rule->errors()->all())); + } + + return $rule; +} + +test('generateCatalogPrices with no rules copies each purchasable\'s own store base price into catalog pricing', function() { + $fixture = CatalogPricingFixture::seed(); + + app(CatalogPricing::class)->generateCatalogPrices(); + + // 3 variants exist in every store (3 x 3), plus 1 UK-only variant that only exists in the UK store. + expect(DB::table(Table::CATALOG_PRICING)->count())->toBe(10); + + foreach ([$fixture->radHood, $fixture->hctWhite, $fixture->hctBlue, $fixture->ddbRed] as $variant) { + $price = DB::table(Table::CATALOG_PRICING) + ->where('purchasableId', $variant->id) + ->where('storeId', $variant->getStore()->id) + ->value('price'); + + expect((float) $price)->toBe($variant->basePrice); + } +}); + +test('generateCatalogPrices applies a catalog pricing rule to matching variant prices', function(array $ruleAttributes, array $conditionRules, string $siteHandle, float $rate, ?array $impactedSkus) { + $fixture = CatalogPricingFixture::seed(); + $sites = $fixture->stores; + + $siteId = match ($siteHandle) { + 'usSite' => $sites->usSite->id, + 'ukSite' => $sites->ukSite->id, + }; + $storeId = match ($ruleAttributes['storeId']) { + 'primaryStore' => $sites->primaryStore->id, + 'ukStore' => $sites->ukStore->id, + }; + $ruleAttributes['storeId'] = $storeId; + + $conditionRuleInstances = []; + foreach ($conditionRules as $property => $factory) { + $conditionRuleInstances[$property] = $factory($fixture); + } + + $allSkus = ['rad-hood', 'hct-white', 'hct-blue', 'ddb-red']; + $priceBySku = Variant::find()->siteId($siteId)->sku($allSkus)->collect() + ->mapWithKeys(fn(Variant $variant) => [$variant->sku => $variant->getPrice()]); + + createCatalogPricingRule($ruleAttributes, $conditionRuleInstances); + + app(CatalogPricing::class)->generateCatalogPrices(); + + $variants = Variant::find()->siteId($siteId)->sku($allSkus)->collect(); + + $variants->each(function(Variant $variant) use ($priceBySku, $rate, $impactedSkus) { + // Variants that don't exist for this site's store hold a `0.00` base price there — nothing + // to assert against. + if ($variant->getPrice() === 0.00) { + return; + } + + $originalPrice = $priceBySku[$variant->sku]; + + if ($impactedSkus !== null && !in_array($variant->sku, $impactedSkus, true)) { + $expectedPrice = $originalPrice; + } else { + $currency = $variant->getStore()->getCurrency(); + $expectedPrice = (float) app(Currencies::class)->getTeller($currency)->multiply($originalPrice, 1 + $rate); + } + + expect($variant->getPrice())->toBe($expectedPrice); + }); +})->with([ + 'rule with no conditions applies to every variant in the store' => [ + [ + 'apply' => CatalogPricingRuleRecord::APPLY_BY_PERCENT, + 'name' => '10% off', + 'enabled' => true, + 'applyAmount' => -0.1, + 'applyPriceType' => CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE, + 'isPromotionalPrice' => false, + 'storeId' => 'primaryStore', + ], + [], + 'usSite', + -0.1, + null, + ], + 'rule with a variant SKU condition only applies to matching variants' => [ + [ + 'apply' => CatalogPricingRuleRecord::APPLY_BY_PERCENT, + 'name' => '30% off', + 'enabled' => true, + 'applyAmount' => -0.3, + 'applyPriceType' => CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE, + 'isPromotionalPrice' => false, + 'storeId' => 'primaryStore', + ], + [ + 'variantCondition' => fn() => tap(new SkuConditionRule(), function(SkuConditionRule $rule) { + $rule->operator = 'ew'; + $rule->value = 'hood'; + }), + ], + 'usSite', + -0.3, + ['rad-hood'], + ], + 'rule with a purchasable SKU condition only applies to matching purchasables' => [ + [ + 'apply' => CatalogPricingRuleRecord::APPLY_BY_PERCENT, + 'name' => '20% off', + 'enabled' => true, + 'applyAmount' => -0.2, + 'applyPriceType' => CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE, + 'isPromotionalPrice' => false, + 'storeId' => 'primaryStore', + ], + [ + 'purchasableCondition' => fn() => tap(new SkuConditionRule(), function(SkuConditionRule $rule) { + $rule->operator = 'bw'; + $rule->value = 'rad'; + }), + ], + 'usSite', + -0.2, + ['rad-hood'], + ], + 'rule with a product type condition only applies to matching products' => [ + [ + 'apply' => CatalogPricingRuleRecord::APPLY_BY_PERCENT, + 'name' => '10% off', + 'enabled' => true, + 'applyAmount' => -0.1, + 'applyPriceType' => CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE, + 'isPromotionalPrice' => false, + 'storeId' => 'primaryStore', + ], + [ + 'productCondition' => fn(CatalogPricingFixture $fixture) => tap(new ProductTypeConditionRule(), function(ProductTypeConditionRule $rule) use ($fixture) { + $rule->setValues([$fixture->tShirtsType->uid]); + }), + ], + 'usSite', + -0.1, + ['hct-white', 'hct-blue'], + ], + 'rule scoped to a non-primary store still applies to that store\'s products' => [ + [ + 'apply' => CatalogPricingRuleRecord::APPLY_BY_PERCENT, + 'name' => '10% off', + 'enabled' => true, + 'applyAmount' => -0.1, + 'applyPriceType' => CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE, + 'isPromotionalPrice' => false, + 'storeId' => 'ukStore', + ], + [ + 'productCondition' => fn(CatalogPricingFixture $fixture) => tap(new ProductTypeConditionRule(), function(ProductTypeConditionRule $rule) use ($fixture) { + $rule->setValues([$fixture->ukOnlyType->uid]); + }), + ], + 'ukSite', + -0.1, + ['ddb-red'], + ], +]); + +test('getPurchasableIds() with a purchasableCondition returns matching purchasable ids', function() { + $fixture = CatalogPricingFixture::seed(); + + $rule = createCatalogPricingRule([ + 'apply' => CatalogPricingRuleRecord::APPLY_BY_PERCENT, + 'name' => '20% off', + 'enabled' => true, + 'applyAmount' => -0.2, + 'applyPriceType' => CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE, + 'isPromotionalPrice' => false, + 'storeId' => $fixture->stores->primaryStore->id, + ], [ + 'purchasableCondition' => tap(new SkuConditionRule(), function(SkuConditionRule $rule) { + $rule->operator = 'bw'; + $rule->value = 'rad'; + }), + ]); + + expect($rule->getPurchasableIds())->toBe([$fixture->radHood->id]); +}); + +test('getPurchasableIds() with a purchasableCondition and isPromotionalPrice excludes purchasables that are not promotable', function() { + $fixture = CatalogPricingFixture::seed(); + + Elements::saveElement(tap($fixture->radHood, function(Variant $variant) { + $variant->promotable = false; + })); + + $rule = createCatalogPricingRule([ + 'apply' => CatalogPricingRuleRecord::APPLY_BY_PERCENT, + 'name' => '20% off', + 'enabled' => true, + 'applyAmount' => -0.2, + 'applyPriceType' => CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE, + 'isPromotionalPrice' => true, + 'storeId' => $fixture->stores->primaryStore->id, + ], [ + 'purchasableCondition' => tap(new PurchasableTypeConditionRule(), function(PurchasableTypeConditionRule $rule) { + $rule->setValues([Variant::class]); + }), + ]); + + $purchasableIds = $rule->getPurchasableIds(); + + expect($purchasableIds)->not->toContain($fixture->radHood->id) + ->and($purchasableIds)->toContain($fixture->hctWhite->id); +}); diff --git a/tests/Feature/Condition/ConditionRuleFormsTest.php b/tests/Feature/Condition/ConditionRuleFormsTest.php new file mode 100644 index 0000000000..10458ee52e --- /dev/null +++ b/tests/Feature/Condition/ConditionRuleFormsTest.php @@ -0,0 +1,89 @@ +mainTag = 'div'; + $condition->name = 'condition'; + + return new ConditionBuilderRenderer($condition)->render(); +} + +test('OrderCondition rules with ported Form API inputs render without error', function() { + /** @var OrderCondition $condition */ + $condition = Conditions::createCondition(['class' => OrderCondition::class]); + $condition->addConditionRule(Conditions::createConditionRule(['class' => CustomerConditionRule::class])); + $condition->addConditionRule(Conditions::createConditionRule(['class' => HasPurchasableConditionRule::class])); + $condition->addConditionRule(Conditions::createConditionRule(['class' => ContainsPurchasablesConditionRule::class])); + $condition->addConditionRule(Conditions::createConditionRule(['class' => TotalPriceConditionRule::class])); + + expect(renderCondition($condition))->toBeString()->not->toBe(''); +}); + +test('CatalogPricingCondition rules with ported Form API inputs render without error', function() { + /** @var CatalogPricingCondition $condition */ + $condition = Conditions::createCondition(['class' => CatalogPricingCondition::class]); + $condition->addConditionRule(Conditions::createConditionRule(['class' => CatalogPricingCustomerConditionRule::class])); + $condition->addConditionRule(Conditions::createConditionRule(['class' => CatalogPricingPurchasableConditionRule::class])); + + expect(renderCondition($condition))->toBeString()->not->toBe(''); +}); + +test('PostalCodeFormulaConditionRule renders without error', function() { + /** @var DiscountAddressCondition $condition */ + $condition = Conditions::createCondition(['class' => DiscountAddressCondition::class]); + $condition->addConditionRule(Conditions::createConditionRule(['class' => PostalCodeFormulaConditionRule::class])); + + expect(renderCondition($condition))->toBeString()->not->toBe(''); +}); + +test('CatalogPricingRulePurchasableCategoryConditionRule renders without error', function() { + /** @var CatalogPricingRulePurchasableCondition $condition */ + $condition = Conditions::createCondition(['class' => CatalogPricingRulePurchasableCondition::class]); + $condition->addConditionRule(Conditions::createConditionRule(['class' => CatalogPricingRulePurchasableCategoryConditionRule::class])); + + expect(renderCondition($condition))->toBeString()->not->toBe(''); +}); + +test('HasOrdersConditionRule (with its nested order-condition builder) renders without error', function() { + /** @var DiscountCustomerCondition $condition */ + $condition = Conditions::createCondition(['class' => DiscountCustomerCondition::class]); + $condition->addConditionRule(Conditions::createConditionRule(['class' => HasOrdersConditionRule::class])); + + expect(renderCondition($condition))->toBeString()->not->toBe(''); +}); + +test('VariantProductConditionRule renders without error', function() { + /** @var CatalogPricingRuleVariantCondition $condition */ + $condition = Conditions::createCondition(['class' => CatalogPricingRuleVariantCondition::class]); + $condition->addConditionRule(Conditions::createConditionRule(['class' => VariantProductConditionRule::class])); + + expect(renderCondition($condition))->toBeString()->not->toBe(''); +}); diff --git a/tests/Feature/Customer/Conditions/CatalogPricingRuleCustomerConditionRuleTest.php b/tests/Feature/Customer/Conditions/CatalogPricingRuleCustomerConditionRuleTest.php new file mode 100644 index 0000000000..d3b9422f5d --- /dev/null +++ b/tests/Feature/Customer/Conditions/CatalogPricingRuleCustomerConditionRuleTest.php @@ -0,0 +1,45 @@ +id() from inside + * modifyQuery(), which was silently non-functional for the same reason as SkuConditionRule (see + * tests/Feature/Purchasable/Conditions/SkuConditionRuleTest.php) — now fixed by calling + * CraftCms\Cms\Element\Queries\ElementQuery::applyId() directly. + */ +function createTestUser(string $username): User +{ + $user = new User(); + $user->username = $username; + $user->email = "$username@crafttest.com"; + $user->active = true; + if (!Elements::saveElement($user)) { + throw new RuntimeException('Could not save user: ' . json_encode($user->errors()->all())); + } + + return $user; +} + +test('modifyQuery filters users by the selected customer', function() { + $userA = createTestUser('catalogpricing-customer-a'); + $userB = createTestUser('catalogpricing-customer-b'); + + $rule = new CatalogPricingRuleCustomerConditionRule(); + $rule->setElementIds([$userA->id]); + + $condition = new CatalogPricingRuleCustomerCondition(User::class); + $condition->addConditionRule($rule); + + $query = User::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + expect($ids)->toContain($userA->id); + expect($ids)->not->toContain($userB->id); +}); diff --git a/tests/Feature/Customer/Conditions/HasOrdersConditionRuleTest.php b/tests/Feature/Customer/Conditions/HasOrdersConditionRuleTest.php new file mode 100644 index 0000000000..c5539d1838 --- /dev/null +++ b/tests/Feature/Customer/Conditions/HasOrdersConditionRuleTest.php @@ -0,0 +1,21 @@ +customerId($element->id)` in `matchElement()`), so its builder + * shouldn't also offer a redundant "Customer" rule — `queryParams = ['customerId']` excludes it. + */ +test('the nested order condition excludes the customer rule', function() { + $rule = new HasOrdersConditionRule(); + + $selectable = $rule->getOrderCondition()->getSelectableConditionRules(); + + expect($selectable)->not->toHaveKey(CustomerConditionRule::class); + expect($selectable)->toHaveKey(CompletedConditionRule::class); +}); diff --git a/tests/Feature/Customer/CustomersTest.php b/tests/Feature/Customer/CustomersTest.php new file mode 100644 index 0000000000..efeb2ffca4 --- /dev/null +++ b/tests/Feature/Customer/CustomersTest.php @@ -0,0 +1,269 @@ +number = bin2hex(random_bytes(16)); + $order->storeId = $fixture->storeId; + $order->setCustomer($user); + + $lineItem = app(LineItems::class)->create($order, [ + 'purchasableId' => $fixture->white->id, + 'qty' => 4, + 'note' => 'My note', + ]); + $order->setLineItems([$lineItem]); + + return $order; +} + +/** + * Ensures a user exists for the given email and is active, so getIsCredentialed() reports true. + */ +function ensureCredentialedCustomer(string $email): User +{ + $user = Users::ensureUserByEmail($email); + Users::activateUser($user); + + return $user; +} + +beforeEach(function() { + $this->fixture = OrdersFixture::seed(); +}); + +test('orderCompleteHandler activates the customer account only when registration is requested or the customer was already credentialed', function(string $email, bool $alreadyActive, bool $register) { + if ($alreadyActive) { + ensureCredentialedCustomer($email); + } + + // Registering a new account sends an activation email; fake notifications so that send + // doesn't need a real mailer/queue round-trip. + Notification::fake(); + + $order = createCustomerOrder($this->fixture, $email); + $originallyCredentialed = $order->getCustomer()->getIsCredentialed(); + $order->registerUserOnOrderComplete = $register; + + expect($order->markAsComplete())->toBeTrue(); + + $foundUser = User::find()->email($email)->status(null)->one(); + expect($foundUser)->not->toBeNull(); + expect($foundUser->getIsCredentialed())->toBe($register || $originallyCredentialed); +})->with([ + 'dont-register-guest' => ['register.guest@crafttest.com', false, false], + 'register-guest' => ['register.guest@crafttest.com', false, true], + 'register-already-credentialed-user' => ['already.credentialed@crafttest.com', true, true], + 'dont-register-already-credentialed-user' => ['already.credentialed@crafttest.com', true, false], +]); + +$guestBillingAddress = [ + 'fullName' => 'Guest Billing', + 'addressLine1' => '1 Main Billing Street', + 'locality' => 'Billingsville', + 'administrativeArea' => 'OR', + 'postalCode' => '12345', + 'countryCode' => 'US', +]; +$guestShippingAddress = [ + 'fullName' => 'Guest Shipping', + 'addressLine1' => '1 Main Shipping Street', + 'locality' => 'Shippingsville', + 'administrativeArea' => 'AL', + 'postalCode' => '98765', + 'countryCode' => 'US', +]; + +test('orderCompleteHandler copies the order\'s addresses into a newly-registered guest\'s address book', function(?array $billingAddress, ?array $shippingAddress, int $addressCount) { + $email = 'guest.person@crafttest.com'; + $isOnlyOneAddress = empty($billingAddress) || empty($shippingAddress); + + // Registering a new account sends an activation email; fake notifications so that send + // doesn't need a real mailer/queue round-trip. + Notification::fake(); + + $order = createCustomerOrder($this->fixture, $email); + $order->registerUserOnOrderComplete = true; + Elements::saveElement($order, false); + + if (!empty($billingAddress)) { + $order->setBillingAddress($billingAddress); + } + + if (!empty($shippingAddress)) { + $order->setShippingAddress($shippingAddress); + } + + expect($order->markAsComplete())->toBeTrue(); + + $customer = $order->getCustomer(); + $userAddresses = Address::find()->ownerId($customer->id)->all(); + expect($userAddresses)->toHaveCount($addressCount); + + $primaryCount = 0; + foreach ($userAddresses as $userAddress) { + if ($addressCount === 1) { + $addressTitle = t('Address', category: 'app'); + if ($isOnlyOneAddress) { + $addressTitle = !empty($billingAddress) ? t('Billing Address', category: 'commerce') : t('Shipping Address', category: 'commerce'); + } + expect($userAddress->title)->toBe($addressTitle); + + $address = $billingAddress ?? $shippingAddress; + expect($userAddress->fullName)->toBe($address['fullName']); + expect($userAddress->addressLine1)->toBe($address['addressLine1']); + expect($userAddress->locality)->toBe($address['locality']); + expect($userAddress->administrativeArea)->toBe($address['administrativeArea']); + expect($userAddress->postalCode)->toBe($address['postalCode']); + expect($userAddress->countryCode)->toBe($address['countryCode']); + } + + if ($userAddress->getIsPrimaryBilling()) { + if ($addressCount === 2) { + expect($userAddress->title)->toBe(t('Billing Address', category: 'commerce')); + expect($userAddress->fullName)->toBe($billingAddress['fullName']); + expect($userAddress->addressLine1)->toBe($billingAddress['addressLine1']); + expect($userAddress->locality)->toBe($billingAddress['locality']); + expect($userAddress->administrativeArea)->toBe($billingAddress['administrativeArea']); + expect($userAddress->postalCode)->toBe($billingAddress['postalCode']); + expect($userAddress->countryCode)->toBe($billingAddress['countryCode']); + } + + $primaryCount++; + } + + if ($userAddress->getIsPrimaryShipping()) { + if ($addressCount === 2) { + expect($userAddress->title)->toBe(t('Shipping Address', category: 'commerce')); + expect($userAddress->fullName)->toBe($shippingAddress['fullName']); + expect($userAddress->addressLine1)->toBe($shippingAddress['addressLine1']); + expect($userAddress->locality)->toBe($shippingAddress['locality']); + expect($userAddress->administrativeArea)->toBe($shippingAddress['administrativeArea']); + expect($userAddress->postalCode)->toBe($shippingAddress['postalCode']); + expect($userAddress->countryCode)->toBe($shippingAddress['countryCode']); + } + + $primaryCount++; + } + } + + expect($primaryCount)->toBe($isOnlyOneAddress ? 1 : 2); +})->with([ + 'guest-two-addresses' => [$guestBillingAddress, $guestShippingAddress, 2], + 'guest-matching-addresses' => [$guestBillingAddress, $guestBillingAddress, 1], + 'guest-one-billing-address' => [$guestBillingAddress, null, 1], + 'guest-one-shipping-address' => [null, $guestShippingAddress, 1], +]); + +$savedBillingAddress = [ + 'fullName' => 'Billing Name', + 'addressLine1' => '1 Main Billing Street', + 'locality' => 'Billingsville', + 'administrativeArea' => 'OR', + 'postalCode' => '12345', + 'countryCode' => 'US', +]; +$savedShippingAddress = [ + 'fullName' => 'Shipping Name', + 'addressLine1' => '1 Main Shipping Street', + 'locality' => 'Shippingsville', + 'administrativeArea' => 'AL', + 'postalCode' => '98765', + 'countryCode' => 'US', +]; + +test('orderCompleteHandler saves the order\'s addresses to a credentialed customer\'s address book only when requested and no source address is already set', function(?bool $saveBilling, ?array $billingAddress, ?bool $saveShipping, ?array $shippingAddress, int $newAddressCount, bool $setSourceBilling, bool $setSourceShipping) { + $email = 'source.address.customer@crafttest.com'; + $customer = ensureCredentialedCustomer($email); + + $order = createCustomerOrder($this->fixture, $email); + + if ($setSourceBilling || $setSourceShipping) { + $sourceAddress = new Address([ + 'fullName' => 'Source Address', + 'addressLine1' => '1 Source Road', + 'locality' => 'Sourcington', + 'administrativeArea' => 'OR', + 'postalCode' => '991199', + 'countryCode' => 'US', + ]); + $sourceAddress->setPrimaryOwner($customer); + $sourceAddress->setOwner($customer); + + if (!Elements::saveElement($sourceAddress, false, false, false)) { + throw new RuntimeException('Could not save source address: ' . json_encode($sourceAddress->errors()->all())); + } + + if ($setSourceBilling) { + $order->sourceBillingAddressId = $sourceAddress->id; + } + + if ($setSourceShipping) { + $order->sourceShippingAddressId = $sourceAddress->id; + } + } + + $originalAddressIds = collect(Address::find()->ownerId($customer->id)->all())->pluck('id')->all(); + + $order->saveBillingAddressOnOrderComplete = $saveBilling; + $order->saveShippingAddressOnOrderComplete = $saveShipping; + $order->setBillingAddress($billingAddress); + $order->setShippingAddress($shippingAddress); + + Elements::saveElement($order, false, false, false); + + expect($order->markAsComplete())->toBeTrue(); + + $addressQuery = Address::find()->ownerId($customer->id); + if (!empty($originalAddressIds)) { + $addressQuery->id(array_merge(['not'], $originalAddressIds)); + } + + $addresses = $addressQuery->all(); + expect($addresses)->toHaveCount($newAddressCount); + + $addressNames = collect($addresses)->pluck('fullName')->all(); + $addressLine1s = collect($addresses)->pluck('addressLine1')->all(); + + if ($billingAddress && $saveBilling && !$setSourceBilling) { + expect($addressNames)->toContain($billingAddress['fullName']); + expect($addressLine1s)->toContain($billingAddress['addressLine1']); + } + + if ($shippingAddress && $saveShipping && !$setSourceShipping) { + expect($addressNames)->toContain($shippingAddress['fullName']); + expect($addressLine1s)->toContain($shippingAddress['addressLine1']); + } +})->with([ + 'save-both' => [true, $savedBillingAddress, true, $savedShippingAddress, 2, false, false], + 'save-billing-only' => [true, $savedBillingAddress, false, null, 1, false, false], + 'save-shipping-only' => [false, null, true, $savedShippingAddress, 1, false, false], + 'save-both-but-same-address' => [true, $savedBillingAddress, true, $savedBillingAddress, 1, false, false], + 'try-to-save-both-but-no-addresses' => [true, null, true, null, 0, false, false], + 'try-to-save-but-source-billing-present' => [true, $savedBillingAddress, false, null, 0, true, false], + 'try-to-save-but-source-shipping-present' => [false, null, true, $savedShippingAddress, 0, false, true], + 'try-to-save-both-but-sources-present' => [true, $savedBillingAddress, true, $savedShippingAddress, 0, true, true], + 'try-save-both-but-billing-source-present' => [true, $savedBillingAddress, true, $savedShippingAddress, 1, true, false], + 'try-save-both-but-shipping-source-present' => [true, $savedBillingAddress, true, $savedShippingAddress, 1, false, true], +]); diff --git a/tests/Feature/Gql/Handlers/HasProductAndHasVariantTest.php b/tests/Feature/Gql/Handlers/HasProductAndHasVariantTest.php new file mode 100644 index 0000000000..cdb2da2ec6 --- /dev/null +++ b/tests/Feature/Gql/Handlers/HasProductAndHasVariantTest.php @@ -0,0 +1,96 @@ +setHandler('relatedToEntries', new class($relatedEntryIds) extends RelatedEntries { + public function __construct(private readonly array $ids) + { + } + + #[\Override] + protected function getIds(string $elementType, array $criteriaList = []): array + { + return [$this->ids]; + } + }); + $argumentManager->setHandler('hasProduct', new HasProduct()); + + $result = $argumentManager->prepareArguments([ + 'hasProduct' => [ + 'relatedToEntries' => [['section' => 'news']], + ], + ]); + + expect($result['hasProduct'])->toBe([ + 'relatedTo' => ['and', ['element' => $relatedEntryIds]], + ]); +}); + +it('passes standard, non-relation arguments through hasProduct unchanged', function() { + $argumentManager = new ArgumentManager(); + $argumentManager->setHandler('hasProduct', new HasProduct()); + + $result = $argumentManager->prepareArguments([ + 'hasProduct' => [ + 'slug' => 'rad-hoodie', + 'type' => 'hoodies', + ], + ]); + + expect($result['hasProduct'])->toBe([ + 'slug' => 'rad-hoodie', + 'type' => 'hoodies', + ]); +}); + +it('processes nested relation arguments on hasVariant by delegating to ArgumentManager', function() { + $relatedEntryIds = [7, 13]; + + $argumentManager = new ArgumentManager(); + $argumentManager->setHandler('relatedToEntries', new class($relatedEntryIds) extends RelatedEntries { + public function __construct(private readonly array $ids) + { + } + + #[\Override] + protected function getIds(string $elementType, array $criteriaList = []): array + { + return [$this->ids]; + } + }); + $argumentManager->setHandler('hasVariant', new HasVariant()); + + $result = $argumentManager->prepareArguments([ + 'hasVariant' => [ + 'relatedToEntries' => [['section' => 'news']], + ], + ]); + + expect($result['hasVariant'])->toBe([ + 'relatedTo' => ['and', ['element' => $relatedEntryIds]], + ]); +}); + +it('passes standard, non-relation arguments through hasVariant unchanged', function() { + $argumentManager = new ArgumentManager(); + $argumentManager->setHandler('hasVariant', new HasVariant()); + + $result = $argumentManager->prepareArguments([ + 'hasVariant' => [ + 'sku' => 'hct-blue', + ], + ]); + + expect($result['hasVariant'])->toBe([ + 'sku' => 'hct-blue', + ]); +}); diff --git a/tests/Feature/Gql/ProductsAndVariantsTest.php b/tests/Feature/Gql/ProductsAndVariantsTest.php new file mode 100644 index 0000000000..d1b183b39a --- /dev/null +++ b/tests/Feature/Gql/ProductsAndVariantsTest.php @@ -0,0 +1,76 @@ +fixture = GqlProductsFixture::seed(); + gqlActivateFullAccessSchema(); +}); + +it('queries products', function() { + $result = graphQL('{products{title}}'); + + expect($result)->not->toHaveKey('errors') + ->and($result['data']['products'])->toBeArray(); +}); + +it('queries variants', function() { + $result = graphQL('{variants{title}}'); + + expect($result)->not->toHaveKey('errors') + ->and($result['data']['variants'])->toBeArray(); +}); + +it('returns a graphql error for an unknown product field', function() { + $result = graphQL('{products{bogus}}'); + + expect($result['errors'][0]['message'])->toContain('Cannot query field "bogus"'); +}); + +it('returns a graphql error for an invalid query argument type', function() { + $result = graphQL('{products(limit:[5,2]){title}}'); + + expect($result['errors'][0]['message'])->toContain('Int cannot represent non-integer value'); +}); + +it('filters products by product type handle', function() { + expect(graphQL('{products(type: "hoodies") {title slug}}'))->toBe([ + 'data' => [ + 'products' => [ + ['title' => 'Rad Hoodie', 'slug' => 'rad-hoodie'], + ], + ], + ]); + + expect(graphQL('{products(type: "tShirts") {title slug}}'))->toBe([ + 'data' => [ + 'products' => [ + ['title' => 'Hypercolor T-Shirt', 'slug' => 'hypercolor-tshirt'], + ], + ], + ]); +}); + +it('resolves variant title, sku, and availability', function() { + expect(graphQL('{variants{title sku}}'))->toBe([ + 'data' => [ + 'variants' => [ + ['title' => 'Rad Hoodie', 'sku' => 'rad-hood'], + ['title' => 'Hypercolor T-Shirt', 'sku' => 'hct-white'], + ['title' => 'Hypercolor T-Shirt', 'sku' => 'hct-blue'], + ], + ], + ]); + + expect(graphQL('{variants{sku promotable availableForPurchase}}'))->toBe([ + 'data' => [ + 'variants' => [ + ['sku' => 'rad-hood', 'promotable' => true, 'availableForPurchase' => true], + ['sku' => 'hct-white', 'promotable' => true, 'availableForPurchase' => true], + ['sku' => 'hct-blue', 'promotable' => true, 'availableForPurchase' => true], + ], + ], + ]); +}); diff --git a/tests/Feature/Gql/Resolvers/Elements/ProductTest.php b/tests/Feature/Gql/Resolvers/Elements/ProductTest.php new file mode 100644 index 0000000000..6cab041a9a --- /dev/null +++ b/tests/Feature/Gql/Resolvers/Elements/ProductTest.php @@ -0,0 +1,81 @@ +fixture = GqlProductsFixture::seed(); + app(Gql::class)->flushCaches(); +}); + +it('returns a ProductQuery for top-level resolution with full access schema', function() { + gqlActivateFullAccessSchema(); + + $query = Product::prepareQuery(null, []); + + expect($query)->toBeInstanceOf(ProductQuery::class); +}); + +it('returns empty collection when schema has no product type access', function() { + gqlActivateSchema([]); + + $result = Product::prepareQuery(null, []); + + expect($result)->toBeInstanceOf(ElementCollection::class) + ->and($result)->toBeEmpty(); +}); + +it('restricts query to allowed product types based on schema', function() { + gqlActivateSchema(["productTypes.{$this->fixture->hoodiesType->uid}:read"]); + + $ids = Product::prepareQuery(null, [])->ids(); + + expect($ids)->toBe([$this->fixture->hoodie->id]); +}); + +it('returns preloaded data when source field is not a query', function() { + gqlActivateFullAccessSchema(); + + $preloaded = collect([(object)['id' => 1, 'title' => 'Test']]); + + $source = new stdClass(); + $source->products = $preloaded; + + $result = Product::prepareQuery($source, [], 'products'); + + expect($result)->toBe($preloaded); +}); + +it('applies arguments as method calls on the query', function() { + gqlActivateFullAccessSchema(); + + $query = Product::prepareQuery(null, ['type' => 'hoodies']); + + expect($query)->toBeInstanceOf(ProductQuery::class) + ->and($query->ids())->toBe([$this->fixture->hoodie->id]); +}); + +it('ignores null argument values without throwing', function() { + gqlActivateFullAccessSchema(); + + $query = Product::prepareQuery(null, ['nonExistentMethod' => null]); + + expect($query)->toBeInstanceOf(ProductQuery::class); +}); + +it('resolves the productTypeHandle and productTypeId fields via graphql', function() { + gqlActivateFullAccessSchema(); + + expect(graphQL('{products(type: "hoodies") {productTypeHandle productTypeId}}'))->toBe([ + 'data' => [ + 'products' => [ + ['productTypeHandle' => 'hoodies', 'productTypeId' => $this->fixture->hoodiesType->id], + ], + ], + ]); +}); diff --git a/tests/Feature/Helpers/CurrencyTest.php b/tests/Feature/Helpers/CurrencyTest.php new file mode 100644 index 0000000000..c10817080d --- /dev/null +++ b/tests/Feature/Helpers/CurrencyTest.php @@ -0,0 +1,59 @@ +toBe($expected); +})->with([ + 'USD-US' => ['USD', 'en-US', '$1,234.56'], + 'USD-GB' => ['USD', 'en-GB', 'US$1,234.56'], + 'USD-FR' => ['USD', 'fr-FR', "1\u{202F}234,56\u{A0}\$US"], + 'EUR-US' => ['EUR', 'en-US', '€1,234.56'], + 'EUR-GB' => ['EUR', 'en-GB', '€1,234.56'], + 'EUR-FR' => ['EUR', 'fr-FR', "1\u{202F}234,56\u{A0}€"], +]); + +test('formatAsCurrency strips trailing zeros when requested', function(string $currency, string $language, float $amount, bool $stripZeros, string $expected) { + Locale::switchAppLanguage($language); + + expect(Currency::formatAsCurrency($amount, $currency, stripZeros: $stripZeros))->toBe($expected); +})->with([ + 'USD-US' => ['USD', 'en-US', 1234.56, true, '$1,234.56'], + 'USD-US-strip' => ['USD', 'en-US', 1234.00, true, '$1,234'], + 'USD-US-no-strip' => ['USD', 'en-US', 1234.00, false, '$1,234.00'], + 'USD-GB' => ['USD', 'en-GB', 1234.56, true, 'US$1,234.56'], + 'USD-GB-strip' => ['USD', 'en-GB', 1234.0, true, 'US$1,234'], + 'USD-GB-no-strip' => ['USD', 'en-GB', 1234.0, false, 'US$1,234.00'], + 'USD-FR' => ['USD', 'fr-FR', 1234.56, true, "1\u{202F}234,56\u{A0}\$US"], + 'USD-FR-strip' => ['USD', 'fr-FR', 1234.00, true, "1\u{202F}234\u{A0}\$US"], + 'USD-FR-no-strip' => ['USD', 'fr-FR', 1234.00, false, "1\u{202F}234,00\u{A0}\$US"], + 'EUR-US' => ['EUR', 'en-US', 1234.56, true, '€1,234.56'], + 'EUR-US-strip' => ['EUR', 'en-US', 1234.00, true, '€1,234'], + 'EUR-US-no-strip' => ['EUR', 'en-US', 1234.00, false, '€1,234.00'], + 'EUR-FR' => ['EUR', 'fr-FR', 1234.56, true, "1\u{202F}234,56\u{A0}€"], + 'EUR-FR-strip' => ['EUR', 'fr-FR', 1234.00, true, "1\u{202F}234\u{A0}€"], + 'EUR-FR-no-strip' => ['EUR', 'fr-FR', 1234.00, false, "1\u{202F}234,00\u{A0}€"], +]); + +test('formatAsCurrency formats negative amounts', function(string $currency, string $language, string $expected) { + Locale::switchAppLanguage($language); + + expect(Currency::formatAsCurrency(-1234.56, $currency))->toBe($expected); +})->with([ + 'USD-US' => ['USD', 'en-US', '-$1,234.56'], + 'USD-GB' => ['USD', 'en-GB', '-US$1,234.56'], + 'USD-FR' => ['USD', 'fr-FR', "-1\u{202F}234,56\u{A0}\$US"], + 'EUR-US' => ['EUR', 'en-US', '-€1,234.56'], + 'EUR-GB' => ['EUR', 'en-GB', '-€1,234.56'], + 'EUR-FR' => ['EUR', 'fr-FR', "-1\u{202F}234,56\u{A0}€"], + 'CHF-DE-CH' => ['CHF', 'de-CH', "CHF-1\u{2019}234.56"], +]); diff --git a/tests/Feature/Helpers/LocaleTest.php b/tests/Feature/Helpers/LocaleTest.php new file mode 100644 index 0000000000..4c6f63836b --- /dev/null +++ b/tests/Feature/Helpers/LocaleTest.php @@ -0,0 +1,49 @@ +orderLanguage = 'nl'; + + $pdf = new Pdf(); + $pdf->language = PdfRecord::LOCALE_ORDER_LANGUAGE; + + expect($pdf->getRenderLanguage($order))->toBe('nl'); +}); + +test('Pdf::getRenderLanguage() returns its own language when not order-language', function() { + $order = new Order(); + $order->orderLanguage = 'nl'; + + $pdf = new Pdf(); + $pdf->language = 'ph'; + + expect($pdf->getRenderLanguage($order))->toBe('ph'); +}); + +test('Email::getRenderLanguage() resolves order-language from the given order', function() { + $order = new Order(); + $order->orderLanguage = 'nl'; + + $email = new Email(); + $email->language = EmailRecord::LOCALE_ORDER_LANGUAGE; + + expect($email->getRenderLanguage($order))->toBe('nl'); +}); + +test('Email::getRenderLanguage() returns its own language when not order-language', function() { + $order = new Order(); + $order->orderLanguage = 'nl'; + + $email = new Email(); + $email->language = 'ph'; + + expect($email->getRenderLanguage($order))->toBe('ph'); +}); diff --git a/tests/Feature/Http/Controllers/CartControllerRateLimitTest.php b/tests/Feature/Http/Controllers/CartControllerRateLimitTest.php new file mode 100644 index 0000000000..0780e4d596 --- /dev/null +++ b/tests/Feature/Http/Controllers/CartControllerRateLimitTest.php @@ -0,0 +1,55 @@ + $fixture->white->id, + 'qty' => 1, + ])->assertOk(); +}); + +it('rate limits rapid repeat requests that carry the same cart number', function() { + $fixture = OrdersFixture::seed(); + + $created = postJson(Url::actionUrl('commerce/cart/update-cart'), [ + 'purchasableId' => $fixture->white->id, + 'qty' => 1, + ])->assertOk(); + + $number = $created->json('cart.number'); + + // The first request naming a cart `number` is within the allowance. + postJson(Url::actionUrl('commerce/cart/update-cart'), [ + 'number' => $number, + 'qty' => 2, + ])->assertOk(); + + // An immediate second request for that same `number` exceeds it. + postJson(Url::actionUrl('commerce/cart/update-cart'), [ + 'number' => $number, + 'qty' => 3, + ])->assertStatus(429); +}); + +it('never rate limits requests that carry neither a number nor a coupon code', function() { + $fixture = OrdersFixture::seed(); + + for ($i = 0; $i < 3; $i++) { + postJson(Url::actionUrl('commerce/cart/update-cart'), [ + 'purchasableId' => $fixture->white->id, + 'qty' => 1, + ])->assertOk(); + } +}); diff --git a/tests/Feature/Http/Controllers/CartTest.php b/tests/Feature/Http/Controllers/CartTest.php new file mode 100644 index 0000000000..30d9f8fa10 --- /dev/null +++ b/tests/Feature/Http/Controllers/CartTest.php @@ -0,0 +1,330 @@ + 'application/json']) + ->assertOk(); + + $cart = $response->json('cart'); + + expect($cart['total'])->toEqual(0); + + // Assert types + expect($cart['number'])->toBeString(); + expect($cart['reference'])->toBeNull(); + expect($cart['couponCode'])->toBeNull(); + expect($cart['isCompleted'])->toBeBool(); + expect($cart['dateOrdered'])->toBeNull(); + expect($cart['datePaid'])->toBeNull(); + expect($cart['dateAuthorized'])->toBeNull(); + expect($cart['currency'])->toBeString(); + expect($cart['gatewayId'])->toBeNull(); + expect($cart['lastIp'])->toBeString(); + expect($cart['message'])->toBeNull(); + expect($cart['returnUrl'])->toBeNull(); + expect($cart['cancelUrl'])->toBeNull(); + expect($cart['orderStatusId'])->toBeNull(); + expect($cart['orderLanguage'])->toBeString(); + expect($cart['orderSiteId'])->toBeInt(); + expect($cart['origin'])->toBeString(); + expect($cart['billingAddressId'])->toBeNull(); + expect($cart['shippingAddressId'])->toBeNull(); + expect($cart['makePrimaryShippingAddress'])->toBeBool(); + expect($cart['makePrimaryBillingAddress'])->toBeBool(); + expect($cart['shippingSameAsBilling'])->toBeBool(); + expect($cart['billingSameAsShipping'])->toBeBool(); + expect($cart['estimatedBillingAddressId'])->toBeNull(); + expect($cart['estimatedShippingAddressId'])->toBeNull(); + expect($cart['estimatedBillingSameAsShipping'])->toBeBool(); + expect($cart['shippingMethodHandle'])->toBeString(); + expect($cart['shippingMethodName'])->toBeNull(); + expect($cart['customerId'])->toBeNull(); + expect($cart['registerUserOnOrderComplete'])->toBeBool(); + expect($cart['paymentSourceId'])->toBeNull(); + expect($cart['storedTotalPrice'])->toBeNull(); + expect($cart['storedTotalPaid'])->toBeNull(); + expect($cart['storedItemTotal'])->toBeNull(); + expect($cart['storedItemSubtotal'])->toBeNull(); + expect($cart['storedTotalShippingCost'])->toBeNull(); + expect($cart['storedTotalDiscount'])->toBeNull(); + expect($cart['storedTotalTax'])->toBeNull(); + expect($cart['storedTotalTaxIncluded'])->toBeNull(); + expect($cart['id'])->toBeNull(); + expect($cart['enabled'])->toBeBool(); + expect($cart['siteId'])->toBeInt(); + expect($cart['status'])->toBeString(); + // Zero-valued monetary/numeric fields round-trip through JSON as PHP integers rather than + // floats (PHP's JSON encoder drops the trailing `.0` from a whole-number float, and + // `TestResponse::json()` decodes it back as an int) - `toBeNumeric()` verifies these are + // still sensible numbers without depending on that JSON-specific type collapse. + expect($cart['adjustmentSubtotal'])->toBeNumeric(); + expect($cart['adjustmentsTotal'])->toBeNumeric(); + expect($cart['paymentCurrency'])->toBeString(); + expect($cart['paymentAmount'])->toBeNumeric(); + expect($cart['email'])->toBeNull(); + expect($cart['isPaid'])->toBeBool(); + expect($cart['itemSubtotal'])->toBeNumeric(); + expect($cart['itemTotal'])->toBeNumeric(); + expect($cart['lineItems'])->toBeArray(); + expect($cart['orderAdjustments'])->toBeArray(); + expect($cart['outstandingBalance'])->toBeNumeric(); + expect($cart['paidStatus'])->toBeString(); + expect($cart['recalculationMode'])->toBeString(); + expect($cart['shortNumber'])->toBeString(); + expect($cart['totalPaid'])->toBeNumeric(); + expect($cart['total'])->toBeNumeric(); + expect($cart['totalPrice'])->toBeNumeric(); + expect($cart['totalQty'])->toBeInt(); + expect($cart['totalSaleAmount'])->toBeNumeric(); + expect($cart['totalPromotionalAmount'])->toBeNumeric(); + expect($cart['totalWeight'])->toBeNumeric(); + expect($cart['adjustmentSubtotalAsCurrency'])->toBeString(); + expect($cart['adjustmentsTotalAsCurrency'])->toBeString(); + expect($cart['itemSubtotalAsCurrency'])->toBeString(); + expect($cart['itemTotalAsCurrency'])->toBeString(); + expect($cart['outstandingBalanceAsCurrency'])->toBeString(); + expect($cart['paymentAmountAsCurrency'])->toBeString(); + expect($cart['totalPaidAsCurrency'])->toBeString(); + expect($cart['totalAsCurrency'])->toBeString(); + expect($cart['totalPriceAsCurrency'])->toBeString(); + expect($cart['totalPromotionalAmountAsCurrency'])->toBeString(); + expect($cart['totalSaleAmountAsCurrency'])->toBeString(); + expect($cart['totalTaxAsCurrency'])->toBeString(); + expect($cart['totalTaxIncludedAsCurrency'])->toBeString(); + expect($cart['totalShippingCostAsCurrency'])->toBeString(); + expect($cart['totalDiscountAsCurrency'])->toBeString(); + expect($cart['storedTotalPriceAsCurrency'])->toBeString(); + expect($cart['storedTotalPaidAsCurrency'])->toBeString(); + expect($cart['storedItemTotalAsCurrency'])->toBeString(); + expect($cart['storedItemSubtotalAsCurrency'])->toBeString(); + expect($cart['storedTotalShippingCostAsCurrency'])->toBeString(); + expect($cart['storedTotalDiscountAsCurrency'])->toBeString(); + expect($cart['storedTotalTaxAsCurrency'])->toBeString(); + expect($cart['storedTotalTaxIncludedAsCurrency'])->toBeString(); + expect($cart['paidStatusHtml'])->toBeString(); + expect($cart['customerLinkHtml'])->toBeString(); + expect($cart['orderStatusHtml'])->toBeString(); + expect($cart['totalTax'])->toBeNumeric(); + expect($cart['totalTaxIncluded'])->toBeNumeric(); + expect($cart['totalShippingCost'])->toBeNumeric(); + expect($cart['totalDiscount'])->toBeNumeric(); + expect($cart['availableShippingMethodOptions'])->toBeArray(); + expect($cart['notices'])->toBeArray(); + expect($cart['billingAddress'])->toBeNull(); + expect($cart['shippingAddress'])->toBeNull(); +}); + +it('adds a single purchasable to the cart', function() { + $fixture = SalesFixture::seed(); + + $response = postJson(Url::actionUrl('commerce/cart/update-cart'), [ + 'purchasableId' => $fixture->radHood->id, + 'qty' => 2, + ])->assertOk(); + + $cart = $response->json('cart'); + + expect($cart['lineItems'])->toHaveCount(1); + expect($cart['totalQty'])->toBe(2); + expect($cart['total'])->toEqual($fixture->radHood->getSalePrice() * 2); +}); + +it('adds multiple purchasables to the cart in a single request', function() { + $fixture = SalesFixture::seed(); + + $response = postJson(Url::actionUrl('commerce/cart/update-cart'), [ + 'purchasables' => [ + ['id' => $fixture->radHood->id, 'qty' => 1], + ['id' => $fixture->hctWhite->id, 'qty' => 2], + ], + ])->assertOk(); + + expect($response->json('cart.lineItems'))->toHaveCount(2); +}); + +it('sets custom field values on shipping and billing addresses when updating the cart', function() { + $field = new PlainText(['name' => 'Test Phone', 'handle' => 'testPhone']); + expect(app(Fields::class)->saveField($field))->toBeTrue(); + + $fieldLayout = app(Addresses::class)->getFieldLayout(); + $fieldLayout->tab(FieldLayout::defaultTabName(), fn($tab) => $tab->field($field->handle)); + expect(app(Addresses::class)->saveFieldLayout($fieldLayout))->toBeTrue(); + + $shippingAddress = [ + 'addressLine1' => '1 Main Street', + 'locality' => 'Bend', + 'administrativeArea' => 'OR', + 'postalCode' => '97701', + 'countryCode' => 'US', + 'fields' => ['testPhone' => '12345'], + ]; + $billingAddress = [ + 'addressLine1' => '100 Main Street', + 'locality' => 'Bend', + 'administrativeArea' => 'OR', + 'postalCode' => '97701', + 'countryCode' => 'US', + 'fields' => ['testPhone' => '67890'], + ]; + + $response = postJson(Url::actionUrl('commerce/cart/update-cart'), [ + 'shippingAddress' => $shippingAddress, + 'billingAddress' => $billingAddress, + ])->assertOk(); + + $cart = $response->json('cart'); + + expect($cart['shippingAddress']['addressLine1'])->toBe($shippingAddress['addressLine1']); + expect($cart['shippingAddress']['testPhone'])->toBe($shippingAddress['fields']['testPhone']); + expect($cart['billingAddress']['addressLine1'])->toBe($billingAddress['addressLine1']); + expect($cart['billingAddress']['testPhone'])->toBe($billingAddress['fields']['testPhone']); +}); + +it('auto-sets the customer\'s primary shipping address on a new cart according to the store setting', function(bool $autoSet) { + $salesFixture = SalesFixture::seed(); + $cartsFixture = CartsFixture::seed(); + + app(Stores::class)->getPrimaryStore()->setAutoSetNewCartAddresses($autoSet); + + actingAs($cartsFixture->credentialedUser); + + $response = postJson(Url::actionUrl('commerce/cart/update-cart'), [ + 'purchasableId' => $salesFixture->hoodie->getDefaultVariant()->id, + 'qty' => 2, + ])->assertOk(); + + $shippingAddress = $response->json('cart.shippingAddress'); + + if ($autoSet) { + expect($shippingAddress['addressLine1'])->toBe('23 Woodworth'); + } else { + expect($shippingAddress)->toBeNull(); + } +})->with([ + 'auto-set enabled' => [true], + 'auto-set disabled' => [false], +]); + +it('sets which addresses should be saved to the customer\'s address book on order completion', function(?bool $saveBilling, ?bool $saveShipping, ?bool $saveBoth) { + $bodyParams = []; + if ($saveBoth) { + $bodyParams['saveAddressesOnOrderComplete'] = true; + } else { + $bodyParams['saveBillingAddressOnOrderComplete'] = $saveBilling; + $bodyParams['saveShippingAddressOnOrderComplete'] = $saveShipping; + } + + $response = postJson(Url::actionUrl('commerce/cart/update-cart'), $bodyParams)->assertOk(); + + $cart = $response->json('cart'); + + if ($saveBoth) { + expect($cart['saveBillingAddressOnOrderComplete'])->toBeTrue(); + expect($cart['saveShippingAddressOnOrderComplete'])->toBeTrue(); + } else { + expect($cart['saveBillingAddressOnOrderComplete'])->toBe($saveBilling); + expect($cart['saveShippingAddressOnOrderComplete'])->toBe($saveShipping); + } +})->with([ + 'save billing address only' => [true, false, false], + 'save shipping address only' => [false, true, false], + 'save both addresses individually' => [true, true, false], + 'save both addresses via the combined flag' => [false, false, true], +]); + +it('sets shipping and/or billing addresses on the cart from the customer\'s address book', function(string $whichAddress, bool $validShipping, bool $validBilling) { + $salesFixture = SalesFixture::seed(); + $cartsFixture = CartsFixture::seed(); + + // Mirrors what a project's own custom validation rules could do to any Commerce-managed + // address — `addressLine1` isn't required by Craft's own address rules for every country, so + // this is the most direct way to force one of the customer's addresses to fail validation. + Event::listen(function(ValidationRulesResolving $event) { + if (!$event->subject instanceof Address) { + return; + } + + $event->addRule('addressLine1', 'required'); + }); + + if (!$validShipping || !$validBilling) { + DB::table(CraftTable::ADDRESSES) + ->where('id', $cartsFixture->credentialedUserAddressId) + ->update(['addressLine1' => null]); + } + + actingAs($cartsFixture->credentialedUser); + + $bodyParams = [ + 'purchasableId' => $salesFixture->hoodie->getDefaultVariant()->id, + 'qty' => 2, + ]; + + if ($whichAddress === 'shipping' || $whichAddress === 'both') { + $bodyParams['shippingAddressId'] = $cartsFixture->credentialedUserAddressId; + } + + if ($whichAddress === 'billing' || $whichAddress === 'both') { + $bodyParams['billingAddressId'] = $cartsFixture->credentialedUserAddressId; + } + + $response = postJson(Url::actionUrl('commerce/cart/update-cart'), $bodyParams); + + $shippingInvalid = ($whichAddress === 'shipping' || $whichAddress === 'both') && !$validShipping; + $billingInvalid = ($whichAddress === 'billing' || $whichAddress === 'both') && !$validBilling; + + if ($shippingInvalid || $billingInvalid) { + $response->assertStatus(400); + $errorKeys = array_keys($response->json('errors', [])); + + if ($shippingInvalid) { + expect($response->json('cart.shippingAddress'))->toBeNull(); + expect(array_any($errorKeys, fn($key) => str_starts_with($key, 'shippingAddress')))->toBeTrue(); + } + + if ($billingInvalid) { + expect($response->json('cart.billingAddress'))->toBeNull(); + expect(array_any($errorKeys, fn($key) => str_starts_with($key, 'billingAddress')))->toBeTrue(); + } + + return; + } + + $response->assertOk(); + + if ($whichAddress === 'shipping' || $whichAddress === 'both') { + expect($response->json('cart.shippingAddress.addressLine1'))->toBe('23 Woodworth'); + } + + if ($whichAddress === 'billing' || $whichAddress === 'both') { + expect($response->json('cart.billingAddress.addressLine1'))->toBe('23 Woodworth'); + } +})->with([ + 'sets the shipping address' => ['shipping', true, true], + 'sets the billing address' => ['billing', true, true], + 'sets both addresses' => ['both', true, true], + 'fails validation when the shipping address is invalid' => ['shipping', false, true], +]); diff --git a/tests/Feature/Http/Controllers/Concerns/HasStoreManagementScreenTest.php b/tests/Feature/Http/Controllers/Concerns/HasStoreManagementScreenTest.php new file mode 100644 index 0000000000..f5d01f72ad --- /dev/null +++ b/tests/Feature/Http/Controllers/Concerns/HasStoreManagementScreenTest.php @@ -0,0 +1,84 @@ +setAccessible(true); + $method->invoke($controller, $storeId); +} + +test('requireStoreAccess allows a store the current user has access to', function() { + $fixture = OrdersFixture::seed(); + $admin = \CraftCms\Cms\User\Elements\User::find()->admin(true)->one(); + $this->actingAs($admin, 'craft'); + request()->setUserResolver(fn() => $admin); + + $controller = App::make(ShippingZonesController::class); + + callRequireStoreAccess($controller, $fixture->storeId); +})->throwsNoExceptions(); + +test('requireStoreAccess aborts for a store not in the allowed list', function() { + $fixture = OrdersFixture::seed(); + $admin = \CraftCms\Cms\User\Elements\User::find()->admin(true)->one(); + $this->actingAs($admin, 'craft'); + request()->setUserResolver(fn() => $admin); + + $controller = App::make(ShippingZonesController::class); + + callRequireStoreAccess($controller, null); +})->throws(HttpException::class); + +test('requireStoreAccess only resolves the allowed store list once per controller instance', function() { + $fixture = OrdersFixture::seed(); + $admin = \CraftCms\Cms\User\Elements\User::find()->admin(true)->one(); + $this->actingAs($admin, 'craft'); + request()->setUserResolver(fn() => $admin); + + $spy = Mockery::spy(Stores::class)->makePartial(); + app()->instance(Stores::class, $spy); + + $controller = App::make(ShippingZonesController::class); + + callRequireStoreAccess($controller, $fixture->storeId); + callRequireStoreAccess($controller, $fixture->storeId); + + $spy->shouldHaveReceived('getStoresByUserId')->once(); +}); + +test('OrderStatusesController has its own equivalent requireStoreAccess that also memoizes', function() { + $fixture = OrdersFixture::seed(); + $admin = \CraftCms\Cms\User\Elements\User::find()->admin(true)->one(); + $this->actingAs($admin, 'craft'); + request()->setUserResolver(fn() => $admin); + + $spy = Mockery::spy(Stores::class)->makePartial(); + app()->instance(Stores::class, $spy); + + $controller = App::make(OrderStatusesController::class); + + callRequireStoreAccess($controller, $fixture->storeId); + callRequireStoreAccess($controller, $fixture->storeId); + + $spy->shouldHaveReceived('getStoresByUserId')->once(); +}); + +test('OrderStatusesController::requireStoreAccess aborts for an invalid store', function() { + $fixture = OrdersFixture::seed(); + $admin = \CraftCms\Cms\User\Elements\User::find()->admin(true)->one(); + $this->actingAs($admin, 'craft'); + request()->setUserResolver(fn() => $admin); + + $controller = App::make(OrderStatusesController::class); + + callRequireStoreAccess($controller, null); +})->throws(HttpException::class); diff --git a/tests/Feature/Http/Controllers/EmailPreviewControllerTest.php b/tests/Feature/Http/Controllers/EmailPreviewControllerTest.php new file mode 100644 index 0000000000..8d8e2f7fec --- /dev/null +++ b/tests/Feature/Http/Controllers/EmailPreviewControllerTest.php @@ -0,0 +1,71 @@ +admin(true)->one()); + prioritizeCommerceRoutes(); + + // Point site template rendering at a directory containing a minimal + // `emails/order-confirmation.twig`, since the real one only ships with a project's own + // site templates, not with Commerce itself. + $templatesPath = dirname(__DIR__, 3) . '/Support/templates'; + Aliases::set('@templates', $templatesPath); + app(TemplateRoots::class)->register(TemplateMode::Site, '', $templatesPath); +}); + +/** @return array{fixture: OrdersFixture, email: Email} */ +function seedOrderConfirmationEmail(): array +{ + $fixture = OrdersFixture::seed(); + + $email = new Email(); + $email->storeId = $fixture->storeId; + $email->name = 'Order Confirmation'; + $email->subject = 'Order Confirmation'; + $email->templatePath = 'emails/order-confirmation'; + if (!app(Emails::class)->saveEmail($email)) { + throw new RuntimeException('Could not save email: ' . json_encode($email->errors()->all())); + } + + return compact('fixture', 'email'); +} + +it('renders the email template for a specific order', function() { + ['fixture' => $fixture, 'email' => $email] = seedOrderConfirmationEmail(); + $order = $fixture->orders['completed-new']; + + $response = get(Url::actionUrl('commerce/email-preview/render', [ + 'email' => $email->id . ':' . $email->storeId, + 'number' => $order->number, + ])); + + $response->assertOk(); + expect($response->getContent()) + ->toContain('Order Confirmation') + ->toContain('

Order Confirmation ' . $order->shortNumber . '

'); +}); + +it('renders the email template for a random completed order when no order number is given', function() { + ['email' => $email] = seedOrderConfirmationEmail(); + + $response = get(Url::actionUrl('commerce/email-preview/render', [ + 'email' => $email->id . ':' . $email->storeId, + ])); + + $response->assertOk(); + expect($response->getContent())->toContain('Order Confirmation'); + expect(preg_match('/

Order Confirmation [0-9a-zA-Z]{7}<\/h1>/', $response->getContent()))->toBe(1); +}); diff --git a/tests/Feature/Http/Controllers/OrdersControllerTest.php b/tests/Feature/Http/Controllers/OrdersControllerTest.php new file mode 100644 index 0000000000..d686a37b28 --- /dev/null +++ b/tests/Feature/Http/Controllers/OrdersControllerTest.php @@ -0,0 +1,166 @@ +admin(true)->one()); + prioritizeCommerceRoutes(); +}); + +/** @return array{order: array} */ +function ordersControllerPayload(Order $order, array $overrides = []): array +{ + return [ + 'order' => array_merge([ + 'id' => $order->id, + 'recalculationMode' => Order::RECALCULATION_MODE_ALL, + 'reference' => $order->reference, + 'customerId' => $order->getCustomerId(), + 'couponCode' => $order->couponCode, + 'isCompleted' => $order->isCompleted, + 'orderStatusId' => $order->orderStatusId, + 'orderSiteId' => $order->orderSiteId, + 'message' => $order->message, + 'shippingMethodHandle' => $order->shippingMethodHandle, + 'shippingMethodName' => $order->shippingMethodName, + 'notices' => [], + 'dateOrdered' => null, + 'lineItems' => [], + 'orderAdjustments' => [], + ], $overrides), + ]; +} + +it('lists purchasables for the order edit purchasable table', function() { + OrdersFixture::seed(); + + $response = get(Url::actionUrl('commerce/orders/purchasables-table', [ + 'siteId' => Sites::getPrimarySite()->id, + ]), ['Accept' => 'application/json'])->assertOk(); + + expect($response->json('pagination.total'))->toBe(2); + expect($response->json('data'))->toHaveCount(2); + + $purchasable = collect($response->json('data'))->last(); + + foreach (['id', 'price', 'priceAsCurrency', 'description', 'sku', 'isAvailable', 'detail'] as $key) { + expect($purchasable)->toHaveKey($key); + } + + expect($purchasable['sku'])->toBe('hct-blue'); +}); + +it('sorts the purchasable table by the requested column', function() { + OrdersFixture::seed(); + + $response = get(Url::actionUrl('commerce/orders/purchasables-table', [ + 'siteId' => Sites::getPrimarySite()->id, + 'sort' => 'sku|desc', + ]), ['Accept' => 'application/json'])->assertOk(); + + $purchasable = collect($response->json('data'))->last(); + + expect($purchasable['sku'])->toBe('hct-blue'); +}); + +it('searches for customers by email', function() { + OrdersFixture::seed(); + + $response = get(Url::actionUrl('commerce/orders/customer-search', [ + 'query' => 'customer1', + ]), ['Accept' => 'application/json'])->assertOk(); + + $customers = $response->json('customers'); + expect($customers)->toHaveCount(1); + + foreach (['cpEditUrl', 'email', 'id', 'photo', 'status', 'totalAddresses'] as $key) { + expect($customers[0])->toHaveKey($key); + } + + expect($customers[0]['email'])->toBe('customer1@crafttest.com'); +}); + +it('returns order counts per status for the index source badges', function() { + $fixture = OrdersFixture::seed(); + + $response = get(Url::actionUrl('commerce/orders/get-index-sources-badge-counts'), ['Accept' => 'application/json']) + ->assertOk(); + + $counts = $response->json('counts'); + + expect($counts)->not->toBeEmpty(); + expect($response->json('total'))->toBe(count($fixture->orders)); + + $firstCount = collect($counts)->first(); + foreach (['orderStatusId', 'handle', 'orderCount'] as $key) { + expect($firstCount)->toHaveKey($key); + } + + $shippedCount = collect($counts)->firstWhere('handle', 'shipped'); + expect($shippedCount['orderCount'])->toBe(1); +}); + +it('returns matching shipping method options for an order', function() { + $fixture = OrdersFixture::seed(); + $order = $fixture->orders['completed-new']; + + $response = postJson( + Url::actionUrl('commerce/orders/get-shipping-method-options'), + ordersControllerPayload($order) + )->assertOk(); + + $options = $response->json('shippingMethodOptions'); + expect($options)->not->toBeEmpty(); + + $option = reset($options); + foreach (['handle', 'name', 'matchesOrder'] as $key) { + expect($option)->toHaveKey($key); + } +}); + +it('fails with a bad request when given an invalid order id', function() { + postJson(Url::actionUrl('commerce/orders/get-shipping-method-options'), [ + 'order' => ['id' => 999999], + ])->assertStatus(400); +}); + +it('includes a custom runtime shipping method registered via the shipping methods event', function() { + $fixture = OrdersFixture::seed(); + $order = $fixture->orders['completed-new']; + + $customMethod = Mockery::mock(ShippingMethodInterface::class); + $customMethod->shouldReceive('getId')->andReturn(null); + $customMethod->shouldReceive('getName')->andReturn('My Custom Carrier'); + $customMethod->shouldReceive('getHandle')->andReturn('myCustomCarrier'); + $customMethod->shouldReceive('getIsEnabled')->andReturn(true); + $customMethod->shouldReceive('getPriceForOrder')->andReturn(0.0); + $customMethod->shouldReceive('matchOrder')->andReturn(true); + + Event::listen(RegisterAvailableShippingMethodsEvent::class, function(RegisterAvailableShippingMethodsEvent $event) use ($customMethod) { + $event->setShippingMethods($event->getShippingMethods()->push($customMethod)); + }); + + $response = postJson( + Url::actionUrl('commerce/orders/get-shipping-method-options'), + ordersControllerPayload($order) + )->assertOk(); + + $options = $response->json('shippingMethodOptions'); + + expect($options)->toHaveKey('myCustomCarrier'); + expect($options['myCustomCarrier']['name'])->toBe('My Custom Carrier'); + expect($options['myCustomCarrier']['handle'])->toBe('myCustomCarrier'); +}); diff --git a/tests/Feature/Http/Controllers/ShippingRulesControllerTest.php b/tests/Feature/Http/Controllers/ShippingRulesControllerTest.php new file mode 100644 index 0000000000..86b1b7c128 --- /dev/null +++ b/tests/Feature/Http/Controllers/ShippingRulesControllerTest.php @@ -0,0 +1,144 @@ +admin(true)->one()); + prioritizeCommerceRoutes(); +}); + +/** @return array{storeId: int, method: ShippingMethod, usOnly: ShippingRule, usOnly2: ShippingRule} */ +function seedShippingRulesForController(): array +{ + $storeId = app(Stores::class)->getPrimaryStore()->id; + + $method = new ShippingMethod(); + $method->storeId = $storeId; + $method->name = 'US Shipping'; + $method->handle = 'usShipping'; + $method->enabled = true; + if (!app(ShippingMethods::class)->saveShippingMethod($method)) { + throw new RuntimeException('Could not save shipping method: ' . json_encode($method->errors()->all())); + } + + $usOnly = new ShippingRule(); + $usOnly->storeId = $storeId; + $usOnly->methodId = $method->id; + $usOnly->name = 'US Shipping'; + $usOnly->enabled = true; + $usOnly->priority = 0; + if (!app(ShippingRules::class)->saveShippingRule($usOnly)) { + throw new RuntimeException('Could not save shipping rule: ' . json_encode($usOnly->errors()->all())); + } + + $usOnly2 = new ShippingRule(); + $usOnly2->storeId = $storeId; + $usOnly2->methodId = $method->id; + $usOnly2->name = 'US Shipping 2'; + $usOnly2->enabled = true; + $usOnly2->priority = 1; + if (!app(ShippingRules::class)->saveShippingRule($usOnly2)) { + throw new RuntimeException('Could not save shipping rule: ' . json_encode($usOnly2->errors()->all())); + } + + return compact('storeId', 'method', 'usOnly', 'usOnly2'); +} + +/** @return array */ +function shippingRuleSaveBody(ShippingMethod $method, ShippingRule $rule, ?string $name = null): array +{ + return [ + 'id' => $rule->id, + 'storeId' => $method->storeId, + 'name' => $name ?? $rule->name, + 'methodId' => $rule->methodId, + 'enabled' => $rule->enabled, + 'orderConditionFormula' => '', + 'baseRate' => ['value' => 0], + 'perItemRate' => ['value' => 0], + 'weightRate' => ['value' => 0], + 'percentageRate' => 0, + 'minRate' => ['value' => 0], + 'maxRate' => ['value' => 0], + 'ruleCategories' => [], + 'orderCondition' => null, + ]; +} + +it('reorders shipping rules', function() { + ['usOnly' => $usOnly, 'usOnly2' => $usOnly2] = seedShippingRulesForController(); + + $ids = [$usOnly2->id, $usOnly->id]; + + postJson(Url::actionUrl('commerce/shipping-rules/reorder'), [ + 'ids' => Json::encode($ids), + ]) + ->assertOk() + ->assertExactJson([]); + + $results = DB::table(Table::SHIPPINGRULES) + ->whereIn('id', $ids) + ->orderBy('priority') + ->pluck('id') + ->all(); + + expect($results)->toBe($ids); +}); + +it('saves changes to an existing shipping rule', function() { + ['method' => $method, 'usOnly' => $usOnly] = seedShippingRulesForController(); + + $newName = $usOnly->name . ' saved'; + + postJson(Url::actionUrl('commerce/shipping-rules/save'), shippingRuleSaveBody($method, $usOnly, $newName)) + ->assertOk(); + + $result = DB::table(Table::SHIPPINGRULES)->where('id', $usOnly->id)->value('name'); + + expect($result)->toBe($newName); +}); + +it('deletes a shipping rule via an ajax request', function() { + ['usOnly' => $usOnly] = seedShippingRulesForController(); + + postJson(Url::actionUrl('commerce/shipping-rules/delete'), ['id' => $usOnly->id], [ + 'X-Requested-With' => 'XMLHttpRequest', + ]) + ->assertOk() + ->assertExactJson([]); + + expect(DB::table(Table::SHIPPINGRULES)->where('id', $usOnly->id)->exists())->toBeFalse(); +}); + +it('deletes a shipping rule via a normal (non-ajax) request', function() { + ['usOnly2' => $usOnly2] = seedShippingRulesForController(); + + postJson(Url::actionUrl('commerce/shipping-rules/delete'), ['id' => $usOnly2->id]); + + expect(DB::table(Table::SHIPPINGRULES)->where('id', $usOnly2->id)->exists())->toBeFalse(); +}); + +it('duplicates a shipping rule', function() { + ['method' => $method, 'usOnly' => $usOnly] = seedShippingRulesForController(); + + postJson(Url::actionUrl('commerce/shipping-rules/duplicate'), shippingRuleSaveBody($method, $usOnly)) + ->assertOk(); + + $count = DB::table(Table::SHIPPINGRULES)->where('name', $usOnly->name)->count(); + + expect($count)->toBe(2); +}); diff --git a/tests/Feature/Inventory/InventoryTest.php b/tests/Feature/Inventory/InventoryTest.php new file mode 100644 index 0000000000..0b1e0a3ba8 --- /dev/null +++ b/tests/Feature/Inventory/InventoryTest.php @@ -0,0 +1,52 @@ +toBeInstanceOf(Inventory::class); +}); + +test('updatePurchasableInventoryLevel sets and adjusts a purchasable\'s stock', function(array $updateConfigs, int $expected) { + $variant = ProductConditionsFixture::seed()->hoodieVariant; + $variant->inventoryTracked = true; + + foreach ($updateConfigs as $updateConfig) { + $qty = $updateConfig['quantity']; + unset($updateConfig['quantity']); + + app(Inventory::class)->updatePurchasableInventoryLevel($variant, $qty, $updateConfig); + } + + expect($variant->getStock())->toBe($expected); +})->with([ + 'simple-single-arg' => [ + [ + ['quantity' => 10], + ], + 10, + ], + 'set-and-adjust' => [ + [ + ['quantity' => 10], + ['quantity' => 2, 'updateAction' => InventoryUpdateQuantityType::ADJUST], + ], + 12, + ], + 'just-adjust' => [ + [ + ['quantity' => 2, 'updateAction' => InventoryUpdateQuantityType::ADJUST], + ], + 2, + ], + 'set-and-adjust-negative' => [ + [ + ['quantity' => 10], + ['quantity' => -2, 'updateAction' => InventoryUpdateQuantityType::ADJUST], + ], + 8, + ], +]); diff --git a/tests/Feature/Order/Adjuster/DiscountTest.php b/tests/Feature/Order/Adjuster/DiscountTest.php new file mode 100644 index 0000000000..e7cddc8474 --- /dev/null +++ b/tests/Feature/Order/Adjuster/DiscountTest.php @@ -0,0 +1,106 @@ +qty = $qty; + $lineItem->setPrice($price); + $lineItem->setIsPromotable($isPromotable); + + return $lineItem; +} + +/** + * Swaps in a partial mock of the `Discounts` service so `adjust()` only ever sees the + * given discount as "active" and "matching the order" — real matching against line items + * (promotability, category/purchasable restrictions) still runs for real. + */ +function mockActiveDiscount(DiscountModel $discount): void +{ + $mock = Mockery::mock(Discounts::class)->makePartial(); + $mock->shouldReceive('getAllActiveDiscounts')->andReturn([$discount]); + $mock->shouldReceive('matchOrder')->andReturn(true); + app()->instance(Discounts::class, $mock); +} + +function orderLevelDiscount(float $baseDiscount): DiscountModel +{ + $discount = new DiscountModel(); + $discount->name = 'Order Level'; + $discount->description = 'Order level discount'; + $discount->allPurchasables = true; + $discount->allCategories = true; + $discount->stopProcessing = false; + $discount->baseDiscount = $baseDiscount; + + return $discount; +} + +test('a base order-level discount applies to a promotable line item', function() { + mockActiveDiscount($discount = orderLevelDiscount(-10)); + + $order = new Order(); + $order->setLineItems([discountLineItem(price: 100, qty: 1, isPromotable: true)]); + + $adjustments = app(Discount::class)->adjust($order); + $order->setAdjustments($adjustments); + + expect($adjustments)->toHaveCount(1); + $adjustment = collect($adjustments)->firstWhere('description', $discount->description); + expect($adjustment)->not->toBeNull(); + expect($adjustment->amount)->toEqual(-10.0); + expect($adjustment->type)->toBe('discount'); + + expect($order->getTotalPrice())->toEqual(90.0); + expect($order->getTotalDiscount())->toEqual(-10.0); +}); + +test('a base order-level discount does not apply to a non-promotable line item', function() { + mockActiveDiscount(orderLevelDiscount(-10)); + + $order = new Order(); + $order->setLineItems([discountLineItem(price: 100, qty: 1, isPromotable: false)]); + + $adjustments = app(Discount::class)->adjust($order); + $order->setAdjustments($adjustments); + + expect($adjustments)->toHaveCount(0); + expect($order->getTotalPrice())->toEqual(100.0); + expect($order->getTotalDiscount())->toEqual(0.0); +}); + +test('a base order-level discount larger than one promotable line item spreads to it but skips the non-promotable one', function() { + mockActiveDiscount($discount = orderLevelDiscount(-110)); + + $order = new Order(); + $order->setLineItems([ + discountLineItem(price: 100, qty: 1, isPromotable: false), + discountLineItem(price: 100, qty: 1, isPromotable: true), + ]); + + $adjustments = app(Discount::class)->adjust($order); + $order->setAdjustments($adjustments); + + expect($adjustments)->toHaveCount(1); + $adjustment = collect($adjustments)->firstWhere('description', $discount->description); + expect($adjustment)->not->toBeNull(); + // The discount is capped at the price of the only promotable line item, not the full base discount. + expect($adjustment->amount)->toEqual(-100.0); + + expect($order->getTotalPrice())->toEqual(100.0); + expect($order->getTotalDiscount())->toEqual(-100.0); +}); diff --git a/tests/Feature/Order/Adjuster/ShippingTest.php b/tests/Feature/Order/Adjuster/ShippingTest.php new file mode 100644 index 0000000000..4c5b33b11b --- /dev/null +++ b/tests/Feature/Order/Adjuster/ShippingTest.php @@ -0,0 +1,53 @@ +shouldReceive('getId')->andReturn(null); + $method->shouldReceive('getType')->andReturn('Third Party'); + $method->shouldReceive('getName')->andReturn('Third Party Flat Rate'); + $method->shouldReceive('getHandle')->andReturn('thirdPartyFlatRate'); + $method->shouldReceive('getIsEnabled')->andReturn(true); + $method->shouldReceive('getMatchingShippingRule')->andReturn(null); + $method->shouldReceive('getPriceForOrder')->andReturn(8.99); + $method->shouldReceive('matchOrder')->andReturnUsing(function() use (&$thirdPartyMethodMatches) { + return $thirdPartyMethodMatches; + }); + + Event::listen(RegisterAvailableShippingMethodsEvent::class, function(RegisterAvailableShippingMethodsEvent $event) use ($method) { + $event->setShippingMethods($event->getShippingMethods()->push($method)); + }); + + $lineItem = new LineItem(); + $lineItem->qty = 1; + $lineItem->setPrice(50); + $lineItem->setIsShippable(true); + + $order = new Order(); + $order->shippingMethodHandle = 'thirdPartyFlatRate'; + $order->setLineItems([$lineItem]); + + $adjuster = new Shipping(); + + $firstPass = $adjuster->adjust($order); + expect($firstPass)->toHaveCount(1); + expect($firstPass[0]->amount)->toEqual(8.99); + + $thirdPartyMethodMatches = false; + + $secondPass = $adjuster->adjust($order); + expect($secondPass)->toBe([]); +}); diff --git a/tests/Feature/Order/Adjuster/TaxTest.php b/tests/Feature/Order/Adjuster/TaxTest.php new file mode 100644 index 0000000000..e2b1e70575 --- /dev/null +++ b/tests/Feature/Order/Adjuster/TaxTest.php @@ -0,0 +1,336 @@ +addConditionRule(new CountryConditionRule(['operator' => 'in', 'values' => $countryCodes])); + + $zone = new TaxAddressZone(); + $zone->setCondition($condition); + + return $zone; +} + +/** + * Builds an in-memory `TaxRate`, without persisting it — {@see Tax::adjust()} never queries + * the database for rates directly (that's `getTaxRates()`, mocked out below), so the adjuster's + * own calculation logic can be exercised entirely against plain objects. + */ +function taxRate(array $item): TaxRate +{ + $rate = Mockery::mock(TaxRate::class)->makePartial(); + $rate->name = $item['name']; + $rate->code = $item['code']; + $rate->rate = $item['rate']; + $rate->include = $item['include']; + $rate->removeIncluded = $item['removeIncluded'] ?? false; + $rate->taxIdValidators = $item['taxIdValidators'] ?? []; + $rate->removeVatIncluded = $item['removeVatIncluded'] ?? false; + $rate->taxable = $item['taxable']; + $rate->taxCategoryId = $item['taxCategoryId']; + $rate->enabled = true; + + if (isset($item['zoneCountries'])) { + $rate->shouldReceive('getIsEverywhere')->andReturn(false); + $rate->shouldReceive('getTaxZone')->andReturn(taxZoneForCountries($item['zoneCountries'])); + } else { + $rate->shouldReceive('getIsEverywhere')->andReturn(true); + $rate->shouldReceive('getTaxZone')->andReturn(null); + } + + return $rate; +} + +/** + * Runs `Tax::adjust()` against an order built from the given address/line-item/tax-rate data. + * `getTaxRates()` and `validateTaxIdNumber()` are mocked (both protected) so the adjuster's own + * tax-calculation logic is isolated from the tax-rate-lookup and VAT-ID-validation-service + * concerns, which are covered elsewhere. + */ +function adjustWithTaxRates(array $addressData, array $lineItemData, array $taxRateData): array +{ + $order = new Order(); + + $address = new Address(); + $address->countryCode = $addressData['countryCode']; + $address->organizationTaxId = $addressData['organizationTaxId'] ?? null; + $order->setShippingAddress($address); + + $lineItems = []; + foreach ($lineItemData as $item) { + $lineItem = new LineItem(); + $lineItem->qty = $item['qty']; + $lineItem->setPrice($item['price']); + $lineItem->taxCategoryId = 1; + $lineItems[] = $lineItem; + } + $order->setLineItems($lineItems); + + $taxAdjuster = Mockery::mock(Tax::class)->makePartial(); + $taxAdjuster->shouldAllowMockingProtectedMethods(); + $taxAdjuster->shouldReceive('getTaxRates')->andReturn(collect(array_map(taxRate(...), $taxRateData))); + $taxAdjuster->shouldReceive('validateTaxIdNumber')->andReturn($addressData['_validateVat'] ?? false); + + $adjustments = $taxAdjuster->adjust($order); + $order->setAdjustments($adjustments); + + return ['order' => $order, 'adjustments' => $adjustments]; +} + +test('tax adjustments', function(array $addressData, array $lineItemData, array $taxRateData, array $expected) { + ['order' => $order, 'adjustments' => $adjustments] = adjustWithTaxRates($addressData, $lineItemData, $taxRateData); + + expect($adjustments)->toHaveCount(count($expected['adjustments'])); + + foreach ($expected['adjustments'] as $index => $item) { + expect($adjustments[$index]->type)->toBe($item['type']); + expect(round($adjustments[$index]->amount, 2))->toEqual($item['amount']); + expect($adjustments[$index]->included)->toBe($item['included']); + expect($adjustments[$index]->description)->toBe($item['description']); + } + + expect($order->getTotalQty())->toEqual($expected['orderTotalQty']); + expect($order->getTotalPrice())->toEqual($expected['orderTotalPrice']); + expect(round($order->getTotalTax(), 2))->toEqual($expected['orderTotalTax']); + expect(round($order->getTotalTaxIncluded(), 2))->toEqual($expected['orderTotalTaxIncluded']); +})->with([ + 'tax-10pct-included' => [ + ['countryCode' => 'AU'], + [['price' => 100, 'qty' => 1]], + [[ + 'name' => 'Australia', 'code' => 'GST', 'taxCategoryId' => 1, 'rate' => 0.1, + 'include' => true, 'taxable' => 'order_total_price', 'zoneCountries' => ['AU'], + ]], + [ + 'adjustments' => [ + ['type' => 'tax', 'amount' => 9.09, 'included' => true, 'description' => '10%'], + ], + 'orderTotalPrice' => 100, + 'orderTotalQty' => 1, + 'orderTotalTax' => 0, + 'orderTotalTaxIncluded' => 9.09, + ], + ], + + 'tax-10pct-not-included' => [ + ['countryCode' => 'AU'], + [['price' => 100, 'qty' => 1]], + [[ + 'name' => 'Australia', 'code' => 'GST', 'taxCategoryId' => 1, 'rate' => 0.1, + 'include' => false, 'taxable' => 'order_total_price', + ]], + [ + 'adjustments' => [ + ['type' => 'tax', 'amount' => 10, 'included' => false, 'description' => '10%'], + ], + 'orderTotalPrice' => 110, + 'orderTotalQty' => 1, + 'orderTotalTax' => 10, + 'orderTotalTaxIncluded' => 0, + ], + ], + + 'tax-10pct-included-2-line-items' => [ + ['countryCode' => 'NL'], + [ + ['price' => 100, 'qty' => 1], + ['price' => 50, 'qty' => 2], + ], + [[ + 'name' => 'Netherlands', 'code' => 'NLVAT', 'taxCategoryId' => 1, 'rate' => 0.1, + 'include' => true, 'taxIdValidators' => ['craft\\commerce\\taxidvalidators\\EuVatIdValidator'], + 'taxable' => 'price_shipping', + ]], + [ + 'adjustments' => [ + ['type' => 'tax', 'amount' => 9.09, 'included' => true, 'description' => '10%'], + ['type' => 'tax', 'amount' => 9.09, 'included' => true, 'description' => '10%'], + ], + 'orderTotalPrice' => 200, + 'orderTotalQty' => 3, + 'orderTotalTax' => 0, + 'orderTotalTaxIncluded' => 18.18, + ], + ], + + 'tax-zone-mismatch-1' => [ + ['countryCode' => 'AU'], + [['price' => 100, 'qty' => 1]], + [[ + 'name' => 'Australia', 'code' => 'GST', 'taxCategoryId' => 1, 'rate' => 0.1, + 'include' => false, 'taxable' => 'order_total_price', 'zoneCountries' => ['NL'], + ]], + [ + 'adjustments' => [], + 'orderTotalPrice' => 100, + 'orderTotalQty' => 1, + 'orderTotalTax' => 0, + 'orderTotalTaxIncluded' => 0, + ], + ], + + 'tax-zone-mismatch-2' => [ + ['countryCode' => 'AU'], + [['price' => 100, 'qty' => 1]], + [[ + 'name' => 'Netherlands', 'code' => 'NLVAT', 'taxCategoryId' => 1, 'rate' => 0.1, + 'include' => true, 'removeIncluded' => true, 'taxable' => 'order_total_price', + 'zoneCountries' => ['NL'], // does not match AU on purpose, to create a mismatch + ]], + [ + 'adjustments' => [ + ['type' => 'discount', 'amount' => -9.09, 'included' => false, 'description' => '10%'], + ], + 'orderTotalPrice' => 90.91, + 'orderTotalQty' => 1, + 'orderTotalTax' => 0, + 'orderTotalTaxIncluded' => 0, + ], + ], + + 'tax-valid-vat-1' => [ + ['countryCode' => 'CZ', 'organizationTaxId' => 'CZ25666011', '_validateVat' => true], + [['price' => 100, 'qty' => 1]], + [[ + 'name' => 'CZ Vat', 'code' => 'CZVAT', 'taxCategoryId' => 1, 'rate' => 0.1, + 'include' => true, 'taxIdValidators' => ['craft\\commerce\\taxidvalidators\\EuVatIdValidator'], + 'removeVatIncluded' => true, 'taxable' => 'order_total_price', 'zoneCountries' => ['CZ'], + ]], + [ + 'adjustments' => [ + ['type' => 'discount', 'amount' => -9.09, 'included' => false, 'description' => '10%'], + ], + 'orderTotalPrice' => 90.91, + 'orderTotalQty' => 1, + 'orderTotalTax' => 0, + 'orderTotalTaxIncluded' => 0, + ], + ], + + 'tax-valid-vat-2 (included tax that does not apply since it has a valid tax ID, but does not remove)' => [ + ['countryCode' => 'CZ', 'organizationTaxId' => 'CZ25666011', '_validateVat' => true], + [['price' => 100, 'qty' => 1]], + [[ + 'name' => 'CZ Vat', 'code' => 'CZVAT', 'taxCategoryId' => 1, 'rate' => 0.1, + 'include' => true, 'taxIdValidators' => ['craft\\commerce\\taxidvalidators\\EuVatIdValidator'], + 'removeVatIncluded' => false, 'taxable' => 'order_total_price', 'zoneCountries' => ['CZ'], + ]], + [ + 'adjustments' => [], + 'orderTotalPrice' => 100, + 'orderTotalQty' => 1, + 'orderTotalTax' => 0, + 'orderTotalTaxIncluded' => 0, + ], + ], + + 'tax-invalid-vat-1 (does not get removed due to an invalid VAT ID)' => [ + ['countryCode' => 'CZ', 'organizationTaxId' => 'CZ99999999', '_validateVat' => false], + [['price' => 100, 'qty' => 1]], + [[ + 'name' => 'CZ Vat', 'code' => 'CZVAT', 'taxCategoryId' => 1, 'rate' => 0.1, + 'include' => true, 'taxIdValidators' => ['craft\\commerce\\taxidvalidators\\EuVatIdValidator'], + 'removeVatIncluded' => true, 'taxable' => 'order_total_price', 'zoneCountries' => ['CZ'], + ]], + [ + 'adjustments' => [ + ['type' => 'tax', 'description' => '10%', 'included' => true, 'amount' => 9.09], + ], + 'orderTotalPrice' => 100, + 'orderTotalQty' => 1, + 'orderTotalTax' => 0, + 'orderTotalTaxIncluded' => 9.09, + ], + ], + + 'tax-20pct-vat-not-included-taxable-line-item-price' => [ + ['countryCode' => 'UK'], + [['price' => 49.17, 'qty' => 1]], + [[ + 'name' => 'UK', 'code' => 'VAT', 'taxCategoryId' => 1, 'rate' => 0.2, + 'include' => false, 'taxIdValidators' => ['craft\\commerce\\taxidvalidators\\EuVatIdValidator'], + 'taxable' => 'price', + ]], + [ + 'adjustments' => [ + ['type' => 'tax', 'amount' => 9.83, 'included' => false, 'description' => '20%'], + ], + 'orderTotalPrice' => 59, + 'orderTotalQty' => 1, + 'orderTotalTax' => 9.83, + 'orderTotalTaxIncluded' => 0, + ], + ], + + 'tax-20pct-vat-not-included-taxable-purchasable-price' => [ + ['countryCode' => 'UK'], + [['price' => 49.17, 'qty' => 1]], + [[ + 'name' => 'UK', 'code' => 'VAT', 'taxCategoryId' => 1, 'rate' => 0.2, + 'include' => false, 'taxIdValidators' => ['craft\\commerce\\taxidvalidators\\EuVatIdValidator'], + 'taxable' => 'purchasable', + ]], + [ + 'adjustments' => [ + ['type' => 'tax', 'amount' => 9.83, 'included' => false, 'description' => '20%'], + ], + 'orderTotalPrice' => 59, + 'orderTotalQty' => 1, + 'orderTotalTax' => 9.83, + 'orderTotalTaxIncluded' => 0, + ], + ], + + 'tax-20pct-vat-not-included-taxable-line-item-price-qty-4' => [ + ['countryCode' => 'UK'], + [['price' => 49.17, 'qty' => 4]], + [[ + 'name' => 'UK', 'code' => 'VAT', 'taxCategoryId' => 1, 'rate' => 0.2, + 'include' => false, 'taxIdValidators' => ['craft\\commerce\\taxidvalidators\\EuVatIdValidator'], + 'taxable' => 'price', + ]], + [ + 'adjustments' => [ + ['type' => 'tax', 'amount' => 39.34, 'included' => false, 'description' => '20%'], + ], + 'orderTotalPrice' => 236.02, + 'orderTotalQty' => 4, + 'orderTotalTax' => 39.34, + 'orderTotalTaxIncluded' => 0, + ], + ], + + 'tax-20pct-vat-not-included-taxable-purchasable-price-qty-4' => [ + ['countryCode' => 'UK'], + [['price' => 49.17, 'qty' => 4]], + [[ + 'name' => 'UK', 'code' => 'VAT', 'taxCategoryId' => 1, 'rate' => 0.2, + 'include' => false, 'taxIdValidators' => ['craft\\commerce\\taxidvalidators\\EuVatIdValidator'], + 'taxable' => 'purchasable', + ]], + [ + 'adjustments' => [ + ['type' => 'tax', 'amount' => 39.32, 'included' => false, 'description' => '20%'], + ], + 'orderTotalPrice' => 236, + 'orderTotalQty' => 4, + 'orderTotalTax' => 39.32, + 'orderTotalTaxIncluded' => 0, + ], + ], +]); diff --git a/tests/Feature/Order/CartsTest.php b/tests/Feature/Order/CartsTest.php new file mode 100644 index 0000000000..f2ebf57884 --- /dev/null +++ b/tests/Feature/Order/CartsTest.php @@ -0,0 +1,225 @@ +runningInConsole() is false, and the + * PHP CLI process running this test suite always reports true there. Setting the private state + * directly sidesteps that guard so a directly-inserted cart row can be looked up by number. + */ +function primeCartSession(Carts $carts, string $cartNumber): void +{ + $property = new ReflectionProperty($carts, 'cartNumber'); + $property->setValue($carts, $cartNumber); +} + +test('getCart auto-sets billing/shipping addresses only for a logged-in customer when the store enables it', function(string $userKey, bool $autoSet, bool $hasBillingAddress, bool $hasShippingAddress, bool $loggedIn) { + $fixture = CartsFixture::seed(); + $user = $fixture->{$userKey}; + + app(Stores::class)->getCurrentStore()->setAutoSetNewCartAddresses($autoSet); + + if ($loggedIn) { + $this->actingAs($user, 'craft'); + } + + $carts = app(Carts::class); + $cartNumber = $carts->generateCartNumber(); + primeCartSession($carts, $cartNumber); + + $cart = new Order(); + $cart->number = $cartNumber; + $cart->setCustomer($user); + Elements::saveElement($cart, false); + + $result = $carts->getCart(); + + expect($result->getBillingAddress() !== null)->toBe($hasBillingAddress); + expect($result->getShippingAddress() !== null)->toBe($hasShippingAddress); +})->with([ + 'anonymous, auto-set disabled' => ['inactiveUser', false, false, false, false], + 'anonymous, auto-set enabled but not logged in' => ['inactiveUser', true, false, false, false], + 'logged in, auto-set disabled' => ['credentialedUser', false, false, false, true], + 'logged in, auto-set enabled' => ['credentialedUser', true, true, true, true], +]); + +test('getCart switches an anonymous session cart to the logged-in customer', function() { + $fixture = CartsFixture::seed(); + $this->actingAs($fixture->credentialedUser, 'craft'); + + $carts = app(Carts::class); + $cartNumber = $carts->generateCartNumber(); + primeCartSession($carts, $cartNumber); + + $order = new Order(); + $order->number = $cartNumber; + $order->setCustomer($fixture->inactiveUser); + Elements::saveElement($order, false); + expect($order->getCustomerId())->toBe($fixture->inactiveUser->id); + + $cart = $carts->getCart(); + + expect($cart->number)->toBe($cartNumber); + expect($cart->getCustomerId())->toBe($fixture->credentialedUser->id); + expect($cart->getEmail())->toBe($fixture->credentialedUser->email); +}); + +test('getCart forgets a credentialed customer\'s cart for an anonymous visitor without prior authorization', function() { + // A credentialed customer's cart is private — an anonymous visitor should never be served it + // unless the session was explicitly authorized (see the next two tests). + // @see https://github.com/craftcms/commerce/issues/4225 + $fixture = CartsFixture::seed(); + + $carts = app(Carts::class); + $cartNumber = $carts->generateCartNumber(); + primeCartSession($carts, $cartNumber); + + $order = new Order(); + $order->number = $cartNumber; + $order->setCustomer($fixture->credentialedUser); + Elements::saveElement($order, false); + + $cart = $carts->getCart(); + + expect($cart->number)->not->toBe($cartNumber); + expect($cart->getCustomerId())->toBeNull(); +}); + +test('getCart serves a credentialed customer\'s cart to an anonymous visitor once the session is authorized', function() { + // @see https://github.com/craftcms/commerce/issues/4225 + $fixture = CartsFixture::seed(); + + $carts = app(Carts::class); + $cartNumber = $carts->generateCartNumber(); + primeCartSession($carts, $cartNumber); + + $order = new Order(); + $order->number = $cartNumber; + $order->setCustomer($fixture->credentialedUser); + Elements::saveElement($order, false); + + // Mirrors what CartController::actionLoadCart() does after validating a load-cart token. + session()->put('commerce:anonymousCartWithCredentialedCustomer:' . $cartNumber, true); + + $cart = $carts->getCart(); + + expect($cart->number)->toBe($cartNumber); + expect($cart->getCustomerId())->toBe($fixture->credentialedUser->id); +}); + +test('getCart lets a different logged-in user acquire an authorized credentialed customer\'s cart', function() { + // @see https://github.com/craftcms/commerce/issues/4225 + $fixture = CartsFixture::seed(); + $this->actingAs($fixture->loadingUser, 'craft'); + + $carts = app(Carts::class); + $cartNumber = $carts->generateCartNumber(); + primeCartSession($carts, $cartNumber); + + $order = new Order(); + $order->number = $cartNumber; + $order->setCustomer($fixture->credentialedUser); + Elements::saveElement($order, false); + expect($order->getCustomerId())->toBe($fixture->credentialedUser->id); + + session()->put('commerce:anonymousCartWithCredentialedCustomer:' . $cartNumber, true); + + $cart = $carts->getCart(); + + expect($cart->number)->toBe($cartNumber); + expect($cart->getCustomerId())->toBe($fixture->loadingUser->id); + expect($cart->getEmail())->toBe($fixture->loadingUser->email); +}); + +test('forgetCart followed by getCart returns a cart with a new number', function() { + $carts = app(Carts::class); + + $initialCart = $carts->getCart(); + $originalNumber = $initialCart->number; + + $carts->forgetCart(); + $newCart = $carts->getCart(); + + expect($newCart->number)->not->toBe($originalNumber); +}); + +test('forgetCart prevents a stale cart cookie from restoring the forgotten cart', function() { + // @see https://github.com/craftcms/commerce/issues/4279 + $carts = app(Carts::class); + $cookieName = $carts->cartCookie['name']; + + $initialCart = $carts->getCart(); + $originalNumber = $initialCart->number; + + $carts->forgetCart(); + + // The test process runs as CLI, so app()->runningInConsole() is true and Carts skips its + // cookie handling entirely. Force it false so getCart() takes the same cookie-reading branch + // a real web request would, otherwise this couldn't catch a regression here. + $runningInConsole = new ReflectionProperty(app(), 'isRunningInConsole'); + $originalRunningInConsole = $runningInConsole->getValue(app()); + $runningInConsole->setValue(app(), false); + + // Simulate a browser that still carries the Set-Cookie value issued before forgetCart(). + $request = Request::create('/', 'GET', [], [$cookieName => $originalNumber]); + app()->instance('request', $request); + Facade::clearResolvedInstance('request'); + + try { + $cart = $carts->getCart(); + + expect($cart->number)->not->toBe($originalNumber); + } finally { + $runningInConsole->setValue(app(), $originalRunningInConsole); + } +}); + +test('peekCart returns the cart matching the cart cookie without starting a new cart session', function() { + $cartNumber = app(Carts::class)->generateCartNumber(); + $order = new Order(); + $order->number = $cartNumber; + Elements::saveElement($order, false); + + $cookieName = app(Carts::class)->cartCookie['name']; + + // Force a real web-request context so a wrongly-queued cart cookie would actually show up + // below — see the note in the previous test. + $runningInConsole = new ReflectionProperty(app(), 'isRunningInConsole'); + $originalRunningInConsole = $runningInConsole->getValue(app()); + $runningInConsole->setValue(app(), false); + + $request = Request::create('/', 'GET', [], [$cookieName => $cartNumber]); + app()->instance('request', $request); + Facade::clearResolvedInstance('request'); + + try { + $cart = app(Carts::class)->peekCart(); + + expect($cart)->not->toBeNull(); + expect($cart->number)->toBe($cartNumber); + // Only setSessionCartNumber() queues the Set-Cookie header that "starts" a cart session — + // peekCart() must never call it. + expect(Cookie::hasQueued($cookieName))->toBeFalse(); + } finally { + $runningInConsole->setValue(app(), $originalRunningInConsole); + } +}); + +test('peekCart returns null when there is no cart cookie', function() { + $request = Request::create('/'); + app()->instance('request', $request); + Facade::clearResolvedInstance('request'); + + expect(app(Carts::class)->peekCart())->toBeNull(); +}); diff --git a/tests/Feature/Order/Conditions/CouponCodeConditionRuleTest.php b/tests/Feature/Order/Conditions/CouponCodeConditionRuleTest.php new file mode 100644 index 0000000000..c7fbe16b0b --- /dev/null +++ b/tests/Feature/Order/Conditions/CouponCodeConditionRuleTest.php @@ -0,0 +1,78 @@ +value = $value; + $rule->operator = $operator; + $condition->addConditionRule($rule); + + return $condition; +} + +test('matchElement matches coupon codes', function(?string $ruleValue, string $operator, ?string $orderCoupon, bool $expectedMatch) { + $fixture = OrdersFixture::seed(); + $order = $fixture->orders['completed-new']; + $order->couponCode = $orderCoupon; + + $condition = couponCondition($ruleValue, $operator); + + expect($condition->matchElement($order))->toBe($expectedMatch); +})->with([ + 'match-equals' => ['coupon1', '=', 'coupon1', true], + 'match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN1', true], + 'no-match-equals' => ['coupon1', '=', 'coupon2', false], + 'no-match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN2', false], + 'no-match-equals-null' => ['coupon1', '=', null, false], + 'match-contains' => ['coupon1', '**', 'coupon1', true], + 'match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN1', true], + 'no-match-contains' => ['coupon1', '**', 'coupon2', false], + 'no-match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN2', false], + 'match-begins-with' => ['coupon', 'bw', 'coupon1', true], + 'match-begins-with-case-insensitive' => ['coupon', 'bw', 'cOuPoN1', true], + 'no-match-begins-with' => ['coupon', 'bw', 'foocoupon2', false], + 'no-match-begins-with-case-insensitive' => ['coupon', 'bw', 'foocOuPoN2', false], + 'match-ends-with' => ['pon1', 'ew', 'coupon1', true], + 'match-ends-with-case-insensitive' => ['pon1', 'ew', 'cOuPoN1', true], + 'no-match-ends-with' => ['pon2', 'ew', 'coupon2foo', false], + 'no-match-ends-with-case-insensitive' => ['pon2', 'ew', 'cOuPoN2foo', false], +]); + +test('modifyQuery filters orders by coupon code', function(?string $ruleValue, string $operator, ?string $orderCoupon, bool $expectedMatch) { + $fixture = OrdersFixture::seed(); + $order = $fixture->orders['completed-new']; + $order->couponCode = $orderCoupon; + if (!Elements::saveElement($order, false)) { + throw new RuntimeException('Could not save order: ' . json_encode($order->errors()->all())); + } + + $condition = couponCondition($ruleValue, $operator); + + $query = Order::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + if ($expectedMatch) { + expect($ids)->toContain($order->id); + } else { + expect($ids)->not->toContain($order->id); + } +})->with([ + 'match-equals' => ['coupon1', '=', 'coupon1', true], + 'match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN1', true], + 'no-match-equals' => ['coupon1', '=', 'coupon2', false], + 'match-contains' => ['coupon1', '**', 'coupon1', true], + 'match-begins-with' => ['coupon', 'bw', 'coupon1', true], + 'no-match-begins-with' => ['coupon', 'bw', 'foocoupon2', false], + 'match-ends-with' => ['pon1', 'ew', 'coupon1', true], + 'no-match-ends-with' => ['pon2', 'ew', 'coupon2foo', false], +]); diff --git a/tests/Feature/Order/Conditions/CustomerConditionRuleTest.php b/tests/Feature/Order/Conditions/CustomerConditionRuleTest.php new file mode 100644 index 0000000000..75aa8c1929 --- /dev/null +++ b/tests/Feature/Order/Conditions/CustomerConditionRuleTest.php @@ -0,0 +1,94 @@ +values = $values; + $rule->operator = $operator; + $condition->addConditionRule($rule); + + return $condition; +} + +beforeEach(function() { + $this->fixture = OrdersFixture::seed(); + $this->order = $this->fixture->orders['completed-new']; + + $this->otherUser = new User(); + $this->otherUser->username = 'not-customer1'; + $this->otherUser->email = 'not-customer1@crafttest.com'; + $this->otherUser->active = true; + if (!Elements::saveElement($this->otherUser)) { + throw new RuntimeException('Could not save user: ' . json_encode($this->otherUser->errors()->all())); + } +}); + +test('matchElement (in) matches the order\'s customer', function() { + $condition = customerCondition([$this->fixture->customer->id]); + + expect($condition->matchElement($this->order))->toBeTrue(); +}); + +test('matchElement (in) does not match a different customer', function() { + $condition = customerCondition([$this->otherUser->id]); + + expect($condition->matchElement($this->order))->toBeFalse(); +}); + +test('matchElement (not in) matches when the order\'s customer is excluded', function() { + $condition = customerCondition([$this->otherUser->id], 'ni'); + + expect($condition->matchElement($this->order))->toBeTrue(); +}); + +test('matchElement (not in) does not match when the order\'s customer is the excluded one', function() { + $condition = customerCondition([$this->fixture->customer->id], 'ni'); + + expect($condition->matchElement($this->order))->toBeFalse(); +}); + +test('modifyQuery (in) matches the order\'s customer', function() { + $condition = customerCondition([$this->fixture->customer->id]); + + $query = Order::find(); + $condition->modifyQuery($query); + + expect($query->ids())->toContain($this->order->id); +}); + +test('modifyQuery (in) does not match a different customer', function() { + $condition = customerCondition([$this->otherUser->id]); + + $query = Order::find(); + $condition->modifyQuery($query); + + expect($query->ids())->toBeEmpty(); +}); + +test('modifyQuery (not in) matches when the order\'s customer is excluded', function() { + $condition = customerCondition([$this->otherUser->id], 'ni'); + + $query = Order::find(); + $condition->modifyQuery($query); + + expect($query->ids())->toContain($this->order->id); +}); + +test('modifyQuery (not in) does not match when the order\'s customer is the excluded one', function() { + $condition = customerCondition([$this->fixture->customer->id], 'ni'); + + $query = Order::find(); + $condition->modifyQuery($query); + + expect($query->ids())->not->toContain($this->order->id); +}); diff --git a/tests/Feature/Order/Conditions/OrderAttributeConditionRulesTest.php b/tests/Feature/Order/Conditions/OrderAttributeConditionRulesTest.php new file mode 100644 index 0000000000..d794d4420d --- /dev/null +++ b/tests/Feature/Order/Conditions/OrderAttributeConditionRulesTest.php @@ -0,0 +1,157 @@ +addConditionRule($rule); + + return $condition; +} + +beforeEach(function() { + $this->fixture = OrdersFixture::seed(); +}); + +test('CompletedConditionRule filters by isCompleted', function() { + $rule = new CompletedConditionRule(); + $rule->value = true; + $condition = conditionWithRule($rule); + + $query = Order::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + expect($ids)->toContain($this->fixture->orders['completed-new']->id); +}); + +test('OrderStatusConditionRule filters by order status', function() { + $rule = new OrderStatusConditionRule(); + $rule->setValues([app(OrderStatuses::class)->getOrderStatusById($this->fixture->shippedOrderStatusId)->uid]); + $condition = conditionWithRule($rule); + + $query = Order::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + expect($ids)->toContain($this->fixture->orders['completed-shipped']->id); + expect($ids)->not->toContain($this->fixture->orders['completed-new']->id); +}); + +test('TotalQtyConditionRule (a Values-attribute rule) filters by total quantity', function() { + $rule = new TotalQtyConditionRule(); + $rule->value = '5'; + $rule->operator = '>='; + $condition = conditionWithRule($rule); + + $query = Order::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + // completed-new/completed-new-past have 1 white + 4 blue = qty 5; completed-shipped has qty 1. + expect($ids)->toContain($this->fixture->orders['completed-new']->id); + expect($ids)->not->toContain($this->fixture->orders['completed-shipped']->id); +}); + +test('TotalPriceConditionRule (a Currency-attribute rule) filters by total price', function() { + $rule = new TotalPriceConditionRule(); + $rule->value = '50'; + $rule->operator = '>'; + $condition = conditionWithRule($rule); + + $query = Order::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + // completed-new totals ~107.95 (1 white @ 19.99 + 4 blue @ 21.99); completed-shipped is ~19.99. + expect($ids)->toContain($this->fixture->orders['completed-new']->id); + expect($ids)->not->toContain($this->fixture->orders['completed-shipped']->id); +}); + +test('ReferenceConditionRule (a Text-attribute rule) filters by reference', function() { + $order = Order::find()->id($this->fixture->orders['completed-new']->id)->one(); + $rule = new ReferenceConditionRule(); + $rule->value = $order->reference; + $condition = conditionWithRule($rule); + + $query = Order::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + expect($ids)->toContain($order->id); + expect($ids)->not->toContain($this->fixture->orders['completed-shipped']->id); +}); + +test('PaidConditionRule filters unpaid orders', function() { + $rule = new PaidConditionRule(); + $rule->value = false; + $condition = conditionWithRule($rule); + + $query = Order::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + // None of the fixture orders have any payments recorded. + expect($ids)->toContain($this->fixture->orders['completed-new']->id); +}); + +test('ShippingMethodConditionRule filters by shipping method handle', function() { + $rule = new ShippingMethodConditionRule(); + $rule->setValues(['usShipping']); + $condition = conditionWithRule($rule); + + $query = Order::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + expect($ids)->toContain($this->fixture->orders['completed-new']->id); + expect($ids)->not->toContain($this->fixture->orders['completed-shipped']->id); +}); + +test('HasAdminNoticesConditionRule filters orders without admin notices', function() { + $rule = new HasAdminNoticesConditionRule(); + $rule->value = false; + $condition = conditionWithRule($rule); + + $query = Order::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + expect($ids)->toContain($this->fixture->orders['completed-new']->id); +}); + +test('ContainsPurchasablesConditionRule filters orders containing a specific purchasable', function() { + $rule = new ContainsPurchasablesConditionRule(); + $rule->setElementIds([$this->fixture->blue->id]); + $condition = conditionWithRule($rule); + + $query = Order::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + // Only completed-new/completed-new-past include the blue variant; completed-shipped is white-only. + expect($ids)->toContain($this->fixture->orders['completed-new']->id); + expect($ids)->not->toContain($this->fixture->orders['completed-shipped']->id); +}); diff --git a/tests/Feature/Order/Conditions/OrderConditionTest.php b/tests/Feature/Order/Conditions/OrderConditionTest.php new file mode 100644 index 0000000000..36f8b7db8e --- /dev/null +++ b/tests/Feature/Order/Conditions/OrderConditionTest.php @@ -0,0 +1,77 @@ +toBeInstanceOf(OrderCondition::class); +}); + +test('the condition exposes all built-in order condition rule types', function() { + $rules = array_keys(Order::createCondition()->getSelectableConditionRules()); + + expect($rules)->toContain( + DateOrderedConditionRule::class, + CompletedConditionRule::class, + CouponCodeConditionRule::class, + CustomerConditionRule::class, + PaidConditionRule::class, + HasPurchasableConditionRule::class, + ItemSubtotalConditionRule::class, + ItemTotalConditionRule::class, + OrderStatusConditionRule::class, + OrderSiteConditionRule::class, + ReferenceConditionRule::class, + ShippingMethodConditionRule::class, + TotalDiscountConditionRule::class, + TotalPaidConditionRule::class, + TotalPriceConditionRule::class, + TotalQtyConditionRule::class, + TotalTaxConditionRule::class, + TotalConditionRule::class, + ); +}); + +/** + * `$queryParams` restricts the builder to rules that don't compete with a param the condition's + * caller already controls outside the builder (see `HasOrdersConditionRule`, which sets + * `queryParams = ['customerId']` since it's already scoping the nested order query to a single + * customer via `Order::find()->customerId(...)`). + */ +test('queryParams excludes rules whose exclusive query param is already reserved', function() { + $condition = new OrderCondition(); + $condition->queryParams = ['customerId']; + + $selectable = $condition->getSelectableConditionRules(); + + expect($selectable)->not->toHaveKey(CustomerConditionRule::class); + expect($selectable)->toHaveKey(CompletedConditionRule::class); +}); + +test('an empty queryParams allow-list excludes nothing', function() { + $condition = new OrderCondition(); + + $selectable = $condition->getSelectableConditionRules(); + + expect($selectable)->toHaveKey(CustomerConditionRule::class); +}); diff --git a/tests/Feature/Order/LineItem/LineItemPurchasableTest.php b/tests/Feature/Order/LineItem/LineItemPurchasableTest.php new file mode 100644 index 0000000000..035c46cf79 --- /dev/null +++ b/tests/Feature/Order/LineItem/LineItemPurchasableTest.php @@ -0,0 +1,166 @@ +title = $sku; + $variant->setPrimaryOwner($fixture->product); + $variant->setSku($sku); + $variant->setBasePrice($basePrice); + $variant->promotable = true; + $variant->siteId = Sites::getCurrentSite()->id; + if (!Elements::saveElement($variant)) { + throw new RuntimeException('Could not save variant: ' . json_encode($variant->errors()->all())); + } + + return Variant::find()->id($variant->id)->one(); +} + +test('price and promotional price round to currency precision, and subtotal reflects the promotional price', function() { + $lineItem = new LineItem(); + $lineItem->setPrice(1.239); + $lineItem->setPromotionalPrice(1.114); + $lineItem->qty = 2; + + expect($lineItem->getPrice())->toBe(1.24); + expect($lineItem->getPromotionalPrice())->toBe(1.11); + expect($lineItem->getSalePrice())->toBe(1.11); + expect($lineItem->getSubtotal())->toBe(2.22); +}); + +test('populate() sets price, sale price, and sku from a purchasable', function() { + $purchasable = new MockPurchasable(); + $lineItem = new LineItem(); + $lineItem->populate($purchasable); + + expect($lineItem->getPrice())->toBe(25.10); + expect($lineItem->getSalePrice())->toBe(25.10); + expect($lineItem->getPromotionalAmount())->toBe(0.0); + expect($lineItem->getSku())->toBe('commerce_testing_unique_sku'); + expect($lineItem->getOnPromotion())->toBeFalse(); +}); + +test('getIsPromotable ignores a manual override when the line item has a live purchasable', function() { + $fixture = OrdersFixture::seed(); + + $lineItem = new LineItem(); + $lineItem->populate($fixture->blue); + + // Manually set the property to make sure it doesn't do anything when it's a purchasable line item. + $lineItem->setIsPromotable(false); + + expect($lineItem->getIsPromotable())->toBeTrue(); +}); + +test('getHasFreeShipping ignores a manual override when the line item has a live purchasable', function() { + $fixture = OrdersFixture::seed(); + + $lineItem = new LineItem(); + $lineItem->populate($fixture->blue); + + // Manually set the property to make sure it doesn't do anything when it's a purchasable line item. + $lineItem->setHasFreeShipping(true); + + expect($lineItem->getHasFreeShipping())->toBeFalse(); +}); + +test('populate() applies an active percentage sale to the line item price', function() { + $fixture = OrdersFixture::seed(); + $variant = promotableVariant($fixture, 'rad-hood', 123.99); + + $sale = new Sale(); + $sale->name = 'My Percentage Sale'; + $sale->description = 'My test percentage sale.'; + // ->apply defaults to Sale::APPLY_BY_PERCENT already. + $sale->applyAmount = -0.10; + $sale->allGroups = true; + $sale->allPurchasables = false; + $sale->allCategories = true; + $sale->setPurchasableIds([$variant->id]); + if (!app(Sales::class)->saveSale($sale)) { + throw new RuntimeException('Could not save sale: ' . json_encode($sale->errors()->all())); + } + + $lineItem = new LineItem(); + $lineItem->populate($variant); + + expect(round($lineItem->getPrice(), 2))->toBe(123.99); + expect(round($lineItem->getSalePrice(), 2))->toBe(111.59); + expect(round($lineItem->getPromotionalAmount(), 2))->toBe(12.40); + expect($lineItem->getOnPromotion())->toBeTrue(); +}); + +test('a custom line item contributes its price times quantity to the order total', function() { + $lineItem = new LineItem(); + $lineItem->type = LineItemType::Custom; + $lineItem->description = 'Custom'; + $lineItem->setSku('custom-sku'); + $lineItem->setPrice(10.00); + $lineItem->qty = 2; + $lineItem->setIsPromotable(false); + $lineItem->setHasFreeShipping(true); + + $order = new Order(); + $order->number = app(Carts::class)->generateCartNumber(); + $order->setLineItems([$lineItem]); + + expect($order->getTotal())->toEqual(20.00); +}); + +test('a custom line item does not include purchasable in extraFields, and toArray() does not throw', function() { + $lineItem = new LineItem(); + $lineItem->type = LineItemType::Custom; + $lineItem->description = 'Custom'; + $lineItem->setSku('custom-sku'); + $lineItem->setPrice(10.00); + $lineItem->qty = 2; + + $order = new Order(); + $order->number = app(Carts::class)->generateCartNumber(); + $order->setLineItems([$lineItem]); + + expect($lineItem->extraFields())->not->toContain('purchasable'); + + $data = $lineItem->toArray([], ['*']); + expect($data)->toBeArray(); + + $data = $lineItem->toArray([], ['purchasable']); + expect($data)->toBeArray(); + expect($data)->not->toHaveKey('purchasable'); +}); + +test('a purchasable line item includes purchasable in both extraFields and toArray()', function() { + $fixture = OrdersFixture::seed(); + + $lineItem = new LineItem(); + $lineItem->populate($fixture->blue); + $lineItem->qty = 1; + + $order = new Order(); + $order->number = app(Carts::class)->generateCartNumber(); + $order->setLineItems([$lineItem]); + + expect($lineItem->extraFields())->toContain('purchasable'); + + $data = $lineItem->toArray([], ['purchasable']); + expect($data)->toHaveKey('purchasable'); + expect($data['purchasable'])->not->toBeNull(); +}); diff --git a/tests/Feature/Order/LineItem/LineItemRulesTest.php b/tests/Feature/Order/LineItem/LineItemRulesTest.php new file mode 100644 index 0000000000..18cc00503f --- /dev/null +++ b/tests/Feature/Order/LineItem/LineItemRulesTest.php @@ -0,0 +1,51 @@ +type = LineItemType::Custom; + $lineItem->qty = 1; + $lineItem->taxCategoryId = 1; + $lineItem->shippingCategoryId = 1; + $lineItem->setPrice(10); + + expect($lineItem->validate())->toBeTrue(); +}); + +test('validate() fails when qty is below 1', function() { + $lineItem = new LineItem(); + $lineItem->type = LineItemType::Custom; + $lineItem->qty = 0; + $lineItem->taxCategoryId = 1; + $lineItem->shippingCategoryId = 1; + + expect($lineItem->validate())->toBeFalse(); + expect($lineItem->errors()->has('qty'))->toBeTrue(); +}); + +test('validate() fails when price is negative', function() { + $lineItem = new LineItem(); + $lineItem->type = LineItemType::Custom; + $lineItem->qty = 1; + $lineItem->taxCategoryId = 1; + $lineItem->shippingCategoryId = 1; + $lineItem->setPrice(-5); + + expect($lineItem->validate())->toBeFalse(); + expect($lineItem->errors()->has('price'))->toBeTrue(); +}); + +test('validate() requires a snapshot when type is Purchasable', function() { + $lineItem = new LineItem(); + $lineItem->type = LineItemType::Purchasable; + $lineItem->qty = 1; + $lineItem->taxCategoryId = 1; + $lineItem->shippingCategoryId = 1; + + expect($lineItem->validate())->toBeFalse(); + expect($lineItem->errors()->has('snapshot'))->toBeTrue(); +}); diff --git a/tests/Feature/Order/LineItem/LineItemValidationTest.php b/tests/Feature/Order/LineItem/LineItemValidationTest.php new file mode 100644 index 0000000000..5aac90eb18 --- /dev/null +++ b/tests/Feature/Order/LineItem/LineItemValidationTest.php @@ -0,0 +1,81 @@ +fixture = OrdersFixture::seed(); + + $this->cart = new Order(); + $this->cart->storeId = $this->fixture->storeId; + $this->cart->setCustomerId($this->fixture->customer->id); + if (!Elements::saveElement($this->cart, false)) { + throw new RuntimeException('Could not save cart: ' . json_encode($this->cart->errors()->all())); + } +}); + +test('saveLineItem fails and returns false when qty exceeds the purchasable max qty', function() { + $variant = new Variant(); + $variant->title = 'Limited'; + $variant->setPrimaryOwner($this->fixture->product); + $variant->setSku('limited-edition'); + $variant->setBasePrice(9.99); + $variant->siteId = Sites::getCurrentSite()->id; + $variant->maxQty = 1; + if (!Elements::saveElement($variant)) { + throw new RuntimeException('Could not save variant: ' . json_encode($variant->errors()->all())); + } + + $lineItem = app(LineItems::class)->create($this->cart, [ + 'purchasableId' => $variant->id, + 'qty' => 5, + ]); + $this->cart->setLineItems([$lineItem]); + + $saved = app(LineItems::class)->saveLineItem($lineItem, true); + + expect($saved)->toBeFalse(); + expect($lineItem->errors()->has('qty'))->toBeTrue(); +}); + +test('saveLineItem succeeds and saves when qty is within limits', function() { + $lineItem = app(LineItems::class)->create($this->cart, [ + 'purchasableId' => $this->fixture->white->id, + 'qty' => 1, + ]); + $this->cart->setLineItems([$lineItem]); + + $saved = app(LineItems::class)->saveLineItem($lineItem, true); + + expect($saved)->toBeTrue(); + expect($lineItem->id)->not->toBeNull(); +}); + +test('saveLineItem skips validation when $runValidation is false', function() { + $variant = new Variant(); + $variant->title = 'Limited'; + $variant->setPrimaryOwner($this->fixture->product); + $variant->setSku('limited-edition-2'); + $variant->setBasePrice(9.99); + $variant->siteId = Sites::getCurrentSite()->id; + $variant->maxQty = 1; + if (!Elements::saveElement($variant)) { + throw new RuntimeException('Could not save variant: ' . json_encode($variant->errors()->all())); + } + + $lineItem = app(LineItems::class)->create($this->cart, [ + 'purchasableId' => $variant->id, + 'qty' => 5, + ]); + $this->cart->setLineItems([$lineItem]); + + $saved = app(LineItems::class)->saveLineItem($lineItem, false); + + expect($saved)->toBeTrue(); +}); diff --git a/tests/Feature/Order/LineItem/LineItemsTest.php b/tests/Feature/Order/LineItem/LineItemsTest.php new file mode 100644 index 0000000000..2fa120cc88 --- /dev/null +++ b/tests/Feature/Order/LineItem/LineItemsTest.php @@ -0,0 +1,120 @@ +getAllLineItemsByOrderId(9999))->toBe([]); + + $fixture = OrdersFixture::seed(); + $order = $fixture->orders['completed-new']; + + $lineItems = app(LineItems::class)->getAllLineItemsByOrderId($order->id); + + expect($lineItems)->toBeArray(); + expect($lineItems)->toHaveCount(2); +}); + +test('resolveLineItem is consistent across repeated calls for an unsaved order', function() { + $fixture = OrdersFixture::seed(); + $order = new Order(); + + $first = app(LineItems::class)->resolveLineItem($order, $fixture->blue->id, ['giftWrapped' => 'no']); + $second = app(LineItems::class)->resolveLineItem($order, $fixture->blue->id, ['giftWrapped' => 'no']); + + expect($second)->toBeInstanceOf(LineItem::class); + expect($second->getPrice())->toBe($first->getPrice()); + expect($second->getSalePrice())->toBe($first->getSalePrice()); + expect($second->getOptionsSignature())->toBe($first->getOptionsSignature()); + expect($second->purchasableId)->toBe($first->purchasableId); + expect($second->orderId)->toBe($first->orderId); +}); + +test('resolveLineItem always returns a brand-new line item for a completed order, even with matching purchasable and options', function() { + // The options signature persisted for a completed order's line item is salted with the line + // item's own id (see LineItem::getOptionsSignature()), so the plain options-only signature + // resolveLineItem() looks up by can never match it — every resolve on a completed order + // creates a fresh line item rather than reusing the existing one. + $fixture = OrdersFixture::seed(); + $order = $fixture->orders['completed-new']; + $orderLineItem = $order->getLineItems()[0]; + + $resolvedLineItem = app(LineItems::class)->resolveLineItem($order, $orderLineItem->purchasableId, $orderLineItem->getOptions()); + + expect($resolvedLineItem)->toBeInstanceOf(LineItem::class); + expect($resolvedLineItem->getPrice())->toBe($orderLineItem->getPrice()); + expect($resolvedLineItem->getSalePrice())->toBe($orderLineItem->getSalePrice()); + expect($resolvedLineItem->getOptionsSignature())->not->toBe($orderLineItem->getOptionsSignature()); + expect($resolvedLineItem->purchasableId)->toBe($orderLineItem->purchasableId); + expect($resolvedLineItem->orderId)->toBe($orderLineItem->orderId); +}); + +test('resolveLineItem populates price from the purchasable for a new line item', function() { + $fixture = OrdersFixture::seed(); + $order = $fixture->orders['completed-new']; + $lineItem = $order->getLineItems()[1]; + + $resolvedLineItem = app(LineItems::class)->resolveLineItem($order, $lineItem->purchasableId, $lineItem->getOptions()); + + expect($resolvedLineItem)->toBeInstanceOf(LineItem::class); + expect($resolvedLineItem->getPrice())->toBe($fixture->blue->getPrice()); +}); + +test('resolveLineItem populates price from the purchasable for an unsaved order', function() { + $fixture = OrdersFixture::seed(); + $order = new Order(); + + $resolvedLineItem = app(LineItems::class)->resolveLineItem($order, $fixture->blue->id, ['giftWrapped' => 'no']); + + expect($resolvedLineItem)->toBeInstanceOf(LineItem::class); + expect($resolvedLineItem->getPrice())->toBe($fixture->blue->getPrice()); +}); + +test('getLineItemById returns the matching line item', function() { + $fixture = OrdersFixture::seed(); + $lineItems = $fixture->orders['completed-new']->getLineItems(); + + $lineItem = app(LineItems::class)->getLineItemById($lineItems[0]->id); + + expect($lineItem->purchasableId)->toBe($lineItems[0]->purchasableId); + expect($lineItem->qty)->toBe($lineItems[0]->qty); +}); + +test('a line item snapshot is unpacked identically whether fetched by id or as part of the order', function() { + $fixture = OrdersFixture::seed(); + $order = $fixture->orders['completed-new']; + + $lineItemById = app(LineItems::class)->getLineItemById($order->getLineItems()[0]->id); + /** @var LineItem $lineItemFromAll */ + $lineItemFromAll = collect(app(LineItems::class)->getAllLineItemsByOrderId($order->id))->firstWhere('id', $lineItemById->id); + + expect($lineItemById->getSnapshot())->toBeArray(); + expect($lineItemFromAll->getSnapshot())->toBeArray(); + expect($lineItemFromAll->getSnapshot())->toBe($lineItemById->getSnapshot()); +}); + +test('create() creates a line item on the order with the given params', function() { + $fixture = OrdersFixture::seed(); + $order = $fixture->orders['completed-new']; + $sourceLineItem = $order->getLineItems()[0]; + $qty = 4; + $note = 'My note'; + + $lineItem = app(LineItems::class)->create($order, [ + 'purchasableId' => $sourceLineItem->purchasableId, + 'options' => $sourceLineItem->getOptions(), + 'qty' => $qty, + 'note' => $note, + ]); + + expect($lineItem)->toBeInstanceOf(LineItem::class); + expect($lineItem->orderId)->toBe($order->id); + expect($lineItem->purchasableId)->toBe($sourceLineItem->purchasableId); + expect($lineItem->getOptions())->toBe($sourceLineItem->getOptions()); + expect($lineItem->qty)->toBe($qty); + expect($lineItem->note)->toBe($note); +}); diff --git a/tests/Feature/Order/OrderAddressesTest.php b/tests/Feature/Order/OrderAddressesTest.php new file mode 100644 index 0000000000..a48ffd8df9 --- /dev/null +++ b/tests/Feature/Order/OrderAddressesTest.php @@ -0,0 +1,94 @@ +setBillingAddress($billingAddress); + $order->setShippingAddress($shippingAddress); + + expect($order->hasMatchingAddresses($attributes))->toBe($expected); +})->with([ + 'all matching' => [ + ['fullName' => 'Johnny Appleseed', 'addressLine1' => '1 Main Street'], + ['fullName' => 'Johnny Appleseed', 'addressLine1' => '1 Main Street'], + true, + ], + 'no matching address' => [ + ['fullName' => 'Johnny Appleseed', 'addressLine1' => '1 Main Street'], + ['fullName' => 'Johnny Appleseed', 'addressLine1' => '123 Main Street'], + false, + ], + 'no matching name' => [ + ['fullName' => 'Johnny Appleseed', 'addressLine1' => '1 Main Street'], + ['fullName' => 'Jenny Appleseed', 'addressLine1' => '1 Main Street'], + false, + ], + 'all matching, full address' => [ + [ + 'fullName' => 'Johnny Appleseed', + 'addressLine1' => '1 Main Street', + 'addressLine2' => 'SW', + 'locality' => 'Bend', + 'administrativeArea' => 'OR', + 'countryCode' => 'US', + 'postalCode' => '12345', + ], + [ + 'fullName' => 'Johnny Appleseed', + 'addressLine1' => '1 Main Street', + 'addressLine2' => 'SW', + 'locality' => 'Bend', + 'administrativeArea' => 'OR', + 'countryCode' => 'US', + 'postalCode' => '12345', + ], + true, + ], + 'matching, restricted to a subset of attributes' => [ + [ + 'fullName' => 'Johnny Appleseed', + 'addressLine1' => '1 Main Street', + 'addressLine2' => 'SW', + 'locality' => 'Bend', + 'administrativeArea' => 'OR', + 'countryCode' => 'US', + 'postalCode' => '12345', + ], + [ + 'fullName' => 'Johnny Appleseed', + 'addressLine1' => '123 Main Street', + 'addressLine2' => 'SW', + 'locality' => 'Bend', + 'administrativeArea' => 'OR', + 'countryCode' => 'US', + 'postalCode' => '12345', + ], + true, + ['addressLine2', 'locality', 'administrativeArea'], + ], + 'not matching, restricted to a subset of attributes' => [ + [ + 'fullName' => 'Johnny Appleseed', + 'addressLine1' => '1 Main Street', + 'addressLine2' => 'SW', + 'locality' => 'Bend', + 'administrativeArea' => 'OR', + 'countryCode' => 'US', + 'postalCode' => '12345', + ], + [ + 'fullName' => 'Johnny Appleseed', + 'addressLine1' => '123 Main Street', + 'addressLine2' => 'SW', + 'locality' => 'Bend', + 'administrativeArea' => 'OR', + 'countryCode' => 'US', + 'postalCode' => '12345', + ], + false, + ['addressLine1'], + ], +]); diff --git a/tests/Feature/Order/OrderCustomerTest.php b/tests/Feature/Order/OrderCustomerTest.php new file mode 100644 index 0000000000..77f156e087 --- /dev/null +++ b/tests/Feature/Order/OrderCustomerTest.php @@ -0,0 +1,68 @@ +username = 'inactive-order-customer'; + $user->email = 'inactive.order.customer@crafttest.com'; + $user->active = false; + if (!Elements::saveElement($user)) { + throw new RuntimeException('Could not save inactive user: ' . json_encode($user->errors()->all())); + } + + return $user; +} + +test('setEmail assigns the order to the existing user matching that email, and logs a deprecation warning', function(string $userKey) { + $user = match ($userKey) { + 'credentialed' => OrdersFixture::seed()->customer, + 'inactive' => inactiveOrderCustomer(), + }; + $email = $user->email; + + $order = new Order(); + $order->setEmail($email); + + expect($order->getEmail())->toBe($email); + expect($order->getCustomer())->not->toBeNull(); + expect($order->getCustomer()->email)->toBe($email); + + $setEmailLogs = array_filter(Deprecator::getRequestLogs(), fn($log) => $log->key === Order::class . '::setEmail'); + expect($setEmailLogs)->toHaveCount(1); +})->with([ + 'existing credentialed user' => ['credentialed'], + 'existing inactive user' => ['inactive'], +]); + +test('setCustomer assigns and clears the order customer', function(string $userKey) { + $user = match ($userKey) { + 'credentialed' => OrdersFixture::seed()->customer, + 'inactive' => inactiveOrderCustomer(), + }; + + $order = new Order(); + $order->setCustomer($user); + + expect($order->getEmail())->toBe($user->email); + expect($order->getCustomer())->not->toBeNull(); + expect($order->getCustomer()->email)->toBe($user->email); + expect($order->getCustomer()->id)->toBe($user->id); + expect($order->getCustomerId())->toBe($user->id); + + $order->setCustomer(); + + expect($order->getCustomer())->toBeNull(); + expect($order->getCustomerId())->toBeNull(); + expect($order->getEmail())->toBeNull(); +})->with([ + 'existing credentialed user' => ['credentialed'], + 'existing inactive user' => ['inactive'], +]); diff --git a/tests/Feature/Order/OrderMarkAsCompleteTest.php b/tests/Feature/Order/OrderMarkAsCompleteTest.php new file mode 100644 index 0000000000..bb47b138ec --- /dev/null +++ b/tests/Feature/Order/OrderMarkAsCompleteTest.php @@ -0,0 +1,109 @@ +createGateway([ + 'type' => Dummy::class, + 'name' => 'Dummy', + 'handle' => 'dummy', + 'isFrontendEnabled' => true, + ]); + $gateway->id = 1; + $order->gatewayId = $gateway->id; + + $mock = Mockery::mock(Gateways::class)->makePartial(); + $mock->shouldReceive('getGatewayById')->andReturn($gateway); + app()->instance(Gateways::class, $mock); +} + +test('markAsComplete sets dateOrdered, isCompleted, and orderCompletedEmail', function() { + $fixture = OrdersFixture::seed(); + $user = Users::ensureUserByEmail('test@newemailaddress.xyz'); + + $order = new Order(); + $order->setCustomer($user); + + $lineItem = app(LineItems::class)->create($order, [ + 'purchasableId' => $fixture->white->id, + 'qty' => 4, + 'note' => 'My note', + ]); + $order->setLineItems([$lineItem]); + + expect($order->dateOrdered)->toBeNull(); + expect($order->isCompleted)->toBeFalse(); + expect($order->orderCompletedEmail)->toBeNull(); + + expect($order->markAsComplete())->toBeTrue(); + + expect($order->dateOrdered)->toBeInstanceOf(DateTime::class); + expect($order->isCompleted)->toBeTrue(); + expect($order->orderCompletedEmail)->toBe($user->email); +}); + +test('saveTransaction updates dateFirstPaid on first payment, and preserves it across a refund and a later repayment', function() { + $fixture = OrdersFixture::seed(); + $order = $fixture->orders['completed-new']; + stubGatewayForOrder($order); + + $transactions = app(Transactions::class); + + $purchase = $transactions->createTransaction($order, typeOverride: TransactionRecord::TYPE_PURCHASE); + $purchase->status = TransactionRecord::STATUS_SUCCESS; + $transactions->saveTransaction($purchase); + + $dateFirstPaid = $purchase->getOrder()->dateFirstPaid; + $datePaid = $purchase->getOrder()->datePaid; + + expect($dateFirstPaid)->not->toBeNull(); + expect($datePaid)->not->toBeNull(); + + // Refunding part of the payment clears datePaid (the order is no longer paid in full) but + // must not disturb dateFirstPaid, which records when the order was first paid at all. + $refund = $transactions->createTransaction(parentTransaction: $purchase, typeOverride: TransactionRecord::TYPE_REFUND); + $refund->amount = 10; + $refund->paymentAmount = 10; + $refund->status = TransactionRecord::STATUS_SUCCESS; + $transactions->saveTransaction($refund); + + $refundDateFirstPaid = $refund->getOrder()->dateFirstPaid; + $refundDatePaid = $refund->getOrder()->datePaid; + + expect($refundDateFirstPaid->format('Y-m-d H:i:s'))->toBe($dateFirstPaid->format('Y-m-d H:i:s')); + expect($refundDatePaid)->toBeNull(); + + // A slight delay so the repayment's timestamp is distinguishable from the first payment's — + // both dates are stored at second resolution. + sleep(3); + + // Paying off the remaining balance updates datePaid again, but dateFirstPaid still doesn't move. + $repayment = $transactions->createTransaction($order, typeOverride: TransactionRecord::TYPE_PURCHASE); + $repayment->status = TransactionRecord::STATUS_SUCCESS; + $transactions->saveTransaction($repayment); + + $nextDateFirstPaid = $repayment->getOrder()->dateFirstPaid; + $nextDatePaid = $repayment->getOrder()->datePaid; + + expect($nextDateFirstPaid->format('Y-m-d H:i:s'))->toBe($dateFirstPaid->format('Y-m-d H:i:s')); + expect($nextDatePaid->format('Y-m-d H:i:s'))->not->toBe($datePaid->format('Y-m-d H:i:s')); + expect($nextDatePaid)->not->toBeNull(); + + $transactions->deleteTransactionById($purchase->id); + $transactions->deleteTransactionById($refund->id); + $transactions->deleteTransactionById($repayment->id); +}); diff --git a/tests/Feature/Order/OrderNoticesTest.php b/tests/Feature/Order/OrderNoticesTest.php new file mode 100644 index 0000000000..33f25c3f67 --- /dev/null +++ b/tests/Feature/Order/OrderNoticesTest.php @@ -0,0 +1,160 @@ +order = new Order(); +}); + +test('addNotice and addNotices append to getNotices, and getFirstNotice returns the earliest match', function() { + $firstNotice = new OrderNotice([ + 'type' => 'priceChange', + 'attribute' => 'lineItems', + 'message' => 'The Price of the product changed.', + ]); + $this->order->addNotice($firstNotice); + + $notices = $this->order->getNotices(); + $firstFromOrder = $this->order->getFirstNotice(); + expect($firstFromOrder->type)->toBe($firstNotice->type); + expect($firstFromOrder->attribute)->toBe($firstNotice->attribute); + expect($firstFromOrder->message)->toBe($firstNotice->message); + expect($notices)->toHaveCount(1); + + $secondNotice = new OrderNotice([ + 'type' => 'lineItemRemoved', + 'attribute' => 'lineItems', + 'message' => 'The x Product is no longer available and has been removed.', + ]); + $this->order->addNotice($secondNotice); + + // The earlier `getNotices()` call returned a snapshot, so it doesn't grow after the fact. + expect($notices)->toHaveCount(1); + expect($this->order->getNotices())->toHaveCount(2); + + $this->order->addNotices([$firstNotice, $secondNotice]); + expect($this->order->getNotices())->toHaveCount(4); +}); + +test('clearNotices removes matches by type, by attribute, by both, or all customer notices', function() { + $firstNotice = new OrderNotice([ + 'type' => 'priceChange', + 'attribute' => 'lineItems', + 'message' => 'The Price of the product changed.', + ]); + $secondNotice = new OrderNotice([ + 'type' => 'lineItemRemoved', + 'attribute' => 'lineItems', + 'message' => 'The x Product is no longer available and has been removed.', + ]); + + $this->order->addNotices([$firstNotice, $secondNotice, $firstNotice, $secondNotice]); + expect($this->order->getNotices())->toHaveCount(4); + + // Clearing by type + $this->order->clearNotices('lineItemRemoved'); + expect($this->order->getNotices())->toHaveCount(2); + $this->order->clearNotices('priceChange'); + expect($this->order->getNotices())->toHaveCount(0); + + $thirdNotice = new OrderNotice([ + 'type' => 'couponNotValid', + 'attribute' => 'couponCode', + 'message' => 'The x Product is no longer available and has been removed.', + ]); + + // Clearing by attribute + $this->order->addNotices([$firstNotice, $secondNotice, $firstNotice, $secondNotice, $thirdNotice]); + expect($this->order->getNotices())->toHaveCount(5); + $this->order->clearNotices(null, 'lineItems'); + expect($this->order->getNotices())->toHaveCount(1); // only $thirdNotice remains + + // Clearing all + $this->order->addNotices([$firstNotice, $secondNotice, $firstNotice, $secondNotice, $thirdNotice]); + $this->order->clearNotices(); + expect($this->order->getNotices())->toHaveCount(0); + + // Clearing by both type and attribute + $this->order->addNotices([$firstNotice, $secondNotice, $firstNotice, $secondNotice, $thirdNotice]); + $this->order->clearNotices('lineItemRemoved', 'lineItems'); + expect($this->order->getNotices())->toHaveCount(3); // both $priceChange copies and $thirdNotice remain + + expect($this->order->hasNotices())->toBeTrue(); + expect($this->order->hasNotices('couponNotValid'))->toBeTrue(); + expect($this->order->getNotices('couponNotValid'))->toHaveCount(1); + expect($this->order->hasNotices(null, 'lineItems'))->toBeTrue(); + expect($this->order->getNotices(null, 'lineItems'))->toHaveCount(2); +}); + +test('getNotices excludes admin notices by default', function() { + $adminNotice = new OrderNotice([ + 'type' => 'adminAlert', + 'attribute' => 'order', + 'message' => 'This order needs review.', + 'noticeType' => OrderNoticeType::Admin, + ]); + $customerNotice = new OrderNotice([ + 'type' => 'priceChange', + 'attribute' => 'lineItems', + 'message' => 'A price changed.', + ]); + + $this->order->addNotices([$adminNotice, $customerNotice]); + + expect($this->order->getNotices())->toHaveCount(1); + expect($this->order->getNotices()[0]->type)->toBe('priceChange'); + expect($this->order->hasNotices('adminAlert'))->toBeFalse(); +}); + +test('getAdminNotices and hasAdminNotices report only admin notices', function() { + $adminNotice = new OrderNotice([ + 'type' => 'adminAlert', + 'attribute' => 'order', + 'message' => 'This order needs review.', + 'noticeType' => OrderNoticeType::Admin, + ]); + $customerNotice = new OrderNotice([ + 'type' => 'priceChange', + 'attribute' => 'lineItems', + 'message' => 'A price changed.', + ]); + + $this->order->addNotices([$adminNotice, $customerNotice]); + + expect($this->order->getAdminNotices())->toHaveCount(1); + expect($this->order->getAdminNotices()[0]->type)->toBe('adminAlert'); + expect($this->order->hasAdminNotices())->toBeTrue(); +}); + +test('clearNotices preserves admin notices unless the admin type is explicitly included', function() { + $adminNotice = new OrderNotice([ + 'type' => 'adminAlert', + 'attribute' => 'order', + 'message' => 'This order needs review.', + 'noticeType' => OrderNoticeType::Admin, + ]); + $customerNotice = new OrderNotice([ + 'type' => 'priceChange', + 'attribute' => 'lineItems', + 'message' => 'A price changed.', + ]); + + $this->order->addNotices([$adminNotice, $customerNotice]); + + // Clearing without an explicit notice type only clears customer notices. + $this->order->clearNotices(); + + expect($this->order->getNotices())->toHaveCount(0); + expect($this->order->getAdminNotices())->toHaveCount(1); + expect($this->order->hasAdminNotices())->toBeTrue(); + + $this->order->clearNotices(noticeTypes: [OrderNoticeType::Customer, OrderNoticeType::Admin]); + + expect($this->order->getNotices())->toHaveCount(0); + expect($this->order->getAdminNotices())->toHaveCount(0); + expect($this->order->hasAdminNotices())->toBeFalse(); +}); diff --git a/tests/Feature/Order/OrderObjectTemplateVariablesTest.php b/tests/Feature/Order/OrderObjectTemplateVariablesTest.php new file mode 100644 index 0000000000..c7396954bc --- /dev/null +++ b/tests/Feature/Order/OrderObjectTemplateVariablesTest.php @@ -0,0 +1,20 @@ +dateOrdered = new DateTime('2026-03-16 12:16:00'); + + $fileName = renderSandboxedObjectTemplate( + 'Invoice-{{ dateOrdered|date(\'Y-m-d\') }}', + $order, + $order->getObjectTemplateVariables(), + ); + + expect($fileName)->toBe('Invoice-2026-03-16'); +}); diff --git a/tests/Feature/Order/OrderPaymentAmountTest.php b/tests/Feature/Order/OrderPaymentAmountTest.php new file mode 100644 index 0000000000..66df92638c --- /dev/null +++ b/tests/Feature/Order/OrderPaymentAmountTest.php @@ -0,0 +1,89 @@ +id = 1000; + + $lineItem = new LineItem(); + $lineItem->price = 10; + $lineItem->qty = 2; + $order->setLineItems([$lineItem]); + + // There's an amount to owe on this order. + expect($order->hasOutstandingBalance())->toBeTrue(); + + // Amount owed is the payment amount. + expect($order->getPaymentAmount())->toBe($order->getOutstandingBalance()); + + // The setter/getter round-trip. + $order->setPaymentAmount(10); + expect($order->getPaymentAmount())->toBe(10.0); + + // Add a $12 successful transaction to the order. + $transaction1 = new Transaction(); + $transaction1->amount = 12; + $transaction1->type = TransactionRecord::TYPE_PURCHASE; + $transaction1->status = TransactionRecord::STATUS_SUCCESS; + $order->setTransactions([$transaction1]); + + expect($order->getOutstandingBalance())->toBe(8.0); + + // Add a $2 successful refund transaction to the order. + $transaction2 = new Transaction(); + $transaction2->amount = 2; + $transaction2->type = TransactionRecord::TYPE_REFUND; + $transaction2->status = TransactionRecord::STATUS_SUCCESS; + $order->setTransactions([$transaction1, $transaction2]); + + // Paid $12 and refunded $2, order price was $20, so the outstanding amount is now $10. + expect($order->getOutstandingBalance())->toBe(10.0); + + // The payment amount set earlier is still 10. + expect($order->getPaymentAmount())->toBe(10.0); + + // Setting a payment amount in excess of the outstanding balance is ignored, and the + // outstanding balance is returned instead. + $order->setPaymentAmount(1000); + expect($order->getPaymentAmount())->toBe($order->getOutstandingBalance()); +}); + +test('isPaymentAmountPartial compares the payment amount against the outstanding balance across currencies', function(array $lineItems, ?float $paymentAmount, string $paymentCurrency, bool $isPartial) { + PaymentCurrenciesFixture::seed(); + + $order = new Order(); + $order->setLineItems(array_map(fn(array $attributes) => new LineItem($attributes), $lineItems)); + $order->setPaymentCurrency($paymentCurrency); + + if ($paymentAmount !== null) { + $order->setPaymentAmount($paymentAmount); + } + + expect($order->isPaymentAmountPartial())->toBe($isPartial); +})->with([ + 'partial-payment' => [ + ['first' => ['qty' => 1, 'price' => 10], 'second' => ['qty' => 1, 'price' => 20]], + 10, + 'AUD', + true, + ], + 'full-payment-specified' => [ + ['first' => ['qty' => 1, 'price' => 10], 'second' => ['qty' => 1, 'price' => 7.75]], + 23.08, + 'AUD', + false, + ], + 'currency-specified-but-no-amount' => [ + ['first' => ['qty' => 1, 'price' => 10], 'second' => ['qty' => 1, 'price' => 20]], + null, + 'AUD', + false, + ], +]); diff --git a/tests/Feature/Order/OrderQueryTest.php b/tests/Feature/Order/OrderQueryTest.php new file mode 100644 index 0000000000..5bf06ddb2d --- /dev/null +++ b/tests/Feature/Order/OrderQueryTest.php @@ -0,0 +1,102 @@ +email($email); + + expect($orderQuery->all())->toHaveCount($count); +})->with([ + 'normal' => ['customer1@crafttest.com', 3], + 'case-insensitive' => ['CuStOmEr1@crafttest.com', 3], + 'no-results' => ['null@craftcms.com', 0], +]); + +test('couponCode() matches case-insensitively and supports :empty:/:notempty:', function(?string $couponCode, int $count) { + $fixture = OrdersFixture::seed(); + $order = $fixture->orders['completed-new']; + + // Temporarily add a coupon code to an order, bypassing the order element entirely so no + // discount needs to exist for the code to be considered valid. + OrderRecord::query()->where('id', $order->id)->update(['couponCode' => 'foo']); + + $orderQuery = Order::find()->couponCode($couponCode); + + expect($orderQuery->all())->toHaveCount($count); + + OrderRecord::query()->where('id', $order->id)->update(['couponCode' => null]); +})->with([ + 'normal' => ['foo', 1], + 'case-insensitive' => ['fOo', 1], + 'using-null' => [null, 3], + 'empty-code' => [':empty:', 2], + 'not-empty-code' => [':notempty:', 1], + 'no-results' => ['nope', 0], +]); + +test('shippingMethodHandle() matches by string, negated string, array, and negated array', function(mixed $handle, int $count) { + OrdersFixture::seed(); + + $orderQuery = Order::find()->isCompleted()->shippingMethodHandle($handle); + + expect($orderQuery->all())->toHaveCount($count); +})->with([ + 'queryShippingByString' => ['usShipping', 1], + 'queryShippingByNotString' => ['not usShipping', 2], + 'queryShippingByArray' => [['usShipping'], 1], + 'queryShippingByNotArray' => [['not', 'usShipping'], 2], +]); + +test('datePaid() and dateFirstPaid() match orders paid within a date range', function() { + OrdersFixture::seed(); + + // Pay one of the completed orders so there's a datePaid/dateFirstPaid to filter on. + $completedOrder = Order::find()->isCompleted()->one(); + + $gateway = app(Gateways::class)->createGateway([ + 'type' => Dummy::class, + 'name' => 'Dummy', + 'handle' => 'dummy', + 'isFrontendEnabled' => true, + ]); + $gateway->id = 1; + $completedOrder->gatewayId = $gateway->id; + + $mock = Mockery::mock(Gateways::class)->makePartial(); + $mock->shouldReceive('getGatewayById')->andReturn($gateway); + app()->instance(Gateways::class, $mock); + + $transactions = app(Transactions::class); + $transaction = $transactions->createTransaction($completedOrder, typeOverride: TransactionRecord::TYPE_PURCHASE); + $transaction->status = TransactionRecord::STATUS_SUCCESS; + $transactions->saveTransaction($transaction); + + $paidOrder = Order::find()->id($completedOrder->id)->one(); + + // Build the query bounds from the order's own persisted (and re-hydrated) datePaid/dateFirstPaid, + // rather than the wall clock, so the assertion doesn't race against exactly when the payment + // above was recorded. + foreach (['datePaid' => $paidOrder->datePaid, 'dateFirstPaid' => $paidOrder->dateFirstPaid] as $property => $paidAt) { + $paidAt = Carbon::instance($paidAt); + + expect(Order::find()->{$property}('>= ' . $paidAt->toAtomString())->all())->toHaveCount(1); + expect(Order::find()->{$property}([ + '< ' . $paidAt->clone()->addWeek()->toAtomString(), + '> ' . $paidAt->clone()->subWeek()->toAtomString(), + ])->all())->toHaveCount(1); + expect(Order::find()->{$property}('< ' . $paidAt->clone()->subWeek()->toAtomString())->all())->toHaveCount(0); + } + + $transactions->deleteTransactionById($transaction->id); +}); diff --git a/tests/Feature/Order/OrderRecalculationTest.php b/tests/Feature/Order/OrderRecalculationTest.php new file mode 100644 index 0000000000..1d98b557d8 --- /dev/null +++ b/tests/Feature/Order/OrderRecalculationTest.php @@ -0,0 +1,227 @@ +shouldReceive('getId')->andReturn(null); + $method->shouldReceive('getType')->andReturn('dynamic'); + $method->shouldReceive('getHandle')->andReturn('dynamicFlatRate'); + $method->shouldReceive('getName')->andReturn('Dynamic Flat Rate'); + $method->shouldReceive('getIsEnabled')->andReturn(true); + $method->shouldReceive('getPriceForOrder')->andReturn(8.99); + $method->shouldReceive('getMatchingShippingRule')->andReturn(null); + $method->shouldReceive('matchOrder')->andReturnUsing($matches); + + $event->getShippingMethods()->push($method); + }); +} + +/** + * Stubs a resolvable gateway for the given order, so `Transactions::createTransaction()` can + * build a transaction against it without a real payment gateway configured in the store. + */ +function stubGatewayForRecalculationOrder(Order $order): void +{ + $gateway = app(Gateways::class)->createGateway([ + 'type' => Dummy::class, + 'name' => 'Dummy', + 'handle' => 'dummy', + 'isFrontendEnabled' => true, + ]); + $gateway->id = 1; + $order->gatewayId = $gateway->id; + + $mock = Mockery::mock(Gateways::class)->makePartial(); + $mock->shouldReceive('getGatewayById')->andReturn($gateway); + app()->instance(Gateways::class, $mock); +} + +/** + * Builds an unsaved cart with a single line item against the fixture's white variant, saved + * once so it has an ID (`recalculate()` requires a saved order). + */ +function cartWithLineItem(OrdersFixture $fixture): Order +{ + $order = new Order(); + Elements::saveElement($order, false); + + $lineItem = app(LineItems::class)->create($order, [ + 'purchasableId' => $fixture->white->id, + 'qty' => 1, + ]); + $order->setLineItems([$lineItem]); + + return $order; +} + +test('a completed and paid order stays locked against recalculation, even after its shipping method stops matching', function() { + $fixture = OrdersFixture::seed(); + + $matches = true; + registerDynamicShippingMethod(function() use (&$matches) { + return $matches; + }); + + $order = cartWithLineItem($fixture); + $order->shippingMethodHandle = 'dynamicFlatRate'; + stubGatewayForRecalculationOrder($order); + + // Cart is untouched, so recalculation mode defaults to `ALL`, as throughout a normal checkout. + expect($order->getRecalculationMode())->toBe(Order::RECALCULATION_MODE_ALL); + + // Checkout: the method matches, its cost gets applied and persisted. + $order->recalculate(); + Elements::saveElement($order, false); + + $totalCollected = $order->getTotalPrice(); + expect($order->getTotalShippingCost())->toBeGreaterThan(0.0); + + // Customer pays the full amount shown at checkout, including shipping. + $transactions = app(Transactions::class); + $transaction = $transactions->createTransaction($order, typeOverride: TransactionRecord::TYPE_PURCHASE); + $transaction->status = TransactionRecord::STATUS_SUCCESS; + $transactions->saveTransaction($transaction); + + // The order is now completed and paid in full... + expect($order->isCompleted)->toBeTrue(); + expect($order->hasOutstandingBalance())->toBeFalse(); + expect($order->getTotalPaid())->toBe($totalCollected); + + // ...and stays locked at `NONE` rather than being restored to the `ALL` mode it had as a cart. + expect($order->getRecalculationMode())->toBe(Order::RECALCULATION_MODE_NONE); + + // Some time later — a queue job, webhook, or fulfillment plugin — something recalculates + // this completed order again, and this time the shipping method fails to match. Doesn't + // matter now, since recalculation is locked out. + $matches = false; + $order->recalculate(); + + // Nothing changed: still completed, still paid, shipping cost and handle untouched, no + // "shippingMethodChanged" notice. + expect($order->isCompleted)->toBeTrue(); + expect($order->getTotalPrice())->toBe($totalCollected); + expect($order->getTotalPaid())->toBe($totalCollected); + expect($order->shippingMethodHandle)->toBe('dynamicFlatRate'); + expect($order->hasNotices('shippingMethodChanged'))->toBeFalse(); +}); + +test('manually unlocking recalculation mode on a completed order lets it drop a shipping cost that no longer matches', function() { + $fixture = OrdersFixture::seed(); + + $matches = true; + registerDynamicShippingMethod(function() use (&$matches) { + return $matches; + }); + + $order = cartWithLineItem($fixture); + $order->shippingMethodHandle = 'dynamicFlatRate'; + stubGatewayForRecalculationOrder($order); + + $order->recalculate(); + Elements::saveElement($order, false); + + $totalCollected = $order->getTotalPrice(); + expect($order->getTotalShippingCost())->toBeGreaterThan(0.0); + + $transactions = app(Transactions::class); + $transaction = $transactions->createTransaction($order, typeOverride: TransactionRecord::TYPE_PURCHASE); + $transaction->status = TransactionRecord::STATUS_SUCCESS; + $transactions->saveTransaction($transaction); + + expect($order->isCompleted)->toBeTrue(); + expect($order->getRecalculationMode())->toBe(Order::RECALCULATION_MODE_NONE); + + // Manually unlock the completed, paid order back to `ALL`. + $order->setRecalculationMode(Order::RECALCULATION_MODE_ALL); + + // The shipping method stops matching, then something saves the order — `afterSave()` + // unconditionally calls `recalculate()`, which now actually runs, since mode is `ALL` again. + $matches = false; + Elements::saveElement($order, false); + + // The shipping cost disappeared, even though the order is still marked completed and paid. + expect($order->isCompleted)->toBeTrue(); + expect($order->getTotalShippingCost())->toBe(0.0); + expect($order->getTotalPrice())->toBeLessThan($totalCollected); + expect($order->getTotalPaid())->toBeGreaterThan($order->getTotalPrice()); +}); + +test('a cart that receives a payment update without completing stays fully recalculable', function() { + $fixture = OrdersFixture::seed(); + + $order = cartWithLineItem($fixture); + $order->recalculate(); + Elements::saveElement($order, false); + + expect($order->hasOutstandingBalance())->toBeTrue(); + expect($order->getRecalculationMode())->toBe(Order::RECALCULATION_MODE_ALL); + + // Nothing paid or authorized, so this can't complete the order, but it still exercises the + // same lock/restore logic that `updateOrderPaidInformation()` runs on every payment update. + $order->updateOrderPaidInformation(); + + expect($order->isCompleted)->toBeFalse(); + expect($order->getRecalculationMode())->toBe(Order::RECALCULATION_MODE_ALL); +}); + +test('saving an already-completed, already-paid order again has no adverse effect on its totals', function() { + $fixture = OrdersFixture::seed(); + + $matches = true; + registerDynamicShippingMethod(function() use (&$matches) { + return $matches; + }); + + $order = cartWithLineItem($fixture); + $order->shippingMethodHandle = 'dynamicFlatRate'; + stubGatewayForRecalculationOrder($order); + + $order->recalculate(); + Elements::saveElement($order, false); + + $totalCollected = $order->getTotalPrice(); + $shippingCost = $order->getTotalShippingCost(); + expect($shippingCost)->toBeGreaterThan(0.0); + + $transactions = app(Transactions::class); + $transaction = $transactions->createTransaction($order, typeOverride: TransactionRecord::TYPE_PURCHASE); + $transaction->status = TransactionRecord::STATUS_SUCCESS; + $transactions->saveTransaction($transaction); + + expect($order->isCompleted)->toBeTrue(); + + // The shipping method stops matching some time later — it doesn't matter, because + // recalculation is locked out. + $matches = false; + + // Custom code saves the already-completed, already-paid order again, for reasons unrelated + // to shipping/adjustments. + Elements::saveElement($order, false); + + expect($order->isCompleted)->toBeTrue(); + expect($order->getRecalculationMode())->toBe(Order::RECALCULATION_MODE_NONE); + expect($order->getTotalShippingCost())->toBe($shippingCost); + expect($order->getTotalPrice())->toBe($totalCollected); + expect($order->getTotalPaid())->toBe($totalCollected); + expect($order->hasOutstandingBalance())->toBeFalse(); +}); diff --git a/tests/Feature/Order/OrderTotalsTest.php b/tests/Feature/Order/OrderTotalsTest.php new file mode 100644 index 0000000000..ee602a992e --- /dev/null +++ b/tests/Feature/Order/OrderTotalsTest.php @@ -0,0 +1,68 @@ +order = new Order(); +}); + +test('getTotalPrice sums line item subtotals (net of promotional prices) plus non-included adjustments', function() { + $lineItem1 = new LineItem(); + $lineItem1->qty = 2; + $lineItem1->price = 10; + expect($lineItem1->getSubtotal())->toBe(20.0); + + $lineItem2 = new LineItem(); + $lineItem2->qty = 3; + $lineItem2->price = 20; + expect($lineItem2->getSubtotal())->toBe(60.0); + + $this->order->setLineItems([$lineItem1, $lineItem2]); + expect($this->order->getTotalPrice())->toBe(80.0); + + $lineItem2->promotionalPrice = 15; + $this->order->setLineItems([$lineItem1, $lineItem2]); + expect($this->order->getTotalPrice())->toBe(65.0); + + // Reset line item 2's promotional price + $lineItem2->promotionalPrice = null; + + $adjustment1 = new OrderAdjustment(); + $adjustment1->amount = -10; + $adjustment1->type = Discount::ADJUSTMENT_TYPE; + $adjustment1->setLineItem($lineItem1); + $adjustment1->name = 'Discount'; + $adjustment1->description = '10 bucks off'; + $adjustment1->setOrder($this->order); + $this->order->setAdjustments([$adjustment1]); + + expect($this->order->getTotalPrice())->toBe(70.0); + + $adjustment2 = new OrderAdjustment(); + $adjustment2->amount = -5; + $adjustment2->type = Discount::ADJUSTMENT_TYPE; + $adjustment2->setLineItem($lineItem2); + $adjustment2->name = 'Discount'; + $adjustment2->description = '5 bucks off'; + $adjustment2->setOrder($this->order); + + $this->order->setAdjustments([$adjustment1, $adjustment2]); + expect($this->order->getTotalPrice())->toBe(65.0); + + // An included adjustment (e.g. tax already baked into the price) doesn't change the total. + $adjustment3 = new OrderAdjustment(); + $adjustment3->amount = 5; + $adjustment3->setLineItem($lineItem2); + $adjustment3->name = 'Tax'; + $adjustment3->description = '5 buck tax'; + $adjustment3->included = true; + $adjustment3->setOrder($this->order); + + $this->order->setAdjustments([$adjustment1, $adjustment2, $adjustment3]); + expect($this->order->getTotalPrice())->toBe(65.0); +}); diff --git a/tests/Feature/Order/OrderValidationTest.php b/tests/Feature/Order/OrderValidationTest.php new file mode 100644 index 0000000000..1a57132e72 --- /dev/null +++ b/tests/Feature/Order/OrderValidationTest.php @@ -0,0 +1,37 @@ +setBillingAddress([]); + $order->setShippingAddress(['addressLine1' => '1 Main Street']); + + expect($order->validate())->toBeFalse(); + expect($order->getErrors())->not->toBeEmpty(); + expect($order->getErrors())->toHaveKey('billingAddress.administrativeArea'); + expect($order->getErrors())->toHaveKey('billingAddress.locality'); + expect($order->getErrors())->toHaveKey('billingAddress.postalCode'); + expect($order->getErrors())->toHaveKey('billingAddress.addressLine1'); + expect($order->getErrors())->toHaveKey('shippingAddress.administrativeArea'); + expect($order->getErrors())->toHaveKey('shippingAddress.locality'); + expect($order->getErrors())->toHaveKey('shippingAddress.postalCode'); + + $order->setBillingAddress([ + 'addressLine1' => 'Downtown', + 'locality' => 'LA', + 'administrativeArea' => 'CA', + 'postalCode' => '90210', + ]); + $order->setShippingAddress([ + 'addressLine1' => '1 Main Street', + 'locality' => 'LA', + 'administrativeArea' => 'CA', + 'postalCode' => '90210', + ]); + + expect($order->validate())->toBeTrue(); + expect($order->getErrors())->toBeEmpty(); +}); diff --git a/tests/Feature/Order/OrdersTest.php b/tests/Feature/Order/OrdersTest.php new file mode 100644 index 0000000000..313c24cf53 --- /dev/null +++ b/tests/Feature/Order/OrdersTest.php @@ -0,0 +1,68 @@ +orders['completed-new']; + + $order = app(Orders::class)->getOrderById($expected->id); + + expect($order)->toBeInstanceOf(Order::class); + expect($order->id)->toBe($expected->id); +}); + +test('getOrderByNumber returns the matching order, and null for an unknown number', function() { + $fixture = OrdersFixture::seed(); + $expected = $fixture->orders['completed-new']; + + $order = app(Orders::class)->getOrderByNumber($expected->number); + + expect($order)->toBeInstanceOf(Order::class); + expect($order->number)->toBe($expected->number); + expect($order->id)->toBe($expected->id); + + expect(app(Orders::class)->getOrderByNumber('invalid'))->toBeNull(); +}); + +test('getOrdersByCustomer returns all completed orders for a customer id', function() { + $fixture = OrdersFixture::seed(); + + $orders = app(Orders::class)->getOrdersByCustomer($fixture->customer->id); + + expect($orders)->toBeArray(); + expect($orders)->toHaveCount(3); + foreach ($orders as $order) { + expect($order->id)->toBeIn([ + $fixture->orders['completed-new']->id, + $fixture->orders['completed-new-past']->id, + $fixture->orders['completed-shipped']->id, + ]); + } +}); + +test('getOrdersByCustomer returns all completed orders for a customer element', function() { + $fixture = OrdersFixture::seed(); + + $orders = app(Orders::class)->getOrdersByCustomer($fixture->customer); + + expect($orders)->toBeArray(); + expect($orders)->toHaveCount(3); +}); + +test('getOrdersByEmail returns all completed orders for an email address', function() { + $fixture = OrdersFixture::seed(); + $email = $fixture->orders['completed-new']->getEmail(); + + $orders = app(Orders::class)->getOrdersByEmail($email); + + expect($orders)->toBeArray(); + expect($orders)->toHaveCount(3); + foreach ($orders as $order) { + expect($order->getEmail())->toBe($email); + } +}); diff --git a/tests/Feature/Order/UserEmailTest.php b/tests/Feature/Order/UserEmailTest.php new file mode 100644 index 0000000000..df74fa1195 --- /dev/null +++ b/tests/Feature/Order/UserEmailTest.php @@ -0,0 +1,43 @@ +orders['completed-new']; + + $cart = new Order(); + $cart->number = bin2hex(random_bytes(16)); + $cart->setCustomer($fixture->customer); + $lineItem = app(LineItems::class)->create($cart, [ + 'purchasableId' => $fixture->white->id, + 'qty' => 4, + 'note' => 'My note', + ]); + $cart->setLineItems([$lineItem]); + if (!Elements::saveElement($cart, false)) { + throw new RuntimeException('Could not save cart: ' . json_encode($cart->errors()->all())); + } + + $newEmail = 'changed@emailaddress.xyz'; + $fixture->customer->email = $newEmail; + if (!Elements::saveElement($fixture->customer, false)) { + throw new RuntimeException('Could not save customer: ' . json_encode($fixture->customer->errors()->all())); + } + + $emails = DB::table(Table::ORDERS) + ->whereIn('id', [$completedOrder->id, $cart->id]) + ->pluck('email'); + + expect($emails)->toHaveCount(2); + foreach ($emails as $email) { + expect($email)->toBe($newEmail); + } +}); diff --git a/tests/Feature/Payment/Gateway/GatewaysTest.php b/tests/Feature/Payment/Gateway/GatewaysTest.php new file mode 100644 index 0000000000..cadce6b77f --- /dev/null +++ b/tests/Feature/Payment/Gateway/GatewaysTest.php @@ -0,0 +1,53 @@ +, 1: array}> $gatewaySpecs */ +function mockGateways(array $gatewaySpecs): void +{ + $gateways = []; + foreach ($gatewaySpecs as $name => [$class, $attributes]) { + $attributes['name'] = $name; + + if (isset($attributes['isFrontendEnabled']) && is_array($attributes['isFrontendEnabled'])) { + putenv(substr((string)$attributes['isFrontendEnabled']['var'], 1) . '=' . $attributes['isFrontendEnabled']['value']); + $attributes['isFrontendEnabled'] = $attributes['isFrontendEnabled']['var']; + } + + $gateways[] = app(Gateways::class)->createGateway(['type' => $class, ...$attributes]); + } + + $mock = Mockery::mock(Gateways::class)->makePartial(); + $mock->shouldReceive('getAllGateways')->andReturn(collect($gateways)); + app()->instance(Gateways::class, $mock); +} + +test('getAllCustomerEnabledGateways filters to only frontend-enabled gateways, resolving env-var references', function() { + mockGateways([ + 'dummy' => [Dummy::class, ['isFrontendEnabled' => true]], + 'dummy-enabled-string' => [Dummy::class, ['isFrontendEnabled' => '1']], + 'dummy-disabled-string' => [Dummy::class, ['isFrontendEnabled' => '0']], + 'dummy-enabled-env' => [Dummy::class, ['isFrontendEnabled' => ['var' => '$DUMMY_ENABLED', 'value' => 'true']]], + 'dummy-disabled-env' => [Dummy::class, ['isFrontendEnabled' => ['var' => '$DUMMY_DISABLED', 'value' => 'false']]], + 'manual' => [Manual::class, ['isFrontendEnabled' => false]], + ]); + + $enabledGateways = app(Gateways::class)->getAllCustomerEnabledGateways(); + + expect($enabledGateways)->toHaveCount(3); + expect($enabledGateways->pluck('name')->values()->all())->toBe([ + 'dummy', 'dummy-enabled-string', 'dummy-enabled-env', + ]); +}); + +test('getAllGatewayTypes returns the built-in Dummy and Manual gateway types', function() { + expect(app(Gateways::class)->getAllGatewayTypes())->toEqualCanonicalizing([ + Dummy::class, + Manual::class, + ]); +}); diff --git a/tests/Feature/Payment/PaymentCurrenciesTest.php b/tests/Feature/Payment/PaymentCurrenciesTest.php new file mode 100644 index 0000000000..60952e1227 --- /dev/null +++ b/tests/Feature/Payment/PaymentCurrenciesTest.php @@ -0,0 +1,137 @@ +getPaymentCurrencyByIso('EUR'); + $aud = $pc->getPaymentCurrencyByIso('AUD'); + + // Install's USD, plus 2 additional currencies in fixture data. + expect($pc->getAllPaymentCurrencies())->toHaveCount(3); + + expect($eur)->not->toBeNull(); + expect($eur->iso)->toBe('EUR'); + expect($aud)->not->toBeNull(); + expect($aud->iso)->toBe('AUD'); + + // Default install has a USD primary currency. + expect($pc->getPrimaryPaymentCurrencyIso())->toBe('USD'); +}); + +test('convert converts an amount from the primary currency to another by ISO code', function() { + PaymentCurrenciesFixture::seed(); + $pc = app(PaymentCurrencies::class); + + expect($pc->convert(10, $pc->getPrimaryPaymentCurrencyIso()))->toBe(10.0); + expect($pc->convert(10, 'EUR'))->toBe(5.0); + expect($pc->convert(10, 'AUD'))->toBe(13.0); +}); + +test('convertCurrency converts between two currencies, normalizing through the primary', function() { + PaymentCurrenciesFixture::seed(); + $pc = app(PaymentCurrencies::class); + $primary = $pc->getPrimaryPaymentCurrencyIso(); + + expect($pc->convertCurrency(20, 'EUR', $primary))->toBe(40.0); + expect($pc->convertCurrency(40, $primary, 'EUR'))->toBe(20.0); + + expect($pc->convertCurrency(13, 'AUD', $primary))->toBe(10.0); + expect($pc->convertCurrency(10, $primary, 'AUD'))->toBe(13.0); + + expect($pc->convertCurrency(13, 'AUD', 'EUR'))->toBe(5.0); + expect($pc->convertCurrency(5, 'EUR', 'AUD'))->toBe(13.0); +}); + +test('convertCurrency throws for an unrecognized currency', function() { + PaymentCurrenciesFixture::seed(); + + expect(fn() => app(PaymentCurrencies::class)->convertCurrency(20, 'aaa', 'bbb'))->toThrow(RuntimeException::class); +}); + +test('convert throws for an unrecognized currency', function() { + PaymentCurrenciesFixture::seed(); + + expect(fn() => app(PaymentCurrencies::class)->convert(20, 'aaa'))->toThrow(RuntimeException::class); +}); + +test('getRateFor returns the raw rate when no event handler overrides it', function() { + $fixture = PaymentCurrenciesFixture::seed(); + $pc = app(PaymentCurrencies::class); + + expect($pc->getRateFor($fixture->eur))->toBe(0.5); +}); + +test('getRateFor returns the event-overridden rate, leaving other currencies alone', function() { + $fixture = PaymentCurrenciesFixture::seed(); + $pc = app(PaymentCurrencies::class); + + Event::listen(PaymentCurrencyRateEvent::class, function(PaymentCurrencyRateEvent $event) { + if ($event->paymentCurrency->iso === 'EUR') { + $event->rate = 0.25; + } + }); + + expect($pc->getRateFor($fixture->eur))->toBe(0.25); + expect($pc->getRateFor($fixture->aud))->toBe(1.3); +}); + +test('convertCurrency uses the event-overridden rate', function() { + PaymentCurrenciesFixture::seed(); + $pc = app(PaymentCurrencies::class); + + Event::listen(PaymentCurrencyRateEvent::class, function(PaymentCurrencyRateEvent $event) { + if ($event->paymentCurrency->iso === 'EUR') { + $event->rate = 0.25; + } + }); + + $converted = $pc->convertCurrency(40, $pc->getPrimaryPaymentCurrencyIso(), 'EUR'); + + expect($converted)->toBe(10.0); +}); + +test('convertAmount uses the event-overridden rate', function() { + PaymentCurrenciesFixture::seed(); + $pc = app(PaymentCurrencies::class); + + Event::listen(PaymentCurrencyRateEvent::class, function(PaymentCurrencyRateEvent $event) { + if ($event->paymentCurrency->iso === 'EUR') { + $event->rate = 0.25; + } + }); + + $usd = new Money(4000, new Currency('USD')); + $converted = $pc->convertAmount($usd, 'EUR'); + + expect($converted->getCurrency()->getCode())->toBe('EUR'); + expect($converted->getAmount())->toBe('1000'); +}); + +test('savePaymentCurrency persists the raw admin-entered rate, not an event-overridden rate', function() { + $fixture = PaymentCurrenciesFixture::seed(); + $pc = app(PaymentCurrencies::class); + + Event::listen(PaymentCurrencyRateEvent::class, function(PaymentCurrencyRateEvent $event) { + $event->rate = 999.0; + }); + + $eur = $fixture->eur; + $eur->rate = 0.75; + + expect($pc->savePaymentCurrency($eur))->toBeTrue(); + + $record = PaymentCurrencyRecord::find($eur->id); + expect($record)->not->toBeNull(); + expect((float) $record->rate)->toBe(0.75); +}); diff --git a/tests/Feature/PluginBootTest.php b/tests/Feature/PluginBootTest.php new file mode 100644 index 0000000000..0fe1d43e31 --- /dev/null +++ b/tests/Feature/PluginBootTest.php @@ -0,0 +1,26 @@ +types(); + + expect($types->contains(Product::class))->toBeTrue(); +}); + +test('Site::getStore() macro resolves via method call', function() { + $site = Sites::getCurrentSite(); + + expect($site->getStore())->toBeInstanceOf(Store::class); +}); + +test('Site::getStore() macro resolves via magic property access', function() { + $site = Sites::getCurrentSite(); + + expect($site->store)->toBeInstanceOf(Store::class); +}); diff --git a/tests/Feature/Product/Conditions/ProductConditionTest.php b/tests/Feature/Product/Conditions/ProductConditionTest.php new file mode 100644 index 0000000000..1756020ede --- /dev/null +++ b/tests/Feature/Product/Conditions/ProductConditionTest.php @@ -0,0 +1,27 @@ +toBeInstanceOf(ProductCondition::class); +}); + +test('the condition exposes all built-in product condition rule types', function() { + $rules = array_keys(Product::createCondition()->getSelectableConditionRules()); + + expect($rules)->toContain( + ProductTypeConditionRule::class, + ProductVariantSkuConditionRule::class, + ProductVariantStockConditionRule::class, + ProductVariantInventoryTrackedConditionRule::class, + ProductVariantPriceConditionRule::class, + ); +}); diff --git a/tests/Feature/Product/Conditions/ProductTypeConditionRuleTest.php b/tests/Feature/Product/Conditions/ProductTypeConditionRuleTest.php new file mode 100644 index 0000000000..57244d59a4 --- /dev/null +++ b/tests/Feature/Product/Conditions/ProductTypeConditionRuleTest.php @@ -0,0 +1,63 @@ +setValues($uids); + $rule->operator = $operator; + $condition->addConditionRule($rule); + + return $condition; +} + +beforeEach(function() { + $this->fixture = ProductConditionsFixture::seed(); +}); + +test('matchElement matches a product of the selected type', function() { + $condition = productTypeCondition([$this->fixture->hoodiesType->uid]); + + expect($condition->matchElement($this->fixture->hoodie))->toBeTrue(); +}); + +test('matchElement does not match a product of a different type', function() { + $condition = productTypeCondition([$this->fixture->tShirtsType->uid]); + + expect($condition->matchElement($this->fixture->hoodie))->toBeFalse(); +}); + +test('matchElement (not in) matches a product whose type is excluded', function() { + $condition = productTypeCondition([$this->fixture->tShirtsType->uid], 'ni'); + + expect($condition->matchElement($this->fixture->hoodie))->toBeTrue(); +}); + +test('modifyQuery filters products by type', function() { + $condition = productTypeCondition([$this->fixture->hoodiesType->uid]); + + $query = Product::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + expect($ids)->toContain($this->fixture->hoodie->id); + expect($ids)->not->toContain($this->fixture->tShirt->id); +}); + +test('modifyQuery (not in) excludes products of the given type', function() { + $condition = productTypeCondition([$this->fixture->hoodiesType->uid], 'ni'); + + $query = Product::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + expect($ids)->not->toContain($this->fixture->hoodie->id); + expect($ids)->toContain($this->fixture->tShirt->id); +}); diff --git a/tests/Feature/Product/Conditions/ProductVariantPriceConditionRuleTest.php b/tests/Feature/Product/Conditions/ProductVariantPriceConditionRuleTest.php new file mode 100644 index 0000000000..ce97fe35e3 --- /dev/null +++ b/tests/Feature/Product/Conditions/ProductVariantPriceConditionRuleTest.php @@ -0,0 +1,55 @@ +value = (string)$value; + if ($operator) { + $rule->operator = $operator; + } + $condition->addConditionRule($rule); + + return $condition; +} + +beforeEach(function() { + $this->fixture = ProductConditionsFixture::seed(); +}); + +test('matchElement compares the variant price', function(float|int $price, ?string $operator, bool $expected) { + $condition = priceCondition($price, $operator); + + expect($condition->matchElement($this->fixture->hoodie))->toBe($expected); +})->with([ + 'greater than, matches' => [100, '>', true], + 'greater than, does not match' => [1000, '>', false], + 'less than, matches' => [1000, '<', true], + 'equals default operator' => [123.99, null, true], +]); + +test('modifyQuery filters products by variant price', function(float|int $price, ?string $operator, bool $expectedHoodieMatch) { + $condition = priceCondition($price, $operator); + + $query = Product::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + if ($expectedHoodieMatch) { + expect($ids)->toContain($this->fixture->hoodie->id); + } else { + expect($ids)->not->toContain($this->fixture->hoodie->id); + } +})->with([ + 'greater than, matches' => [100, '>', true], + 'greater than, does not match' => [1000, '>', false], + 'less than, matches' => [1000, '<', true], + 'equals default operator' => [123.99, null, true], +]); diff --git a/tests/Feature/Product/Conditions/ProductVariantSkuConditionRuleTest.php b/tests/Feature/Product/Conditions/ProductVariantSkuConditionRuleTest.php new file mode 100644 index 0000000000..9075283a3e --- /dev/null +++ b/tests/Feature/Product/Conditions/ProductVariantSkuConditionRuleTest.php @@ -0,0 +1,46 @@ +fixture = ProductConditionsFixture::seed(); +}); + +function skuCondition(string $value, string $operator = '='): ProductCondition +{ + $condition = Product::createCondition(); + $rule = new ProductVariantSkuConditionRule(); + $rule->value = $value; + $rule->operator = $operator; + $condition->addConditionRule($rule); + + return $condition; +} + +test('matchElement matches a product with a variant of the given sku', function() { + $condition = skuCondition('rad-hood'); + + expect($condition->matchElement($this->fixture->hoodie))->toBeTrue(); +}); + +test('matchElement does not match a product without a variant of the given sku', function() { + $condition = skuCondition('does-not-exist'); + + expect($condition->matchElement($this->fixture->hoodie))->toBeFalse(); +}); + +test('modifyQuery filters products by variant sku prefix', function() { + $condition = skuCondition('rad', 'bw'); + + $query = Product::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + expect($ids)->toContain($this->fixture->hoodie->id); + expect($ids)->not->toContain($this->fixture->tShirt->id); +}); diff --git a/tests/Feature/Product/Conditions/ProductVariantStockConditionRuleTest.php b/tests/Feature/Product/Conditions/ProductVariantStockConditionRuleTest.php new file mode 100644 index 0000000000..293af8ccd2 --- /dev/null +++ b/tests/Feature/Product/Conditions/ProductVariantStockConditionRuleTest.php @@ -0,0 +1,69 @@ +value = (string)$value; + $rule->operator = $operator; + $condition->addConditionRule($rule); + + return $condition; +} + +/** + * A variant's stock is derived from its real inventory levels ({@see \CraftCms\Commerce\Purchasable\Elements\Purchasable::getStock()}), + * not a column that can be written to directly. + */ +function setVariantStock(Variant $variant, int $stock): void +{ + $variant->inventoryTracked = true; + Elements::saveElement($variant); + + app(Inventory::class)->updatePurchasableInventoryLevel($variant, $stock); +} + +beforeEach(function() { + $this->fixture = ProductConditionsFixture::seed(); +}); + +test('matchElement matches a product with a tracked variant whose stock is below value', function() { + setVariantStock($this->fixture->hoodieVariant, 9); + $product = Product::find()->id($this->fixture->hoodie->id)->one(); + + $condition = productVariantStockCondition(10); + + expect($condition->matchElement($product))->toBeTrue(); +}); + +test('matchElement does not match a product whose variant stock is not below value', function() { + setVariantStock($this->fixture->hoodieVariant, 50); + $product = Product::find()->id($this->fixture->hoodie->id)->one(); + + $condition = productVariantStockCondition(10); + + expect($condition->matchElement($product))->toBeFalse(); +}); + +test('modifyQuery filters products by variant stock', function() { + setVariantStock($this->fixture->hoodieVariant, 9); + setVariantStock($this->fixture->tShirtVariant, 50); + + $condition = productVariantStockCondition(10); + $query = Product::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + expect($ids)->toContain($this->fixture->hoodie->id); + expect($ids)->not->toContain($this->fixture->tShirt->id); +}); diff --git a/tests/Feature/Product/ProductGetVariantsTest.php b/tests/Feature/Product/ProductGetVariantsTest.php new file mode 100644 index 0000000000..1108bbc5ea --- /dev/null +++ b/tests/Feature/Product/ProductGetVariantsTest.php @@ -0,0 +1,117 @@ +getProperty('_variants'); + + return $property->getValue($product); +} + +test('getVariants returns an empty collection for a product with no ID', function() { + $product = new Product(); + + $variants = $product->getVariants(); + + expect($variants)->toBeInstanceOf(VariantCollection::class); + expect($variants->isEmpty())->toBeTrue(); +}); + +test('getVariants does not memoize an empty result, so it re-queries on the next call', function() { + $product = new Product(); + $product->id = 999999; + + expect($product->getVariants()->isEmpty())->toBeTrue(); + expect(productVariantsProperty($product))->toBeNull(); + + expect($product->getVariants()->isEmpty())->toBeTrue(); + expect(productVariantsProperty($product))->toBeNull(); +}); + +test('getVariants memoizes a non-empty result', function() { + $fixture = ProductConditionsFixture::seed(); + + $variants1 = $fixture->hoodie->getVariants(); + expect($variants1->isEmpty())->toBeFalse(); + expect(productVariantsProperty($fixture->hoodie))->toBeInstanceOf(VariantCollection::class); + + $variants2 = $fixture->hoodie->getVariants(); + expect($variants2->count())->toBe($variants1->count()); + expect($variants2->first()->id)->toBe($variants1->first()->id); +}); + +test('getVariants on a product being duplicated fetches the source product\'s variants, not its own', function() { + $fixture = ProductConditionsFixture::seed(); + + $duplicate = new Product(); + $duplicate->id = 999998; + $duplicate->typeId = $fixture->hoodie->typeId; + $duplicate->siteId = 999999; + $duplicate->duplicateOf = $fixture->hoodie; + + $variants = $duplicate->getVariants(); + + expect($variants->pluck('id')->all())->toBe($fixture->hoodie->getVariants()->pluck('id')->all()); +}); + +test('getVariants can include or exclude disabled variants', function() { + $fixture = ProductConditionsFixture::seed(); + + $disabled = new Variant(); + $disabled->title = 'Disabled Variant'; + $disabled->sku = 'disabled-variant-sku'; + $disabled->enabled = false; + $disabled->setOwner($fixture->hoodie); + + $variants = $fixture->hoodie->getVariants()->all(); + $variants[] = $disabled; + $fixture->hoodie->setVariants($variants); + + expect($fixture->hoodie->getVariants(false)->every(fn(Variant $v) => $v->enabled))->toBeTrue(); + expect($fixture->hoodie->getVariants(true)->contains(fn(Variant $v) => !$v->enabled))->toBeTrue(); +}); + +test('a null includeDisabled resolves based on whether NestedElementsController is the active controller', function(?bool $includeDisabled, ?string $controllerClass, int $expectedCount) { + request()->setRouteResolver(function() use ($controllerClass) { + if ($controllerClass === null) { + return null; + } + + $route = Mockery::mock(Route::class); + $route->shouldReceive('getControllerClass')->andReturn($controllerClass); + + return $route; + }); + + $product = new Product(); + + $enabled = new Variant(); + $enabled->enabled = true; + $enabled->sku = 'enabled-sku'; + + $disabled = new Variant(); + $disabled->enabled = false; + $disabled->sku = 'disabled-sku'; + + $product->setVariants([$enabled, $disabled]); + + expect($product->getVariants($includeDisabled))->toHaveCount($expectedCount); + + // The includeDisabled filter must never mutate the internal collection — both variants remain + // regardless of which ones are returned. + expect(productVariantsProperty($product))->toHaveCount(2); +})->with([ + 'null, no active controller, excludes disabled' => [null, null, 1], + 'null, NestedElementsController active, includes disabled' => [null, NestedElementsController::class, 2], + 'explicit false overrides an active NestedElementsController' => [false, NestedElementsController::class, 1], + 'explicit true works without an active controller' => [true, null, 2], +]); diff --git a/tests/Feature/Product/ProductPermissionTest.php b/tests/Feature/Product/ProductPermissionTest.php new file mode 100644 index 0000000000..daea276328 --- /dev/null +++ b/tests/Feature/Product/ProductPermissionTest.php @@ -0,0 +1,211 @@ +makePartial(); + $mock->shouldReceive('doesUserHavePermission') + ->andReturnUsing(fn(int $userId, string $checkPermission): bool => in_array($checkPermission, $permissions, true)); + app()->instance(UserPermissions::class, $mock); +} + +/** + * Stubs `ProductTypes::getProductTypeById()` so `Product::getType()` resolves a known, + * unpersisted product type with a fixed `uid` for the permission strings below to key off of. + */ +function stubProductType(int $id, string $uid): ProductType +{ + $productType = new ProductType(); + $productType->id = $id; + $productType->uid = $uid; + + $mock = Mockery::mock(ProductTypes::class)->makePartial(); + $mock->shouldReceive('getProductTypeById')->andReturn($productType); + app()->instance(ProductTypes::class, $mock); + + return $productType; +} + +/** @return array{User, Product} */ +function existingProduct(): array +{ + $productType = stubProductType(1, 'randomuid'); + + $user = new User(); + $user->id = 1; + $user->admin = false; + + $product = new Product(); + $product->id = 100; + $product->typeId = $productType->id; + + return [$user, $product]; +} + +/** @return array{User, Product} */ +function newProduct(): array +{ + $productType = stubProductType(1, 'randomuid'); + + $user = new User(); + $user->id = 1; + $user->admin = false; + + $product = new Product(); + $product->typeId = $productType->id; + + return [$user, $product]; +} + +test('canView returns false with no permissions', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions([]); + + expect($product->canView($user))->toBeFalse(); +}); + +test('canView returns true with view permission on the product\'s product type', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions(['commerce-viewProductType:randomuid']); + + expect($product->canView($user))->toBeTrue(); +}); + +test('canView returns false when the view permission is for a different product type', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions(['commerce-viewProductType:anotherrandomuid']); + + expect($product->canView($user))->toBeFalse(); +}); + +test('canView returns false with only a save permission', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions(['commerce-saveProductType:randomuid']); + + expect($product->canView($user))->toBeFalse(); +}); + +test('canSave returns true for an existing product with the save permission', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions(['commerce-viewProductType:randomuid', 'commerce-saveProductType:randomuid']); + + expect($product->canSave($user))->toBeTrue(); +}); + +test('canSave returns false for an existing product with only the view permission', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions(['commerce-viewProductType:randomuid']); + + expect($product->canSave($user))->toBeFalse(); +}); + +test('canSave returns false for an existing product with only the create permission', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions(['commerce-viewProductType:randomuid', 'commerce-createProductType:randomuid']); + + expect($product->canSave($user))->toBeFalse(); +}); + +test('canSave returns true for a new product with the create permission', function() { + [$user, $product] = newProduct(); + grantProductPermissionTestPermissions(['commerce-viewProductType:randomuid', 'commerce-createProductType:randomuid']); + + expect($product->canSave($user))->toBeTrue(); +}); + +test('canSave returns false for a new product with only the view permission', function() { + [$user, $product] = newProduct(); + grantProductPermissionTestPermissions(['commerce-viewProductType:randomuid']); + + expect($product->canSave($user))->toBeFalse(); +}); + +test('canSave returns false for a new product with only the save permission', function() { + [$user, $product] = newProduct(); + grantProductPermissionTestPermissions(['commerce-viewProductType:randomuid', 'commerce-saveProductType:randomuid']); + + expect($product->canSave($user))->toBeFalse(); +}); + +test('canDelete returns true with the delete permission', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions(['commerce-viewProductType:randomuid', 'commerce-deleteProductType:randomuid']); + + expect($product->canDelete($user))->toBeTrue(); +}); + +test('canDelete returns false with only the view permission', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions(['commerce-viewProductType:randomuid']); + + expect($product->canDelete($user))->toBeFalse(); +}); + +test('canDelete returns false with only the save permission', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions(['commerce-viewProductType:randomuid', 'commerce-saveProductType:randomuid']); + + expect($product->canDelete($user))->toBeFalse(); +}); + +test('canDuplicate returns true with both the create and save permissions', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions([ + 'commerce-viewProductType:randomuid', + 'commerce-createProductType:randomuid', + 'commerce-saveProductType:randomuid', + ]); + + expect($product->canDuplicate($user))->toBeTrue(); +}); + +test('canDuplicate returns false with only the create permission', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions(['commerce-viewProductType:randomuid', 'commerce-createProductType:randomuid']); + + expect($product->canDuplicate($user))->toBeFalse(); +}); + +test('canDuplicate returns false with only the save permission', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions(['commerce-viewProductType:randomuid', 'commerce-saveProductType:randomuid']); + + expect($product->canDuplicate($user))->toBeFalse(); +}); + +test('canCreateDrafts always returns true', function() { + [$user, $product] = existingProduct(); + grantProductPermissionTestPermissions([]); + + expect($product->canCreateDrafts($user))->toBeTrue(); +}); + +test('an admin user bypasses all product type permissions', function() { + [, $product] = existingProduct(); + grantProductPermissionTestPermissions([]); + + $admin = new User(); + $admin->id = 1; + $admin->admin = true; + + expect($product->canView($admin))->toBeTrue(); + expect($product->canSave($admin))->toBeTrue(); + expect($product->canDelete($admin))->toBeTrue(); + expect($product->canDuplicate($admin))->toBeTrue(); +}); diff --git a/tests/Feature/Product/ProductPricingCatalogTest.php b/tests/Feature/Product/ProductPricingCatalogTest.php new file mode 100644 index 0000000000..c6acbcc214 --- /dev/null +++ b/tests/Feature/Product/ProductPricingCatalogTest.php @@ -0,0 +1,145 @@ +storeId = app(Stores::class)->getPrimaryStore()->id; + $catalogPricingRule->name = $rule['name']; + $catalogPricingRule->applyAmount = $rule['applyAmount']; + $catalogPricingRule->isPromotionalPrice = $rule['isPromotionalPrice'] ?? false; + $catalogPricingRule->enabled = true; + + /** @var CatalogPricingRuleVariantCondition $variantCondition */ + $variantCondition = $catalogPricingRule->getVariantCondition(); + $skuRule = new SkuConditionRule(); + $skuRule->value = $rule['sku']; + $variantCondition->addConditionRule($skuRule); + $catalogPricingRule->setVariantCondition($variantCondition); + + expect(app(CatalogPricingRules::class)->saveCatalogPricingRule($catalogPricingRule))->toBeTrue(); + + return $catalogPricingRule->id; +} + +/** + * @param array $rules + * @return int[] The saved rules' IDs. + */ +function applyCatalogPricingRulesForSkus(array $rules): array +{ + $ids = array_map(saveCatalogPricingRuleForSku(...), $rules); + + if (!empty($ids)) { + app(CatalogPricing::class)->generateCatalogPrices(); + } + + return $ids; +} + +/** @param int[] $ids */ +function removeCatalogPricingRules(array $ids): void +{ + if (empty($ids)) { + return; + } + + foreach ($ids as $id) { + app(CatalogPricingRules::class)->deleteCatalogPricingRuleById($id); + } + + app(CatalogPricing::class)->generateCatalogPrices(); +} + +beforeEach(function() { + $this->fixture = ProductConditionsFixture::seed(); +}); + +test('getDefaultPrice reflects any active catalog pricing rules for the default variant', function(array $rules, float $expectedPrice) { + $ruleIds = applyCatalogPricingRulesForSkus($rules); + + $product = Product::find()->defaultSku('rad-hood')->one(); + + expect($product)->toBeInstanceOf(Product::class); + expect($product->getDefaultPrice())->toEqual($expectedPrice); + + removeCatalogPricingRules($ruleIds); +})->with('catalogPricingRuleScenarios'); + +test('the defaultPrice and defaultSku query scopes stay in sync with the default variant, even with catalog pricing active', function(array $rules, float $expectedPrice) { + $ruleIds = applyCatalogPricingRulesForSkus($rules); + + $product = Product::find()->defaultPrice($expectedPrice)->one(); + $variant = $product->getDefaultVariant(); + + expect($product)->toBeInstanceOf(Product::class); + expect($variant)->toBeInstanceOf(Variant::class); + expect($product->defaultSku)->toBe('rad-hood'); + expect($variant->getSku())->toBe('rad-hood'); + expect($product->defaultPrice)->toEqual($expectedPrice); + expect($variant->getPrice())->toEqual($expectedPrice); + + removeCatalogPricingRules($ruleIds); +})->with('catalogPricingRuleScenarios'); + +test('orderBy(defaultPrice) sorts products by their catalog price, not just their base price', function(array $rules) { + $ruleIds = applyCatalogPricingRulesForSkus($rules); + + $ascending = Product::find()->orderBy(['defaultPrice' => SORT_ASC])->all(); + $price = null; + foreach ($ascending as $product) { + expect($product->getDefaultPrice())->toBeGreaterThanOrEqual($price ?? -INF); + $price = $product->getDefaultPrice(); + } + + $descending = Product::find()->orderBy(['defaultPrice' => SORT_DESC])->all(); + $price = null; + foreach ($descending as $product) { + expect($product->getDefaultPrice())->toBeLessThanOrEqual($price ?? INF); + $price = $product->getDefaultPrice(); + } + + removeCatalogPricingRules($ruleIds); +})->with('catalogPricingRuleScenarios'); + +dataset('catalogPricingRuleScenarios', [ + 'no catalog pricing rules' => [ + [], + 123.99, + ], + 'a single rule reduces the price' => [ + [ + ['name' => 'Test Rule', 'sku' => 'rad-hood', 'applyAmount' => -0.1], + ], + 111.59, + ], + 'a promotional rule does not affect the default price' => [ + [ + ['name' => 'Test Rule', 'sku' => 'rad-hood', 'applyAmount' => -0.1, 'isPromotionalPrice' => true], + ], + 123.99, + ], + 'a non-promotional rule wins over an additional promotional rule' => [ + [ + ['name' => 'Test Rule - 5%', 'sku' => 'rad-hood', 'applyAmount' => -0.05], + ['name' => 'Test Rule - 1%', 'sku' => 'rad-hood', 'applyAmount' => -0.01, 'isPromotionalPrice' => true], + ], + 117.79, + ], +]); diff --git a/tests/Feature/Product/ProductQueryTest.php b/tests/Feature/Product/ProductQueryTest.php new file mode 100644 index 0000000000..f2ddf1b756 --- /dev/null +++ b/tests/Feature/Product/ProductQueryTest.php @@ -0,0 +1,75 @@ +fixture = ProductConditionsFixture::seed(); +}); + +test('find returns a ProductQuery', function() { + expect(Product::find())->toBeInstanceOf(ProductQuery::class); +}); + +test('defaultPrice filters products by their default variant price', function(mixed $price, int $count) { + $query = Product::find()->defaultPrice($price); + + expect($query->all())->toHaveCount($count); +})->with([ + 'exact match' => [123.99, 1], + 'exact match, no results' => [999, 0], + 'greater than, matches both' => ['> 1', 2], + 'greater than, no results' => ['> 999', 0], + 'less than, matches both' => ['< 150', 2], + 'less than, no results' => ['< 1', 0], + 'range, matches both' => [['and', '> 5', '< 200'], 2], + 'range, no results' => [['and', '> 500', '< 2000'], 0], + 'in, matches both' => [[123.99, 19.99], 2], + 'in, no results' => [[1, 2], 0], +]); + +test('hasVariant filters products to those matching a variant query', function(Closure $variantQuery, int $count) { + $query = Product::find()->hasVariant($variantQuery()); + + expect($query->all())->toHaveCount($count); +})->with([ + 'no criteria matches every product with a variant' => [fn() => Variant::find(), 2], + 'sku criteria narrows to a single product' => [fn() => Variant::find()->sku('rad-hood'), 1], +]); + +test('with(variants) eager loads each product\'s variants', function() { + $results = Product::find()->with(['variants'])->all(); + + expect($results)->toHaveCount(2); + foreach ($results as $product) { + expect($product->getVariants()->first())->toBeInstanceOf(Variant::class); + } +}); + +test('with(variants) narrowed by a variant query only eager loads and returns matching products', function() { + $query = Product::find()->hasVariant(['sku' => 'rad-hood']); + $query->with([['variants', ['sku' => 'rad-hood']]]); + + $results = $query->all(); + + expect($results)->toHaveCount(1); + expect($results[0]->title)->toBe($this->fixture->hoodie->title); + expect($results[0]->getVariants()->first())->toBeInstanceOf(Variant::class); +}); + +test('orderBy sorts products', function(array $orderBy, array $expectedTitles) { + $results = Product::find()->orderBy($orderBy)->all(); + + $titles = collect($results)->map(fn(Product $p) => $p->title)->all(); + + expect($titles)->toBe($expectedTitles); +})->with([ + 'title ascending' => [['title' => SORT_ASC], ['Plain T-Shirt', 'Rad Hoodie']], + 'title descending' => [['title' => SORT_DESC], ['Rad Hoodie', 'Plain T-Shirt']], + 'default price ascending' => [['defaultPrice' => SORT_ASC], ['Plain T-Shirt', 'Rad Hoodie']], + 'default price descending' => [['defaultPrice' => SORT_DESC], ['Rad Hoodie', 'Plain T-Shirt']], +]); diff --git a/tests/Feature/Product/ProductTest.php b/tests/Feature/Product/ProductTest.php new file mode 100644 index 0000000000..7c27312350 --- /dev/null +++ b/tests/Feature/Product/ProductTest.php @@ -0,0 +1,342 @@ +name = $handle; + $productType->handle = $handle; + $productType->skuFormat = $skuFormat; + $productType->hasVariantTitleField = false; + $productType->variantTitleFormat = '{product.title}'; + + $siteSettings = new ProductTypeSite(); + $siteSettings->siteId = $site->id; + $siteSettings->hasUrls = false; + $siteSettings->enabledByDefault = true; + $productType->setSiteSettings([$site->id => $siteSettings]); + + if (!app(ProductTypes::class)->saveProductType($productType)) { + throw new RuntimeException('Could not save product type: ' . json_encode($productType->errors()->all())); + } + + return $productType; +} + +/** + * Deletes any existing sequence counter for a SKU base, so a deduplication suffix is predictable. + */ +function resetSkuSequence(string $baseSku): void +{ + DB::table(CraftTable::SEQUENCES)->where('name', 'sku::' . $baseSku)->delete(); +} + +test('a product with a fully populated variant validates without errors', function() { + $fixture = ProductConditionsFixture::seed(); + + $product = new Product(); + $product->enabled = false; + $product->title = 'test'; + $product->typeId = $fixture->hoodiesType->id; + + $variant = new Variant(); + $variant->title = 'variant 1'; + $product->setVariants([$variant]); + + $product->validate(); + + expect($product->errors()->all())->toBeEmpty(); +}); + +test('constructing a product from an array mass-assigns its properties and nested variants', function() { + $productType = createSkuFormatProductType('massAssignment', null); + + $product = new Product([ + 'title' => 'Test Product', + 'typeId' => $productType->id, + 'enabled' => true, + 'variants' => [ + [ + 'title' => 'Test Variant', + 'basePrice' => 123, + 'sku' => '123', + 'enabled' => true, + ], + ], + ]); + + expect($product->title)->toBe('Test Product'); + expect($product->typeId)->toBe($productType->id); + expect($product->enabled)->toBeTrue(); + + $variants = $product->getVariants(true); + expect($variants)->toHaveCount(1); + + $variant = $variants->first(); + expect($variant->title)->toBe('Test Variant'); + expect($variant->basePrice)->toEqual(123); + expect($variant->sku)->toBe('123'); + expect($variant->enabled)->toBeTrue(); +}); + +test('getVariants, getDefaultVariant and getCheapestVariant reflect each variant\'s default/enabled flags', function(array $variantData, array $expected) { + $product = new Product(); + $product->enabled = true; + $product->typeId = 2001; + $product->title = 'Test Product'; + + $variants = []; + $count = 1; + $defaultVariantId = null; + foreach ($variantData as [$id, $price, $default, $enabled]) { + $variant = new Variant(); + $variant->id = $id; + $variant->title = sprintf('Test Variant #%s', $count); + $variant->isDefault = $default; + $defaultVariantId = $default ? $id : $defaultVariantId; + $variant->enabled = $enabled; + $variant->price = $price; + + $variants[] = $variant; + $count++; + } + + $product->setVariants($variants); + if ($defaultVariantId) { + $product->defaultVariantId = $defaultVariantId; + } + + expect($product->getVariants(true))->toHaveCount($expected['variantCount']); + expect($product->getVariants())->toHaveCount($expected['enabledVariantCount']); + + $defaultVariant = $product->getDefaultVariant(true); + expect($defaultVariant->title)->toBe($expected['defaultVariantTitle']); + + $cheapestVariant = $product->getCheapestVariant(true); + expect($cheapestVariant->title)->toBe($expected['cheapestVariantTitle']); + + $defaultEnabledVariant = $product->getDefaultVariant(); + expect($defaultEnabledVariant->title ?? null)->toBe($expected['defaultEnabledVariantTitle']); + + $cheapestEnabledVariant = $product->getCheapestVariant(); + expect($cheapestEnabledVariant->title ?? null)->toBe($expected['cheapestEnabledVariantTitle']); +})->with([ + 'all enabled' => [ + [[1001, 123, true, true], [1002, 456, false, true], [1003, 789, false, true]], + [ + 'variantCount' => 3, + 'enabledVariantCount' => 3, + 'cheapestVariantTitle' => 'Test Variant #1', + 'defaultVariantTitle' => 'Test Variant #1', + 'cheapestEnabledVariantTitle' => 'Test Variant #1', + 'defaultEnabledVariantTitle' => 'Test Variant #1', + ], + ], + 'one disabled' => [ + [[1001, 123, false, false], [1002, 456, false, true], [1003, 789, true, true]], + [ + 'variantCount' => 3, + 'enabledVariantCount' => 2, + 'cheapestVariantTitle' => 'Test Variant #1', + 'defaultVariantTitle' => 'Test Variant #3', + 'cheapestEnabledVariantTitle' => 'Test Variant #2', + 'defaultEnabledVariantTitle' => 'Test Variant #3', + ], + ], + 'all disabled' => [ + [[1001, 123, false, false], [1002, 456, true, false], [1003, 99, false, false]], + [ + 'variantCount' => 3, + 'enabledVariantCount' => 0, + 'cheapestVariantTitle' => 'Test Variant #3', + 'defaultVariantTitle' => 'Test Variant #2', + 'cheapestEnabledVariantTitle' => null, + 'defaultEnabledVariantTitle' => null, + ], + ], +]); + +test('saving a variant updates its owning product\'s denormalized default-variant data, and keeps it in sync on later saves', function() { + $productType = createSkuFormatProductType('saveProductAndVariants', null); + + $product = new Product(); + $product->title = 'Test Product'; + $product->typeId = $productType->id; + $product->slug = 'test-product'; + $product->enabled = true; + $product->enabledForSite = true; + $product->postDate = new DateTime('now'); + + expect(Elements::saveElement($product, false))->toBeTrue(); + + $variant = new Variant(); + $variant->title = 'Test Variant'; + $variant->slug = 'test-variant'; + $variant->setPrimaryOwner($product); + $variant->setSku('test-variant-sku'); + $variant->setBasePrice(99.99); + $variant->sortOrder = 0; + $variant->inventoryTracked = false; + $variant->isDefault = true; + + $variant2 = new Variant(); + $variant2->title = 'Test Variant 2'; + $variant2->slug = 'test-variant-2'; + $variant2->setPrimaryOwner($product); + $variant2->setSku('test-variant-sku2'); + $variant2->setBasePrice(100.99); + $variant2->sortOrder = 1; + $variant2->inventoryTracked = false; + $variant2->isDefault = false; + + expect(Elements::saveElement($variant, false))->toBeTrue(); + expect(Elements::saveElement($variant2, false))->toBeTrue(); + + $productData = DB::table(Table::PRODUCTS) + ->select(['defaultVariantId', 'defaultSku', 'defaultPrice', 'defaultWidth', 'defaultHeight', 'defaultLength', 'defaultWeight']) + ->where('id', $product->id) + ->first(); + + // Check the product data in the database + expect($productData->defaultVariantId)->toEqual($variant->id); + expect($productData->defaultSku)->toBe('test-variant-sku'); + expect((float)$productData->defaultPrice)->toEqual(99.99); + expect((float)$productData->defaultWidth)->toEqual(0); + expect((float)$productData->defaultHeight)->toEqual(0); + expect((float)$productData->defaultLength)->toEqual(0); + expect((float)$productData->defaultWeight)->toEqual(0); + + // Check a freshly-queried product object reflects the same data + $reloadedProduct = Product::find()->id($product->id)->one(); + expect($reloadedProduct->getDefaultVariant()->id)->toEqual($variant->id); + expect($reloadedProduct->defaultSku)->toBe('test-variant-sku'); + expect($reloadedProduct->defaultPrice)->toEqual(99.99); + + // Make changes and independently save the default variant to check the product data is updated + $variant->setSku('test-variant-sku-updated'); + $variant->setBasePrice(199.99); + + expect(Elements::saveElement($variant, false))->toBeTrue(); + + $newProductData = DB::table(Table::PRODUCTS) + ->select(['defaultVariantId', 'defaultSku', 'defaultPrice', 'defaultWidth', 'defaultHeight', 'defaultLength', 'defaultWeight']) + ->where('id', $product->id) + ->first(); + + expect($newProductData->defaultVariantId)->toEqual($variant->id); + expect($newProductData->defaultSku)->toBe('test-variant-sku-updated'); + expect((float)$newProductData->defaultPrice)->toEqual(199.99); + expect((float)$newProductData->defaultWidth)->toEqual(0); + expect((float)$newProductData->defaultHeight)->toEqual(0); + expect((float)$newProductData->defaultLength)->toEqual(0); + expect((float)$newProductData->defaultWeight)->toEqual(0); + + Elements::deleteElementById($product->id, Product::class, null, true); +}); + +test('an empty SKU is generated from the product type\'s SKU format', function() { + $productType = createSkuFormatProductType('skuFormatGenerated', 'generated-sku-from-format'); + + $product = new Product(); + $product->title = 'SKU Format Test Product'; + $product->typeId = $productType->id; + $product->enabled = false; + + $variant = new Variant(); + $variant->title = 'Test Variant'; + // SKU intentionally not set — should be generated from skuFormat + + $product->setVariants([$variant]); + $product->validate(); + + expect($variant->sku)->toBe('generated-sku-from-format'); +}); + +test('a generated SKU is deduplicated when it collides with an existing SKU', function() { + // ProductConditionsFixture saves a variant with the 'rad-hood' SKU already. + ProductConditionsFixture::seed(); + resetSkuSequence('rad-hood'); + + $productType = createSkuFormatProductType('skuFormatCollision', 'rad-hood'); + + $product = new Product(); + $product->title = 'Collision Test Product'; + $product->typeId = $productType->id; + $product->enabled = false; + + $variant = new Variant(); + $variant->title = 'Test Variant'; + // No SKU — format generates 'rad-hood', which collides with the fixture's variant + + $product->setVariants([$variant]); + $product->validate(); + + expect($variant->sku)->toBe('rad-hood-1'); +}); + +test('generated SKUs are deduplicated independently for each colliding variant on the same product', function() { + // VariantQueryFixture saves a variant with the 'hct-white' SKU already. + VariantQueryFixture::seed(); + resetSkuSequence('hct-white'); + + $productType = createSkuFormatProductType('skuFormatMultiCollision', 'hct-white'); + + $product = new Product(); + $product->title = 'Multi-Variant Collision Test'; + $product->typeId = $productType->id; + $product->enabled = false; + + $variant1 = new Variant(); + $variant1->title = 'Variant One'; + + $variant2 = new Variant(); + $variant2->title = 'Variant Two'; + + $product->setVariants([$variant1, $variant2]); + $product->validate(); + + expect($variant1->sku)->toBe('hct-white-1'); + expect($variant2->sku)->toBe('hct-white-2'); +}); + +test('a SKU format referencing {id} is regenerated once the variant has been assigned one', function() { + $productType = createSkuFormatProductType('skuFormatWithId', 'SKU-{id}'); + + $product = new Product(); + $product->title = 'SKU Format Id Test Product'; + $product->typeId = $productType->id; + $product->enabled = false; + + expect(Elements::saveElement($product, false))->toBeTrue(); + + $variant = new Variant(); + $variant->title = 'Test Variant'; + $variant->setPrimaryOwner($product); + // No SKU — format references {id}, which isn't available until after the element is saved + + expect(Elements::saveElement($variant, false))->toBeTrue(); + + expect($variant->sku)->toBe('SKU-' . $variant->id); + + Elements::deleteElementById($product->id, Product::class, null, true); +}); diff --git a/tests/Feature/Product/Variant/Conditions/VariantConditionRuleTest.php b/tests/Feature/Product/Variant/Conditions/VariantConditionRuleTest.php new file mode 100644 index 0000000000..f48556d0f0 --- /dev/null +++ b/tests/Feature/Product/Variant/Conditions/VariantConditionRuleTest.php @@ -0,0 +1,36 @@ +id() from inside modifyQuery(), which + * was silently non-functional for the same reason as SkuConditionRule (see + * tests/Feature/Purchasable/Conditions/SkuConditionRuleTest.php) — now fixed by calling + * CraftCms\Cms\Element\Queries\ElementQuery::applyId() directly. + * + * VariantConditionRule isn't currently registered as a selectable rule on any condition, so this + * exercises modifyQuery() directly rather than through a Condition — mirroring the where()-wrapped + * call ElementCondition::modifyQuery() would make. + */ +beforeEach(function() { + $this->fixture = ProductConditionsFixture::seed(); +}); + +test('modifyQuery filters variants by the selected variant', function() { + $rule = new VariantConditionRule(); + $rule->setElementIds([$this->fixture->hoodieVariant->id]); + + $query = Variant::find(); + $query->where(function(Builder $builder) use ($rule, $query) { + $rule->modifyQuery($builder, $query); + }); + $ids = $query->ids(); + + expect($ids)->toContain($this->fixture->hoodieVariant->id); + expect($ids)->not->toContain($this->fixture->tShirtVariant->id); +}); diff --git a/tests/Feature/Product/Variant/Conditions/VariantProductConditionRuleTest.php b/tests/Feature/Product/Variant/Conditions/VariantProductConditionRuleTest.php new file mode 100644 index 0000000000..0d9b8173a1 --- /dev/null +++ b/tests/Feature/Product/Variant/Conditions/VariantProductConditionRuleTest.php @@ -0,0 +1,39 @@ +ownerId() from inside + * modifyQuery(), which was silently non-functional for the same reason as SkuConditionRule (see + * tests/Feature/Purchasable/Conditions/SkuConditionRuleTest.php) — now fixed by applying the + * ownerId filter directly to $query. + */ +beforeEach(function() { + $this->fixture = ProductConditionsFixture::seed(); +}); + +test('matchElement matches a variant owned by the given product', function() { + $rule = new VariantProductConditionRule(); + $rule->setElementIds([$this->fixture->hoodie->id]); + + expect($rule->matchElement($this->fixture->hoodieVariant))->toBeTrue(); + expect($rule->matchElement($this->fixture->tShirtVariant))->toBeFalse(); +}); + +test('modifyQuery filters variants by owning product', function() { + $condition = Variant::createCondition(); + $rule = new VariantProductConditionRule(); + $rule->setElementIds([$this->fixture->hoodie->id]); + $condition->addConditionRule($rule); + + $query = Variant::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + expect($ids)->toContain($this->fixture->hoodieVariant->id); + expect($ids)->not->toContain($this->fixture->tShirtVariant->id); +}); diff --git a/tests/Feature/Product/Variant/PricingCatalogTest.php b/tests/Feature/Product/Variant/PricingCatalogTest.php new file mode 100644 index 0000000000..f3b0e1439a --- /dev/null +++ b/tests/Feature/Product/Variant/PricingCatalogTest.php @@ -0,0 +1,179 @@ +storeId = app(Stores::class)->getPrimaryStore()->id; + $catalogPricingRule->name = $rule['name']; + $catalogPricingRule->applyAmount = $rule['applyAmount']; + $catalogPricingRule->isPromotionalPrice = $rule['isPromotionalPrice'] ?? false; + $catalogPricingRule->enabled = true; + + /** @var CatalogPricingRuleVariantCondition $variantCondition */ + $variantCondition = $catalogPricingRule->getVariantCondition(); + $skuRule = new SkuConditionRule(); + $skuRule->value = $rule['sku']; + $variantCondition->addConditionRule($skuRule); + $catalogPricingRule->setVariantCondition($variantCondition); + + expect(app(CatalogPricingRules::class)->saveCatalogPricingRule($catalogPricingRule))->toBeTrue(); + + return $catalogPricingRule->id; +} + +/** + * @param array $rules + * @return int[] The saved rules' IDs. + */ +function applyVariantCatalogPricingRulesForSkus(array $rules): array +{ + $ids = array_map(saveVariantCatalogPricingRuleForSku(...), $rules); + + if (!empty($ids)) { + app(CatalogPricing::class)->generateCatalogPrices(); + } + + return $ids; +} + +/** @param int[] $ids */ +function removeVariantCatalogPricingRules(array $ids): void +{ + if (empty($ids)) { + return; + } + + foreach ($ids as $id) { + app(CatalogPricingRules::class)->deleteCatalogPricingRuleById($id); + } + + app(CatalogPricing::class)->generateCatalogPrices(); +} + +beforeEach(function() { + $this->fixture = ProductConditionsFixture::seed(); +}); + +test('a variant with no catalog pricing rules and no sales falls back to its base price', function() { + $variant = Variant::find()->sku('rad-hood')->one(); + + expect($variant->getPrice())->toEqual(123.99); + expect($variant->getPromotionalPrice())->toBeNull(); + expect($variant->getSalePrice())->toEqual(123.99); +}); + +test('getPrice/getPromotionalPrice/getSalePrice reflect any active catalog pricing rules', function(array $rules, float|int|null $salePrice, float|int|null $promotionalPrice, float|int|null $price) { + $ruleIds = applyVariantCatalogPricingRulesForSkus($rules); + + $variant = Variant::find()->sku('rad-hood')->one(); + + expect($variant)->toBeInstanceOf(Variant::class); + expect($variant->getPrice())->toEqual($price); + expect($variant->getPromotionalPrice())->toEqual($promotionalPrice); + expect($variant->getSalePrice())->toEqual($salePrice); + + removeVariantCatalogPricingRules($ruleIds); +})->with('variantCatalogPricingRuleScenarios'); + +test('the price/promotionalPrice/salePrice query scopes stay in sync with catalog pricing rules', function(array $rules, float|int|null $salePrice, float|int|null $promotionalPrice, float|int|null $price) { + $ruleIds = applyVariantCatalogPricingRulesForSkus($rules); + + $variant = Variant::find()->price($price)->one(); + expect($variant)->toBeInstanceOf(Variant::class); + expect($variant->getSku())->toBe('rad-hood'); + expect($variant->getPrice())->toEqual($price); + + if ($promotionalPrice !== null) { + $variant = Variant::find()->promotionalPrice($promotionalPrice)->one(); + expect($variant)->toBeInstanceOf(Variant::class); + expect($variant->getSku())->toBe('rad-hood'); + expect($variant->getPromotionalPrice())->toEqual($promotionalPrice); + } + + // Once a catalog pricing rule exists, `salePrice` filters against the real + // `catalogprices.salePrice` column. With none active, it instead compiles to a raw + // `CASE WHEN ... END` expression (see QueriesPurchasablePricing::applySalePrice) — under + // SQLite specifically, a bound parameter never matches a computed expression's result due + // to a storage-class mismatch (same underlying quirk as COM-667/COM-668's salePrice-order + // no-op), so that branch isn't exercised here. + if (!empty($rules)) { + $variant = Variant::find()->salePrice($salePrice)->one(); + expect($variant)->toBeInstanceOf(Variant::class); + expect($variant->getSku())->toBe('rad-hood'); + expect($variant->getSalePrice())->toEqual($salePrice); + } + + removeVariantCatalogPricingRules($ruleIds); +})->with('variantCatalogPricingRuleScenarios'); + +test('the onPromotion query scope matches variants with an active promotional price', function(array $rules, float|int|null $salePrice, float|int|null $promotionalPrice, float|int|null $price) { + $ruleIds = applyVariantCatalogPricingRulesForSkus($rules); + + $variantOnPromotion = Variant::find()->onPromotion()->one(); + + if ($promotionalPrice !== null) { + expect($variantOnPromotion)->toBeInstanceOf(Variant::class); + expect($variantOnPromotion->getSku())->toBe('rad-hood'); + expect($variantOnPromotion->getPromotionalPrice())->toEqual($promotionalPrice); + } else { + expect($variantOnPromotion)->toBeNull(); + } + + // Exercised for coverage of the inverse scope's SQL path, but not asserted on: its result + // is ambiguous whenever nothing has a promotional price set yet, since a plain "<" price + // comparison against a NULL promotional price never matches either way (see + // QueriesPurchasablePricing::applyOnPromotion). + Variant::find()->onPromotion(false)->one(); + + removeVariantCatalogPricingRules($ruleIds); +})->with('variantCatalogPricingRuleScenarios'); + +dataset('variantCatalogPricingRuleScenarios', [ + 'no catalog pricing rules' => [ + [], + 123.99, + null, + 123.99, + ], + 'a single rule reduces the price' => [ + [ + ['name' => 'Test Rule', 'sku' => 'rad-hood', 'applyAmount' => -0.1], + ], + 111.59, + null, + 111.59, + ], + 'a promotional rule discounts the sale price without changing the price' => [ + [ + ['name' => 'Test Rule', 'sku' => 'rad-hood', 'applyAmount' => -0.1, 'isPromotionalPrice' => true], + ], + 111.59, + 111.59, + 123.99, + ], + 'a non-promotional rule wins over an additional promotional rule' => [ + [ + ['name' => 'Test Rule - 5%', 'sku' => 'rad-hood', 'applyAmount' => -0.05], + ['name' => 'Test Rule - 1%', 'sku' => 'rad-hood', 'applyAmount' => -0.01, 'isPromotionalPrice' => true], + ], + 117.79, + null, + 117.79, + ], +]); diff --git a/tests/Feature/Product/Variant/PricingSalesTest.php b/tests/Feature/Product/Variant/PricingSalesTest.php new file mode 100644 index 0000000000..0509cdb9e4 --- /dev/null +++ b/tests/Feature/Product/Variant/PricingSalesTest.php @@ -0,0 +1,18 @@ +fixture = SalesFixture::seed(); +}); + +test('a variant with an active sale and no catalog pricing rules uses the sales system for its promotional and sale price', function() { + $variant = Variant::find()->sku('rad-hood')->one(); + + expect($variant->getPrice())->toEqual(123.99); + expect($variant->getPromotionalPrice())->toEqual(111.59); + expect($variant->getSalePrice())->toEqual(111.59); +}); diff --git a/tests/Feature/Product/Variant/VariantCollectionTest.php b/tests/Feature/Product/Variant/VariantCollectionTest.php new file mode 100644 index 0000000000..0b1a96b556 --- /dev/null +++ b/tests/Feature/Product/Variant/VariantCollectionTest.php @@ -0,0 +1,98 @@ +fixture = VariantQueryFixture::seed(); +}); + +test('collect() returns a VariantCollection', function() { + $collection = Variant::find()->limit(4)->collect(); + + expect($collection)->toBeInstanceOf(VariantCollection::class); + expect($collection)->toBeInstanceOf(ElementCollection::class); +}); + +test('cheapest() returns the variant with the lowest sale price', function() { + $collection = Variant::find()->collect(); + + expect($collection)->toBeInstanceOf(VariantCollection::class); + expect($collection->cheapest())->not->toBeNull(); + expect($collection->cheapest()->getSku())->toBe('hct-white'); +}); + +test('make() builds variants from raw attributes, including custom field values', function() { + $field = new PlainText([ + 'name' => 'My Variant Heading Field', + 'handle' => 'myVariantHeadingField', + ]); + expect(app(Fields::class)->saveField($field))->toBeTrue(); + + $fieldLayout = new FieldLayout(['type' => Variant::class]); + $fieldLayout->tab(FieldLayout::defaultTabName(), fn($tab) => $tab->field($field->handle)); + + // Built as its own product type (rather than attaching the layout to, and re-saving, the + // shared fixture's already-saved product type): re-validating the same in-memory + // ProductType instance a second time reuses its first call's cached validator, including a + // unique-handle rule that ignores the pre-save `null` id, so re-saving an already-saved + // instance fails validation even though nothing actually conflicts. It needs to pick up + // this field layout on its one and only save instead. + $site = Sites::getCurrentSite(); + $productType = new ProductType(); + $productType->name = 'Field Test Hoodies'; + $productType->handle = 'fieldTestHoodies'; + $productType->hasVariantTitleField = false; + $productType->variantTitleFormat = '{product.title}'; + $productType->setVariantFieldLayout($fieldLayout); + + $siteSettings = new ProductTypeSite(); + $siteSettings->siteId = $site->id; + $siteSettings->hasUrls = false; + $siteSettings->enabledByDefault = true; + $productType->setSiteSettings([$site->id => $siteSettings]); + + expect(app(ProductTypes::class)->saveProductType($productType))->toBeTrue(); + + $product = new Product(); + $product->typeId = $productType->id; + $product->title = 'Rad Hoodie'; + $product->enabled = true; + $product->siteId = $site->id; + expect(Elements::saveElement($product))->toBeTrue(); + $product = Product::find()->id($product->id)->one(); + + $attrs = [ + 'ownerId' => $product->id, + 'owner' => $product, + 'primaryOwnerId' => $product->id, + 'primaryOwner' => $product, + 'title' => 'Test Variant', + 'basePrice' => 123.0, + 'sku' => '123', + 'enabled' => true, + 'myVariantHeadingField' => 'bar', + ]; + + $collection = VariantCollection::make([$attrs]); + + expect($collection)->toBeInstanceOf(VariantCollection::class); + + $variant = $collection->first(); + foreach (['title', 'basePrice', 'sku', 'myVariantHeadingField'] as $key) { + expect($variant->$key)->toEqual($attrs[$key]); + } +}); diff --git a/tests/Feature/Product/Variant/VariantEagerLoadingTest.php b/tests/Feature/Product/Variant/VariantEagerLoadingTest.php new file mode 100644 index 0000000000..afdd41719f --- /dev/null +++ b/tests/Feature/Product/Variant/VariantEagerLoadingTest.php @@ -0,0 +1,32 @@ +fixture = ProductConditionsFixture::seed(); +}); + +test('eagerLoadingMap resolves product/owner/primaryOwner to Product and falls through to the base implementation for other handles', function(string $handle, ?array $expected) { + $variants = Variant::find()->all(); + + $map = Variant::eagerLoadingMap($variants, $handle); + + if ($expected !== null) { + expect($map)->not->toBeEmpty(); + foreach ($expected as $key => $value) { + expect($map)->toHaveKey($key); + expect($map[$key])->toEqual($value); + } + } else { + expect($map)->toBeEmpty(); + } +})->with([ + 'product' => ['product', ['elementType' => Product::class]], + 'owner' => ['owner', ['elementType' => Product::class]], + 'primaryOwner' => ['primaryOwner', ['elementType' => Product::class]], + 'an unrecognized handle falls through to the base implementation, which has nothing to eager-load' => ['customField', null], +]); diff --git a/tests/Feature/Product/Variant/VariantOwnerTest.php b/tests/Feature/Product/Variant/VariantOwnerTest.php new file mode 100644 index 0000000000..2a198ec132 --- /dev/null +++ b/tests/Feature/Product/Variant/VariantOwnerTest.php @@ -0,0 +1,59 @@ +fixture = VariantQueryFixture::seed(); +}); + +test('ownerType resolves to Product', function() { + $method = new ReflectionMethod(Variant::class, 'ownerType'); + + expect($method->invoke(new Variant()))->toBe(Product::class); +}); + +test('product is included in extraFields', function() { + expect((new Variant())->extraFields())->toContain('product'); +}); + +test('getOwner returns the owning product', function() { + $owner = $this->fixture->whiteVariant->getOwner(); + + expect($owner)->toBeInstanceOf(Product::class); + expect($owner->id)->toBe($this->fixture->tee->id); +}); + +test('getPrimaryOwner returns the owning product', function() { + $primaryOwner = $this->fixture->whiteVariant->getPrimaryOwner(); + + expect($primaryOwner)->toBeInstanceOf(Product::class); + expect($primaryOwner->id)->toBe($this->fixture->tee->id); +}); + +test('getProduct still works as an alias for getOwner', function() { + $product = $this->fixture->hoodieVariant->getProduct(); + + expect($product)->toBeInstanceOf(Product::class); + expect($product->id)->toBe($this->fixture->hoodie->id); +}); + +test('setOwner accepts a Product instance', function() { + $variant = new Variant(); + $variant->setOwner($this->fixture->tee); + + $owner = $variant->getOwner(); + expect($owner)->toBeInstanceOf(Product::class); + expect($owner->id)->toBe($this->fixture->tee->id); +}); + +test('a variant\'s owner resolves to the same site as the variant itself', function() { + $owner = $this->fixture->whiteVariant->getOwner(); + + expect($owner)->toBeInstanceOf(Product::class); + expect($owner->id)->toBe($this->fixture->tee->id); + expect($owner->siteId)->toBe($this->fixture->whiteVariant->siteId); +}); diff --git a/tests/Feature/Product/Variant/VariantQueryAuthorizationTest.php b/tests/Feature/Product/Variant/VariantQueryAuthorizationTest.php new file mode 100644 index 0000000000..678dbd3e6a --- /dev/null +++ b/tests/Feature/Product/Variant/VariantQueryAuthorizationTest.php @@ -0,0 +1,67 @@ +makePartial(); + $mock->shouldReceive('doesUserHavePermission') + ->andReturnUsing(fn(int $userId, string $checkPermission): bool => in_array($checkPermission, $permissions, true)); + app()->instance(UserPermissions::class, $mock); +} + +beforeEach(function() { + $this->fixture = ProductConditionsFixture::seed(); + + $this->user = new User(); + $this->user->username = 'variant-query-non-admin'; + $this->user->email = 'variant-query-non-admin@crafttest.com'; + $this->user->admin = false; + if (!Elements::saveElement($this->user)) { + throw new RuntimeException('Could not save user: ' . json_encode($this->user->errors()->all())); + } + + $this->actingAs($this->user, 'craft'); +}); + +test('editable respects the current viewProductType permission', function() { + grantVariantQueryTestPermissions(["commerce-viewProductType:{$this->fixture->tShirtsType->uid}"]); + + $ids = Variant::find()->editable(true)->ids(); + + expect($ids)->toBe([$this->fixture->tShirtVariant->id]); +}); + +test('savable respects the current saveProductType permission', function() { + grantVariantQueryTestPermissions(["commerce-saveProductType:{$this->fixture->tShirtsType->uid}"]); + + $ids = Variant::find()->savable(true)->ids(); + + expect($ids)->toBe([$this->fixture->tShirtVariant->id]); +}); + +test('editable ignores a permission string outside the current viewProductType/saveProductType set', function() { + grantVariantQueryTestPermissions(["commerce-editProductType:{$this->fixture->tShirtsType->uid}"]); + + $ids = Variant::find()->editable(true)->ids(); + + expect($ids)->toBe([]); +}); diff --git a/tests/Feature/Product/Variant/VariantQuerySiteTest.php b/tests/Feature/Product/Variant/VariantQuerySiteTest.php new file mode 100644 index 0000000000..3331bdb944 --- /dev/null +++ b/tests/Feature/Product/Variant/VariantQuerySiteTest.php @@ -0,0 +1,123 @@ + 'Second Primary Site', + 'handle' => 'secondPrimarySite', + 'language' => 'en-US', + 'baseUrl' => 'https://secondPrimarySite.test', + 'hasUrls' => true, + 'groupId' => $stores->usSite->groupId, + ]); + if (!Sites::saveSite($secondPrimarySite)) { + throw new RuntimeException('Could not save site: ' . json_encode($secondPrimarySite->errors()->all())); + } + + $productType = new ProductType(); + $productType->name = 'Multi-Site Widgets'; + $productType->handle = 'multiSiteWidgets'; + $productType->hasVariantTitleField = true; + $productType->variantTitleFormat = '{product.title} - {title}'; + $productType->setSiteSettings([ + $stores->usSite->id => variantSiteQueryProductTypeSite($stores->usSite->id), + $secondPrimarySite->id => variantSiteQueryProductTypeSite($secondPrimarySite->id), + $stores->euSite->id => variantSiteQueryProductTypeSite($stores->euSite->id), + ]); + if (!app(ProductTypes::class)->saveProductType($productType)) { + throw new RuntimeException('Could not save product type: ' . json_encode($productType->errors()->all())); + } + + $product = new Product(); + $product->typeId = $productType->id; + $product->title = 'Widget'; + $product->enabled = true; + $product->siteId = $stores->usSite->id; + if (!Elements::saveElement($product)) { + throw new RuntimeException('Could not save product: ' . json_encode($product->errors()->all())); + } + + foreach (['Red' => true, 'Blue' => false] as $title => $isDefault) { + $variant = new Variant(); + $variant->title = $title; + $variant->setPrimaryOwner($product); + $variant->setSku('widget-' . strtolower($title)); + $variant->setBasePrice(10.0); + $variant->isDefault = $isDefault; + $variant->promotable = true; + $variant->siteId = $stores->usSite->id; + if (!Elements::saveElement($variant)) { + throw new RuntimeException('Could not save variant: ' . json_encode($variant->errors()->all())); + } + } + + return [$stores, $secondPrimarySite]; +} + +function variantSiteQueryProductTypeSite(int $siteId): ProductTypeSite +{ + $siteSettings = new ProductTypeSite(); + $siteSettings->siteId = $siteId; + $siteSettings->hasUrls = false; + $siteSettings->enabledByDefault = true; + + return $siteSettings; +} + +beforeEach(function() { + [$this->stores, $this->secondPrimarySite] = seedVariantSiteQueryScenario(); +}); + +test('site scopes results to variants existing in the given site', function() { + $ids = Variant::find()->site($this->stores->usSite->handle)->ids(); + + expect($ids)->toHaveCount(2); +}); + +test('site accepts multiple sites under the same store, and every result resolves to that store', function() { + $variants = Variant::find()->site([$this->stores->usSite->handle, $this->secondPrimarySite->handle])->all(); + + expect($variants)->toHaveCount(4); + foreach ($variants as $variant) { + expect($variant->getStore()->handle)->toBe($this->stores->primaryStore->handle); + } +}); + +test('site accepts multiple sites across different stores, and each result resolves to its own store', function() { + $variants = Variant::find()->site([$this->stores->usSite->handle, $this->stores->euSite->handle])->all(); + + expect($variants)->toHaveCount(4); + + $storeHandleBySite = []; + foreach ($variants as $variant) { + $storeHandleBySite[$variant->getSite()->handle] = $variant->getStore()->handle; + } + + expect($storeHandleBySite)->toBe([ + $this->stores->usSite->handle => $this->stores->primaryStore->handle, + $this->stores->euSite->handle => $this->stores->euStore->handle, + ]); +}); diff --git a/tests/Feature/Product/Variant/VariantQuerySmokeTest.php b/tests/Feature/Product/Variant/VariantQuerySmokeTest.php new file mode 100644 index 0000000000..82469ce86a --- /dev/null +++ b/tests/Feature/Product/Variant/VariantQuerySmokeTest.php @@ -0,0 +1,38 @@ +fixture = ProductConditionsFixture::seed(); +}); + +test('typeId filters variants by owning product type', function() { + $ids = Variant::find()->typeId($this->fixture->hoodiesType->id)->ids(); + + expect($ids)->toContain($this->fixture->hoodieVariant->id); + expect($ids)->not->toContain($this->fixture->tShirtVariant->id); +}); + +test('productStatus filters variants by owning product status', function() { + $this->fixture->tShirt->enabled = false; + \CraftCms\Cms\Support\Facades\Elements::saveElement($this->fixture->tShirt, false); + + $ids = Variant::find()->productStatus('disabled')->ids(); + + expect($ids)->toContain($this->fixture->tShirtVariant->id); + expect($ids)->not->toContain($this->fixture->hoodieVariant->id); +}); + +test('editable aborts to an empty result set when there is no current user', function() { + $ids = Variant::find()->editable()->ids(); + + expect($ids)->toBe([]); +}); diff --git a/tests/Feature/Product/Variant/VariantQueryTest.php b/tests/Feature/Product/Variant/VariantQueryTest.php new file mode 100644 index 0000000000..3fcff6fe47 --- /dev/null +++ b/tests/Feature/Product/Variant/VariantQueryTest.php @@ -0,0 +1,112 @@ +fixture = VariantQueryFixture::seed(); +}); + +test('find returns a VariantQuery', function() { + expect(Variant::find())->toBeInstanceOf(VariantQuery::class); +}); + +test('shippingCategoryId filters to variants with the given shipping category', function() { + $ids = Variant::find()->shippingCategoryId($this->fixture->specialShippingCategory->id)->ids(); + + expect($ids)->toContain($this->fixture->whiteVariant->id, $this->fixture->hoodieVariant->id); + expect($ids)->not->toContain($this->fixture->blueVariant->id); +}); + +test('shippingCategory filters by handle', function() { + $ids = Variant::find()->shippingCategory('specialShipping')->ids(); + + expect($ids)->toContain($this->fixture->whiteVariant->id, $this->fixture->hoodieVariant->id); + expect($ids)->not->toContain($this->fixture->blueVariant->id); +}); + +test('shippingCategory filters by a ShippingCategory instance', function() { + $ids = Variant::find()->shippingCategory($this->fixture->specialShippingCategory)->ids(); + + expect($ids)->toContain($this->fixture->whiteVariant->id); + expect($ids)->not->toContain($this->fixture->blueVariant->id); +}); + +test('taxCategoryId filters to variants with the given tax category', function() { + $ids = Variant::find()->taxCategoryId($this->fixture->reducedTaxCategory->id)->ids(); + + expect($ids)->toContain($this->fixture->whiteVariant->id); + expect($ids)->not->toContain($this->fixture->blueVariant->id, $this->fixture->hoodieVariant->id); +}); + +test('taxCategory filters by handle', function() { + $ids = Variant::find()->taxCategory('reducedTax')->ids(); + + expect($ids)->toContain($this->fixture->whiteVariant->id); + expect($ids)->not->toContain($this->fixture->blueVariant->id, $this->fixture->hoodieVariant->id); +}); + +test('taxCategory filters by a TaxCategory instance', function() { + $ids = Variant::find()->taxCategory($this->fixture->reducedTaxCategory)->ids(); + + expect($ids)->toContain($this->fixture->whiteVariant->id); + expect($ids)->not->toContain($this->fixture->blueVariant->id); +}); + +test('every variant has its price and salePrice populated', function() { + $results = Variant::find()->all(); + + expect($results)->not->toBeEmpty(); + foreach ($results as $variant) { + expect($variant->getPrice())->not->toBeNull(); + expect($variant->getSalePrice())->not->toBeNull(); + } +}); + +test('orderBy sorts variants by price', function(string $orderBy, bool $descending) { + $skus = Variant::find()->orderBy($orderBy)->collect()->map(fn(Variant $v) => $v->getSku())->all(); + $expected = ['hct-white', 'hct-blue', 'rad-hood']; + + expect($skus)->toBe($descending ? array_reverse($expected) : $expected); +})->with([ + 'price asc' => ['price ASC', false], + 'price desc' => ['price DESC', true], +]); + +test('a catalog pricing rule targeting a sku updates that variant\'s price and salePrice', function() { + $storeId = app(Stores::class)->getPrimaryStore()->id; + + $catalogPricingRule = new CatalogPricingRule(); + $catalogPricingRule->storeId = $storeId; + $catalogPricingRule->name = 'Half off blue'; + $catalogPricingRule->apply = CatalogPricingRuleRecord::APPLY_BY_PERCENT; + $catalogPricingRule->applyAmount = 50 / -100; + $catalogPricingRule->applyPriceType = CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE; + $catalogPricingRule->enabled = true; + + $variantCondition = $catalogPricingRule->getVariantCondition(); + $rule = new SkuConditionRule(); + $rule->value = 'hct-blue'; + $variantCondition->addConditionRule($rule); + $catalogPricingRule->setVariantCondition($variantCondition); + + expect(app(CatalogPricingRules::class)->saveCatalogPricingRule($catalogPricingRule))->toBeTrue(); + + app(CatalogPricing::class)->generateCatalogPrices(); + + $blue = Variant::find()->sku('hct-blue')->one(); + expect($blue->getPrice())->toEqual(11.0); + expect($blue->getSalePrice())->toEqual(11.0); + + $white = Variant::find()->sku('hct-white')->one(); + expect($white->getPrice())->toEqual(19.99); +}); diff --git a/tests/Feature/Promotion/CouponsTest.php b/tests/Feature/Promotion/CouponsTest.php new file mode 100644 index 0000000000..8e96f84817 --- /dev/null +++ b/tests/Feature/Promotion/CouponsTest.php @@ -0,0 +1,111 @@ +getAllCodes(); + + expect($codes)->toBeArray()->not->toBeEmpty(); + expect($codes)->toContain('discount_1'); +}); + +test('getCouponByCode returns the matching coupon, or null when the code is unknown', function(string $code, ?string $expectedCode) { + DiscountsFixture::seed(); + $coupon = app(Coupons::class)->getCouponByCode($code); + + if ($expectedCode === null) { + expect($coupon)->toBeNull(); + } else { + expect($coupon)->toBeInstanceOf(Coupon::class); + expect($coupon->code)->toBe($expectedCode); + } +})->with([ + 'existing code' => ['discount_1', 'discount_1'], + 'unknown code' => ['invalid_code', null], +]); + +test('getCouponsByDiscountId returns only the coupons for that discount, or none for an unknown ID', function() { + $fixture = DiscountsFixture::seed(); + $coupons = app(Coupons::class); + + expect($coupons->getCouponsByDiscountId(0))->toBe([]); + + $result = $coupons->getCouponsByDiscountId($fixture->discountWithCoupon->id); + expect($result)->not->toBeEmpty(); + expect(array_map(fn(Coupon $c) => $c->code, $result))->toContain('discount_1'); +}); + +test('generateCouponCodes generates the requested count of codes matching the format', function(int $count, string $format, array $existingCodes) { + DiscountsFixture::seed(); + $codes = app(Coupons::class)->generateCouponCodes($count, $format, $existingCodes); + + expect($codes)->toHaveCount($count); + expect($codes[0])->toMatch('/' . str_replace(Coupons::COUPON_FORMAT_REPLACEMENT_CHAR, '.', $format) . '/'); +})->with([ + 'simple format, no existing codes' => [10, 'commerce_####', []], + 'restrictive format with an excluded existing code' => [25, 'commerce_#_coupons', ['commerce_A_coupons']], +]); + +test('generateCouponCodes throws when the format cannot produce enough unique codes', function() { + DiscountsFixture::seed(); + + app(Coupons::class)->generateCouponCodes(45, 'commerce_#', []); +})->throws(Exception::class); + +test('saveCoupon persists a new coupon but rejects a duplicate code', function(string $code, bool $expectedResult) { + $fixture = DiscountsFixture::seed(); + $coupon = new Coupon(['code' => $code, 'discountId' => $fixture->discountWithCoupon->id]); + + $result = app(Coupons::class)->saveCoupon($coupon, true); + + expect($result)->toBe($expectedResult); + + if ($expectedResult) { + expect($coupon->id)->not->toBeNull(); + } else { + expect($coupon->id)->toBeNull(); + } +})->with([ + 'new, unique code' => ['test_code', true], + 'code already in use' => ['discount_1', false], +]); + +test('deleteCouponById removes the coupon record', function() { + $fixture = DiscountsFixture::seed(); + + $couponRecord = new CouponRecord(); + $couponRecord->code = 'commerce_test_code'; + $couponRecord->discountId = $fixture->discountWithCoupon->id; + $couponRecord->uses = 0; + $couponRecord->maxUses = null; + $couponRecord->save(); + + expect(app(Coupons::class)->deleteCouponById($couponRecord->id))->toBeTrue(); +}); + +test('saveDiscountCoupons adds new coupons and removes ones no longer on the discount', function() { + $fixture = DiscountsFixture::seed(); + + $discount = app(Discounts::class)->getDiscountById($fixture->discountWithCoupon->id); + $newCoupon = new Coupon(['discountId' => $discount->id, 'code' => 'new_commerce_coupon', 'uses' => 0]); + $discount->setCoupons([...$discount->getCoupons(), $newCoupon]); + + expect(app(Coupons::class)->saveDiscountCoupons($discount))->toBeTrue(); + + // Clearing the coupon list entirely should delete every coupon that was on the discount. + $discount->setCoupons([]); + expect(app(Coupons::class)->saveDiscountCoupons($discount))->toBeTrue(); + expect(app(Coupons::class)->getCouponsByDiscountId($discount->id))->toBe([]); +}); + +test('saveDiscountCoupons throws when the discount has not been saved yet', function() { + app(Coupons::class)->saveDiscountCoupons(new Discount()); +})->throws(RuntimeException::class); diff --git a/tests/Feature/Promotion/Data/DiscountTest.php b/tests/Feature/Promotion/Data/DiscountTest.php new file mode 100644 index 0000000000..03e1e4aeaf --- /dev/null +++ b/tests/Feature/Promotion/Data/DiscountTest.php @@ -0,0 +1,105 @@ +percentDiscount = (float)$percentDiscount; + + expect($discount->getPercentDiscountAsPercent())->toBe($expected); +})->with([ + ['-0.1000', '10%'], + [0, '0%'], + [-0.1, '10%'], + [-0.15, '15%'], + [-0.105, '10.5%'], + [-0.10504, '10.504%'], + ['-0.1050400', '10.504%'], +]); + +test('hasOrderCondition is false when no order condition has been set', function() { + expect((new Discount())->hasOrderCondition())->toBeFalse(); +}); + +test('hasOrderCondition is false when the order condition has no rules', function() { + $discount = new Discount(); + $discount->setOrderCondition($discount->getOrderCondition()); + + expect($discount->hasOrderCondition())->toBeFalse(); +}); + +test('hasOrderCondition is true once a rule is added', function() { + $discount = new Discount(); + $discount->storeId = app(Stores::class)->getPrimaryStore()->id; + $condition = $discount->getOrderCondition(); + $condition->addConditionRule(new CompletedConditionRule()); + $discount->setOrderCondition($condition); + + expect($discount->hasOrderCondition())->toBeTrue(); +}); + +test('hasCustomerCondition is false when no customer condition has been set', function() { + expect((new Discount())->hasCustomerCondition())->toBeFalse(); +}); + +test('hasCustomerCondition is false when the customer condition has no rules', function() { + $discount = new Discount(); + $discount->setCustomerCondition($discount->getCustomerCondition()); + + expect($discount->hasCustomerCondition())->toBeFalse(); +}); + +test('hasCustomerCondition is true once a rule is added', function() { + $discount = new Discount(); + $condition = $discount->getCustomerCondition(); + $condition->addConditionRule(new SignedInConditionRule()); + $discount->setCustomerCondition($condition); + + expect($discount->hasCustomerCondition())->toBeTrue(); +}); + +test('hasBillingAddressCondition is false when no billing address condition has been set', function() { + expect((new Discount())->hasBillingAddressCondition())->toBeFalse(); +}); + +test('hasBillingAddressCondition is false when the billing address condition has no rules', function() { + $discount = new Discount(); + $discount->setBillingAddressCondition($discount->getBillingAddressCondition()); + + expect($discount->hasBillingAddressCondition())->toBeFalse(); +}); + +test('hasBillingAddressCondition is true once a rule is added', function() { + $discount = new Discount(); + $condition = $discount->getBillingAddressCondition(); + $condition->addConditionRule(new PostalCodeFormulaConditionRule()); + $discount->setBillingAddressCondition($condition); + + expect($discount->hasBillingAddressCondition())->toBeTrue(); +}); + +test('hasShippingAddressCondition is false when no shipping address condition has been set', function() { + expect((new Discount())->hasShippingAddressCondition())->toBeFalse(); +}); + +test('hasShippingAddressCondition is false when the shipping address condition has no rules', function() { + $discount = new Discount(); + $discount->setShippingAddressCondition($discount->getShippingAddressCondition()); + + expect($discount->hasShippingAddressCondition())->toBeFalse(); +}); + +test('hasShippingAddressCondition is true once a rule is added', function() { + $discount = new Discount(); + $condition = $discount->getShippingAddressCondition(); + $condition->addConditionRule(new PostalCodeFormulaConditionRule()); + $discount->setShippingAddressCondition($condition); + + expect($discount->hasShippingAddressCondition())->toBeTrue(); +}); diff --git a/tests/Feature/Promotion/DiscountsTest.php b/tests/Feature/Promotion/DiscountsTest.php new file mode 100644 index 0000000000..fe87bc8286 --- /dev/null +++ b/tests/Feature/Promotion/DiscountsTest.php @@ -0,0 +1,542 @@ + $orderConfig + */ +function discountsTestOrderCouponAvailable(array $orderConfig, bool $expectedResult, string $expectedExplanation = ''): void +{ + $order = new Order($orderConfig); + + $explanation = ''; + $result = app(Discounts::class)->orderCouponAvailable($order, $explanation); + + expect($result)->toBe($expectedResult); + expect($explanation)->toBe($expectedExplanation); +} + +function discountsTestUpdateDiscount(int $discountId, array $data): void +{ + DB::table(Table::DISCOUNTS)->where('id', $discountId)->update($data); +} + +/** + * Fills in default attributes shared by every discount in a dataset. + * + * @param array> $discounts + * @return array> + */ +function discountsTestCreateDiscounts(array $discounts): array +{ + return collect($discounts)->mapWithKeys(fn(array $d, string $key) => [$key => array_merge([ + 'name' => 'Discount - ' . $key, + 'perItemDiscount' => 1, + 'enabled' => true, + 'allCategories' => true, + 'allPurchasables' => true, + 'percentageOffSubject' => 'original', + ], $d)])->all(); +} + +// --------------------------------------------------------------------------------------------- +// orderCouponAvailable() +// --------------------------------------------------------------------------------------------- + +test('orderCouponAvailable is false for an unknown coupon code', function() { + DiscountsFixture::seed(); + + discountsTestOrderCouponAvailable(['couponCode' => 'invalid_coupon'], false, 'Coupon not valid.'); +}); + +test('orderCouponAvailable is true for a valid coupon code with a signed-in customer', function() { + $fixture = DiscountsFixture::seed(); + $this->actingAs($fixture->customer, 'craft'); + + discountsTestOrderCouponAvailable(['couponCode' => 'discount_1', 'customerId' => $fixture->customer->id], true); +}); + +test('orderCouponAvailable is false once the discount has been disabled', function() { + $fixture = DiscountsFixture::seed(); + $this->actingAs($fixture->customer, 'craft'); + discountsTestUpdateDiscount($fixture->discountWithCoupon->id, ['enabled' => false]); + + discountsTestOrderCouponAvailable(['couponCode' => 'discount_1', 'customerId' => $fixture->customer->id], false, 'Coupon not valid.'); +}); + +test('orderCouponAvailable is false once the discount has expired', function() { + $fixture = DiscountsFixture::seed(); + $this->actingAs($fixture->customer, 'craft'); + discountsTestUpdateDiscount($fixture->discountWithCoupon->id, ['dateTo' => '2019-05-01 10:21:33']); + + discountsTestOrderCouponAvailable(['couponCode' => 'discount_1', 'customerId' => $fixture->customer->id], false, 'Discount is out of date.'); +}); + +test('orderCouponAvailable is false before the discount has started', function() { + $fixture = DiscountsFixture::seed(); + $this->actingAs($fixture->customer, 'craft'); + $dateFrom = (new DateTime('now'))->add(new DateInterval('P2D')); + discountsTestUpdateDiscount($fixture->discountWithCoupon->id, ['dateFrom' => $dateFrom->format('Y-m-d H:i:s')]); + + discountsTestOrderCouponAvailable(['couponCode' => 'discount_1', 'customerId' => $fixture->customer->id], false, 'Discount is out of date.'); +}); + +test('orderCouponAvailable is false once the discount has reached its total use limit', function() { + $fixture = DiscountsFixture::seed(); + $this->actingAs($fixture->customer, 'craft'); + // The fixture discount's totalDiscountUseLimit is 2. + discountsTestUpdateDiscount($fixture->discountWithCoupon->id, ['totalDiscountUses' => 2]); + + discountsTestOrderCouponAvailable(['couponCode' => 'discount_1', 'customerId' => $fixture->customer->id], false, 'Discount use has reached its limit.'); +}); + +test('orderCouponAvailable is false for a per-user-limited coupon with no signed-in customer', function() { + DiscountsFixture::seed(); + // The fixture discount's perUserLimit is already 1. + + discountsTestOrderCouponAvailable(['couponCode' => 'discount_1', 'customerId' => null], false, 'This coupon is for registered users and limited to 1 uses.'); +}); + +test('orderCouponAvailable is false once the signed-in customer has used their per-user limit', function() { + $fixture = DiscountsFixture::seed(); + $this->actingAs($fixture->customer, 'craft'); + // The fixture discount's perUserLimit is already 1. + + $usage = new CustomerDiscountUse(); + $usage->customerId = $fixture->customer->id; + $usage->discountId = $fixture->discountWithCoupon->id; + $usage->uses = 1; + $usage->save(); + + discountsTestOrderCouponAvailable(['couponCode' => 'discount_1', 'customerId' => $fixture->customer->id], false, 'This coupon is for registered users and limited to 1 uses.'); +}); + +test('orderCouponAvailable is false once the customer email has used its per-email limit', function() { + $fixture = DiscountsFixture::seed(); + $this->actingAs($fixture->customer, 'craft'); + discountsTestUpdateDiscount($fixture->discountWithCoupon->id, ['perEmailLimit' => 1]); + + $usage = new EmailDiscountUse(); + $usage->email = $fixture->customer->email; + $usage->discountId = $fixture->discountWithCoupon->id; + $usage->uses = 1; + $usage->save(); + + discountsTestOrderCouponAvailable(['couponCode' => 'discount_1', 'customerId' => $fixture->customer->id], false, 'This coupon is limited to 1 uses.'); +}); + +// --------------------------------------------------------------------------------------------- +// matchLineItem() +// +// Category-relation matching isn't covered: `matchLineItem()` unconditionally calls +// `craft\elements\Category::find()` whenever a discount has `allCategories: false`, which is +// currently broken (see COM-644/645/646/647), so it isn't exercised here. +// --------------------------------------------------------------------------------------------- + +test('matchLineItem matches a fully-promotable line item against an all-purchasables, all-categories discount', function() { + $order = new Order(['couponCode' => null]); + $lineItem = new LineItem(['qty' => 2]); + $lineItem->setPrice(10); + $lineItem->setIsPromotable(true); + $lineItem->setOrder($order); + + $discount = new Discount(['allPurchasables' => true, 'allCategories' => true]); + + expect(app(Discounts::class)->matchLineItem($lineItem, $discount))->toBeTrue(); +}); + +test('matchLineItem does not match a line item on sale when the discount excludes promotional items', function() { + $order = new Order(['couponCode' => null]); + $lineItem = new LineItem(['qty' => 2]); + $lineItem->setPrice(15); + $lineItem->setPromotionalPrice(10); + $lineItem->setOrder($order); + + $discount = new Discount(['excludeOnPromotion' => true]); + + expect(app(Discounts::class)->matchLineItem($lineItem, $discount))->toBeFalse(); +}); + +test('matchLineItem does not match a line item that is not promotable', function() { + $order = new Order(['couponCode' => null]); + $lineItem = new LineItem(['qty' => 2]); + $lineItem->setPrice(15); + $lineItem->setIsPromotable(false); + $lineItem->setOrder($order); + + $discount = new Discount(); + + expect(app(Discounts::class)->matchLineItem($lineItem, $discount))->toBeFalse(); +}); + +// --------------------------------------------------------------------------------------------- +// orderCompleteHandler() +// --------------------------------------------------------------------------------------------- + +test('orderCompleteHandler records discount, customer, email, and coupon usage', function() { + $fixture = DiscountsFixture::seed(); + $discountId = $fixture->discountWithCoupon->id; + + discountsTestUpdateDiscount($discountId, ['perUserLimit' => 0, 'perEmailLimit' => 0]); + + $order = new Order(); + $order->couponCode = 'discount_1'; + $order->setCustomerId($fixture->customer->id); + + $adjustment = new OrderAdjustment(); + $adjustment->name = 'Discount'; + $adjustment->type = DiscountAdjuster::ADJUSTMENT_TYPE; + $adjustment->amount = -5; + $adjustment->setSourceSnapshot(['discountUseId' => $discountId]); + $order->setAdjustments([$adjustment]); + + app(Discounts::class)->orderCompleteHandler($order); + + expect((int) DB::table(Table::DISCOUNTS)->where('id', $discountId)->value('totalDiscountUses'))->toBe(1); + + $customerUse = DB::table(Table::CUSTOMER_DISCOUNTUSES) + ->where('customerId', $fixture->customer->id) + ->where('discountId', $discountId) + ->first(); + expect($customerUse)->not->toBeNull(); + expect((int) $customerUse->uses)->toBe(1); + + $emailUse = DB::table(Table::EMAIL_DISCOUNTUSES) + ->where('email', $order->getEmail()) + ->where('discountId', $discountId) + ->first(); + expect($emailUse)->not->toBeNull(); + expect((int) $emailUse->uses)->toBe(1); + + expect((int) DB::table(Table::COUPONS)->where('code', 'discount_1')->value('uses'))->toBe(1); +}); + +test('orderCompleteHandler is a no-op when the order has no discount adjustments', function(?string $couponCode) { + $order = new Order(['couponCode' => $couponCode]); + + expect(app(Discounts::class)->orderCompleteHandler($order))->toBeNull(); +})->with([ + 'no coupon code' => [null], + 'invalid coupon code' => ['i_dont_exist_as_coupon'], +]); + +// --------------------------------------------------------------------------------------------- +// ensureSortOrder() +// --------------------------------------------------------------------------------------------- + +test('ensureSortOrder resequences every discount for the store into a contiguous 1..N order', function() { + $storeId = app(Stores::class)->getPrimaryStore()->id; + + $ids = []; + for ($i = 1; $i <= 5; $i++) { + $discount = new DiscountRecord(); + $discount->name = 'Dummy Discount ' . $i; + // Randomise the sort order so ensureSortOrder() actually has something to fix. + $discount->sortOrder = $i + random_int(1, 15); + $discount->storeId = $storeId; + $discount->enabled = true; + $discount->allCategories = true; + $discount->allPurchasables = true; + $discount->percentageOffSubject = 'original'; + $discount->save(); + $ids[] = $discount->id; + } + + app(Discounts::class)->ensureSortOrder($storeId); + + $discountRows = DB::table(Table::DISCOUNTS)->select(['id', 'sortOrder'])->orderBy('sortOrder')->get()->values(); + foreach ($discountRows as $i => $row) { + expect((int) $row->sortOrder)->toBe($i + 1); + } + + $allDiscounts = app(Discounts::class)->getAllDiscounts($storeId)->values(); + foreach ($allDiscounts as $i => $discount) { + expect($discount->sortOrder)->toBe($i + 1); + } + + foreach ($ids as $id) { + app(Discounts::class)->deleteDiscountById($id); + } +}); + +// --------------------------------------------------------------------------------------------- +// getAllActiveDiscounts() +// +// Datasets scoping a discount to specific categories (`allCategories: false` + `categoryIds`) +// are omitted — category-based discount matching is currently broken (see COM-644/645/646/647) +// so it isn't exercised here. +// --------------------------------------------------------------------------------------------- + +test('getAllActiveDiscounts returns only the discounts a given order currently qualifies for', function(array|false $orderConfig, int $expectedCount, array $discountConfigs) { + $fixture = DiscountsFixture::seed(); + + $discountIds = []; + foreach ($discountConfigs as $config) { + $emailUses = $config['_emailUses'] ?? []; + unset($config['_emailUses']); + + if (isset($config['purchasableIds'])) { + $config['purchasableIds'] = Variant::find()->sku($config['purchasableIds'])->ids(); + } + + $config['storeId'] = $fixture->storeId; + + $discount = new Discount($config); + expect(app(Discounts::class)->saveDiscount($discount))->toBeTrue(); + $discountIds[] = $discount->id; + + if ($discount->totalDiscountUses > 0) { + DB::table(Table::DISCOUNTS)->where('id', $discount->id)->update(['totalDiscountUses' => $discount->totalDiscountUses]); + } + + if (!empty($emailUses)) { + foreach ($emailUses as $email => $uses) { + $usage = new EmailDiscountUse(); + $usage->email = $email; + $usage->discountId = $discount->id; + $usage->uses = $uses; + $usage->save(); + } + } + } + + if ($orderConfig === false) { + $activeDiscounts = app(Discounts::class)->getAllActiveDiscounts(); + } else { + $order = new Order(array_diff_key($orderConfig, ['_lineItems' => true])); + + if (isset($orderConfig['_lineItems'])) { + $lineItems = []; + foreach ($orderConfig['_lineItems'] as $sku => $qty) { + $variant = Variant::find()->sku($sku)->one(); + $lineItems[] = app(LineItems::class)->create($order, [ + 'purchasableId' => $variant->id, + 'options' => [], + 'qty' => $qty, + ]); + } + $order->setLineItems($lineItems); + } + + $activeDiscounts = app(Discounts::class)->getAllActiveDiscounts($order); + } + + expect($activeDiscounts)->toHaveCount($expectedCount); + + foreach ($discountIds as $id) { + app(Discounts::class)->deleteDiscountById($id); + } +})->with([ + 'no order' => [false, 1, []], + 'order with valid coupon' => [['couponCode' => 'discount_1'], 1, []], + 'order with invalid coupon' => [['couponCode' => 'coupon_code_doesnt_exist'], 0, []], + 'order discounts by date' => [ + [], + 3, + (function() { + $yesterday = (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(12, 0)->modify('-1 day'); + $tomorrow = (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(12, 0)->modify('+1 day'); + + return discountsTestCreateDiscounts([ + 'date-from-valid' => ['dateFrom' => $yesterday], + 'date-from-invalid' => ['dateFrom' => $tomorrow], + 'date-to-valid' => ['dateTo' => $tomorrow], + 'date-to-invalid' => ['dateTo' => $yesterday], + 'date-to-from-valid' => ['dateFrom' => $yesterday, 'dateTo' => $tomorrow], + 'date-to-from-invalid' => ['dateFrom' => $tomorrow, 'dateTo' => (clone $tomorrow)->modify('+1 day')], + ]); + })(), + ], + 'order discounts by total use limit' => [ + [], + 4, + discountsTestCreateDiscounts([ + 'total-limit-zero' => ['totalDiscountUseLimit' => 0], + 'total-limit-zero-with-uses' => ['totalDiscountUses' => 10, 'totalDiscountUseLimit' => 0], + 'total-limit-valid-with-no-uses' => ['totalDiscountUses' => 0, 'totalDiscountUseLimit' => 10], + 'total-limit-valid-with-uses' => ['totalDiscountUses' => 7, 'totalDiscountUseLimit' => 10], + 'total-limit-invalid-equals' => ['totalDiscountUses' => 10, 'totalDiscountUseLimit' => 10], + 'total-limit-invalid-extra' => ['totalDiscountUses' => 11, 'totalDiscountUseLimit' => 10], + ]), + ], + 'order discounts by email limit, order has no email' => [ + [], + 1, + discountsTestCreateDiscounts([ + 'total-limit-zero' => ['perEmailLimit' => 0], + 'total-limit' => ['perEmailLimit' => 1], + ]), + ], + 'order discounts by email limit' => [ + ['email' => 'per.email.limit@crafttest.com'], + 4, + discountsTestCreateDiscounts([ + 'total-limit-zero' => ['perEmailLimit' => 0], + 'total-limit-zero-with-uses' => ['_emailUses' => ['per.email.limit@crafttest.com' => 10], 'perEmailLimit' => 0], + 'total-limit-valid-with-no-uses' => ['perEmailLimit' => 10], + 'total-limit-valid-with-uses' => ['_emailUses' => ['per.email.limit@crafttest.com' => 7], 'perEmailLimit' => 10], + 'total-limit-invalid-equals' => ['_emailUses' => ['per.email.limit@crafttest.com' => 10], 'perEmailLimit' => 10], + 'total-limit-invalid-extra' => ['_emailUses' => ['per.email.limit@crafttest.com' => 11], 'perEmailLimit' => 10], + ]), + ], + 'purchase total limit, no line items' => [ + [], + 2, + discountsTestCreateDiscounts([ + 'purchase-total-zero' => ['purchaseTotal' => 0], + 'purchase-total-all-purchasables-false' => ['purchaseTotal' => 10, 'allPurchasables' => false, 'purchasableIds' => ['rad-hood']], + // Dropped: 'purchase-total-all-categories-false' and 'purchase-total-both-all-false' — + // both set `allCategories: false` with real `categoryIds`, which crashes (see above). + ]), + ], + 'purchase total limit, with a line item' => [ + ['_lineItems' => ['rad-hood' => 1]], + // 'purchase-total-valid' (a $150 minimum spend against a single $123.99 rad-hood line + // item) is correctly excluded — despite its name, "valid"/"invalid" here describe the + // purchaseTotal value's format (a round number vs. a two-decimal one), not whether the + // discount is expected to match. + 3, + discountsTestCreateDiscounts([ + 'purchase-total-zero' => ['purchaseTotal' => 0], + 'purchase-total-all-purchasables-false' => ['purchaseTotal' => 10, 'allPurchasables' => false, 'purchasableIds' => ['rad-hood']], + // Dropped: 'purchase-total-all-categories-false' and 'purchase-total-both-all-false' (see above). + 'purchase-total-valid' => ['purchaseTotal' => 150], + 'purchase-total-invalid' => ['purchaseTotal' => 10.99], + ]), + ], + 'qty limits, no line items' => [ + [], + 4, + discountsTestCreateDiscounts([ + 'purchase-qty-zero' => ['purchaseQty' => 0], + 'max-qty-zero' => ['maxPurchaseQty' => 0], + 'both-zero' => ['purchaseQty' => 0, 'maxPurchaseQty' => 0], + 'purchase-qty-all-purchasables-false' => ['purchaseQty' => 4, 'allPurchasables' => false, 'purchasableIds' => ['rad-hood']], + // Dropped: 'purchase-total-all-categories-false' and 'purchase-total-both-all-false' (see above). + ]), + ], + 'qty limits, with line items' => [ + ['_lineItems' => ['rad-hood' => 4]], + 6, + discountsTestCreateDiscounts([ + 'purchase-qty-zero' => ['purchaseQty' => 0], + 'max-qty-zero' => ['maxPurchaseQty' => 0], + 'both-zero' => ['purchaseQty' => 0, 'maxPurchaseQty' => 0], + 'purchase-qty-valid' => ['purchaseQty' => 3], + 'purchase-qty-invalid' => ['purchaseQty' => 5], + 'max-qty-valid' => ['maxPurchaseQty' => 10], + 'max-qty-invalid' => ['maxPurchaseQty' => 3], + 'both-valid' => ['purchaseQty' => 2, 'maxPurchaseQty' => 10], + 'both-invalid' => ['purchaseQty' => 10, 'maxPurchaseQty' => 14], + ]), + ], + 'purchasables restriction, one line item' => [ + ['_lineItems' => ['rad-hood' => 1]], + 3, + discountsTestCreateDiscounts([ + 'all-purchasables' => ['allPurchasables' => true], + 'one-to-one' => ['allPurchasables' => false, 'purchasableIds' => ['rad-hood']], + 'one-to-many' => ['allPurchasables' => false, 'purchasableIds' => ['rad-hood', 'hct-white']], + 'no-match' => ['allPurchasables' => false, 'purchasableIds' => ['hct-blue']], + ]), + ], + 'purchasables restriction, multiple line items' => [ + ['_lineItems' => ['rad-hood' => 1, 'hct-white' => 1]], + 2, + discountsTestCreateDiscounts([ + 'one' => ['allPurchasables' => false, 'purchasableIds' => ['rad-hood']], + 'many' => ['allPurchasables' => false, 'purchasableIds' => ['rad-hood', 'hct-white']], + 'no-match' => ['allPurchasables' => false, 'purchasableIds' => ['hct-blue']], + ]), + ], +]); + +// --------------------------------------------------------------------------------------------- +// appendCouponCode() +// --------------------------------------------------------------------------------------------- + +test('appendCouponCode adds string codes and Coupon models to a discount that requires a coupon code', function() { + $discount = new Discount([ + 'name' => 'Test Discount', + 'enabled' => true, + 'requireCouponCode' => true, + 'storeId' => app(Stores::class)->getPrimaryStore()->id, + 'perItemDiscount' => 10, + ]); + expect(app(Discounts::class)->saveDiscount($discount))->toBeTrue(); + + expect(app(Discounts::class)->appendCouponCode($discount->id, 'TESTCODE123', 5))->toBeTrue(); + + $coupons = app(Coupons::class)->getCouponsByDiscountId($discount->id); + expect($coupons)->toHaveCount(1); + expect($coupons[0]->code)->toBe('TESTCODE123'); + expect($coupons[0]->maxUses)->toBe(5); + expect($coupons[0]->uses)->toBe(0); + + expect(app(Discounts::class)->appendCouponCode($discount->id, 'TESTCODE456'))->toBeTrue(); + expect(app(Coupons::class)->getCouponsByDiscountId($discount->id))->toHaveCount(2); + + $couponModel = new Coupon(['code' => 'MODELCODE789', 'maxUses' => 10, 'uses' => 0]); + expect(app(Discounts::class)->appendCouponCode($discount->id, $couponModel))->toBeTrue(); + + $coupons = app(Coupons::class)->getCouponsByDiscountId($discount->id); + expect($coupons)->toHaveCount(3); + + $added = collect($coupons)->first(fn(Coupon $c) => $c->code === 'MODELCODE789'); + expect($added)->not->toBeNull(); + expect($added->maxUses)->toBe(10); + expect($added->uses)->toBe(0); +}); + +test('appendCouponCode throws when the discount does not require a coupon code', function() { + $discount = new Discount([ + 'name' => 'Test Discount No Coupon', + 'enabled' => true, + 'requireCouponCode' => false, + 'storeId' => app(Stores::class)->getPrimaryStore()->id, + 'perItemDiscount' => 10, + ]); + expect(app(Discounts::class)->saveDiscount($discount))->toBeTrue(); + + app(Discounts::class)->appendCouponCode($discount->id, 'SHOULDFAIL'); +})->throws(RuntimeException::class, 'does not require a coupon code'); + +test('appendCouponCode throws for an unknown discount ID', function() { + app(Discounts::class)->appendCouponCode(999999, 'SHOULDFAIL'); +})->throws(RuntimeException::class, 'No discount exists with the ID "999999"'); + +test('appendCouponCode returns false and leaves validation errors on the coupon model when it is invalid', function() { + $discount = new Discount([ + 'name' => 'Test Discount', + 'enabled' => true, + 'requireCouponCode' => true, + 'storeId' => app(Stores::class)->getPrimaryStore()->id, + 'perItemDiscount' => 10, + ]); + expect(app(Discounts::class)->saveDiscount($discount))->toBeTrue(); + + $couponModel = new Coupon(['code' => '', 'maxUses' => 10]); + $result = app(Discounts::class)->appendCouponCode($discount->id, $couponModel); + + expect($result)->toBeFalse(); + expect($couponModel->errors()->isEmpty())->toBeFalse(); + expect($couponModel->errors()->has('code'))->toBeTrue(); +}); diff --git a/tests/Feature/Promotion/SalesTest.php b/tests/Feature/Promotion/SalesTest.php new file mode 100644 index 0000000000..83d4e91fb9 --- /dev/null +++ b/tests/Feature/Promotion/SalesTest.php @@ -0,0 +1,122 @@ +getAllSales(); + + expect($sales)->toHaveCount(2); + + $firstSale = $sales[$fixture->percentageSale->id] ?? null; + expect($firstSale)->not->toBeNull(); + expect($firstSale->name)->toBe($fixture->percentageSale->name); + + expect(array_map(intval(...), $firstSale->getPurchasableIds()))->toBe([$fixture->radHood->id]); + expect($firstSale->getUserGroupIds())->toBe([]); +}); + +test('getSaleById returns a sale by ID, or null when none matches', function() { + $fixture = SalesFixture::seed(); + $sales = app(Sales::class); + + $sale = $sales->getSaleById($fixture->percentageSale->id); + expect($sale->name)->toBe($fixture->percentageSale->name); + + expect($sales->getSaleById(999999))->toBeNull(); +}); + +test('getSalesForPurchasable returns sales matching the purchasable', function() { + $fixture = SalesFixture::seed(); + $sales = app(Sales::class); + + $sale = $sales->getSaleById($fixture->percentageSale->id); + + expect($sales->getSalesForPurchasable($fixture->radHood))->toBe([$sale]); +}); + +test('getSalesRelatedToPurchasable returns sales related by purchasable ID', function() { + $fixture = SalesFixture::seed(); + $sales = app(Sales::class); + + $sale = $sales->getSaleById($fixture->allRelationshipsSale->id); + + expect($sales->getSalesRelatedToPurchasable($fixture->hctWhite))->toBe([$sale]); +}); + +test('getSalePriceForPurchasable applies the matching sale for the signed-in customer', function() { + $fixture = SalesFixture::seed(); + $sales = app(Sales::class); + + $this->actingAs($fixture->customer, 'craft'); + + $salePrice = $sales->getSalePriceForPurchasable($fixture->radHood); + expect($salePrice)->not->toBe($fixture->radHood->getPrice()); + expect($salePrice)->toBe(111.59); + + $salePrice = $sales->getSalePriceForPurchasable($fixture->hctWhite); + expect($salePrice)->not->toBe($fixture->hctWhite->getPrice()); + expect($salePrice)->toBe(15.99); +}); + +test('saveSale persists changes and bumps dateUpdated', function() { + $fixture = SalesFixture::seed(); + $sales = app(Sales::class); + + $sale = $sales->getSaleById($fixture->allRelationshipsSale->id); + $originalName = $sale->name; + $originalDateUpdated = DB::table(Table::SALES)->where('id', $sale->id)->value('dateUpdated'); + + $sale->name = 'CHANGED'; + + // Absolutely make sure enough time has passed. + sleep(1); + $saveResult = $sales->saveSale($sale); + $newDateUpdated = DB::table(Table::SALES)->where('id', $sale->id)->value('dateUpdated'); + + expect($sale->errors()->isEmpty())->toBeTrue(); + expect($saveResult)->toBeTrue(); + expect($sale->name)->not->toBe($originalName); + expect($sale->name)->toBe('CHANGED'); + expect($newDateUpdated)->toBeGreaterThan($originalDateUpdated); +}); + +test('reorderSales updates sortOrder, reflected immediately by getAllSales', function() { + SalesFixture::seed(); + $sales = app(Sales::class); + + $originalOrder = array_map(intval(...), array_keys($sales->getAllSales())); + $newOrder = array_reverse($originalOrder); + + expect($sales->reorderSales($newOrder))->toBeTrue(); + + $dbOrder = array_map( + intval(...), + DB::table(Table::SALES)->orderBy('sortOrder')->pluck('id')->all(), + ); + expect($dbOrder)->not->toBe($originalOrder); + expect($dbOrder)->toBe($newOrder); + + // Make sure the order has updated if we retrieve the sales again in the same request. + $newOrderFromGetSales = array_map(intval(...), array_keys($sales->getAllSales())); + expect($newOrderFromGetSales)->toBe($dbOrder); +}); + +test('deleteSaleById removes the sale and clears memoized lookups', function() { + $fixture = SalesFixture::seed(); + $sales = app(Sales::class); + + // Pre-fetch to exercise memoization before the delete. + $sales->getAllSales(); + + $id = $fixture->percentageSale->id; + expect($sales->deleteSaleById($id))->toBeTrue(); + + expect($sales->getSaleById($id))->toBeNull(); + expect(array_key_exists($id, $sales->getAllSales()))->toBeFalse(); +}); diff --git a/tests/Feature/Purchasable/Conditions/PurchasableConditionRuleTest.php b/tests/Feature/Purchasable/Conditions/PurchasableConditionRuleTest.php new file mode 100644 index 0000000000..78449eeb25 --- /dev/null +++ b/tests/Feature/Purchasable/Conditions/PurchasableConditionRuleTest.php @@ -0,0 +1,41 @@ +id() from inside modifyQuery(), + * which was silently non-functional for the same reason as SkuConditionRule (see + * tests/Feature/Purchasable/Conditions/SkuConditionRuleTest.php) — now fixed by calling + * CraftCms\Cms\Element\Queries\ElementQuery::applyId() directly. + */ +beforeEach(function() { + $this->fixture = ProductConditionsFixture::seed(); +}); + +test('modifyQuery filters variants by the selected purchasable ids', function() { + $rule = new PurchasableConditionRule(); + $rule->setElementIds([Variant::class => [$this->fixture->hoodieVariant->id]]); + + $condition = new CatalogPricingRulePurchasableCondition(Variant::class); + $condition->addConditionRule($rule); + + $query = Variant::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + expect($ids)->toContain($this->fixture->hoodieVariant->id); + expect($ids)->not->toContain($this->fixture->tShirtVariant->id); +}); + +test('matchElement matches an element with a selected id', function() { + $rule = new PurchasableConditionRule(); + $rule->setElementIds([Variant::class => [$this->fixture->hoodieVariant->id]]); + + expect($rule->matchElement($this->fixture->hoodieVariant))->toBeTrue(); + expect($rule->matchElement($this->fixture->tShirtVariant))->toBeFalse(); +}); diff --git a/tests/Feature/Purchasable/Conditions/SkuConditionRuleTest.php b/tests/Feature/Purchasable/Conditions/SkuConditionRuleTest.php new file mode 100644 index 0000000000..3a669423a4 --- /dev/null +++ b/tests/Feature/Purchasable/Conditions/SkuConditionRuleTest.php @@ -0,0 +1,45 @@ +sku() from inside modifyQuery(), which + * was silently non-functional until PurchasableQuery moved its sku/stock/etc scope filters into + * CraftCms\Commerce\Purchasable\Queries\Concerns\QueriesPurchasableAttributes — the constructor's + * own beforeQuery() callback always ran first and never saw the value set that late. + */ +function purchasableSkuCondition(string $value): VariantCondition +{ + $condition = Variant::createCondition(); + $rule = new SkuConditionRule(); + $rule->value = $value; + $condition->addConditionRule($rule); + + return $condition; +} + +beforeEach(function() { + $this->fixture = ProductConditionsFixture::seed(); +}); + +test('matchElement matches a variant with the given sku', function() { + $condition = purchasableSkuCondition($this->fixture->hoodieVariant->getSku()); + + expect($condition->matchElement($this->fixture->hoodieVariant))->toBeTrue(); +}); + +test('modifyQuery filters variants by sku', function() { + $condition = purchasableSkuCondition($this->fixture->hoodieVariant->getSku()); + + $query = Variant::find(); + $condition->modifyQuery($query); + $ids = $query->ids(); + + expect($ids)->toContain($this->fixture->hoodieVariant->id); + expect($ids)->not->toContain($this->fixture->tShirtVariant->id); +}); diff --git a/tests/Feature/Purchasable/DonationQueryTest.php b/tests/Feature/Purchasable/DonationQueryTest.php new file mode 100644 index 0000000000..1588cdab6a --- /dev/null +++ b/tests/Feature/Purchasable/DonationQueryTest.php @@ -0,0 +1,49 @@ +toBeInstanceOf(DonationQuery::class); + expect(Donation::find())->toBeInstanceOf(PurchasableQuery::class); +}); + +test('availableForPurchase filters by whether the donation purchasable can be bought', function(bool $availableForPurchase) { + $donation = null; + + // Make sure a donation purchasable exists to query against. + if (DB::table(Table::DONATIONS)->count() === 0) { + $primaryStore = app(Stores::class)->getPrimaryStore(); + $donation = new Donation(); + $donation->siteId = Sites::getPrimarySite()->id; + $donation->sku = 'DONATION-CC5'; + $donation->availableForPurchase = false; + $donation->setTaxCategoryId(app(TaxCategories::class)->getDefaultTaxCategory()->id); + $donation->setShippingCategoryId(app(ShippingCategories::class)->getDefaultShippingCategory($primaryStore->id)->id); + expect(Elements::saveElement($donation))->toBeTrue(); + } + + $query = Donation::find()->availableForPurchase($availableForPurchase)->status(null); + $all = $query->all(); + + // The donation purchasable above is never available for purchase. + expect($all)->toHaveCount($availableForPurchase ? 0 : 1); + + $toDelete = $donation ?? ($all[0] ?? null); + if ($toDelete !== null) { + Elements::deleteElement($toDelete, true); + } +})->with([ + 'available' => [true], + 'not-available' => [false], +]); diff --git a/tests/Feature/Purchasable/PurchasablesTest.php b/tests/Feature/Purchasable/PurchasablesTest.php new file mode 100644 index 0000000000..c1fe75d38e --- /dev/null +++ b/tests/Feature/Purchasable/PurchasablesTest.php @@ -0,0 +1,21 @@ +getPurchasableById($fixture->white->id); + expect($cached->maxQty)->toBeNull(); + + $fixture->white->maxQty = 3; + expect(Elements::saveElement($fixture->white))->toBeTrue(); + + $refetched = app(Purchasables::class)->getPurchasableById($fixture->white->id); + + expect($refetched->maxQty)->toBe(3); +}); diff --git a/tests/Feature/Shipping/ShippingCategoriesTest.php b/tests/Feature/Shipping/ShippingCategoriesTest.php new file mode 100644 index 0000000000..676a9d4e17 --- /dev/null +++ b/tests/Feature/Shipping/ShippingCategoriesTest.php @@ -0,0 +1,28 @@ +getPrimaryStore()->id; + + $shippingCategory = new ShippingCategory(); + $shippingCategory->storeId = $storeId; + $shippingCategory->name = 'Another Shipping Category'; + $shippingCategory->handle = 'anotherShippingCategory'; + if (!app(ShippingCategories::class)->saveShippingCategory($shippingCategory)) { + throw new RuntimeException('Could not save shipping category: ' . json_encode($shippingCategory->errors()->all())); + } + + expect($shippingCategory->default)->toBeFalse(); + + $result = app(ShippingCategories::class)->deleteShippingCategoryById($shippingCategory->id); + + expect($result)->toBeTrue() + ->and(app(ShippingCategories::class)->getShippingCategoryById($shippingCategory->id))->toBeNull() + ->and(ShippingCategoryRecord::withTrashed()->find($shippingCategory->id))->not->toBeNull(); +}); diff --git a/tests/Feature/Shipping/ShippingMethodsTest.php b/tests/Feature/Shipping/ShippingMethodsTest.php new file mode 100644 index 0000000000..6ebbba10c6 --- /dev/null +++ b/tests/Feature/Shipping/ShippingMethodsTest.php @@ -0,0 +1,37 @@ +shouldReceive('getHandle')->andReturn($handle); + $method->shouldReceive('getName')->andReturn($name); + $method->shouldReceive('getPriceForOrder')->andReturn($price); + $method->shouldReceive('getIsEnabled')->andReturn(true); + $method->shouldReceive('matchOrder')->andReturn(true); + + return $method; +} + +test('getMatchingShippingMethods sorts matching methods by price', function() { + $order = new Order(); + + Event::listen(RegisterAvailableShippingMethodsEvent::class, function(RegisterAvailableShippingMethodsEvent $event) { + $shippingMethods = $event->getShippingMethods(); + + $shippingMethods->push(mockShippingMethod('First', 'first', 12.34)); + $shippingMethods->push(mockShippingMethod('Second', 'second', 12.35)); + $shippingMethods->push(mockShippingMethod('Really First', 'reallyFirst', 12.33)); + }); + + $matchingMethods = app(ShippingMethods::class)->getMatchingShippingMethods($order); + + expect(array_keys($matchingMethods))->toBe(['reallyFirst', 'first', 'second']); +}); diff --git a/tests/Feature/Shipping/ShippingRulesTest.php b/tests/Feature/Shipping/ShippingRulesTest.php new file mode 100644 index 0000000000..fd8ef653e3 --- /dev/null +++ b/tests/Feature/Shipping/ShippingRulesTest.php @@ -0,0 +1,104 @@ +getPrimaryStore()->id; + + $extraCategory = new ShippingCategory(); + $extraCategory->storeId = $storeId; + $extraCategory->name = 'Extra Category'; + $extraCategory->handle = 'extraCategory'; + if (!app(ShippingCategories::class)->saveShippingCategory($extraCategory)) { + throw new RuntimeException('Could not save shipping category: ' . json_encode($extraCategory->errors()->all())); + } + + $method = new ShippingMethod(); + $method->storeId = $storeId; + $method->name = 'Test Shipping'; + $method->handle = 'testShipping'; + $method->enabled = true; + if (!app(ShippingMethods::class)->saveShippingMethod($method)) { + throw new RuntimeException('Could not save shipping method: ' . json_encode($method->errors()->all())); + } + + $ruleIds = []; + foreach (['Rule One', 'Rule Two'] as $name) { + $rule = new ShippingRule(); + $rule->storeId = $storeId; + $rule->methodId = $method->id; + $rule->name = $name; + if (!app(ShippingRules::class)->saveShippingRule($rule)) { + throw new RuntimeException('Could not save shipping rule: ' . json_encode($rule->errors()->all())); + } + $ruleIds[] = $rule->id; + } + + return $ruleIds; +} + +test('getShippingRuleCategoriesByRuleIds returns categories indexed by rule ID then category ID', function() { + $ruleIds = seedShippingRulesWithCategories(); + + $categoriesByRuleId = app(ShippingRuleCategories::class)->getShippingRuleCategoriesByRuleIds($ruleIds); + + expect(array_keys($categoriesByRuleId))->toEqualCanonicalizing($ruleIds); + + foreach ($categoriesByRuleId as $ruleId => $categories) { + expect($ruleIds)->toContain($ruleId) + ->and($categories)->toHaveCount(2); + + foreach ($categories as $categoryId => $ruleCategory) { + expect($ruleCategory->shippingCategoryId)->toBe($categoryId); + } + } +}); + +test('getShippingRuleCategoriesByRuleIds returns an empty array for an empty input', function() { + $result = app(ShippingRuleCategories::class)->getShippingRuleCategoriesByRuleIds([]); + + expect($result)->toBe([]); +}); + +test('getAllShippingRules eager loads each rule\'s shipping rule categories', function() { + $ruleIds = seedShippingRulesWithCategories(); + + $rules = app(ShippingRules::class)->getAllShippingRules()->whereIn('id', $ruleIds); + + expect($rules)->toHaveCount(2); + + foreach ($rules as $rule) { + expect($rule->getShippingRuleCategories())->toHaveCount(2); + } +}); + +test('bulk fetching shipping rule categories matches fetching them one rule at a time', function() { + $ruleIds = seedShippingRulesWithCategories(); + + $bulkCategories = app(ShippingRuleCategories::class)->getShippingRuleCategoriesByRuleIds($ruleIds); + + foreach ($ruleIds as $ruleId) { + $singleCategories = app(ShippingRuleCategories::class)->getShippingRuleCategoriesByRuleId($ruleId); + $bulkForRule = $bulkCategories[$ruleId] ?? []; + + expect(array_keys($singleCategories))->toEqualCanonicalizing(array_keys($bulkForRule)); + } +}); diff --git a/tests/Feature/Stats/AverageOrderTotalTest.php b/tests/Feature/Stats/AverageOrderTotalTest.php new file mode 100644 index 0000000000..5987ea6781 --- /dev/null +++ b/tests/Feature/Stats/AverageOrderTotalTest.php @@ -0,0 +1,36 @@ +fixture = OrdersFixture::seed(); +}); + +test('getData', function(string $dateRange, DateTime $startDate, DateTime $endDate, ?float $average) { + $stat = new AverageOrderTotal($dateRange, $startDate, $endDate, $this->fixture->storeId); + $data = $stat->get(); + + if ($average === null) { + expect($data)->toBeNull(); + } else { + expect((float) $data)->toBe($average); + } +})->with(function() { + return [ + [ + AverageOrderTotal::DATE_RANGE_TODAY, + new DateTime('now')->setTime(0, 0), + new DateTime('now')->setTime(0, 0), + 63.97, + ], + [ + AverageOrderTotal::DATE_RANGE_CUSTOM, + new DateTime('7 days ago')->setTime(0, 0), + new DateTime('5 days ago')->setTime(0, 0), + null, + ], + ]; +}); diff --git a/tests/Feature/Stats/NewCustomersTest.php b/tests/Feature/Stats/NewCustomersTest.php new file mode 100644 index 0000000000..7df9d5d597 --- /dev/null +++ b/tests/Feature/Stats/NewCustomersTest.php @@ -0,0 +1,37 @@ +fixture = OrdersFixture::seed(); +}); + +test('getData', function(string $dateRange, DateTime $startDate, DateTime $endDate, ?float $count) { + $stat = new NewCustomers($dateRange, $startDate, $endDate, $this->fixture->storeId); + $data = $stat->get(); + + expect($data)->toBeNumeric(); + expect((float) $data)->toBe($count); +})->with([ + [ + NewCustomers::DATE_RANGE_CUSTOM, + new DateTime('2 days ago')->setTime(0, 0), + new DateTime('0 days ago')->setTime(0, 0), + 1.0, + ], + [ + NewCustomers::DATE_RANGE_TODAY, + new DateTime('now')->setTime(0, 0), + new DateTime('now')->setTime(0, 0), + 0.0, + ], + [ + NewCustomers::DATE_RANGE_CUSTOM, + new DateTime('7 days ago')->setTime(0, 0), + new DateTime('5 days ago')->setTime(0, 0), + 0.0, + ], +]); diff --git a/tests/Feature/Stats/RepeatCustomersTest.php b/tests/Feature/Stats/RepeatCustomersTest.php new file mode 100644 index 0000000000..c8bc233d81 --- /dev/null +++ b/tests/Feature/Stats/RepeatCustomersTest.php @@ -0,0 +1,37 @@ +fixture = OrdersFixture::seed(); +}); + +test('getData', function(string $dateRange, DateTime $startDate, DateTime $endDate, int $total, int $repeat, int $percentage) { + $stat = new RepeatCustomers($dateRange, $startDate, $endDate, $this->fixture->storeId); + $data = $stat->get(); + + expect($data)->toBeArray(); + expect($data['total'])->toBe($total); + expect($data['repeat'])->toBe($repeat); + expect((int) $data['percentage'])->toBe($percentage); +})->with([ + [ + RepeatCustomers::DATE_RANGE_TODAY, + new DateTime('now')->setTime(0, 0), + new DateTime('now')->setTime(0, 0), + 1, + 1, + 100, + ], + [ + RepeatCustomers::DATE_RANGE_CUSTOM, + new DateTime('7 days ago')->setTime(0, 0), + new DateTime('5 days ago')->setTime(0, 0), + 0, + 0, + 0, + ], +]); diff --git a/tests/Feature/Stats/StatTest.php b/tests/Feature/Stats/StatTest.php new file mode 100644 index 0000000000..3616483a34 --- /dev/null +++ b/tests/Feature/Stats/StatTest.php @@ -0,0 +1,120 @@ +createChartQuery(); + } + }; +} + +test('instantiating with a date range populates the chart with both endpoints', function(string $dateRange, DateTime $startDate, DateTime $endDate) { + $storeId = app(Stores::class)->getPrimaryStore()->id; + $stat = createStatClass($dateRange, $startDate, $endDate, $storeId); + + $data = $stat->get(); + + expect($data)->toHaveKey($startDate->format('Y-m-d')); + expect($data)->toHaveKey($endDate->format('Y-m-d')); + expect($data)->toHaveCount(2); +})->with('instantiateDatesDataProvider'); + +test('predefined date ranges produce a chart bucket for every day/month in range', function(string $dateRange, DateTime $startDate, DateTime $endDate, int $keysCount, bool $keyedByDays = true) { + $format = $keyedByDays ? 'Y-m-d' : 'Y-n'; + $storeId = app(Stores::class)->getPrimaryStore()->id; + $stat = createStatClass($dateRange, $startDate, $endDate, $storeId); + + $data = $stat->get(); + + while ($startDate <= $endDate) { + expect($data)->toHaveKey($startDate->format($format)); + + if ($keyedByDays) { + $startDate->add(new DateInterval('P1D')); + } else { + $startDate->add(new DateInterval('P1M')); + } + } + + expect($data)->toHaveCount($keysCount); +})->with('predefinedDateRangesDataProvider'); + +dataset('instantiateDatesDataProvider', function() { + $tz = new DateTimeZone('America/Los_Angeles'); + + return [ + [ + StatInterface::DATE_RANGE_CUSTOM, + new DateTime('yesterday', $tz)->setTime(0, 0), + new DateTime('now', $tz)->setTime(0, 0), + ], + ]; +}); + +dataset('predefinedDateRangesDataProvider', function() { + $tz = new DateTimeZone('America/Los_Angeles'); + $today = new DateTime('now', $tz)->setTime(0, 0); + + return [ + StatInterface::DATE_RANGE_TODAY => [ + StatInterface::DATE_RANGE_TODAY, + clone $today, + clone $today, + 1, + ], + StatInterface::DATE_RANGE_PAST7DAYS => [ + StatInterface::DATE_RANGE_PAST7DAYS, + new DateTime('6 days ago', $tz)->setTime(0, 0), + clone $today, + 7, + ], + StatInterface::DATE_RANGE_PAST30DAYS => [ + StatInterface::DATE_RANGE_PAST30DAYS, + new DateTime('29 days ago', $tz)->setTime(0, 0), + clone $today, + 30, + ], + StatInterface::DATE_RANGE_PAST90DAYS => [ + StatInterface::DATE_RANGE_PAST90DAYS, + new DateTime('89 days ago', $tz)->setTime(0, 0), + clone $today, + 90, + ], + StatInterface::DATE_RANGE_PASTYEAR => [ + StatInterface::DATE_RANGE_PASTYEAR, + new DateTime('11 months ago', $tz)->setTime(0, 0), + clone $today, + 12, + false, + ], + StatInterface::DATE_RANGE_THISMONTH => [ + StatInterface::DATE_RANGE_THISMONTH, + new DateTime('now', $tz)->setDate((int) $today->format('Y'), (int) $today->format('n'), 1)->setTime(0, 0), + clone $today, + (int) $today->format('t'), + ], + StatInterface::DATE_RANGE_THISWEEK => [ + StatInterface::DATE_RANGE_THISWEEK, + new DateTime('Monday this week', $tz)->setTime(0, 0), + clone $today, + 7, + ], + StatInterface::DATE_RANGE_THISYEAR => [ + StatInterface::DATE_RANGE_THISYEAR, + new DateTime('first day of January ' . $today->format('Y'), $tz)->setTime(0, 0), + clone $today, + (int) ($today->diff(new DateTime('first day of January ' . $today->format('Y'), $tz)->setTime(0, 0))->format('%m')) + 1, + false, + ], + ]; +}); diff --git a/tests/Feature/Stats/TopCustomersTest.php b/tests/Feature/Stats/TopCustomersTest.php new file mode 100644 index 0000000000..8acef15ba8 --- /dev/null +++ b/tests/Feature/Stats/TopCustomersTest.php @@ -0,0 +1,54 @@ +fixture = OrdersFixture::seed(); +}); + +test('getData', function(string $dateRange, string $type, DateTime $startDate, DateTime $endDate, int $count, ?Closure $customerData) { + $stat = new TopCustomers($dateRange, $type, $startDate, $endDate, $this->fixture->storeId); + $data = $stat->get(); + + expect($data)->toBeArray(); + expect($data)->toHaveCount($count); + + if ($count !== 0) { + $topCustomer = array_shift($data); + $expected = $customerData($this->fixture); + + foreach (['total', 'average', 'customerId', 'email', 'count'] as $key) { + expect($topCustomer)->toHaveKey($key); + expect($topCustomer[$key])->toBe($expected[$key]); + } + + expect($topCustomer['customer'])->toBeInstanceOf(User::class); + } +})->with([ + [ + TopCustomers::DATE_RANGE_TODAY, + 'total', + new DateTime('now')->setTime(0, 0), + new DateTime('now')->setTime(0, 0), + 1, + fn(OrdersFixture $fixture) => [ + 'total' => 127.94, + 'average' => 63.97, + 'customerId' => $fixture->customer->id, + 'email' => $fixture->customer->email, + 'count' => 2, + ], + ], + [ + TopCustomers::DATE_RANGE_CUSTOM, + 'total', + new DateTime('7 days ago')->setTime(0, 0), + new DateTime('5 days ago')->setTime(0, 0), + 0, + null, + ], +]); diff --git a/tests/Feature/Stats/TopProductTypesTest.php b/tests/Feature/Stats/TopProductTypesTest.php new file mode 100644 index 0000000000..f4540cc440 --- /dev/null +++ b/tests/Feature/Stats/TopProductTypesTest.php @@ -0,0 +1,64 @@ +fixture = OrdersFixture::seed(); + + $admin = User::find()->admin(true)->one(); + $this->actingAs($admin, 'craft'); + // `actingAs()` doesn't retroactively update the already-bound `request()` singleton's user + // resolver in this Testbench setup, and `getViewableProductTypeIds()` reads the current user + // via `request()->craftUser()` rather than the `Auth` facade. + request()->setUserResolver(fn() => $admin); +}); + +test('getData', function(string $dateRange, string $type, DateTime $startDate, DateTime $endDate, int $count, ?array $productTypeData) { + $stat = new TopProductTypes($dateRange, $type, $startDate, $endDate, $this->fixture->storeId); + $data = $stat->get(); + + expect($data)->toBeArray(); + expect($data)->toHaveCount($count); + + if ($count !== 0) { + $topProductType = array_shift($data); + + expect($topProductType['id'])->toBe($this->fixture->product->typeId); + + foreach (['name', 'qty', 'revenue'] as $key) { + expect($topProductType)->toHaveKey($key); + expect($topProductType[$key])->toBe($productTypeData[$key]); + } + + expect($topProductType['productType'])->toBeInstanceOf(ProductType::class); + } +})->with(function() { + return [ + [ + TopProducts::DATE_RANGE_TODAY, + 'revenue', + new DateTime('now')->setTime(0, 0), + new DateTime('now')->setTime(0, 0), + 1, + [ + 'name' => 'T-Shirts', + 'qty' => 6, + 'revenue' => 127.94, + ], + ], + [ + TopProducts::DATE_RANGE_CUSTOM, + 'revenue', + new DateTime('7 days ago')->setTime(0, 0), + new DateTime('5 days ago')->setTime(0, 0), + 0, + null, + ], + ]; +}); diff --git a/tests/Feature/Stats/TopProductsTest.php b/tests/Feature/Stats/TopProductsTest.php new file mode 100644 index 0000000000..e0cd157cae --- /dev/null +++ b/tests/Feature/Stats/TopProductsTest.php @@ -0,0 +1,53 @@ +fixture = OrdersFixture::seed(); +}); + +test('getData', function(string $dateRange, string $type, DateTime $startDate, DateTime $endDate, int $count, ?Closure $productData) { + $stat = new TopProducts($dateRange, $type, $startDate, $endDate, storeId: $this->fixture->storeId); + $data = $stat->get(); + + expect($data)->toBeArray(); + expect($data)->toHaveCount($count); + + if ($count !== 0) { + $topProduct = array_shift($data); + $expected = $productData($this->fixture); + + foreach (['id', 'title', 'qty', 'revenue'] as $key) { + expect($topProduct)->toHaveKey($key); + expect($topProduct[$key])->toBe($expected[$key]); + } + + expect($topProduct['product'])->toBeInstanceOf(Product::class); + } +})->with([ + [ + TopProducts::DATE_RANGE_TODAY, + 'revenue', + new DateTime('now')->setTime(0, 0), + new DateTime('now')->setTime(0, 0), + 1, + fn(OrdersFixture $fixture) => [ + 'id' => $fixture->product->id, + 'title' => 'Hypercolor T-Shirt', + 'qty' => 6, + 'revenue' => 127.94, + ], + ], + [ + TopProducts::DATE_RANGE_CUSTOM, + 'revenue', + new DateTime('7 days ago')->setTime(0, 0), + new DateTime('5 days ago')->setTime(0, 0), + 0, + null, + ], +]); diff --git a/tests/Feature/Stats/TopPurchasablesTest.php b/tests/Feature/Stats/TopPurchasablesTest.php new file mode 100644 index 0000000000..18d9124d96 --- /dev/null +++ b/tests/Feature/Stats/TopPurchasablesTest.php @@ -0,0 +1,59 @@ +fixture = OrdersFixture::seed(); + + $admin = User::find()->admin(true)->one(); + $this->actingAs($admin, 'craft'); + // `actingAs()` doesn't retroactively update the already-bound `request()` singleton's user + // resolver in this Testbench setup, and `getViewableProductTypeIds()` reads the current user + // via `request()->craftUser()` rather than the `Auth` facade. + request()->setUserResolver(fn() => $admin); +}); + +test('getData', function(string $dateRange, string $type, DateTime $startDate, DateTime $endDate, int $count, ?Closure $purchasableData) { + $stat = new TopPurchasables($dateRange, $type, $startDate, $endDate, $this->fixture->storeId); + $data = $stat->get(); + + expect($data)->toBeArray(); + expect($data)->toHaveCount($count); + + if ($count !== 0) { + $topPurchasable = array_shift($data); + $expected = $purchasableData($this->fixture); + + foreach (['purchasableId', 'description', 'sku', 'qty', 'revenue'] as $key) { + expect($topPurchasable)->toHaveKey($key); + expect($topPurchasable[$key])->toBe($expected[$key]); + } + } +})->with([ + 'date-today' => [ + TopPurchasables::DATE_RANGE_TODAY, + 'revenue', + new DateTime('now')->setTime(0, 0), + new DateTime('now')->setTime(0, 0), + 2, + fn(OrdersFixture $fixture) => [ + 'purchasableId' => $fixture->blue->id, + 'description' => $fixture->blue->getDescription(), + 'sku' => 'hct-blue', + 'qty' => 4, + 'revenue' => 87.96, + ], + ], + 'date-custom' => [ + TopPurchasables::DATE_RANGE_CUSTOM, + 'qty', + new DateTime('7 days ago')->setTime(0, 0), + new DateTime('5 days ago')->setTime(0, 0), + 0, + null, + ], +]); diff --git a/tests/Feature/Stats/TotalOrdersByCountryTest.php b/tests/Feature/Stats/TotalOrdersByCountryTest.php new file mode 100644 index 0000000000..d6e79701ec --- /dev/null +++ b/tests/Feature/Stats/TotalOrdersByCountryTest.php @@ -0,0 +1,48 @@ +fixture = OrdersFixture::seed(); +}); + +test('getData', function(string $dateRange, string $type, DateTime $startDate, DateTime $endDate, int $count, array $countryData) { + $stat = new TotalOrdersByCountry($dateRange, $type, $startDate, $endDate, $this->fixture->storeId); + $data = $stat->get(); + + expect($data)->toBeArray(); + expect($data)->toHaveCount($count); + + if ($count !== 0) { + $firstItem = array_shift($data); + + foreach ($countryData as $key => $value) { + expect($firstItem)->toHaveKey($key); + expect($firstItem[$key])->toBe($value); + } + } +})->with([ + [ + TotalOrdersByCountry::DATE_RANGE_TODAY, + 'shipping', + new DateTime('now')->setTime(0, 0), + new DateTime('now')->setTime(0, 0), + 1, + [ + 'total' => 2, + 'name' => 'United States', + 'countryCode' => 'US', + ], + ], + [ + TotalOrdersByCountry::DATE_RANGE_CUSTOM, + 'shipping', + new DateTime('7 days ago')->setTime(0, 0), + new DateTime('5 days ago')->setTime(0, 0), + 0, + [], + ], +]); diff --git a/tests/Feature/Stats/TotalOrdersTest.php b/tests/Feature/Stats/TotalOrdersTest.php new file mode 100644 index 0000000000..8b50a9fdba --- /dev/null +++ b/tests/Feature/Stats/TotalOrdersTest.php @@ -0,0 +1,54 @@ +fixture = OrdersFixture::seed(); +}); + +test('getData', function(string $case) { + // Computed here, after the app has booted and pinned its timezone (see TestCase::setUp()), + // rather than in the ->with() dataset — datasets are resolved before beforeEach()/app boot, + // so a `new DateTime('now')` captured there can land on a different calendar date than one + // computed here (and than the one TotalOrders computes internally), depending on how far the + // pre-boot default PHP timezone is from the app's pinned one. + $now = new DateTime(); + + [$dateRange, $startDate, $endDate, $total, $daysDiff] = match ($case) { + 'today' => [ + TotalOrders::DATE_RANGE_TODAY, + (clone $now)->setTime(0, 0), + (clone $now)->setTime(0, 0), + 2, + 1, + ], + 'custom' => [ + TotalOrders::DATE_RANGE_CUSTOM, + (clone $now)->modify('-7 days')->setTime(0, 0), + (clone $now)->modify('-5 days')->setTime(0, 0), + 0, + 3, + ], + }; + + $stat = new TotalOrders($dateRange, $startDate, $endDate, $this->fixture->storeId); + $data = $stat->get(); + + expect($data)->toBeArray(); + expect($data)->toHaveKey('total'); + expect($data['total'])->toBe($total); + expect($data)->toHaveKey('chart'); + expect($data['chart'])->toBeArray(); + expect($data['chart'])->toHaveKey($startDate->format('Y-m-d')); + expect($data['chart'])->toHaveKey($endDate->format('Y-m-d')); + expect($data['chart'])->toHaveCount($daysDiff); + + $firstItem = array_shift($data['chart']); + expect($firstItem)->toHaveKey('total'); + expect($firstItem)->toHaveKey('datekey'); + expect($firstItem['datekey'])->toBe($startDate->format('Y-m-d')); + expect($firstItem['total'])->toBe($total); +})->with(['today', 'custom']); diff --git a/tests/Feature/Stats/TotalRevenueTest.php b/tests/Feature/Stats/TotalRevenueTest.php new file mode 100644 index 0000000000..eb9c3e94ea --- /dev/null +++ b/tests/Feature/Stats/TotalRevenueTest.php @@ -0,0 +1,42 @@ +fixture = OrdersFixture::seed(); +}); + +test('getData', function(string $dateRange, DateTime $startDate, DateTime $endDate, int $count, float $revenue, string $type) { + $stat = new TotalRevenue($dateRange, $startDate, $endDate, $this->fixture->storeId); + $stat->type = $type; + $data = $stat->get(); + + expect($data)->toBeArray(); + + $todaysStats = array_pop($data); + expect($todaysStats)->toHaveKey('count'); + expect($todaysStats)->toHaveKey('revenue'); + expect($todaysStats)->toHaveKey('datekey'); + expect($todaysStats['count'])->toBe($count); + expect((float) $todaysStats['revenue'])->toBe($revenue); +})->with([ + [ + TotalRevenue::DATE_RANGE_TODAY, + new DateTime('now')->setTime(0, 0), + new DateTime('now')->setTime(0, 0), + 2, + 127.94, + TotalRevenue::TYPE_TOTAL, + ], + [ + TotalRevenue::DATE_RANGE_TODAY, + new DateTime('now')->setTime(0, 0), + new DateTime('now')->setTime(0, 0), + 2, + 0.0, + TotalRevenue::TYPE_TOTAL_PAID, + ], +]); diff --git a/tests/Feature/Store/Data/StoreSettingsTest.php b/tests/Feature/Store/Data/StoreSettingsTest.php new file mode 100644 index 0000000000..323f9a6e7f --- /dev/null +++ b/tests/Feature/Store/Data/StoreSettingsTest.php @@ -0,0 +1,15 @@ +getPrimaryStore(); + $address = $store->getSettings()->getLocationAddress(); + + expect($address)->toBeInstanceOf(Address::class) + ->and($address->countryCode)->toBe('US') + ->and($address->title)->toBe('Store'); +}); diff --git a/tests/Feature/Store/StoresTest.php b/tests/Feature/Store/StoresTest.php new file mode 100644 index 0000000000..e91ca40ba8 --- /dev/null +++ b/tests/Feature/Store/StoresTest.php @@ -0,0 +1,85 @@ +setAccessible(true); + $originalValue = $prop->getValue($projectConfig); + + $prop->setValue($projectConfig, $isApplying); + + try { + $callback(); + } finally { + $prop->setValue($projectConfig, $originalValue); + } +} + +test('getAllStores returns every store, with exactly one primary', function() { + $fixture = StoresFixture::seed(); + $stores = app(Stores::class)->getAllStores(); + + expect($stores)->toBeInstanceOf(Collection::class) + ->and($stores)->toHaveCount(3) + ->and($stores->firstWhere('primary', true)->handle)->toBe('primary') + ->and($stores->where('primary', false)->all())->toHaveCount(2); +}); + +test('getStoreBySiteId returns the store mapped to a site, and null for an unmapped site', function() { + $fixture = StoresFixture::seed(); + $stores = app(Stores::class); + + expect($stores->getStoreBySiteId($fixture->usSite->id)?->handle)->toBe('primary') + ->and($stores->getStoreBySiteId($fixture->euSite->id)?->handle)->toBe('euStore') + ->and($stores->getStoreBySiteId($fixture->ukSite->id)?->handle)->toBe('ukStore') + ->and($stores->getStoreBySiteId(999999))->toBeNull(); +}); + +test('afterSaveCraftSiteHandler skips creating a mapping while applying external changes', function() { + // While a project config apply (e.g. `craft up`) is in progress, the incoming sitestores + // config is responsible for creating the mapping via handleChangedSiteStore(). If + // afterSaveCraftSiteHandler() also created one here, it would assign the wrong (primary) + // store and trigger an unwanted project config write. + $fixture = StoresFixture::seed(); + $site = $fixture->ukSite; + + // Remove the fixture's existing mapping so the handler would normally recreate one. + SiteStoreRecord::where('siteId', $site->id)->delete(); + + withApplyingExternalChanges(true, function() use ($site) { + app(Stores::class)->afterSaveCraftSiteHandler(new SiteSaved(site: $site)); + }); + + expect(SiteStoreRecord::where('siteId', $site->id)->first())->toBeNull(); +}); + +test('afterSaveCraftSiteHandler creates a mapping to the primary store outside of an apply', function() { + $fixture = StoresFixture::seed(); + $site = $fixture->ukSite; + + SiteStoreRecord::where('siteId', $site->id)->delete(); + + withApplyingExternalChanges(false, function() use ($site) { + app(Stores::class)->afterSaveCraftSiteHandler(new SiteSaved(site: $site)); + }); + + $siteStore = SiteStoreRecord::where('siteId', $site->id)->first(); + + expect($siteStore)->not->toBeNull() + ->and($siteStore->storeId)->toBe($fixture->primaryStore->id); +}); diff --git a/tests/Feature/Tax/TaxCategoriesTest.php b/tests/Feature/Tax/TaxCategoriesTest.php new file mode 100644 index 0000000000..40d2ca01a3 --- /dev/null +++ b/tests/Feature/Tax/TaxCategoriesTest.php @@ -0,0 +1,27 @@ +whiteVariant->getTaxCategory()->id; + + $result = app(TaxCategories::class)->deleteTaxCategoryById($taxCategoryId); + + expect($result)->toBeTrue() + ->and(TaxCategoryRecord::find($taxCategoryId))->toBeNull() + ->and(TaxCategoryRecord::onlyTrashed()->where('id', $taxCategoryId)->first())->toBeInstanceOf(TaxCategoryRecord::class); +}); + +test('deleteTaxCategoryById refuses to delete the default tax category', function() { + $default = app(TaxCategories::class)->getDefaultTaxCategory(); + + $result = app(TaxCategories::class)->deleteTaxCategoryById($default->id); + + expect($result)->toBeFalse() + ->and(TaxCategoryRecord::find($default->id))->not->toBeNull(); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000000..fa64dba2ad --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,12 @@ +in('Feature'); +uses(UnitTestCase::class)->in('Unit'); diff --git a/tests/Support/CartsFixture.php b/tests/Support/CartsFixture.php new file mode 100644 index 0000000000..3889fad0c0 --- /dev/null +++ b/tests/Support/CartsFixture.php @@ -0,0 +1,97 @@ +build(); + + return $fixture; + } + + private function build(): void + { + $inactiveUser = new User(); + $inactiveUser->username = 'cart-inactive-user'; + $inactiveUser->email = 'cart-inactive-user@crafttest.com'; + $inactiveUser->firstName = 'Inactive'; + $inactiveUser->lastName = 'User'; + $inactiveUser->active = false; + if (!Elements::saveElement($inactiveUser)) { + throw new RuntimeException('Could not save inactive user: ' . json_encode($inactiveUser->errors()->all())); + } + $this->inactiveUser = $inactiveUser; + $this->savePrimaryAddress($inactiveUser); + + $credentialedUser = new User(); + $credentialedUser->username = 'cart-credentialed-user'; + $credentialedUser->email = 'cart-credentialed-user@crafttest.com'; + $credentialedUser->firstName = 'Credentialed'; + $credentialedUser->lastName = 'User'; + $credentialedUser->active = true; + if (!Elements::saveElement($credentialedUser)) { + throw new RuntimeException('Could not save credentialed user: ' . json_encode($credentialedUser->errors()->all())); + } + $this->credentialedUser = $credentialedUser; + $this->credentialedUserAddressId = $this->savePrimaryAddress($credentialedUser); + + $loadingUser = new User(); + $loadingUser->username = 'cart-loading-user'; + $loadingUser->email = 'cart-loading-user@crafttest.com'; + $loadingUser->firstName = 'Loading'; + $loadingUser->lastName = 'User'; + $loadingUser->active = true; + if (!Elements::saveElement($loadingUser)) { + throw new RuntimeException('Could not save loading user: ' . json_encode($loadingUser->errors()->all())); + } + $this->loadingUser = $loadingUser; + } + + private function savePrimaryAddress(User $user): int + { + $address = new Address(); + $address->setPrimaryOwner($user); + $address->title = $user->firstName . ' ' . $user->lastName; + $address->firstName = $user->firstName; + $address->lastName = $user->lastName; + $address->addressLine1 = '23 Woodworth'; + $address->locality = 'County Island'; + $address->postalCode = '12345'; + $address->countryCode = 'US'; + $address->administrativeArea = 'NY'; + if (!Elements::saveElement($address)) { + throw new RuntimeException('Could not save address: ' . json_encode($address->errors()->all())); + } + + app(Customers::class)->savePrimaryShippingAddressId($user, $address->id); + app(Customers::class)->savePrimaryBillingAddressId($user, $address->id); + + return $address->id; + } +} diff --git a/tests/Support/CatalogPricingFixture.php b/tests/Support/CatalogPricingFixture.php new file mode 100644 index 0000000000..398d2a1824 --- /dev/null +++ b/tests/Support/CatalogPricingFixture.php @@ -0,0 +1,148 @@ +build(); + + return $fixture; + } + + private function build(): void + { + $this->stores = StoresFixture::seed(); + + $allSiteIds = [$this->stores->usSite->id, $this->stores->euSite->id, $this->stores->ukSite->id]; + + $this->hoodiesType = $this->createProductType('hoodies', 'Hoodies', $allSiteIds); + $this->tShirtsType = $this->createProductType('tShirts', 'T-Shirts', $allSiteIds); + $this->ukOnlyType = $this->createProductType('ukOnly', 'UK Only Product Type', [$this->stores->ukSite->id]); + + [$this->hoodie, $this->radHood] = $this->createProductAndVariant( + $this->hoodiesType, + 'Rad Hoodie', + 'rad-hood', + 123.99, + $this->stores->usSite->id, + ); + + $this->tShirt = $this->createProduct($this->tShirtsType, 'Hypercolor T-Shirt', $this->stores->usSite->id); + $this->hctWhite = $this->createVariant($this->tShirt, 'White', 'hct-white', 19.99, isDefault: true); + $this->hctBlue = $this->createVariant($this->tShirt, 'Blue', 'hct-blue', 21.99, isDefault: false); + + [$this->bus, $this->ddbRed] = $this->createProductAndVariant( + $this->ukOnlyType, + 'Double Decker Bus Toy', + 'ddb-red', + 24.99, + $this->stores->ukSite->id, + ); + } + + /** @param int[] $siteIds */ + private function createProductType(string $handle, string $name, array $siteIds): ProductType + { + $productType = new ProductType(); + $productType->name = $name; + $productType->handle = $handle; + $productType->hasVariantTitleField = false; + $productType->variantTitleFormat = '{product.title}'; + + $siteSettings = []; + foreach ($siteIds as $siteId) { + $settings = new ProductTypeSite(); + $settings->siteId = $siteId; + $settings->hasUrls = false; + $settings->enabledByDefault = true; + $siteSettings[$siteId] = $settings; + } + $productType->setSiteSettings($siteSettings); + + if (!app(ProductTypes::class)->saveProductType($productType)) { + throw new RuntimeException('Could not save product type: ' . json_encode($productType->errors()->all())); + } + + return $productType; + } + + /** @return array{0: Product, 1: Variant} */ + private function createProductAndVariant(ProductType $productType, string $title, string $sku, float $price, int $siteId): array + { + $product = $this->createProduct($productType, $title, $siteId); + $variant = $this->createVariant($product, $title, $sku, $price, isDefault: true); + + return [$product, $variant]; + } + + private function createProduct(ProductType $productType, string $title, int $siteId): Product + { + $product = new Product(); + $product->typeId = $productType->id; + $product->title = $title; + $product->enabled = true; + $product->siteId = $siteId; + if (!Elements::saveElement($product)) { + throw new RuntimeException('Could not save product: ' . json_encode($product->errors()->all())); + } + + return Product::find()->id($product->id)->siteId($siteId)->one(); + } + + private function createVariant(Product $product, string $title, string $sku, float $price, bool $isDefault): Variant + { + $variant = new Variant(); + $variant->title = $title; + $variant->setPrimaryOwner($product); + $variant->setSku($sku); + $variant->setBasePrice($price); + $variant->isDefault = $isDefault; + $variant->promotable = true; + $variant->siteId = $product->siteId; + if (!Elements::saveElement($variant)) { + throw new RuntimeException('Could not save variant: ' . json_encode($variant->errors()->all())); + } + + return Variant::find()->id($variant->id)->siteId($product->siteId)->one(); + } +} diff --git a/tests/Support/CommerceActionRoutes.php b/tests/Support/CommerceActionRoutes.php new file mode 100644 index 0000000000..b8c1941a7f --- /dev/null +++ b/tests/Support/CommerceActionRoutes.php @@ -0,0 +1,54 @@ +getRoutes()->getRoutes() as $route) { + /** @var Route $route */ + $controller = $route->getAction('controller'); + + // Some routes are registered with a leading `\` on the controller's FQCN and some + // aren't (depends on whether the array callable passed to Route::get()/post() etc. used + // a `::class` reference or a literal string), so normalize it away before comparing. + if (is_string($controller) && str_starts_with(ltrim($controller, '\\'), 'CraftCms\\Commerce\\')) { + $commerceRoutes[] = $route; + } else { + $otherRoutes[] = $route; + } + } + + $reordered = new RouteCollection(); + + foreach ([...$commerceRoutes, ...$otherRoutes] as $route) { + $reordered->add($route); + } + + $router->setRoutes($reordered); +} diff --git a/tests/Support/DatabaseLock.php b/tests/Support/DatabaseLock.php new file mode 100644 index 0000000000..ae5c2f6fb4 --- /dev/null +++ b/tests/Support/DatabaseLock.php @@ -0,0 +1,59 @@ + self::release()); + } + + private static function release(): void + { + if (self::$lockHandle === null) { + return; + } + + flock(self::$lockHandle, LOCK_UN); + fclose(self::$lockHandle); + + self::$lockHandle = null; + } + + private static function lockFile(): string + { + $workspaceRoot = dirname(__DIR__, 2); + $workspaceHash = md5($workspaceRoot); + $temporaryDirectory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR); + + return "$temporaryDirectory/commerce-database-$workspaceHash.lock"; + } +} diff --git a/tests/Support/DiscountsFixture.php b/tests/Support/DiscountsFixture.php new file mode 100644 index 0000000000..70928c2d1a --- /dev/null +++ b/tests/Support/DiscountsFixture.php @@ -0,0 +1,183 @@ +build(); + + return $fixture; + } + + private function build(): void + { + $site = Sites::getCurrentSite(); + $this->storeId = app(Stores::class)->getPrimaryStore()->id; + + $hoodiesType = $this->createProductType('discountsHoodies', 'Hoodies', $site->id); + [$this->hoodie, $this->radHood] = $this->createProductAndVariant($hoodiesType, 'Rad Hoodie', 'rad-hood', 123.99, $site->id); + + $tShirtsType = $this->createProductType('discountsTShirts', 'T-Shirts', $site->id); + $this->tShirt = $this->createProduct($tShirtsType, 'Hypercolor T-Shirt', $site->id); + $this->hctWhite = $this->createVariant($this->tShirt, 'White', 'hct-white', 19.99, $site->id, isDefault: true); + $this->hctBlue = $this->createVariant($this->tShirt, 'Blue', 'hct-blue', 21.99, $site->id, isDefault: false); + + $this->customer = $this->createCustomer(); + + $this->discountWithCoupon = $this->createDiscount([ + 'name' => 'Discount 1', + 'storeId' => $this->storeId, + 'perUserLimit' => 1, + 'totalDiscountUseLimit' => 2, + 'baseDiscount' => 10, + 'perItemDiscount' => 5, + 'percentDiscount' => 15.25, + 'enabled' => true, + 'allCategories' => true, + 'allPurchasables' => true, + 'percentageOffSubject' => 'original', + 'requireCouponCode' => true, + ], coupons: [new Coupon(['code' => 'discount_1', 'uses' => 0, 'maxUses' => null])]); + } + + private function createProductType(string $handle, string $name, int $siteId): ProductType + { + $productType = new ProductType(); + $productType->name = $name; + $productType->handle = $handle; + $productType->hasVariantTitleField = false; + $productType->variantTitleFormat = '{product.title}'; + + $siteSettings = new ProductTypeSite(); + $siteSettings->siteId = $siteId; + $siteSettings->hasUrls = false; + $siteSettings->enabledByDefault = true; + $productType->setSiteSettings([$siteId => $siteSettings]); + + if (!app(ProductTypes::class)->saveProductType($productType)) { + throw new RuntimeException('Could not save product type: ' . json_encode($productType->errors()->all())); + } + + return $productType; + } + + /** @return array{0: Product, 1: Variant} */ + private function createProductAndVariant(ProductType $productType, string $title, string $sku, float $price, int $siteId): array + { + $product = $this->createProduct($productType, $title, $siteId); + $variant = $this->createVariant($product, $title, $sku, $price, $siteId, isDefault: true); + + return [$product, $variant]; + } + + private function createProduct(ProductType $productType, string $title, int $siteId): Product + { + $product = new Product(); + $product->typeId = $productType->id; + $product->title = $title; + $product->enabled = true; + $product->siteId = $siteId; + if (!Elements::saveElement($product)) { + throw new RuntimeException('Could not save product: ' . json_encode($product->errors()->all())); + } + + return Product::find()->id($product->id)->one(); + } + + private function createVariant(Product $product, string $title, string $sku, float $price, int $siteId, bool $isDefault): Variant + { + $variant = new Variant(); + $variant->title = $title; + $variant->setPrimaryOwner($product); + $variant->setSku($sku); + $variant->setBasePrice($price); + $variant->isDefault = $isDefault; + $variant->promotable = true; + $variant->siteId = $siteId; + if (!Elements::saveElement($variant)) { + throw new RuntimeException('Could not save variant: ' . json_encode($variant->errors()->all())); + } + + return Variant::find()->id($variant->id)->one(); + } + + private function createCustomer(): User + { + $customer = new User(); + $customer->username = 'discountsTestCustomer'; + $customer->email = 'discountsTestCustomer@crafttest.com'; + $customer->active = true; + if (!Elements::saveElement($customer)) { + throw new RuntimeException('Could not save customer: ' . json_encode($customer->errors()->all())); + } + + return User::find()->id($customer->id)->one(); + } + + /** + * @param array $attributes + * @param int[] $purchasableIds + * @param int[] $categoryIds + * @param Coupon[] $coupons + */ + private function createDiscount(array $attributes, array $purchasableIds = [], array $categoryIds = [], array $coupons = []): Discount + { + $discount = new Discount($attributes); + $discount->setPurchasableIds($purchasableIds); + $discount->setCategoryIds($categoryIds); + + if (!empty($coupons)) { + $discount->setCoupons($coupons); + } + + if (!app(Discounts::class)->saveDiscount($discount)) { + throw new RuntimeException('Could not save discount: ' . json_encode($discount->errors()->all())); + } + + return $discount; + } +} diff --git a/tests/Support/GqlProductsFixture.php b/tests/Support/GqlProductsFixture.php new file mode 100644 index 0000000000..b2e1deb99c --- /dev/null +++ b/tests/Support/GqlProductsFixture.php @@ -0,0 +1,113 @@ +build(); + + return $fixture; + } + + private function build(): void + { + $site = Sites::getCurrentSite(); + + $this->hoodiesType = $this->createProductType('hoodies', 'Hoodies', $site->id); + $this->tShirtsType = $this->createProductType('tShirts', 'T-Shirts', $site->id); + + $this->hoodie = $this->createProduct($this->hoodiesType, 'Rad Hoodie', 'rad-hoodie', $site->id); + $this->hoodieVariant = $this->createVariant($this->hoodie, 'Rad Hoodie', 'rad-hood', 123.99, $site->id, isDefault: true); + + $this->tShirt = $this->createProduct($this->tShirtsType, 'Hypercolor T-Shirt', 'hypercolor-tshirt', $site->id); + $this->whiteVariant = $this->createVariant($this->tShirt, 'White', 'hct-white', 19.99, $site->id, isDefault: true); + $this->blueVariant = $this->createVariant($this->tShirt, 'Blue', 'hct-blue', 21.99, $site->id); + } + + private function createProductType(string $handle, string $name, int $siteId): ProductType + { + $productType = new ProductType(); + $productType->name = $name; + $productType->handle = $handle; + // With no dedicated title field, a variant's title always derives from the default + // '{product.title}' format, regardless of the title we set on it when creating it below. + $productType->hasVariantTitleField = false; + + $siteSettings = new ProductTypeSite(); + $siteSettings->siteId = $siteId; + $siteSettings->hasUrls = false; + $siteSettings->enabledByDefault = true; + $productType->setSiteSettings([$siteId => $siteSettings]); + + if (!app(ProductTypes::class)->saveProductType($productType)) { + throw new RuntimeException('Could not save product type: ' . json_encode($productType->errors()->all())); + } + + return $productType; + } + + private function createProduct(ProductType $productType, string $title, string $slug, int $siteId): Product + { + $product = new Product(); + $product->typeId = $productType->id; + $product->title = $title; + $product->slug = $slug; + $product->enabled = true; + $product->siteId = $siteId; + if (!Elements::saveElement($product)) { + throw new RuntimeException('Could not save product: ' . json_encode($product->errors()->all())); + } + + return Product::find()->id($product->id)->one(); + } + + private function createVariant(Product $product, string $title, string $sku, float $price, int $siteId, bool $isDefault = false): Variant + { + $variant = new Variant(); + $variant->title = $title; + $variant->setPrimaryOwner($product); + $variant->setSku($sku); + $variant->setBasePrice($price); + $variant->isDefault = $isDefault; + $variant->promotable = true; + $variant->siteId = $siteId; + + if (!Elements::saveElement($variant)) { + throw new RuntimeException('Could not save variant: ' . json_encode($variant->errors()->all())); + } + + return Variant::find()->id($variant->id)->one(); + } +} diff --git a/tests/Support/MockPurchasable.php b/tests/Support/MockPurchasable.php new file mode 100644 index 0000000000..2d88ebc6b0 --- /dev/null +++ b/tests/Support/MockPurchasable.php @@ -0,0 +1,36 @@ +isPromotable; + } + + #[\Override] + public function getPrice(): ?float + { + return 25.10; + } + + #[\Override] + public function getSku(): string + { + return 'commerce_testing_unique_sku'; + } +} diff --git a/tests/Support/OrdersFixture.php b/tests/Support/OrdersFixture.php new file mode 100644 index 0000000000..1acedb79ec --- /dev/null +++ b/tests/Support/OrdersFixture.php @@ -0,0 +1,262 @@ + */ + public array $orders = []; + + public static function seed(): self + { + $fixture = new self(); + $fixture->build(); + + return $fixture; + } + + private function build(): void + { + $site = Sites::getCurrentSite(); + $this->storeId = app(Stores::class)->getPrimaryStore()->id; + + $customer = new User(); + $customer->username = 'customer1'; + $customer->email = 'customer1@crafttest.com'; + $customer->active = true; + if (!Elements::saveElement($customer)) { + throw new RuntimeException('Could not save customer: ' . json_encode($customer->errors()->all())); + } + $this->customer = $customer; + + $shippingMethod = new ShippingMethod(); + $shippingMethod->name = 'US Shipping'; + $shippingMethod->handle = 'usShipping'; + $shippingMethod->storeId = $this->storeId; + $shippingMethod->enabled = true; + if (!app(ShippingMethods::class)->saveShippingMethod($shippingMethod)) { + throw new RuntimeException('Could not save shipping method: ' . json_encode($shippingMethod->errors()->all())); + } + + // No zone on the rule means it matches every address; no rates set means $0 shipping. + $shippingRule = new ShippingRule(); + $shippingRule->name = 'US Shipping'; + $shippingRule->methodId = $shippingMethod->id; + $shippingRule->enabled = true; + $shippingRule->priority = 0; + if (!app(ShippingRules::class)->saveShippingRule($shippingRule)) { + throw new RuntimeException('Could not save shipping rule: ' . json_encode($shippingRule->errors()->all())); + } + + $shippedStatus = new OrderStatus(); + $shippedStatus->name = 'Shipped'; + $shippedStatus->handle = 'shipped'; + $shippedStatus->color = 'purple'; + $shippedStatus->storeId = $this->storeId; + if (!app(OrderStatuses::class)->saveOrderStatus($shippedStatus)) { + throw new RuntimeException('Could not save order status: ' . json_encode($shippedStatus->errors()->all())); + } + $this->shippedOrderStatusId = $shippedStatus->id; + + $productType = new ProductType(); + $productType->name = 'T-Shirts'; + $productType->handle = 'tShirts'; + $productType->hasVariantTitleField = false; + $productType->variantTitleFormat = '{product.title}'; + + $siteSettings = new ProductTypeSite(); + $siteSettings->siteId = $site->id; + $siteSettings->hasUrls = false; + $siteSettings->enabledByDefault = true; + $productType->setSiteSettings([$site->id => $siteSettings]); + + if (!app(ProductTypes::class)->saveProductType($productType)) { + throw new RuntimeException('Could not save product type: ' . json_encode($productType->errors()->all())); + } + + $product = new Product(); + $product->typeId = $productType->id; + $product->title = 'Hypercolor T-Shirt'; + $product->enabled = true; + $product->siteId = $site->id; + if (!Elements::saveElement($product)) { + throw new RuntimeException('Could not save product: ' . json_encode($product->errors()->all())); + } + $this->product = $product; + + $white = new Variant(); + $white->title = 'White'; + $white->setPrimaryOwner($product); + $white->setSku('hct-white'); + $white->setBasePrice(19.99); + $white->isDefault = true; + $white->promotable = true; + $white->siteId = $site->id; + if (!Elements::saveElement($white)) { + throw new RuntimeException('Could not save white variant: ' . json_encode($white->errors()->all())); + } + + $blue = new Variant(); + $blue->title = 'Blue'; + $blue->setPrimaryOwner($product); + $blue->setSku('hct-blue'); + $blue->setBasePrice(21.99); + $blue->promotable = true; + $blue->siteId = $site->id; + if (!Elements::saveElement($blue)) { + throw new RuntimeException('Could not save blue variant: ' . json_encode($blue->errors()->all())); + } + + // Re-query fresh (rather than reuse the in-memory instances) so each variant's owner + // lookup round-trips through the database, same as it would for a real request. + $this->white = Variant::find()->id($white->id)->one(); + $this->blue = Variant::find()->id($blue->id)->one(); + + // Match `Stat`'s own date-range resolution (`new DateTime()`, no explicit timezone) so + // "today"/"yesterday" here line up with what the stat queries consider "today", whatever + // the environment's default timezone is. + $yesterday = new DateTime('now'); + $yesterday->modify('-1 day'); + $yesterday->setTime(23, 59, 59); + + $apple = [ + 'firstName' => 'Tim', 'lastName' => 'Cook', + 'addressLine1' => 'One Apple Park Way', 'locality' => 'Cupertino', + 'postalCode' => '95014', 'countryCode' => 'US', 'administrativeArea' => 'CA', + ]; + $bttf = [ + 'firstName' => 'Emmett', 'lastName' => 'Brown', + 'addressLine1' => '1640 Riverside Drive', 'locality' => 'Hill Valley', + 'postalCode' => '88', 'countryCode' => 'US', 'administrativeArea' => 'CA', + ]; + $bob = [ + 'firstName' => 'Bob', 'lastName' => 'Belcher', + 'addressLine1' => '101 Ocean Avenue', 'locality' => 'Long Island', + 'postalCode' => '12345', 'countryCode' => 'US', 'administrativeArea' => 'NY', + ]; + + $this->orders['completed-new'] = $this->createOrder( + lineItems: [[$this->white->id, 1], [$this->blue->id, 4]], + billingAddress: $apple, + shippingAddress: $apple, + shippingMethodHandle: 'usShipping', + ); + + $this->orders['completed-new-past'] = $this->createOrder( + lineItems: [[$this->white->id, 1], [$this->blue->id, 4]], + billingAddress: $bttf, + shippingAddress: $bttf, + dateOrdered: $yesterday, + ); + + $this->orders['completed-shipped'] = $this->createOrder( + lineItems: [[$this->white->id, 1]], + billingAddress: $bttf, + shippingAddress: $bob, + orderStatusId: $this->shippedOrderStatusId, + ); + } + + /** @param array $lineItems */ + private function createOrder( + array $lineItems, + array $billingAddress, + array $shippingAddress, + ?string $shippingMethodHandle = null, + ?DateTime $dateOrdered = null, + ?int $orderStatusId = null, + ): Order { + $order = new Order(); + $order->number = bin2hex(random_bytes(16)); + $order->storeId = $this->storeId; + $order->setCustomerId($this->customer->id); + + if ($shippingMethodHandle) { + $order->shippingMethodHandle = $shippingMethodHandle; + } + + if ($orderStatusId) { + $order->orderStatusId = $orderStatusId; + } + + if (!Elements::saveElement($order, false)) { + throw new RuntimeException('Could not save order: ' . json_encode($order->errors()->all())); + } + + $items = []; + foreach ($lineItems as [$purchasableId, $qty]) { + $items[] = app(LineItems::class)->create($order, [ + 'purchasableId' => $purchasableId, + 'qty' => $qty, + ]); + } + $order->setLineItems($items); + $order->setBillingAddress($billingAddress); + $order->setShippingAddress($shippingAddress); + + if (!Elements::saveElement($order, false)) { + throw new RuntimeException('Could not re-save order: ' . json_encode($order->errors()->all())); + } + + if (!$order->markAsComplete()) { + throw new RuntimeException('Could not complete order: ' . json_encode($order->errors()->all())); + } + + // markAsComplete() resets the order status to the store's default, so re-apply the + // requested one (if any) after completion, same as dateOrdered below. + if ($orderStatusId && $order->orderStatusId !== $orderStatusId) { + $order->orderStatusId = $orderStatusId; + if (!Elements::saveElement($order, false)) { + throw new RuntimeException('Could not update orderStatusId: ' . json_encode($order->errors()->all())); + } + } + + if ($dateOrdered) { + $order->dateOrdered = $dateOrdered; + if (!Elements::saveElement($order, false)) { + throw new RuntimeException('Could not update dateOrdered: ' . json_encode($order->errors()->all())); + } + } + + return Order::find()->id($order->id)->one(); + } +} diff --git a/tests/Support/PaymentCurrenciesFixture.php b/tests/Support/PaymentCurrenciesFixture.php new file mode 100644 index 0000000000..0a4715be3e --- /dev/null +++ b/tests/Support/PaymentCurrenciesFixture.php @@ -0,0 +1,53 @@ +build(); + + return $fixture; + } + + private function build(): void + { + $this->storeId = app(Stores::class)->getPrimaryStore()->id; + + $this->eur = $this->createCurrency('EUR', 0.5); + $this->aud = $this->createCurrency('AUD', 1.3); + } + + private function createCurrency(string $iso, float $rate): PaymentCurrency + { + $currency = new PaymentCurrency(); + $currency->iso = $iso; + $currency->rate = $rate; + $currency->storeId = $this->storeId; + + if (!app(PaymentCurrencies::class)->savePaymentCurrency($currency)) { + throw new RuntimeException('Could not save payment currency: ' . $iso); + } + + return $currency; + } +} diff --git a/tests/Support/ProductConditionsFixture.php b/tests/Support/ProductConditionsFixture.php new file mode 100644 index 0000000000..9a57b60e06 --- /dev/null +++ b/tests/Support/ProductConditionsFixture.php @@ -0,0 +1,113 @@ +build(); + + return $fixture; + } + + private function build(): void + { + $site = Sites::getCurrentSite(); + + $this->hoodiesType = $this->createProductType('hoodies', 'Hoodies', $site->id); + $this->tShirtsType = $this->createProductType('tShirts', 'T-Shirts', $site->id); + + [$this->hoodie, $this->hoodieVariant] = $this->createProduct( + $this->hoodiesType, + 'Rad Hoodie', + 'rad-hood', + 123.99, + $site->id, + ); + + [$this->tShirt, $this->tShirtVariant] = $this->createProduct( + $this->tShirtsType, + 'Plain T-Shirt', + 'plain-tee', + 19.99, + $site->id, + ); + } + + private function createProductType(string $handle, string $name, int $siteId): ProductType + { + $productType = new ProductType(); + $productType->name = $name; + $productType->handle = $handle; + $productType->hasVariantTitleField = false; + $productType->variantTitleFormat = '{product.title}'; + + $siteSettings = new ProductTypeSite(); + $siteSettings->siteId = $siteId; + $siteSettings->hasUrls = false; + $siteSettings->enabledByDefault = true; + $productType->setSiteSettings([$siteId => $siteSettings]); + + if (!app(ProductTypes::class)->saveProductType($productType)) { + throw new RuntimeException('Could not save product type: ' . json_encode($productType->errors()->all())); + } + + return $productType; + } + + /** @return array{0: Product, 1: Variant} */ + private function createProduct(ProductType $productType, string $title, string $sku, float $price, int $siteId): array + { + $product = new Product(); + $product->typeId = $productType->id; + $product->title = $title; + $product->enabled = true; + $product->siteId = $siteId; + if (!Elements::saveElement($product)) { + throw new RuntimeException('Could not save product: ' . json_encode($product->errors()->all())); + } + + $variant = new Variant(); + $variant->title = $title; + $variant->setPrimaryOwner($product); + $variant->setSku($sku); + $variant->setBasePrice($price); + $variant->isDefault = true; + $variant->promotable = true; + $variant->siteId = $siteId; + if (!Elements::saveElement($variant)) { + throw new RuntimeException('Could not save variant: ' . json_encode($variant->errors()->all())); + } + + return [Product::find()->id($product->id)->one(), Variant::find()->id($variant->id)->one()]; + } +} diff --git a/tests/Support/SalesFixture.php b/tests/Support/SalesFixture.php new file mode 100644 index 0000000000..811c8b783e --- /dev/null +++ b/tests/Support/SalesFixture.php @@ -0,0 +1,188 @@ +build(); + + return $fixture; + } + + private function build(): void + { + $site = Sites::getCurrentSite(); + + $hoodiesType = $this->createProductType('hoodies', 'Hoodies', $site->id); + [$this->hoodie, $this->radHood] = $this->createProductAndVariant($hoodiesType, 'Rad Hoodie', 'rad-hood', 123.99, $site->id); + + $tShirtsType = $this->createProductType('tShirts', 'T-Shirts', $site->id); + [$this->tShirt, $this->hctWhite] = $this->createProductAndVariant($tShirtsType, 'Hypercolor T-Shirt', 'hct-white', 19.99, $site->id); + + $this->group = $this->createUserGroup('Sales Test Group', 'salesTestGroup'); + $this->customer = $this->createCustomer($this->group); + + $this->percentageSale = $this->createSale([ + 'name' => 'My Percentage Sale', + 'description' => 'My test percentage sale.', + 'sortOrder' => 1, + 'apply' => SaleRecord::APPLY_BY_PERCENT, + 'applyAmount' => -0.1000, + 'allGroups' => true, + 'allPurchasables' => false, + 'allCategories' => true, + ], purchasableIds: [$this->radHood->id]); + + $this->allRelationshipsSale = $this->createSale([ + 'name' => 'All Relationships', + 'description' => 'All the relationships.', + 'sortOrder' => 2, + 'apply' => SaleRecord::APPLY_BY_PERCENT, + 'applyAmount' => -0.2000, + 'allGroups' => false, + 'allPurchasables' => false, + 'allCategories' => true, + ], purchasableIds: [$this->hctWhite->id], userGroupIds: [$this->group->id]); + } + + private function createProductType(string $handle, string $name, int $siteId): ProductType + { + $productType = new ProductType(); + $productType->name = $name; + $productType->handle = $handle; + $productType->hasVariantTitleField = false; + $productType->variantTitleFormat = '{product.title}'; + + $siteSettings = new ProductTypeSite(); + $siteSettings->siteId = $siteId; + $siteSettings->hasUrls = false; + $siteSettings->enabledByDefault = true; + $productType->setSiteSettings([$siteId => $siteSettings]); + + if (!app(ProductTypes::class)->saveProductType($productType)) { + throw new RuntimeException('Could not save product type: ' . json_encode($productType->errors()->all())); + } + + return $productType; + } + + /** @return array{0: Product, 1: Variant} */ + private function createProductAndVariant(ProductType $productType, string $title, string $sku, float $price, int $siteId): array + { + $product = new Product(); + $product->typeId = $productType->id; + $product->title = $title; + $product->enabled = true; + $product->siteId = $siteId; + if (!Elements::saveElement($product)) { + throw new RuntimeException('Could not save product: ' . json_encode($product->errors()->all())); + } + + $variant = new Variant(); + $variant->title = $title; + $variant->setPrimaryOwner($product); + $variant->setSku($sku); + $variant->setBasePrice($price); + $variant->isDefault = true; + $variant->promotable = true; + $variant->siteId = $siteId; + if (!Elements::saveElement($variant)) { + throw new RuntimeException('Could not save variant: ' . json_encode($variant->errors()->all())); + } + + return [Product::find()->id($product->id)->one(), Variant::find()->id($variant->id)->one()]; + } + + private function createUserGroup(string $name, string $handle): UserGroup + { + $group = new UserGroup(); + $group->name = $name; + $group->handle = $handle; + + if (!app(UserGroups::class)->saveGroup($group)) { + throw new RuntimeException('Could not save user group: ' . $handle); + } + + return $group; + } + + private function createCustomer(UserGroup $group): User + { + $customer = new User(); + $customer->username = 'salesTestCustomer'; + $customer->email = 'salesTestCustomer@crafttest.com'; + $customer->active = true; + if (!Elements::saveElement($customer)) { + throw new RuntimeException('Could not save customer: ' . json_encode($customer->errors()->all())); + } + + if (!app(Users::class)->assignUserToGroups($customer->id, [$group->id])) { + throw new RuntimeException('Could not assign customer to group: ' . $group->handle); + } + + // Re-query fresh so `getGroups()` isn't memoized empty from before the assignment above. + return User::find()->id($customer->id)->one(); + } + + /** + * @param array $attributes + * @param int[] $purchasableIds + * @param int[] $userGroupIds + */ + private function createSale(array $attributes, array $purchasableIds = [], array $userGroupIds = []): Sale + { + $sale = new Sale($attributes); + $sale->setPurchasableIds($purchasableIds); + $sale->setUserGroupIds($userGroupIds); + + if (!app(Sales::class)->saveSale($sale)) { + throw new RuntimeException('Could not save sale: ' . json_encode($sale->errors()->all())); + } + + return $sale; + } +} diff --git a/tests/Support/StoresFixture.php b/tests/Support/StoresFixture.php new file mode 100644 index 0000000000..0fd50f4e5b --- /dev/null +++ b/tests/Support/StoresFixture.php @@ -0,0 +1,108 @@ +build(); + + return $fixture; + } + + private function build(): void + { + $stores = app(Stores::class); + + $this->primaryStore = $stores->getPrimaryStore(); + $this->usSite = Sites::getCurrentSite(); + + $this->euSite = $this->createSite('EU Site', 'euSite', 'nl'); + $this->euStore = $this->createStore('EU Store', 'euStore'); + $this->assignSiteToStore($this->euSite, $this->euStore); + + $this->ukSite = $this->createSite('UK Site', 'ukSite', 'en-GB'); + $this->ukStore = $this->createStore('UK Store', 'ukStore'); + $this->assignSiteToStore($this->ukSite, $this->ukStore); + } + + private function createSite(string $name, string $handle, string $language): Site + { + $site = new Site([ + 'name' => $name, + 'handle' => $handle, + 'language' => $language, + 'baseUrl' => 'https://' . $handle . '.test', + 'hasUrls' => true, + 'groupId' => $this->usSite->groupId, + ]); + + if (!Sites::saveSite($site)) { + throw new RuntimeException('Could not save site: ' . json_encode($site->errors()->all())); + } + + return $site; + } + + private function createStore(string $name, string $handle): Store + { + $store = new Store([ + 'name' => $name, + 'handle' => $handle, + 'primary' => false, + ]); + + if (!app(Stores::class)->saveStore($store)) { + throw new RuntimeException('Could not save store: ' . json_encode($store->errors()->all())); + } + + return app(Stores::class)->getStoreByHandle($handle) + ?? throw new RuntimeException('Store not found after save: ' . $handle); + } + + private function assignSiteToStore(Site $site, Store $store): void + { + $stores = app(Stores::class); + + // Creating the site above triggers Commerce's SiteSaved listener, which maps the new + // site to the primary store by default (one row per site) — fetch and repoint that row. + $siteStore = $stores->getAllSiteStores()->firstWhere('siteId', $site->id); + + if (!$siteStore) { + $siteStore = new SiteStore(['siteId' => $site->id]); + } + + $siteStore->storeId = $store->id; + + if (!$stores->saveSiteStore($siteStore)) { + throw new RuntimeException('Could not save site store mapping: ' . json_encode($siteStore->errors()->all())); + } + } +} diff --git a/tests/Support/VariantQueryFixture.php b/tests/Support/VariantQueryFixture.php new file mode 100644 index 0000000000..e7add3f220 --- /dev/null +++ b/tests/Support/VariantQueryFixture.php @@ -0,0 +1,174 @@ +build(); + + return $fixture; + } + + private function build(): void + { + $site = Sites::getCurrentSite(); + $storeId = app(Stores::class)->getPrimaryStore()->id; + + $this->specialShippingCategory = $this->createShippingCategory($storeId); + $this->reducedTaxCategory = $this->createTaxCategory(); + + $this->teesType = $this->createProductType('tees', 'Tees', $site->id); + $this->hoodiesType = $this->createProductType('hoodies', 'Hoodies', $site->id); + + // A variant's shipping category must be one its product type allows — see + // Variant::beforeSave(), which silently resets it back to the store default otherwise. + // The association is owned by the shipping category, not the product type. + $this->specialShippingCategory->setProductTypes([$this->teesType, $this->hoodiesType]); + if (!app(ShippingCategories::class)->saveShippingCategory($this->specialShippingCategory)) { + throw new RuntimeException('Could not save shipping category: ' . json_encode($this->specialShippingCategory->errors()->all())); + } + + $this->tee = $this->createProduct($this->teesType, 'Hypercolor T-Shirt', $site->id); + + $this->whiteVariant = $this->createVariant($this->tee, 'White', 'hct-white', 19.99, $site->id, isDefault: true, overrides: [ + 'shippingCategoryId' => $this->specialShippingCategory->id, + 'taxCategoryId' => $this->reducedTaxCategory->id, + ]); + $this->blueVariant = $this->createVariant($this->tee, 'Blue', 'hct-blue', 21.99, $site->id); + + $this->hoodie = $this->createProduct($this->hoodiesType, 'Rad Hoodie', $site->id); + $this->hoodieVariant = $this->createVariant($this->hoodie, 'Rad Hoodie', 'rad-hood', 123.99, $site->id, isDefault: true, overrides: [ + 'shippingCategoryId' => $this->specialShippingCategory->id, + ]); + } + + private function createShippingCategory(int $storeId): ShippingCategory + { + $category = new ShippingCategory(); + $category->storeId = $storeId; + $category->name = 'Special Shipping'; + $category->handle = 'specialShipping'; + + if (!app(ShippingCategories::class)->saveShippingCategory($category)) { + throw new RuntimeException('Could not save shipping category: ' . json_encode($category->errors()->all())); + } + + return $category; + } + + private function createTaxCategory(): TaxCategory + { + $category = new TaxCategory(); + $category->name = 'Reduced Tax'; + $category->handle = 'reducedTax'; + + if (!app(TaxCategories::class)->saveTaxCategory($category)) { + throw new RuntimeException('Could not save tax category: ' . json_encode($category->errors()->all())); + } + + return $category; + } + + private function createProductType(string $handle, string $name, int $siteId): ProductType + { + $productType = new ProductType(); + $productType->name = $name; + $productType->handle = $handle; + $productType->hasVariantTitleField = true; + $productType->variantTitleFormat = '{product.title} - {title}'; + + $siteSettings = new ProductTypeSite(); + $siteSettings->siteId = $siteId; + $siteSettings->hasUrls = false; + $siteSettings->enabledByDefault = true; + $productType->setSiteSettings([$siteId => $siteSettings]); + + if (!app(ProductTypes::class)->saveProductType($productType)) { + throw new RuntimeException('Could not save product type: ' . json_encode($productType->errors()->all())); + } + + return $productType; + } + + private function createProduct(ProductType $productType, string $title, int $siteId): Product + { + $product = new Product(); + $product->typeId = $productType->id; + $product->title = $title; + $product->enabled = true; + $product->siteId = $siteId; + if (!Elements::saveElement($product)) { + throw new RuntimeException('Could not save product: ' . json_encode($product->errors()->all())); + } + + return Product::find()->id($product->id)->one(); + } + + /** @param array $overrides */ + private function createVariant(Product $product, string $title, string $sku, float $price, int $siteId, bool $isDefault = false, array $overrides = []): Variant + { + $variant = new Variant(); + $variant->title = $title; + $variant->setPrimaryOwner($product); + $variant->setSku($sku); + $variant->setBasePrice($price); + $variant->isDefault = $isDefault; + $variant->promotable = true; + $variant->siteId = $siteId; + + if (isset($overrides['shippingCategoryId'])) { + $variant->setShippingCategoryId($overrides['shippingCategoryId']); + } + if (isset($overrides['taxCategoryId'])) { + $variant->setTaxCategoryId($overrides['taxCategoryId']); + } + + if (!Elements::saveElement($variant)) { + throw new RuntimeException('Could not save variant: ' . json_encode($variant->errors()->all())); + } + + return Variant::find()->id($variant->id)->one(); + } +} diff --git a/tests/Support/gql.php b/tests/Support/gql.php new file mode 100644 index 0000000000..09af7b38a9 --- /dev/null +++ b/tests/Support/gql.php @@ -0,0 +1,50 @@ + ..., 'errors' => ...] result array. + * + * Goes straight through Gql::executeQuery() rather than a real HTTP request to the `graphql/api` + * action — this Testbench harness boots Craft as a package dependency with Commerce installed on + * top of a skeleton app, and a real routed request's site resolution doesn't hold up in that setup + * the way it does in a full Craft application. craftcms/yii2-adapter's own Laravel test suite for + * legacy GQL behavior takes the same direct-execution approach for this reason. + */ +function graphQL(string $query): array +{ + // debugMode: true restores the `FieldsOnCorrectType`/`KnownTypeNames` validation rules that + // Gql::getValidationRules() otherwise skips outside debug mode (a documented perf trade-off, + // since generating their suggestion messages requires building the full schema) - without it, + // an unknown field name is silently dropped instead of producing a GraphQL error. + return GqlFacade::executeQuery(GqlFacade::getActiveSchema(), $query, debugMode: true); +} + +function gqlActivateFullAccessSchema(?string $name = null): void +{ + app(Gql::class)->flushCaches(); + + GqlFacade::setActiveSchema(new GqlSchema([ + 'name' => $name ?? 'GraphQL ' . bin2hex(random_bytes(4)), + 'scope' => GqlHelper::createFullAccessSchema()->scope, + ])); +} + +function gqlActivateSchema(array $scope, ?string $name = null): GqlSchema +{ + app(Gql::class)->flushCaches(); + + $schema = new GqlSchema([ + 'name' => $name ?? 'GraphQL ' . bin2hex(random_bytes(4)), + 'scope' => $scope, + ]); + GqlFacade::setActiveSchema($schema); + + return $schema; +} diff --git a/tests/Support/templates/emails/order-confirmation.twig b/tests/Support/templates/emails/order-confirmation.twig new file mode 100644 index 0000000000..683f2326b1 --- /dev/null +++ b/tests/Support/templates/emails/order-confirmation.twig @@ -0,0 +1,9 @@ + + + + Order Confirmation + + +

Order Confirmation {{ order.shortNumber }}

+ + diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000000..dc7f139fcf --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,205 @@ +basePath()`) + // is already created by the time that attribute's `beforeEach()` fires. + // Create the symlink once, persistently, before the app exists at all. + $skeletonVendorPath = default_skeleton_path() . '/vendor'; + if (!is_link($skeletonVendorPath) && !is_dir($skeletonVendorPath)) { + symlink(package_path('vendor'), $skeletonVendorPath); + } + + // Craft defaults to the Solo edition (max 1 user) when CRAFT_EDITION/`system.edition` + // aren't set, which silently blocks `Elements::saveElement()` for any second user — + // needed by tests that create their own customer/author fixtures. Must be set before + // `Edition::get()`'s first call, since it caches its result for the rest of the request. + putenv('CRAFT_EDITION=pro'); + + // `Sites::setCurrentSite()` sets `$_SERVER['CRAFT_SITE']` as a side effect of resolving + // the current site for a real request — a plain superglobal, not container-scoped state, + // so it survives into the next test's fresh `Application`. `Yii2ServiceProvider::boot()` + // eagerly builds the legacy `Craft::$app` bridge on every test (not just the first), and + // that build happens before this test's own database transaction/install state is ready. + // With `CRAFT_SITE` left over from a previous test's request, `_requestedSite()` takes a + // branch that resolves the site by that env var instead of skipping straight to its + // fallback logic — and at this early a point, that lookup can find nothing yet, which its + // non-nullable return type turns into a hard `TypeError` instead of a graceful null. Clear + // it before `parent::setUp()` boots the fresh application, since that's when it happens. + unset($_SERVER['CRAFT_SITE'], $_SERVER['CRAFT_SITE_UPPER']); + + parent::setUp(); + + $this->registerSqliteCompatibilityFunctions(); + + config()->set('app.debug', true); + + app()->setLocale('en-US'); + app()->maintenanceMode()->deactivate(); + + File::cleanDirectory(config_path('craft/project')); + File::cleanDirectory(storage_path('runtime/compiled_classes')); + } + + /** + * A handful of query scopes use raw SQL functions (`LEFT()`, `RAND()`) that only MySQL and + * PostgreSQL provide — both of Commerce's supported production databases — since this suite + * runs against SQLite instead, register compatible substitutes on the current connection so + * those scopes behave the same here rather than raising "no such function" errors. + */ + protected function registerSqliteCompatibilityFunctions(): void + { + if (DB::connection()->getDriverName() !== 'sqlite') { + return; + } + + $pdo = DB::connection()->getPdo(); + $pdo->sqliteCreateFunction('LEFT', fn(?string $string, int $length): string => substr((string)$string, 0, $length), 2); + $pdo->sqliteCreateFunction('RAND', fn(): float => mt_rand() / mt_getrandmax(), 0); + } + + protected function connectionsToTransact(): array + { + if (config('database.default') === 'sqlite') { + return [config('database.default')]; + } + + return [config('database.default'), 'db2']; + } + + #[Override] + protected function tearDown(): void + { + parent::tearDown(); + } + + protected function refreshTestDatabase(): void + { + if (!RefreshDatabaseState::$migrated) { + Context::forgetHidden('craft.info'); + Context::forgetHidden('craft.isInstalled'); + + $this->artisan('db:wipe'); + + $site = new Site([ + 'name' => 'Craft test site', + 'handle' => 'defaultSite', + 'language' => 'en-US', + 'baseUrl' => 'https://localhost/', + 'primary' => true, + 'hasUrls' => true, + ]); + + $craftMigration = new Install( + username: 'craftcms', + password: 'craftcms2018!!', + email: 'support@craftcms.com', + site: $site, + )->silent(); + + Cache::lock(\CraftCms\Cms\ProjectConfig\ProjectConfig::MUTEX_NAME)->forceRelease(); + + $migrator = app(Migrator::class)->track('craft'); + $migrator->runMigration($craftMigration, 'up'); + $migrator->getRepository()->log('Install', 1); + + foreach ($migrator->getPendingMigrations() as $file) { + $migrator->getRepository()->log($migrator->getMigrationName($file), 1); + } + + // Install Commerce via its Yii2 plugin system + Craft::$app->plugins->installPlugin('commerce'); + + // `Craft::$app->plugins->installPlugin()` is a thin proxy to `CraftCms\Cms\Plugin\Plugins` + // (the actual, shared plugin manager — there's only one). Its own `installPlugin()` calls + // `loadPlugins()` as its first line, which runs *before* Commerce has a row in the `plugins` + // table yet, so it finds nothing to register and sets its internal `pluginsLoaded` flag to + // `true`. Since `Plugins` is a container singleton, that flag then permanently short-circuits + // every later `loadPlugins()` call for the rest of the test run, so Commerce's Laravel + // `register()`/`boot()` (GQL argument handlers, widgets, permissions, CP nav, macros, event + // listeners, etc.) never fire. Forgetting the singleton forces the next resolution to + // re-scan the `plugins` table, which now has Commerce's row, and register it correctly. + app()->forgetInstance(\CraftCms\Cms\Plugin\Plugins::class); + app(\CraftCms\Cms\Plugin\Plugins::class)->loadPlugins(); + + RefreshDatabaseState::$migrated = true; + } + + $this->beginDatabaseTransaction(); + } + + #[Override] + protected function defineEnvironment($app): void + { + File::cleanDirectory(config_path('craft/project')); + File::cleanDirectory(storage_path('runtime/compiled_classes')); + + $app->useEnvironmentPath(__DIR__); + $app->bootstrapWith([LoadEnvironmentVariables::class]); + + tap($app->make(ConfigRepository::class), function(ConfigRepository $config) { + $config->set('auth.defaults.guard', 'craft'); + $config->set('auth.guards.craft', ['driver' => 'session', 'provider' => 'users']); + + // Laravel's password broker (activation/password-reset emails) hashes its tokens + // with this key. It's never set via the environment in this suite, so without it + // DatabaseTokenRepository's $hashKey constructor argument is null. + $config->set('app.key', 'base64:' . base64_encode(str_repeat('a', 32))); + + $connection = env('DB_CONNECTION', 'testing'); + $driver = $config->get("database.connections.{$connection}.driver"); + + $config->set('database.default', $connection); + $config->set("database.connections.{$connection}.database", env('DB_DATABASE', ':memory:')); + $config->set("database.connections.{$connection}.host", env('DB_HOST', '127.0.0.1')); + $config->set("database.connections.{$connection}.username", env('DB_USERNAME', 'root')); + $config->set("database.connections.{$connection}.password", env('DB_PASSWORD', '')); + $config->set("database.connections.{$connection}.charset", env('DB_CHARSET', in_array($driver, ['mysql', 'mariadb']) ? 'utf8mb4' : 'utf8')); + $config->set("database.connections.{$connection}.collation", env('DB_COLLATION', in_array($driver, ['mysql', 'mariadb']) ? 'utf8mb4_unicode_ci' : 'utf8')); + $config->set("database.connections.{$connection}.prefix", env('DB_PREFIX')); + + DB::setDefaultConnection($connection); + }); + } +} diff --git a/tests/Unit/CatalogPricing/Conditions/CatalogPricingConditionTest.php b/tests/Unit/CatalogPricing/Conditions/CatalogPricingConditionTest.php new file mode 100644 index 0000000000..bfe2168234 --- /dev/null +++ b/tests/Unit/CatalogPricing/Conditions/CatalogPricingConditionTest.php @@ -0,0 +1,30 @@ +getConditionRules()->addRule(new CatalogPricingCustomerConditionRule()); + + $selectable = $condition->getSelectableConditionRules(); + + expect($selectable)->not->toHaveKey(CatalogPricingCustomerConditionRule::class); +}); + +test('without a conflicting rule already added, the customer rule remains selectable', function() { + $condition = new CatalogPricingCondition(); + + $selectable = $condition->getSelectableConditionRules(); + + expect($selectable)->toHaveKey(CatalogPricingCustomerConditionRule::class); +}); diff --git a/tests/Unit/Customer/Conditions/DiscountGroupConditionRuleTest.php b/tests/Unit/Customer/Conditions/DiscountGroupConditionRuleTest.php new file mode 100644 index 0000000000..99daf706d1 --- /dev/null +++ b/tests/Unit/Customer/Conditions/DiscountGroupConditionRuleTest.php @@ -0,0 +1,50 @@ + new ReflectionMethod($rule, 'matchValue')->invoke($rule, $value); + +test('no configured groups always matches', function() use ($match) { + $rule = new DiscountGroupConditionRule(); + $rule->operator = 'in'; + + expect($match($rule, ['group-a']))->toBeTrue(); + expect($match($rule, null))->toBeTrue(); +}); + +test('in-all operator requires every configured group to be present', function() use ($match) { + $rule = new DiscountGroupConditionRule(); + $rule->operator = 'inAll'; + $rule->setValues(['group-a', 'group-b']); + + expect($match($rule, ['group-a', 'group-b']))->toBeTrue(); + expect($match($rule, ['group-a', 'group-b', 'group-c']))->toBeTrue(); + expect($match($rule, ['group-a']))->toBeFalse(); + expect($match($rule, []))->toBeFalse(); +}); + +test('in operator matches when any configured group is present', function() use ($match) { + $rule = new DiscountGroupConditionRule(); + $rule->operator = 'in'; + $rule->setValues(['group-a', 'group-b']); + + expect($match($rule, ['group-a']))->toBeTrue(); + expect($match($rule, ['group-c']))->toBeFalse(); +}); + +test('not-in operator matches when no configured group is present', function() use ($match) { + $rule = new DiscountGroupConditionRule(); + $rule->operator = 'ni'; + $rule->setValues(['group-a', 'group-b']); + + expect($match($rule, ['group-c']))->toBeTrue(); + expect($match($rule, ['group-a']))->toBeFalse(); +}); diff --git a/tests/Unit/Helpers/LocaleTest.php b/tests/Unit/Helpers/LocaleTest.php new file mode 100644 index 0000000000..8f49c5d9e8 --- /dev/null +++ b/tests/Unit/Helpers/LocaleTest.php @@ -0,0 +1,29 @@ +language)->toBe('nl'); +}); + +test('Pdf::getRenderLanguage() throws without an order when language is order-language', function() { + $pdf = new Pdf(); + $pdf->language = PdfRecord::LOCALE_ORDER_LANGUAGE; + + expect(fn() => $pdf->getRenderLanguage())->toThrow(InvalidArgumentException::class); +}); + +test('Email::getRenderLanguage() throws without an order when language is order-language', function() { + $email = new Email(); + $email->language = EmailRecord::LOCALE_ORDER_LANGUAGE; + + expect(fn() => $email->getRenderLanguage())->toThrow(InvalidArgumentException::class); +}); diff --git a/tests/Unit/Helpers/LocalizationTest.php b/tests/Unit/Helpers/LocalizationTest.php new file mode 100644 index 0000000000..af19c1fcd3 --- /dev/null +++ b/tests/Unit/Helpers/LocalizationTest.php @@ -0,0 +1,24 @@ +toBe($expected); +})->with([ + 'null' => [null, 0.0], + 'empty string' => ['', 0.0], + 'percent symbol alone' => ['%', 0.0], + 'padded percent symbol' => [' % ', 0.0], + 'int zero' => [0, 0.0], + 'float' => [0.5, 0.5], + 'int' => [50, 50.0], + 'one' => [1, 1.0], + 'string zero' => ['0', 0.0], + 'string one' => ['1', 0.01], + 'string fifty' => ['50', 0.5], + 'padded string zero' => [' 0.0 ', 0.0], + 'fraction with trailing percent' => [' .5 % ', 0.005], + 'fraction with leading percent' => [' % 0.5 ', 0.005], +]); diff --git a/tests/Unit/Order/Conditions/CouponCodeConditionRuleTest.php b/tests/Unit/Order/Conditions/CouponCodeConditionRuleTest.php new file mode 100644 index 0000000000..5a027c96ad --- /dev/null +++ b/tests/Unit/Order/Conditions/CouponCodeConditionRuleTest.php @@ -0,0 +1,53 @@ + new ReflectionMethod($rule, 'matchValue')->invoke($rule, $value); + +test('equals operator is case-insensitive', function() use ($match) { + $rule = new CouponCodeConditionRule(); + $rule->operator = '='; + $rule->value = 'SUMMER10'; + + expect($match($rule, 'summer10'))->toBeTrue(); + expect($match($rule, 'Summer10'))->toBeTrue(); + expect($match($rule, 'winter10'))->toBeFalse(); +}); + +test('does-not-equal operator is case-insensitive', function() use ($match) { + $rule = new CouponCodeConditionRule(); + $rule->operator = '!='; + $rule->value = 'SUMMER10'; + + expect($match($rule, 'summer10'))->toBeFalse(); + expect($match($rule, 'winter10'))->toBeTrue(); +}); + +test('empty operator value always matches', function() use ($match) { + $rule = new CouponCodeConditionRule(); + $rule->operator = '='; + $rule->value = ''; + + expect($match($rule, 'anything'))->toBeTrue(); +}); + +test('empty and not-empty operators check for a coupon code presence', function() use ($match) { + $emptyRule = new CouponCodeConditionRule(); + $emptyRule->operator = 'empty'; + + expect($match($emptyRule, ''))->toBeTrue(); + expect($match($emptyRule, 'summer10'))->toBeFalse(); + + $notEmptyRule = new CouponCodeConditionRule(); + $notEmptyRule->operator = 'notempty'; + + expect($match($notEmptyRule, 'summer10'))->toBeTrue(); + expect($match($notEmptyRule, ''))->toBeFalse(); +}); diff --git a/tests/Unit/Order/Conditions/PaymentGatewayConditionRuleTest.php b/tests/Unit/Order/Conditions/PaymentGatewayConditionRuleTest.php new file mode 100644 index 0000000000..bf50720931 --- /dev/null +++ b/tests/Unit/Order/Conditions/PaymentGatewayConditionRuleTest.php @@ -0,0 +1,49 @@ +setAttributes(['value' => 'gateway-uid-1']); + + expect($rule->getValues())->toBe(['gateway-uid-1']); +}); + +test('setAttributes() prefers values over a legacy value when both are present', function() { + $rule = new PaymentGatewayConditionRule(); + $rule->setAttributes(['value' => 'gateway-uid-1', 'values' => ['gateway-uid-2']]); + + expect($rule->getValues())->toBe(['gateway-uid-2']); +}); + +test('getConfig() never emits the legacy value key', function() { + $rule = new PaymentGatewayConditionRule(); + $rule->setValues(['gateway-uid-1', 'gateway-uid-2']); + + $config = $rule->getConfig(); + + expect($config)->not->toHaveKey('value'); + expect($config['values'])->toBe(['gateway-uid-1', 'gateway-uid-2']); +}); + +test('getValue() returns the first of multiple selected values', function() { + $rule = new PaymentGatewayConditionRule(); + $rule->setValues(['gateway-uid-1', 'gateway-uid-2']); + + expect($rule->getValue())->toBe('gateway-uid-1'); +}); + +test('setValue() replaces the values array with a single-item array', function() { + $rule = new PaymentGatewayConditionRule(); + $rule->setValues(['gateway-uid-1', 'gateway-uid-2']); + $rule->setValue('gateway-uid-3'); + + expect($rule->getValues())->toBe(['gateway-uid-3']); +}); diff --git a/tests/Unit/Order/Conditions/TotalDiscountConditionRuleTest.php b/tests/Unit/Order/Conditions/TotalDiscountConditionRuleTest.php new file mode 100644 index 0000000000..b4e6e70d53 --- /dev/null +++ b/tests/Unit/Order/Conditions/TotalDiscountConditionRuleTest.php @@ -0,0 +1,42 @@ + new ReflectionMethod($rule, 'matchValue')->invoke($rule, $value); + +test('empty configured value always matches', function() use ($match) { + $rule = new TotalDiscountConditionRule(); + $rule->operator = '='; + $rule->value = ''; + + expect($match($rule, -50.0))->toBeTrue(); +}); + +test('greater-than operator compares against the negated value', function() use ($match) { + $rule = new TotalDiscountConditionRule(); + $rule->operator = '>'; + $rule->value = '5'; + + // A $10 discount (-10) is a *bigger* discount than $5 (-5), so -10 > -5 is false — + // matching the swapped "is less than" label this operator displays for this rule. + expect($match($rule, -10.0))->toBeFalse(); + // A $2 discount (-2) is smaller than $5, so -2 > -5 is true. + expect($match($rule, -2.0))->toBeTrue(); +}); + +test('equals operator matches the exact negated amount', function() use ($match) { + $rule = new TotalDiscountConditionRule(); + $rule->operator = '='; + $rule->value = '10'; + + expect($match($rule, -10.0))->toBeTrue(); + expect($match($rule, -5.0))->toBeFalse(); +}); diff --git a/tests/Unit/Order/LineItem/LineItemTest.php b/tests/Unit/Order/LineItem/LineItemTest.php new file mode 100644 index 0000000000..5324a2a4f4 --- /dev/null +++ b/tests/Unit/Order/LineItem/LineItemTest.php @@ -0,0 +1,50 @@ + 'bar', + 'numFoo' => 999, + 'emoji' => '❌', + ]; + + $lineItem = new LineItem(); + + $lineItem->setOptions($options); + expect($lineItem->getOptions())->toBe($options); + + $lineItem->setOptions(json_encode($options)); + expect($lineItem->getOptions())->toBe($options); +}); + +test('two line items with identical options produce identical options signatures', function() { + $options = ['Larry' => 'David']; + + $lineItem1 = new LineItem(); + $lineItem2 = new LineItem(); + $lineItem1->setOptions($options); + $lineItem2->setOptions($options); + + expect($lineItem1->getOptionsSignature())->toBe($lineItem2->getOptionsSignature()); +}); + +test('changing the options changes the options signature', function() { + $lineItem = new LineItem(); + $lineItem->setOptions(['foo' => 1]); + $signature = $lineItem->getOptionsSignature(); + + $lineItem->setOptions(['foo' => 2]); + + expect($lineItem->getOptionsSignature())->not->toBe($signature); +}); diff --git a/tests/Unit/Promotion/Data/SaleTest.php b/tests/Unit/Promotion/Data/SaleTest.php new file mode 100644 index 0000000000..f1f9dd3cec --- /dev/null +++ b/tests/Unit/Promotion/Data/SaleTest.php @@ -0,0 +1,57 @@ +getCategoryIds())->toBe([]); + + $sale->setCategoryIds([1, 2, 3, 4, 1]); + + expect($sale->getCategoryIds())->toBe([1, 2, 3, 4]); +}); + +test('setPurchasableIds dedupes and getPurchasableIds returns blank array when unset', function() { + $sale = new Sale(); + + expect($sale->getPurchasableIds())->toBe([]); + + $sale->setPurchasableIds([1, 2, 3, 4, 1]); + + expect($sale->getPurchasableIds())->toBe([1, 2, 3, 4]); +}); + +test('setUserGroupIds dedupes and getUserGroupIds returns blank array when unset', function() { + $sale = new Sale(); + + expect($sale->getUserGroupIds())->toBe([]); + + $sale->setUserGroupIds([1, 2, 3, 4, 1]); + + expect($sale->getUserGroupIds())->toBe([1, 2, 3, 4]); +}); + +test('getApplyAmountAsPercent formats the (always negative) apply amount as a percent', function(string|int|float $applyAmount, string $expected) { + $sale = new Sale(); + $sale->applyAmount = (float)$applyAmount; + + expect($sale->getApplyAmountAsPercent())->toBe($expected); +})->with([ + ['-0.1000', '10%'], + [0, '0%'], + [-0.1, '10%'], + [-0.15, '15%'], + [-0.105, '10.5%'], + [-0.10504, '10.504%'], + ['-0.1050400', '10.504%'], +]); + +test('getApplyAmountAsFlat flips the sign of the (always negative) apply amount', function() { + $sale = new Sale(); + $sale->applyAmount = -0.15; + + expect($sale->getApplyAmountAsFlat())->toBe('0.15'); +}); diff --git a/tests/Unit/Shipping/Data/ShippingMethodOptionTest.php b/tests/Unit/Shipping/Data/ShippingMethodOptionTest.php new file mode 100644 index 0000000000..53038ddccc --- /dev/null +++ b/tests/Unit/Shipping/Data/ShippingMethodOptionTest.php @@ -0,0 +1,14 @@ +price = 0.0; + $option->matchesOrder = false; + + expect($option->fields())->not->toHaveKeys(['dateCreated', 'dateUpdated']) + ->and($option->toArray())->not->toHaveKeys(['dateCreated', 'dateUpdated']); +}); diff --git a/tests/Unit/Store/Data/StoreSettingsTest.php b/tests/Unit/Store/Data/StoreSettingsTest.php new file mode 100644 index 0000000000..114b3e2683 --- /dev/null +++ b/tests/Unit/Store/Data/StoreSettingsTest.php @@ -0,0 +1,56 @@ + $store->setCountries($countries))->toThrow(InvalidArgumentException::class); + return; + } + + $store->setCountries($countries); + + expect($store->getCountries())->toEqual($expected); +})->with([ + [json_encode(['US', 'CA']), false, ['US', 'CA']], + ['US', true, []], + [['US', 'GB'], false, ['US', 'GB']], +]); + +test('getCountriesList returns country names keyed by code, for the selected countries only', function(array $countries, array $expected) { + $store = new StoreSettings(); + $store->setCountries($countries); + + expect($store->getCountriesList())->toEqual($expected); +})->with([ + [['US', 'GB', 'LV'], ['LV' => 'Latvia', 'GB' => 'United Kingdom', 'US' => 'United States']], + [['US', 'GB'], ['GB' => 'United Kingdom', 'US' => 'United States']], + [['US'], ['US' => 'United States']], + [['XX'], []], + [[], []], +]); + +test('getAdministrativeAreasListByCountryCode returns subdivisions keyed by country, for the selected countries only', function(array $countries, array $expected) { + $store = new StoreSettings(); + $store->setCountries($countries); + + expect($store->getAdministrativeAreasListByCountryCode())->toEqual($expected); +})->with([ + [[], []], + [['AU'], [ + 'AU' => [ + 'ACT' => 'Australian Capital Territory', + 'NSW' => 'New South Wales', + 'NT' => 'Northern Territory', + 'QLD' => 'Queensland', + 'SA' => 'South Australia', + 'TAS' => 'Tasmania', + 'VIC' => 'Victoria', + 'WA' => 'Western Australia', + ], + ]], +]); diff --git a/tests/Unit/Tax/Data/TaxRateTest.php b/tests/Unit/Tax/Data/TaxRateTest.php new file mode 100644 index 0000000000..2f7a6c8ae0 --- /dev/null +++ b/tests/Unit/Tax/Data/TaxRateTest.php @@ -0,0 +1,20 @@ +rate = (float)$rate; + + expect($taxRate->getRateAsPercent())->toBe($expected); +})->with([ + ['0.1000', '10%'], + [0, '0%'], + [0.1, '10%'], + [0.15, '15%'], + [0.105, '10.5%'], + [0.10504, '10.504%'], + ['0.1050400', '10.504%'], +]); diff --git a/tests/Unit/Transfer/Elements/TransferTest.php b/tests/Unit/Transfer/Elements/TransferTest.php new file mode 100644 index 0000000000..93070c5694 --- /dev/null +++ b/tests/Unit/Transfer/Elements/TransferTest.php @@ -0,0 +1,130 @@ +setDetails($details); + + return $transfer; +} + +test('sumDetailsQuanity sums quantity across all details', function() { + $transfer = transferWithDetails([ + ['inventoryItemId' => 1, 'quantity' => 3], + ['inventoryItemId' => 2, 'quantity' => 5], + ]); + + expect($transfer->sumDetailsQuanity())->toBe(8); +}); + +test('getTotalAccepted, getTotalRejected and getTotalReceived sum across details', function() { + $transfer = transferWithDetails([ + ['inventoryItemId' => 1, 'quantity' => 5, 'quantityAccepted' => 2, 'quantityRejected' => 1], + ['inventoryItemId' => 2, 'quantity' => 4, 'quantityAccepted' => 3, 'quantityRejected' => 0], + ]); + + expect($transfer->getTotalAccepted())->toBe(5); + expect($transfer->getTotalRejected())->toBe(1); + expect($transfer->getTotalReceived())->toBe(6); +}); + +test('isAllReceived is true only when every detail is fully received', function() { + $notAllReceived = transferWithDetails([ + ['inventoryItemId' => 1, 'quantity' => 5, 'quantityAccepted' => 2, 'quantityRejected' => 0], + ]); + expect($notAllReceived->isAllReceived())->toBeFalse(); + + $allReceived = transferWithDetails([ + ['inventoryItemId' => 1, 'quantity' => 5, 'quantityAccepted' => 3, 'quantityRejected' => 2], + ]); + expect($allReceived->isAllReceived())->toBeTrue(); +}); + +test('updateTransferStatus does nothing while still a draft', function() { + $transfer = transferWithDetails([ + ['inventoryItemId' => 1, 'quantity' => 5, 'quantityAccepted' => 5], + ]); + $transfer->setTransferStatus(TransferStatusType::DRAFT); + + $transfer->updateTransferStatus(); + + expect($transfer->getTransferStatus())->toBe(TransferStatusType::DRAFT); +}); + +test('updateTransferStatus moves to received once everything has been received', function() { + $transfer = transferWithDetails([ + ['inventoryItemId' => 1, 'quantity' => 5, 'quantityAccepted' => 5], + ]); + $transfer->setTransferStatus(TransferStatusType::PENDING); + + $transfer->updateTransferStatus(); + + expect($transfer->getTransferStatus())->toBe(TransferStatusType::RECEIVED); +}); + +test('updateTransferStatus moves to partial once some but not all has been received', function() { + $transfer = transferWithDetails([ + ['inventoryItemId' => 1, 'quantity' => 5, 'quantityAccepted' => 2], + ]); + $transfer->setTransferStatus(TransferStatusType::PENDING); + + $transfer->updateTransferStatus(); + + expect($transfer->getTransferStatus())->toBe(TransferStatusType::PARTIAL); +}); + +test('updateTransferStatus stays pending when nothing has been received yet', function() { + $transfer = transferWithDetails([ + ['inventoryItemId' => 1, 'quantity' => 5], + ]); + $transfer->setTransferStatus(TransferStatusType::DRAFT); + + // simulate the draft -> pending move made by afterSave() before updateTransferStatus() runs + $transfer->setTransferStatus(TransferStatusType::PENDING); + $transfer->updateTransferStatus(); + + expect($transfer->getTransferStatus())->toBe(TransferStatusType::PENDING); +}); + +test('validateLocations adds an error when origin and destination match', function() { + $transfer = new Transfer(); + $transfer->originLocationId = 1; + $transfer->destinationLocationId = 1; + + $transfer->validateLocations(); + + expect($transfer->errors()->has('originLocationId'))->toBeTrue(); +}); + +test('validateLocations adds no error when origin and destination differ', function() { + $transfer = new Transfer(); + $transfer->originLocationId = 1; + $transfer->destinationLocationId = 2; + + $transfer->validateLocations(); + + expect($transfer->errors()->has('originLocationId'))->toBeFalse(); +}); + +test('isTransferDraft, isTransferPending, isTransferPartial and isTransferReceived reflect the status', function() { + $transfer = new Transfer(); + + $transfer->setTransferStatus(TransferStatusType::DRAFT); + expect($transfer->isTransferDraft())->toBeTrue(); + expect($transfer->isTransferPending())->toBeFalse(); + + $transfer->setTransferStatus(TransferStatusType::PENDING); + expect($transfer->isTransferPending())->toBeTrue(); + expect($transfer->isTransferDraft())->toBeFalse(); + + $transfer->setTransferStatus(TransferStatusType::PARTIAL); + expect($transfer->isTransferPartial())->toBeTrue(); + + $transfer->setTransferStatus(TransferStatusType::RECEIVED); + expect($transfer->isTransferReceived())->toBeTrue(); +}); diff --git a/tests/UnitTestCase.php b/tests/UnitTestCase.php new file mode 100644 index 0000000000..ff01490cef --- /dev/null +++ b/tests/UnitTestCase.php @@ -0,0 +1,37 @@ +set('database.default', 'sqlite'); + $config->set('database.connections.sqlite', array_merge( + $config->get('database.connections.sqlite', []), + [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ], + )); + }); + + DB::purge('sqlite'); + DB::setDefaultConnection('sqlite'); + } +} diff --git a/tests/_bootstrap.php b/tests/_bootstrap.php deleted file mode 100644 index eae6cbaab0..0000000000 --- a/tests/_bootstrap.php +++ /dev/null @@ -1,29 +0,0 @@ - App::env('DB_DSN') ?: null, - 'driver' => App::env('DB_DRIVER'), - 'server' => App::env('DB_SERVER'), - 'port' => App::env('DB_PORT'), - 'database' => App::env('DB_DATABASE'), - 'user' => App::env('DB_USER'), - 'password' => App::env('DB_PASSWORD'), - 'schema' => App::env('DB_SCHEMA'), - 'tablePrefix' => App::env('DB_TABLE_PREFIX'), -]; diff --git a/tests/_craft/config/test.php b/tests/_craft/config/test.php deleted file mode 100644 index a5b52b4abd..0000000000 --- a/tests/_craft/config/test.php +++ /dev/null @@ -1,5 +0,0 @@ - - - Order Confirmation - - -

Order Confirmation {{ order.shortNumber }}

- -

Thank you for placing an order.

- - \ No newline at end of file diff --git a/tests/_envs/installed.yml b/tests/_envs/installed.yml deleted file mode 100644 index 2e4e462f2e..0000000000 --- a/tests/_envs/installed.yml +++ /dev/null @@ -1,8 +0,0 @@ -# `installed` environment config -# This environment can be used after a full test has been run once. -# It expects that the database has been setup and everything installed. -# Cleanup and setup will be skipped -modules: - config: - \craft\test\Craft: - dbSetup: {clean: false, setupCraft: false} diff --git a/tests/_output/.gitignore b/tests/_output/.gitignore deleted file mode 100644 index c96a04f008..0000000000 --- a/tests/_output/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/tests/_support/AcceptanceTester.php b/tests/_support/AcceptanceTester.php deleted file mode 100644 index 2fd5315c1a..0000000000 --- a/tests/_support/AcceptanceTester.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @author Global Network Group | Giel Tettelaar - * @since 5.0.7 - */ -class Gql extends Module -{ -} diff --git a/tests/_support/Helper/Unit.php b/tests/_support/Helper/Unit.php deleted file mode 100644 index 03e4543c94..0000000000 --- a/tests/_support/Helper/Unit.php +++ /dev/null @@ -1,12 +0,0 @@ - - * @author Global Network Group | Giel Tettelaar - * @since 2.1 - */ -class DiscountsFixture extends BaseModelFixture -{ - /** - * @inheritdoc - */ - public $dataFile = __DIR__ . '/data/discounts.php'; - - /** - * @inheritdoc - */ - public $modelClass = Discount::class; - - /** - * @inheritDoc - */ - public string $saveMethod = 'saveDiscount'; - - /** - * @inheritDoc - */ - public string $deleteMethod = 'deleteDiscountById'; - - /** - * @inheritDoc - */ - public $service = 'discounts'; - - /** - * @inheritDoc - */ - public function init(): void - { - $this->service = Plugin::getInstance()->get($this->service); - - parent::init(); - } - - /** - * @inheritdoc - */ - protected function prepData($data) - { - if (empty($data['_coupons'])) { - unset($data['_coupons']); - return $data; - } - - $data['coupons'] = []; - foreach ($data['_coupons'] as $c) { - $data['coupons'][] = \Craft::createObject(Coupon::class, ['config' => [ - 'attributes' => $c, - ]]); - } - - unset($data['_coupons']); - return $data; - } - - /** - * @inheritdoc - */ - public function unload(): void - { - // @TODO Investigate why the FK cascade delete on coupons does not fire during fixture unload, then remove this manual cleanup - if (isset($this->data) && !empty($this->data)) { - foreach ($this->data as $discount) { - $coupons = CouponRecord::find()->where(['discountId' => $discount['id']])->all(); - - if (empty($coupons)) { - continue; - } - - foreach ($coupons as $coupon) { - $coupon->delete(); - } - } - } - - parent::unload(); - } -} diff --git a/tests/fixtures/GqlSchemasFixture.php b/tests/fixtures/GqlSchemasFixture.php deleted file mode 100644 index b4e9d60be3..0000000000 --- a/tests/fixtures/GqlSchemasFixture.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @author Global Network Group | Giel Tettelaar - * @since 5.0.7 - */ -class GqlSchemasFixture extends ActiveFixture -{ - /** - * @inheritdoc - */ - public $modelClass = GqlSchema::class; - - /** - * @inheritdoc - */ - public $dataFile = __DIR__ . '/data/gql-schemas.php'; - - /** - * @inheritdoc - */ - public $depends = [ - ProductFixture::class, - ]; - - /** - * @inheritdoc - */ - protected function loadData($file, $throwException = true) - { - $file = parent::loadData($file, $throwException); - $productTypeUids = (new Query())->select('uid')->from(Table::PRODUCTTYPES)->column(); - $siteUids = (new Query())->select('uid')->from(\craft\db\Table::SITES)->column(); - - foreach ($file as &$row) { - if (!isset($row['scope'])) { - continue; - } - - foreach ($siteUids as $siteUid) { - $row['scope'][] = 'sites.' . $siteUid . ':read'; - } - - foreach ($productTypeUids as $typeUid) { - $row['scope'][] = 'productTypes.' . $typeUid . ':read'; - } - } - - return $file; - } -} diff --git a/tests/fixtures/ProductFixture.php b/tests/fixtures/ProductFixture.php deleted file mode 100644 index 81099e4367..0000000000 --- a/tests/fixtures/ProductFixture.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 3.1.4 - * @method Product getElement(string $key) - */ -class ProductFixture extends BaseProductFixture -{ - /** - * @inheritdoc - */ - public $dataFile = __DIR__ . '/data/products.php'; - - /** - * @inheritdoc - */ - public $depends = [ProductTypeFixture::class]; -} diff --git a/tests/fixtures/ProductTypesShippingCategoriesFixture.php b/tests/fixtures/ProductTypesShippingCategoriesFixture.php deleted file mode 100644 index 0f98badd3a..0000000000 --- a/tests/fixtures/ProductTypesShippingCategoriesFixture.php +++ /dev/null @@ -1,28 +0,0 @@ - -* @since 4.0.5 -*/ -class SubscriptionPlansFixture extends ActiveFixture -{ - /** - * @inheritdoc - */ - public $dataFile = __DIR__ . '/data/subscription-plans.php'; - - /** - * @inheritdoc - */ - public $modelClass = Plan::class; -} diff --git a/tests/fixtures/SubscriptionsFixture.php b/tests/fixtures/SubscriptionsFixture.php deleted file mode 100644 index e9d3606620..0000000000 --- a/tests/fixtures/SubscriptionsFixture.php +++ /dev/null @@ -1,56 +0,0 @@ - -* @since 4.0.4 -*/ -class SubscriptionsFixture extends BaseElementFixture -{ - /** - * @inheritdoc - */ - public $dataFile = __DIR__ . '/data/subscriptions.php'; - - public $depends = [SubscriptionPlansFixture::class]; - - /** - * @inheritdoc - */ - protected function createElement(): ElementInterface - { - return new Subscription(); - } - - /** - * @inheritdoc - */ - protected function populateElement(ElementInterface $element, array $attributes): void - { - /** @var Subscription $element */ - foreach ($attributes as $name => $val) { - if ($name === '_plan') { - if ($plan = Plugin::getInstance()->getPlans()->getPlanByHandle($val)) { - $element->planId = $plan->id; - } - - unset($attributes['_plan']); - } - } - - parent::populateElement($element, $attributes); - } -} diff --git a/tests/fixtures/TaxCategoryFixture.php b/tests/fixtures/TaxCategoryFixture.php deleted file mode 100644 index 58df087a55..0000000000 --- a/tests/fixtures/TaxCategoryFixture.php +++ /dev/null @@ -1,28 +0,0 @@ - '1000', - 'name' => 'Types', - 'scope' => [], - 'dateCreated' => '2018-08-08 20:00:00', - 'dateUpdated' => '2018-08-08 20:00:00', - 'uid' => 'gql-main-token-------------------uid', - ], -]; diff --git a/tests/fixtures/data/subscription-plans.php b/tests/fixtures/data/subscription-plans.php deleted file mode 100644 index 11a0406f16..0000000000 --- a/tests/fixtures/data/subscription-plans.php +++ /dev/null @@ -1,22 +0,0 @@ - [ - 'gatewayId' => 1, - 'name' => 'Monthly Subscription', - 'handle' => 'monthlySubscription', - 'reference' => 'monthly_sub', - 'enabled' => true, - 'planData' => 'dummy.plan', - 'sortOrder' => 1, - ], - 'weekly-disabled' => [ - 'gatewayId' => 1, - 'name' => 'Weekly Subscription', - 'handle' => 'weeklySubscription', - 'reference' => 'weekly_sub', - 'enabled' => false, - 'planData' => 'dummy.plan', - 'sortOrder' => 2, - ], -]; diff --git a/tests/fixtures/data/subscriptions.php b/tests/fixtures/data/subscriptions.php deleted file mode 100644 index 2907eacf4d..0000000000 --- a/tests/fixtures/data/subscriptions.php +++ /dev/null @@ -1,13 +0,0 @@ - [ - '_plan' => 'monthlySubscription', - 'userId' => 1, - 'gatewayId' => 1, - 'reference' => 'sub_000000000000XXXXXXXXXXXX', - 'trialDays' => 0, - 'hasStarted' => true, - 'subscriptionData' => ['test' => 'Sub Data'], - ], -]; diff --git a/tests/functional.suite.yml b/tests/functional.suite.yml deleted file mode 100644 index 0f69c911cf..0000000000 --- a/tests/functional.suite.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Codeception Test Suite Configuration -# -# Suite for functional tests -# Emulate web requests and make application process them -# Include one of framework modules (Symfony2, Yii2, Laravel5) to use it - -actor: FunctionalTester -modules: - enabled: - - Asserts - - \craft\test\Craft - - \Helper\Functional diff --git a/tests/functional/.gitkeep b/tests/functional/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/gql.suite.yml b/tests/gql.suite.yml deleted file mode 100644 index 0046eead8d..0000000000 --- a/tests/gql.suite.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Codeception Test Suite Configuration -# -# Suite for GraphQL tests -# Emulate web requests and make application process them -# Include one of framework modules (Symfony2, Yii2, Laravel5) to use it - -actor: GqlTester -modules: - enabled: - - Asserts - - \craft\test\Craft: - edition: 1 - - \Helper\Gql - - REST: - url: 'http://testing.craft.local/' - depends: PhpBrowser - part: Json diff --git a/tests/gql/GqlCest.php b/tests/gql/GqlCest.php deleted file mode 100644 index f332adcd95..0000000000 --- a/tests/gql/GqlCest.php +++ /dev/null @@ -1,133 +0,0 @@ - [ - 'class' => ProductFixture::class, - ], - 'gqlSchemas' => [ - 'class' => GqlSchemasFixture::class, - ], - ]; - } - - private bool $tokenStatus; - - /** - * @param FunctionalTester $I - */ - public function _before(FunctionalTester $I) - { - $gql = Craft::$app->getGql(); - $token = $gql->getPublicToken(); - $this->tokenStatus = $token->enabled; - $token->enabled = false; - $gql->saveToken($token); - - $this->_setSchema(1000); - } - - /** - * @param FunctionalTester $I - */ - public function _after(FunctionalTester $I) - { - $gql = Craft::$app->getGql(); - $token = $gql->getPublicToken(); - $token->enabled = $this->tokenStatus; - $gql->saveToken($token); - - $gql->flushCaches(); - } - - /** - * @param int $schemaId - * @return GqlSchema|null - * @throws Exception - */ - public function _setSchema(int $schemaId): ?GqlSchema - { - $gqlService = Craft::$app->getGql(); - $schema = $gqlService->getSchemaById($schemaId); - $gqlService->setActiveSchema($schema); - - return $schema; - } - - /** - * Test whether all query types work correctly - */ - public function testQuerying(FunctionalTester $I): void - { - $queryTypes = [ - 'products', - 'variants', - ]; - - foreach ($queryTypes as $queryType) { - $I->amOnPage('?action=graphql/api&query={' . $queryType . '{title}}'); - $I->see('"' . $queryType . '":['); - } - } - - /** - * Test whether querying for wrong gql field returns the correct error. - */ - public function testWrongGqlField(FunctionalTester $I): void - { - $parameter = 'bogus'; - $I->amOnPage('?action=graphql/api&query={products{' . $parameter . '}}'); - $I->see('"Cannot query field \"' . $parameter . '\"'); - } - - /** - * Test whether querying with wrong parameters returns the correct error. - */ - public function testWrongGqlQueryParameter(FunctionalTester $I): void - { - $I->amOnPage('?action=graphql/api&query={products(limit:[5,2]){title}}'); - $I->see('requires type Int'); - } - - /** - * Test whether query results yield the expected results. - */ - public function testQueryResults(FunctionalTester $I): void - { - $testData = file_get_contents(__DIR__ . '/data/gql.txt'); - foreach (explode('-----TEST DELIMITER-----', $testData) as $case) { - [$query, $response] = explode('-----RESPONSE DELIMITER-----', $case); - [$schemaId, $query] = explode('-----TOKEN DELIMITER-----', $query); - $this->_setSchema((int)trim($schemaId)); - $I->amOnPage('?action=graphql/api&query=' . urlencode(trim($query))); - $I->see(trim($response)); - $gqlService = Craft::$app->getGql(); - $gqlService->flushCaches(); - } - } -} diff --git a/tests/gql/_bootstrap.php b/tests/gql/_bootstrap.php deleted file mode 100644 index 9be98403e8..0000000000 --- a/tests/gql/_bootstrap.php +++ /dev/null @@ -1,6 +0,0 @@ - - * @since 3.3.5 - */ -class DiscountTest extends Unit -{ - /** - * @var Plugin|null - */ - public ?Plugin $pluginInstance = null; - - /** - * @var string|null - */ - public ?string $originalEdition = null; - - /** - * @inheritdoc - */ - protected function _before(): void - { - parent::_before(); - - $this->pluginInstance = Plugin::getInstance(); - } - - /** - * @inheritdoc - */ - protected function _after(): void - { - parent::_after(); - } - - /** - * @dataProvider adjustDataProvider - */ - public function testAdjust($lineItemData, $discountData, $expected): void - { - // Create discount model - $discount = new DiscountModel(); - - foreach ($discountData as $prop => $discountDatum) { - $discount->{$prop} = $discountDatum; - } - - // Mock discounts service - $this->pluginInstance->set('discounts', $this->make(Discounts::class, [ - 'getAllActiveDiscounts' => fn($o) => [$discount], - 'matchOrder' => fn($o, $d) => true, - ])); - - $order = new Order(); - - $lineItems = []; - foreach ($lineItemData as $item) { - $lineItem = $this->make(LineItem::class, [ - 'qty' => $item['qty'], - 'price' => $item['price'], - 'getPurchasable' => fn() => $item['purchasable'], - ]); - $lineItems[] = $lineItem; - } - - $order->setLineItems($lineItems); - - $discountAdjuster = $this->make(Discount::class, []); - - $adjustments = $discountAdjuster->adjust($order); - $order->setAdjustments($adjustments); - - self::assertCount(count($expected['adjustments']), $adjustments, 'Total number of adjustments'); - - foreach ($expected['adjustments'] as $index => $item) { - /** @var OrderAdjustment|null $adj */ - $adj = ArrayHelper::firstWhere($adjustments, 'description', $item['description']); - self::assertNotNull($adj); - self::assertEquals($item['amount'], $adj->amount, 'Adjustment amount'); - self::assertEquals($item['type'], $adj->type, 'Adjustment type'); - } - - self::assertEquals($expected['orderTotalPrice'], $order->getTotalPrice(), 'Order total price'); - self::assertEquals($expected['orderTotalDiscount'], $order->getTotalDiscount(), 'Order total discount'); - } - - /** - * @return array[] - */ - public function adjustDataProvider(): array - { - $orderLevelDiscount = [ - 'name' => 'Order Level', - 'description' => 'Order level discount', - 'allPurchasables' => true, - 'allCategories' => true, - 'stopProcessing' => false, - 'baseDiscount' => -10, - ]; - - $lineItemPromotable = [ - 'price' => 100, - 'qty' => 1, - 'purchasable' => new class() extends Purchasable { - public function getPrice(): float - { - return 100; - } - - public function getSku(): string - { - return 'testing'; - } - - public function getIsPromotable(): bool - { - return true; - } - }, - ]; - - $lineItemNonPromotable = [ - 'price' => 100, - 'qty' => 1, - 'purchasable' => new class() extends Purchasable { - public function getPrice(): float - { - return 100; - } - - public function getSku(): string - { - return 'testingNon'; - } - - public function getIsPromotable(): bool - { - return false; - } - }, - ]; - - return [ - // Example 1) 10 base discount (order level) with promotable line item - [ - [ // Line Items - $lineItemPromotable, - ], - $orderLevelDiscount, - [ - 'adjustments' => [ - [ - 'type' => 'discount', - 'amount' => $orderLevelDiscount['baseDiscount'], - 'description' => $orderLevelDiscount['description'], - ], - ], - 'orderTotalPrice' => 90, - 'orderTotalDiscount' => $orderLevelDiscount['baseDiscount'], - ], - ], - // Example 2) 10 base discount (order level) with non-promotable line item - [ - [ // Line Items - $lineItemNonPromotable, - ], - $orderLevelDiscount, - [ - 'adjustments' => [ - ], - 'orderTotalPrice' => 100, - 'orderTotalDiscount' => 0, - ], - ], - // Example 3) 10 base discount (order level) with both promotable and non-promotable line items - [ - [ // Line Items - $lineItemNonPromotable, - $lineItemPromotable, - ], - array_merge($orderLevelDiscount, ['baseDiscount' => -110]), - [ - 'adjustments' => [ - [ - 'type' => 'discount', - 'amount' => -100, - 'description' => $orderLevelDiscount['description'], - ], - ], - 'orderTotalPrice' => 100, - 'orderTotalDiscount' => -100, - ], - ], - ]; - } -} diff --git a/tests/unit/adjusters/ShippingTest.php b/tests/unit/adjusters/ShippingTest.php deleted file mode 100644 index fc67962c78..0000000000 --- a/tests/unit/adjusters/ShippingTest.php +++ /dev/null @@ -1,115 +0,0 @@ - - */ -class ShippingTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * Toggled mid-test to simulate the registration handler matching the - * order, then failing to on a later call. - * - * @var bool - */ - private bool $_thirdPartyMethodMatches = true; - - /** - * @inheritdoc - */ - protected function _before(): void - { - parent::_before(); - - $this->_thirdPartyMethodMatches = true; - - // Simulate a third-party plugin registering a shipping method by - // re-evaluating live availability on every call, instead of - // returning a static, persisted method. - Event::on( - ShippingMethods::class, - ShippingMethods::EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS, - function(RegisterAvailableShippingMethodsEvent $event) { - $shippingMethods = $event->getShippingMethods(); - $shippingMethods->push($this->make(ShippingMethod::class, [ - // Plugins must set this themselves on methods they register - - // `Order::getAvailableShippingMethodOptions()` silently drops any - // `ShippingMethod` whose `storeId` doesn't match the order's. - 'storeId' => $event->order->storeId, - 'handle' => 'thirdPartyFlatRate', - 'name' => 'Third Party Flat Rate', - 'getIsEnabled' => true, - 'getMatchingShippingRule' => fn(Order $order) => null, - 'getPriceForOrder' => fn(Order $order) => 8.99, - 'matchOrder' => fn(Order $order) => $this->_thirdPartyMethodMatches, - ])); - } - ); - } - - /** - * @inheritdoc - */ - protected function _after(): void - { - Event::off(ShippingMethods::class, ShippingMethods::EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS); - - parent::_after(); - } - - /** - * Confirms `Shipping::adjust()` correctly drops the adjustment (no error) - * when a registered method stops matching. Correct cart behaviour, and - * not, by itself, a bug - see - * {@see \craftcommercetests\unit\elements\order\OrderRecalculationTest} - * for the recalculation-lock bug this scenario was originally written to - * demonstrate. - */ - public function testAdjusterDropsAdjustmentWhenMethodDoesNotMatch(): void - { - $lineItem = $this->make(LineItem::class, [ - 'id' => 1, - 'qty' => 1, - 'price' => 50, - 'getIsShippable' => true, - ]); - - $order = new Order(); - $order->shippingMethodHandle = 'thirdPartyFlatRate'; - $order->setLineItems([$lineItem]); - - $adjuster = new Shipping(); - - $firstPass = $adjuster->adjust($order); - self::assertCount(1, $firstPass, 'Shipping adjustment should be present while the method matches.'); - self::assertEquals(8.99, $firstPass[0]->amount); - - $this->_thirdPartyMethodMatches = false; - - $secondPass = $adjuster->adjust($order); - self::assertSame([], $secondPass, 'No adjustment, no exception - this part is expected.'); - } -} diff --git a/tests/unit/adjusters/TaxTest.php b/tests/unit/adjusters/TaxTest.php deleted file mode 100644 index 56175e435c..0000000000 --- a/tests/unit/adjusters/TaxTest.php +++ /dev/null @@ -1,710 +0,0 @@ - - * @since 3.3.4 - */ -class TaxTest extends Unit -{ - /** - * @var Plugin|null - */ - public ?Plugin $pluginInstance = null; - - /** - * @inheritdoc - */ - protected function _before(): void - { - parent::_before(); - - // start with fresh cache - Craft::$app->getCache()->flush(); - $this->pluginInstance = Plugin::getInstance(); - } - - /** - * @inheritdoc - */ - protected function _after(): void - { - parent::_after(); - } - - public function testEuValidator(): void - { - $validator = new EuVatIdValidator(); - - $vatNumber = 'PL7272445205'; - - self::assertTrue($validator->validateFormat($vatNumber)); - - $vatNumber = 'PL99999999999'; - - self::assertFalse($validator->validateFormat($vatNumber)); - } - - /** - * @dataProvider dataCases - */ - public function testAdjust($addressData, $lineItemData, $taxRateData, $expected): void - { - $order = new Order(); - - $address = new Address(); - $address->countryCode = $addressData['countryCode']; - $address->organizationTaxId = $addressData['organizationTaxId'] ?? null; - - $order->setShippingAddress($address); - - $taxRates = []; - foreach ($taxRateData as $item) { - $rate = $this->make(TaxRate::class, [ - 'getIsEverywhere' => !isset($item['zone']), - 'getTaxZone' => function() use ($item) { - if (isset($item['zone'])) { - $zone = $this->make(TaxAddressZone::class, []); - - if (isset($item['zone']['condition'])) { - $zone->setCondition($item['zone']['condition']); - } - return $zone; - } - - return null; - }, - ]); - - $rate->name = $item['name']; - $rate->code = $item['code']; - $rate->rate = $item['rate']; - $rate->include = $item['include']; - $rate->removeIncluded = $item['removeIncluded'] ?? false; - $rate->taxIdValidators = $item['taxIdValidators'] ?? []; - $rate->removeVatIncluded = $item['removeVatIncluded'] ?? false; - $rate->taxable = $item['taxable']; - $rate->taxCategoryId = $item['taxCategoryId']; - $taxRates[] = $rate; - } - - $lineItems = []; - foreach ($lineItemData as $item) { - $lineItem = new LineItem(); - $lineItem->qty = $item['qty']; - $lineItem->price = $item['price']; - $lineItem->taxCategoryId = 1; - $lineItems[] = $lineItem; - } - - $order->setLineItems($lineItems); - - $taxAdjuster = $this->make(Tax::class, [ - 'getTaxRates' => collect($taxRates), - 'validateTaxIdNumber' => fn($vatNum) => $addressData['_validateVat'] ?? false, - ]); - - $adjustments = $taxAdjuster->adjust($order); - $order->setAdjustments($adjustments); - - self::assertCount(count($expected['adjustments']), $adjustments, 'Total number of adjustments'); - - foreach ($expected['adjustments'] as $index => $item) { - self::assertEquals($item['type'], $adjustments[$index]->type, 'Adjustment type'); - self::assertEquals($item['amount'], round($adjustments[$index]->amount, 2), 'Adjustment amount'); - self::assertEquals($item['included'], $adjustments[$index]->included, 'Adjustment included'); - self::assertEquals($item['description'], $adjustments[$index]->description, 'Adjustment description'); - } - - self::assertEquals($expected['orderTotalQty'], $order->getTotalQty(), 'Order total quantity'); - self::assertEquals($expected['orderTotalPrice'], $order->getTotalPrice(), 'Order total price'); - self::assertEquals($expected['orderTotalTax'], round($order->getTotalTax(), 2), 'Order total tax'); - self::assertEquals($expected['orderTotalTaxIncluded'], round($order->getTotalTaxIncluded(), 2), 'Order total included tax'); - } - - /** - * @return array[] - */ - public function dataCases(): array - { - $uid = StringHelper::UUID(); - return [ - - // Example 1) 10% included tax - 'tax-10pct-included' => [ - [ // Address - 'countryCode' => 'AU', - ], - [ // Line Items - ['price' => 100, 'qty' => 1], // 100 total price - ], - [ // Tax Rates - [ - 'name' => 'Australia', - 'code' => 'GST', - 'taxCategoryId' => 1, - 'rate' => 0.1, - 'include' => true, - 'taxable' => 'order_total_price', - 'zone' => [ - 'condition' => [ - 'class' => ZoneAddressCondition::class, - 'config' => '{"elementType":null,"fieldContext":"global"}', - 'conditionRules' => [ - [ - 'uid' => $uid, - 'class' => CountryConditionRule::class, - 'type' => Json::encode([ - 'class' => CountryConditionRule::class, - 'uid' => $uid, - 'operator' => 'in', - 'values' => ['AU'], - ]), - 'operator' => 'in', - 'values' => [ - 'AU', - ], - ], - ], - ], - ], - ], - ], - [ - 'adjustments' => [ - [ - 'type' => 'tax', - 'amount' => 9.09, - 'included' => true, - 'description' => '10%', - ], - ], - 'orderTotalPrice' => 100, - 'orderTotalQty' => 1, - 'orderTotalTax' => 0, - 'orderTotalTaxIncluded' => 9.09, - ], - ], - - // Example 2) 10% not included - 'tax-10pct-not-included' => [ - [ // Address - 'countryCode' => 'AU', - ], - [ // Line Items - ['price' => 100, 'qty' => 1], // 100 total price - ], - [ // Tax Rates - [ - 'name' => 'Australia', - 'code' => 'GST', - 'taxCategoryId' => 1, - 'rate' => 0.1, - 'include' => false, - 'taxable' => 'order_total_price', - // zone is everywhere - ], - ], - [ - 'adjustments' => [ - [ - 'type' => 'tax', - 'amount' => 10, - 'included' => false, - 'description' => '10%', - ], - ], - 'orderTotalPrice' => 110, - 'orderTotalQty' => 1, - 'orderTotalTax' => 10, - 'orderTotalTaxIncluded' => 0, - ], - ], - - // Example 3) 10% included, 2 line items, isVat - 'tax-10pct-included-2-line-items' => [ - [ // Address - 'countryCode' => 'NL', - ], - [ // Line Items - ['price' => 100, 'qty' => 1], // 100 total price - ['price' => 50, 'qty' => 2], // 100 total price - ], - [ // Tax Rates - [ - 'name' => 'Netherlands', - 'code' => 'NLVAT', - 'taxCategoryId' => 1, - 'rate' => 0.1, - 'include' => true, - 'taxIdValidators' => [EuVatIdValidator::class], - 'taxable' => 'price_shipping', - // zone is everywhere - ], - ], - [ - 'adjustments' => [ - [ - 'type' => 'tax', - 'amount' => 9.09, - 'included' => true, - 'description' => '10%', - ], - [ - 'type' => 'tax', - 'amount' => 9.09, - 'included' => true, - 'description' => '10%', - ], - ], - 'orderTotalPrice' => 200, - 'orderTotalQty' => 3, - 'orderTotalTax' => 0, - 'orderTotalTaxIncluded' => 18.18, - ], - ], - - // Example 4) 10% tax that does not apply due to zone mismatch - 'tax-zone-mismatch-1' => [ - [ // Address - 'countryCode' => 'AU', - ], - [ // Line Items - ['price' => 100, 'qty' => 1], // 100 total price - ], - [ // Tax Rates - [ - 'name' => 'Australia', - 'code' => 'GST', - 'taxCategoryId' => 1, - 'rate' => 0.1, - 'include' => false, - 'taxable' => 'order_total_price', - 'zone' => [ - 'condition' => [ - 'class' => ZoneAddressCondition::class, - 'config' => '{"elementType":null,"fieldContext":"global"}', - 'conditionRules' => [ - [ - 'uid' => $uid, - 'class' => CountryConditionRule::class, - 'type' => Json::encode([ - 'class' => CountryConditionRule::class, - 'uid' => $uid, - 'operator' => 'in', - 'values' => ['NL'], - ]), - 'operator' => 'in', - 'values' => [ - 'NL', - ], - ], - ], - ], - ], - ], - ], - [ - 'adjustments' => [], - 'orderTotalPrice' => 100, - 'orderTotalQty' => 1, - 'orderTotalTax' => 0, - 'orderTotalTaxIncluded' => 0, - ], - ], - - // Example 5) 10% tax that gets removed due to zone mismatch - 'tax-zone-mismatch-2' => [ - [ // Address - 'countryCode' => 'AU', - ], - [ // Line Items - ['price' => 100, 'qty' => 1], // 100 total price - ], - [ // Tax Rates - [ - 'name' => 'Netherlands', - 'code' => 'NLVAT', - 'taxCategoryId' => 1, - 'rate' => 0.1, - 'include' => true, - 'removeIncluded' => true, - 'taxable' => 'order_total_price', - 'zone' => [ - 'condition' => [ - 'class' => ZoneAddressCondition::class, - 'config' => '{"elementType":null,"fieldContext":"global"}', - 'conditionRules' => [ - [ - 'uid' => $uid, - 'class' => CountryConditionRule::class, - 'type' => Json::encode([ - 'class' => CountryConditionRule::class, - 'uid' => $uid, - 'operator' => 'in', - 'values' => ['NL'], - ]), - 'operator' => 'in', - 'values' => [ - 'NL', // Not AU on purpose to create mismatch - ], - ], - ], - ], - ], - ], - ], - [ - 'adjustments' => [ - [ - 'type' => 'discount', - 'amount' => -9.09, - 'included' => false, - 'description' => '10%', - ], - ], - 'orderTotalPrice' => 90.91, - 'orderTotalQty' => 1, - 'orderTotalTax' => 0, - 'orderTotalTaxIncluded' => 0, - ], - ], - - // Example 6) 10% tax that gets removed due to valid VAT ID - 'tax-valid-vat-1' => [ - [ // Address - 'countryCode' => 'CZ', - 'organizationTaxId' => 'CZ25666011', - '_validateVat' => true, - ], - [ // Line Items - ['price' => 100, 'qty' => 1], // 100 total price - ], - [ // Tax Rates - [ - 'name' => 'CZ Vat', - 'code' => 'CZVAT', - 'taxCategoryId' => 1, - 'rate' => 0.1, - 'include' => true, - 'taxIdValidators' => [EuVatIdValidator::class], - 'removeVatIncluded' => true, - 'taxable' => 'order_total_price', - 'zone' => [ - 'condition' => [ - 'class' => ZoneAddressCondition::class, - 'config' => '{"elementType":null,"fieldContext":"global"}', - 'conditionRules' => [ - [ - 'uid' => $uid, - 'class' => CountryConditionRule::class, - 'type' => Json::encode([ - 'class' => CountryConditionRule::class, - 'uid' => $uid, - 'operator' => 'in', - 'values' => ['CZ'], - ]), - 'operator' => 'in', - 'values' => [ - 'CZ', - ], - ], - ], - ], - ], - ], - ], - [ - 'adjustments' => [ - [ - 'type' => 'discount', - 'amount' => -9.09, - 'included' => false, - 'description' => '10%', - ], - ], - 'orderTotalPrice' => 90.91, - 'orderTotalQty' => 1, - 'orderTotalTax' => 0, - 'orderTotalTaxIncluded' => 0, - ], - ], - - // Example 7) 10% included tax that does not apply since it has a valid tax ID, but does not remove - 'tax-valid-vat-2' => [ - [ // Address - 'countryCode' => 'CZ', - 'organizationTaxId' => 'CZ25666011', - '_validateVat' => true, - ], - [ // Line Items - ['price' => 100, 'qty' => 1], // 100 total price - ], - [ // Tax Rates - [ - 'name' => 'CZ Vat', - 'code' => 'CZVAT', - 'taxCategoryId' => 1, - 'rate' => 0.1, - 'include' => true, - 'taxIdValidators' => [EuVatIdValidator::class], - 'removeVatIncluded' => false, - 'taxable' => 'order_total_price', - 'zone' => [ - 'condition' => [ - 'class' => ZoneAddressCondition::class, - 'config' => '{"elementType":null,"fieldContext":"global"}', - 'conditionRules' => [ - [ - 'uid' => $uid, - 'class' => CountryConditionRule::class, - 'type' => Json::encode([ - 'class' => CountryConditionRule::class, - 'uid' => $uid, - 'operator' => 'in', - 'values' => ['CZ'], - ]), - 'operator' => 'in', - 'values' => [ - 'CZ', - ], - ], - ], - ], - ], - ], - ], - [ - 'adjustments' => [], - 'orderTotalPrice' => 100, - 'orderTotalQty' => 1, - 'orderTotalTax' => 0, - 'orderTotalTaxIncluded' => 0, - ], - ], - - // Example 6) 10% tax that does not get removed due to an invalid VAT ID - 'tax-invalid-vat-1' => [ - [ // Address - 'countryCode' => 'CZ', - 'organizationTaxId' => 'CZ99999999', - '_validateVat' => false, - ], - [ // Line Items - ['price' => 100, 'qty' => 1], // 100 total price - ], - [ // Tax Rates - [ - 'name' => 'CZ Vat', - 'code' => 'CZVAT', - 'taxCategoryId' => 1, - 'rate' => 0.1, - 'include' => true, - 'taxIdValidators' => [EuVatIdValidator::class], - 'removeVatIncluded' => true, - 'taxable' => 'order_total_price', - 'zone' => [ - 'condition' => [ - 'class' => ZoneAddressCondition::class, - 'config' => '{"elementType":null,"fieldContext":"global"}', - 'conditionRules' => [ - [ - 'uid' => $uid, - 'class' => CountryConditionRule::class, - 'type' => Json::encode([ - 'class' => CountryConditionRule::class, - 'uid' => $uid, - 'operator' => 'in', - 'values' => ['CZ'], - ]), - 'operator' => 'in', - 'values' => [ - 'CZ', - ], - ], - ], - ], - ], - ], - ], - [ - 'adjustments' => [ - [ - 'type' => 'tax', - 'description' => '10%', - 'included' => true, - 'amount' => 9.09, - ], - ], - 'orderTotalPrice' => 100, - 'orderTotalQty' => 1, - 'orderTotalTax' => 0, - 'orderTotalTaxIncluded' => 9.09, - ], - ], - - // Example 7) line item taxable VAT 20% tax - 'tax-20pct-vat-not-included-taxable-line-item-price' => [ - [ // Address - 'countryCode' => 'UK', - ], - [ // Line Items - ['price' => 49.17, 'qty' => 1], // 49.17 total price - ], - [ // Tax Rates - [ - 'name' => 'UK', - 'code' => 'VAT', - 'taxCategoryId' => 1, - 'rate' => 0.2, - 'include' => false, - 'taxIdValidators' => [EuVatIdValidator::class], - 'taxable' => 'price', - ], - ], - [ - 'adjustments' => [ - [ - 'type' => 'tax', - 'amount' => 9.83, - 'included' => false, - 'description' => '20%', - ], - ], - 'orderTotalPrice' => 59, - 'orderTotalQty' => 1, - 'orderTotalTax' => 9.83, - 'orderTotalTaxIncluded' => 0, - ], - ], - - // Example 8) Purchasable taxable VAT 20% tax - 'tax-20pct-vat-not-included-taxable-purchasable-price' => [ - [ // Address - 'countryCode' => 'UK', - ], - [ // Line Items - ['price' => 49.17, 'qty' => 1], // 49.17 total price - ], - [ // Tax Rates - [ - 'name' => 'UK', - 'code' => 'VAT', - 'taxCategoryId' => 1, - 'rate' => 0.2, - 'include' => false, - 'taxIdValidators' => [EuVatIdValidator::class], - 'taxable' => 'purchasable', - ], - ], - [ - 'adjustments' => [ - [ - 'type' => 'tax', - 'amount' => 9.83, - 'included' => false, - 'description' => '20%', - ], - ], - 'orderTotalPrice' => 59, - 'orderTotalQty' => 1, - 'orderTotalTax' => 9.83, - 'orderTotalTaxIncluded' => 0, - ], - ], - - // Example 9) Line Item taxable VAT 20% tax with qty 4 - 'tax-20pct-vat-not-included-taxable-line-item-price-qty-4' => [ - [ // Address - 'countryCode' => 'UK', - ], - [ // Line Items - ['price' => 49.17, 'qty' => 4], // 49.17 total price - ], - [ // Tax Rates - [ - 'name' => 'UK', - 'code' => 'VAT', - 'taxCategoryId' => 1, - 'rate' => 0.2, - 'include' => false, - 'taxIdValidators' => [EuVatIdValidator::class], - 'taxable' => 'price', - ], - ], - [ - 'adjustments' => [ - [ - 'type' => 'tax', - 'amount' => 39.34, - 'included' => false, - 'description' => '20%', - ], - ], - 'orderTotalPrice' => 236.02, - 'orderTotalQty' => 4, - 'orderTotalTax' => 39.34, - 'orderTotalTaxIncluded' => 0, - ], - ], - - // Example 10) Purchasable taxable VAT 20% tax with qty 4 - 'tax-20pct-vat-not-included-taxable-purchasable-price-qty-4' => [ - [ // Address - 'countryCode' => 'UK', - ], - [ // Line Items - ['price' => 49.17, 'qty' => 4], // 49.17 total price - ], - [ // Tax Rates - [ - 'name' => 'UK', - 'code' => 'VAT', - 'taxCategoryId' => 1, - 'rate' => 0.2, - 'include' => false, - 'taxIdValidators' => [EuVatIdValidator::class], - 'taxable' => 'purchasable', - ], - ], - [ - 'adjustments' => [ - [ - 'type' => 'tax', - 'amount' => 39.32, - 'included' => false, - 'description' => '20%', - ], - ], - 'orderTotalPrice' => 236, - 'orderTotalQty' => 4, - 'orderTotalTax' => 39.32, - 'orderTotalTaxIncluded' => 0, - ], - ], - ]; - } -} diff --git a/tests/unit/controllers/CartControllerRateLimitTest.php b/tests/unit/controllers/CartControllerRateLimitTest.php deleted file mode 100644 index 89abd21f6b..0000000000 --- a/tests/unit/controllers/CartControllerRateLimitTest.php +++ /dev/null @@ -1,171 +0,0 @@ - - * @since 4.11.0 - */ -class CartControllerRateLimitTest extends TestCase -{ - private IpRateLimitIdentity $identity; - private Action $action; - private Request $request; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - protected function setUp(): void - { - parent::setUp(); - - Craft::$app->getCache()->flush(); - - $this->identity = new IpRateLimitIdentity([ - 'limit' => 3, - 'window' => 10, - 'ip' => '192.168.1.1', - 'keyPrefix' => 'cart-rate-limit', - ]); - - $controller = $this->createMock(Controller::class); - $this->action = new Action('test-action', $controller); - $this->request = Craft::$app->getRequest(); - } - - /** - * @return void - */ - public function testGetRateLimit(): void - { - [$limit, $window] = $this->identity->getRateLimit($this->request, $this->action); - self::assertSame(3, $limit); - self::assertSame(10, $window); - } - - /** - * @return void - */ - public function testLoadAllowanceReturnsDefaultWhenCacheEmpty(): void - { - [$allowance, $timestamp] = $this->identity->loadAllowance($this->request, $this->action); - self::assertSame(3, $allowance); - self::assertEqualsWithDelta(time(), $timestamp, 1); - } - - /** - * @return void - */ - public function testSaveAndLoadAllowance(): void - { - $this->identity->saveAllowance($this->request, $this->action, 1, 1000000); - - [$allowance, $timestamp] = $this->identity->loadAllowance($this->request, $this->action); - self::assertSame(1, $allowance); - self::assertSame(1000000, $timestamp); - } - - /** - * @return void - */ - public function testDifferentIpsGetIndependentAllowances(): void - { - // Save allowance for first IP - $this->identity->saveAllowance($this->request, $this->action, 0, 1000000); - - // Create identity with different IP - $otherIdentity = new IpRateLimitIdentity([ - 'limit' => 3, - 'window' => 10, - 'ip' => '10.0.0.1', - 'keyPrefix' => 'cart-rate-limit', - ]); - - // Second IP should still have full allowance (cache miss = default) - [$allowance, $timestamp] = $otherIdentity->loadAllowance($this->request, $this->action); - self::assertSame(3, $allowance); - self::assertEqualsWithDelta(time(), $timestamp, 1); - - // First IP should still be exhausted - [$allowance] = $this->identity->loadAllowance($this->request, $this->action); - self::assertSame(0, $allowance); - } - - public function testMultipleRequests(): void - { - $request = Craft::$app->getRequest(); - $request->enableCsrfValidation = false; - $cartController = new CartController('cart', Plugin::getInstance()); - - $request->headers->set('Accept', 'application/json'); - $request->headers->set('X-Http-Method-Override', 'POST'); - - // Create a cart to get a cart number - $variant = Variant::find()->sku('rad-hood')->one(); - $bodyParams = [ - 'purchasableId' => $variant->id, - 'qty' => 1, - ]; - $request->setBodyParams($bodyParams); - - // Refresh CartController to ensure the request is properly initialized with the new body params - $cartController = new CartController('cart', Plugin::getInstance()); - - $cartController->runAction('update-cart'); - $cart = Plugin::getInstance()->getCarts()->getCart(); - - // First request with `number` should succeed - $bodyParams['number'] = $cart->number; - $bodyParams['qty'] += 1; - - $cartController = new CartController('cart', Plugin::getInstance()); - - $request->setBodyParams($bodyParams); - $result = $cartController->runAction('update-cart'); - - self::assertSame(200, $result->getStatusCode()); - - $cartController = new CartController('cart', Plugin::getInstance()); - - // Second request with same `number` should fail with 429 Too Many Requests - $request->setBodyParams($bodyParams); - - try { - $result = $cartController->runAction('update-cart'); - } catch (TooManyRequestsHttpException $e) { - self::assertSame(429, $e->statusCode); - } - - if ($cart->id) { - Craft::$app->getElements()->deleteElement($cart, true); - } - } -} diff --git a/tests/unit/controllers/CartTest.php b/tests/unit/controllers/CartTest.php deleted file mode 100644 index 736d4d1097..0000000000 --- a/tests/unit/controllers/CartTest.php +++ /dev/null @@ -1,624 +0,0 @@ - - * @since 3.2.0 - */ -class CartTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var CartController - */ - protected CartController $cartController; - - /** - * @var Request - */ - protected Request $request; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - 'sales' => [ - 'class' => SalesFixture::class, - ], - 'customer' => [ - 'class' => CustomerFixture::class, - ], - 'addresses' => [ - 'class' => CustomerAddressFixture::class, - ], - ]; - } - - /** - * @inheritDoc - */ - protected function _before(): void - { - parent::_before(); - - $this->cartController = new CartController('cart', Plugin::getInstance()); - $this->request = Craft::$app->getRequest(); - $this->request->enableCsrfValidation = false; - } - - /** - * @throws InvalidRouteException - */ - public function testGetCart(): void - { - $this->request->headers->set('Accept', 'application/json'); - $return = $this->cartController->runAction('get-cart'); - - self::assertInstanceOf(Response::class, $return); - - $data = $return->data; - self::assertArrayHasKey('cart', $data); - self::assertArrayHasKey('total', $data['cart']); - self::assertEquals(0, $data['cart']['total']); - - // Assert types - self::assertIsString($data['cart']['number']); - self::assertNull($data['cart']['reference']); - self::assertNull($data['cart']['couponCode']); - self::assertIsBool($data['cart']['isCompleted']); - self::assertNull($data['cart']['dateOrdered']); - self::assertNull($data['cart']['datePaid']); - self::assertNull($data['cart']['dateAuthorized']); - self::assertIsString($data['cart']['currency']); - self::assertNull($data['cart']['gatewayId']); - self::assertIsString($data['cart']['lastIp']); - self::assertNull($data['cart']['message']); - self::assertNull($data['cart']['returnUrl']); - self::assertNull($data['cart']['cancelUrl']); - self::assertNull($data['cart']['orderStatusId']); - self::assertIsString($data['cart']['orderLanguage']); - self::assertIsInt($data['cart']['orderSiteId']); - self::assertIsString($data['cart']['origin']); - self::assertNull($data['cart']['billingAddressId']); - self::assertNull($data['cart']['shippingAddressId']); - self::assertIsBool($data['cart']['makePrimaryShippingAddress']); - self::assertIsBool($data['cart']['makePrimaryBillingAddress']); - self::assertIsBool($data['cart']['shippingSameAsBilling']); - self::assertIsBool($data['cart']['billingSameAsShipping']); - self::assertNull($data['cart']['estimatedBillingAddressId']); - self::assertNull($data['cart']['estimatedShippingAddressId']); - self::assertIsBool($data['cart']['estimatedBillingSameAsShipping']); - self::assertIsString($data['cart']['shippingMethodHandle']); - self::assertNull($data['cart']['shippingMethodName']); - self::assertNull($data['cart']['customerId']); - self::assertIsBool($data['cart']['registerUserOnOrderComplete']); - self::assertNull($data['cart']['paymentSourceId']); - self::assertNull($data['cart']['storedTotalPrice']); - self::assertNull($data['cart']['storedTotalPaid']); - self::assertNull($data['cart']['storedItemTotal']); - self::assertNull($data['cart']['storedItemSubtotal']); - self::assertNull($data['cart']['storedTotalShippingCost']); - self::assertNull($data['cart']['storedTotalDiscount']); - self::assertNull($data['cart']['storedTotalTax']); - self::assertNull($data['cart']['storedTotalTaxIncluded']); - self::assertNull($data['cart']['id']); - self::assertIsBool($data['cart']['enabled']); - self::assertIsInt($data['cart']['siteId']); - self::assertIsString($data['cart']['status']); - self::assertIsFloat($data['cart']['adjustmentSubtotal']); - self::assertIsFloat($data['cart']['adjustmentsTotal']); - self::assertIsString($data['cart']['paymentCurrency']); - self::assertIsFloat($data['cart']['paymentAmount']); - self::assertNull($data['cart']['email']); - self::assertIsBool($data['cart']['isPaid']); - self::assertIsFloat($data['cart']['itemSubtotal']); - self::assertIsFloat($data['cart']['itemTotal']); - self::assertIsArray($data['cart']['lineItems']); - self::assertIsArray($data['cart']['orderAdjustments']); - self::assertIsFloat($data['cart']['outstandingBalance']); - self::assertIsString($data['cart']['paidStatus']); - self::assertIsString($data['cart']['recalculationMode']); - self::assertIsString($data['cart']['shortNumber']); - self::assertIsFloat($data['cart']['totalPaid']); - self::assertIsFloat($data['cart']['total']); - self::assertIsFloat($data['cart']['totalPrice']); - self::assertIsInt($data['cart']['totalQty']); - self::assertIsFloat($data['cart']['totalSaleAmount']); - self::assertIsFloat($data['cart']['totalPromotionalAmount']); - self::assertIsFloat($data['cart']['totalWeight']); - self::assertIsString($data['cart']['adjustmentSubtotalAsCurrency']); - self::assertIsString($data['cart']['adjustmentsTotalAsCurrency']); - self::assertIsString($data['cart']['itemSubtotalAsCurrency']); - self::assertIsString($data['cart']['itemTotalAsCurrency']); - self::assertIsString($data['cart']['outstandingBalanceAsCurrency']); - self::assertIsString($data['cart']['paymentAmountAsCurrency']); - self::assertIsString($data['cart']['totalPaidAsCurrency']); - self::assertIsString($data['cart']['totalAsCurrency']); - self::assertIsString($data['cart']['totalPriceAsCurrency']); - self::assertIsString($data['cart']['totalPromotionalAmountAsCurrency']); - self::assertIsString($data['cart']['totalSaleAmountAsCurrency']); - self::assertIsString($data['cart']['totalTaxAsCurrency']); - self::assertIsString($data['cart']['totalTaxIncludedAsCurrency']); - self::assertIsString($data['cart']['totalShippingCostAsCurrency']); - self::assertIsString($data['cart']['totalDiscountAsCurrency']); - self::assertIsString($data['cart']['storedTotalPriceAsCurrency']); - self::assertIsString($data['cart']['storedTotalPaidAsCurrency']); - self::assertIsString($data['cart']['storedItemTotalAsCurrency']); - self::assertIsString($data['cart']['storedItemSubtotalAsCurrency']); - self::assertIsString($data['cart']['storedTotalShippingCostAsCurrency']); - self::assertIsString($data['cart']['storedTotalDiscountAsCurrency']); - self::assertIsString($data['cart']['storedTotalTaxAsCurrency']); - self::assertIsString($data['cart']['storedTotalTaxIncludedAsCurrency']); - self::assertIsString($data['cart']['paidStatusHtml']); - self::assertIsString($data['cart']['customerLinkHtml']); - self::assertIsString($data['cart']['orderStatusHtml']); - self::assertIsFloat($data['cart']['totalTax']); - self::assertIsFloat($data['cart']['totalTaxIncluded']); - self::assertIsFloat($data['cart']['totalShippingCost']); - self::assertIsFloat($data['cart']['totalDiscount']); - self::assertIsArray($data['cart']['availableShippingMethodOptions']); - self::assertIsArray($data['cart']['notices']); - self::assertNull($data['cart']['billingAddress']); - self::assertNull($data['cart']['shippingAddress']); - } - - /** - * @throws Throwable - * @throws ElementNotFoundException - * @throws Exception - * @throws InvalidRouteException - */ - public function testAddSinglePurchasable(): void - { - $this->request->headers->set('Accept', 'application/json'); - $this->request->headers->set('X-Http-Method-Override', 'POST'); - - $variant = Variant::find()->sku('rad-hood')->one(); - $this->request->setBodyParams([ - 'purchasableId' => $variant->id, - 'qty' => 2, - ]); - - $this->cartController->runAction('update-cart'); - $cart = Plugin::getInstance()->getCarts()->getCart(); - - self::assertCount(1, $cart->getLineItems()); - self::assertSame(2, $cart->getTotalQty()); - self::assertSame($variant->getSalePrice() * 2, $cart->getTotal()); - - if ($cart->id) { - Craft::$app->getElements()->deleteElement($cart, true); - } - } - - /** - * @throws Throwable - * @throws ElementNotFoundException - * @throws InvalidPluginException - * @throws Exception - * @throws InvalidRouteException - */ - public function testAddMultiplePurchasables(): void - { - $this->request->headers->set('X-Http-Method-Override', 'POST'); - - $variants = Variant::find()->sku(['rad-hood', 'hct-white'])->all(); - $purchasables = []; - foreach ($variants as $key => $variant) { - $purchasables[] = [ - 'id' => $variant->id, - 'qty' => $key + 1, - ]; - } - $this->request->setBodyParams([ - 'purchasables' => $purchasables, - ]); - - $this->cartController->runAction('update-cart'); - $cart = Plugin::getInstance()->getCarts()->getCart(); - - self::assertCount(2, $cart->getLineItems(), 'Has all items in the car'); - - if ($cart->id) { - Craft::$app->getElements()->deleteElement($cart, true); - } - } - - /** - * @throws ElementNotFoundException - * @throws Exception - * @throws InvalidPluginException - * @throws InvalidRouteException - * @throws Throwable - * @throws \craft\errors\InvalidFieldException - * @throws InvalidConfigException - */ - public function testAddAddressCustomFieldsOnUpdateCart(): void - { - $this->request->headers->set('X-Http-Method-Override', 'POST'); - - $shippingAddress = [ - 'addressLine1' => '1 Main Street', - 'fields' => ['testPhone' => '12345'], - ]; - $billingAddress = [ - 'addressLine1' => '100 Main Street', - 'fields' => ['testPhone' => '67890'], - ]; - - $this->request->setBodyParams([ - 'shippingAddress' => $shippingAddress, - 'billingAddress' => $billingAddress, - ]); - - $this->cartController->runAction('update-cart'); - - $cart = Plugin::getInstance()->getCarts()->getCart(); - - $cartShippingAddress = $cart->getShippingAddress(); - $cartBillingAddress = $cart->getBillingAddress(); - - self::assertEquals($shippingAddress['addressLine1'], $cartShippingAddress->addressLine1); - self::assertEquals($shippingAddress['fields']['testPhone'], $cartShippingAddress->testPhone); - self::assertEquals($billingAddress['addressLine1'], $cartBillingAddress->addressLine1); - self::assertEquals($billingAddress['fields']['testPhone'], $cartBillingAddress->testPhone); - - if ($cart->id) { - Craft::$app->getElements()->deleteElement($cart, true); - } - } - - /** - * @param string $customerHandle - * @param bool $autoSet - * @return void - * @throws ElementNotFoundException - * @throws Exception - * @throws InvalidConfigException - * @throws InvalidPluginException - * @throws InvalidRouteException - * @throws Throwable - * @dataProvider autoSetNewCartAddressesDataProvider - * @since 4.0.4 - */ - public function testAutoSetNewCartAddresses(string $customerHandle, bool $autoSet): void - { - $this->request->headers->set('X-Http-Method-Override', 'POST'); - $originalStoresService = Plugin::getInstance()->get('stores'); - $storesService = $this->make(Stores::class, [ - 'getStoreById' => function(int $id) use ($autoSet) { - /** @var Store $store */ - $store = Plugin::getInstance()->getStores()->getAllStores()->firstWhere('id', $id); - $store->setAutoSetNewCartAddresses($autoSet); - return $store; - }, - ]); - Plugin::getInstance()->set('stores', $storesService); - - $customerFixture = $this->tester->grabFixture('customer'); - /** @var User|CustomerBehavior $customer */ - $customer = $customerFixture->getElement($customerHandle); - Craft::$app->getUser()->setIdentity( - Craft::$app->getUsers()->getUserById($customer->id) - ); - $customerShippingAddress = $customer->getPrimaryShippingAddress(); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - $bodyParams = [ - 'purchasableId' => $product->getDefaultVariant()->id, - 'qty' => 2, - ]; - - $this->request->setBodyParams($bodyParams); - - $this->cartController->runAction('update-cart'); - - $cart = Plugin::getInstance()->getCarts()->getCart(); - - $shippingAddress = $cart->getShippingAddress(); - - if ($autoSet === true) { - self::assertEquals($customerShippingAddress->addressLine1, $shippingAddress->addressLine1); - } else { - self::assertNull($shippingAddress); - } - - Plugin::getInstance()->getCarts()->forgetCart(); - - if ($autoSet === true) { - Craft::$app->getElements()->deleteElement($cart->getShippingAddress(), true); - } - - Craft::$app->getElements()->deleteElement($cart, true); - Plugin::getInstance()->set('stores', $originalStoresService); - } - - /** - * @return array[] - * @since 4.0.4 - */ - public function autoSetNewCartAddressesDataProvider(): array - { - return [ - 'auto-set' => [ - 'customer3', // customer - true, // auto set - ], - 'dont-auto-set' => [ - 'customer3', // customer - true, // auto set - ], - ]; - } - - /** - * @param bool|null $saveBillingAddress - * @param bool|null $saveShippingAddress - * @param bool|null $saveBoth - * @return void - * @throws ElementNotFoundException - * @throws Exception - * @throws InvalidConfigException - * @throws InvalidPluginException - * @throws InvalidRouteException - * @throws Throwable - * @since 4.3.0 - * @dataProvider setSaveAddressesDataProvider - */ - public function testSetSaveAddresses(?bool $saveBillingAddress, ?bool $saveShippingAddress, ?bool $saveBoth): void - { - $this->request->headers->set('X-Http-Method-Override', 'POST'); - - $bodyParams = []; - if ($saveBoth) { - $bodyParams['saveAddressesOnOrderComplete'] = true; - } else { - $bodyParams['saveBillingAddressOnOrderComplete'] = $saveBillingAddress; - $bodyParams['saveShippingAddressOnOrderComplete'] = $saveShippingAddress; - } - - $this->request->setBodyParams($bodyParams); - $this->cartController->runAction('update-cart'); - - $cart = Plugin::getInstance()->getCarts()->getCart(); - - if ($saveBoth) { - self::assertTrue($cart->saveBillingAddressOnOrderComplete); - self::assertTrue($cart->saveShippingAddressOnOrderComplete); - } else { - self::assertEquals($saveBillingAddress, $cart->saveBillingAddressOnOrderComplete); - self::assertEquals($saveShippingAddress, $cart->saveShippingAddressOnOrderComplete); - } - - Plugin::getInstance()->getCarts()->forgetCart(); - - Craft::$app->getElements()->deleteElement($cart, true); - } - - /** - * @return array[] - * @since 4.3.0 - */ - public function setSaveAddressesDataProvider(): array - { - return [ - 'save-billing' => [ - true, // save billing - false, // save shipping - false, // save both - ], - 'save-shipping' => [ - false, // save billing - true, // save shipping - false, // save both - ], - 'save-both' => [ - false, // save billing - false, // save shipping - true, // save both - ], - 'save-both-individually' => [ - true, // save billing - true, // save shipping - false, // save both - ], - ]; - } - - /** - * @param string $whichAddress - * @param bool $validShipping - * @param bool $validBilling - * @return void - * @throws ElementNotFoundException - * @throws Exception - * @throws InvalidConfigException - * @throws InvalidRouteException - * @throws Throwable - * @dataProvider setAddressesOnCartDataProvider - */ - public function testSetAddressesOnCart(string $whichAddress = 'shipping', bool $validShipping = true, bool $validBilling = true): void - { - $this->request->headers->set('X-Http-Method-Override', 'POST'); - - $customerFixture = $this->tester->grabFixture('customer'); - /** @var User|CustomerBehavior $customer */ - $customer = $customerFixture->getElement('credentialed-user'); - Craft::$app->getUser()->setIdentity( - Craft::$app->getUsers()->getUserById($customer->id) - ); - $customerShippingAddress = $customer->getPrimaryShippingAddress(); - $customerBillingAddress = $customer->getPrimaryBillingAddress(); - - Event::on(Address::class, Address::EVENT_DEFINE_RULES, function(DefineRulesEvent $event) { - $event->rules[] = [['addressLine1'], 'required']; - }); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - $bodyParams = [ - 'purchasableId' => $product->getDefaultVariant()->id, - 'qty' => 2, - ]; - - if ($whichAddress === 'shipping' || $whichAddress === 'both') { - $bodyParams['shippingAddressId'] = $customerShippingAddress->id; - } - - if ($whichAddress === 'billing' || $whichAddress === 'both') { - $bodyParams['billingAddressId'] = $customerBillingAddress->id; - } - - if (!$validShipping) { - Db::update(Table::ADDRESSES, ['addressLine1' => null], ['id' => $customerShippingAddress->id]); - } - - if (!$validBilling) { - Db::update(Table::ADDRESSES, ['addressLine1' => null], ['id' => $customerBillingAddress->id]); - } - - $this->request->setBodyParams($bodyParams); - $primaryStore = Plugin::getInstance()->getStores()->getPrimaryStore(); - $originalSettingValue = $primaryStore->getAutoSetNewCartAddresses(false); - $primaryStore->setAutoSetNewCartAddresses(false); - - $this->cartController->runAction('update-cart'); - - $cart = Plugin::getInstance()->getCarts()->getCart(); - - $shippingAddress = $cart->getShippingAddress(); - $billingAddress = $cart->getBillingAddress(); - - if ($whichAddress === 'shipping' || $whichAddress === 'both') { - if (!$validShipping) { - self::assertNull($shippingAddress); - self::assertTrue($cart->hasErrors()); - // loop through the error keys to make sure there is one starting with `shippingAddress` - $errorKeys = array_keys($cart->getErrors()); - $found = false; - foreach ($errorKeys as $errorKey) { - if (str_starts_with($errorKey, 'shippingAddress')) { - $found = true; - break; - } - } - - self::assertTrue($found); - } else { - self::assertEquals($customerShippingAddress->addressLine1, $shippingAddress->addressLine1); - } - } - - if ($whichAddress === 'billing' || $whichAddress === 'both') { - if (!$validBilling) { - self::assertNull($billingAddress); - self::assertTrue($cart->hasErrors()); - // loop through the error keys to make sure there is one starting with `billingAddress` - $errorKeys = array_keys($cart->getErrors()); - $found = false; - foreach ($errorKeys as $errorKey) { - if (str_starts_with($errorKey, 'billingAddress')) { - $found = true; - break; - } - } - - self::assertTrue($found); - } else { - self::assertEquals($customerBillingAddress->addressLine1, $billingAddress->addressLine1); - } - } - - Plugin::getInstance()->getCarts()->forgetCart(); - - if (($whichAddress === 'shipping' || $whichAddress === 'both') && $validShipping) { - Craft::$app->getElements()->deleteElement($cart->getShippingAddress(), true); - } - - if (($whichAddress === 'billing' || $whichAddress === 'both') && $validBilling) { - Craft::$app->getElements()->deleteElement($cart->getBillingAddress(), true); - } - - Craft::$app->getElements()->deleteElement($cart, true); - - $primaryStore->setAutoSetNewCartAddresses($originalSettingValue); - } - - /** - * @return array[] - */ - public function setAddressesOnCartDataProvider(): array - { - return [ - 'shipping' => [ - 'shipping', - true, - true, - ], - 'billing' => [ - 'billing', - true, - true, - ], - 'both' => [ - 'both', - true, - true, - ], - 'invalid-shipping' => [ - 'shipping', - false, - true, - ], - ]; - } -} diff --git a/tests/unit/controllers/EmailPreviewControllerTest.php b/tests/unit/controllers/EmailPreviewControllerTest.php deleted file mode 100644 index 67f5e1b2cf..0000000000 --- a/tests/unit/controllers/EmailPreviewControllerTest.php +++ /dev/null @@ -1,110 +0,0 @@ - - * @since 3.2.14.1 - */ -class EmailPreviewControllerTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var EmailPreviewController - */ - protected EmailPreviewController $controller; - - /** - * @var Request - */ - protected Request $request; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'emails' => [ - 'class' => EmailsFixture::class, - ], - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @inheritDoc - */ - protected function _before(): void - { - parent::_before(); - - // Mock admin user - Craft::$app->getUser()->setIdentity( - Craft::$app->getUsers()->getUserById('1') - ); - Craft::$app->getUser()->getIdentity()->password = '$2y$13$tAtJfYFSRrnOkIbkruGGEu7TPh0Ixvxq0r.XgWqIgNWuWpxpA7SxK'; - - $this->controller = new EmailPreviewController('emailPreview', Plugin::getInstance()); - $this->request = Craft::$app->getRequest(); - $this->request->enableCsrfValidation = false; - } - - public function testRenderRandomOrder(): void - { - $email = $this->tester->grabFixture('emails')['order-confirmation']; - Craft::$app->getRequest()->setQueryParams(['email' => $email['id'] . ':' . $email['storeId']]); - - $response = $this->controller->runAction('render'); - (new TemplateResponseFormatter())->format($response); - - self::assertInstanceOf(Response::class, $response); - self::assertIsString($response->content); - self::assertStringContainsString('Order Confirmation', $response->content); - self::assertRegExp('/

Order Confirmation [0-9a-zA-Z]{7}<\/h1>/', $response->content); - } - - public function testRenderSpecificOrder(): void - { - $email = $this->tester->grabFixture('emails')['order-confirmation']; - /** @var Order $order */ - $order = $this->tester->grabFixture('orders')->getElement('completed-new'); - - Craft::$app->getRequest()->setQueryParams([ - 'email' => $email['id'] . ':' . $email['storeId'], - 'number' => $order->number, - ]); - - $response = $this->controller->runAction('render'); - (new TemplateResponseFormatter())->format($response); - - self::assertInstanceOf(Response::class, $response); - self::assertIsString($response->content); - self::assertStringContainsString('Order Confirmation', $response->content); - self::assertStringContainsString('

Order Confirmation ' . $order->shortNumber . '

', $response->content); - } -} diff --git a/tests/unit/controllers/OrdersControllerTest.php b/tests/unit/controllers/OrdersControllerTest.php deleted file mode 100644 index 017a01952e..0000000000 --- a/tests/unit/controllers/OrdersControllerTest.php +++ /dev/null @@ -1,296 +0,0 @@ - - * @since 3.2.14 - */ -class OrdersControllerTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var OrdersController - */ - protected OrdersController $controller; - - /** - * @var Request - */ - protected Request $request; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @inheritDoc - */ - protected function _before(): void - { - parent::_before(); - - // Mock admin user - Craft::$app->getUser()->setIdentity( - Craft::$app->getUsers()->getUserById('1') - ); - Craft::$app->getUser()->getIdentity()->password = '$2y$13$tAtJfYFSRrnOkIbkruGGEu7TPh0Ixvxq0r.XgWqIgNWuWpxpA7SxK'; - - $this->controller = new OrdersController('orders', Plugin::getInstance()); - $this->request = Craft::$app->getRequest(); - $this->request->enableCsrfValidation = false; - } - - public function testPurchasablesTable(): void - { - $this->request->getHeaders()->set('Accept', 'application/json'); - Craft::$app->getRequest()->setQueryParams(['siteId' => Craft::$app->getSites()->getPrimarySite()->id]); - - $response = $this->controller->runAction('purchasables-table'); - - self::assertInstanceOf(Response::class, $response); - - self::assertArrayHasKey('pagination', $response->data); - self::assertArrayHasKey('data', $response->data); - - self::assertSame(3, $response->data['pagination']['total']); - self::assertCount(3, $response->data['data']); - - $purchasable = array_pop($response->data['data']); - - $keys = ['id', 'price', 'priceAsCurrency', 'description', 'sku', 'priceAsCurrency', 'isAvailable', 'detail']; - foreach ($keys as $key) { - self::assertArrayHasKey($key, $purchasable); - } - - self::assertEquals('hct-blue', $purchasable['sku']); - } - - public function testPurchasablesTableSort(): void - { - $this->request->getHeaders()->set('Accept', 'application/json'); - - Craft::$app->getRequest()->setQueryParams([ - 'sort' => 'sku|desc', - 'siteId' => Craft::$app->getSites()->getPrimarySite()->id, - ]); - - $response = $this->controller->runAction('purchasables-table'); - - self::assertInstanceOf(Response::class, $response); - - $purchasable = array_pop($response->data['data']); - - self::assertEquals('hct-blue', $purchasable['sku']); - } - - public function testCustomerSearch(): void - { - $this->request->getHeaders()->set('Accept', 'application/json'); - - Craft::$app->getRequest()->setQueryParams(['query' => 'customer1']); - $response = $this->controller->runAction('customer-search'); - - self::assertEquals(200, $response->statusCode); - self::assertIsArray($response->data); - self::assertCount(1, $response->data); - $customer = $response->data['customers'][0] ?? []; - $keys = [ - 'cpEditUrl', - 'email', - 'id', - 'photo', - 'status', - 'totalAddresses', - ]; - - foreach ($keys as $key) { - self::assertArrayHasKey($key, $customer); - } - - self::assertEquals('customer1@crafttest.com', $customer['email']); - } - - public function testGetIndexSourcesBadgeCounts(): void - { - $this->request->getHeaders()->set('Accept', 'application/json'); - - $response = $this->controller->runAction('get-index-sources-badge-counts'); - - self::assertEquals(200, $response->statusCode); - self::assertIsArray($response->data); - self::assertArrayHasKey('counts', $response->data); - self::assertArrayHasKey('total', $response->data); - self::assertCount(4, $response->data['counts']); - - $keys = ['orderStatusId', 'handle', 'orderCount']; - foreach ($keys as $key) { - self::assertArrayHasKey($key, array_shift($response->data['counts'])); - } - } - - public function testGetShippingMethodOptionsReturnsOptions(): void - { - $ordersFixture = $this->tester->grabFixture('orders'); - $order = $ordersFixture->getElement('completed-new'); - - $this->request->getHeaders()->set('Accept', 'application/json'); - $this->request->getHeaders()->set('X-Http-Method-Override', 'POST'); - $this->request->setRawBody(Json::encode($this->_buildOrderPayload($order))); - - $response = $this->controller->runAction('get-shipping-method-options'); - - self::assertEquals(200, $response->statusCode); - self::assertArrayHasKey('shippingMethodOptions', $response->data); - self::assertNotEmpty($response->data['shippingMethodOptions']); - - $option = reset($response->data['shippingMethodOptions']); - self::assertArrayHasKey('handle', $option); - self::assertArrayHasKey('name', $option); - self::assertArrayHasKey('matchesOrder', $option); - } - - public function testGetShippingMethodOptionsInvalidOrderId(): void - { - $this->request->getHeaders()->set('Accept', 'application/json'); - $this->request->getHeaders()->set('X-Http-Method-Override', 'POST'); - $this->request->setRawBody(Json::encode(['order' => ['id' => 1]])); - - $response = $this->controller->runAction('get-shipping-method-options'); - - self::assertEquals(400, $response->statusCode); - } - - public function testGetShippingMethodOptionsIncludesCustomRuntimeMethod(): void - { - $ordersFixture = $this->tester->grabFixture('orders'); - $order = $ordersFixture->getElement('completed-new'); - - $customMethod = new class() implements ShippingMethodInterface { - public function getType(): string - { - return 'Custom'; - } - public function getId(): ?int - { - return null; - } - public function getName(): string - { - return 'My Custom Carrier'; - } - public function getHandle(): string - { - return 'myCustomCarrier'; - } - public function getCpEditUrl(): string - { - return ''; - } - public function getShippingRules(): Collection - { - return collect(); - } - public function getIsEnabled(): bool - { - return true; - } - public function getPriceForOrder(Order $order): float - { - return 0.0; - } - public function getMatchingShippingRule(Order $order): ?ShippingRuleInterface - { - return null; - } - public function matchOrder(Order $order): bool - { - return true; - } - }; - - Event::on( - ShippingMethods::class, - ShippingMethods::EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS, - $listener = function(RegisterAvailableShippingMethodsEvent $e) use ($customMethod) { - $e->setShippingMethods($e->getShippingMethods()->push($customMethod)); - } - ); - - $this->request->getHeaders()->set('Accept', 'application/json'); - $this->request->getHeaders()->set('X-Http-Method-Override', 'POST'); - $this->request->setRawBody(Json::encode($this->_buildOrderPayload($order))); - - try { - $response = $this->controller->runAction('get-shipping-method-options'); - } finally { - Event::off(ShippingMethods::class, ShippingMethods::EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS, $listener); - } - - self::assertEquals(200, $response->statusCode); - - $options = $response->data['shippingMethodOptions']; - self::assertArrayHasKey('myCustomCarrier', $options); - self::assertEquals('My Custom Carrier', $options['myCustomCarrier']['name']); - self::assertEquals('myCustomCarrier', $options['myCustomCarrier']['handle']); - } - - private function _buildOrderPayload(Order $order, array $overrides = []): array - { - return [ - 'order' => array_merge([ - 'id' => $order->id, - 'recalculationMode' => Order::RECALCULATION_MODE_ALL, - 'reference' => $order->reference, - 'customerId' => $order->getCustomerId(), - 'couponCode' => $order->couponCode, - 'isCompleted' => $order->isCompleted, - 'orderStatusId' => $order->orderStatusId, - 'orderSiteId' => $order->orderSiteId, - 'message' => $order->message, - 'shippingMethodHandle' => $order->shippingMethodHandle, - 'shippingMethodName' => $order->shippingMethodName, - 'notices' => [], - 'dateOrdered' => null, - 'lineItems' => [], - 'orderAdjustments' => [], - ], $overrides), - ]; - } -} diff --git a/tests/unit/controllers/PaymentSourcesControllerTest.php b/tests/unit/controllers/PaymentSourcesControllerTest.php deleted file mode 100644 index fd14b6f55e..0000000000 --- a/tests/unit/controllers/PaymentSourcesControllerTest.php +++ /dev/null @@ -1,181 +0,0 @@ - - */ -class PaymentSourcesControllerTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var PaymentSourcesController - */ - protected PaymentSourcesController $controller; - - /** - * @var Request - */ - protected Request $request; - - /** - * @var PaymentSources - */ - private PaymentSources $_originalPaymentSourcesService; - - public function _fixtures(): array - { - return [ - 'customer' => [ - 'class' => CustomerFixture::class, - ], - ]; - } - - protected function _before(): void - { - parent::_before(); - - $this->controller = new PaymentSourcesController('payment-sources', Plugin::getInstance()); - $this->request = Craft::$app->getRequest(); - $this->request->enableCsrfValidation = false; - - $this->_originalPaymentSourcesService = Plugin::getInstance()->getPaymentSources(); - } - - protected function _after(): void - { - Plugin::getInstance()->set('paymentSources', $this->_originalPaymentSourcesService); - parent::_after(); - } - - private function _makePaymentSource(int $customerId): PaymentSource - { - $paymentSource = new PaymentSource(); - $paymentSource->id = 999; - $paymentSource->customerId = $customerId; - $paymentSource->gatewayId = 1; - $paymentSource->token = 'tok_test'; - $paymentSource->description = 'Test payment source'; - $paymentSource->response = ''; - - return $paymentSource; - } - - /** - * A user with `commerce-editOrders` but not Craft's `editUsers` permission must not be - * able to delete another customer's payment source. - */ - public function testDeleteDeniedForNonOwnerWithoutEditUsersPermission(): void - { - $this->_assertDeleteDenied(['commerce-manageOrders', 'commerce-editOrders']); - } - - /** - * A user with Craft's `editUsers`/`viewUsers` permissions but neither `commerce-editOrders` - * nor `commerce-deleteOrders` must not be able to delete another customer's payment source. - */ - public function testDeleteDeniedForNonOwnerWithoutOrderPermission(): void - { - $this->_assertDeleteDenied(['viewUsers', 'editUsers', 'commerce-manageOrders']); - } - - /** - * A user with `commerce-editOrders` and Craft's `editUsers` permission is allowed to - * delete another customer's payment source. - */ - public function testDeleteAllowedForNonOwnerWithEditOrdersAndEditUsersPermissions(): void - { - $this->_assertDeleteAllowed(['commerce-manageOrders', 'commerce-editOrders', 'viewUsers', 'editUsers']); - } - - /** - * A user with `commerce-deleteOrders` and Craft's `editUsers` permission is allowed to - * delete another customer's payment source. - */ - public function testDeleteAllowedForNonOwnerWithDeleteOrdersAndEditUsersPermissions(): void - { - $this->_assertDeleteAllowed(['commerce-manageOrders', 'commerce-deleteOrders', 'viewUsers', 'editUsers']); - } - - private function _assertDeleteAllowed(array $operatorPermissions): void - { - /** @var User $owner */ - $owner = $this->tester->grabFixture('customer')->getElement('customer1'); - /** @var User $operator */ - $operator = $this->tester->grabFixture('customer')->getElement('customer2'); - - Craft::$app->getUserPermissions()->saveUserPermissions($operator->id, $operatorPermissions); - - $paymentSource = $this->_makePaymentSource($owner->id); - - $paymentSourcesService = $this->make(PaymentSources::class, [ - 'getPaymentSourceById' => fn() => $paymentSource, - 'deletePaymentSourceById' => fn() => true, - ]); - Plugin::getInstance()->set('paymentSources', $paymentSourcesService); - - Craft::$app->getUser()->setIdentity($operator); - - $this->request->headers->set('Accept', 'application/json'); - $this->request->headers->set('X-Http-Method-Override', 'POST'); - $this->request->setBodyParams(['id' => $paymentSource->id]); - - $response = $this->controller->runAction('delete'); - - self::assertNotNull($response); - self::assertSame(200, $response->statusCode); - self::assertSame('Payment source deleted.', $response->data['message']); - } - - private function _assertDeleteDenied(array $operatorPermissions): void - { - /** @var User $owner */ - $owner = $this->tester->grabFixture('customer')->getElement('customer1'); - /** @var User $operator */ - $operator = $this->tester->grabFixture('customer')->getElement('customer2'); - - Craft::$app->getUserPermissions()->saveUserPermissions($operator->id, $operatorPermissions); - - $paymentSource = $this->_makePaymentSource($owner->id); - - $paymentSourcesService = $this->make(PaymentSources::class, [ - 'getPaymentSourceById' => fn() => $paymentSource, - 'deletePaymentSourceById' => function() { - self::fail('deletePaymentSourceById() should not be called when the operator lacks the full permission set.'); - }, - ]); - Plugin::getInstance()->set('paymentSources', $paymentSourcesService); - - Craft::$app->getUser()->setIdentity($operator); - - $this->request->headers->set('X-Http-Method-Override', 'POST'); - $this->request->setBodyParams(['id' => $paymentSource->id]); - - $response = $this->controller->runAction('delete'); - - self::assertNull($response); - } -} diff --git a/tests/unit/controllers/ShippingRulesControllerTest.php b/tests/unit/controllers/ShippingRulesControllerTest.php deleted file mode 100644 index 2e3f850c37..0000000000 --- a/tests/unit/controllers/ShippingRulesControllerTest.php +++ /dev/null @@ -1,237 +0,0 @@ - - * @since 4.0.4 - */ -class ShippingRulesControllerTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var ShippingRulesController - */ - protected ShippingRulesController $controller; - - /** - * @var Request - */ - protected Request $request; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'shippingMethods' => [ - 'class' => ShippingMethodsFixture::class, - ], - 'shipping' => [ - 'class' => ShippingFixture::class, - ], - ]; - } - - /** - * @inheritDoc - */ - protected function _before(): void - { - parent::_before(); - - // Mock admin user - Craft::$app->getUser()->setIdentity( - Craft::$app->getUsers()->getUserById('1') - ); - Craft::$app->getUser()->getIdentity()->password = '$2y$13$tAtJfYFSRrnOkIbkruGGEu7TPh0Ixvxq0r.XgWqIgNWuWpxpA7SxK'; - - $this->controller = new ShippingRulesController('shippingRules', Plugin::getInstance()); - $this->request = Craft::$app->getRequest(); - $this->request->enableCsrfValidation = false; - } - - /** - * @return void - * @throws InvalidRouteException - */ - public function testReorder(): void - { - $this->request->headers->set('Accept', 'application/json'); - $this->request->headers->set('X-Http-Method-Override', 'POST'); - $shippingFixture = $this->tester->grabFixture('shipping'); - - $ids = [$shippingFixture->data['us-only-2']['id'], $shippingFixture->data['us-only']['id']]; - $this->request->setBodyParams(['ids' => Json::encode($ids)]); - - $response = $this->controller->runAction('reorder'); - - self::assertEquals(200, $response->statusCode); - self::assertIsArray($response->data); - self::assertEmpty($response->data); - - // Check rules have been reordered - $results = (new Query()) - ->from(Table::SHIPPINGRULES) - ->select(['id']) - ->where(['id' => $ids]) - ->orderBy(['priority' => SORT_ASC]) - ->column(); - - self::assertEquals($ids, $results); - } - - /** - * @return void - * @throws InvalidRouteException - */ - public function testSave(): void - { - $this->request->headers->set('X-Http-Method-Override', 'POST'); - $shippingFixture = $this->tester->grabFixture('shipping'); - $rule = $shippingFixture->data['us-only']; - $methodsFixture = $this->tester->grabFixture('shippingMethods'); - $method = ArrayHelper::firstWhere($methodsFixture->data, 'id', $rule['methodId']); - $newName = $rule['name'] . ' saved'; - - $this->request->setBodyParams([ - 'id' => $rule['id'], - 'storeId' => $method['storeId'], - 'name' => $newName, - 'methodId' => $rule['methodId'], - 'enabled' => $rule['enabled'], - 'orderConditionFormula' => '', - 'baseRate' => ['value' => 0], - 'perItemRate' => ['value' => 0], - 'weightRate' => ['value' => 0], - 'percentageRate' => 0, - 'minRate' => ['value' => 0], - 'maxRate' => ['value' => 0], - 'ruleCategories' => [], - 'orderCondition' => null, - ]); - - $this->controller->runAction('save'); - - // Check rules have been reordered - $result = (new Query()) - ->from(Table::SHIPPINGRULES) - ->select(['name']) - ->where(['id' => $rule['id']]) - ->scalar(); - - self::assertEquals($newName, $result); - } - - /** - * @return void - * @throws InvalidRouteException - */ - public function testDeleteAjax(): void - { - // Test Ajax delete - $this->request->headers->set('X-Requested-With', 'XMLHttpRequest'); - $this->request->headers->set('Accept', 'application/json'); - $this->request->headers->set('X-Http-Method-Override', 'POST'); - $shippingFixture = $this->tester->grabFixture('shipping'); - - $this->request->setBodyParams(['id' => $shippingFixture->data['us-only']['id']]); - - $response = $this->controller->runAction('delete'); - - self::assertEquals(200, $response->statusCode); - self::assertIsArray($response->data); - self::assertEmpty($response->data); - } - - /** - * @return void - * @throws InvalidRouteException - */ - public function testDelete(): void - { - $originalEdition = Plugin::getInstance()->edition; - - $this->request->headers->set('X-Http-Method-Override', 'POST'); - $shippingFixture = $this->tester->grabFixture('shipping'); - - $this->request->setBodyParams(['id' => $shippingFixture->data['us-only-2']['id']]); - - $this->controller->runAction('delete'); - - self::assertFalse(false, (new Query()) - ->from(Table::SHIPPINGRULES) - ->select(['name']) - ->where(['id' => $shippingFixture->data['us-only-2']['id']]) - ->exists()); - - Plugin::getInstance()->edition = $originalEdition; - } - - /** - * @return void - * @throws InvalidRouteException - */ - public function testDuplicate(): void - { - $this->request->headers->set('X-Http-Method-Override', 'POST'); - $shippingFixture = $this->tester->grabFixture('shipping'); - $rule = $shippingFixture->data['us-only']; - $methodsFixture = $this->tester->grabFixture('shippingMethods'); - $method = ArrayHelper::firstWhere($methodsFixture->data, 'id', $rule['methodId']); - - $this->request->setBodyParams([ - 'id' => $rule['id'], - 'storeId' => $method['storeId'], - 'name' => $rule['name'], - 'methodId' => $rule['methodId'], - 'enabled' => $rule['enabled'], - 'orderConditionFormula' => '', - 'baseRate' => ['value' => 0], - 'perItemRate' => ['value' => 0], - 'weightRate' => ['value' => 0], - 'percentageRate' => 0, - 'minRate' => ['value' => 0], - 'maxRate' => ['value' => 0], - 'ruleCategories' => [], - 'orderCondition' => null, - ]); - - $this->controller->runAction('duplicate'); - - // Check rules have been reordered - $result = (new Query()) - ->from(Table::SHIPPINGRULES) - ->select(['id']) - ->where(['name' => $rule['name']]) - ->count(); - - self::assertEquals(2, $result); - } -} diff --git a/tests/unit/elements/address/CustomerAddressBehaviorTest.php b/tests/unit/elements/address/CustomerAddressBehaviorTest.php deleted file mode 100644 index b74b8c449d..0000000000 --- a/tests/unit/elements/address/CustomerAddressBehaviorTest.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @since 5.0.10 - */ -class CustomerAddressBehaviorTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - public function testHasPropertiesAndMethods(): void - { - $address = \Craft::createObject(['class' => Address::class, 'primaryOwnerId' => User::find()->one()->id]); - - self::assertInstanceOf(CustomerAddressBehavior::class, $address->getBehavior('commerce:address')); - self::assertArrayHasKey('isPrimaryBilling', $address->toArray()); - self::assertArrayHasKey('isPrimaryShipping', $address->toArray()); - } -} diff --git a/tests/unit/elements/donation/DonationQueryTest.php b/tests/unit/elements/donation/DonationQueryTest.php deleted file mode 100644 index ee43f5a871..0000000000 --- a/tests/unit/elements/donation/DonationQueryTest.php +++ /dev/null @@ -1,94 +0,0 @@ - - * @since 5.0.0 - */ -class DonationQueryTest extends Unit -{ - /** - * @var UnitTester - */ - protected $tester; - - public $depends = [ - StoreFixture::class, - ]; - - /** - * @return void - */ - public function testQuery(): void - { - self::assertInstanceOf(DonationQuery::class, Donation::find()); - self::assertInstanceOf(PurchasableQuery::class, Donation::find()); - } - - /** - * @param bool $availableForPurchase - * @return void - * @dataProvider availableForPurchaseDataProvider - */ - public function testAvailableForPurchase(bool $availableForPurchase): void - { - // Make sure donation is installed - if ((int)(new Query())->from(Table::DONATIONS)->count() === 0) { - $primaryStore = Plugin::getInstance()->getStores()->getPrimaryStore(); - $primarySite = Craft::$app->getSites()->getPrimarySite(); - $donation = new Donation(); - $donation->siteId = $primarySite->id; - $donation->sku = 'DONATION-CC5'; - $donation->availableForPurchase = false; - $donation->taxCategoryId = Plugin::getInstance()->getTaxCategories()->getDefaultTaxCategory()->id; - $donation->shippingCategoryId = Plugin::getInstance()->getShippingCategories()->getDefaultShippingCategory($primaryStore->id)->id; - Craft::$app->getElements()->saveElement($donation); - } - - $query = Donation::find(); - - self::assertTrue(method_exists($query, 'availableForPurchase')); - $query->availableForPurchase($availableForPurchase); - $query->status(null); - $all = $query->all(); - - // Donation on installation is not available for purchase - self::assertCount($availableForPurchase ? 0 : 1, $all); - - if (isset($donation) || count($all)) { - // Delete donation - $donation ??= $all[0]; - Craft::$app->getElements()->deleteElement($donation, true); - } - } - - /** - * @return array - */ - public function availableForPurchaseDataProvider(): array - { - return [ - 'available' => [true], - 'not-available' => [false], - ]; - } -} diff --git a/tests/unit/elements/order/OrderAddressesTest.php b/tests/unit/elements/order/OrderAddressesTest.php deleted file mode 100644 index 7681673f32..0000000000 --- a/tests/unit/elements/order/OrderAddressesTest.php +++ /dev/null @@ -1,198 +0,0 @@ - - * @since 4.1 - */ -class OrderAddressesTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var Order - */ - protected Order $order; - - /** - * @var Plugin|null - */ - protected ?Plugin $pluginInstance = null; - - /** - * @param array|null $billingAddress - * @param array|null $shippingAddress - * @param bool $expected - * @return void - * @throws InvalidConfigException - * @dataProvider hasMatchingAddressesDataProvider - */ - public function testHasMatchingAddresses(?array $billingAddress, ?array $shippingAddress, bool $expected, ?array $attributes = null): void - { - $this->order->setBillingAddress(Craft::createObject($billingAddress)); - $this->order->setShippingAddress(Craft::createObject($shippingAddress)); - - self::assertSame($expected, $this->order->hasMatchingAddresses($attributes)); - } - - public function hasMatchingAddressesDataProvider(): array - { - return [ - 'all-matching' => [ - [ - 'class' => Address::class, - 'fullName' => 'Johnny Appleseed', - 'addressLine1' => '1 Main Street', - ], - [ - 'class' => Address::class, - 'fullName' => 'Johnny Appleseed', - 'addressLine1' => '1 Main Street', - ], - true, - ], - 'no-matching-address' => [ - [ - 'class' => Address::class, - 'fullName' => 'Johnny Appleseed', - 'addressLine1' => '1 Main Street', - ], - [ - 'class' => Address::class, - 'fullName' => 'Johnny Appleseed', - 'addressLine1' => '123 Main Street', - ], - false, - ], - 'no-matching-name' => [ - [ - 'class' => Address::class, - 'fullName' => 'Johnny Appleseed', - 'addressLine1' => '1 Main Street', - ], - [ - 'class' => Address::class, - 'fullName' => 'Jenny Appleseed', - 'addressLine1' => '1 Main Street', - ], - false, - ], - 'all-matching-full' => [ - [ - 'class' => Address::class, - 'fullName' => 'Johnny Appleseed', - 'addressLine1' => '1 Main Street', - 'addressLine2' => 'SW', - 'locality' => 'Bend', - 'administrativeArea' => 'OR', - 'countryCode' => 'US', - 'postalCode' => '12345', - ], - [ - 'class' => Address::class, - 'fullName' => 'Johnny Appleseed', - 'addressLine1' => '1 Main Street', - 'addressLine2' => 'SW', - 'locality' => 'Bend', - 'administrativeArea' => 'OR', - 'countryCode' => 'US', - 'postalCode' => '12345', - ], - true, - ], - 'attributes-matching' => [ - [ - 'class' => Address::class, - 'fullName' => 'Johnny Appleseed', - 'addressLine1' => '1 Main Street', - 'addressLine2' => 'SW', - 'locality' => 'Bend', - 'administrativeArea' => 'OR', - 'countryCode' => 'US', - 'postalCode' => '12345', - ], - [ - 'class' => Address::class, - 'fullName' => 'Johnny Appleseed', - 'addressLine1' => '123 Main Street', - 'addressLine2' => 'SW', - 'locality' => 'Bend', - 'administrativeArea' => 'OR', - 'countryCode' => 'US', - 'postalCode' => '12345', - ], - true, - [ - 'addressLine2', - 'locality', - 'administrativeArea', - ], - ], - 'attributes-not-matching' => [ - [ - 'class' => Address::class, - 'fullName' => 'Johnny Appleseed', - 'addressLine1' => '1 Main Street', - 'addressLine2' => 'SW', - 'locality' => 'Bend', - 'administrativeArea' => 'OR', - 'countryCode' => 'US', - 'postalCode' => '12345', - ], - [ - 'class' => Address::class, - 'fullName' => 'Johnny Appleseed', - 'addressLine1' => '123 Main Street', - 'addressLine2' => 'SW', - 'locality' => 'Bend', - 'administrativeArea' => 'OR', - 'countryCode' => 'US', - 'postalCode' => '12345', - ], - false, - [ - 'addressLine1', - ], - ], - ]; - } - - /** - * @inheritdoc - */ - protected function _before(): void - { - parent::_before(); - - $this->pluginInstance = Plugin::getInstance(); - - $this->order = new Order(); - } - - /** - * @inheritdoc - */ - protected function _after(): void - { - parent::_after(); - } -} diff --git a/tests/unit/elements/order/OrderCustomerTest.php b/tests/unit/elements/order/OrderCustomerTest.php deleted file mode 100644 index 8404d5030f..0000000000 --- a/tests/unit/elements/order/OrderCustomerTest.php +++ /dev/null @@ -1,112 +0,0 @@ - - * @since 4.3.0 - */ -class OrderCustomerTest extends Unit -{ - /** - * @var UnitTester - */ - protected $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @param string $email - * @return void - * @throws Exception - * @throws InvalidConfigException - * @dataProvider emailDataProvider - */ - public function testSetEmail(string $email): void - { - \Craft::$app->set('deprecator', $this->make(Deprecator::class, [ - 'log' => function(string $key, string $message, ?string $file = null, ?int $line = null) { - self::once(); - self::assertEquals(Order::class . '::setEmail', $key); - }, - ])); - - $order = new Order(); - $order->setEmail($email); - - self::assertEquals($email, $order->getEmail()); - self::assertNotNull($order->getCustomer()); - self::assertEquals($email, $order->getCustomer()->email); - } - - /** - * @return array[] - */ - public function emailDataProvider(): array - { - return [ - 'existing-credentialed-user' => ['email' => 'customer1@crafttest.com'], - 'existing-inactive-user' => ['email' => 'inactive.user@crafttest.com'], - ]; - } - - /** - * @param string $email - * @return void - * @dataProvider customerDataProvider - */ - public function testSetCustomer(string $email): void - { - $user = \Craft::$app->getUsers()->getUserByUsernameOrEmail($email); - $order = new Order(); - $order->setCustomer($user); - - self::assertEquals($email, $order->getEmail()); - self::assertNotNull($order->getCustomer()); - self::assertEquals($email, $order->getCustomer()->email); - self::assertEquals($user->id, $order->getCustomer()->id); - self::assertEquals($user->id, $order->getCustomerId()); - - // Test remove customer - $order->setCustomer(); - self::assertNull($order->getCustomer()); - self::assertNull($order->getCustomerId()); - self::assertNull($order->getEmail()); - } - - /** - * @return array[] - */ - public function customerDataProvider(): array - { - return [ - 'existing-credentialed-user' => ['email' => 'customer1@crafttest.com'], - 'existing-inactive-user' => ['email' => 'inactive.user@crafttest.com'], - ]; - } -} diff --git a/tests/unit/elements/order/OrderMarkAsCompleteTest.php b/tests/unit/elements/order/OrderMarkAsCompleteTest.php deleted file mode 100644 index 02c3455600..0000000000 --- a/tests/unit/elements/order/OrderMarkAsCompleteTest.php +++ /dev/null @@ -1,172 +0,0 @@ - - * @since 4.2.12 - */ -class OrderMarkAsCompleteTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var Plugin|null - */ - protected ?Plugin $pluginInstance = null; - - /** - * @var array - */ - private array $_deleteElementIds = []; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * - */ - public function testUpdatedProperties(): void - { - $order = new Order(); - $email = 'test@newemailaddress.xyz'; - $user = \Craft::$app->getUsers()->ensureUserByEmail($email); - $order->setCustomer($user); - /** @var Order $order */ - $completedOrder = $this->tester->grabFixture('orders')->getElement('completed-new'); - $lineItem = $completedOrder->getLineItems()[0]; - $qty = 4; - $note = 'My note'; - $lineItem = $this->pluginInstance->getLineItems()->create($order, [ - 'purchasableId' => $lineItem->purchasableId, - 'qty' => $qty, - 'note' => $note, - ]); - $order->setLineItems([$lineItem]); - - self::assertNull($order->dateOrdered); - self::assertFalse($order->isCompleted); - self::assertNull($order->orderCompletedEmail); - - self::assertTrue($order->markAsComplete()); - - self::assertInstanceOf(\DateTime::class, $order->dateOrdered); - self::assertTrue($order->isCompleted); - self::assertEquals($email, $order->orderCompletedEmail); - - $this->_deleteElementIds[] = $order->id; - $this->_deleteElementIds[] = $user->id; - } - - /** - * @return void - * @throws \Throwable - * @throws \craft\commerce\errors\CurrencyException - * @throws \craft\commerce\errors\OrderStatusException - * @throws \craft\commerce\errors\TransactionException - * @throws \craft\errors\ElementNotFoundException - * @throws \yii\base\Exception - * @throws \yii\base\InvalidConfigException - */ - public function testPaidDatesUpdated(): void - { - $completedOrder = Order::find()->isCompleted()->isPaid(false)->one(); - - $transaction = $this->pluginInstance->getTransactions()->createTransaction($completedOrder, typeOverride: TransactionRecord::TYPE_PURCHASE); - $transaction->status = TransactionRecord::STATUS_SUCCESS; - - $this->pluginInstance->getTransactions()->saveTransaction($transaction); - - $dateFirstPaid = $transaction->getOrder()->dateFirstPaid; - $datePaid = $transaction->getOrder()->datePaid; - - self::assertNotNull($dateFirstPaid); - self::assertNotNull($datePaid); - - // Check date first paid doesn't change if the order is refunded - $transaction2 = $this->pluginInstance->getTransactions()->createTransaction($completedOrder, parentTransaction: $transaction, typeOverride: TransactionRecord::TYPE_REFUND); - $transaction2->amount = 10; - $transaction2->paymentAmount = 10; - $transaction2->status = TransactionRecord::STATUS_SUCCESS; - - $this->pluginInstance->getTransactions()->saveTransaction($transaction2); - - $refundDateFirstPaid = $transaction2->getOrder()->dateFirstPaid; - $refundDatePaid = $transaction2->getOrder()->datePaid; - - self::assertEquals($dateFirstPaid->format('Y-m-d H:i:s'), $refundDateFirstPaid->format('Y-m-d H:i:s')); - self::assertNull($refundDatePaid); - self::assertNotEquals($datePaid->format('Y-m-d H:i:s'), $refundDatePaid); - - // Check first date paid doesn't change if another payment is made - $transaction3 = $this->pluginInstance->getTransactions()->createTransaction($completedOrder, typeOverride: TransactionRecord::TYPE_PURCHASE); - $transaction->paymentAmount = $transaction2->getOrder()->outstandingBalance; - $transaction->amount = $transaction2->getOrder()->outstandingBalance; - $transaction3->status = TransactionRecord::STATUS_SUCCESS; - - // Make sure there is a slight delay so the timestamps differ - sleep(3); - - $this->pluginInstance->getTransactions()->saveTransaction($transaction3); - - $nextDateFirstPaid = $transaction3->getOrder()->dateFirstPaid; - $nextDatePaid = $transaction3->getOrder()->datePaid; - - self::assertEquals($dateFirstPaid->format('Y-m-d H:i:s'), $nextDateFirstPaid->format('Y-m-d H:i:s')); - self::assertNotEquals($datePaid->format('Y-m-d H:i:s'), $nextDatePaid->format('Y-m-d H:i:s')); - self::assertNotNull($nextDatePaid); - - $this->pluginInstance->getTransactions()->deleteTransactionById($transaction->id); - $this->pluginInstance->getTransactions()->deleteTransactionById($transaction2->id); - $this->pluginInstance->getTransactions()->deleteTransactionById($transaction3->id); - } - - /** - * @inheritdoc - */ - protected function _before(): void - { - parent::_before(); - - $this->pluginInstance = Plugin::getInstance(); - } - - /** - * @inheritdoc - */ - protected function _after(): void - { - parent::_after(); - - // Cleanup data. - foreach ($this->_deleteElementIds as $elementId) { - \Craft::$app->getElements()->deleteElementById($elementId, null, null, true); - } - } -} diff --git a/tests/unit/elements/order/OrderNoticesTest.php b/tests/unit/elements/order/OrderNoticesTest.php deleted file mode 100644 index d853d35e6a..0000000000 --- a/tests/unit/elements/order/OrderNoticesTest.php +++ /dev/null @@ -1,297 +0,0 @@ - - * @since 3.3 - */ -class OrderNoticesTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var Order - */ - protected Order $order; - - /** - * @var Plugin|null - */ - protected ?Plugin $pluginInstance = null; - - /** - * @group OrderNotices - */ - public function testOrderNotices(): void - { - /** @var OrderNotice $firstNotice */ - $firstNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'priceChange', - 'attribute' => 'lineItems', - 'message' => 'The Price of the product changed.', - ], - ]); - $this->order->addNotice($firstNotice); - - $notices = $this->order->getNotices(); - $firstNotice = $this->order->getFirstNotice(); - self::assertEquals($firstNotice->type, $firstNotice->type); - self::assertEquals($firstNotice->attribute, $firstNotice->attribute); - self::assertEquals($firstNotice->message, $firstNotice->message); - self::assertCount(1, $notices); - - /** @var OrderNotice $secondNotice */ - $secondNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'lineItemRemoved', - 'attribute' => 'lineItems', - 'message' => 'The x Product is no longer available and has been removed.', - ], - ]); - - $this->order->addNotice($secondNotice); - - self::assertCount(1, $notices); - self::assertCount(2, $this->order->getNotices()); - - $this->order->addNotices([$firstNotice, $secondNotice]); - self::assertCount(4, $this->order->getNotices()); - } - - /** - * @group OrderNotices - */ - public function testClearOrderNotices(): void - { - $firstNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'priceChange', - 'attribute' => 'lineItems', - 'message' => 'The Price of the product changed.', - ], - ]); - - $secondNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'lineItemRemoved', - 'attribute' => 'lineItems', - 'message' => 'The x Product is no longer available and has been removed.', - ], - ]); - - $this->order->addNotices([$firstNotice, $secondNotice, $firstNotice, $secondNotice]); - self::assertCount(4, $this->order->getNotices()); - - // Test clearing by type - $this->order->clearNotices('lineItemRemoved'); - self::assertCount(2, $this->order->getNotices()); - $this->order->clearNotices('priceChange'); - self::assertCount(0, $this->order->getNotices()); - - // use a third notice - $thirdNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'couponNotValid', - 'attribute' => 'couponCode', - 'message' => 'The x Product is no longer available and has been removed.', - ], - ]); - - // Test clearing by attribute - $this->order->addNotices([$firstNotice, $secondNotice, $firstNotice, $secondNotice, $thirdNotice]); - self::assertCount(5, $this->order->getNotices()); - $this->order->clearNotices(null, 'lineItems'); - self::assertCount(1, $this->order->getNotices()); // only $thirdNotice should remain - - // test clearing all - $this->order->addNotices([$firstNotice, $secondNotice, $firstNotice, $secondNotice, $thirdNotice]); - $this->order->clearNotices(); - self::assertCount(0, $this->order->getNotices()); // only $thirdNotice should remain - - // test clearing using both type and attribute - $this->order->addNotices([$firstNotice, $secondNotice, $firstNotice, $secondNotice, $thirdNotice]); - $this->order->clearNotices('lineItemRemoved', 'lineItems'); - self::assertCount(3, $this->order->getNotices()); // only $thirdNotice and - - self::assertTrue($this->order->hasNotices()); - self::assertTrue($this->order->hasNotices('couponNotValid')); - self::assertCount(1, $this->order->getNotices('couponNotValid')); - self::assertTrue($this->order->hasNotices(null, 'lineItems')); - self::assertCount(2, $this->order->getNotices(null, 'lineItems')); - } - - /** - * @group OrderNotices - */ - public function testForAdminNoticesHiddenByDefault(): void - { - $adminNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'adminAlert', - 'attribute' => 'order', - 'message' => 'This order needs review.', - 'noticeType' => OrderNoticeType::Admin, - ], - ]); - - $customerNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'priceChange', - 'attribute' => 'lineItems', - 'message' => 'A price changed.', - ], - ]); - - $this->order->addNotices([$adminNotice, $customerNotice]); - - // getNotices() must not return admin notices - self::assertCount(1, $this->order->getNotices()); - self::assertEquals('priceChange', $this->order->getNotices()[0]->type); - - self::assertFalse($this->order->hasNotices('adminAlert')); - } - - /** - * @group OrderNotices - */ - public function testGetAdminNotices(): void - { - $adminNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'adminAlert', - 'attribute' => 'order', - 'message' => 'This order needs review.', - 'noticeType' => OrderNoticeType::Admin, - ], - ]); - - $customerNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'priceChange', - 'attribute' => 'lineItems', - 'message' => 'A price changed.', - ], - ]); - - $this->order->addNotices([$adminNotice, $customerNotice]); - - self::assertCount(1, $this->order->getAdminNotices()); - self::assertEquals('adminAlert', $this->order->getAdminNotices()[0]->type); - self::assertTrue($this->order->hasAdminNotices()); - } - - /** - * @group OrderNotices - */ - public function testClearNoticesPreservesAdminByDefault(): void - { - $adminNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'adminAlert', - 'attribute' => 'order', - 'message' => 'This order needs review.', - 'noticeType' => OrderNoticeType::Admin, - ], - ]); - - $customerNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'priceChange', - 'attribute' => 'lineItems', - 'message' => 'A price changed.', - ], - ]); - - $this->order->addNotices([$adminNotice, $customerNotice]); - - // Clearing without the flag preserves admin notices - $this->order->clearNotices(); - - self::assertCount(0, $this->order->getNotices()); - self::assertCount(1, $this->order->getAdminNotices()); - self::assertTrue($this->order->hasAdminNotices()); - } - - /** - * @group OrderNotices - */ - public function testClearNoticesWithFlagClearsAll(): void - { - $adminNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'adminAlert', - 'attribute' => 'order', - 'message' => 'This order needs review.', - 'noticeType' => OrderNoticeType::Admin, - ], - ]); - - $customerNotice = Craft::createObject([ - 'class' => OrderNotice::class, - 'attributes' => [ - 'type' => 'priceChange', - 'attribute' => 'lineItems', - 'message' => 'A price changed.', - ], - ]); - - $this->order->addNotices([$adminNotice, $customerNotice]); - - $this->order->clearNotices(noticeTypes: [OrderNoticeType::Customer, OrderNoticeType::Admin]); - - self::assertCount(0, $this->order->getNotices()); - self::assertCount(0, $this->order->getAdminNotices()); - self::assertFalse($this->order->hasAdminNotices()); - } - - /** - * - */ - protected function _before(): void - { - parent::_before(); - - $this->pluginInstance = Plugin::getInstance(); - $this->order = new Order(); - } - - /** - * - */ - protected function _after(): void - { - parent::_after(); - } -} diff --git a/tests/unit/elements/order/OrderObjectTemplateVariablesTest.php b/tests/unit/elements/order/OrderObjectTemplateVariablesTest.php deleted file mode 100644 index 7ddb902052..0000000000 --- a/tests/unit/elements/order/OrderObjectTemplateVariablesTest.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @since 5.6 - */ -class OrderObjectTemplateVariablesTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * `craft\web\View::renderObjectTemplate()` now populates its template variables from `fields()` before - * falling back to an object's raw attributes. `Order::fields()` re-serializes datetime attributes into - * `['date' => ..., 'time' => ...]` arrays for the control panel's Vue components, so without passing the - * raw values in via `Order::getObjectTemplateVariables()`, filtering `dateOrdered` through the `date` Twig - * filter in an object template (e.g. a PDF file name format) would throw an "Array to string conversion" - * exception. - */ - public function testDateFilterOnDatetimeAttribute(): void - { - $order = new Order(); - $order->dateOrdered = new DateTime('2026-03-16 12:16:00'); - - $fileName = \Craft::$app->getView()->renderSandboxedObjectTemplate( - 'Invoice-{{ dateOrdered|date(\'Y-m-d\') }}', - $order, - $order->getObjectTemplateVariables(), - ); - - self::assertSame('Invoice-2026-03-16', $fileName); - } -} diff --git a/tests/unit/elements/order/OrderPaymentAmountTest.php b/tests/unit/elements/order/OrderPaymentAmountTest.php deleted file mode 100644 index 3794a3bd64..0000000000 --- a/tests/unit/elements/order/OrderPaymentAmountTest.php +++ /dev/null @@ -1,185 +0,0 @@ - - * @since 3.3 - */ -class OrderPaymentAmountTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var Order - */ - protected Order $order; - - /** - * @var Plugin|null - */ - protected ?Plugin $pluginInstance = null; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'payment-currencies' => [ - 'class' => PaymentCurrenciesFixture::class, - ], - ]; - } - - /** - * @group PaymentCurrencies - */ - public function testOrderPaymentAmounts(): void - { - $this->order = new Order(); - $this->order->id = 1000; - - $lineItem = new LineItem(); - $lineItem->price = 10; - $lineItem->qty = 2; - $this->order->setLineItems([$lineItem]); - - // We have an amount to owe on this order - self::assertTrue($this->order->hasOutstandingBalance()); - - // Amount owed is the payment amount - self::assertEquals($this->order->getPaymentAmount(), $this->order->getOutstandingBalance()); - - // Check setter/getter is working - $amountToPay = 10; - $this->order->setPaymentAmount($amountToPay); - self::assertEquals($this->order->getPaymentAmount(), $amountToPay); - - // Add a $12 successful transaction to the order - $transaction1 = new Transaction(); - $transaction1->amount = 12; - $transaction1->type = TransactionRecord::TYPE_PURCHASE; - $transaction1->status = TransactionRecord::STATUS_SUCCESS; - $this->order->setTransactions([$transaction1]); - - self::assertEquals($this->order->getOutstandingBalance(), 8); - - // Add a $2 successful refund transaction to the order - $transaction2 = new Transaction(); - $transaction2->amount = 2; - $transaction2->type = TransactionRecord::TYPE_REFUND; - $transaction2->status = TransactionRecord::STATUS_SUCCESS; - $this->order->setTransactions([$transaction1, $transaction2]); - - // Paid $12 and refunded $2, order price was $20, but outstanding amount now $10 - self::assertEquals($this->order->getOutstandingBalance(), 10); - - // Payment amount is still 10 - self::assertEquals($this->order->getPaymentAmount(), 10); - - // Setting a payment amount in excess of the outstanding balance is ignored and just set to the outstanding balance - $this->order->setPaymentAmount(1000); - self::assertEquals($this->order->getPaymentAmount(), $this->order->getOutstandingBalance()); - } - - /** - * @dataProvider isPaymentAmountPartialDataProvider - */ - public function testIsPaymentAmountPartial($lineItems, $paymentAmount, $paymentCurrency, $isPartial) - { - foreach ($lineItems as &$item) { - $item = Craft::createObject(LineItem::class, [ - 'config' => ['attributes' => $item], - ]); - } - unset($item); - - $this->order->setLineItems($lineItems); - $this->order->setPaymentCurrency($paymentCurrency); - - if ($paymentAmount !== null) { - $this->order->setPaymentAmount($paymentAmount); - } - - self::assertEquals($isPartial, $this->order->isPaymentAmountPartial()); - } - - /** - * @return array[] - */ - public function isPaymentAmountPartialDataProvider() - { - $lineItems = [ - 'first' => [ - 'qty' => 1, - 'price' => 10, - ], - 'second' => [ - 'qty' => 1, - 'price' => 20, - ], - ]; - - return [ - 'partial-payment' => [ - $lineItems, - 10, - 'AUD', - true, - ], - 'full-payment-specified' => [ - array_merge($lineItems, ['second' => ['price' => 7.75, 'qty' => 1]]), - 23.08, - 'AUD', - false, - ], - 'currency-specified-but-no-amount' => [ - $lineItems, - null, - 'AUD', - false, - ], - ]; - } - - /** - * - */ - protected function _before(): void - { - parent::_before(); - - $this->pluginInstance = Plugin::getInstance(); - - $this->order = new Order(); - } - - /** - * - */ - protected function _after(): void - { - parent::_after(); - } -} diff --git a/tests/unit/elements/order/OrderQueryTest.php b/tests/unit/elements/order/OrderQueryTest.php deleted file mode 100644 index d31d21bf4d..0000000000 --- a/tests/unit/elements/order/OrderQueryTest.php +++ /dev/null @@ -1,189 +0,0 @@ - - * @since 3.4.16 - */ -class OrderQueryTest extends Unit -{ - /** - * @var UnitTester - */ - protected $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @param string $email - * @param int $count - * @return void - * @dataProvider emailDataProvider - */ - public function testEmail(string $email, int $count): void - { - $orderQuery = Order::find(); - $orderQuery->email($email); - - self::assertCount($count, $orderQuery->all()); - } - - /** - * @return array[] - */ - public function emailDataProvider(): array - { - return [ - 'normal' => ['customer1@crafttest.com', 3], - 'case-insensitive' => ['CuStOmEr1@crafttest.com', 3], - 'no-results' => ['null@craftcms.com', 0], - ]; - } - - /** - * @param string|null $couponCode - * @param int $count - * @return void - * @throws ModuleException - * @dataProvider couponCodeDataProvider - */ - public function testCouponCode(?string $couponCode, int $count): void - { - $ordersFixture = $this->tester->grabFixture('orders'); - /** @var Order $order */ - $order = $ordersFixture->getElement('completed-new'); - - // Temporarily add a coupon code to an order - \craft\commerce\records\Order::updateAll(['couponCode' => 'foo'], ['id' => $order->id]); - - $orderQuery = Order::find(); - $orderQuery->couponCode($couponCode); - - self::assertCount($count, $orderQuery->all()); - - // Remove temporary coupon code - \craft\commerce\records\Order::updateAll(['couponCode' => null], ['id' => $order->id]); - } - - /** - * @return array[] - */ - public function couponCodeDataProvider(): array - { - return [ - 'normal' => ['foo', 1], - 'case-insensitive' => ['fOo', 1], - 'using-null' => [null, 3], - 'empty-code' => [':empty:', 2], - 'not-empty-code' => [':notempty:', 1], - 'no-results' => ['nope', 0], - ]; - } - - /** - * @param int $count - * @return void - * @dataProvider shippingMethodHandleDataProvider - */ - public function testShippingMethodHandle(mixed $handle, int $count): void - { - $orderQuery = Order::find()->isCompleted()->shippingMethodHandle($handle); - $foo = \craft\commerce\records\Order::find()->select(['id', 'isCompleted', 'shippingMethodHandle', 'email'])->asArray()->all(); - self::assertCount($count, $orderQuery->all()); - } - - /** - * @return array - */ - public function shippingMethodHandleDataProvider(): array - { - return [ - 'queryShippingByString' => ['usShipping', 1], - 'queryShippingByNotString' => ['not usShipping', 2], - 'queryShippingByArray' => [['usShipping'], 1], - 'queryShippingByNotArray' => [['not', 'usShipping'], 2], - ]; - } - - /** - * @param string $property - * @param int $expected - * @return void - * @throws \Throwable - * @throws CurrencyException - * @throws OrderStatusException - * @throws TransactionException - * @throws ElementNotFoundException - * @throws Exception - * @throws InvalidConfigException - * @throws StaleObjectException - * @since 5.5.0 - * @dataProvider paidDatesDataProvider - */ - public function testPaidDates(string $property, mixed $value, int $expected): void - { - // Update one order to have paid dates for testing - $completedOrder = Order::find()->isCompleted()->isPaid(false)->one(); - - $transaction = Plugin::getInstance()->getTransactions()->createTransaction($completedOrder, typeOverride: TransactionRecord::TYPE_PURCHASE); - $transaction->status = TransactionRecord::STATUS_SUCCESS; - - Plugin::getInstance()->getTransactions()->saveTransaction($transaction); - - $orderQuery = Order::find() - ->{$property}($value); - - self::assertCount($expected, $orderQuery->all()); - - Plugin::getInstance()->getTransactions()->deleteTransactionById($transaction->id); - } - - public function paidDatesDataProvider(): array - { - $current = DateTimeHelper::currentUTCDateTime(); - $lastWeek = DateTimeHelper::lastWeek(); - $nextWeek = DateTimeHelper::nextWeek(); - return [ - 'date-paid-string' => ['datePaid', '>= ' . $current->format(\DateTime::ATOM), 1], - 'date-paid-array' => ['datePaid', ['< ' . $nextWeek->format(\DateTime::ATOM), '> ' . $lastWeek->format(\DateTime::ATOM)], 1], - 'date-paid-no-results' => ['datePaid', '< ' . $lastWeek->format(\DateTime::ATOM), 0], - 'date-first-paid-string' => ['dateFirstPaid', '>= ' . $current->format(\DateTime::ATOM), 1], - 'date-first-paid-array' => ['dateFirstPaid', ['< ' . $nextWeek->format(\DateTime::ATOM), '> ' . $lastWeek->format(\DateTime::ATOM)], 1], - 'date-first-paid-no-results' => ['dateFirstPaid', '< ' . $lastWeek->format(\DateTime::ATOM), 0], - ]; - } -} diff --git a/tests/unit/elements/order/OrderRecalculationTest.php b/tests/unit/elements/order/OrderRecalculationTest.php deleted file mode 100644 index d182910094..0000000000 --- a/tests/unit/elements/order/OrderRecalculationTest.php +++ /dev/null @@ -1,397 +0,0 @@ - - */ -class OrderRecalculationTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var Plugin|null - */ - protected ?Plugin $pluginInstance = null; - - /** - * Toggled mid-test to simulate the registration handler matching the - * order, then failing to on a later call. - * - * @var bool - */ - private bool $_thirdPartyMethodMatches = true; - - /** - * @var int[] Element IDs created directly by test methods (not fixtures), for cleanup. - */ - private array $_deleteElementIds = []; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @inheritdoc - */ - protected function _before(): void - { - parent::_before(); - - $this->pluginInstance = Plugin::getInstance(); - $this->_thirdPartyMethodMatches = true; - - // No discounts in play; keeps the test isolated from fixture state. - $this->pluginInstance->set('discounts', $this->make(Discounts::class, [ - 'getAllActiveDiscounts' => fn() => [], - ])); - - // Simulate a third-party plugin registering a shipping method by - // re-evaluating live availability on every call, instead of - // returning a static, persisted method. - Event::on( - ShippingMethods::class, - ShippingMethods::EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS, - function(RegisterAvailableShippingMethodsEvent $event) { - $shippingMethods = $event->getShippingMethods(); - $shippingMethods->push($this->make(ShippingMethod::class, [ - // Plugins must set this themselves on methods they register - - // `Order::getAvailableShippingMethodOptions()` silently drops any - // `ShippingMethod` whose `storeId` doesn't match the order's. - 'storeId' => $event->order->storeId, - 'handle' => 'thirdPartyFlatRate', - 'name' => 'Third Party Flat Rate', - 'getIsEnabled' => true, - 'getMatchingShippingRule' => fn(Order $order) => null, - 'getPriceForOrder' => fn(Order $order) => 8.99, - 'matchOrder' => fn(Order $order) => $this->_thirdPartyMethodMatches, - ])); - } - ); - } - - /** - * @inheritdoc - */ - protected function _after(): void - { - Event::off(ShippingMethods::class, ShippingMethods::EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS); - - foreach ($this->_deleteElementIds as $elementId) { - // Pass the element type explicitly: one test saves an anonymous - // subclass of Order as a spy, whose class name in the `elements` - // table's `type` column can't be resolved back via `class_exists()`. - Craft::$app->getElements()->deleteElementById($elementId, Order::class, null, true); - } - $this->_deleteElementIds = []; - - parent::_after(); - } - - /** - * Confirms the fix: an order already completed and paid in full - - * including the shipping cost - stays locked against recalculation, - * even after the registration handler stops matching. Before the fix, - * `recalculate()` would still run in `RECALCULATION_MODE_ALL` here and - * silently drop the shipping cost from a paid order. - * - * @throws Throwable - */ - public function testCompletedAndPaidOrderStaysLockedAgainstRecalculation(): void - { - // A real, saved cart order - `recalculate()` requires one, and only a - // real element exercises `afterSave()`/`markAsComplete()`/ - // `updateOrderPaidInformation()` the way production code does. - $order = new Order(); - Craft::$app->getElements()->saveElement($order, false); - $this->_deleteElementIds[] = $order->id; - - $variant = Variant::find()->indexBy('sku')->all()['hct-white']; - $lineItem = $this->pluginInstance->getLineItems()->create($order, [ - 'purchasableId' => $variant->id, - 'qty' => 1, - 'note' => '', - ]); - $order->setLineItems([$lineItem]); - $order->shippingMethodHandle = 'thirdPartyFlatRate'; - - $gateway = $this->pluginInstance->getGateways()->getGatewayByHandle('dummy'); - $order->gatewayId = $gateway->id; - - // Cart is untouched, so recalculation mode defaults to `ALL`, as - // throughout a normal checkout. - self::assertEquals(Order::RECALCULATION_MODE_ALL, $order->getRecalculationMode()); - - // Checkout: the method matches, its cost gets applied and persisted. - $order->recalculate(); - Craft::$app->getElements()->saveElement($order, false); - - $totalCollected = $order->getTotalPrice(); - self::assertGreaterThan(0, $order->getTotalShippingCost(), 'Sanity check: shipping cost was applied before payment.'); - - // Customer pays the full amount shown at checkout, including shipping. - $transaction = $this->pluginInstance->getTransactions()->createTransaction($order, typeOverride: TransactionRecord::TYPE_PURCHASE); - $transaction->status = TransactionRecord::STATUS_SUCCESS; - $this->pluginInstance->getTransactions()->saveTransaction($transaction); - - // The order is now completed and paid in full... - self::assertTrue($order->isCompleted); - self::assertFalse($order->hasOutstandingBalance()); - self::assertEquals($totalCollected, $order->getTotalPaid()); - - // ...and, with the fix, stays locked at `NONE` rather than being - // restored to the `ALL` mode it had as a cart. - self::assertEquals( - Order::RECALCULATION_MODE_NONE, - $order->getRecalculationMode(), - 'A completed order must stay locked against recalculation.' - ); - - // Some time later - a queue job, webhook, or fulfillment plugin - - // something recalculates this completed order again, and this time - // the registration handler fails to match. Doesn't matter now, - // since recalculation is locked out. - $this->_thirdPartyMethodMatches = false; - - $order->recalculate(); - - // Nothing changed: still completed, still paid, shipping cost and - // handle untouched, no "shippingMethodChanged" notice. - self::assertTrue($order->isCompleted); - self::assertEquals($totalCollected, $order->getTotalPrice()); - self::assertEquals($totalCollected, $order->getTotalPaid()); - self::assertEquals('thirdPartyFlatRate', $order->shippingMethodHandle); - self::assertFalse($order->hasNotices('shippingMethodChanged')); - } - - /** - * Control test: proves the bug was real by simulating the old, - * unconditional restore that `updateOrderPaidInformation()` used to do - - * manually unlocking a completed, paid order back to - * `RECALCULATION_MODE_ALL` and saving it. This isn't something the fixed - * code does; it's here to show the failure mode described in the class - * docblock actually happens, and that the other tests in this file would - * catch a regression back to it. - * - * @throws Throwable - */ - public function testManuallyUnlockingRecalculationModeOnCompletedOrderDropsShippingCost(): void - { - $order = new Order(); - Craft::$app->getElements()->saveElement($order, false); - $this->_deleteElementIds[] = $order->id; - - $variant = Variant::find()->indexBy('sku')->all()['hct-white']; - $lineItem = $this->pluginInstance->getLineItems()->create($order, [ - 'purchasableId' => $variant->id, - 'qty' => 1, - 'note' => '', - ]); - $order->setLineItems([$lineItem]); - $order->shippingMethodHandle = 'thirdPartyFlatRate'; - - $gateway = $this->pluginInstance->getGateways()->getGatewayByHandle('dummy'); - $order->gatewayId = $gateway->id; - - $order->recalculate(); - Craft::$app->getElements()->saveElement($order, false); - - $totalCollected = $order->getTotalPrice(); - self::assertGreaterThan(0, $order->getTotalShippingCost(), 'Sanity check: shipping cost was applied before payment.'); - - $transaction = $this->pluginInstance->getTransactions()->createTransaction($order, typeOverride: TransactionRecord::TYPE_PURCHASE); - $transaction->status = TransactionRecord::STATUS_SUCCESS; - $this->pluginInstance->getTransactions()->saveTransaction($transaction); - - self::assertTrue($order->isCompleted); - self::assertEquals(Order::RECALCULATION_MODE_NONE, $order->getRecalculationMode()); - - // Simulate the pre-fix bug: restore the cart's original mode after - // completion instead of staying locked at `NONE`. - $order->setRecalculationMode(Order::RECALCULATION_MODE_ALL); - - // The registration handler stops matching, then something saves the - // order - `afterSave()` unconditionally calls `recalculate()`, which - // now actually runs, since mode is `ALL` again. - $this->_thirdPartyMethodMatches = false; - Craft::$app->getElements()->saveElement($order, false); - - // The shipping cost silently disappeared, even though the order is - // still marked completed and paid - this is the bug. - self::assertTrue($order->isCompleted); - self::assertEquals(0.0, $order->getTotalShippingCost()); - self::assertLessThan($totalCollected, $order->getTotalPrice()); - self::assertGreaterThan($order->getTotalPrice(), $order->getTotalPaid(), 'Order now looks overpaid relative to its (wrongly recalculated) total.'); - } - - /** - * Confirms the fix doesn't regress the case it must leave alone: a cart - * that receives a payment/authorization update without completing (e.g. - * a partial payment) stays fully editable and recalculable, as before. - * - * @throws Throwable - */ - public function testUpdatingPaidInformationWithoutCompletingStaysRecalculable(): void - { - $order = new Order(); - Craft::$app->getElements()->saveElement($order, false); - $this->_deleteElementIds[] = $order->id; - - // A real, priced line item - an empty cart's $0 total is trivially - // "paid in full", but this needs a genuine amount still owing. - $variant = Variant::find()->indexBy('sku')->all()['hct-white']; - $lineItem = $this->pluginInstance->getLineItems()->create($order, [ - 'purchasableId' => $variant->id, - 'qty' => 1, - 'note' => '', - ]); - $order->setLineItems([$lineItem]); - $order->recalculate(); - Craft::$app->getElements()->saveElement($order, false); - - self::assertTrue($order->hasOutstandingBalance(), 'Sanity check: the order has an amount still owing.'); - self::assertEquals(Order::RECALCULATION_MODE_ALL, $order->getRecalculationMode()); - - // Nothing paid or authorized, so this can't complete the order, but - // it still exercises the same lock/restore logic that - // `updateOrderPaidInformation()` runs on every payment update. - $order->updateOrderPaidInformation(); - - self::assertFalse($order->isCompleted, 'Sanity check: nothing was paid, so the order has not completed.'); - self::assertEquals( - Order::RECALCULATION_MODE_ALL, - $order->getRecalculationMode(), - 'A cart that receives a payment update without completing must remain fully recalculable.' - ); - } - - /** - * Confirms that saving an already-completed, already-paid order again - * afterwards - as custom code might do, e.g. a controller action or - * queue job unrelated to shipping - has no adverse effect. - * `updateOrderPaidInformation()` already saves the order itself; this - * covers an *extra* save on top of that. Recalculation stays locked at - * `NONE`, so the extra save is a no-op as far as adjustments go. - * - * Also spies on `updateOrderPaidInformation()` itself, to confirm it's - * actually the successful transaction save that triggers it, rather than - * this test only happening to reproduce the same end state some other way. - * - * @throws Throwable - */ - public function testSavingCompletedOrderAgainAfterPaymentHasNoAdverseEffect(): void - { - // A spy on `updateOrderPaidInformation()`: still runs the real method - // via reflection (invoking the original, bypassing this override), - // but additionally expects to be called exactly once. `Expected::once()` - // is verified automatically when the test finishes. Uses `construct()` - // rather than `make()` so Order's real constructor/`init()` still runs - // (e.g. defaulting `siteId`), instead of leaving the order half-built. - $order = $this->construct(Order::class, [], [ - 'updateOrderPaidInformation' => Expected::once(function() use (&$order) { - (new ReflectionMethod(Order::class, 'updateOrderPaidInformation'))->invoke($order); - }), - ]); - Craft::$app->getElements()->saveElement($order, false); - $this->_deleteElementIds[] = $order->id; - - $variant = Variant::find()->indexBy('sku')->all()['hct-white']; - $lineItem = $this->pluginInstance->getLineItems()->create($order, [ - 'purchasableId' => $variant->id, - 'qty' => 1, - 'note' => '', - ]); - $order->setLineItems([$lineItem]); - $order->shippingMethodHandle = 'thirdPartyFlatRate'; - - $gateway = $this->pluginInstance->getGateways()->getGatewayByHandle('dummy'); - $order->gatewayId = $gateway->id; - - $order->recalculate(); - Craft::$app->getElements()->saveElement($order, false); - - $totalCollected = $order->getTotalPrice(); - $shippingCost = $order->getTotalShippingCost(); - self::assertGreaterThan(0, $shippingCost, 'Sanity check: shipping cost was applied before payment.'); - - $transaction = $this->pluginInstance->getTransactions()->createTransaction($order, typeOverride: TransactionRecord::TYPE_PURCHASE); - $transaction->status = TransactionRecord::STATUS_SUCCESS; - $this->pluginInstance->getTransactions()->saveTransaction($transaction); - - self::assertTrue($order->isCompleted); - - // The registration handler stops matching some time later - it - // doesn't matter, because recalculation is locked out. - $this->_thirdPartyMethodMatches = false; - - // Custom code saves the already-completed, already-paid order again, - // for reasons unrelated to shipping/adjustments. - Craft::$app->getElements()->saveElement($order, false); - - self::assertTrue($order->isCompleted); - self::assertEquals(Order::RECALCULATION_MODE_NONE, $order->getRecalculationMode()); - self::assertEquals($shippingCost, $order->getTotalShippingCost(), 'Shipping cost must survive an unrelated save.'); - self::assertEquals($totalCollected, $order->getTotalPrice()); - self::assertEquals($totalCollected, $order->getTotalPaid()); - self::assertFalse($order->hasOutstandingBalance()); - } -} diff --git a/tests/unit/elements/order/OrderTotalsTest.php b/tests/unit/elements/order/OrderTotalsTest.php deleted file mode 100644 index 45126f0674..0000000000 --- a/tests/unit/elements/order/OrderTotalsTest.php +++ /dev/null @@ -1,120 +0,0 @@ - - * @author Global Network Group | Giel Tettelaar - * @since 2.1 - */ -class OrderTotalsTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var Order - */ - protected Order $order; - - /** - * @var Plugin|null - */ - protected ?Plugin $pluginInstance = null; - - /** - * - */ - public function testOrderSumTotalPrice(): void - { - $lineItem1 = new LineItem(); - $lineItem1->qty = 2; - $lineItem1->price = 10; - self::assertEquals(20, $lineItem1->getSubtotal()); - - $lineItem2 = new LineItem(); - $lineItem2->qty = 3; - $lineItem2->price = 20; - self::assertEquals(60, $lineItem2->getSubtotal()); - - $this->order->setLineItems([$lineItem1, $lineItem2]); - self::assertEquals(80, $this->order->getTotalPrice()); - - $lineItem2->promotionalPrice = 15; - $this->order->setLineItems([$lineItem1, $lineItem2]); - self::assertEquals(65, $this->order->getTotalPrice()); - - // Reset line item 2 promotional price - $lineItem2->promotionalPrice = null; - - $adjustment1 = new OrderAdjustment(); - $adjustment1->amount = -10; - $adjustment1->type = Discount::ADJUSTMENT_TYPE; - $adjustment1->setLineItem($lineItem1); - $adjustment1->name = 'Discount'; - $adjustment1->description = '10 bucks off'; - $adjustment1->setOrder($this->order); - $this->order->setAdjustments([$adjustment1]); - - self::assertEquals(70, $this->order->getTotalPrice()); - - $adjustment2 = new OrderAdjustment(); - $adjustment2->amount = -5; - $adjustment1->type = Discount::ADJUSTMENT_TYPE; - $adjustment2->setLineItem($lineItem2); - $adjustment2->name = 'Discount'; - $adjustment2->description = '5 bucks off'; - $adjustment2->setOrder($this->order); - - $this->order->setAdjustments([$adjustment1, $adjustment2]); - self::assertEquals(65, $this->order->getTotalPrice()); - - $adjustment3 = new OrderAdjustment(); - $adjustment3->amount = 5; - $adjustment3->setLineItem($lineItem2); - $adjustment3->name = 'Tax'; - $adjustment3->description = '5 buck tax'; - $adjustment3->included = true; - $adjustment3->setOrder($this->order); - - $this->order->setAdjustments([$adjustment1, $adjustment2, $adjustment3]); - self::assertEquals(65, $this->order->getTotalPrice()); - } - - /** - * @inheritdoc - */ - protected function _before(): void - { - parent::_before(); - - $this->pluginInstance = Plugin::getInstance(); - - $this->order = new Order(); - } - - /** - * @inheritdoc - */ - protected function _after(): void - { - parent::_after(); - } -} diff --git a/tests/unit/elements/order/OrderValidationTest.php b/tests/unit/elements/order/OrderValidationTest.php deleted file mode 100644 index c861882d42..0000000000 --- a/tests/unit/elements/order/OrderValidationTest.php +++ /dev/null @@ -1,96 +0,0 @@ - - * @since 4.1 - */ -class OrderValidationTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var Order - */ - protected Order $order; - - /** - * @var Plugin|null - */ - protected ?Plugin $pluginInstance = null; - - /** - * - */ - public function testAddressValidation(): void - { - $billingAddress = new Address(); - $this->order->setBillingAddress($billingAddress); - - $shippingAddress = new Address(); - $shippingAddress->addressLine1 = '1 Main Street'; - $this->order->setShippingAddress($shippingAddress); - - $validationResult = $this->order->validate(); - - self::assertFalse($validationResult); - self::assertNotEmpty($this->order->getErrors()); - self::assertArrayHasKey('billingAddress.administrativeArea', $this->order->getErrors()); - self::assertArrayHasKey('billingAddress.locality', $this->order->getErrors()); - self::assertArrayHasKey('billingAddress.postalCode', $this->order->getErrors()); - self::assertArrayHasKey('billingAddress.addressLine1', $this->order->getErrors()); - self::assertArrayHasKey('shippingAddress.administrativeArea', $this->order->getErrors()); - self::assertArrayHasKey('shippingAddress.locality', $this->order->getErrors()); - self::assertArrayHasKey('shippingAddress.postalCode', $this->order->getErrors()); - - $billingAddress->addressLine1 = 'Downtown'; - $shippingAddress->locality = $billingAddress->locality = 'LA'; - $shippingAddress->administrativeArea = $billingAddress->administrativeArea = 'CA'; - $shippingAddress->postalCode = $billingAddress->postalCode = '90210'; - - $this->order->setBillingAddress($billingAddress); - $this->order->setShippingAddress($shippingAddress); - - $validationResult = $this->order->validate(); - - self::assertTrue($validationResult); - self::assertEmpty($this->order->getErrors()); - } - - /** - * @inheritdoc - */ - protected function _before(): void - { - parent::_before(); - - $this->pluginInstance = Plugin::getInstance(); - - $this->order = new Order(); - } - - /** - * @inheritdoc - */ - protected function _after(): void - { - parent::_after(); - } -} diff --git a/tests/unit/elements/order/conditions/CouponCodeConditionRuleTest.php b/tests/unit/elements/order/conditions/CouponCodeConditionRuleTest.php deleted file mode 100644 index 1905ef5bb6..0000000000 --- a/tests/unit/elements/order/conditions/CouponCodeConditionRuleTest.php +++ /dev/null @@ -1,162 +0,0 @@ - - * @since 5.3.0 - */ -class CouponCodeConditionRuleTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @group Order - * @dataProvider matchElementDataProvider - */ - public function testMatchElement(?string $coupon, string $operator = '=', ?string $orderCoupon = null, bool $expectedMatch = true): void - { - $condition = $this->_createCondition($coupon, $operator); - - $ordersFixture = $this->tester->grabFixture('orders'); - /** @var Order $order */ - $order = $ordersFixture->getElement('completed-new'); - - if ($orderCoupon) { - $order->couponCode = $orderCoupon; - } - - $match = $condition->matchElement($order); - - if ($expectedMatch) { - self::assertTrue($match); - } else { - self::assertFalse($match); - } - } - - /** - * @return array[] - */ - public function matchElementDataProvider(): array - { - return [ - 'match-equals' => ['coupon1', '=', 'coupon1', true], - 'match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN1', true], - 'no-match-equals' => ['coupon1', '=', 'coupon2', false], - 'no-match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN2', false], - 'no-match-equals-null' => ['coupon1', '=', null, false], - 'match-contains' => ['coupon1', '**', 'coupon1', true], - 'match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN1', true], - 'no-match-contains' => ['coupon1', '**', 'coupon2', false], - 'no-match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN2', false], - 'match-begins-with' => ['coupon', 'bw', 'coupon1', true], - 'match-begins-with-case-insensitive' => ['coupon', 'bw', 'cOuPoN1', true], - 'no-match-begins-with' => ['coupon', 'bw', 'foocoupon2', false], - 'no-match-begins-with-case-insensitive' => ['coupon', 'bw', 'foocOuPoN2', false], - 'match-ends-with' => ['pon1', 'ew', 'coupon1', true], - 'match-ends-with-case-insensitive' => ['pon1', 'ew', 'cOuPoN1', true], - 'no-match-ends-with' => ['pon2', 'ew', 'coupon2foo', false], - 'no-match-ends-with-case-insensitive' => ['pon2', 'ew', 'cOuPoN2foo', false], - ]; - } - - /** - * @group Order - * @dataProvider modifyQueryDataProvider - */ - public function testModifyQuery(?string $coupon, string $operator = '=', ?string $orderCoupon = null, int $expectedResults = 0): void - { - $condition = $this->_createCondition($coupon, $operator); - $orderFixture = $this->tester->grabFixture('orders'); - /** @var Order $order */ - $order = $orderFixture->getElement('completed-new'); - - // Temporarily add a coupon code to an order - \craft\commerce\records\Order::updateAll(['couponCode' => $orderCoupon], ['id' => $order->id]); - - $query = Order::find(); - $condition->modifyQuery($query); - - self::assertCount($expectedResults, $query->ids()); - - if ($expectedResults > 0) { - self::assertContainsEquals($order->id, $query->ids()); - } else { - self::assertEmpty($query->ids()); - } - - // Remove temporary coupon code - \craft\commerce\records\Order::updateAll(['couponCode' => null], ['id' => $order->id]); - } - - /** - * @return array[] - */ - public function modifyQueryDataProvider(): array - { - return [ - 'match-equals' => ['coupon1', '=', 'coupon1', 1], - 'match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN1', 1], - 'no-match-equals' => ['coupon1', '=', 'coupon2', 0], - 'no-match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN2', 0], - 'no-match-equals-null' => ['coupon1', '=', null, 0], - 'match-contains' => ['coupon1', '**', 'coupon1', 1], - 'match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN1', 1], - 'no-match-contains' => ['coupon1', '**', 'coupon2', 0], - 'no-match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN2', 0], - 'match-begins-with' => ['coupon', 'bw', 'coupon1', 1], - 'match-begins-with-case-insensitive' => ['coupon', 'bw', 'cOuPoN1', 1], - 'no-match-begins-with' => ['coupon', 'bw', 'foocoupon2', 0], - 'no-match-begins-with-case-insensitive' => ['coupon', 'bw', 'foocOuPoN2', 0], - 'match-ends-with' => ['pon1', 'ew', 'coupon1', 1], - 'match-ends-with-case-insensitive' => ['pon1', 'ew', 'cOuPoN1', 1], - 'no-match-ends-with' => ['pon2', 'ew', 'coupon2foo', 0], - 'no-match-ends-with-case-insensitive' => ['pon2', 'ew', 'cOuPoN2foo', 0], - ]; - } - - /** - * @param string|null $value - * @param string|null $operator - * @return OrderCondition - */ - private function _createCondition(?string $value, ?string $operator = null): OrderCondition - { - $condition = Order::createCondition(); - /** @var CouponCodeConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(CouponCodeConditionRule::class); - $rule->value = $value; - - if ($operator) { - $rule->operator = $operator; - } - - $condition->addConditionRule($rule); - - return $condition; - } -} diff --git a/tests/unit/elements/order/conditions/CustomerConditionRuleTest.php b/tests/unit/elements/order/conditions/CustomerConditionRuleTest.php deleted file mode 100644 index 24cca0815c..0000000000 --- a/tests/unit/elements/order/conditions/CustomerConditionRuleTest.php +++ /dev/null @@ -1,181 +0,0 @@ - - * @since 4.3.1 - */ -class CustomerConditionRuleTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @group Order - */ - public function testMatchElementIn(): void - { - $user = User::find()->email('customer1@crafttest.com')->one(); - $condition = $this->_createCondition([$user?->id]); - - $ordersFixture = $this->tester->grabFixture('orders'); - /** @var Order $order */ - $order = $ordersFixture->getElement('completed-new'); - - self::assertTrue($condition->matchElement($order)); - } - - /** - * @group Order - */ - public function testNotMatchElementIn(): void - { - $user = User::find()->email('not customer1@crafttest.com')->one(); - $condition = $this->_createCondition([$user?->id]); - - $ordersFixture = $this->tester->grabFixture('orders'); - /** @var Order $order */ - $order = $ordersFixture->getElement('completed-new'); - - self::assertFalse($condition->matchElement($order)); - } - - /** - * @group Order - */ - public function testMatchElementNotIn(): void - { - $user = User::find()->email('not customer1@crafttest.com')->one(); - $condition = $this->_createCondition([$user?->id], 'ni'); - - $ordersFixture = $this->tester->grabFixture('orders'); - /** @var Order $order */ - $order = $ordersFixture->getElement('completed-new'); - - self::assertTrue($condition->matchElement($order)); - } - - /** - * @group Order - */ - public function testNotMatchElementNotIn(): void - { - $user = User::find()->email('customer1@crafttest.com')->one(); - $condition = $this->_createCondition([$user?->id], 'ni'); - - $ordersFixture = $this->tester->grabFixture('orders'); - /** @var Order $order */ - $order = $ordersFixture->getElement('completed-new'); - - self::assertFalse($condition->matchElement($order)); - } - - /** - * @group Order - */ - public function testModifyQueryMatch(): void - { - $user = User::find()->email('customer1@crafttest.com')->one(); - $condition = $this->_createCondition([$user?->id]); - - $orderFixture = $this->tester->grabFixture('orders'); - /** @var Order $order */ - $order = $orderFixture->getElement('completed-new'); - - $query = Order::find(); - $condition->modifyQuery($query); - - self::assertContainsEquals($order->id, $query->ids()); - } - - /** - * @group Order - */ - public function testModifyQueryNotMatch(): void - { - $user = User::find()->email('not customer1@crafttest.com')->one(); - $condition = $this->_createCondition([$user?->id]); - - $query = Order::find(); - $condition->modifyQuery($query); - - self::assertEmpty($query->ids()); - } - - /** - * @group Order - */ - public function testModifyQueryMatchNotIn(): void - { - $user = User::find()->email('not customer1@crafttest.com')->one(); - $condition = $this->_createCondition([$user?->id], 'ni'); - - $orderFixture = $this->tester->grabFixture('orders'); - /** @var Order $order */ - $order = $orderFixture->getElement('completed-new'); - - $query = Order::find(); - $condition->modifyQuery($query); - - self::assertContainsEquals($order->id, $query->ids()); - } - - /** - * @group Order - */ - public function testModifyQueryNotMatchNotIn(): void - { - $user = User::find()->email('customer1@crafttest.com')->one(); - $condition = $this->_createCondition([$user?->id], 'ni'); - - $query = Order::find(); - $condition->modifyQuery($query); - - self::assertEmpty($query->ids()); - } - - /** - * @param array $values - * @param string|null $operator - * @return OrderCondition - */ - private function _createCondition(array $values, ?string $operator = null): OrderCondition - { - $condition = Order::createCondition(); - /** @var CustomerConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(CustomerConditionRule::class); - $rule->values = $values; - - if ($operator) { - $rule->operator = $operator; - } - - $condition->addConditionRule($rule); - - return $condition; - } -} diff --git a/tests/unit/elements/order/conditions/OrderConditionTest.php b/tests/unit/elements/order/conditions/OrderConditionTest.php deleted file mode 100644 index fd5cdec51f..0000000000 --- a/tests/unit/elements/order/conditions/OrderConditionTest.php +++ /dev/null @@ -1,88 +0,0 @@ - - * @since 4.3.1 - */ -class OrderConditionTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @group Product Condition - */ - public function testCreateCondition(): void - { - self::assertInstanceOf(OrderCondition::class, Order::createCondition()); - } - - /** - * @group Product Condition - */ - public function testConditionRuleTypes(): void - { - $rules = Order::createCondition()->getSelectableConditionRules(); - $rules = array_keys($rules); - - self::assertContains(DateOrderedConditionRule::class, $rules); - self::assertContains(CompletedConditionRule::class, $rules); - self::assertContains(CouponCodeConditionRule::class, $rules); - self::assertContains(CustomerConditionRule::class, $rules); - self::assertContains(PaidConditionRule::class, $rules); - self::assertContains(HasPurchasableConditionRule::class, $rules); - self::assertContains(ItemSubtotalConditionRule::class, $rules); - self::assertContains(ItemTotalConditionRule::class, $rules); - self::assertContains(OrderStatusConditionRule::class, $rules); - self::assertContains(OrderSiteConditionRule::class, $rules); - self::assertContains(ReferenceConditionRule::class, $rules); - self::assertContains(ShippingMethodConditionRule::class, $rules); - self::assertContains(TotalDiscountConditionRule::class, $rules); - self::assertContains(TotalPaidConditionRule::class, $rules); - self::assertContains(TotalPriceConditionRule::class, $rules); - self::assertContains(TotalQtyConditionRule::class, $rules); - self::assertContains(TotalTaxConditionRule::class, $rules); - self::assertContains(TotalConditionRule::class, $rules); - } -} diff --git a/tests/unit/elements/product/ProductGetVariantsTest.php b/tests/unit/elements/product/ProductGetVariantsTest.php deleted file mode 100644 index 047b5bf956..0000000000 --- a/tests/unit/elements/product/ProductGetVariantsTest.php +++ /dev/null @@ -1,277 +0,0 @@ - - * @since 5.4.4 - */ -class ProductGetVariantsTest extends Unit -{ - /** - * @var \UnitTester - */ - protected $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * Test that getVariants() returns empty collection when product has no ID - */ - public function testGetVariantsReturnsEmptyCollectionForNewProduct(): void - { - $product = new Product(); - $variants = $product->getVariants(); - - self::assertInstanceOf(VariantCollection::class, $variants); - self::assertTrue($variants->isEmpty()); - } - - /** - * Test that getVariants() doesn't memoize empty collections - */ - public function testGetVariantsDoesNotMemoizeEmptyCollections(): void - { - $product = new Product(); - $product->id = 999999; // Non-existent ID - $product->typeId = 2000; - - // First call should return empty collection - $variants1 = $product->getVariants(); - self::assertTrue($variants1->isEmpty()); - - // Access private _variants property to check if it was memoized - $reflection = new ReflectionClass($product); - $variantsProperty = $reflection->getProperty('_variants'); - $variantsProperty->setAccessible(true); - - // Should be null, not an empty collection - self::assertNull($variantsProperty->getValue($product)); - - // Second call should also return empty collection (new query) - $variants2 = $product->getVariants(); - self::assertTrue($variants2->isEmpty()); - - // Still should not be memoized - self::assertNull($variantsProperty->getValue($product)); - } - - /** - * Test that getVariants() memoizes non-empty collections - */ - public function testGetVariantsMemoizesNonEmptyCollections(): void - { - // Get a product that has variants from fixtures - /** @var ProductFixture $productFixture */ - $productFixture = $this->tester->grabFixture('products'); - $product = $productFixture->getElement('rad-hoodie'); - self::assertNotNull($product); - - // First call - $variants1 = $product->getVariants(); - self::assertFalse($variants1->isEmpty()); - - // Access private _variants property - $reflection = new ReflectionClass($product); - $variantsProperty = $reflection->getProperty('_variants'); - $variantsProperty->setAccessible(true); - - // Should be memoized - $memoizedVariants = $variantsProperty->getValue($product); - self::assertInstanceOf(VariantCollection::class, $memoizedVariants); - self::assertNotNull($memoizedVariants); - - // Second call should return the memoized collection - $variants2 = $product->getVariants(); - self::assertCount($variants1->count(), $variants2); - self::assertEquals($variants1->first()->id, $variants2->first()->id); - } - - /** - * Test that createVariantQuery does not use duplicateOf product ID when available - */ - public function testCreateVariantQueryDoesNotUseDuplicateOfId(): void - { - /** @var ProductFixture $productFixture */ - $productFixture = $this->tester->grabFixture('products'); - $originalProduct = $productFixture->getElement('rad-hoodie'); - self::assertNotNull($originalProduct); - - // Create a duplicate product - $duplicateProduct = new Product(); - $duplicateProduct->id = 999998; - $duplicateProduct->typeId = $originalProduct->typeId; - $duplicateProduct->siteId = 1002; // Different site - $duplicateProduct->duplicateOf = $originalProduct; - - // Use reflection to access private createVariantQuery method - $reflection = new ReflectionClass(Product::class); - $method = $reflection->getMethod('createVariantQuery'); - $method->setAccessible(true); - - /** @var VariantQuery $query */ - $query = $method->invoke(null, $duplicateProduct); - - // Use reflection to check the query's ownerId and siteId - $queryReflection = new ReflectionClass($query); - $ownerIdProperty = $queryReflection->getProperty('ownerId'); - $ownerIdProperty->setAccessible(true); - $siteIdProperty = $queryReflection->getProperty('siteId'); - $siteIdProperty->setAccessible(true); - - // Should use the original product's ID and siteId - self::assertNotEquals($originalProduct->id, $ownerIdProperty->getValue($query)); - self::assertNotEquals($originalProduct->siteId, $siteIdProperty->getValue($query)); - } - - /** - * Test includeDisabled parameter - */ - public function testGetVariantsIncludeDisabledParameter(): void - { - /** @var ProductFixture $productFixture */ - $productFixture = $this->tester->grabFixture('products'); - $product = $productFixture->getElement('hypercolor-tshirt'); - self::assertNotNull($product); - - // Create a disabled variant - $disabledVariant = new Variant(); - $disabledVariant->title = 'Disabled Variant'; - $disabledVariant->sku = 'disabled-variant-sku'; - $disabledVariant->enabled = false; - $disabledVariant->setOwner($product); - - // Add to product's variants - $variants = $product->getVariants()->all(); - $variants[] = $disabledVariant; - $product->setVariants($variants); - - // Test without includeDisabled (default) - $enabledVariants = $product->getVariants(false); - foreach ($enabledVariants as $variant) { - self::assertTrue($variant->enabled); - } - - // Test with includeDisabled - $allVariants = $product->getVariants(true); - $hasDisabledVariant = false; - foreach ($allVariants as $variant) { - if (!$variant->enabled) { - $hasDisabledVariant = true; - break; - } - } - self::assertTrue($hasDisabledVariant); - } - - /** - * Tests every combination of the nullable $includeDisabled parameter against the - * NestedElementsController detection introduced alongside the signature change. - * Also asserts that the internal $_variants collection is never mutated by the filter. - * - * @dataProvider getVariantsNullableIncludeDisabledDataProvider - */ - public function testGetVariantsNullableIncludeDisabled(?bool $includeDisabled, bool $useNestedElementsController, int $expectedCount): void - { - $originalController = Craft::$app->controller; - - try { - if ($useNestedElementsController) { - $mockController = $this->getMockBuilder(NestedElementsController::class) - ->disableOriginalConstructor() - ->getMock(); - Craft::$app->controller = $mockController; - } - - $product = new Product(); - $product->typeId = 2000; - - $enabled = new Variant(); - $enabled->enabled = true; - $enabled->sku = 'enabled-sku'; - - $disabled = new Variant(); - $disabled->enabled = false; - $disabled->sku = 'disabled-sku'; - - $product->setVariants([$enabled, $disabled]); - - $result = $product->getVariants($includeDisabled); - self::assertCount($expectedCount, $result); - - // The internal collection must never be mutated by the filter — - // regardless of which parameter was passed, all set variants must be retained. - $reflection = new ReflectionClass($product); - $variantsProperty = $reflection->getProperty('_variants'); - $variantsProperty->setAccessible(true); - - /** @var VariantCollection $internalVariants */ - $internalVariants = $variantsProperty->getValue($product); - self::assertInstanceOf(VariantCollection::class, $internalVariants); - self::assertCount(2, $internalVariants, '_variants must retain all variants regardless of the filter applied'); - } finally { - // Clean up so the controller state does not leak into subsequent tests - Craft::$app->controller = $originalController; - } - } - - /** - * @return array - */ - public function getVariantsNullableIncludeDisabledDataProvider(): array - { - return [ - // null resolves to false when no NestedElementsController is active - 'null-no-controller-excludes-disabled' => [ - 'includeDisabled' => null, - 'useNestedElementsController' => false, - 'expectedCount' => 1, - ], - // null resolves to true when NestedElementsController is the active controller - 'null-nested-elements-controller-includes-disabled' => [ - 'includeDisabled' => null, - 'useNestedElementsController' => true, - 'expectedCount' => 2, - ], - // Explicit false must override the NestedElementsController detection - 'explicit-false-with-nested-elements-controller' => [ - 'includeDisabled' => false, - 'useNestedElementsController' => true, - 'expectedCount' => 1, - ], - // Explicit true must work even without a special controller - 'explicit-true-without-controller' => [ - 'includeDisabled' => true, - 'useNestedElementsController' => false, - 'expectedCount' => 2, - ], - ]; - } -} diff --git a/tests/unit/elements/product/ProductPricingCatalogTest.php b/tests/unit/elements/product/ProductPricingCatalogTest.php deleted file mode 100644 index f0dbea2e40..0000000000 --- a/tests/unit/elements/product/ProductPricingCatalogTest.php +++ /dev/null @@ -1,280 +0,0 @@ - - * @since 5.x.x - */ -class ProductPricingCatalogTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @param string $sku - * @param array|null $rules - * @param float|int|null $price - * @return void - * @throws Exception - * @throws InvalidConfigException - * @throws StaleObjectException - * @throws Throwable - * @throws \yii\db\Exception - * @dataProvider productCatalogPricesDataProvider - */ - public function testProductCatalogPrices(string $sku, ?array $rules, float|int|null $price): void - { - $catalogPricingRules = $this->_createCatalogPricingRules($rules); - - $product = Product::find()->defaultSku($sku)->one(); - - self::assertInstanceof(Product::class, $product); - self::assertEquals($price, $product->getDefaultPrice()); - - // Tidy up at the end of the test - $this->_deleteCatalogPricingRules($catalogPricingRules); - } - - /** - * @param string $sku - * @param array|null $rules - * @param float|int|null $price - * @return void - * @throws Exception - * @throws InvalidConfigException - * @throws StaleObjectException - * @throws Throwable - * @throws \yii\db\Exception - * @dataProvider productCatalogPricesDataProvider - */ - public function testProductCatalogPricesQuerying(string $sku, ?array $rules, float|int|null $price): void - { - $catalogPricingRules = $this->_createCatalogPricingRules($rules); - - $product = Product::find()->defaultPrice($price)->one(); - $variant = $product->getDefaultVariant(); - self::assertInstanceof(Product::class, $product); - self::assertInstanceof(Variant::class, $variant); - self::assertEquals($sku, $product->defaultSku); - self::assertEquals($sku, $variant->getSku()); - self::assertEquals($price, $product->defaultPrice); - self::assertEquals($price, $variant->getPrice()); - - // Tidy up at the end of the test - $this->_deleteCatalogPricingRules($catalogPricingRules); - } - - /** - * @param string $sku - * @param array|null $rules - * @param float|int|null $price - * @return void - * @throws Exception - * @throws InvalidConfigException - * @throws StaleObjectException - * @throws Throwable - * @throws \yii\db\Exception - * @dataProvider productCatalogPricesDataProvider - */ - public function testProductCatalogPricesSorting(string $sku, ?array $rules, float|int|null $price): void - { - $catalogPricingRules = $this->_createCatalogPricingRules($rules); - - $orderBy = ['defaultPrice' => SORT_ASC]; - $products = Product::find()->orderBy($orderBy)->all(); - $price = null; - foreach ($products as $product) { - self::assertGreaterThanOrEqual($price, $product->getDefaultPrice()); - $price = $product->getDefaultPrice(); - } - - $orderBy = ['defaultPrice' => SORT_DESC]; - $products = Product::find()->orderBy($orderBy)->all(); - $price = 999999999; - foreach ($products as $product) { - self::assertLessThanOrEqual($price, $product->getDefaultPrice()); - $price = $product->getDefaultPrice(); - } - - // Tidy up at the end of the test - $this->_deleteCatalogPricingRules($catalogPricingRules); - } - - /** - * @param array|null $rules - * @return array - * @throws Exception - * @throws InvalidConfigException - * @throws \yii\db\Exception - */ - private function _createCatalogPricingRules(?array $rules): array - { - $catalogPricingRules = []; - - if (!empty($rules)) { - foreach ($rules as $rule) { - $catalogPricingRule = Craft::createObject($rule); - Plugin::getInstance()->getCatalogPricingRules()->saveCatalogPricingRule($catalogPricingRule); - $catalogPricingRules[] = $catalogPricingRule->id; - } - - Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); - } - - return $catalogPricingRules; - } - - /** - * @param array|null $rules - * @return void - * @throws InvalidConfigException - * @throws StaleObjectException - * @throws Throwable - * @throws \yii\db\Exception - */ - private function _deleteCatalogPricingRules(?array $rules): void - { - // Tidy up at the end of the test - if (!empty($rules)) { - foreach ($rules as $catalogPricingRule) { - Plugin::getInstance()->getCatalogPricingRules()->deleteCatalogPricingRuleById($catalogPricingRule); - } - - Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); - } - } - - /** - * @return array[] - * @throws InvalidConfigException - */ - public function productCatalogPricesDataProvider(): array - { - return [ - 'no catalog prices' => [ - 'sku' => 'rad-hood', - 'rules' => null, - 'price' => 123.99, - ], - 'rad hood reduced price' => [ - 'sku' => 'rad-hood', - 'rules' => [ - [ - 'class' => CatalogPricingRule::class, - 'attributes' => [ - 'name' => 'Test Rule', - 'storeId' => 1, - 'applyAmount' => -0.1, - 'variantCondition' => Craft::$app->getConditions()->createCondition([ - 'class' => CatalogPricingRuleVariantCondition::class, - 'conditionRules' => [ - Craft::$app->getConditions()->createConditionRule([ - 'type' => SkuConditionRule::class, - 'value' => 'rad-hood', - ]), - ], - ]), - ], - ], - ], - 'price' => 111.59, - ], - 'rad hood promotional price' => [ - 'sku' => 'rad-hood', - 'rules' => [ - [ - 'class' => CatalogPricingRule::class, - 'attributes' => [ - 'name' => 'Test Rule', - 'storeId' => 1, - 'applyAmount' => -0.1, - 'isPromotionalPrice' => true, - 'variantCondition' => Craft::$app->getConditions()->createCondition([ - 'class' => CatalogPricingRuleVariantCondition::class, - 'conditionRules' => [ - Craft::$app->getConditions()->createConditionRule([ - 'type' => SkuConditionRule::class, - 'value' => 'rad-hood', - ]), - ], - ]), - ], - ], - ], - 'price' => 123.99, - ], - 'rad hood two rules' => [ - 'sku' => 'rad-hood', - 'rules' => [ - [ - 'class' => CatalogPricingRule::class, - 'attributes' => [ - 'name' => 'Test Rule - 5%', - 'storeId' => 1, - 'applyAmount' => -0.05, - 'variantCondition' => Craft::$app->getConditions()->createCondition([ - 'class' => CatalogPricingRuleVariantCondition::class, - 'conditionRules' => [ - Craft::$app->getConditions()->createConditionRule([ - 'type' => SkuConditionRule::class, - 'value' => 'rad-hood', - ]), - ], - ]), - ], - ], - [ - 'class' => CatalogPricingRule::class, - 'attributes' => [ - 'name' => 'Test Rule - 1%', - 'storeId' => 1, - 'applyAmount' => -0.01, - 'isPromotionalPrice' => true, - 'variantCondition' => Craft::$app->getConditions()->createCondition([ - 'class' => CatalogPricingRuleVariantCondition::class, - 'conditionRules' => [ - Craft::$app->getConditions()->createConditionRule([ - 'type' => SkuConditionRule::class, - 'value' => 'rad-hood', - ]), - ], - ]), - ], - ], - ], - 'price' => 117.79, - ], - ]; - } -} diff --git a/tests/unit/elements/product/ProductQueryTest.php b/tests/unit/elements/product/ProductQueryTest.php deleted file mode 100644 index 05154cda09..0000000000 --- a/tests/unit/elements/product/ProductQueryTest.php +++ /dev/null @@ -1,177 +0,0 @@ - - * @since 4.3.0 - */ -class ProductQueryTest extends Unit -{ - /** - * @var UnitTester - */ - protected $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @return void - */ - public function testQuery(): void - { - self::assertInstanceOf(ProductQuery::class, Product::find()); - } - - /** - * @param int $count - * @return void - * @dataProvider defaultPriceDataProvider - */ - public function testDefaultPrice(mixed $price, int $count): void - { - $query = Product::find(); - - self::assertTrue(method_exists($query, 'defaultPrice')); - $query->defaultPrice($price); - - self::assertCount($count, $query->all()); - } - - /** - * @return array[] - */ - public function defaultPriceDataProvider(): array - { - return [ - 'exact-results' => [123.99, 1], - 'exact-no-results' => [999, 0], - 'greater-than-results' => ['> 1', 2], - 'greater-than-no-results' => ['> 999', 0], - 'less-than-results' => ['< 150', 2], - 'less-than-no-results' => ['< 1', 0], - 'range-results' => [['and', '> 5', '< 200'], 2], - 'range-no-results' => [['and', '> 500', '< 2000'], 0], - 'in-results' => [[123.99, 19.99], 2], - 'in-no-results' => [[1, 2], 0], - ]; - } - - /** - * @param VariantQuery $variantQuery - * @param int $count - * @return void - * @dataProvider hasVariantDataProvider - */ - public function testHasVariant(VariantQuery $variantQuery, int $count): void - { - $query = Product::find(); - - self::assertTrue(method_exists($query, 'hasVariant')); - $query->hasVariant($variantQuery); - - self::assertCount($count, $query->all()); - } - - /** - * @return array[] - */ - public function hasVariantDataProvider(): array - { - return [ - 'no-params' => [Variant::find(), 2], - 'specific-variant' => [Variant::find()->sku('rad-hood'), 1], - ]; - } - - /** - * @return void - * @dataProvider withVariantsDataProvider - */ - public function testWithVariants(ProductQuery $query, int $count, ?array $variantQuery, ?string $title): void - { - $with = ['variants']; - - if (!empty($variantQuery)) { - $query->hasVariant($variantQuery); - $with = [ - ['variants', $variantQuery], - ]; - } - - $query->with($with); - - $results = $query->all(); - - self::assertCount($count, $results); - if ($count) { - /** @var Product $product */ - $product = $results[0]; - self::assertInstanceOf(Variant::class, $product->getVariants()[0]); - self::assertEquals($title, $product->title); - } - } - - public function withVariantsDataProvider(): array - { - return [ - 'no-params' => [Product::find(), 2, null, 'Hypercolor T-Shirt'], - 'specific-variant' => [Product::find(), 1, ['sku' => 'rad-hood'], 'Rad Hoodie'], - ]; - } - - /** - * @param array $orderBy - * @param array $expected - * @return void - * @dataProvider orderByDataProvider - */ - public function testOrderBy(array $orderBy, array $expected): void - { - $query = Product::find(); - - $query->orderBy($orderBy); - $results = $query->all(); - - self::assertCount(2, $results); - - foreach ($expected as $index => $title) { - self::assertEquals($title, $results[$index]->title); - } - } - - public function orderByDataProvider(): array - { - return [ - 'title-asc' => [['title' => SORT_ASC], ['Hypercolor T-Shirt', 'Rad Hoodie']], - 'title-desc' => [['title' => SORT_DESC], ['Rad Hoodie', 'Hypercolor T-Shirt']], - 'default-price-asc' => [['defaultPrice' => SORT_ASC], ['Hypercolor T-Shirt', 'Rad Hoodie']], - 'default-price-desc' => [['defaultPrice' => SORT_DESC], ['Rad Hoodie', 'Hypercolor T-Shirt']], - ]; - } -} diff --git a/tests/unit/elements/product/ProductTest.php b/tests/unit/elements/product/ProductTest.php deleted file mode 100644 index f9e2273175..0000000000 --- a/tests/unit/elements/product/ProductTest.php +++ /dev/null @@ -1,495 +0,0 @@ - - * @since 3.3.3 - */ -class ProductTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @group Product - */ - public function testProductPopulationAndValidation(): void - { - $product = new Product(); - $product->enabled = false; - $product->title = 'test'; - $product->typeId = 2000; - - $variant = new Variant(); - $variant->title = 'variant 1'; - $product->setVariants([$variant]); - - $product->validate(); - - self::assertCount(0, $product->getErrors()); - } - - /** - * @group Product - * @dataProvider productMassAssignmentDataProvider - */ - public function testProductMassAssignment(array $data, array $checkKeys): void - { - $data += ['class' => Product::class]; - $product = \Craft::createObject($data); - - foreach ($checkKeys as $checkKey) { - if ($checkKey === 'variants') { - self::assertCount(count($data[$checkKey]), $product->getVariants()); - $variant = $product->getVariants()[0]; - - foreach (array_keys($data[$checkKey][0]) as $variantKey) { - self::assertEquals($data[$checkKey][0][$variantKey], $variant->$variantKey); - } - continue; - } - - self::assertEquals($data[$checkKey], $product->$checkKey); - } - } - - public function productMassAssignmentDataProvider(): array - { - return [ - 'just-properties' => [ - [ - 'title' => 'Test Product', - 'typeId' => 2000, - 'enabled' => true, - 'variants' => [ - [ - 'title' => 'Test Variant', - 'basePrice' => 123, - 'sku' => '123', - 'enabled' => true, - ], - ], - ], - ['title', 'typeId', 'enabled', 'variants'], - ], - 'props-and-custom-fields' => [ - [ - 'title' => 'Test Product', - 'typeId' => 2000, - 'enabled' => true, - 'variants' => [ - [ - 'title' => 'Test Variant', - 'basePrice' => 123, - 'sku' => '123', - 'enabled' => true, - 'myVariantHeadingField' => 'bar', - ], - ], - 'myHeadingField' => 'foo', - ], - ['title', 'typeId', 'enabled', 'variants'], - ], - ]; - } - - /** - * @dataProvider productVariantMethodsDataProvider - */ - public function testProductVariantMethods(int $productTypeId, array $variantData, array $expected): void - { - $product = new Product(); - $product->enabled = true; - $product->typeId = $productTypeId; - $product->title = 'Test Product'; - - $variants = []; - $count = 1; - $defaultVariantId = null; - foreach ($variantData as [$id, $price, $default, $enabled]) { - $variant = new Variant(); - $variant->id = $id; - $variant->title = sprintf('Test Variant #%s', $count); - $variant->isDefault = $default; - $defaultVariantId = $default ? $id : $defaultVariantId; - $variant->enabled = $enabled; - $variant->price = $price; - - $variants[] = $variant; - $count++; - } - - $product->setVariants($variants); - if ($defaultVariantId) { - $product->defaultVariantId = $defaultVariantId; - } - - self::assertCount($expected['variantCount'], $product->getVariants(true)); - self::assertCount($expected['enabledVariantCount'], $product->getVariants()); - - $defaultVariant = $product->getDefaultVariant(true); - self::assertSame($expected['defaultVariantTitle'], $defaultVariant->title); - - $cheapestVariant = $product->getCheapestVariant(true); - self::assertSame($expected['cheapestVariantTitle'], $cheapestVariant->title); - - $defaultEnabledVariant = $product->getDefaultVariant(); - self::assertSame($expected['defaultEnabledVariantTitle'], $defaultEnabledVariant->title ?? null); - - $cheapestEnabledVariant = $product->getCheapestVariant(); - self::assertSame($expected['cheapestEnabledVariantTitle'], $cheapestEnabledVariant->title ?? null); - } - - /** - * @return array - */ - public function productVariantMethodsDataProvider(): array - { - return [ - 'All Enabled' => [ - 2001, - [[1001, 123, true, true], [1002, 456, false, true], [1003, 789, false, true]], - [ - 'variantCount' => 3, - 'enabledVariantCount' => 3, - 'cheapestVariantTitle' => 'Test Variant #1', - 'defaultVariantTitle' => 'Test Variant #1', - 'cheapestEnabledVariantTitle' => 'Test Variant #1', - 'defaultEnabledVariantTitle' => 'Test Variant #1', - ], - ], - 'One Disabled' => [ - 2001, - [[1001, 123, false, false], [1002, 456, false, true], [1003, 789, true, true]], - [ - 'variantCount' => 3, - 'enabledVariantCount' => 2, - 'cheapestVariantTitle' => 'Test Variant #1', - 'defaultVariantTitle' => 'Test Variant #3', - 'cheapestEnabledVariantTitle' => 'Test Variant #2', - 'defaultEnabledVariantTitle' => 'Test Variant #3', - ], - ], - 'All Disabled' => [ - 2001, - [[1001, 123, false, false], [1002, 456, true, false], [1003, 99, false, false]], - [ - 'variantCount' => 3, - 'enabledVariantCount' => 0, - 'cheapestVariantTitle' => 'Test Variant #3', - 'defaultVariantTitle' => 'Test Variant #2', - 'cheapestEnabledVariantTitle' => null, - 'defaultEnabledVariantTitle' => null, - ], - ], - ]; - } - - /** - * @return void - * @dataProvider productSavedInSitesDataProvider - */ - public function testProductSavedInSites(int $siteId, int $count): void - { - self::assertCount($count, Product::find()->siteId($siteId)->all()); - } - - public function productSavedInSitesDataProvider(): array - { - return [ - 'primary' => [1, 2], - 'uk' => [1002, 3], - ]; - } - - /** - * @return void - * @throws \Throwable - * @throws \craft\errors\ElementNotFoundException - * @throws \yii\base\Exception - * @throws \yii\base\InvalidConfigException - */ - public function testSaveProductAndVariants(): void - { - $product = new Product(); - $product->title = 'Test Product'; - $product->typeId = 2000; - $product->slug = 'test-product'; - $product->enabled = true; - $product->enabledForSite = true; - $product->postDate = (new DateTime('now')); - - $variants = []; - $variant = new Variant(); - $variant->title = 'Test Variant'; - $variant->slug = 'test-variant'; - $variant->sku = 'test-variant-sku'; - $variant->basePrice = 99.99; - $variant->sortOrder = 0; - $variant->width = null; - $variant->height = null; - $variant->length = null; - $variant->weight = null; - $variant->inventoryTracked = false; - $variant->minQty = null; - $variant->maxQty = null; - $variant->isDefault = true; - - $variants[] = $variant; - - $variant = new Variant(); - $variant->title = 'Test Variant 2'; - $variant->slug = 'test-variant 2'; - $variant->sku = 'test-variant-sku2'; - $variant->basePrice = 100.99; - $variant->sortOrder = 1; - $variant->width = null; - $variant->height = null; - $variant->length = null; - $variant->weight = null; - $variant->inventoryTracked = false; - $variant->minQty = null; - $variant->maxQty = null; - $variant->isDefault = false; - $variants[] = $variant; - - $product->setVariants($variants); - - \Craft::$app->getElements()->saveElement($product, false); - - // Check default data when the variant is saved as part of the product save - $productData = (new Query()) - ->select([ - 'defaultVariantId', - 'defaultSku', - 'defaultPrice', - 'defaultWidth', - 'defaultHeight', - 'defaultLength', - 'defaultWeight', - ]) - ->from(Table::PRODUCTS) - ->where(['id' => $product->id]) - ->one(); - - $defaultVariantData = (new Query()) - ->select([ - 'v.id', - ]) - ->from(Table::VARIANTS . ' v') - ->leftJoin(Table::PURCHASABLES . ' p', '[[p.id]] = [[v.id]]') - ->where(['primaryOwnerId' => $product->id]) - ->andWhere(['p.sku' => 'test-variant-sku']) - ->one(); - - // Check the product object - self::assertEquals($defaultVariantData['id'], $product->getDefaultVariant()->id); - self::assertEquals('test-variant-sku', $product->defaultSku); - self::assertEquals(99.99, $product->defaultPrice); - self::assertEquals(0, $product->defaultWidth); - self::assertEquals(0, $product->defaultHeight); - self::assertEquals(0, $product->defaultLength); - self::assertEquals(0, $product->defaultWeight); - - // Check the product data in the database - self::assertEquals($defaultVariantData['id'], $productData['defaultVariantId']); - self::assertEquals('test-variant-sku', $productData['defaultSku']); - self::assertEquals(99.99, $productData['defaultPrice']); - self::assertEquals(0, $productData['defaultWidth']); - self::assertEquals(0, $productData['defaultHeight']); - self::assertEquals(0, $productData['defaultLength']); - self::assertEquals(0, $productData['defaultWeight']); - - // Make changes and independently save the default variant to check the product data is updated - $variant = $product->getDefaultVariant(); - $variant->setSku('test-variant-sku-updated'); - $variant->basePrice = 199.99; - - \Craft::$app->getElements()->saveElement($variant, false); - - $newProductData = (new Query()) - ->select([ - 'defaultVariantId', - 'defaultSku', - 'defaultPrice', - 'defaultWidth', - 'defaultHeight', - 'defaultLength', - 'defaultWeight', - ]) - ->from(Table::PRODUCTS) - ->where(['id' => $product->id]) - ->one(); - - self::assertEquals($defaultVariantData['id'], $newProductData['defaultVariantId']); - self::assertEquals('test-variant-sku-updated', $newProductData['defaultSku']); - self::assertEquals(199.99, $newProductData['defaultPrice']); - self::assertEquals(0, $newProductData['defaultWidth']); - self::assertEquals(0, $newProductData['defaultHeight']); - self::assertEquals(0, $newProductData['defaultLength']); - self::assertEquals(0, $newProductData['defaultWeight']); - - // Remove the product - \Craft::$app->getElements()->deleteElementById($product->id, Product::class, null, true); - } - - /** - * @group Product - */ - public function testSkuFormatGeneratesSkuWhenEmpty(): void - { - $this->setProductTypeSkuFormat(2001, 'generated-sku-from-format'); - - $product = new Product(); - $product->title = 'SKU Format Test Product'; - $product->typeId = 2001; - $product->enabled = false; - - $variant = new Variant(); - $variant->title = 'Test Variant'; - // SKU intentionally not set — should be generated from skuFormat - - $product->setVariants([$variant]); - $product->validate(); - - self::assertEquals('generated-sku-from-format', $variant->sku); - - $this->setProductTypeSkuFormat(2001, null); - } - - /** - * @group Product - */ - public function testSkuFormatDeduplicatesWhenCollisionExists(): void - { - // Fixture variant 'rad-hood' already exists in the PURCHASABLES table. - // Reset the sequence so the suffix is always predictable (-1). - $this->resetSkuSequence('rad-hood'); - $this->setProductTypeSkuFormat(2001, 'rad-hood'); - - $product = new Product(); - $product->title = 'Collision Test Product'; - $product->typeId = 2001; - $product->enabled = false; - - $variant = new Variant(); - $variant->title = 'Test Variant'; - // No SKU — format generates 'rad-hood' which collides with the fixture variant - - $product->setVariants([$variant]); - $product->validate(); - - self::assertEquals('rad-hood-1', $variant->sku); - - $this->setProductTypeSkuFormat(2001, null); - } - - /** - * @group Product - */ - public function testSkuFormatDeduplicatesMultipleVariantsWithCollision(): void - { - // Fixture variant 'hct-white' already exists in the PURCHASABLES table. - // Reset the sequence so suffixes are always predictable (-1, -2). - $this->resetSkuSequence('hct-white'); - $this->setProductTypeSkuFormat(2001, 'hct-white'); - - $product = new Product(); - $product->title = 'Multi-Variant Collision Test'; - $product->typeId = 2001; - $product->enabled = false; - - $variant1 = new Variant(); - $variant1->title = 'Variant One'; - - $variant2 = new Variant(); - $variant2->title = 'Variant Two'; - - $product->setVariants([$variant1, $variant2]); - $product->validate(); - - self::assertEquals('hct-white-1', $variant1->sku); - self::assertEquals('hct-white-2', $variant2->sku); - - $this->setProductTypeSkuFormat(2001, null); - } - - /** - * @group Product - */ - public function testSkuFormatWithIdIsRegeneratedAfterIdAssigned(): void - { - $this->setProductTypeSkuFormat(2001, 'SKU-{id}'); - - $product = new Product(); - $product->title = 'SKU Format Id Test Product'; - $product->typeId = 2001; - $product->enabled = false; - - $variant = new Variant(); - $variant->title = 'Test Variant'; - // No SKU — format references {id}, which isn't available until after the element is saved - - $product->setVariants([$variant]); - \Craft::$app->getElements()->saveElement($product, false); - - $savedVariant = $product->getDefaultVariant(); - self::assertEquals('SKU-' . $savedVariant->id, $savedVariant->sku); - - \Craft::$app->getElements()->deleteElementById($product->id, Product::class, null, true); - $this->setProductTypeSkuFormat(2001, null); - } - - private function resetSkuSequence(string $baseSku): void - { - \Craft::$app->getDb()->createCommand() - ->delete(CraftTable::SEQUENCES, ['name' => 'sku::' . $baseSku]) - ->execute(); - } - - private function setProductTypeSkuFormat(int $typeId, ?string $skuFormat): void - { - $productTypesService = Plugin::getInstance()->getProductTypes(); - // Ensure types are loaded into cache - $productTypesService->getAllProductTypes(); - - $reflection = new ReflectionClass($productTypesService); - $prop = $reflection->getProperty('_allProductTypes'); - $prop->setAccessible(true); - - foreach ($prop->getValue($productTypesService) as $type) { - if ($type->id === $typeId) { - $type->skuFormat = $skuFormat; - return; - } - } - } -} diff --git a/tests/unit/elements/product/conditions/ProductConditionTest.php b/tests/unit/elements/product/conditions/ProductConditionTest.php deleted file mode 100644 index 601cd92068..0000000000 --- a/tests/unit/elements/product/conditions/ProductConditionTest.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @since 4.3.0 - */ -class ProductTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @group Product Condition - */ - public function testCreateCondition(): void - { - self::assertInstanceOf(ProductCondition::class, Product::createCondition()); - } - - /** - * @group Product Condition - */ - public function testConditionRuleTypes(): void - { - $rules = array_keys(Product::createCondition()->getSelectableConditionRules()); - - self::assertContains(ProductTypeConditionRule::class, $rules); - self::assertContains(ProductVariantSkuConditionRule::class, $rules); - self::assertContains(ProductVariantStockConditionRule::class, $rules); - } -} diff --git a/tests/unit/elements/product/conditions/ProductTypeConditionRuleTest.php b/tests/unit/elements/product/conditions/ProductTypeConditionRuleTest.php deleted file mode 100644 index 258198c690..0000000000 --- a/tests/unit/elements/product/conditions/ProductTypeConditionRuleTest.php +++ /dev/null @@ -1,119 +0,0 @@ - - * @since 4.3.0 - */ -class ProductTypeConditionRuleTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'product-types' => [ - 'class' => ProductTypeFixture::class, - ], - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @group Product - */ - public function testMatchElement(): void - { - $productTypeModel = Plugin::getInstance()->getProductTypes()->getProductTypeByHandle('hoodies'); - $condition = Product::createCondition(); - /** @var ProductTypeConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(ProductTypeConditionRule::class); - $rule->setValues([$productTypeModel->uid]); - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - - self::assertTrue($condition->matchElement($product)); - } - - /** - * @group Product - */ - public function testNotMatchElement(): void - { - $productTypeModel = Plugin::getInstance()->getProductTypes()->getProductTypeByHandle('tShirts'); - $condition = Product::createCondition(); - /** @var ProductTypeConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(ProductTypeConditionRule::class); - $rule->setValues([$productTypeModel->uid]); - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - - self::assertFalse($condition->matchElement($product)); - } - - /** - * @group Product - */ - public function testMatchElementNotIn(): void - { - $productTypeModel = Plugin::getInstance()->getProductTypes()->getProductTypeByHandle('tShirts'); - $condition = Product::createCondition(); - /** @var ProductTypeConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(ProductTypeConditionRule::class); - $rule->setValues([$productTypeModel->uid]); - $rule->operator = 'ni'; - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - - self::assertTrue($condition->matchElement($product)); - } - - /** - * @group Product - */ - public function testModifyQueryMatch(): void - { - $productTypeModel = Plugin::getInstance()->getProductTypes()->getProductTypeByHandle('hoodies'); - $condition = Product::createCondition(); - /** @var ProductTypeConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(ProductTypeConditionRule::class); - $rule->setValues([$productTypeModel->uid]); - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - - $query = Product::find(); - $condition->modifyQuery($query); - - self::assertContainsEquals($product->id, $query->ids()); - } -} diff --git a/tests/unit/elements/product/conditions/ProductVariantHasUnlimitedStockConditionRuleTest.php b/tests/unit/elements/product/conditions/ProductVariantHasUnlimitedStockConditionRuleTest.php deleted file mode 100644 index 4751a037a3..0000000000 --- a/tests/unit/elements/product/conditions/ProductVariantHasUnlimitedStockConditionRuleTest.php +++ /dev/null @@ -1,157 +0,0 @@ - - * @since 4.3.0 - */ -class ProductVariantHasUnlimitedStockConditionRuleTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @group Product - * @dataProvider matchElementDataProvider - * @param bool $hasUnlimitedStock - * @throws InvalidConfigException - */ - public function testMatchElement(bool $hasUnlimitedStock): void - { - $condition = Product::createCondition(); - /** @var ProductVariantHasUnlimitedStockConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(ProductVariantHasUnlimitedStockConditionRule::class); - $rule->value = $hasUnlimitedStock; - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - - if (!$hasUnlimitedStock) { - $variants = $product->getVariants(); - $variants->each(function(&$variant) { - $variant->hasUnlimitedStock = false; - }); - $product->setVariants($variants); - } - - self::assertTrue($condition->matchElement($product)); - } - - /** - * @group Product - * @dataProvider matchElementDataProvider - * @param bool $hasUnlimitedStock - * @throws InvalidConfigException - */ - public function testNotMatchElement(bool $hasUnlimitedStock): void - { - $condition = Product::createCondition(); - /** @var ProductVariantHasUnlimitedStockConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(ProductVariantHasUnlimitedStockConditionRule::class); - $rule->value = $hasUnlimitedStock; - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - - if ($hasUnlimitedStock) { - $variants = $product->getVariants(); - $variants->each(function(&$variant) { - $variant->hasUnlimitedStock = false; - }); - $product->setVariants($variants); - } - - self::assertFalse($condition->matchElement($product)); - } - - /** - * @param bool $hasUnlimitedStock - * @return void - * @throws ElementNotFoundException - * @throws Exception - * @throws InvalidConfigException - * @throws \Throwable - * @dataProvider matchElementDataProvider - */ - public function testModifyQueryMatch(bool $hasUnlimitedStock): void - { - $primaryStore = Plugin::getInstance()->getStores()->getPrimaryStore(); - $condition = Product::createCondition(); - /** @var ProductVariantHasUnlimitedStockConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(ProductVariantHasUnlimitedStockConditionRule::class); - $rule->value = $hasUnlimitedStock; - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - - if (!$hasUnlimitedStock) { - $originalValues = (new Query()) - ->from(Table::PURCHASABLES_STORES) - ->select(['purchasableId', 'stock', 'inventoryTracked']) - ->indexBy('purchasableId') - ->all(); - - \Craft::$app->getDb()->createCommand() - ->update(Table::PURCHASABLES_STORES, ['stock' => 9, 'inventoryTracked' => true], ['storeId' => $primaryStore->id]) - ->execute(); - } - - $query = Product::find(); - $condition->modifyQuery($query); - - self::assertContainsEquals($product->id, $query->ids()); - - if (!$hasUnlimitedStock) { - foreach ($originalValues as $purchasableId => $values) { - \Craft::$app->getDb()->createCommand() - ->update(Table::PURCHASABLES_STORES, $values, ['purchasableId' => $purchasableId, 'storeId' => $primaryStore->id]) - ->execute(); - } - } - } - - /** - * @return array - */ - public function matchElementDataProvider(): array - { - return [ - [true], - [false], - ]; - } -} diff --git a/tests/unit/elements/product/conditions/ProductVariantPriceConditionRuleTest.php b/tests/unit/elements/product/conditions/ProductVariantPriceConditionRuleTest.php deleted file mode 100644 index 82364e6d9c..0000000000 --- a/tests/unit/elements/product/conditions/ProductVariantPriceConditionRuleTest.php +++ /dev/null @@ -1,116 +0,0 @@ - - * @since 4.3.0 - */ -class ProductVariantPriceConditionRuleTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @group Product - * @dataProvider matchElementDataProvider - */ - public function testMatchElement(float|int $price, ?string $operator, bool $expected): void - { - $condition = Product::createCondition(); - /** @var ProductVariantPriceConditionRule $rule */ - $rule = Craft::$app->getConditions()->createConditionRule(ProductVariantPriceConditionRule::class); - $rule->value = $price; - - if ($operator) { - $rule->operator = $operator; - } - - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - - self::assertSame($expected, $condition->matchElement($product)); - } - - /** - * @return void - * @throws Throwable - * @throws ElementNotFoundException - * @throws Exception - * @throws InvalidConfigException - * @dataProvider modifyQueryDataProvider - */ - public function testModifyQueryMatch(float|int $price, ?string $operator, int $expected): void - { - $condition = Product::createCondition(); - /** @var ProductVariantPriceConditionRule $rule */ - $rule = Craft::$app->getConditions()->createConditionRule(ProductVariantPriceConditionRule::class); - $rule->value = $price; - - if ($operator) { - $rule->operator = $operator; - } - - $condition->addConditionRule($rule); - - $query = Product::find(); - $condition->modifyQuery($query); - - self::assertCount($expected, $query->ids()); - } - - /** - * @return array[] - */ - public function matchElementDataProvider(): array - { - return [ - [100, '>', true], - [1000, '>', false], - [1000, '<', true], - [123.99, null, true], - ]; - } - - /** - * @return array[] - */ - public function modifyQueryDataProvider(): array - { - return [ - [100, '>', 1], - [1000, '>', 0], - [1000, '<', 2], - [123.99, null, 1], - ]; - } -} diff --git a/tests/unit/elements/product/conditions/ProductVariantSkuConditionRuleTest.php b/tests/unit/elements/product/conditions/ProductVariantSkuConditionRuleTest.php deleted file mode 100644 index 6de798d0e7..0000000000 --- a/tests/unit/elements/product/conditions/ProductVariantSkuConditionRuleTest.php +++ /dev/null @@ -1,89 +0,0 @@ - - * @since 4.3.0 - */ -class ProductVariantSkuConditionRuleTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @group Product - */ - public function testMatchElement(): void - { - $condition = Product::createCondition(); - /** @var ProductVariantSkuConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(ProductVariantSkuConditionRule::class); - $rule->value = 'rad-hood'; - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - - self::assertTrue($condition->matchElement($product)); - } - - /** - * @group Product - */ - public function testNotMatchElement(): void - { - $condition = Product::createCondition(); - /** @var ProductVariantSkuConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(ProductVariantSkuConditionRule::class); - $rule->value = 'does-not-exist'; - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - - self::assertFalse($condition->matchElement($product)); - } - - public function testModifyQueryMatch(): void - { - $condition = Product::createCondition(); - /** @var ProductVariantSkuConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(ProductVariantSkuConditionRule::class); - $rule->value = 'rad'; - $rule->operator = 'bw'; - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - - $query = Product::find(); - $condition->modifyQuery($query); - - self::assertContainsEquals($product->id, $query->ids()); - } -} diff --git a/tests/unit/elements/product/conditions/ProductVariantStockConditionRuleTest.php b/tests/unit/elements/product/conditions/ProductVariantStockConditionRuleTest.php deleted file mode 100644 index 1252afd4ff..0000000000 --- a/tests/unit/elements/product/conditions/ProductVariantStockConditionRuleTest.php +++ /dev/null @@ -1,123 +0,0 @@ - - * @since 4.3.0 - */ -class ProductVariantStockConditionRuleTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @group Product - */ - public function testMatchElement(): void - { - $condition = Product::createCondition(); - /** @var ProductVariantStockConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(ProductVariantStockConditionRule::class); - $rule->value = 10; - $rule->operator = '<'; - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - // /** @var Product $product */ - // $product = $productsFixture->getElement('rad-hoodie'); - - - $product = $this->make(Product::class, [ - 'getVariants' => fn() => VariantCollection::make([ - $this->make(Variant::class, [ - 'getStock' => 9, - 'inventoryTracked' => true, - ]), - ]), - ]); - - self::assertTrue($condition->matchElement($product)); - } - - /** - * @group Product - */ - public function testNotMatchElement(): void - { - $condition = Product::createCondition(); - /** @var ProductVariantStockConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(ProductVariantStockConditionRule::class); - $rule->value = 10; - $rule->operator = '<'; - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - - self::assertFalse($condition->matchElement($product)); - } - - public function testModifyQueryMatch(): void - { - $primaryStore = Plugin::getInstance()->getStores()->getPrimaryStore(); - $condition = Product::createCondition(); - /** @var ProductVariantStockConditionRule $rule */ - $rule = \Craft::$app->getConditions()->createConditionRule(ProductVariantStockConditionRule::class); - $rule->value = 10; - $rule->operator = '<'; - $condition->addConditionRule($rule); - - $productsFixture = $this->tester->grabFixture('products'); - /** @var Product $product */ - $product = $productsFixture->getElement('rad-hoodie'); - - $originalValues = (new Query()) - ->from(Table::PURCHASABLES_STORES) - ->select(['purchasableId', 'stock', 'inventoryTracked']) - ->indexBy('purchasableId') - ->all(); - - \Craft::$app->getDb()->createCommand() - ->update(Table::PURCHASABLES_STORES, ['stock' => 9, 'inventoryTracked' => true], ['storeId' => $primaryStore->id]) - ->execute(); - - $query = Product::find(); - $condition->modifyQuery($query); - - self::assertContainsEquals($product->id, $query->ids()); - - foreach ($originalValues as $purchasableId => $values) { - \Craft::$app->getDb()->createCommand() - ->update(Table::PURCHASABLES_STORES, $values, ['purchasableId' => $purchasableId, 'storeId' => $primaryStore->id]) - ->execute(); - } - } -} diff --git a/tests/unit/elements/subscriptions/SubscriptionTest.php b/tests/unit/elements/subscriptions/SubscriptionTest.php deleted file mode 100644 index 9359c50b48..0000000000 --- a/tests/unit/elements/subscriptions/SubscriptionTest.php +++ /dev/null @@ -1,208 +0,0 @@ - - * @since 4.0.4 - */ -class SubscriptionTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - 'subscriptions' => [ - 'class' => SubscriptionsFixture::class, - ], - ]; - } - - /** - * @param array $attributes - * @param Order|null $order - * @return void - * @throws InvalidConfigException - * @dataProvider getOrderDataProvider - */ - public function testGetOrder(?string $orderFixtureHandle): void - { - $subscription = Craft::createObject(Subscription::class); - - if ($orderFixtureHandle) { - $orderFixture = $this->tester->grabFixture('orders')->getElement($orderFixtureHandle); - $subscription->orderId = $orderFixture->id; - $order = Plugin::getInstance()->getOrders()->getOrderById($orderFixture->id); - - self::assertEquals($order->toArray(), $subscription->getOrder()->toArray()); - } else { - self::assertEquals(null, $subscription->getOrder()); - } - } - - /** - * @return array - */ - public function getOrderDataProvider(): array - { - return [ - 'no-order' => [ - null, - ], - 'order' => [ - 'completed-new', - ], - ]; - } - - /** - * @param array $attributes - * @param string $expected - * @return void - * @throws InvalidConfigException - * @dataProvider getGatewayDataProvider - */ - public function testGetGateway(array $attributes, ?string $expected): void - { - $subscription = Craft::createObject(Subscription::class, [ - 'config' => [ - 'attributes' => $attributes, - ], - ]); - - if ($expected === null) { - self::assertNull($subscription->getGateway()); - } else { - self::assertEquals($expected, $subscription->getGateway()->handle); - } - } - - /** - * @return array[] - */ - public function getGatewayDataProvider(): array - { - return [ - 'no-gateway' => [ - [], - null, - ], - 'gateway' => [ - ['gatewayId' => 1], - 'dummy', - ], - ]; - } - - /** - * @param array $attributes - * @param array|null $expected - * @return void - * @throws InvalidConfigException - * @dataProvider getAlternativePlansDataProvider - */ - public function testGetAlternativePlans(array $attributes, ?array $expected): void - { - $subscription = Craft::createObject(Subscription::class, [ - 'config' => [ - 'attributes' => $attributes, - ], - ]); - - if ($expected === null) { - self::assertNull($subscription->getAlternativePlans()); - } else { - self::assertEquals($expected, $subscription->getAlternativePlans()); - } - } - - /** - * @return array[] - */ - public function getAlternativePlansDataProvider(): array - { - return [ - 'no-gateway' => [ - [], - [], - ], - 'gateway' => [ - ['gatewayId' => 1], - [], - ], - ]; - } - - /** - * @param array $attributes - * @param bool $expected - * @return void - * @throws InvalidConfigException - * @dataProvider getIsOnTrialDataProvider - */ - public function testGetIsOnTrial(array $attributes, bool $expected): void - { - $subscription = Craft::createObject(Subscription::class, [ - 'config' => [ - 'attributes' => $attributes, - ], - ]); - - self::assertEquals($subscription->getIsOnTrial(), $expected); - } - - /** - * @return array[] - * @throws \Exception - */ - public function getIsOnTrialDataProvider(): array - { - return [ - 'no-attributes' => [ - [], - false, - ], - 'on-trial' => [ - ['trialDays' => 10], - false, - ], - 'expired' => [ - [ - 'isExpired' => true, - 'dataExpired' => (new DateTime('yesterday', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 'trialDays' => 10, - ], - false, - ], - ]; - } -} diff --git a/tests/unit/elements/user/CustomerBehaviorTest.php b/tests/unit/elements/user/CustomerBehaviorTest.php deleted file mode 100644 index f5efa97442..0000000000 --- a/tests/unit/elements/user/CustomerBehaviorTest.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @since 5.0.10 - */ -class CustomerBehaviorTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - public function testHasPropertiesAndMethods(): void - { - $user = User::find()->one(); - - self::assertInstanceOf(CustomerBehavior::class, $user->getBehavior('commerce:customer')); - self::assertArrayHasKey('primaryBillingAddressId', $user->toArray()); - self::assertArrayHasKey('primaryShippingAddressId', $user->toArray()); - } -} diff --git a/tests/unit/elements/user/UserEmailTest.php b/tests/unit/elements/user/UserEmailTest.php deleted file mode 100644 index 8743c54e87..0000000000 --- a/tests/unit/elements/user/UserEmailTest.php +++ /dev/null @@ -1,136 +0,0 @@ - - * @since 4.2.12 - */ -class UserEmailTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var string|null - */ - private ?string $_originalEmail = null; - - /** - * @var User|null - */ - private ?User $_user = null; - - /** - * @var array - */ - private array $_deleteElementIds = []; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @inheritdoc - */ - protected function _before(): void - { - parent::_before(); - - $this->_user = User::find()->admin()->one(); - $this->_originalEmail = $this->_user->email; - } - - public function testUpdatedEmail(): void - { - $completedOrder = $this->tester->grabFixture('orders')->getElement('completed-new'); - $lineItem = $completedOrder->getLineItems()[0]; - $qty = 4; - $note = 'My note'; - - // Create order - $order = new Order(); - $order->setCustomer($this->_user); - $lineItem = Plugin::getInstance()->getLineItems()->create($order, [ - 'purchasableId' => $lineItem->purchasableId, - 'qty' => $qty, - 'note' => $note, - ]); - $order->setLineItems([$lineItem]); - $order->markAsComplete(); - $this->_deleteElementIds[] = $order->id; - - // Create cart - $cart = new Order(); - $cart->setCustomer($this->_user); - $lineItem = Plugin::getInstance()->getLineItems()->create($cart, [ - 'purchasableId' => $lineItem->purchasableId, - 'options' => [], - 'qty' => $qty, - 'note' => $note, - ]); - $cart->setLineItems([$lineItem]); - \Craft::$app->getElements()->saveElement($cart, false, false, false); - $this->_deleteElementIds[] = $cart->id; - - // Update email - $newEmail = 'changed@emailaddress.xyz'; - $this->_user->email = $newEmail; - \Craft::$app->getElements()->saveElement($this->_user, false, false ,false); - - $emails = (new Query()) - ->from(\craft\commerce\db\Table::ORDERS) - ->select(['email']) - ->where(['id' => [$order->id, $cart->id]]) - ->column(); - - self::assertNotEmpty($emails); - foreach ($emails as $email) { - self::assertEquals($newEmail, $email); - } - } - - /** - * @inheritdoc - */ - protected function _after(): void - { - parent::_after(); - - // Reset user email - $this->_user->email = $this->_originalEmail; - \Craft::$app->getElements()->saveElement($this->_user, false, false, false); - $this->_user = null; - $this->_originalEmail = null; - - // Cleanup data. - foreach ($this->_deleteElementIds as $elementId) { - \Craft::$app->getElements()->deleteElementById($elementId, null, null, true); - } - } -} diff --git a/tests/unit/elements/user/UserSubscriptionDeletionTest.php b/tests/unit/elements/user/UserSubscriptionDeletionTest.php deleted file mode 100644 index 7aa6bf6285..0000000000 --- a/tests/unit/elements/user/UserSubscriptionDeletionTest.php +++ /dev/null @@ -1,91 +0,0 @@ - - * @since 5.7.0 - */ -class UserSubscriptionDeletionTest extends Unit -{ - protected UnitTester $tester; - - private ?User $_user = null; - private ?int $_subscriptionId = null; - - public function _fixtures(): array - { - return [ - 'plans' => ['class' => SubscriptionPlansFixture::class], - ]; - } - - protected function _before(): void - { - parent::_before(); - - $user = new User(); - $user->username = 'subscription-cascade-test-' . uniqid(); - $user->email = 'subscription-cascade-' . uniqid() . '@crafttest.com'; - Craft::$app->getElements()->saveElement($user, false); - $this->_user = $user; - - $plan = $this->tester->grabFixture('plans')->getModel('monthly'); - - $subscription = new Subscription(); - $subscription->userId = $user->id; - $subscription->planId = $plan->id; - $subscription->gatewayId = $plan->gatewayId; - $subscription->reference = 'test-cascade-' . uniqid(); - $subscription->trialDays = 0; - $subscription->hasStarted = true; - $subscription->subscriptionData = ['test' => 'cascade-delete']; - Craft::$app->getElements()->saveElement($subscription, false); - $this->_subscriptionId = $subscription->id; - } - - public function testSubscriptionIsDeletedWhenUserIsHardDeleted(): void - { - self::assertNotNull( - Subscription::find()->id($this->_subscriptionId)->status(null)->one(), - 'Subscription should exist before user deletion.' - ); - - // Soft-delete then hard-delete the user, mimicking the trash → permanently delete flow - Craft::$app->getElements()->deleteElement($this->_user); - Craft::$app->getElements()->deleteElement($this->_user, true); - $this->_user = null; - - self::assertNull( - Subscription::find()->id($this->_subscriptionId)->status(null)->one(), - 'Subscription should be deleted when its user is hard-deleted.' - ); - } - - protected function _after(): void - { - parent::_after(); - - // Clean up if the test failed before the user was deleted - if ($this->_user?->id) { - Craft::$app->getElements()->deleteElementById($this->_user->id, User::class, null, true); - } - - $this->_user = null; - $this->_subscriptionId = null; - } -} diff --git a/tests/unit/elements/variant/PricingCatalogTest.php b/tests/unit/elements/variant/PricingCatalogTest.php deleted file mode 100644 index 47a44b7fd4..0000000000 --- a/tests/unit/elements/variant/PricingCatalogTest.php +++ /dev/null @@ -1,346 +0,0 @@ - - * @since 5.0.0 - */ -class PricingCatalogTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @return void - * @throws Throwable - * @throws InvalidConfigException - */ - public function testVariantPricing(): void - { - $variant = Variant::find()->sku('rad-hood')->one(); - - Plugin::getInstance()->set('catalogPricingRules', $this->make(CatalogPricingRules::class, [ - 'canUseCatalogPricingRules' => function() { - self::atLeastOnce(); - return true; - }, - ])); - - Plugin::getInstance()->set('sales', $this->make(Sales::class, [ - 'getAllSales' => function() { - self::never(); - return []; - }, - ])); - - self::assertEquals(123.99, $variant->getPrice()); - self::assertEquals(null, $variant->getPromotionalPrice()); - self::assertEquals(123.99, $variant->getSalePrice()); - } - - /** - * @param string $sku - * @param array|null $rules - * @param float|int|null $salePrice - * @param float|int|null $promotionalPrice - * @param float|int|null $price - * @return void - * @throws Exception - * @throws InvalidConfigException - * @throws StaleObjectException - * @throws Throwable - * @throws \yii\db\Exception - * @dataProvider variantCatalogPricesDataProvider - * @since 5.1.0 - */ - public function testVariantCatalogPrices(string $sku, ?array $rules, float|int|null $salePrice, float|int|null $promotionalPrice, float|int|null $price): void - { - $catalogPricingRules = []; - - if (!empty($rules)) { - foreach ($rules as $rule) { - $catalogPricingRule = Craft::createObject($rule); - Plugin::getInstance()->getCatalogPricingRules()->saveCatalogPricingRule($catalogPricingRule); - $catalogPricingRules[] = $catalogPricingRule->id; - } - - Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); - } - - $variant = Variant::find()->sku($sku)->one(); - - self::assertInstanceof(Variant::class, $variant); - self::assertEquals($price, $variant->getPrice()); - self::assertEquals($promotionalPrice, $variant->getPromotionalPrice()); - self::assertEquals($salePrice, $variant->getSalePrice()); - - // Tidy up at the end of the test - if (!empty($catalogPricingRules)) { - foreach ($catalogPricingRules as $catalogPricingRule) { - Plugin::getInstance()->getCatalogPricingRules()->deleteCatalogPricingRuleById($catalogPricingRule); - } - - Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); - } - } - - /** - * @param string $sku - * @param array|null $rules - * @param float|int|null $salePrice - * @param float|int|null $promotionalPrice - * @param float|int|null $price - * @return void - * @throws Exception - * @throws InvalidConfigException - * @throws StaleObjectException - * @throws Throwable - * @throws \yii\db\Exception - * @dataProvider variantCatalogPricesDataProvider - * @since 5.1.0 - */ - public function testVariantCatalogPricesQuerying(string $sku, ?array $rules, float|int|null $salePrice, float|int|null $promotionalPrice, float|int|null $price): void - { - $catalogPricingRules = $this->_createCatalogPricingRules($rules); - - $variant = Variant::find()->price($price)->one(); - self::assertInstanceof(Variant::class, $variant); - self::assertEquals($sku, $variant->getSku()); - self::assertEquals($price, $variant->getPrice()); - - if ($promotionalPrice !== null) { - $variant = Variant::find()->promotionalPrice($promotionalPrice)->one(); - self::assertInstanceof(Variant::class, $variant); - self::assertEquals($sku, $variant->getSku()); - self::assertEquals($promotionalPrice, $variant->getPromotionalPrice()); - } - - $variant = Variant::find()->salePrice($salePrice)->one(); - self::assertInstanceof(Variant::class, $variant); - self::assertEquals($sku, $variant->getSku()); - self::assertEquals($salePrice, $variant->getSalePrice()); - - // Tidy up at the end of the test - $this->_deleteCatalogPricingRules($catalogPricingRules); - } - - /** - * @param string $sku - * @param array|null $rules - * @param float|int|null $salePrice - * @param float|int|null $promotionalPrice - * @param float|int|null $price - * @return void - * @throws Exception - * @throws InvalidConfigException - * @throws StaleObjectException - * @throws Throwable - * @throws \yii\db\Exception - * @dataProvider variantCatalogPricesDataProvider - * @since 5.2.0 - */ - public function testVariantOnPromotion(string $sku, ?array $rules, float|int|null $salePrice, float|int|null $promotionalPrice, float|int|null $price): void - { - $catalogPricingRules = $this->_createCatalogPricingRules($rules); - - $variantHasPromotionalPrice = Variant::find()->onPromotion()->one(); - $variantHasntPromotionalPrice = Variant::find()->onPromotion(false)->one(); - - if ($promotionalPrice !== null) { - self::assertInstanceof(Variant::class, $variantHasPromotionalPrice); - self::assertEquals($sku, $variantHasPromotionalPrice->getSku()); - self::assertEquals($promotionalPrice, $variantHasPromotionalPrice->getPromotionalPrice()); - } else { - self::assertNull($variantHasPromotionalPrice); - } - - $this->_deleteCatalogPricingRules($catalogPricingRules); - } - - /** - * @param array|null $rules - * @return array - * @throws Exception - * @throws InvalidConfigException - * @throws \yii\db\Exception - */ - private function _createCatalogPricingRules(?array $rules): array - { - $catalogPricingRules = []; - - if (!empty($rules)) { - foreach ($rules as $rule) { - $catalogPricingRule = Craft::createObject($rule); - Plugin::getInstance()->getCatalogPricingRules()->saveCatalogPricingRule($catalogPricingRule); - $catalogPricingRules[] = $catalogPricingRule->id; - } - - Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); - } - - return $catalogPricingRules; - } - - /** - * @param array|null $rules - * @return void - * @throws InvalidConfigException - * @throws StaleObjectException - * @throws Throwable - * @throws \yii\db\Exception - */ - private function _deleteCatalogPricingRules(?array $rules): void - { - // Tidy up at the end of the test - if (!empty($rules)) { - foreach ($rules as $catalogPricingRule) { - Plugin::getInstance()->getCatalogPricingRules()->deleteCatalogPricingRuleById($catalogPricingRule); - } - - Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); - } - } - - /** - * @return array[] - * @throws InvalidConfigException - */ - public function variantCatalogPricesDataProvider(): array - { - return [ - 'no catalog prices' => [ - 'sku' => 'rad-hood', - 'rules' => null, - 'salePrice' => 123.99, - 'promotionalPrice' => null, - 'price' => 123.99, - ], - 'rad hood reduced price' => [ - 'sku' => 'rad-hood', - 'rules' => [ - [ - 'class' => CatalogPricingRule::class, - 'attributes' => [ - 'name' => 'Test Rule', - 'storeId' => 1, - 'applyAmount' => -0.1, - 'variantCondition' => Craft::$app->getConditions()->createCondition([ - 'class' => CatalogPricingRuleVariantCondition::class, - 'conditionRules' => [ - Craft::$app->getConditions()->createConditionRule([ - 'type' => SkuConditionRule::class, - 'value' => 'rad-hood', - ]), - ], - ]), - ], - ], - ], - 'salePrice' => 111.59, - 'promotionalPrice' => null, - 'price' => 111.59, - ], - 'rad hood promotional price' => [ - 'sku' => 'rad-hood', - 'rules' => [ - [ - 'class' => CatalogPricingRule::class, - 'attributes' => [ - 'name' => 'Test Rule', - 'storeId' => 1, - 'applyAmount' => -0.1, - 'isPromotionalPrice' => true, - 'variantCondition' => Craft::$app->getConditions()->createCondition([ - 'class' => CatalogPricingRuleVariantCondition::class, - 'conditionRules' => [ - Craft::$app->getConditions()->createConditionRule([ - 'type' => SkuConditionRule::class, - 'value' => 'rad-hood', - ]), - ], - ]), - ], - ], - ], - 'salePrice' => 111.59, - 'promotionalPrice' => 111.59, - 'price' => 123.99, - ], - 'rad hood two rules' => [ - 'sku' => 'rad-hood', - 'rules' => [ - [ - 'class' => CatalogPricingRule::class, - 'attributes' => [ - 'name' => 'Test Rule - 5%', - 'storeId' => 1, - 'applyAmount' => -0.05, - 'variantCondition' => Craft::$app->getConditions()->createCondition([ - 'class' => CatalogPricingRuleVariantCondition::class, - 'conditionRules' => [ - Craft::$app->getConditions()->createConditionRule([ - 'type' => SkuConditionRule::class, - 'value' => 'rad-hood', - ]), - ], - ]), - ], - ], - [ - 'class' => CatalogPricingRule::class, - 'attributes' => [ - 'name' => 'Test Rule - 1%', - 'storeId' => 1, - 'applyAmount' => -0.01, - 'isPromotionalPrice' => true, - 'variantCondition' => Craft::$app->getConditions()->createCondition([ - 'class' => CatalogPricingRuleVariantCondition::class, - 'conditionRules' => [ - Craft::$app->getConditions()->createConditionRule([ - 'type' => SkuConditionRule::class, - 'value' => 'rad-hood', - ]), - ], - ]), - ], - ], - ], - 'salePrice' => 117.79, - 'promotionalPrice' => null, - 'price' => 117.79, - ], - ]; - } -} diff --git a/tests/unit/elements/variant/PricingSalesTest.php b/tests/unit/elements/variant/PricingSalesTest.php deleted file mode 100644 index 20ee2b3e11..0000000000 --- a/tests/unit/elements/variant/PricingSalesTest.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @since 5.0.0 - */ -class PricingSalesTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - 'sales' => [ - 'class' => SalesFixture::class, - ], - ]; - } - - public function testVariantPricing() - { - $variant = Variant::find()->sku('rad-hood')->one(); - - Plugin::getInstance()->set('catalogPricingRules', $this->make(CatalogPricingRules::class, [ - 'canUseCatalogPricingRules' => function() { - self::atLeastOnce(); - return false; - }, - ])); - - self::assertEquals(123.99, $variant->getPrice()); - self::assertEquals(111.59, $variant->getPromotionalPrice()); - self::assertEquals(111.59, $variant->getSalePrice()); - } -} diff --git a/tests/unit/elements/variant/VariantCollectionTest.php b/tests/unit/elements/variant/VariantCollectionTest.php deleted file mode 100644 index 843c5b7148..0000000000 --- a/tests/unit/elements/variant/VariantCollectionTest.php +++ /dev/null @@ -1,89 +0,0 @@ - - * @since 5.0.0 - */ -class VariantCollectionTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @return void - */ - public function testVariantQueryCollect(): void - { - $collection = Variant::find()->limit(4)->collect(); - - self::assertInstanceOf(VariantCollection::class, $collection); - self::assertInstanceOf(ElementCollection::class, $collection); - } - - /** - * @return void - */ - public function testCheapest(): void - { - $collection = Variant::find()->collect(); - - self::assertInstanceOf(VariantCollection::class, $collection); - self::assertNotNull($collection->cheapest()); - self::assertEquals('hct-white', $collection->cheapest()->sku); - } - - public function testMake(): void - { - $product = Product::find()->slug('rad-hoodie')->one(); - $attrs = [ - 'ownerId' => $product->id, - 'owner' => $product, - 'primaryOwnerId' => $product->id, - 'primaryOwner' => $product, - 'title' => 'Test Variant', - 'basePrice' => 123, - 'sku' => '123', - 'enabled' => true, - 'myVariantHeadingField' => 'bar', - ]; - - $collection = VariantCollection::make([$attrs]); - - self::assertInstanceOf(VariantCollection::class, $collection); - - $variant = $collection->first(); - foreach ([ - 'title', - 'basePrice', - 'sku', - 'myVariantHeadingField', - ] as $key) { - self::assertEquals($attrs[$key], $variant->$key); - } - } -} diff --git a/tests/unit/elements/variant/VariantEagerLoadingTest.php b/tests/unit/elements/variant/VariantEagerLoadingTest.php deleted file mode 100644 index 617e1aec89..0000000000 --- a/tests/unit/elements/variant/VariantEagerLoadingTest.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @since 5.4.8 - */ -class VariantEagerLoadingTest extends Unit -{ - /** - * @var UnitTester - */ - protected $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - public function testEagerLoadingMap(): void - { - $variants = Variant::find()->all(); - - $handles = [ - 'product' => ['elementType' => Product::class], - 'owner' => ['elementType' => Product::class], - 'primaryOwner' => ['elementType' => Product::class], - 'customField' => null, - ]; - - foreach ($handles as $handle => $value) { - $map = Variant::eagerLoadingMap($variants, $handle); - - if (is_array($value)) { - self::assertNotEmpty($map); - foreach ($value as $key => $item) { - self::assertArrayHasKey($key, $map); - self::assertEquals($item, $map[$key]); - } - } else { - self::assertEmpty($map); - } - } - } -} diff --git a/tests/unit/elements/variant/VariantOwnerTest.php b/tests/unit/elements/variant/VariantOwnerTest.php deleted file mode 100644 index ac22ab6762..0000000000 --- a/tests/unit/elements/variant/VariantOwnerTest.php +++ /dev/null @@ -1,168 +0,0 @@ - - * @since 5.4.4 - */ -class VariantOwnerTest extends Unit -{ - /** - * @var \UnitTester - */ - protected $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * Test that ownerType is set to Product in init() - */ - public function testOwnerTypeIsSetInInit(): void - { - $variant = new Variant(); - - // Access protected ownerType property - $reflection = new ReflectionClass($variant); - $ownerTypeProperty = $reflection->getProperty('ownerType'); - $ownerTypeProperty->setAccessible(true); - - self::assertEquals(Product::class, $ownerTypeProperty->getValue($variant)); - } - - /** - * Test that 'product' is included in extraFields - */ - public function testProductIncludedInExtraFields(): void - { - $variant = new Variant(); - $extraFields = $variant->extraFields(); - - self::assertContains('product', $extraFields); - } - - /** - * Test getOwner returns Product instance - */ - public function testGetOwnerReturnsProduct(): void - { - /** @var ProductFixture $productFixture */ - $productFixture = $this->tester->grabFixture('products'); - $product = $productFixture->getElement('rad-hoodie'); - self::assertNotNull($product); - - $variants = $product->getVariants(); - self::assertFalse($variants->isEmpty()); - - $variant = $variants->first(); - $owner = $variant->getOwner(); - - self::assertInstanceOf(Product::class, $owner); - self::assertEquals($product->id, $owner->id); - } - - /** - * Test getPrimaryOwner returns Product instance - */ - public function testGetPrimaryOwnerReturnsProduct(): void - { - /** @var ProductFixture $productFixture */ - $productFixture = $this->tester->grabFixture('products'); - $product = $productFixture->getElement('rad-hoodie'); - self::assertNotNull($product); - - $variants = $product->getVariants(); - self::assertFalse($variants->isEmpty()); - - $variant = $variants->first(); - $primaryOwner = $variant->getPrimaryOwner(); - - self::assertInstanceOf(Product::class, $primaryOwner); - self::assertEquals($product->id, $primaryOwner->id); - } - - /** - * Test that getProduct() still works (backward compatibility) - */ - public function testGetProductMethodStillWorks(): void - { - /** @var ProductFixture $productFixture */ - $productFixture = $this->tester->grabFixture('products'); - $product = $productFixture->getElement('hypercolor-tshirt'); - self::assertNotNull($product); - - $variants = $product->getVariants(); - self::assertFalse($variants->isEmpty()); - - $variant = $variants->first(); - $productFromMethod = $variant->getProduct(); - - self::assertInstanceOf(Product::class, $productFromMethod); - self::assertEquals($product->id, $productFromMethod->id); - } - - /** - * Test setOwner works with Product - */ - public function testSetOwnerWorksWithProduct(): void - { - /** @var ProductFixture $productFixture */ - $productFixture = $this->tester->grabFixture('products'); - $product = $productFixture->getElement('rad-hoodie'); - self::assertNotNull($product); - - $variant = new Variant(); - $variant->setOwner($product); - - $owner = $variant->getOwner(); - self::assertInstanceOf(Product::class, $owner); - self::assertEquals($product->id, $owner->id); - } - - /** - * Test variant owner relationship with different sites - */ - public function testVariantOwnerWithDifferentSites(): void - { - // Get a product that exists in multiple sites - /** @var ProductFixture $productFixture */ - $productFixture = $this->tester->grabFixture('products'); - $product = $productFixture->getElement('double-decker-bus-toy'); - self::assertNotNull($product); - - // Get variants - $variants = $product->getVariants(); - self::assertFalse($variants->isEmpty()); - - $variant = $variants->first(); - - // Test that owner is resolved correctly even for different sites - $owner = $variant->getOwner(); - self::assertInstanceOf(Product::class, $owner); - self::assertEquals($product->id, $owner->id); - self::assertEquals($product->siteId, $owner->siteId); - } -} diff --git a/tests/unit/elements/variant/VariantQueryTest.php b/tests/unit/elements/variant/VariantQueryTest.php deleted file mode 100644 index 35818f2ac8..0000000000 --- a/tests/unit/elements/variant/VariantQueryTest.php +++ /dev/null @@ -1,566 +0,0 @@ - - * @since 5.0.0 - */ -class VariantQueryTest extends Unit -{ - /** - * @var UnitTester - */ - protected $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'shippingCategories' => [ - 'class' => ShippingCategoryFixture::class, - ], - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @return void - */ - public function testQuery(): void - { - self::assertInstanceOf(VariantQuery::class, Variant::find()); - } - - /** - * @return void - */ - public function testShippingCategoryId(): void - { - self::assertTrue(method_exists(Variant::find(), 'shippingCategoryId'), 'shippingCategoryId method exists'); - - [$shippingCategoryId, $tests] = $this->_getShippingCategoryIdData(); - - foreach ($tests as $key => [$criteria, $count]) { - $query = Variant::find(); - $query->shippingCategoryId($criteria); - - self::assertCount($count, $query->all(), "shippingCategoryId Test $key"); - } - } - - /** - * @return void - */ - public function testShippingCategoryIdProperty(): void - { - self::assertTrue(property_exists(Variant::find(), 'shippingCategoryId'), 'shippingCategoryId property exists'); - - [$shippingCategoryId, $tests] = $this->_getShippingCategoryIdData(); - - foreach ($tests as $key => [$criteria, $count]) { - $query = Variant::find(); - $query->shippingCategoryId = $criteria; - self::assertCount($count, $query->all(), "shippingCategoryIdProperty Test $key"); - } - } - - /** - * @return array - */ - private function _getShippingCategoryIdData(): array - { - $fixture = $this->tester->grabFixture('shippingCategories'); - $shippingCategoryId = $fixture->data['anotherShippingCategory']['id']; - - return [ - $shippingCategoryId, - [ - 'no-params' => [null, 3], - 'specific-id' => [$shippingCategoryId, 1], - 'in' => [[$shippingCategoryId, 99999], 1], - 'not-in' => [['not', 99998, 99999], 3], - ], - ]; - } - - /** - * @return void - */ - public function testShippingCategory(): void - { - self::assertTrue(method_exists(Variant::find(), 'shippingCategoryId')); - $fixture = $this->tester->grabFixture('shippingCategories'); - $shippingCategoryId = $fixture->data['anotherShippingCategory']['id']; - - $matchingShippingCategory = new ShippingCategory(['id' => $shippingCategoryId]); - $nonMatchingShippingCategory = new ShippingCategory(['id' => 99999]); - - $tests = [ - 'no-params' => [null, 3], - 'specific-handle' => ['anotherShippingCategory', 1], - 'in' => [['anotherShippingCategory', 'general'], 3], - 'not-in' => [['not', 'foo', 'bar'], 3], - 'matching-shipping-category' => [$matchingShippingCategory, 1], - 'non-matching-shipping-category' => [$nonMatchingShippingCategory, 0], - ]; - - foreach ($tests as $key => [$criteria, $count]) { - $query = Variant::find(); - $query->shippingCategory($criteria); - - self::assertCount($count, $query->all()); - } - } - - /** - * @param int $count - * @return void - * @dataProvider taxCategoryIdDataProvider - */ - public function testTaxCategoryId(mixed $taxCategoryId, int $count): void - { - $query = Variant::find(); - - self::assertTrue(method_exists($query, 'taxCategoryId')); - $query->taxCategoryId($taxCategoryId); - - self::assertCount($count, $query->all()); - } - - /** - * @param int $count - * @return void - * @dataProvider taxCategoryIdDataProvider - */ - public function testTaxCategoryIdProperty(mixed $taxCategoryId, int $count): void - { - $query = Variant::find(); - - self::assertTrue(method_exists($query, 'taxCategoryId')); - $query->taxCategoryId = $taxCategoryId; - - self::assertCount($count, $query->all()); - } - - /** - * @return array - */ - public function taxCategoryIdDataProvider(): array - { - return [ - 'no-params' => [null, 3], - 'specific-id' => [101, 3], - 'in' => [[101, 102], 3], - 'not-in' => [['not', 102, 103], 3], - 'greater-than' => ['> 100', 3], - 'less-than' => ['< 100', 0], - ]; - } - - /** - * @param int $count - * @return void - * @dataProvider taxCategoryDataProvider - */ - public function testTaxCategory(mixed $taxCategory, int $count): void - { - $query = Variant::find(); - - self::assertTrue(method_exists($query, 'taxCategoryId')); - $query->taxCategory($taxCategory); - - self::assertCount($count, $query->all()); - } - - /** - * @return array - */ - public function taxCategoryDataProvider(): array - { - $matchingTaxCategory = new TaxCategory(['id' => 101]); - $nonMatchingTaxCategory = new TaxCategory(['id' => 999]); - - return [ - 'no-params' => [null, 3], - 'specific-handle' => ['anotherTaxCategory', 3], - 'in' => [['anotherTaxCategory', 'general'], 3], - 'not-in' => [['not', 'foo', 'bar'], 3], - 'matching-tax-category' => [$matchingTaxCategory, 3], - 'non-matching-tax-category' => [$nonMatchingTaxCategory, 0], - ]; - } - - /** - * @param array $sites - * @return void - * @dataProvider queryingBySiteDataProvider - */ - public function testQueryingBySite(array $sites, int $count, array $siteHandleToStoreHandle): void - { - $query = Variant::find(); - $query->site($sites); - $results = $query->all(); - - // Assert the correct number of results - self::assertCount($count, $results); - - // Check that by querying site the correct store is returned - foreach ($results as $variant) { - self::assertSame($siteHandleToStoreHandle[$variant->getSite()->handle], $variant->getStore()->handle); - } - } - - public function queryingBySiteDataProvider(): array - { - return [ - 'one-site' => [['testSite1'], 3, ['testSite1' => 'primary']], - 'two-sites-same-store' => [['testSite1', 'defaultSite'], 6, ['testSite1' => 'primary', 'defaultSite' => 'primary']], - 'two-sites-different-stores' => [['testSite1', 'testSite2'], 6, ['testSite1' => 'primary', 'testSite2' => 'euStore']], - ]; - } - - /** - * @return void - */ - public function testHasPricePropertiesPopulated(): void - { - $query = Variant::find(); - $results = $query->all(); - - foreach ($results as $variant) { - self::assertNotNull($variant->price); - self::assertNotNull($variant->salePrice); - } - } - - public function testPriceQueryForCatalogPricingRule(): void - { - // Create on the fly catalog pricing rule - $primaryStore = Plugin::getInstance()->getStores()->getPrimaryStore(); - $catalogPricingRule = new CatalogPricingRule(); - $catalogPricingRule->apply = \craft\commerce\records\CatalogPricingRule::APPLY_BY_PERCENT; - $catalogPricingRule->applyAmount = 50 / -100; - $catalogPricingRule->applyPriceType = \craft\commerce\records\CatalogPricingRule::APPLY_PRICE_TYPE_PRICE; - $catalogPricingRule->dateFrom = null; - $catalogPricingRule->dateTo = null; - $catalogPricingRule->description = ''; - $catalogPricingRule->enabled = true; - $catalogPricingRule->isPromotionalPrice = false; - $catalogPricingRule->name = 'Test'; - $catalogPricingRule->storeId = $primaryStore->id; - - $purchasableCondition = $catalogPricingRule->getPurchasableCondition(); - $purchasableConditionRule = new PurchasableConditionRule(); - $purchasableConditionRule->setElementIds([ - 'craft\\commerce\\elements\\Variant' => [(new Query())->from(Table::PURCHASABLES)->select('id')->where(['sku' => 'hct-blue'])->scalar()], - ]); - - $purchasableCondition->addConditionRule($purchasableConditionRule); - - $catalogPricingRule->setPurchasableCondition($purchasableCondition); - Plugin::getInstance()->getCatalogPricingRules()->saveCatalogPricingRule($catalogPricingRule); - - Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); - - $query = Variant::find(); - $query->price('<= 11'); - $results = $query->all(); - - self::assertCount(1, $results); - self::assertSame('hct-blue', $results[0]->sku); - self::assertEquals(11, $results[0]->getPrice()); - - // Check sale price - $query = Variant::find(); - $query->salePrice('<= 11'); - $results = $query->all(); - - self::assertCount(1, $results); - self::assertSame('hct-blue', $results[0]->sku); - self::assertEquals(11, $results[0]->getSalePrice()); - - // Delete the catalog pricing rule - Plugin::getInstance()->getCatalogPricingRules()->deleteCatalogPricingRuleById($catalogPricingRule->id); - Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); - } - - public function testPromotionalPriceQueryForCatalogPricingRule(): void - { - // Create on the fly catalog pricing rule - $primaryStore = Plugin::getInstance()->getStores()->getPrimaryStore(); - $catalogPricingRule = new CatalogPricingRule(); - $catalogPricingRule->apply = \craft\commerce\records\CatalogPricingRule::APPLY_BY_PERCENT; - $catalogPricingRule->applyAmount = 50 / -100; - $catalogPricingRule->applyPriceType = \craft\commerce\records\CatalogPricingRule::APPLY_PRICE_TYPE_PRICE; - $catalogPricingRule->dateFrom = null; - $catalogPricingRule->dateTo = null; - $catalogPricingRule->description = ''; - $catalogPricingRule->enabled = true; - $catalogPricingRule->isPromotionalPrice = true; - $catalogPricingRule->name = 'Test'; - $catalogPricingRule->storeId = $primaryStore->id; - - $purchasableCondition = $catalogPricingRule->getPurchasableCondition(); - $purchasableConditionRule = new PurchasableConditionRule(); - $purchasableConditionRule->setElementIds([ - 'craft\\commerce\\elements\\Variant' => [(new Query())->from(Table::PURCHASABLES)->select('id')->where(['sku' => 'hct-blue'])->scalar()], - ]); - - $purchasableCondition->addConditionRule($purchasableConditionRule); - - $catalogPricingRule->setPurchasableCondition($purchasableCondition); - Plugin::getInstance()->getCatalogPricingRules()->saveCatalogPricingRule($catalogPricingRule); - - Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); - - $query = Variant::find(); - $query->promotionalPrice('<= 11'); - $results = $query->all(); - - self::assertCount(1, $results); - self::assertSame('hct-blue', $results[0]->sku); - self::assertEquals(11, $results[0]->getPromotionalPrice()); - - // Check sale price - $query = Variant::find(); - $query->salePrice('<= 11'); - $results = $query->all(); - - self::assertCount(1, $results); - self::assertSame('hct-blue', $results[0]->sku); - self::assertEquals(11, $results[0]->getSalePrice()); - - // Check the price hasn't been altered - $query = Variant::find(); - $query->sku('hct-blue'); - $query->price('> 11'); - $results = $query->all(); - - self::assertCount(1, $results); - self::assertSame('hct-blue', $results[0]->sku); - self::assertEquals(21.99, $results[0]->getPrice()); - - // Delete the catalog pricing rule - Plugin::getInstance()->getCatalogPricingRules()->deleteCatalogPricingRuleById($catalogPricingRule->id); - Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); - } - - /** - * @param array $expectedSkuOrder - * @return void - * @dataProvider orderByDataProvider - */ - public function testOrderBy(mixed $orderBy, array $expectedSkuOrder): void - { - $query = Variant::find(); - $query->orderBy($orderBy); - - $results = $query->collect()->map(fn(Variant $v) => $v->getSku())->all(); - - self::assertEquals($expectedSkuOrder, $results); - } - - /** - * @return array[] - */ - public function orderByDataProvider(): array - { - return [ - 'sku-asc' => ['sku ASC', ['hct-blue', 'hct-white', 'rad-hood']], - 'price-asc' => ['price ASC', ['hct-white', 'hct-blue', 'rad-hood']], - 'price-desc' => ['price DESC', array_reverse(['hct-white', 'hct-blue', 'rad-hood'])], - 'sale-price-asc' => ['salePrice ASC', ['hct-white', 'hct-blue', 'rad-hood']], - 'sale-price-desc' => ['salePrice DESC', array_reverse(['hct-white', 'hct-blue', 'rad-hood'])], - 'base-price-asc' => ['basePrice ASC', ['hct-white', 'hct-blue', 'rad-hood']], - 'base-price-desc' => ['basePrice DESC', array_reverse(['hct-white', 'hct-blue', 'rad-hood'])], - ]; - } - - /** - * @param int $expectedCount - * @return void - * @since 5.5.0 - * @dataProvider productStatusDataProvider - */ - public function testProductStatus(mixed $status, int $expectedCount): void - { - $query = Variant::find(); - $query->productStatus($status); - - self::assertCount($expectedCount, $query->all()); - } - - /** - * @return array[] - */ - public function productStatusDataProvider(): array - { - return [ - 'product-live' => ['live', 3], - 'product-live-const' => [Product::STATUS_LIVE, 3], - 'product-live-const-array' => [[Product::STATUS_LIVE], 3], - 'product-pending' => ['pending', 0], - 'product-pending-const' => [Product::STATUS_PENDING, 0], - 'product-pending-const-array' => [[Product::STATUS_PENDING], 0], - 'product-expired' => ['expired', 0], - 'product-expired-const' => [Product::STATUS_EXPIRED, 0], - 'product-expired-const-array' => [[Product::STATUS_EXPIRED], 0], - 'product-enabled' => ['enabled', 3], - 'product-enabled-const' => [Element::STATUS_ENABLED, 3], - 'product-enabled-const-array' => [[Element::STATUS_ENABLED], 3], - 'product-disabled' => ['disabled', 0], - 'product-disabled-const' => [Element::STATUS_DISABLED, 0], - 'product-disabled-const-array' => [[Element::STATUS_DISABLED], 0], - 'product-enabled-disabled' => [['enabled', 'disabled'], 3], - 'product-enabled-disabled-const' => [[Element::STATUS_ENABLED, Element::STATUS_DISABLED], 3], - 'product-not-disabled-array' => [['not', Element::STATUS_DISABLED], 3], - 'product-not-enabled' => [['not', Element::STATUS_ENABLED], 0], - ]; - } - - /** - * Regression test for VariantQuery::beforePrepare() checking the current - * `commerce-viewProductType` permission (rather than the retired - * `commerce-editProductType` permission) when the `editable` param is set. - * - * @return void - * @since 5.7.0 - */ - public function testEditableRespectsViewProductTypePermission(): void - { - $originalIdentity = Craft::$app->getUser()->getIdentity(); - $teesUid = Plugin::getInstance()->getProductTypes()->getProductTypeByHandle('tShirts')->uid; - - $user = new User(); - $user->id = 999999; - $user->admin = false; - Craft::$app->getUser()->setIdentity($user); - - $this->tester->mockMethods( - Craft::$app, - 'userPermissions', - [ - 'getPermissionsByUserId' => fn() => ["commerce-viewproducttype:$teesUid"], - ], - [] - ); - - try { - $query = Variant::find(); - $query->editable(true); - $skus = $query->collect()->map(fn(Variant $v) => $v->getSku())->all(); - sort($skus); - - // Only the two variants belonging to the "tShirts" product type should be returned. - self::assertEquals(['hct-blue', 'hct-white'], $skus); - } finally { - Craft::$app->getUser()->setIdentity($originalIdentity); - } - } - - /** - * Regression test for VariantQuery::beforePrepare() checking the current - * `commerce-saveProductType` permission (rather than the retired - * `commerce-editProductType` permission) when the `savable` param is set. - * - * @return void - * @since 5.7.0 - */ - public function testSavableRespectsSaveProductTypePermission(): void - { - $originalIdentity = Craft::$app->getUser()->getIdentity(); - $teesUid = Plugin::getInstance()->getProductTypes()->getProductTypeByHandle('tShirts')->uid; - - $user = new User(); - $user->id = 999999; - $user->admin = false; - Craft::$app->getUser()->setIdentity($user); - - $this->tester->mockMethods( - Craft::$app, - 'userPermissions', - [ - 'getPermissionsByUserId' => fn() => ["commerce-saveproducttype:$teesUid"], - ], - [] - ); - - try { - $query = Variant::find(); - $query->savable(true); - $skus = $query->collect()->map(fn(Variant $v) => $v->getSku())->all(); - sort($skus); - - // Only the two variants belonging to the "tShirts" product type should be returned. - self::assertEquals(['hct-blue', 'hct-white'], $skus); - } finally { - Craft::$app->getUser()->setIdentity($originalIdentity); - } - } - - /** - * Pins the fix: holding only the retired `commerce-editProductType` permission - * must NOT satisfy editable()/savable() anymore. - * - * @return void - * @since 5.7.0 - */ - public function testEditableIgnoresLegacyEditProductTypePermission(): void - { - $originalIdentity = Craft::$app->getUser()->getIdentity(); - $teesUid = Plugin::getInstance()->getProductTypes()->getProductTypeByHandle('tShirts')->uid; - - $user = new User(); - $user->id = 999999; - $user->admin = false; - Craft::$app->getUser()->setIdentity($user); - - $this->tester->mockMethods( - Craft::$app, - 'userPermissions', - [ - 'getPermissionsByUserId' => fn() => ["commerce-editproducttype:$teesUid"], - ], - [] - ); - - try { - $query = Variant::find(); - $query->editable(true); - - self::assertCount(0, $query->all()); - } finally { - Craft::$app->getUser()->setIdentity($originalIdentity); - } - } -} diff --git a/tests/unit/gql/ArgumentHandlerTest.php b/tests/unit/gql/ArgumentHandlerTest.php deleted file mode 100644 index d98392ae55..0000000000 --- a/tests/unit/gql/ArgumentHandlerTest.php +++ /dev/null @@ -1,132 +0,0 @@ - - * @since 5.6.5 - */ -class ArgumentHandlerTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * Tests that HasProduct processes nested GQL relation arguments (e.g. relatedToEntries) - * by delegating to ArgumentManager, converting them to proper relatedTo criteria. - */ - public function testHasProductProcessesNestedRelationArgs(): void - { - $relatedEntryIds = [42, 99]; - - $relatedEntriesHandler = $this->make(RelatedEntries::class, [ - 'getIds' => fn() => [$relatedEntryIds], - ]); - - $argumentManager = new ArgumentManager(); - $relatedEntriesHandler->setArgumentManager($argumentManager); - $argumentManager->setHandler('relatedToEntries', $relatedEntriesHandler); - - $hasProductHandler = new HasProduct(); - $hasProductHandler->setArgumentManager($argumentManager); - $argumentManager->setHandler('hasProduct', $hasProductHandler); - - $result = $argumentManager->prepareArguments([ - 'hasProduct' => [ - 'relatedToEntries' => [['section' => 'news']], - ], - ]); - - self::assertIsArray($result['hasProduct']); - self::assertArrayNotHasKey('relatedToEntries', $result['hasProduct']); - self::assertArrayHasKey('relatedTo', $result['hasProduct']); - self::assertSame(['and', ['element' => $relatedEntryIds]], $result['hasProduct']['relatedTo']); - } - - /** - * Tests that HasProduct passes standard (non-GQL) criteria through unchanged. - */ - public function testHasProductPassesThroughStandardArgs(): void - { - $argumentManager = new ArgumentManager(); - $hasProductHandler = new HasProduct(); - $hasProductHandler->setArgumentManager($argumentManager); - $argumentManager->setHandler('hasProduct', $hasProductHandler); - - $result = $argumentManager->prepareArguments([ - 'hasProduct' => [ - 'slug' => 'rad-hoodie', - 'type' => 'hoodies', - ], - ]); - - self::assertSame(['slug' => 'rad-hoodie', 'type' => 'hoodies'], $result['hasProduct']); - } - - /** - * Tests that HasVariant processes nested GQL relation arguments (e.g. relatedToEntries) - * by delegating to ArgumentManager, converting them to proper relatedTo criteria. - */ - public function testHasVariantProcessesNestedRelationArgs(): void - { - $relatedEntryIds = [7, 13]; - - $relatedEntriesHandler = $this->make(RelatedEntries::class, [ - 'getIds' => fn() => [$relatedEntryIds], - ]); - - $argumentManager = new ArgumentManager(); - $relatedEntriesHandler->setArgumentManager($argumentManager); - $argumentManager->setHandler('relatedToEntries', $relatedEntriesHandler); - - $hasVariantHandler = new HasVariant(); - $hasVariantHandler->setArgumentManager($argumentManager); - $argumentManager->setHandler('hasVariant', $hasVariantHandler); - - $result = $argumentManager->prepareArguments([ - 'hasVariant' => [ - 'relatedToEntries' => [['section' => 'news']], - ], - ]); - - self::assertIsArray($result['hasVariant']); - self::assertArrayNotHasKey('relatedToEntries', $result['hasVariant']); - self::assertArrayHasKey('relatedTo', $result['hasVariant']); - self::assertSame(['and', ['element' => $relatedEntryIds]], $result['hasVariant']['relatedTo']); - } - - /** - * Tests that HasVariant passes standard (non-GQL) criteria through unchanged. - */ - public function testHasVariantPassesThroughStandardArgs(): void - { - $argumentManager = new ArgumentManager(); - $hasVariantHandler = new HasVariant(); - $hasVariantHandler->setArgumentManager($argumentManager); - $argumentManager->setHandler('hasVariant', $hasVariantHandler); - - $result = $argumentManager->prepareArguments([ - 'hasVariant' => [ - 'sku' => 'hct-blue', - ], - ]); - - self::assertSame(['sku' => 'hct-blue'], $result['hasVariant']); - } -} diff --git a/tests/unit/gql/ProductResolverTest.php b/tests/unit/gql/ProductResolverTest.php deleted file mode 100644 index ae8e655d60..0000000000 --- a/tests/unit/gql/ProductResolverTest.php +++ /dev/null @@ -1,106 +0,0 @@ -tester->mockMethods( - Craft::$app, - 'gql', - ['getActiveSchema' => $this->make(GqlSchema::class, [ - 'scope' => [ - 'productTypes.type-1-uid:read', - 'productTypes.type-2-uid:read', - ], - ])] - ); - } - - /** - * Test resolving fields on products. - * - * @dataProvider productFieldTestDataProvider - * - * @param string $gqlTypeClass The Gql type class - * @param string $propertyName The property being tested - * @param mixed $result True for exact match, false for non-existing or a callback for fetching the data - * @throws \Exception - */ - public function testProductFieldResolving(string $gqlTypeClass, string $propertyName, mixed $result): void - { - $typeHandle = StringHelper::UUID(); - - $mockElement = $this->make( - ProductElement::class, [ - 'postDate' => new \DateTime(), - '__get' => fn($property) => in_array($property, ['plainTextField', 'typeface'], false) ? 'ok' : $this->$property, - 'getType' => fn() => $this->make(ProductType::class, ['handle' => $typeHandle]), - ] - ); - - $this->_runTest($mockElement, $gqlTypeClass, $propertyName, $result); - } - - /** - * Run the test on an element for a type class with the property name. - * - * @param $element - * @param string $gqlTypeClass The Gql type class - * @param string $propertyName The property being tested - * @param mixed $result True for exact match, false for non-existing or a callback for fetching the data - * @throws \Exception - */ - public function _runTest($element, string $gqlTypeClass, string $propertyName, mixed $result): void - { - $resolveInfo = $this->make(ResolveInfo::class, ['fieldName' => $propertyName]); - $resolve = fn() => $this->make($gqlTypeClass)->resolveWithDirectives($element, [], null, $resolveInfo); - - if (is_callable($result)) { - self::assertEquals($result($element), $resolve()); - } elseif ($result === true) { - self::assertEquals($element->$propertyName, $resolve()); - self::assertNotNull($element->$propertyName); - } else { - $this->tester->expectThrowable(GqlException::class, $resolve); - } - } - - /** - * @return array - */ - public function productFieldTestDataProvider(): array - { - return [ - [ProductGqlType::class, 'productTypeHandle', fn($source) => $source->getType()->handle], - [ProductGqlType::class, 'plainTextField', true], - [ProductGqlType::class, 'notAField', false], - ]; - } -} diff --git a/tests/unit/helpers/CurrencyHelperTest.php b/tests/unit/helpers/CurrencyHelperTest.php deleted file mode 100644 index ac066c7dfe..0000000000 --- a/tests/unit/helpers/CurrencyHelperTest.php +++ /dev/null @@ -1,278 +0,0 @@ - - * @since 4.5.4 - */ -class CurrencyHelperTest extends Unit -{ - /** - * @var UnitTester - */ - protected $tester; - - /** - * @param string $currency - * @param string $language - * @param string $expected - * @return void - * @throws InvalidConfigException - * @throws CurrencyException - * @dataProvider formatAsCurrencyDataProvider - */ - public function testFormatAsCurrency(string $currency, string $language, string $expected): void - { - $originalLocale = \Craft::$app->getLocale(); - Locale::switchAppLanguage($language); - $amount = 1234.56; - $formattedValue = Currency::formatAsCurrency($amount, $currency); - - self::assertEquals($expected, $formattedValue); - Locale::switchAppLanguage($originalLocale->getLanguageID()); - } - - public function formatAsCurrencyDataProvider(): array - { - return [ - 'USD-US' => [ - 'USD', - 'en-US', - '$1,234.56', - ], - 'USD-GB' => [ - 'USD', - 'en-GB', - 'US$1,234.56', - ], - 'USD-FR' => [ - 'USD', - 'fr-FR', - '1 234,56 $US', - ], - 'EUR-US' => [ - 'EUR', - 'en-US', - '€1,234.56', - ], - 'EUR-GB' => [ - 'EUR', - 'en-GB', - '€1,234.56', - ], - 'EUR-FR' => [ - 'EUR', - 'fr-FR', - '1 234,56 €', - ], - ]; - } - - /** - * @param string $currency - * @param string $language - * @param string $expected - * @return void - * @dataProvider formatAsCurrencyStripZerosDataProvider - * @since 5.1.4 - */ - public function testFormatAsCurrencyStripZeros(string $currency, string $language, float $amount, bool $zeros, string $expected): void - { - $originalLocale = \Craft::$app->getLocale(); - Locale::switchAppLanguage($language); - $formattedValue = Currency::formatAsCurrency($amount, $currency, stripZeros: $zeros); - - self::assertEquals($expected, $formattedValue); - Locale::switchAppLanguage($originalLocale->getLanguageID()); - } - - /** - * @return array[] - */ - public function formatAsCurrencyStripZerosDataProvider(): array - { - return [ - 'USD-US' => [ - 'USD', - 'en-US', - 1234.56, - true, - '$1,234.56', - ], - 'USD-US-strip' => [ - 'USD', - 'en-US', - 1234.00, - true, - '$1,234', - ], - 'USD-US-no-strip' => [ - 'USD', - 'en-US', - 1234.00, - false, - '$1,234.00', - ], - 'USD-GB' => [ - 'USD', - 'en-GB', - 1234.56, - true, - 'US$1,234.56', - ], - 'USD-GB-strip' => [ - 'USD', - 'en-GB', - 1234.0, - true, - 'US$1,234', - ], - 'USD-GB-no-strip' => [ - 'USD', - 'en-GB', - 1234.0, - false, - 'US$1,234.00', - ], - 'USD-FR' => [ - 'USD', - 'fr-FR', - 1234.56, - true, - '1 234,56 $US', - ], - 'USD-FR-strip' => [ - 'USD', - 'fr-FR', - 1234.00, - true, - '1 234 $US', - ], - 'USD-FR-no-strip' => [ - 'USD', - 'fr-FR', - 1234.00, - false, - '1 234,00 $US', - ], - 'EUR-US' => [ - 'EUR', - 'en-US', - 1234.56, - true, - '€1,234.56', - ], - 'EUR-US-strip' => [ - 'EUR', - 'en-US', - 1234.00, - true, - '€1,234', - ], - 'EUR-US-no-strip' => [ - 'EUR', - 'en-US', - 1234.00, - false, - '€1,234.00', - ], - 'EUR-FR' => [ - 'EUR', - 'fr-FR', - 1234.56, - true, - '1 234,56 €', - ], - 'EUR-FR-strip' => [ - 'EUR', - 'fr-FR', - 1234.00, - true, - '1 234 €', - ], - 'EUR-FR-no-strip' => [ - 'EUR', - 'fr-FR', - 1234.00, - false, - '1 234,00 €', - ], - ]; - } - - /** - * @param string $currency - * @param string $language - * @param string $expected - * @return void - * @throws CurrencyException - * @throws InvalidConfigException - * @dataProvider formatAsCurrencyNegativeDataProvider - */ - public function testFormatAsCurrencyNegative(string $currency, string $language, string $expected): void - { - $originalLocale = \Craft::$app->getLocale(); - Locale::switchAppLanguage($language); - $amount = -1234.56; - $formattedValue = Currency::formatAsCurrency($amount, $currency); - - self::assertEquals($expected, $formattedValue); - Locale::switchAppLanguage($originalLocale->getLanguageID()); - } - - public function formatAsCurrencyNegativeDataProvider(): array - { - return [ - 'USD-US' => [ - 'USD', - 'en-US', - '-$1,234.56', - ], - 'USD-GB' => [ - 'USD', - 'en-GB', - '-US$1,234.56', - ], - 'USD-FR' => [ - 'USD', - 'fr-FR', - '-1 234,56 $US', - ], - 'EUR-US' => [ - 'EUR', - 'en-US', - '-€1,234.56', - ], - 'EUR-GB' => [ - 'EUR', - 'en-GB', - '-€1,234.56', - ], - 'EUR-FR' => [ - 'EUR', - 'fr-FR', - '-1 234,56 €', - ], - 'CHF-DE-CH' => [ - 'CHF', - 'de-CH', - 'CHF-1’234.56', - ], - ]; - } -} diff --git a/tests/unit/helpers/DebugPanelHelperTest.php b/tests/unit/helpers/DebugPanelHelperTest.php deleted file mode 100644 index ad839a7d34..0000000000 --- a/tests/unit/helpers/DebugPanelHelperTest.php +++ /dev/null @@ -1,175 +0,0 @@ - - * @since 4.0 - */ -class DebugPanelHelperTest extends Unit -{ - /** - * @var UnitTester - */ - protected $tester; - - /** - * @param array $models - * @param array|null $names - * @param array|null $prepend - * @param array $expected - * @throws \yii\base\InvalidConfigException - * @dataProvider prependOrAppendModelTabDataProvider - */ - public function testPrependOrAppendModelTab(array $models, ?array $names, ?array $prepend, array $expected): void - { - Craft::$app->getConfig()->getGeneral()->devMode = true; - Craft::$app->getUser()->setIdentity( - Craft::$app->getUsers()->getUserById('1') - ); - - $usersServices = $this->make(Users::class, [ - 'getUserPreferences' => fn($userId) => [ - 'enableDebugToolbarForSite' => true, - 'enableDebugToolbarForCp' => true, - ], - ]); - Craft::$app->set('users', $usersServices); - - foreach ($models as $key => $model) { - DebugPanel::prependOrAppendModelTab($model, $names[$key], $prepend[$key]); - } - - $event = new CommerceDebugPanelDataEvent(['nav' => [], 'content' => []]); - $commercePanel = new CommercePanel(); - $commercePanel->trigger(CommercePanel::EVENT_AFTER_DATA_PREPARE, $event); - - foreach ($models as $key => $model) { - self::assertIsArray($event->nav); - self::assertIsArray($event->content); - self::assertContains($expected[$key]['name'], $event->nav); - self::assertStringContainsString($expected[$key]['content'], $event->content[$expected[$key]['position']]); - } - } - - /** - * @return array - * @throws \yii\base\InvalidConfigException - */ - public function prependOrAppendModelTabDataProvider(): array - { - $discount = new Discount(); - $discount->id = 1; - - $sale = new Sale(); - $sale->id = 123; - return [ - [ - [ - $discount, - ], - [ - null, - ], - [ - true, - ], - [ - [ - 'name' => 'Discount (ID: 1)', - 'content' => 'id1', - 'position' => 0, - ], - ], - ], - [ - [ - $sale, - $discount, - ], - [ - 'Test Custom Name', - null, - ], - [ - false, - true, - ], - [ - [ - 'name' => 'Test Custom Name', - 'content' => 'id123', - 'position' => 1, - ], - [ - 'name' => 'Discount (ID: 1)', - 'content' => 'id1', - 'position' => 0, - ], - ], - ], - ]; - } - - /** - * @param string $attr - * @param string|null $label - * @param string $expected - * @return void - * @dataProvider renderModelAttributeRowDataProvider - */ - public function testRenderModelAttributeRow(string $attr, mixed $value, ?string $label = null, string $expected = ''): void - { - self::assertEquals($expected, DebugPanel::renderModelAttributeRow($attr, $value, $label)); - } - - public function renderModelAttributeRowDataProvider(): array - { - $discountVarDump = VarDumper::dumpAsString(new Discount()); - return [ - [ - 'stringAttr', - 'Test string', - null, - 'stringAttrTest string', - ], - [ - 'stringAttr', - 'Custom label', - 'Customize the label', - 'Customize the labelCustom label', - ], - [ - 'modelAttr', - $discountVarDump, - null, - 'modelAttr' . $discountVarDump . '', - ], - [ - 'attrHtml', - 'Extra & useful HTML', - null, - 'attrHtml' . Html::encode('Extra & useful HTML') . '', - ], - ]; - } -} diff --git a/tests/unit/helpers/LocaleHelperTest.php b/tests/unit/helpers/LocaleHelperTest.php deleted file mode 100644 index 9d1c4554ad..0000000000 --- a/tests/unit/helpers/LocaleHelperTest.php +++ /dev/null @@ -1,98 +0,0 @@ - - * @since 3.2.14 - */ -class LocaleHelperTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - public function testPdfGetRenderLanguageException(): void - { - $this->tester->expectThrowable(InvalidArgumentException::class, function() { - $pdf = new Pdf(); - $pdf->language = PdfRecord::LOCALE_ORDER_LANGUAGE; - $pdf->getRenderLanguage(); - }); - } - - public function testPdfGetOrderLanguage(): void - { - $order = new Order(); - $order->orderLanguage = 'nl'; - - $pdf = new Pdf(); - $pdf->language = PdfRecord::LOCALE_ORDER_LANGUAGE; - - $language = $pdf->getRenderLanguage($order); - - self::assertEquals('nl', $language); - - $pdf = new Pdf(); - $pdf->language = 'ph'; - - $language = $pdf->getRenderLanguage($order); - - self::assertEquals('ph', $language); - } - - public function testEmailGetRenderLanguageException(): void - { - $this->tester->expectThrowable(InvalidArgumentException::class, function() { - $email = new Email(); - $email->language = EmailRecord::LOCALE_ORDER_LANGUAGE; - $email->getRenderLanguage(); - }); - } - - public function testEmailGetOrderLanguage(): void - { - $order = new Order(); - $order->orderLanguage = 'nl'; - - $email = new Email(); - $email->language = EmailRecord::LOCALE_ORDER_LANGUAGE; - - $language = $email->getRenderLanguage($order); - - self::assertEquals('nl', $language); - - $pdf = new Email(); - $email->language = 'ph'; - - $language = $email->getRenderLanguage($order); - - self::assertEquals('ph', $language); - } - - public function testSwitchLanguage(): void - { - Locale::switchAppLanguage('nl'); - - self::assertEquals('nl', Craft::$app->language); - } -} diff --git a/tests/unit/helpers/LocalizationHelperTest.php b/tests/unit/helpers/LocalizationHelperTest.php deleted file mode 100644 index 8f12d678af..0000000000 --- a/tests/unit/helpers/LocalizationHelperTest.php +++ /dev/null @@ -1,62 +0,0 @@ - - * @since 3.2.14 - */ -class LocalizationHelperTest extends Unit -{ - /** - * @var UnitTester - */ - protected $tester; - - /** - * @param float $expected - * @param int|float|string|null $number - * @dataProvider normalizePercentageDataProvider - */ - public function testNormalizePercentage(float $expected, $number): void - { - self::assertEquals($expected, Localization::normalizePercentage($number)); - } - - /** - * @return array - */ - public function normalizePercentageDataProvider(): array - { - $pct = Craft::$app->getLocale()->getNumberSymbol(Locale::SYMBOL_PERCENT); - return [ - [0.0, null], - [0.0, ''], - [0.0, $pct], - [0.0, " $pct "], - [0.0, 0], - [0.5, 0.5], - [50.0, 50], - [1.0, 1], - [0.0, '0'], - [0.01, '1'], - [0.5, '50'], - [0.0, ' 0.0 '], - [0.005, " .5 $pct "], - [0.005, " $pct 0.5 "], - ]; - } -} diff --git a/tests/unit/models/DiscountTest.php b/tests/unit/models/DiscountTest.php deleted file mode 100644 index 047f4b994a..0000000000 --- a/tests/unit/models/DiscountTest.php +++ /dev/null @@ -1,203 +0,0 @@ - - * @since 4.0 - */ -class DiscountTest extends Unit -{ - /** - * @dataProvider getPercentDiscountAsPercentDataProvider - */ - public function testGetPercentDiscountAsPercent($percentDiscount, $expected): void - { - $discount = new Discount(); - $discount->percentDiscount = $percentDiscount; - - self::assertSame($expected, $discount->getPercentDiscountAsPercent()); - } - - /** - * @return array - */ - public function getPercentDiscountAsPercentDataProvider(): array - { - return [ - ['-0.1000', '10%'], - [0, '0%'], - [-0.1, '10%'], - [-0.15, '15%'], - [-0.105, '10.5%'], - [-0.10504, '10.504%'], - ['-0.1050400', '10.504%'], - ]; - } - - /** - * @param ElementConditionInterface|array|string $condition - * @param bool $expected - * @return void - * @throws InvalidConfigException - * @since 4.3.0 - * @dataProvider conditionBuilderDataProvider - */ - public function testHasOrderCondition(ElementConditionInterface|array|string $condition, bool $expected): void - { - if ($condition === 'class' || $condition === 'rules') { - /** @var DiscountOrderCondition $condition */ - $conditionBuilder = \Craft::$app->getConditions()->createCondition([ - 'class' => DiscountOrderCondition::class, - ]); - $conditionBuilder->storeId = 1; - - if ($condition === 'rules') { - $rule = \Craft::$app->getConditions()->createConditionRule([ - 'type' => IdConditionRule::class, - 'value' => 1, - ]); - $conditionBuilder->addConditionRule($rule); - } - - $condition = $conditionBuilder; - } - - /** @var Discount $discount */ - $discount = \Craft::createObject([ - 'class' => Discount::class, - 'orderCondition' => $condition, - ]); - - self::assertSame($expected, $discount->hasOrderCondition()); - } - - /** - * @param ElementConditionInterface|array|string $condition - * @param bool $expected - * @return void - * @throws InvalidConfigException - * @since 4.3.0 - * @dataProvider conditionBuilderDataProvider - */ - public function testHasCustomerCondition(ElementConditionInterface|array|string $condition, bool $expected): void - { - if ($condition === 'class' || $condition === 'rules') { - /** @var DiscountCustomerCondition $condition */ - $conditionBuilder = \Craft::$app->getConditions()->createCondition(DiscountCustomerCondition::class); - - if ($condition === 'rules') { - $rule = \Craft::$app->getConditions()->createConditionRule([ - 'type' => IdConditionRule::class, - 'value' => 1, - ]); - $conditionBuilder->addConditionRule($rule); - } - - $condition = $conditionBuilder; - } - - /** @var Discount $discount */ - $discount = \Craft::createObject([ - 'class' => Discount::class, - 'customerCondition' => $condition, - ]); - - self::assertSame($expected, $discount->hasCustomerCondition()); - } - - - /** - * @param ElementConditionInterface|array|string $condition - * @param bool $expected - * @return void - * @throws InvalidConfigException - * @since 4.3.0 - * @dataProvider conditionBuilderDataProvider - */ - public function testHasBillingAddressCondition(ElementConditionInterface|array|string $condition, bool $expected): void - { - if ($condition === 'class' || $condition === 'rules') { - /** @var DiscountAddressCondition $condition */ - $conditionBuilder = \Craft::$app->getConditions()->createCondition(DiscountAddressCondition::class); - - if ($condition === 'rules') { - $rule = \Craft::$app->getConditions()->createConditionRule([ - 'type' => IdConditionRule::class, - 'value' => 1, - ]); - $conditionBuilder->addConditionRule($rule); - } - - $condition = $conditionBuilder; - } - - /** @var Discount $discount */ - $discount = \Craft::createObject([ - 'class' => Discount::class, - 'billingAddressCondition' => $condition, - ]); - - self::assertSame($expected, $discount->hasBillingAddressCondition()); - } - - /** - * @param ElementConditionInterface|array|string $condition - * @param bool $expected - * @return void - * @throws InvalidConfigException - * @since 4.3.0 - * @dataProvider conditionBuilderDataProvider - */ - public function testHasShippingAddressCondition(ElementConditionInterface|array|string $condition, bool $expected): void - { - if ($condition === 'class' || $condition === 'rules') { - /** @var DiscountAddressCondition $condition */ - $conditionBuilder = \Craft::$app->getConditions()->createCondition(DiscountAddressCondition::class); - - if ($condition === 'rules') { - $rule = \Craft::$app->getConditions()->createConditionRule([ - 'type' => IdConditionRule::class, - 'value' => 1, - ]); - $conditionBuilder->addConditionRule($rule); - } - - $condition = $conditionBuilder; - } - - /** @var Discount $discount */ - $discount = \Craft::createObject([ - 'class' => Discount::class, - 'shippingAddressCondition' => $condition, - ]); - - self::assertSame($expected, $discount->hasShippingAddressCondition()); - } - - public function conditionBuilderDataProvider(): array - { - return [ - 'blank-string' => ['', false], - 'empty-array' => [[], false], - 'no-rules' => ['class', false], - 'rules' => ['rules', true], - ]; - } -} diff --git a/tests/unit/models/LineItemTest.php b/tests/unit/models/LineItemTest.php deleted file mode 100644 index 825a2a28aa..0000000000 --- a/tests/unit/models/LineItemTest.php +++ /dev/null @@ -1,265 +0,0 @@ - - * @since 3.1.4 - */ -class LineItemTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - 'sales' => [ - 'class' => SalesFixture::class, - ], - ]; - } - - /** - * - */ - public function testPriceRounding(): void - { - $lineItem = new LineItem(); - $lineItem->setPrice(1.239); - $lineItem->setPromotionalPrice(1.114); - $lineItem->qty = 2; - - self::assertSame(1.24, $lineItem->getPrice()); - self::assertSame(1.11, $lineItem->getPromotionalPrice()); - self::assertSame(1.11, $lineItem->getSalePrice()); - self::assertSame(2.22, $lineItem->getSubtotal()); - } - - /** - * - */ - public function testPopulateFromPurchasable(): void - { - $purchasable = new Purchasable(); - $lineItem = new LineItem(); - $lineItem->populateFromPurchasable($purchasable); - - self::assertSame(25.10, $lineItem->price); - self::assertSame(25.10, $lineItem->salePrice); - self::assertSame(0.0, $lineItem->getPromotionalAmount()); - self::assertSame('commerce_testing_unique_sku', $lineItem->sku); - self::assertFalse($lineItem->getOnPromotion()); - } - - /** - * - */ - public function testAppliedSale(): void - { - $variant = Variant::find()->sku('rad-hood')->one(); - $lineItem = new LineItem(); - $lineItem->populateFromPurchasable($variant); - - self::assertSame(123.99, round($lineItem->price, 2)); - self::assertSame(111.59, round($lineItem->salePrice, 2)); - self::assertSame(12.40, round($lineItem->getPromotionalAmount(), 2)); - self::assertTrue($lineItem->getOnPromotion()); - } - - /** - * - */ - public function testSetOptions(): void - { - $options = [ - 'foo' => 'bar', - 'numFoo' => 999, - 'emoji' => '❌', - ]; - $jsonOptions = Json::encode($options); - $lineItem = new LineItem(); - - $output = [ - 'foo' => 'bar', - 'numFoo' => 999, - 'emoji' => ':x:', - ]; - - // @TODO Update this assertion when emoji handling in LineItem::setOptions() is refactored #COM-46 - $lineItem->setOptions($options); - if (Craft::$app->getDb()->getSupportsMb4()) { - self::assertSame($options, $lineItem->getOptions()); - } else { - self::assertSame($output, $lineItem->getOptions()); - } - - $lineItem->setOptions($jsonOptions); - if (Craft::$app->getDb()->getSupportsMb4()) { - self::assertSame($options, $lineItem->getOptions()); - } else { - self::assertSame($output, $lineItem->getOptions()); - } - } - - /** - * - */ - public function testConsistentOptionsSignatures(): void - { - $options = ['Larry' => 'David']; - $lineItem1 = new LineItem(); - $lineItem2 = new LineItem(); - - $lineItem1->setOptions($options); - $lineItem2->setOptions($options); - - self::assertSame($lineItem1->getOptionsSignature(), $lineItem2->getOptionsSignature()); - } - - /** - * - */ - public function testUniqueOptionSignatures(): void - { - $lineItem = new LineItem(); - $lineItem->setOptions(['foo' => 1]); - $signature = $lineItem->getOptionsSignature(); - - $lineItem->setOptions(['foo' => 2]); - - self::assertNotSame($signature, $lineItem->getOptionsSignature()); - } - - /** - * @return void - * @throws SiteNotFoundException - * @throws InvalidConfigException - * @since 5.1.0 - */ - public function testIsPromotableProperty(): void - { - $variant = Variant::find()->sku('hct-blue')->one(); - $lineItem = new LineItem(); - $lineItem->populateFromPurchasable($variant); - - // Manually set the property the make sure it doesn't do anything when it is a purchasable line item - $lineItem->setIsPromotable(false); - - self::assertTrue($lineItem->getIsPromotable()); - } - - /** - * @return void - * @throws SiteNotFoundException - * @throws InvalidConfigException - * @since 5.1.0 - */ - public function testHasFreeShippingProperty(): void - { - $variant = Variant::find()->sku('hct-blue')->one(); - $lineItem = new LineItem(); - $lineItem->populate($variant); - - // Manually set the property the make sure it doesn't do anything when it is a purchasable line item - $lineItem->setHasFreeShipping(true); - - self::assertFalse($lineItem->getHasFreeShipping()); - } - - /** - * @return void - * @since 5.1.0 - */ - public function testCustomLineItem(): void - { - $lineItem = new LineItem(); - $lineItem->type = LineItemType::Custom; - $lineItem->description = 'Custom'; - $lineItem->setSku('custom-sku'); - $lineItem->setPrice(10.00); - $lineItem->qty = 2; - $lineItem->setIsPromotable(false); - $lineItem->setHasFreeShipping(true); - - $order = new Order(); - $order->number = Plugin::getInstance()->getCarts()->generateCartNumber(); - - $order->setLineItems([$lineItem]); - - self::assertEquals(20.00, $order->getTotal()); - } - - /** - * @return void - * @since 5.1.0 - */ - public function testCustomLineItemToArrayDoesNotThrow(): void - { - $lineItem = new LineItem(); - $lineItem->type = LineItemType::Custom; - $lineItem->description = 'Custom'; - $lineItem->setSku('custom-sku'); - $lineItem->setPrice(10.00); - $lineItem->qty = 2; - - $order = new Order(); - $order->number = Plugin::getInstance()->getCarts()->generateCartNumber(); - $order->setLineItems([$lineItem]); - - self::assertNotContains('purchasable', $lineItem->extraFields()); - - $data = $lineItem->toArray([], ['*']); - self::assertIsArray($data); - - $data = $lineItem->toArray([], ['purchasable']); - self::assertIsArray($data); - self::assertArrayNotHasKey('purchasable', $data); - } - - /** - * @return void - * @since 5.1.0 - */ - public function testPurchasableLineItemToArrayIncludesPurchasable(): void - { - $variant = Variant::find()->sku('rad-hood')->one(); - $lineItem = new LineItem(); - $lineItem->populateFromPurchasable($variant); - $lineItem->qty = 1; - - $order = new Order(); - $order->number = Plugin::getInstance()->getCarts()->generateCartNumber(); - $order->setLineItems([$lineItem]); - - self::assertContains('purchasable', $lineItem->extraFields()); - - $data = $lineItem->toArray([], ['purchasable']); - self::assertArrayHasKey('purchasable', $data); - self::assertNotNull($data['purchasable']); - } -} diff --git a/tests/unit/models/SaleTest.php b/tests/unit/models/SaleTest.php deleted file mode 100644 index 054964c4bc..0000000000 --- a/tests/unit/models/SaleTest.php +++ /dev/null @@ -1,100 +0,0 @@ - - * @since 3.1.4 - */ -class SaleTest extends Unit -{ - /** - * - */ - public function testSetCategoryIds(): void - { - $sale = new Sale(); - $ids = [1, 2, 3, 4, 1]; - - self::assertSame([], $sale->getCategoryIds(), 'No category IDs returns blank array'); - - $sale->setCategoryIds($ids); - self::assertSame([1, 2, 3, 4], $sale->getCategoryIds()); - } - - /** - * - */ - public function testSetPurchasableIds(): void - { - $sale = new Sale(); - $ids = [1, 2, 3, 4, 1]; - - self::assertSame([], $sale->getPurchasableIds(), 'No purchasable IDs returns blank array'); - - $sale->setPurchasableIds($ids); - self::assertSame([1, 2, 3, 4], $sale->getPurchasableIds()); - } - - /** - * - */ - public function testSetUserGroupIds(): void - { - $sale = new Sale(); - $ids = [1, 2, 3, 4, 1]; - - self::assertSame([], $sale->getUserGroupIds(), 'No user group IDs returns blank array'); - - $sale->setUserGroupIds($ids); - self::assertSame([1, 2, 3, 4], $sale->getUserGroupIds()); - } - - /** - * @dataProvider getApplyAMountAsPercentDataProvider - */ - public function testGetApplyAmountAsPercent($applyAmount, $expected): void - { - $sale = new Sale(); - $sale->applyAmount = $applyAmount; - - self::assertSame($expected, $sale->getApplyAmountAsPercent()); - } - - /** - * - */ - public function testGetApplyAmountAsFlat(): void - { - $sale = new Sale(); - $sale->applyAmount = '-0.1500'; - - self::assertSame('0.15', $sale->getApplyAmountAsFlat()); - } - - /** - * @return array - */ - public function getApplyAMountAsPercentDataProvider(): array - { - return [ - ['-0.1000', '10%'], - [0, '0%'], - [-0.1, '10%'], - [-0.15, '15%'], - [-0.105, '10.5%'], - [-0.10504, '10.504%'], - ['-0.1050400', '10.504%'], - ]; - } -} diff --git a/tests/unit/models/StoreTest.php b/tests/unit/models/StoreTest.php deleted file mode 100644 index ee0ae5c04b..0000000000 --- a/tests/unit/models/StoreTest.php +++ /dev/null @@ -1,467 +0,0 @@ - - * @since 4.0 - */ -class StoreTest extends Unit -{ - /** - * @param bool $expectException - * @param array $expected - * @return void - * @dataProvider setCountriesDataProvider - */ - public function testSetCountries(mixed $countries, bool $expectException, array $expected): void - { - $store = new StoreSettings(); - if ($expectException) { - $this->expectException(InvalidConfigException::class); - } - - $store->setCountries($countries); - - self::assertEquals($expected, $store->getCountries()); - } - - /** - * @param array $countries - * @param array $expected - * @return void - * @throws InvalidConfigException - * @dataProvider getCountriesDataProvider - */ - public function testGetCountriesList(array $countries, array $expected): void - { - $store = new StoreSettings(); - $store->setCountries($countries); - - self::assertEquals($expected, $store->getCountriesList()); - } - - /** - * @param array $countries - * @param array $expected - * @return void - * @throws InvalidConfigException - * @dataProvider getAdministrativeAreasListByCountryCodeDataProvider - */ - public function testGetAdministrativeAreasListByCountryCode(array $countries, array $expected): void - { - $store = new StoreSettings(); - $store->setCountries($countries); - - self::assertEquals($expected, $store->getAdministrativeAreasListByCountryCode()); - } - - /** - * @return array - */ - public function getAdministrativeAreasListByCountryCodeDataProvider(): array - { - return [ - [['US', 'GB'], [ - 'US' => [ - 'AL' => 'Alabama', - 'AK' => 'Alaska', - 'AS' => 'American Samoa', - 'AZ' => 'Arizona', - 'AR' => 'Arkansas', - 'AA' => 'Armed Forces (AA)', - 'AE' => 'Armed Forces (AE)', - 'AP' => 'Armed Forces (AP)', - 'CA' => 'California', - 'CO' => 'Colorado', - 'CT' => 'Connecticut', - 'DE' => 'Delaware', - 'DC' => 'District of Columbia', - 'FL' => 'Florida', - 'GA' => 'Georgia', - 'GU' => 'Guam', - 'HI' => 'Hawaii', - 'ID' => 'Idaho', - 'IL' => 'Illinois', - 'IN' => 'Indiana', - 'IA' => 'Iowa', - 'KS' => 'Kansas', - 'KY' => 'Kentucky', - 'LA' => 'Louisiana', - 'ME' => 'Maine', - 'MH' => 'Marshall Islands', - 'MD' => 'Maryland', - 'MA' => 'Massachusetts', - 'MI' => 'Michigan', - 'FM' => 'Micronesia', - 'MN' => 'Minnesota', - 'MS' => 'Mississippi', - 'MO' => 'Missouri', - 'MT' => 'Montana', - 'NE' => 'Nebraska', - 'NV' => 'Nevada', - 'NH' => 'New Hampshire', - 'NJ' => 'New Jersey', - 'NM' => 'New Mexico', - 'NY' => 'New York', - 'NC' => 'North Carolina', - 'ND' => 'North Dakota', - 'MP' => 'Northern Mariana Islands', - 'OH' => 'Ohio', - 'OK' => 'Oklahoma', - 'OR' => 'Oregon', - 'PW' => 'Palau', - 'PA' => 'Pennsylvania', - 'PR' => 'Puerto Rico', - 'RI' => 'Rhode Island', - 'SC' => 'South Carolina', - 'SD' => 'South Dakota', - 'TN' => 'Tennessee', - 'TX' => 'Texas', - 'UT' => 'Utah', - 'VT' => 'Vermont', - 'VI' => 'Virgin Islands', - 'VA' => 'Virginia', - 'WA' => 'Washington', - 'WV' => 'West Virginia', - 'WI' => 'Wisconsin', - 'WY' => 'Wyoming', - ], - 'GB' => [ - "Antrim and Newtownabbey" => "Antrim and Newtownabbey", - "Ards and North Down" => "Ards and North Down", - "Armagh City, Banbridge and Craigavon" => "Armagh City, Banbridge and Craigavon", - "Barking and Dagenham" => "Barking and Dagenham", - "Barnet" => "Barnet", - "Barnsley" => "Barnsley", - "Bath and North East Somerset" => "Bath and North East Somerset", - "Bedford" => "Bedford", - "Belfast City" => "Belfast City", - "Bexley" => "Bexley", - "Birmingham" => "Birmingham", - "Blackburn with Darwen" => "Blackburn with Darwen", - "Blackpool" => "Blackpool", - "Blaenau Gwent" => "Blaenau Gwent", - "Bolton" => "Bolton", - "Bournemouth, Christchurch and Poole" => "Bournemouth, Christchurch and Poole", - "Bracknell Forest" => "Bracknell Forest", - "Bradford" => "Bradford", - "Brent" => "Brent", - "Bridgend" => "Bridgend", - "Brighton and Hove" => "Brighton and Hove", - "Bristol, City of" => "Bristol, City of", - "Bromley" => "Bromley", - "Buckinghamshire" => "Buckinghamshire", - "Bury" => "Bury", - "Caerphilly" => "Caerphilly", - "Calderdale" => "Calderdale", - "Cambridgeshire" => "Cambridgeshire", - "Camden" => "Camden", - "Cardiff" => "Cardiff", - "Carmarthenshire" => "Carmarthenshire", - "Causeway Coast and Glens" => "Causeway Coast and Glens", - "Central Bedfordshire" => "Central Bedfordshire", - "Ceredigion" => "Ceredigion", - "Cheshire East" => "Cheshire East", - "Cheshire West and Chester" => "Cheshire West and Chester", - "Clackmannanshire" => "Clackmannanshire", - "Conwy" => "Conwy", - "Cornwall" => "Cornwall", - "Coventry" => "Coventry", - "Croydon" => "Croydon", - "Cumbria" => "Cumbria", - "Darlington" => "Darlington", - "Denbighshire" => "Denbighshire", - "Derby" => "Derby", - "Derbyshire" => "Derbyshire", - "Derry and Strabane" => "Derry and Strabane", - "Devon" => "Devon", - "Doncaster" => "Doncaster", - "Dorset" => "Dorset", - "Dudley" => "Dudley", - "Dumfries and Galloway" => "Dumfries and Galloway", - "Dundee City" => "Dundee City", - "Durham, County" => "Durham, County", - "Ealing" => "Ealing", - "East Ayrshire" => "East Ayrshire", - "East Dunbartonshire" => "East Dunbartonshire", - "East Lothian" => "East Lothian", - "East Renfrewshire" => "East Renfrewshire", - "East Riding of Yorkshire" => "East Riding of Yorkshire", - "East Sussex" => "East Sussex", - "Edinburgh, City of" => "Edinburgh, City of", - "Eilean Siar" => "Eilean Siar", - "Enfield" => "Enfield", - "Essex" => "Essex", - "Falkirk" => "Falkirk", - "Fermanagh and Omagh" => "Fermanagh and Omagh", - "Fife" => "Fife", - "Flintshire" => "Flintshire", - "Gateshead" => "Gateshead", - "Glasgow City" => "Glasgow City", - "Gloucestershire" => "Gloucestershire", - "Greenwich" => "Greenwich", - "Gwynedd" => "Gwynedd", - "Hackney" => "Hackney", - "Halton" => "Halton", - "Hammersmith and Fulham" => "Hammersmith and Fulham", - "Hampshire" => "Hampshire", - "Haringey" => "Haringey", - "Harrow" => "Harrow", - "Hartlepool" => "Hartlepool", - "Havering" => "Havering", - "Herefordshire" => "Herefordshire", - "Hertfordshire" => "Hertfordshire", - "Highland" => "Highland", - "Hillingdon" => "Hillingdon", - "Hounslow" => "Hounslow", - "Inverclyde" => "Inverclyde", - "Isle of Anglesey" => "Isle of Anglesey", - "Isle of Wight" => "Isle of Wight", - "Isles of Scilly" => "Isles of Scilly", - "Islington" => "Islington", - "Kensington and Chelsea" => "Kensington and Chelsea", - "Kent" => "Kent", - "Kingston upon Hull" => "Kingston upon Hull", - "Kingston upon Thames" => "Kingston upon Thames", - "Kirklees" => "Kirklees", - "Knowsley" => "Knowsley", - "Lambeth" => "Lambeth", - "Lancashire" => "Lancashire", - "Leeds" => "Leeds", - "Leicester" => "Leicester", - "Leicestershire" => "Leicestershire", - "Lewisham" => "Lewisham", - "Lincolnshire" => "Lincolnshire", - "Lisburn and Castlereagh" => "Lisburn and Castlereagh", - "Liverpool" => "Liverpool", - "London, City of" => "London, City of", - "Luton" => "Luton", - "Manchester" => "Manchester", - "Medway" => "Medway", - "Merthyr Tydfil" => "Merthyr Tydfil", - "Merton" => "Merton", - "Mid and East Antrim" => "Mid and East Antrim", - "Mid-Ulster" => "Mid-Ulster", - "Middlesbrough" => "Middlesbrough", - "Midlothian" => "Midlothian", - "Milton Keynes" => "Milton Keynes", - "Monmouthshire" => "Monmouthshire", - "Moray" => "Moray", - "Neath Port Talbot" => "Neath Port Talbot", - "Newcastle upon Tyne" => "Newcastle upon Tyne", - "Newham" => "Newham", - "Newport" => "Newport", - "Newry, Mourne and Down" => "Newry, Mourne and Down", - "Norfolk" => "Norfolk", - "North Ayrshire" => "North Ayrshire", - "North East Lincolnshire" => "North East Lincolnshire", - "North Lanarkshire" => "North Lanarkshire", - "North Lincolnshire" => "North Lincolnshire", - "North Northamptonshire" => "North Northamptonshire", - "North Somerset" => "North Somerset", - "North Tyneside" => "North Tyneside", - "North Yorkshire" => "North Yorkshire", - "Northumberland" => "Northumberland", - "Nottingham" => "Nottingham", - "Nottinghamshire" => "Nottinghamshire", - "Oldham" => "Oldham", - "Orkney Islands" => "Orkney Islands", - "Oxfordshire" => "Oxfordshire", - "Pembrokeshire" => "Pembrokeshire", - "Perth and Kinross" => "Perth and Kinross", - "Peterborough" => "Peterborough", - "Plymouth" => "Plymouth", - "Portsmouth" => "Portsmouth", - "Powys" => "Powys", - "Reading" => "Reading", - "Redbridge" => "Redbridge", - "Redcar and Cleveland" => "Redcar and Cleveland", - "Renfrewshire" => "Renfrewshire", - "Rhondda Cynon Taff" => "Rhondda Cynon Taff", - "Richmond upon Thames" => "Richmond upon Thames", - "Rochdale" => "Rochdale", - "Rotherham" => "Rotherham", - "Rutland" => "Rutland", - "Salford" => "Salford", - "Sandwell" => "Sandwell", - "Scottish Borders" => "Scottish Borders", - "Sefton" => "Sefton", - "Sheffield" => "Sheffield", - "Shetland Islands" => "Shetland Islands", - "Shropshire" => "Shropshire", - "Slough" => "Slough", - "Solihull" => "Solihull", - "Somerset" => "Somerset", - "South Ayrshire" => "South Ayrshire", - "South Gloucestershire" => "South Gloucestershire", - "South Lanarkshire" => "South Lanarkshire", - "South Tyneside" => "South Tyneside", - "Southampton" => "Southampton", - "Southend-on-Sea" => "Southend-on-Sea", - "Southwark" => "Southwark", - "St. Helens" => "St. Helens", - "Staffordshire" => "Staffordshire", - "Stirling" => "Stirling", - "Stockport" => "Stockport", - "Stockton-on-Tees" => "Stockton-on-Tees", - "Stoke-on-Trent" => "Stoke-on-Trent", - "Suffolk" => "Suffolk", - "Sunderland" => "Sunderland", - "Surrey" => "Surrey", - "Sutton" => "Sutton", - "Swansea" => "Swansea", - "Swindon" => "Swindon", - "Tameside" => "Tameside", - "Telford and Wrekin" => "Telford and Wrekin", - "Thurrock" => "Thurrock", - "Torbay" => "Torbay", - "Torfaen" => "Torfaen", - "Tower Hamlets" => "Tower Hamlets", - "Trafford" => "Trafford", - "Vale of Glamorgan, The" => "Vale of Glamorgan, The", - "Wakefield" => "Wakefield", - "Walsall" => "Walsall", - "Waltham Forest" => "Waltham Forest", - "Wandsworth" => "Wandsworth", - "Warrington" => "Warrington", - "Warwickshire" => "Warwickshire", - "West Berkshire" => "West Berkshire", - "West Dunbartonshire" => "West Dunbartonshire", - "West Lothian" => "West Lothian", - "West Northamptonshire" => "West Northamptonshire", - "West Sussex" => "West Sussex", - "Westminster" => "Westminster", - "Wigan" => "Wigan", - "Wiltshire" => "Wiltshire", - "Windsor and Maidenhead" => "Windsor and Maidenhead", - "Wirral" => "Wirral", - "Wokingham" => "Wokingham", - "Wolverhampton" => "Wolverhampton", - "Worcestershire" => "Worcestershire", - "Wrexham" => "Wrexham", - "York" => "York", - "Aberdeen City" => "Aberdeen City", - "Aberdeenshire" => "Aberdeenshire", - "Angus" => "Angus", - "Argyll and Bute" => "Argyll and Bute", - "Westmorland and Furness" => "Westmorland and Furness", - ], - ]], - [['AU', 'US'], [ - 'AU' => [ - 'ACT' => 'Australian Capital Territory', - 'NSW' => 'New South Wales', - 'NT' => 'Northern Territory', - 'QLD' => 'Queensland', - 'SA' => 'South Australia', - 'TAS' => 'Tasmania', - 'VIC' => 'Victoria', - 'WA' => 'Western Australia', - ], - 'US' => [ - 'AL' => 'Alabama', - 'AK' => 'Alaska', - 'AS' => 'American Samoa', - 'AZ' => 'Arizona', - 'AR' => 'Arkansas', - 'AA' => 'Armed Forces (AA)', - 'AE' => 'Armed Forces (AE)', - 'AP' => 'Armed Forces (AP)', - 'CA' => 'California', - 'CO' => 'Colorado', - 'CT' => 'Connecticut', - 'DE' => 'Delaware', - 'DC' => 'District of Columbia', - 'FL' => 'Florida', - 'GA' => 'Georgia', - 'GU' => 'Guam', - 'HI' => 'Hawaii', - 'ID' => 'Idaho', - 'IL' => 'Illinois', - 'IN' => 'Indiana', - 'IA' => 'Iowa', - 'KS' => 'Kansas', - 'KY' => 'Kentucky', - 'LA' => 'Louisiana', - 'ME' => 'Maine', - 'MH' => 'Marshall Islands', - 'MD' => 'Maryland', - 'MA' => 'Massachusetts', - 'MI' => 'Michigan', - 'FM' => 'Micronesia', - 'MN' => 'Minnesota', - 'MS' => 'Mississippi', - 'MO' => 'Missouri', - 'MT' => 'Montana', - 'NE' => 'Nebraska', - 'NV' => 'Nevada', - 'NH' => 'New Hampshire', - 'NJ' => 'New Jersey', - 'NM' => 'New Mexico', - 'NY' => 'New York', - 'NC' => 'North Carolina', - 'ND' => 'North Dakota', - 'MP' => 'Northern Mariana Islands', - 'OH' => 'Ohio', - 'OK' => 'Oklahoma', - 'OR' => 'Oregon', - 'PW' => 'Palau', - 'PA' => 'Pennsylvania', - 'PR' => 'Puerto Rico', - 'RI' => 'Rhode Island', - 'SC' => 'South Carolina', - 'SD' => 'South Dakota', - 'TN' => 'Tennessee', - 'TX' => 'Texas', - 'UT' => 'Utah', - 'VT' => 'Vermont', - 'VI' => 'Virgin Islands', - 'VA' => 'Virginia', - 'WA' => 'Washington', - 'WV' => 'West Virginia', - 'WI' => 'Wisconsin', - 'WY' => 'Wyoming', - ], - ]], - [[], []], - ]; - } - - /** - * @return array - */ - public function getCountriesDataProvider(): array - { - return [ - [['US', 'GB', 'LV'], ['LV' => 'Latvia', 'GB' => 'United Kingdom', 'US' => 'United States']], - [['US', 'GB'], ['GB' => 'United Kingdom', 'US' => 'United States']], - [['US'], ['US' => 'United States']], - [['XX'], []], - [[], []], - ]; - } - - /** - * @return array[] - */ - public function setCountriesDataProvider(): array - { - return [ - [json_encode(['US', 'CA']), false, ['US', 'CA']], - ['US', true, []], - [['US', 'GB'], false, ['US', 'GB']], - ]; - } -} diff --git a/tests/unit/models/TaxRateTest.php b/tests/unit/models/TaxRateTest.php deleted file mode 100644 index 2908cef942..0000000000 --- a/tests/unit/models/TaxRateTest.php +++ /dev/null @@ -1,47 +0,0 @@ - - * @since 3.4.10.1 - */ -class TaxRateTest extends Unit -{ - /** - * @dataProvider getRateAsPercentDataProvider - */ - public function testGetRateAsPercent($rate, $expected): void - { - $taxRate = new TaxRate(); - $taxRate->rate = $rate; - - self::assertSame($expected, $taxRate->getRateAsPercent()); - } - - /** - * @return array - */ - public function getRateAsPercentDataProvider(): array - { - return [ - ['0.1000', '10%'], - [0, '0%'], - [0.1, '10%'], - [0.15, '15%'], - [0.105, '10.5%'], - [0.10504, '10.504%'], - ['0.1050400', '10.504%'], - ]; - } -} diff --git a/tests/unit/services/CartsTest.php b/tests/unit/services/CartsTest.php deleted file mode 100644 index 907a7d532c..0000000000 --- a/tests/unit/services/CartsTest.php +++ /dev/null @@ -1,407 +0,0 @@ - - * @since 4.2.2 - */ -class CartsTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - public function _fixtures(): array - { - return [ - 'customer' => [ - 'class' => CustomerFixture::class, - ], - 'customerAddresses' => [ - 'class' => CustomerAddressFixture::class, - ], - ]; - } - - /** - * @param string $email - * @param bool $autoSet - * @param bool $hasBillingAddress - * @param bool $hasShippingAddress - * @return void - * @throws \Throwable - * @throws \craft\errors\ElementNotFoundException - * @throws \yii\base\Exception - * @throws \yii\base\InvalidConfigException - * @throws \yii\base\UnknownPropertyException - * @dataProvider getCartDataProvider - */ - public function testGetCartAutoSetAddresses(string $email, bool $autoSet, bool $hasBillingAddress, bool $hasShippingAddress, bool $loggedIn): void - { - $cartNumber = Plugin::getInstance()->getCarts()->generateCartNumber(); - - $store = Plugin::getInstance()->getStores()->getCurrentStore(); - Plugin::getInstance()->set('stores', $this->make(Stores::class, [ - 'getStoreById' => function(int $id) use ($autoSet, $store) { - $store->setAutoSetNewCartAddresses($autoSet); - return $store; - }, - ])); - - Plugin::getInstance()->set('carts', $this->make(Carts::class, [ - 'getSessionCartNumber' => fn() => $cartNumber, - ])); - - $user = Craft::$app->getUsers()->getUserByUsernameOrEmail($email); - if ($loggedIn) { - Craft::$app->getUser()->setIdentity($user); - Craft::$app->getUser()->getIdentity()->password = $user->password; - } - - $newCart = new Order(); - $newCart->setCustomer($user); - $newCart->number = $cartNumber; - Craft::$app->getElements()->saveElement($newCart, false); - - $cart = Plugin::getInstance()->getCarts()->getCart(); - - if ($hasBillingAddress) { - self::assertNotNull($cart->getBillingAddress()); - } else { - self::assertNull($cart->getBillingAddress()); - } - if ($hasShippingAddress) { - self::assertNotNull($cart->getShippingAddress()); - } else { - self::assertNull($cart->getShippingAddress()); - } - - Craft::$app->getElements()->deleteElement($newCart, true); - } - - public function getCartDataProvider(): array - { - return [ - 'inactive-user-no-auto-set-addresses' => ['inactive.user@crafttest.com', false, false, false, false], - 'inactive-user-auto-set-addresses' => ['inactive.user@crafttest.com', true, false, false, false], - 'logged-in-user-no-auto-set-addresses' => ['cred.user@crafttest.com', false, false, false, true], - 'logged-in-user-auto-set-addresses' => ['cred.user@crafttest.com', true, true, true, true], - ]; - } - - /** - * Tests that calling forgetCart() followed by getCart() in the same request returns a new - * cart with a different number — verifying the fix in loadCookie() that respects the `false` - * set by forgetCart() and prevents the cookie from restoring the forgotten cart. - * - * @see https://github.com/craftcms/commerce/issues/4279 - */ - public function testForgetCartPreventsCartRestoration(): void - { - $cartsService = Plugin::getInstance()->getCarts(); - - // First call generates an in-memory cart number and returns a new cart. - $initialCart = $cartsService->getCart(); - $originalNumber = $initialCart->number; - - // forgetCart() sets the private $_cartNumber sentinel to `false`. - $cartsService->forgetCart(); - - // A subsequent getCart() must generate a completely new number and return a fresh cart. - $newCart = $cartsService->getCart(); - - self::assertNotEquals( - $originalNumber, - $newCart->number, - 'After forgetCart(), getCart() should return a cart with a new number.', - ); - } - - /** - * Demonstrates that without the fix, a web request whose cookie still carries the forgotten - * cart number causes getCart() to reuse that number — exactly as the old loadCookie() would - * have behaved before the `$this->_cartNumber === false` guard was introduced. - * - * @see https://github.com/craftcms/commerce/issues/4279 - */ - public function testForgetCartWithRestoredCartNumberReturnsSameNumber(): void - { - $carts = Plugin::getInstance()->getCarts(); - - // Get an initial cart and number. - $initialCart = $carts->getCart(); - $originalNumber = $initialCart->number; - - // Forget the cart — $_cartNumber is now `false`. - $carts->forgetCart(); - - $cookieName = 'test_commerce_cart'; - $carts->cartCookie = ['name' => $cookieName]; - - // Simulate the pre-fix state: set $_cartNumber to `null`. - // In old code the `$this->_cartNumber === false` guard didn't exist, so even after - // forgetCart() wrote `false`, loadCookie() would proceed and silently overwrite it - // with whatever value was in the request cookie. - $reflection = new \ReflectionClass($carts); - $cartNumberProp = $reflection->getProperty('_cartNumber'); - $cartNumberProp->setAccessible(true); - $cartNumberProp->setValue($carts, null); - - $requestCookies = new \yii\web\CookieCollection(); - $requestCookies->add(new \yii\web\Cookie([ - 'name' => $cookieName, - 'value' => $originalNumber, - ])); - - $originalRequest = \Craft::$app->getRequest(); - - // Create a mock request class to return test data - $requestMock = $this->make(Request::class, [ - 'getIsConsoleRequest' => false, - 'getCookies' => $requestCookies, - ]); - - Craft::$app->set('request', $requestMock); - - try { - $restoredCart = $carts->getCart(); - - self::assertEquals( - $originalNumber, - $restoredCart->number, - 'Without the false guard in loadCookie(), the request cookie restores the forgotten cart number.', - ); - } finally { - \Craft::$app->set('request', $originalRequest); - } - } - - public function testGetCartSwitchCustomer(): void - { - $cartNumber = Plugin::getInstance()->getCarts()->generateCartNumber(); - Plugin::getInstance()->set('carts', $this->make(Carts::class, [ - 'getSessionCartNumber' => fn() => $cartNumber, - ])); - - $inactiveUser = $this->tester->grabFixture('customer')->getElement('inactive-user'); - $credUser = $this->tester->grabFixture('customer')->getElement('credentialed-user'); - $originalIdentity = Craft::$app->getUser()->getIdentity(); - Craft::$app->getUser()->setIdentity($credUser); - Craft::$app->getUser()->getIdentity()->password = $credUser->password; - - - $order = new Order(); - $order->number = $cartNumber; - $order->setCustomer($inactiveUser); - - Craft::$app->getElements()->saveElement($order, false); - self::assertEquals($inactiveUser->id, $order->getCustomerId()); - - $cart = Plugin::getInstance()->getCarts()->getCart(); - - // assert customer has changed; - self::assertNotEquals($inactiveUser->id, $cart->getCustomerId()); - self::assertEquals($credUser->id, $cart->getCustomerId()); - self::assertEquals($credUser->email, $cart->getEmail()); - - // Reset data - Craft::$app->getUser()->setIdentity($originalIdentity); - Craft::$app->getElements()->deleteElement($cart, true); - } - - /** - * A credentialed user's cart must not be served to an anonymous visitor when the session - * hasn't been authorized to use it (the default privacy guard). - * - * @see https://github.com/craftcms/commerce/issues/4225 - */ - public function testCredentialedCartForgottenForAnonymousWithoutAuthorization(): void - { - $cartNumber = Plugin::getInstance()->getCarts()->generateCartNumber(); - Plugin::getInstance()->set('carts', $this->make(Carts::class, [ - 'getSessionCartNumber' => fn() => $cartNumber, - ])); - - $credUser = $this->tester->grabFixture('customer')->getElement('credentialed-user'); - $originalIdentity = Craft::$app->getUser()->getIdentity(); - Craft::$app->getUser()->setIdentity(null); - Craft::$app->getSession()->remove('commerce:anonymousCartWithCredentialedCustomer:' . $cartNumber); - - $order = new Order(); - $order->number = $cartNumber; - $order->setCustomer($credUser); - Craft::$app->getElements()->saveElement($order, false); - - try { - $cart = Plugin::getInstance()->getCarts()->getCart(); - - // The credentialed cart should have been forgotten, so a fresh anonymous cart is returned. - self::assertNull($cart->getCustomerId()); - } finally { - Craft::$app->getUser()->setIdentity($originalIdentity); - Craft::$app->getElements()->deleteElement($order, true); - } - } - - /** - * When a session has been authorized to use a cart (e.g. it was loaded via a valid load-cart - * token), an anonymous visitor should be able to retrieve that credentialed user's cart. - * - * @see https://github.com/craftcms/commerce/issues/4225 - */ - public function testAuthorizedCredentialedCartServedToAnonymous(): void - { - $cartNumber = Plugin::getInstance()->getCarts()->generateCartNumber(); - Plugin::getInstance()->set('carts', $this->make(Carts::class, [ - 'getSessionCartNumber' => fn() => $cartNumber, - ])); - - $credUser = $this->tester->grabFixture('customer')->getElement('credentialed-user'); - $originalIdentity = Craft::$app->getUser()->getIdentity(); - Craft::$app->getUser()->setIdentity(null); - - $order = new Order(); - $order->number = $cartNumber; - $order->setCustomer($credUser); - Craft::$app->getElements()->saveElement($order, false); - - // Mirror what CartController::actionLoadCart() does after validating a token. - Craft::$app->getSession()->set('commerce:anonymousCartWithCredentialedCustomer:' . $cartNumber, true); - - try { - $cart = Plugin::getInstance()->getCarts()->getCart(); - - // The cart is served as-is; the anonymous visitor doesn't take ownership. - self::assertSame($cartNumber, $cart->number); - self::assertEquals($credUser->id, $cart->getCustomerId()); - } finally { - Craft::$app->getUser()->setIdentity($originalIdentity); - Craft::$app->getSession()->remove('commerce:anonymousCartWithCredentialedCustomer:' . $cartNumber); - Craft::$app->getElements()->deleteElement($cart, true); - } - } - - /** - * When a logged-in user loads another credentialed user's cart via an authorized session, - * the cart should be acquired to the logged-in user's account. - * - * @see https://github.com/craftcms/commerce/issues/4225 - */ - public function testAuthorizedCredentialedCartAcquiredByLoggedInUser(): void - { - $cartNumber = Plugin::getInstance()->getCarts()->generateCartNumber(); - Plugin::getInstance()->set('carts', $this->make(Carts::class, [ - 'getSessionCartNumber' => fn() => $cartNumber, - ])); - - $credUser = $this->tester->grabFixture('customer')->getElement('credentialed-user'); - $loadingUser = $this->tester->grabFixture('customer')->getElement('customer1'); - $originalIdentity = Craft::$app->getUser()->getIdentity(); - Craft::$app->getUser()->setIdentity($loadingUser); - Craft::$app->getUser()->getIdentity()->password = $loadingUser->password; - - $order = new Order(); - $order->number = $cartNumber; - $order->setCustomer($credUser); - Craft::$app->getElements()->saveElement($order, false); - self::assertEquals($credUser->id, $order->getCustomerId()); - - // Mirror what CartController::actionLoadCart() does after validating a token. - Craft::$app->getSession()->set('commerce:anonymousCartWithCredentialedCustomer:' . $cartNumber, true); - - try { - $cart = Plugin::getInstance()->getCarts()->getCart(); - - // The cart is retained and acquired to the logged-in user. - self::assertSame($cartNumber, $cart->number); - self::assertEquals($loadingUser->id, $cart->getCustomerId()); - self::assertEquals($loadingUser->email, $cart->getEmail()); - } finally { - Craft::$app->getUser()->setIdentity($originalIdentity); - Craft::$app->getSession()->remove('commerce:anonymousCartWithCredentialedCustomer:' . $cartNumber); - Craft::$app->getElements()->deleteElement($cart, true); - } - } - - public function testPeekCartDoesNotStartCartSession(): void - { - $originalCarts = Plugin::getInstance()->getCarts(); - $cartNumber = $originalCarts->generateCartNumber(); - $cookieName = $originalCarts->cartCookie['name']; - - $order = new Order(); - $order->number = $cartNumber; - Craft::$app->getElements()->saveElement($order, false); - - $carts = $this->make(Carts::class, [ - 'setSessionCartNumber' => function() { - self::fail('Peek cart retrieval should not update the cart session.'); - }, - ]); - $carts->cartCookie = ['name' => $cookieName]; - Plugin::getInstance()->set('carts', $carts); - - $requestCookies = new \yii\web\CookieCollection(); - $requestCookies->add(new \yii\web\Cookie([ - 'name' => $cookieName, - 'value' => $cartNumber, - ])); - $originalRequest = Craft::$app->getRequest(); - $requestMock = $this->make(Request::class, [ - 'getCookies' => $requestCookies, - ]); - Craft::$app->set('request', $requestMock); - - try { - $cart = Plugin::getInstance()->getCarts()->peekCart(); - - self::assertNotNull($cart); - self::assertSame($cartNumber, $cart->number); - } finally { - Craft::$app->set('request', $originalRequest); - Craft::$app->getElements()->deleteElement($order, true); - } - } - - public function testPeekCartReturnsNullWithNoCookie(): void - { - $cookieName = Plugin::getInstance()->getCarts()->cartCookie['name']; - - $carts = $this->make(Carts::class); - $carts->cartCookie = ['name' => $cookieName]; - Plugin::getInstance()->set('carts', $carts); - - $originalRequest = Craft::$app->getRequest(); - $requestMock = $this->make(Request::class, [ - 'getCookies' => new \yii\web\CookieCollection(), - ]); - Craft::$app->set('request', $requestMock); - - try { - $cart = Plugin::getInstance()->getCarts()->peekCart(); - self::assertNull($cart); - } finally { - Craft::$app->set('request', $originalRequest); - } - } -} diff --git a/tests/unit/services/CatalogPricingQueueTest.php b/tests/unit/services/CatalogPricingQueueTest.php deleted file mode 100644 index 01aff995c2..0000000000 --- a/tests/unit/services/CatalogPricingQueueTest.php +++ /dev/null @@ -1,470 +0,0 @@ - - * @since 5.7.0 - */ -class CatalogPricingQueueTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - private int $_storeId; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'stores' => [ - 'class' => StoreFixture::class, - ], - ]; - } - - protected function _before(): void - { - parent::_before(); - // Clear the catalog pricing queue table before each test - CatalogPricingQueueRecord::deleteAll(); - $this->_storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - } - - protected function _after(): void - { - parent::_after(); - // Clean up the queue table after each test - CatalogPricingQueueRecord::deleteAll(); - } - - /** - * Test that a single purchasable ID creates a new queue row. - * - * @see https://github.com/craftcms/commerce/issues/4277 - */ - public function testCreateQueueRowForSinglePurchasableId(): void - { - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1], - 'storeId' => $this->_storeId, - ]); - - $rows = CatalogPricingQueueRecord::find()->all(); - self::assertCount(1, $rows); - - /** @var CatalogPricingQueueRecord $row */ - $row = $rows[0]; - self::assertEquals(CatalogPricingQueueRecord::TYPE_PURCHASABLE, $row->type); - self::assertEquals($this->_storeId, $row->storeId); - self::assertEquals([1], $row->getIds()); - self::assertFalse((bool)$row->reserved); - } - - /** - * Test that a single rule ID creates a new queue row. - */ - public function testCreateQueueRowForSingleRuleId(): void - { - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'catalogPricingRuleIds' => [5], - 'storeId' => $this->_storeId, - ]); - - $rows = CatalogPricingQueueRecord::find()->all(); - self::assertCount(1, $rows); - - /** @var CatalogPricingQueueRecord $row */ - $row = $rows[0]; - self::assertEquals(CatalogPricingQueueRecord::TYPE_RULE, $row->type); - self::assertEquals($this->_storeId, $row->storeId); - self::assertEquals([5], $row->getIds()); - self::assertFalse((bool)$row->reserved); - } - - /** - * Test that purchasable and rule IDs create separate queue rows. - */ - public function testPurchasableAndRuleTypesAreSeparated(): void - { - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1, 2], - 'catalogPricingRuleIds' => [5, 6], - 'storeId' => $this->_storeId, - ]); - - /** @var CatalogPricingQueueRecord[] $rows */ - $rows = CatalogPricingQueueRecord::find()->orderBy(['type' => SORT_ASC])->all(); - self::assertCount(2, $rows); - - // First row should be purchasable type - $purchasableRow = $rows[0]; - self::assertEquals(CatalogPricingQueueRecord::TYPE_PURCHASABLE, $purchasableRow->type); - self::assertEquals([1, 2], $purchasableRow->getIds()); - - // Second row should be rule type - $ruleRow = $rows[1]; - self::assertEquals(CatalogPricingQueueRecord::TYPE_RULE, $ruleRow->type); - self::assertEquals([5, 6], $ruleRow->getIds()); - } - - /** - * Test that different stores create separate queue rows for the same type. - */ - public function testDifferentStoresCreateSeparateRows(): void - { - $primaryStore = Plugin::getInstance()->getStores()->getPrimaryStore(); - $ukStore = Plugin::getInstance()->getStores()->getStoreByHandle('ukStore'); - - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1], - 'storeId' => $primaryStore->id, - ]); - - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1], - 'storeId' => $ukStore->id, - ]); - - /** @var CatalogPricingQueueRecord[] $rows */ - $rows = CatalogPricingQueueRecord::find()->orderBy(['storeId' => SORT_ASC])->all(); - self::assertCount(2, $rows); - - self::assertEquals($primaryStore->id, $rows[0]->storeId); - self::assertEquals($ukStore->id, $rows[1]->storeId); - } - - /** - * Test that multiple calls to queue IDs for the same store/type merge into one row. - */ - public function testMultipleQueuesForSameStoreAndTypeMerge(): void - { - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1, 2], - 'storeId' => $this->_storeId, - ]); - - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [3, 4], - 'storeId' => $this->_storeId, - ]); - - $rows = CatalogPricingQueueRecord::find()->all(); - self::assertCount(1, $rows, 'Multiple queue calls should merge into a single row'); - - $row = $rows[0]; - self::assertEquals([1, 2, 3, 4], $row->getIds(), 'IDs should be merged and sorted'); - } - - /** - * Test that duplicate IDs in merged rows are deduplicated. - */ - public function testDuplicateIdsAreDeduplicated(): void - { - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1, 2, 3], - 'storeId' => $this->_storeId, - ]); - - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [2, 3, 4], - 'storeId' => $this->_storeId, - ]); - - $row = CatalogPricingQueueRecord::findOne(['storeId' => $this->_storeId, 'type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE]); - self::assertEquals([1, 2, 3, 4], $row->getIds(), 'Duplicate IDs should be removed and sorted'); - } - - /** - * Test that null storeId (meaning all stores) is properly handled. - */ - public function testNullStoreIdRepresentsAllStores(): void - { - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1], - 'storeId' => null, - ]); - - $row = CatalogPricingQueueRecord::findOne(['type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE]); - self::assertNull($row->storeId, 'storeId should be null to represent all stores'); - self::assertEquals([1], $row->getIds()); - } - - /** - * Test that merging IDs where one side is null expands scope to null (all stores/ids). - */ - public function testMergingWithNullIdsExpandsScope(): void - { - // First queue specific IDs - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1, 2], - 'storeId' => $this->_storeId, - ]); - - // Then queue with null (meaning all), should merge and expand scope - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => null, - 'storeId' => $this->_storeId, - ]); - - $row = CatalogPricingQueueRecord::findOne(['storeId' => $this->_storeId, 'type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE]); - self::assertNull($row->getIds(), 'IDs should be null (broader scope) when merging specific IDs with null'); - } - - /** - * Test that reserved rows are not merged into. - */ - public function testReservedRowsAreNotMergedInto(): void - { - // Create and reserve a row - $record = new CatalogPricingQueueRecord(); - $record->storeId = $this->_storeId; - $record->type = CatalogPricingQueueRecord::TYPE_PURCHASABLE; - $record->setIds([1]); - $record->reserved = true; - $record->save(false); - - // Try to queue more IDs for the same store/type - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [2], - 'storeId' => $this->_storeId, - ]); - - /** @var CatalogPricingQueueRecord[] $rows */ - $rows = CatalogPricingQueueRecord::find() - ->where(['storeId' => $this->_storeId, 'type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE]) - ->orderBy(['reserved' => SORT_DESC]) - ->all(); - - self::assertCount(2, $rows, 'A new row should be created instead of merging into the reserved row'); - - // One should be reserved with ID 1 - $reservedRow = $rows[0]; - self::assertNotNull($reservedRow); - self::assertEquals([1], $reservedRow->getIds()); - - // One should be unreserved with ID 2 - $unreservedRow = $rows[1]; - self::assertNotNull($unreservedRow); - self::assertEquals([2], $unreservedRow->getIds()); - } - - /** - * Test that IDs are sorted numerically in the queue row. - */ - public function testIdsAreSortedNumerically(): void - { - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [100, 5, 50, 1], - 'storeId' => $this->_storeId, - ]); - - $row = CatalogPricingQueueRecord::findOne(['storeId' => $this->_storeId]); - self::assertEquals([1, 5, 50, 100], $row->getIds(), 'IDs should be sorted numerically'); - } - - /** - * Test that zero and negative IDs are filtered out. - */ - public function testZeroAndNegativeIdsAreFiltered(): void - { - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1, 0, -5, 2], - 'storeId' => $this->_storeId, - ]); - - $row = CatalogPricingQueueRecord::findOne(['storeId' => $this->_storeId]); - self::assertEquals([1, 2], $row->getIds(), 'Zero and negative IDs should be filtered out'); - } - - /** - * Test that `areCatalogPricingJobsRunning()` returns true when queue has pending rows. - */ - public function testAreCatalogPricingJobsRunningReturnsTrueWhenPending(): void - { - self::assertFalse(Plugin::getInstance()->getCatalogPricing()->areCatalogPricingJobsRunning(), 'Should be false when queue is empty'); - - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1], - 'storeId' => $this->_storeId, - ]); - - self::assertTrue(Plugin::getInstance()->getCatalogPricing()->areCatalogPricingJobsRunning(), 'Should be true when queue has pending rows'); - } - - /** - * Test that `reserveCatalogPricingQueueRow()` marks a row as reserved. - */ - public function testReserveCatalogPricingQueueRowMarksAsReserved(): void - { - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1], - 'storeId' => $this->_storeId, - ]); - - // All rows should be unreserved initially - self::assertCount(0, CatalogPricingQueueRecord::find()->where(['reserved' => true])->all()); - - $reserved = Plugin::getInstance()->getCatalogPricing()->reserveCatalogPricingQueueRow(); - - self::assertNotNull($reserved, 'Should return a reserved row'); - self::assertTrue((bool)$reserved->reserved); - self::assertEquals([1], $reserved->getIds()); - - // Verify in database - $dbRow = CatalogPricingQueueRecord::findOne($reserved->id); - self::assertTrue((bool)$dbRow->reserved); - } - - /** - * Test that multiple pending rows can be reserved one at a time. - */ - public function testMultiplePendingRowsCanBeReservedInOrder(): void - { - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1], - 'storeId' => $this->_storeId, - ]); - - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [2], - 'storeId' => Plugin::getInstance()->getStores()->getStoreByHandle('ukStore')->id, - ]); - - $first = Plugin::getInstance()->getCatalogPricing()->reserveCatalogPricingQueueRow(); - self::assertNotNull($first); - self::assertEquals([1], $first->getIds()); - - $second = Plugin::getInstance()->getCatalogPricing()->reserveCatalogPricingQueueRow(); - self::assertNotNull($second); - self::assertEquals([2], $second->getIds()); - - $third = Plugin::getInstance()->getCatalogPricing()->reserveCatalogPricingQueueRow(); - self::assertNull($third, 'Should return null when no pending rows remain'); - } - - /** - * Test that `releaseCatalogPricingQueueRowById()` marks a reserved row as unreserved. - */ - public function testReleaseCatalogPricingQueueByIdMarksAsUnreserved(): void - { - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1], - 'storeId' => $this->_storeId, - ]); - - $reserved = Plugin::getInstance()->getCatalogPricing()->reserveCatalogPricingQueueRow(); - self::assertTrue((bool)$reserved->reserved); - - Plugin::getInstance()->getCatalogPricing()->releaseCatalogPricingQueueRowById($reserved->id); - - $released = CatalogPricingQueueRecord::findOne($reserved->id); - self::assertFalse((bool)$released->reserved); - } - - /** - * Test that `deleteCatalogPricingQueueRowById()` removes a row from the queue. - */ - public function testDeleteCatalogPricingQueueByIdRemovesRow(): void - { - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ - 'purchasableIds' => [1], - 'storeId' => $this->_storeId, - ]); - - $row = CatalogPricingQueueRecord::findOne(['storeId' => $this->_storeId]); - self::assertNotNull($row); - - Plugin::getInstance()->getCatalogPricing()->deleteCatalogPricingQueueRowById($row->id); - - $deleted = CatalogPricingQueueRecord::findOne($row->id); - self::assertNull($deleted); - } - - /** - * Test complex scenario: multiple stores and types with merging and reservation. - * - * @dataProvider complexQueueScenarioDataProvider - */ - public function testComplexQueueScenario(array $queueCalls, array $expectedRows): void - { - $stores = Plugin::getInstance()->getStores()->getAllStores(); - // Execute all queue calls - foreach ($queueCalls as $call) { - $call['storeId'] = $stores->firstWhere('handle', $call['storeId'])?->id ?? null; - Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob($call); - } - - // Verify the state of all rows - /** @var CatalogPricingQueueRecord[] $allRows */ - $allRows = CatalogPricingQueueRecord::find()->all(); - self::assertCount(count($expectedRows), $allRows, 'Should have expected number of rows'); - - foreach ($expectedRows as $index => $expected) { - $expected['storeId'] = $stores->firstWhere('handle', $expected['storeId'])?->id ?? null; - $row = $allRows[$index] ?? null; - self::assertNotNull($row, "Row at index $index should exist"); - self::assertEquals($expected['storeId'] ?? null, $row->storeId, "Row $index storeId mismatch"); - self::assertEquals($expected['type'], $row->type, "Row $index type mismatch"); - self::assertEquals($expected['ids'], $row->getIds(), "Row $index IDs mismatch"); - } - } - - public function complexQueueScenarioDataProvider(): array - { - $primaryStoreHandle = 'primary'; - $ukStoreHandle = 'ukStore'; - - return [ - 'single-store-multiple-types' => [ - [ - ['purchasableIds' => [1, 2], 'storeId' => $primaryStoreHandle], - ['catalogPricingRuleIds' => [10, 11], 'storeId' => $primaryStoreHandle], - ], - [ - ['storeId' => $primaryStoreHandle, 'type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE, 'ids' => [1, 2]], - ['storeId' => $primaryStoreHandle, 'type' => CatalogPricingQueueRecord::TYPE_RULE, 'ids' => [10, 11]], - ], - ], - 'multi-store-same-type-merging' => [ - [ - ['purchasableIds' => [1], 'storeId' => $primaryStoreHandle], - ['purchasableIds' => [2], 'storeId' => $primaryStoreHandle], - ['purchasableIds' => [1], 'storeId' => $ukStoreHandle], - ], - [ - ['storeId' => $primaryStoreHandle, 'type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE, 'ids' => [1, 2]], - ['storeId' => $ukStoreHandle, 'type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE, 'ids' => [1]], - ], - ], - 'null-store-with-specific-stores' => [ - [ - ['purchasableIds' => [1, 2], 'storeId' => $primaryStoreHandle], - ['purchasableIds' => [3], 'storeId' => null], - ], - [ - ['storeId' => $primaryStoreHandle, 'type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE, 'ids' => [1, 2]], - ['storeId' => null, 'type' => CatalogPricingQueueRecord::TYPE_PURCHASABLE, 'ids' => [3]], - ], - ], - ]; - } -} diff --git a/tests/unit/services/CatalogPricingTest.php b/tests/unit/services/CatalogPricingTest.php deleted file mode 100644 index 321112fee6..0000000000 --- a/tests/unit/services/CatalogPricingTest.php +++ /dev/null @@ -1,298 +0,0 @@ - - * @since 5.3.7 - */ -class CatalogPricingTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - private array $_fixtureKeys = [ - 'rad-hoodie', - 'hypercolor-tshirt', - 'double-decker-bus-toy', - ]; - - private array $_createdRules = []; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - protected function _after() - { - parent::_after(); - - if (empty($this->_createdRules)) { - return; - } - - foreach ($this->_createdRules as $rule) { - Plugin::getInstance()->getCatalogPricingRules()->deleteCatalogPricingRuleById($rule->id); - } - } - - public function testGeneratePricesNoRules(): void - { - /** @var ProductFixture $productsFixture */ - $productsFixture = $this->tester->grabFixture('products'); - - Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); - - // From the product fixture 3 variants exists in all stores, 1 variant only exists in the `ukStore` - // (3 x 3) + 1 = 10 - self::assertCount(10, (new Query())->select('id')->from(Table::CATALOG_PRICING)->all()); - - $checkVariantPrices = function(Product $product) { - $storeId = $product->getStore()->id; - $product->getVariants()->each(function(Variant $variant) use ($storeId) { - $price = (new Query()) - ->select('price') - ->from(Table::CATALOG_PRICING) - ->where(['purchasableId' => $variant->id]) - ->andWhere(['storeId' => $storeId]) - ->scalar(); - self::assertEquals($variant->basePrice, $price, - $variant->title . ' price has been generated correctly' - ); - }); - }; - - // Check that the prices are correct - foreach ($this->_fixtureKeys as $key) { - $product = $productsFixture->getElement($key); - $checkVariantPrices($product); - } - } - - /** - * @return void - * @throws \Codeception\Exception\ModuleException - * @dataProvider generatePricesWithRulesDataProvider - */ - public function testGeneratePricesWithRules(array $ruleConfig, int $siteId, float $rate, ?array $impactedSkus = null): void - { - /** @var ProductFixture $productsFixture */ - $productsFixture = $this->tester->grabFixture('products'); - - $priceBySku = []; - $variantSkus = []; - - foreach ($this->_fixtureKeys as $key) { - $product = $productsFixture->getElement($key); - $product->getVariants()->each(function(Variant $variant) use (&$variantSkus, &$priceBySku) { - $priceBySku[$variant->sku] = $variant->getPrice(); - $variantSkus[] = $variant->sku; - }); - } - - $rule = $this->_createRule($ruleConfig); - - // Generate the prices - Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices(); - $variants = Variant::find()->siteId($siteId)->sku($variantSkus)->collect(); - - $variants->each(function(Variant $variant) use ($priceBySku, $rate, $impactedSkus) { - // skip assertions for variants that are priced `0.00` as just in tests they are not priced in each store - if ($variant->getPrice() === 0.00) { - return; - } - - if ($impactedSkus !== null && !in_array($variant->sku, $impactedSkus, true)) { - $price = $priceBySku[$variant->sku]; - } else { - $currency = $variant->getStore()->getCurrency(); - $price = (float)Plugin::getInstance()->getCurrencies()->getTeller($currency)->multiply($priceBySku[$variant->sku], 1 + $rate); - } - - self::assertEquals($variant->getPrice(), $price); - }); - } - - public function generatePricesWithRulesDataProvider(): array - { - return [ - 'rule-no-conditions' => [ - [ - 'apply' => CatalogPricingRuleRecord::APPLY_BY_PERCENT, - 'name' => '10% off', - 'enabled' => true, - 'applyAmount' => -0.1, - 'applyPriceType' => CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE, - 'isPromotionalPrice' => false, - 'storeId' => 1, - ], - 1, - -0.1, - ], - 'rule-purchasable-condition' => [ - [ - 'apply' => CatalogPricingRuleRecord::APPLY_BY_PERCENT, - 'name' => '20% off', - 'enabled' => true, - 'applyAmount' => -0.2, - 'applyPriceType' => CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE, - 'isPromotionalPrice' => false, - 'storeId' => 1, - '_conditionRules' => [ - 'purchasableCondition' => [ - [ - 'class' => SkuConditionRule::class, - 'operator' => 'bw', - 'value' => 'rad', - ], - ], - ], - ], - 1, - -0.2, - ['rad-hood'], - ], - 'rule-variant-condition' => [ - [ - 'apply' => CatalogPricingRuleRecord::APPLY_BY_PERCENT, - 'name' => '30% off', - 'enabled' => true, - 'applyAmount' => -0.3, - 'applyPriceType' => CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE, - 'isPromotionalPrice' => false, - 'storeId' => 1, - '_conditionRules' => [ - 'variantCondition' => [ - [ - 'class' => SkuConditionRule::class, - 'operator' => 'ew', - 'value' => 'hood', - ], - ], - ], - ], - 1, - -0.3, - ['rad-hood'], - ], - 'rule-product-condition' => [ - [ - 'apply' => CatalogPricingRuleRecord::APPLY_BY_PERCENT, - 'name' => '10% off', - 'enabled' => true, - 'applyAmount' => -0.1, - 'applyPriceType' => CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE, - 'isPromotionalPrice' => false, - 'storeId' => 1, - '_conditionRules' => [ - 'productCondition' => [ - [ - 'class' => ProductTypeConditionRule::class, - 'operator' => 'in', - 'values' => fn(): array => [(new Query())->select('uid')->from(Table::PRODUCTTYPES)->where(['handle' => 'tShirts'])->scalar()], - ], - ], - ], - ], - 1, - -0.1, - ['hct-white', 'hct-blue'], - ], - 'rule-product-condition-no-primary-site-store' => [ - [ - 'apply' => CatalogPricingRuleRecord::APPLY_BY_PERCENT, - 'name' => '10% off', - 'enabled' => true, - 'applyAmount' => -0.1, - 'applyPriceType' => CatalogPricingRuleRecord::APPLY_PRICE_TYPE_PRICE, - 'isPromotionalPrice' => false, - 'storeId' => fn() => Plugin::getInstance()->getStores()->getStoreByHandle('ukStore')->id, - '_conditionRules' => [ - 'productCondition' => [ - [ - 'class' => ProductTypeConditionRule::class, - 'operator' => 'in', - 'values' => fn(): array => [(new Query())->select('uid')->from(Table::PRODUCTTYPES)->where(['handle' => 'ukOnly'])->scalar()], - ], - ], - ], - ], - 1002, - -0.1, - ['ddb-red'], - ], - ]; - } - - private function _createRule(array $config): CatalogPricingRule - { - $rule = new CatalogPricingRule(); - $conditionRules = []; - - if (isset($config['_conditionRules'])) { - $conditionRules = $config['_conditionRules']; - unset($config['_conditionRules']); - } - - foreach ($config as $key => $item) { - // if the item is a closure, call it to get the value - $config[$key] = is_callable($item) ? $item() : $item; - } - - /** @var CatalogPricingRule $rule */ - $rule = \Craft::configure($rule, $config); - - foreach ($conditionRules as $condition => $rules) { - /** @var BaseCondition $c */ - $c = $rule->$condition; - - foreach ($rules as $r) { - foreach ($r as $key => $item) { - // if the item is a closure, call it to get the value - $r[$key] = is_callable($item) ? $item() : $item; - } - - $c->addConditionRule($c->createConditionRule($r)); - } - - $rule->$condition = $c; - } - - Plugin::getInstance()->getCatalogPricingRules()->saveCatalogPricingRule($rule); - - $this->_createdRules[] = $rule; - - return $rule; - } -} diff --git a/tests/unit/services/CouponsTest.php b/tests/unit/services/CouponsTest.php deleted file mode 100644 index a7ab88bcb5..0000000000 --- a/tests/unit/services/CouponsTest.php +++ /dev/null @@ -1,250 +0,0 @@ - - * @since 4.0 - */ -class CouponsTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var Coupons - */ - private Coupons $_service; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'discounts' => [ - 'class' => DiscountsFixture::class, - ], - ]; - } - - /** - * @return void - * @throws \Codeception\Exception\ModuleException - */ - public function testGetAllCodes(): void - { - $coupons = $this->tester->grabFixture('discounts')['discount_with_coupon']['coupons']; - $codes = $this->_service->getAllCodes(); - - self::assertIsArray($codes); - self::assertNotEmpty($codes); - self::assertContains($coupons[0]->code, $codes); - } - - /** - * @dataProvider getCouponByCodeDataProvider - * @param string $code - * @param Coupon|null $expectedCoupon - * @return void - * @throws \yii\base\InvalidConfigException - */ - public function testGetCouponByCode(string $code, ?Coupon $expectedCoupon): void - { - $coupon = $this->_service->getCouponByCode($code); - - if (!$expectedCoupon) { - self::assertNull($coupon); - } else { - self::assertNotNull($coupon); - self::assertInstanceOf($expectedCoupon::class, $coupon); - self::assertEquals($expectedCoupon->code, $coupon->code); - } - } - - /** - * @return \string[][] - */ - public function getCouponByCodeDataProvider(): array - { - return [ - ['discount_1', new Coupon(['code' => 'discount_1'])], - ['invalid_code', null], - ]; - } - - /** - * @return void - * @throws \Codeception\Exception\ModuleException - * @throws \yii\base\InvalidConfigException - */ - public function testGetCouponsByDiscountId(): void - { - $discount = $this->tester->grabFixture('discounts')['discount_with_coupon']; - - $coupons = $this->_service->getCouponsByDiscountId(0); - - self::assertIsArray($coupons); - self::assertEmpty($coupons); - - $coupons = $this->_service->getCouponsByDiscountId($discount['id']); - - self::assertIsArray($coupons); - self::assertNotEmpty($coupons); - self::assertContains($discount['coupons'][0]->code, ArrayHelper::getColumn($coupons, 'code')); - } - - /** - * @dataProvider generateCouponCodesDataProvider - * @param int $count - * @param string $format - * @param array $existingCodes - * @param bool $exception - * @return void - * @throws \Exception - */ - public function testGenerateCouponCodes(int $count, string $format, array $existingCodes, bool $exception): void - { - if ($exception) { - $this->expectException(\Exception::class); - } - $codes = $this->_service->generateCouponCodes($count, $format, $existingCodes); - - self::assertIsArray($codes); - self::assertCount($count, $codes); - if (!empty($existingCodes)) { - self::assertNotContains($existingCodes, $codes); - } - self::assertMatchesRegularExpression('/' . str_replace(Coupons::COUPON_FORMAT_REPLACEMENT_CHAR, '.', $format) . '/', $codes[0]); - } - - - public function generateCouponCodesDataProvider(): array - { - return [ - [ - 10, - 'commerce_####', - [], - false, - ], - [ - 45, - 'commerce_#', - [], - true, - ], - [ - 25, - 'commerce_#_coupons', - ['commerce_A_coupons'], - false, - ], - ]; - } - - /** - * @dataProvider saveCouponDataProvider - * - * @param Coupon $newCoupon - * @param bool $runValidation - * @param bool $expectedResult - * @return void - * @throws \Exception - */ - public function testSaveCoupon(Coupon $newCoupon, bool $runValidation, bool $expectedResult): void - { - $newCoupon->discountId = $this->tester->grabFixture('discounts')['discount_with_coupon']['id']; - $result = $this->_service->saveCoupon($newCoupon, $runValidation); - - self::assertSame($expectedResult, $result); - - if ($expectedResult) { - self::assertNotNull($newCoupon->id); - } else { - self::assertNull($newCoupon->id); - } - } - - /** - * @return array[] - */ - public function saveCouponDataProvider(): array - { - return [ - [new Coupon(['code' => 'test_code']), true, true], - [new Coupon(['code' => 'discount_1']), true, false], - ]; - } - - /** - * @return void - * @throws \Codeception\Exception\ModuleException - * @throws \Throwable - * @throws \yii\db\StaleObjectException - */ - public function testDeleteCouponById(): void - { - $couponRecord = new \craft\commerce\records\Coupon(); - $couponRecord->code = 'commerce_test_code'; - $couponRecord->discountId = $this->tester->grabFixture('discounts')['discount_with_coupon']['id']; - $couponRecord->uses = 0; - $couponRecord->maxUses = null; - $couponRecord->save(); - - $result = $this->_service->deleteCouponById($couponRecord->id); - self::assertEquals(true, $result); - } - - public function testSaveDiscountCoupons(): void - { - $discountFixture = $this->tester->grabFixture('discounts')['discount_with_coupon']; - $newCoupon = new Coupon([ - 'discountId' => $discountFixture['id'], - 'code' => 'new_commerce_coupon', - 'uses' => 0, - ]); - - $discount = Plugin::getInstance()->getDiscounts()->getDiscountById($discountFixture['id']); - $discount->setCoupons([...$discount->getCoupons(), $newCoupon]); - - $result = $this->_service->saveDiscountCoupons($discount); - self::assertEquals(true, $result); - - $discount->setCoupons([]); - - $result = $this->_service->saveDiscountCoupons($discount); - self::assertEquals(true, $result); - - $this->expectException(\Exception::class); - $this->_service->saveDiscountCoupons(new Discount()); - } - - /** - * - */ - protected function _before(): void - { - parent::_before(); - - $this->_service = Plugin::getInstance()->getCoupons(); - } -} diff --git a/tests/unit/services/CustomersTest.php b/tests/unit/services/CustomersTest.php deleted file mode 100644 index dfb8d467f8..0000000000 --- a/tests/unit/services/CustomersTest.php +++ /dev/null @@ -1,516 +0,0 @@ - - * @since 4.3.0 - */ -class CustomersTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var OrdersFixture - */ - protected OrdersFixture $fixtureData; - - /** - * @var array - */ - private array $_deleteElementIds = []; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'customer' => [ - 'class' => CustomerFixture::class, - ], - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - protected function _before(): void - { - parent::_before(); - - $this->fixtureData = $this->tester->grabFixture('orders'); - } - - public function testOrderCompleteHandlerNotCalled(): void - { - Plugin::getInstance()->set('customers', $this->make(Customers::class, [ - 'orderCompleteHandler' => function() { - self::never(); - }, - ])); - - /** @var Order $completedOrder */ - $completedOrder = $this->fixtureData->getElement('completed-new'); - - self::assertTrue($completedOrder->markAsComplete()); - } - - public function testOrderCompleteHandlerCalled(): void - { - Plugin::getInstance()->set('customers', $this->make(Customers::class, [ - 'orderCompleteHandler' => function() { - self::once(); - }, - ])); - - $order = $this->_createOrder('test@newemailaddress.xyz'); - - self::assertTrue($order->markAsComplete()); - - $this->_deleteElementIds[] = $order->id; - $this->_deleteElementIds[] = $order->getCustomer()->id; - } - - /** - * @param string $email - * @param bool $register - * @param bool $deleteUser an argument to help with cleanup - * @return void - * @throws \Throwable - * @throws OrderStatusException - * @throws ElementNotFoundException - * @throws Exception - * @throws InvalidConfigException - * @dataProvider registerOnCheckoutDataProvider - */ - public function testRegisterOnCheckout(string $email, bool $register, bool $deleteUser): void - { - $order = $this->_createOrder($email); - $originallyCredentialed = $order->getCustomer()->getIsCredentialed(); - - $order->registerUserOnOrderComplete = $register; - - self::assertTrue($order->markAsComplete()); - - $foundUser = User::find()->email($email)->status(null)->one(); - self::assertNotNull($foundUser); - - if ($register || $originallyCredentialed) { - self::assertTrue($foundUser->getIsCredentialed()); - } else { - self::assertFalse($foundUser->getIsCredentialed()); - } - - $this->_deleteElementIds[] = $order->id; - if ($deleteUser) { - $this->_deleteElementIds[] = $order->getCustomer()->id; - } - } - - /** - * @return array[] - */ - public function registerOnCheckoutDataProvider(): array - { - return [ - 'dont-register-guest' => ['guest@crafttest.com', false, true], - 'register-guest' => ['guest@crafttest.com', true, true], - 'register-credentialed-user' => ['cred.user@crafttest.com', true, false], - 'dont-register-credentialed-user' => ['cred.user@crafttest.com', false, false], - ]; - } - - /** - * @param string $email - * @param bool $deleteUser - * @param Address|null $billingAddres - * @param Address|null $shippingAddress - * @return void - * @throws ElementNotFoundException - * @throws Exception - * @throws OrderStatusException - * @throws \Throwable - * @dataProvider registerOnCheckoutCopyAddressesDataProvider - */ - public function testRegisterOnCheckoutCopyAddresses(string $email, ?array $billingAddress, ?array $shippingAddress, int $addressCount): void - { - $isOnlyOneAddress = empty($billingAddress) || empty($shippingAddress); - $order = $this->_createOrder($email); - $order->registerUserOnOrderComplete = true; - \Craft::$app->getElements()->saveElement($order, false); - - if (!empty($billingAddress)) { - $order->setBillingAddress($billingAddress); - } - - if (!empty($shippingAddress)) { - $order->setShippingAddress($shippingAddress); - } - - self::assertTrue($order->markAsComplete()); - - $userAddresses = Address::find()->ownerId($order->getCustomer()->id)->all(); - self::assertCount($addressCount, $userAddresses); - - $primaryCount = 0; - foreach ($userAddresses as $userAddress) { - if ($addressCount === 1) { - $addressTitle = \Craft::t('app', 'Address'); - if ($isOnlyOneAddress) { - $addressTitle = !empty($billingAddress) ? \Craft::t('commerce', 'Billing Address') : \Craft::t('commerce', 'Shipping Address'); - } - self::assertEquals($addressTitle, $userAddress->title); - - $address = $billingAddress ?? $shippingAddress; - self::assertEquals($address['fullName'], $userAddress->fullName); - self::assertEquals($address['addressLine1'], $userAddress->addressLine1); - self::assertEquals($address['locality'], $userAddress->locality); - self::assertEquals($address['administrativeArea'], $userAddress->administrativeArea); - self::assertEquals($address['postalCode'], $userAddress->postalCode); - self::assertEquals($address['countryCode'], $userAddress->countryCode); - } - - if ($userAddress->getIsPrimaryBilling()) { - if ($addressCount === 2) { - self::assertEquals(\Craft::t('commerce', 'Billing Address'), $userAddress->title); - self::assertEquals($billingAddress['fullName'], $userAddress->fullName); - self::assertEquals($billingAddress['addressLine1'], $userAddress->addressLine1); - self::assertEquals($billingAddress['locality'], $userAddress->locality); - self::assertEquals($billingAddress['administrativeArea'], $userAddress->administrativeArea); - self::assertEquals($billingAddress['postalCode'], $userAddress->postalCode); - self::assertEquals($billingAddress['countryCode'], $userAddress->countryCode); - } - - $primaryCount++; - } - if ($userAddress->getIsPrimaryShipping()) { - if ($addressCount === 2) { - self::assertEquals(\Craft::t('commerce', 'Shipping Address'), $userAddress->title); - self::assertEquals($shippingAddress['fullName'], $userAddress->fullName); - self::assertEquals($shippingAddress['addressLine1'], $userAddress->addressLine1); - self::assertEquals($shippingAddress['locality'], $userAddress->locality); - self::assertEquals($shippingAddress['administrativeArea'], $userAddress->administrativeArea); - self::assertEquals($shippingAddress['postalCode'], $userAddress->postalCode); - self::assertEquals($shippingAddress['countryCode'], $userAddress->countryCode); - } - - $primaryCount++; - } - } - - self::assertEquals($isOnlyOneAddress ? 1 : 2, $primaryCount); - - $this->_deleteElementIds[] = $order->id; - $this->_deleteElementIds[] = $order->getCustomer()->id; - } - - /** - * @return array[] - */ - public function registerOnCheckoutCopyAddressesDataProvider(): array - { - $billingAddress = [ - 'fullName' => 'Guest Billing', - 'addressLine1' => '1 Main Billing Street', - 'locality' => 'Billingsville', - 'administrativeArea' => 'OR', - 'postalCode' => '12345', - 'countryCode' => 'US', - ]; - $shippingAddress = [ - 'fullName' => 'Guest Shipping', - 'addressLine1' => '1 Main Shipping Street', - 'locality' => 'Shippingsville', - 'administrativeArea' => 'AL', - 'postalCode' => '98765', - 'countryCode' => 'US', - ]; - - return [ - 'guest-two-addresses' => [ - 'guest.person@crafttest.com', - $billingAddress, - $shippingAddress, - 2, - ], - 'guest-matching-addresses' => [ - 'guest.person@crafttest.com', - $billingAddress, - $billingAddress, - 1, - ], - 'guest-one-billing-address' => [ - 'guest.person@crafttest.com', - $billingAddress, - null, - 1, - ], - 'guest-one-shipping-address' => [ - 'guest.person@crafttest.com', - null, - $shippingAddress, - 1, - ], - ]; - } - - /** - * @param bool|null $saveBilling - * @param array|null $billingAddress - * @param bool|null $saveShipping - * @param array|null $shippingAddress - * @param bool $setSourceBilling - * @param bool $setSourceShipping - * @return void - * @throws ElementNotFoundException - * @throws Exception - * @throws OrderStatusException - * @throws \Throwable - * @dataProvider saveAddressesOnOrderCompleteDataProvider - * @since 4.3.0 - */ - public function testSaveAddressesOnOrderComplete(?bool $saveBilling, ?array $billingAddress, ?bool $saveShipping, ?array $shippingAddress, int $newAddressCount, bool $setSourceBilling, bool $setSourceShipping): void - { - $order = $this->_createOrder('cred.user@crafttest.com'); - $customer = $order->getCustomer(); - $sourceAddress = [ - 'fullName' => 'Source Address', - 'addressLine1' => '1 Source Road', - 'locality' => 'Sourcington', - 'administrativeArea' => 'OR', - 'postalCode' => '991199', - 'countryCode' => 'US', - 'ownerId' => $customer->id, - ]; - - if ($setSourceBilling || $setSourceShipping) { - $sourceAddressModel = \Craft::createObject([ - 'class' => Address::class, - 'attributes' => $sourceAddress, - ]); - \Craft::$app->getElements()->saveElement($sourceAddressModel, false, false ,false); - $this->_deleteElementIds[] = $sourceAddressModel->id; - - if ($setSourceBilling) { - $order->sourceBillingAddressId = $sourceAddressModel->id; - } - - if ($setSourceShipping) { - $order->sourceShippingAddressId = $sourceAddressModel->id; - } - } - $originalAddressIds = collect($customer->getAddresses())->pluck('id')->all(); - - $order->saveBillingAddressOnOrderComplete = $saveBilling; - $order->saveShippingAddressOnOrderComplete = $saveShipping; - - $order->setBillingAddress($billingAddress); - $order->setShippingAddress($shippingAddress); - - \Craft::$app->getElements()->saveElement($order, false, false, false); - - // Get the ID in early to delete in case of failure - $this->_deleteElementIds[] = $order->id; - - self::assertTrue($order->markAsComplete()); - - // @TODO Switch to `$customer->getAddresses()` once its memoization is fixed in Craft core - $addressQuery = Address::find()->ownerId($customer->id); - // @TODO Switch to the `primaryOwnerId` query param once it is fixed in Craft core - // $addressQuery = Address::find()->primaryOwnerId($customer->id); - - if (!empty($originalAddressIds)) { - $addressQuery->id(array_merge(['not'], $originalAddressIds)); - } - - $addresses = $addressQuery->all(); - self::assertCount($newAddressCount, $addresses); - $addressNames = collect($addresses)->pluck('fullName')->all(); - $addressLine1s = collect($addresses)->pluck('addressLine1')->all(); - - if ($billingAddress && $saveBilling && !$setSourceBilling) { - self::assertContains($billingAddress['fullName'], $addressNames); - self::assertContains($billingAddress['addressLine1'], $addressLine1s); - } - - if ($shippingAddress && $saveShipping && !$setSourceShipping) { - self::assertContains($shippingAddress['fullName'], $addressNames); - self::assertContains($shippingAddress['addressLine1'], $addressLine1s); - } - } - - /** - * @return array - */ - public function saveAddressesOnOrderCompleteDataProvider(): array - { - $billingAddress = [ - 'fullName' => 'Billing Name', - 'addressLine1' => '1 Main Billing Street', - 'locality' => 'Billingsville', - 'administrativeArea' => 'OR', - 'postalCode' => '12345', - 'countryCode' => 'US', - ]; - $shippingAddress = [ - 'fullName' => 'Shipping Name', - 'addressLine1' => '1 Main Shipping Street', - 'locality' => 'Shippingsville', - 'administrativeArea' => 'AL', - 'postalCode' => '98765', - 'countryCode' => 'US', - ]; - - return [ - 'save-both' => [ - true, // save billing - $billingAddress, // billing address - true, // save shipping - $shippingAddress, // shipping address - 2, // new address count - false, // set source billing - false, // set source shipping - ], - 'save-billing-only' => [ - true, - $billingAddress, - false, - null, - 1, - false, - false, - ], - 'save-shipping-only' => [ - false, - null, - true, - $shippingAddress, - 1, - false, - false, - ], - 'save-both-but-same-address' => [ - true, - $billingAddress, - true, - $billingAddress, - 1, - false, - false, - ], - 'try-to-save-both-but-no-addresses' => [ - true, - null, - true, - null, - 0, - false, - false, - ], - 'try-to-save-but-source-billing-present' => [ - true, - $billingAddress, - false, - null, - 0, - true, - false, - ], - 'try-to-save-but-source-shipping-present' => [ - false, - null, - true, - $shippingAddress, - 0, - false, - true, - ], - 'try-to-save-both-but-sources-present' => [ - true, - $billingAddress, - true, - $shippingAddress, - 0, - true, - true, - ], - 'try-save-both-but-billing-source-present' => [ - true, - $billingAddress, - true, - $shippingAddress, - 1, - true, - false, - ], - 'try-save-both-but-shipping-source-present' => [ - true, - $billingAddress, - true, - $shippingAddress, - 1, - false, - true, - ], - ]; - } - - /** - * @inheritdoc - */ - protected function _after(): void - { - parent::_after(); - - // Cleanup data. - foreach ($this->_deleteElementIds as $elementId) { - \Craft::$app->getElements()->deleteElementById($elementId, null, null, true); - } - } - - private function _createOrder(string $email): Order - { - $order = new Order(); - $user = \Craft::$app->getUsers()->ensureUserByEmail($email); - $order->setCustomer($user); - - $completedOrder = $this->fixtureData->getElement('completed-new'); - $lineItem = $completedOrder->getLineItems()[0]; - $qty = 4; - $note = 'My note'; - $lineItem = Plugin::getInstance()->getLineItems()->create($order, [ - 'purchasableId' => $lineItem->purchasableId, - 'options' => [], - 'qty' => $qty, - 'note' => $note, - ]); - $order->setLineItems([$lineItem]); - - return $order; - } -} diff --git a/tests/unit/services/DiscountsTest.php b/tests/unit/services/DiscountsTest.php deleted file mode 100644 index 300e9d85f8..0000000000 --- a/tests/unit/services/DiscountsTest.php +++ /dev/null @@ -1,1024 +0,0 @@ - - * @author Global Network Group | Giel Tettelaar - * @since 2.1 - */ -class DiscountsTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var Discounts $discounts - */ - protected Discounts $discounts; - - /** - * @var User|null - */ - private ?User $_user = null; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'discounts' => [ - 'class' => DiscountsFixture::class, - ], - 'customers' => [ - 'class' => CustomerFixture::class, - ], - 'products' => [ - 'class' => ProductFixture::class, - ], - 'categories' => [ - 'class' => CategoriesFixture::class, - ], - ]; - } - - /** - * - */ - public function testOrderCouponAvailableWithInvalidCoupon(): void - { - $this->orderCouponAvailableTest( - ['couponCode' => 'invalid_coupon'], - false, - 'Coupon not valid.' - ); - } - - /** - * - */ - public function testSuccessOrderCouponAvailable(): void - { - $this->orderCouponAvailableTest( - ['couponCode' => 'discount_1', 'customerId' => $this->_user->id], - true, - '' - ); - } - - /** - * @throws Exception - */ - public function testExistingCouponNotEnabled(): void - { - // Set it to be disabled - $this->updateOrderCoupon([ - 'enabled' => false, - ]); - - $this->orderCouponAvailableTest( - ['couponCode' => 'discount_1', 'customerId' => $this->_user->id], - false, - 'Coupon not valid.' - ); - } - - /** - * @throws Exception - */ - public function testOrderCouponExpired(): void - { - // Invalidate the coupon.... It's valid until sometime in the past. - $this->updateOrderCoupon([ - 'dateTo' => '2019-05-01 10:21:33', - ]); - - $this->orderCouponAvailableTest( - ['couponCode' => 'discount_1', 'customerId' => $this->_user->id], - false, - 'Discount is out of date.' - ); - } - - /** - * @throws Exception - */ - public function testOrderCouponNotYetValid(): void - { - // Set the coupon to start in two days from now. - $date = new DateTime('now'); - $date->add(new DateInterval('P2D')); - $this->updateOrderCoupon([ - 'dateFrom' => $date->format('Y-m-d H:i:s'), - ]); - - $this->orderCouponAvailableTest( - ['couponCode' => 'discount_1', 'customerId' => $this->_user->id], - false, - 'Discount is out of date.' - ); - } - - /** - * @throws Exception - */ - public function testCouponThatHasBeenUsedTooMuch(): void - { - $this->updateOrderCoupon([ - 'totalDiscountUses' => 2, - ]); - - $this->orderCouponAvailableTest( - ['couponCode' => 'discount_1', 'customerId' => $this->_user->id], - false, - 'Discount use has reached its limit.' - ); - } - - /** - * @throws Exception - */ - public function testCouponWithUseLimitAndNoUserOnClient(): void - { - $this->updateOrderCoupon([ - 'perUserLimit' => true, - ]); - - $this->orderCouponAvailableTest( - ['couponCode' => 'discount_1', 'customerId' => null], - false, - 'This coupon is for registered users and limited to 1 uses.' - ); - } - - /** - * @throws Exception - */ - public function testCouponPerUserLimit(): void - { - $this->updateOrderCoupon([ - 'perUserLimit' => '1', - ]); - - Craft::$app->getDb()->createCommand() - ->insert('{{%commerce_customer_discountuses}}', [ - 'customerId' => $this->_user->id, - 'discountId' => $this->tester->grabFixture('discounts')['discount_with_coupon']['id'], - 'uses' => '1', - ])->execute(); - - $this->orderCouponAvailableTest( - ['couponCode' => 'discount_1', 'customerId' => $this->_user->id], - false, - 'This coupon is for registered users and limited to 1 uses.' - ); - - Craft::$app->getDb()->createCommand()->truncateTable(Table::CUSTOMER_DISCOUNTUSES)->execute(); - } - - /** - * @throws Exception - * @todo Replace stub with fixture data. #COM-54 - * - */ - public function testCouponPerEmailLimit(): void - { - $this->updateOrderCoupon([ - 'perEmailLimit' => '1', - ]); - - Craft::$app->getDb()->createCommand() - ->insert(Table::EMAIL_DISCOUNTUSES, [ - 'email' => 'testing@craftcommerce.com', - 'discountId' => $this->tester->grabFixture('discounts')['discount_with_coupon']['id'], - 'uses' => '1', - ])->execute(); - - /** @var Order $order */ - $order = Stub::construct( - Order::class, - [['couponCode' => 'discount_1', 'customerId' => $this->_user->id]], - ['getEmail' => 'testing@craftcommerce.com'] - ); - - $explanation = ''; - $result = $this->discounts->orderCouponAvailable($order, $explanation); - self::assertFalse($result); - self::assertSame('This coupon is limited to 1 uses.', $explanation); - - Craft::$app->getDb()->createCommand()->truncateTable(Table::CUSTOMER_DISCOUNTUSES)->execute(); - } - - /** - * - */ - public function testLineItemMatchingSuccess(): void - { - $this->matchLineItems( - ['couponCode' => null], - ['qty' => 2, 'price' => 10], - ['allPurchasables' => true, 'allCategories' => true], - [], - true - ); - } - - /** - * - */ - public function testLineItemMatchingSaleFail(): void - { - $this->matchLineItems( - ['couponCode' => null], - ['qty' => 2, 'price' => 15, 'promotionalPrice' => 10], - ['excludeOnPromotion' => true], - [], - false - ); - } - - /** - * - */ - public function testLineItemMatchingIfNotPromotable(): void - { - $this->matchLineItems( - ['couponCode' => null], - ['qty' => 2, 'price' => 15], - [], - ['isPromotable' => false], - false - ); - } - - // @TODO Add coverage for lineItemMatching against category and purchasableIds based discount conditions #COM-54 - - /** - * @throws Exception - * @throws InvalidConfigException - */ - public function testOrderCompleteHandler(): void - { - $discountId = $this->tester->grabFixture('discounts')['discount_with_coupon']['id']; - - // @TODO Replace the mocked Order with a fully saved real order to exercise the complete code path #COM-54 - /** @var Order $order */ - $order = $this->make(Order::class, [ - 'getAdjustmentsByType' => function($type) use ($discountId) { - $adjustment = new OrderAdjustment(); - $adjustment->sourceSnapshot = ['discountUseId' => $discountId]; - - return [$adjustment]; - }, - ]); - $order->couponCode = 'discount_1'; - $order->setCustomerId($this->_user->id); - - $this->updateOrderCoupon([ - 'perUserLimit' => '0', - 'perEmailLimit' => '0', - ]); - - $this->discounts->orderCompleteHandler($order); - - // Get thew new Total uses. - $totalUses = (int)(new Query()) - ->select('totalDiscountUses') - ->from('{{%commerce_discounts}}') - ->where(['id' => $discountId]) - ->scalar(); - - self::assertSame(1, $totalUses); - - // Get the Customer Discount Uses - $customerUses = (new Query()) - ->select('*') - ->from('{{%commerce_customer_discountuses}}') - ->where(['customerId' => $this->_user->id, 'discountId' => $discountId, 'uses' => '1']) - ->one(); - - self::assertNotNull($customerUses); - - // Get the Email Discount Uses - $customerEmail = $order->getCustomer()->email; - $customerUses = (new Query()) - ->select('*') - ->from('{{%commerce_email_discountuses}}') - ->where(['email' => $customerEmail, 'discountId' => $discountId, 'uses' => '1']) - ->one(); - - self::assertNotNull($customerUses); - - // Coupon uses - $couponUses = (new Query()) - ->select('uses') - ->from(Table::COUPONS) - ->where(['code' => 'discount_1']) - ->scalar(); - - self::assertEquals(1, $couponUses); - } - - /** - * - */ - public function testVoidIfNoCouponCode(): void - { - $order = new Order(['couponCode' => null]); - self::assertNull( - $this->discounts->orderCompleteHandler($order) - ); - } - - /** - * - */ - public function testVoidIfInvalidCouponCode(): void - { - $order = new Order(['couponCode' => 'i_dont_exist_as_coupon']); - self::assertNull( - $this->discounts->orderCompleteHandler($order) - ); - } - - /** - * @return void - * @throws Exception - * @throws \Random\RandomException - */ - public function testEnsureSortOrder(): void - { - $ids = []; - $storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - // Create dummy discount records - for ($i = 1; $i <= 5; $i++) { - $discount = new \craft\commerce\records\Discount(); - $discount->name = 'Dummy Discount ' . $i; - // randomise the sort order - $discount->sortOrder = $i + random_int(1, 15); - $discount->storeId = $storeId; - $discount->enabled = true; - $discount->allCategories = true; - $discount->allPurchasables = true; - $discount->percentageOffSubject = 'original'; - $discount->save(); - $ids[] = $discount->id; - } - - $this->discounts->ensureSortOrder($storeId); - - // Check table directly - $discountRows = (new Query()) - ->select(['id', 'sortOrder']) - ->from(Table::DISCOUNTS) - ->orderBy(['sortOrder' => SORT_ASC]) - ->all(); - - for ($i = 0; $i < count($discountRows); $i++) { - self::assertEquals($i + 1, $discountRows[$i]['sortOrder']); - } - - // Check get all method - $allDiscounts = $this->discounts->getAllDiscounts(); - for ($i = 0; $i < count($allDiscounts); $i++) { - self::assertEquals($i + 1, $allDiscounts[$i]->sortOrder); - } - - // delete temp records - foreach ($ids as $id) { - $this->discounts->deleteDiscountById($id); - } - } - - /** - * @param array|false $attributes - * @param int $count - * @return void - * @throws \Exception - * @dataProvider gatAllActiveDiscountsDataProvider - */ - public function testGetAllActiveDiscounts(array|false $attributes, int $count, array $discounts): void - { - if (!empty($discounts)) { - foreach ($discounts as &$discount) { - $emailUses = $discount['_emailUses'] ?? []; - - if (isset($discount['purchasableIds'])) { - $discount['purchasableIds'] = Variant::find()->sku($discount['purchasableIds'])->ids(); - } - - if (isset($discount['categoryIds'])) { - $discount['categoryIds'] = Category::find()->slug($discount['categoryIds'])->ids(); - } - - $discountModel = Craft::createObject([ - 'class' => Discount::class, - 'attributes' => $discount, - ]); - Plugin::getInstance()->getDiscounts()->saveDiscount($discountModel); - $discount = $discountModel->id; - - if ($discountModel->totalDiscountUses > 0) { - Craft::$app->getDb()->createCommand() - ->update(Table::DISCOUNTS, [ - 'totalDiscountUses' => $discountModel->totalDiscountUses, - ], [ - 'id' => $discountModel->id, - ]) - ->execute(); - } - - if (!empty($emailUses)) { - $emailUses = collect($emailUses)->map(fn($uses, $email) => [$email, $discountModel->id, $uses])->all(); - Craft::$app->getDb()->createCommand() - ->batchInsert(Table::EMAIL_DISCOUNTUSES, ['email', 'discountId', 'uses'], $emailUses) - ->execute(); - } - } - } - - if ($attributes === false) { - $activeDiscounts = $this->discounts->getAllActiveDiscounts(); - } else { - $order = new Order(array_diff_key($attributes, array_flip(['_lineItems']))); - - if (isset($attributes['_lineItems'])) { - $lineItems = []; - foreach ($attributes['_lineItems'] as $sku => $qty) { - $variant = Variant::find()->sku($sku)->one(); - $lineItems[] = Plugin::getInstance()->getLineItems()->create($order, [ - 'purchasableId' => $variant->id, - 'options' => [], - 'qty' => $qty, - ]); - } - $order->setLineItems($lineItems); - } - - $activeDiscounts = $this->discounts->getAllActiveDiscounts($order); - } - - if ($count > 0) { - self::assertCount($count, $activeDiscounts); - self::assertNotEmpty($activeDiscounts); - } else { - self::assertEmpty($activeDiscounts); - } - - // Tidy up the discounts - if (!empty($discounts)) { - foreach ($discounts as $discountId) { - Plugin::getInstance()->getDiscounts()->deleteDiscountById($discountId); - } - } - } - - /** - * @return array[] - */ - public function gatAllActiveDiscountsDataProvider(): array - { - $yesterday = (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(12, 0)->modify('-1 day'); - $tomorrow = (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(12, 0)->modify('+1 day'); - - function _createDiscounts($discounts) - { - return collect($discounts)->mapWithKeys(fn(array $d, string $key) => [$key => array_merge([ - 'name' => 'Discount - ' . $key, - 'perItemDiscount' => '1', - 'enabled' => true, - 'allCategories' => true, - 'allPurchasables' => true, - 'percentageOffSubject' => 'original', - 'storeId' => 1, - ], $d)])->all(); - } - - return [ - 'no-order' => [false, 1, []], - 'order-with-valid-coupon' => [['couponCode' => 'discount_1'], 1, []], - 'order-with-invalid-coupon' => [['couponCode' => 'coupon_code_doesnt_exist'], 0, []], - 'order-discounts-dates' => [ - [], - 3, - _createDiscounts([ - 'date-from-valid' => [ - 'dateFrom' => $yesterday, - ], - 'date-from-invalid' => [ - 'dateFrom' => $tomorrow, - ], - 'date-to-valid' => [ - 'dateTo' => $tomorrow, - ], - 'date-to-invalid' => [ - 'dateTo' => $yesterday, - ], - 'date-to-from-valid' => [ - 'dateFrom' => $yesterday, - 'dateTo' => $tomorrow, - ], - 'date-to-from-invalid' => [ - 'dateFrom' => $tomorrow, - 'dateTo' => $tomorrow->modify('+1 day'), - ], - ]), - ], - 'order-discounts-limits' => [ - [], - 4, - _createDiscounts([ - 'total-limit-zero' => [ - 'totalDiscountUseLimit' => 0, - ], - 'total-limit-zero-with-uses' => [ - 'totalDiscountUses' => 10, - 'totalDiscountUseLimit' => 0, - ], - 'total-limit-valid-with-no-uses' => [ - 'totalDiscountUses' => 0, - 'totalDiscountUseLimit' => 10, - ], - 'total-limit-valid-with-uses' => [ - 'totalDiscountUses' => 7, - 'totalDiscountUseLimit' => 10, - ], - 'total-limit-invalid-equals' => [ - 'totalDiscountUses' => 10, - 'totalDiscountUseLimit' => 10, - ], - 'total-limit-invalid-extra' => [ - 'totalDiscountUses' => 11, - 'totalDiscountUseLimit' => 10, - ], - ]), - ], - 'order-discounts-email-limits-no-email' => [ - [], - 1, - _createDiscounts([ - 'total-limit-zero' => [ - 'perEmailLimit' => 0, - ], - 'total-limit' => [ - 'perEmailLimit' => 1, - ], - ]), - ], - 'order-discounts-email-limits' => [ - ['email' => 'per.email.limit@crafttest.com'], - 4, - _createDiscounts([ - 'total-limit-zero' => [ - 'perEmailLimit' => 0, - ], - 'total-limit-zero-with-uses' => [ - '_emailUses' => ['per.email.limit@crafttest.com' => 10], - 'perEmailLimit' => 0, - ], - 'total-limit-valid-with-no-uses' => [ - 'perEmailLimit' => 10, - ], - 'total-limit-valid-with-uses' => [ - '_emailUses' => ['per.email.limit@crafttest.com' => 7], - 'perEmailLimit' => 10, - ], - 'total-limit-invalid-equals' => [ - '_emailUses' => ['per.email.limit@crafttest.com' => 10], - 'perEmailLimit' => 10, - ], - 'total-limit-invalid-extra' => [ - '_emailUses' => ['per.email.limit@crafttest.com' => 11], - 'perEmailLimit' => 10, - ], - ]), - ], - 'purchase-total-limit-no-items' => [ - [], - 4, - _createDiscounts([ - 'purchase-total-zero' => [ - 'purchaseTotal' => 0, - ], - 'purchase-total-all-purchasables-false' => [ - 'purchaseTotal' => 10, - 'allPurchasables' => false, - 'purchasableIds' => ['rad-hood'], - ], - 'purchase-total-all-categories-false' => [ - 'purchaseTotal' => 10, - 'allCategories' => false, - 'categoryIds' => ['commerce-category'], - ], - 'purchase-total-both-all-false' => [ - 'purchaseTotal' => 10, - 'allPurchasables' => false, - 'purchasableIds' => ['rad-hood'], - 'allCategories' => false, - 'categoryIds' => ['commerce-category'], - ], - ]), - ], - 'purchase-total-limit-with-items' => [ - [ - '_lineItems' => ['rad-hood' => 1], - ], - 5, - _createDiscounts([ - 'purchase-total-zero' => [ - 'purchaseTotal' => 0, - ], - 'purchase-total-all-purchasables-false' => [ - 'purchaseTotal' => 10, - 'allPurchasables' => false, - 'purchasableIds' => ['rad-hood'], - ], - 'purchase-total-all-categories-false' => [ - 'purchaseTotal' => 10, - 'allCategories' => false, - 'categoryIds' => ['commerce-category'], - ], - 'purchase-total-both-all-false' => [ - 'purchaseTotal' => 10, - 'allPurchasables' => false, - 'purchasableIds' => ['rad-hood'], - 'allCategories' => false, - 'categoryIds' => ['commerce-category'], - ], - 'purchase-total-valid' => [ - 'purchaseTotal' => 150, - ], - 'purchase-total-invalid' => [ - 'purchaseTotal' => 10.99, - ], - ]), - ], - 'qty-limits-no-items' => [ - [], - 6, - _createDiscounts([ - 'purchase-qty-zero' => [ - 'purchaseQty' => 0, - ], - 'max-qty-zero' => [ - 'maxPurchaseQty' => 0, - ], - 'both-zero' => [ - 'purchaseQty' => 0, - 'maxPurchaseQty' => 0, - ], - 'purchase-qty-all-purchasables-false' => [ - 'purchaseQty' => 4, - 'allPurchasables' => false, - 'purchasableIds' => ['rad-hood'], - ], - 'purchase-total-all-categories-false' => [ - 'purchaseTotal' => 10, - 'allCategories' => false, - 'categoryIds' => ['commerce-category'], - ], - 'purchase-total-both-all-false' => [ - 'purchaseTotal' => 10, - 'allPurchasables' => false, - 'purchasableIds' => ['rad-hood'], - 'allCategories' => false, - 'categoryIds' => ['commerce-category'], - ], - ]), - ], - 'qty-limits-with-items' => [ - ['_lineItems' => ['rad-hood' => 4]], - 6, - _createDiscounts([ - 'purchase-qty-zero' => [ - 'purchaseQty' => 0, - ], - 'max-qty-zero' => [ - 'maxPurchaseQty' => 0, - ], - 'both-zero' => [ - 'purchaseQty' => 0, - 'maxPurchaseQty' => 0, - ], - 'purchase-qty-valid' => [ - 'purchaseQty' => 3, - ], - 'purchase-qty-invalid' => [ - 'purchaseQty' => 5, - ], - 'max-qty-valid' => [ - 'maxPurchaseQty' => 10, - ], - 'max-qty-invalid' => [ - 'maxPurchaseQty' => 3, - ], - 'both-valid' => [ - 'purchaseQty' => 2, - 'maxPurchaseQty' => 10, - ], - 'both-invalid' => [ - 'purchaseQty' => 10, - 'maxPurchaseQty' => 14, - ], - ]), - ], - 'purchasables-one-lineitem' => [ - ['_lineItems' => ['rad-hood' => 1]], - 3, - _createDiscounts([ - 'all-purchasables' => [ - 'allPurchasables' => true, - ], - 'one-to-one' => [ - 'allPurchasables' => false, - 'purchasableIds' => ['rad-hood'], - ], - 'one-to-many' => [ - 'allPurchasables' => false, - 'purchasableIds' => ['rad-hood', 'hct-white'], - ], - 'no-match' => [ - 'allPurchasables' => false, - 'purchasableIds' => ['hct-blue'], - ], - ]), - ], - 'purchasables-multi-lineitems' => [ - ['_lineItems' => ['rad-hood' => 1, 'hct-white' => 1]], - 2, - _createDiscounts([ - 'one' => [ - 'allPurchasables' => false, - 'purchasableIds' => ['rad-hood'], - ], - 'many' => [ - 'allPurchasables' => false, - 'purchasableIds' => ['rad-hood', 'hct-white'], - ], - 'no-match' => [ - 'allPurchasables' => false, - 'purchasableIds' => ['hct-blue'], - ], - ]), - ], - ]; - } - - /** - * Test appending a coupon code to a discount - */ - public function testAppendCouponCode(): void - { - // Create a discount that requires a coupon code - $discount = new Discount(); - $discount->name = 'Test Discount'; - $discount->enabled = true; - $discount->requireCouponCode = true; - $discount->storeId = 1; - $discount->perItemDiscount = 10; - - // Save the discount - self::assertTrue($this->discounts->saveDiscount($discount)); - - // Test 1: Append a coupon code as string - $couponCode = 'TESTCODE123'; - $maxUses = 5; - - self::assertTrue($this->discounts->appendCouponCode($discount->id, $couponCode, $maxUses)); - - // Verify the coupon was added - $coupons = Plugin::getInstance()->getCoupons()->getCouponsByDiscountId($discount->id); - self::assertCount(1, $coupons); - self::assertEquals($couponCode, $coupons[0]->code); - self::assertEquals($maxUses, $coupons[0]->maxUses); - self::assertEquals(0, $coupons[0]->uses); - - // Test 2: Append another coupon as string without maxUses - $couponCode2 = 'TESTCODE456'; - self::assertTrue($this->discounts->appendCouponCode($discount->id, $couponCode2)); - - $coupons = Plugin::getInstance()->getCoupons()->getCouponsByDiscountId($discount->id); - self::assertCount(2, $coupons); - - // Test 3: Append a coupon using a Coupon model - $couponModel = new Coupon(); - $couponModel->code = 'MODELCODE789'; - $couponModel->maxUses = 10; - $couponModel->uses = 0; - - self::assertTrue($this->discounts->appendCouponCode($discount->id, $couponModel)); - - $coupons = Plugin::getInstance()->getCoupons()->getCouponsByDiscountId($discount->id); - self::assertCount(3, $coupons); - - // Find the coupon we just added - $addedCoupon = null; - foreach ($coupons as $c) { - if ($c->code === 'MODELCODE789') { - $addedCoupon = $c; - break; - } - } - - self::assertNotNull($addedCoupon); - self::assertEquals(10, $addedCoupon->maxUses); - self::assertEquals(0, $addedCoupon->uses); - - // Clean up - $this->discounts->deleteDiscountById($discount->id); - } - - /** - * Test appending a coupon code to a discount that doesn't require coupon codes - */ - public function testAppendCouponCodeToNonCouponDiscount(): void - { - // Create a discount that doesn't require a coupon code - $discount = new Discount(); - $discount->name = 'Test Discount No Coupon'; - $discount->enabled = true; - $discount->requireCouponCode = false; - $discount->storeId = 1; - $discount->perItemDiscount = 10; - - // Save the discount - self::assertTrue($this->discounts->saveDiscount($discount)); - - // Try to append a coupon code - should throw exception - $this->expectException(\Exception::class); - $this->expectExceptionMessage('The discount with ID "' . $discount->id . '" does not require a coupon code'); - - $this->discounts->appendCouponCode($discount->id, 'SHOULDFAIL'); - - // Clean up - $this->discounts->deleteDiscountById($discount->id); - } - - /** - * Test appending a coupon code to a non-existent discount - */ - public function testAppendCouponCodeToNonExistentDiscount(): void - { - // Try to append a coupon code to a non-existent discount - should throw exception - $this->expectException(\Exception::class); - $this->expectExceptionMessage('No discount exists with the ID "999999"'); - - $this->discounts->appendCouponCode(999999, 'SHOULDFAIL'); - } - - /** - * Test appending a coupon model with validation errors - */ - public function testAppendCouponModelWithValidationErrors(): void - { - // Create a discount that requires a coupon code - $discount = new Discount(); - $discount->name = 'Test Discount'; - $discount->enabled = true; - $discount->requireCouponCode = true; - $discount->storeId = 1; - $discount->perItemDiscount = 10; - - // Save the discount - self::assertTrue($this->discounts->saveDiscount($discount)); - - // Create a coupon model with empty code (should fail validation) - $couponModel = new Coupon(); - $couponModel->code = ''; // Empty code should fail validation - $couponModel->maxUses = 10; - - // Try to append the invalid coupon - $result = $this->discounts->appendCouponCode($discount->id, $couponModel); - - // Should return false due to validation errors - self::assertFalse($result); - - // Check that the coupon has validation errors - self::assertTrue($couponModel->hasErrors()); - self::assertArrayHasKey('code', $couponModel->getErrors()); - - // Clean up - $this->discounts->deleteDiscountById($discount->id); - } - - /** - * @param array $orderConfig - * @param array $lineItemConfig - * @param array $discountConfig - * @param array $purchasableConfig - * @param bool $desiredResult - * @throws \Exception - */ - protected function matchLineItems(array $orderConfig, array $lineItemConfig, array $discountConfig, array $purchasableConfig, bool $desiredResult) - { - $order = new Order($orderConfig); - $lineItem = new LineItem($lineItemConfig); - $lineItem->setOrder($order); - - $lineItem->setPurchasable( - new Purchasable($purchasableConfig) - ); - - $discount = new Discount($discountConfig); - - $this->assertSame( - $desiredResult, - $this->discounts->matchLineItem($lineItem, $discount) - ); - } - - /** - * @param int $discountId - * - * @return Discount - */ - protected function getDiscountById(int $discountId): Discount - { - return Plugin::getInstance()->discounts->getDiscountById($discountId); - } - - /** - * @param array $data - * @throws Exception - */ - protected function updateOrderCoupon(array $data) - { - $discount = $this->tester->grabFixture('discounts')['discount_with_coupon']; - Craft::$app->getDb()->createCommand() - ->update( - Table::DISCOUNTS, - $data, - ['id' => $discount['id']] - )->execute(); - } - - /** - * @param array $orderConfig - * @param bool $desiredResult - * @param string $desiredExplanation - */ - protected function orderCouponAvailableTest(array $orderConfig, bool $desiredResult, string $desiredExplanation = '') - { - $order = new Order($orderConfig); - - $explanation = ''; - $result = $this->discounts->orderCouponAvailable($order, $explanation); - self::assertSame($desiredResult, $result); - self::assertSame($desiredExplanation, $explanation); - } - - /** - * - */ - protected function _before() - { - parent::_before(); - $this->discounts = Plugin::getInstance()->getDiscounts(); - $customerFixture = $this->tester->grabFixture('customers'); - $this->_user = $customerFixture->getElement('customer1'); - Craft::$app->getUser()->setIdentity($this->_user); - } - - protected function _after() - { - Craft::$app->getUser()->setIdentity(null); - parent::_after(); - } -} diff --git a/tests/unit/services/GatewaysTest.php b/tests/unit/services/GatewaysTest.php deleted file mode 100644 index 5a1b8c4386..0000000000 --- a/tests/unit/services/GatewaysTest.php +++ /dev/null @@ -1,110 +0,0 @@ - - * @since 4.0.0 - */ -class GatewaysTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @param array $gateways - * @param int $count - * @return void - * @throws InvalidConfigException - * @dataProvider getAllCustomerEnabledGatewaysDataProvider - */ - public function testGetAllCustomerEnabledGateways(array $gateways, int $count, array $enabledKeys): void - { - foreach ($gateways as $name => &$gateway) { - [$class, $attributes] = $gateway; - $attributes['name'] = $name; - - if (isset($attributes['isFrontendEnabled']) && is_array($attributes['isFrontendEnabled'])) { - putenv(substr($attributes['isFrontendEnabled']['var'], 1) . '=' . $attributes['isFrontendEnabled']['value']); - $attributes['isFrontendEnabled'] = $attributes['isFrontendEnabled']['var']; - } - $gateway = Craft::createObject($class, ['config' => ['attributes' => $attributes]]); - } - - unset($gateway); - - $this->tester->mockMethods(Plugin::getInstance(), 'gateways', [ - 'getAllGateways' => collect($gateways), - ]); - - $enabledGateways = Plugin::getInstance()->getGateways()->getAllCustomerEnabledGateways(); - self::assertCount($count, $enabledGateways); - self::assertEquals($enabledKeys, ArrayHelper::getColumn($enabledGateways, 'name', false)); - } - - public function getAllCustomerEnabledGatewaysDataProvider(): array - { - return [ - [ - [ - 'dummy' => [ - Dummy::class, - [ - 'isFrontendEnabled' => true, - ], - ], - 'dummy-enabled-string' => [ - Dummy::class, - [ - 'isFrontendEnabled' => '1', - ], - ], - 'dummy-disabled-string' => [ - Dummy::class, - [ - 'isFrontendEnabled' => '0', - ], - ], - 'dummy-enabled-env' => [ - Dummy::class, - [ - 'isFrontendEnabled' => ['var' => '$DUMMY_ENABLED', 'value' => 'true'], - ], - ], - 'dummy-disabled-env' => [ - Dummy::class, - [ - 'isFrontendEnabled' => ['var' => '$DUMMY_DISABLED', 'value' => 'false'], - ], - ], - 'manual' => [ - Manual::class, - [ - 'isFrontendEnabled' => false, - ], - ], - ], - 3, - ['dummy', 'dummy-enabled-string', 'dummy-enabled-env'], - ], - ]; - } -} diff --git a/tests/unit/services/InventoryMovementTest.php b/tests/unit/services/InventoryMovementTest.php deleted file mode 100644 index 8058f2249c..0000000000 --- a/tests/unit/services/InventoryMovementTest.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @since 5.0.0 - * - */ -#[Group('inventory')] -class InventoryMovementTest extends Unit -{ - public function testGetInventoryItems() - { - $inventory = Plugin::getInstance()->getInventory(); - } -} diff --git a/tests/unit/services/InventoryTest.php b/tests/unit/services/InventoryTest.php deleted file mode 100644 index 2314675ad8..0000000000 --- a/tests/unit/services/InventoryTest.php +++ /dev/null @@ -1,102 +0,0 @@ - - * @since 5.3.0 - */ -class InventoryTest extends Unit -{ - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'products' => [ - 'class' => ProductFixture::class, - ], - ]; - } - - /** - * @param array $updateConfigs - * @param int $expected - * @return void - * @throws DeprecationException - * @throws InvalidConfigException - * @throws Exception - * @dataProvider setStockLevelDataProvider - */ - public function testUpdatePurchasableInventoryLevel(array $updateConfigs, int $expected): void - { - $variant = Variant::find()->sku('rad-hood')->one(); - $originalInventoryTracked = $variant->inventoryTracked; - $variant->inventoryTracked = true; - $originalStock = $variant->getStock(); - - foreach ($updateConfigs as $updateConfig) { - $qty = $updateConfig['quantity']; - unset($updateConfig['quantity']); - - Plugin::getInstance()->getInventory()->updatePurchasableInventoryLevel($variant, $qty, $updateConfig); - } - - self::assertEquals($expected, $variant->getStock()); - - Plugin::getInstance()->getInventory()->updatePurchasableInventoryLevel($variant, $originalStock); - $variant->inventoryTracked = $originalInventoryTracked; - } - - /** - * @return array[] - */ - public function setStockLevelDataProvider(): array - { - return [ - 'simple-single-arg' => [ - [ - ['quantity' => 10], - ], - 'expected' => 10, - ], - 'set-and-adjust' => [ - [ - ['quantity' => 10], - ['quantity' => 2, 'updateAction' => InventoryUpdateQuantityType::ADJUST], - ], - 'expected' => 12, - ], - 'just-adjust' => [ - [ - ['quantity' => 2, 'updateAction' => InventoryUpdateQuantityType::ADJUST], - ], - 'expected' => 2, - ], - 'set-and-adjust-negative' => [ - [ - ['quantity' => 10], - ['quantity' => -2, 'updateAction' => InventoryUpdateQuantityType::ADJUST], - ], - 'expected' => 8, - ], - ]; - } -} diff --git a/tests/unit/services/LineItemsTest.php b/tests/unit/services/LineItemsTest.php deleted file mode 100644 index 013fb370fd..0000000000 --- a/tests/unit/services/LineItemsTest.php +++ /dev/null @@ -1,180 +0,0 @@ - - * @since 3.2.14 - */ -class LineItemsTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var LineItems - */ - protected LineItems $service; - - /** - * @var OrdersFixture - */ - protected OrdersFixture $fixtureData; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - protected function _before(): void - { - parent::_before(); - - $this->service = Plugin::getInstance()->get('lineItems'); - $this->fixtureData = $this->tester->grabFixture('orders'); - } - - public function testGetAllLineItemsByOrderId(): void - { - $lineItems = $this->service->getAllLineItemsByOrderId(9999); - - self::assertIsArray($lineItems); - self::assertCount(0, $lineItems); - - $lineItems = $this->service->getAllLineItemsByOrderId($this->fixtureData->getElement('completed-new')['id']); - - self::assertIsArray($lineItems); - self::assertCount(2, $lineItems); - } - - public function testResolveLineItemExisting(): void - { - $order = new Order(); - $variant = Variant::find()->sku('hct-blue')->one(); - - $orderLineItem = $this->service->resolveLineItem($order, $variant->id, ['giftWrapped' => 'no']); - - $resolvedLineItem = $this->service->resolveLineItem($order, $variant->id, ['giftWrapped' => 'no']); - - self::assertInstanceOf(LineItem::class, $resolvedLineItem); - // Test that resolving line items without saving is consistent - self::assertEquals($orderLineItem->getPrice(), $resolvedLineItem->getPrice()); - self::assertEquals($orderLineItem->getSalePrice(), $resolvedLineItem->getSalePrice()); - self::assertEquals($orderLineItem->getOptionsSignature(), $resolvedLineItem->getOptionsSignature()); - self::assertEquals($orderLineItem->purchasableId, $resolvedLineItem->purchasableId); - self::assertEquals($orderLineItem->orderId, $resolvedLineItem->orderId); - } - - - public function testResolveLineItemExistingCompletedOrder(): void - { - // Resolving a line item on a completed order should return a brand-new line item - // even if the purchasable and options are the same - /** @var Order $order */ - $order = $this->fixtureData->getElement('completed-new'); - $orderLineItem = $order->getLineItems()[0]; - - $resolvedLineItem = $this->service->resolveLineItem($order, $orderLineItem->purchasableId, $orderLineItem->getOptions()); - - self::assertInstanceOf(LineItem::class, $resolvedLineItem); - // Test that resolving line items without saving is consistent - self::assertEquals($orderLineItem->getPrice(), $resolvedLineItem->getPrice()); - self::assertEquals($orderLineItem->getSalePrice(), $resolvedLineItem->getSalePrice()); - self::assertNotEquals($orderLineItem->getOptionsSignature(), $resolvedLineItem->getOptionsSignature()); - self::assertEquals($orderLineItem->purchasableId, $resolvedLineItem->purchasableId); - self::assertEquals($orderLineItem->orderId, $resolvedLineItem->orderId); - } - - public function testResolveLineItemNew(): void - { - /** @var Order $order */ - $order = $this->fixtureData->getElement('completed-new'); - $lineItem = $order->getLineItems()[1]; - $variant = Variant::find()->id($lineItem->purchasableId)->one(); - - $resolvedLineItem = $this->service->resolveLineItem($order, $lineItem->purchasableId, $lineItem->getOptions()); - - self::assertInstanceOf(LineItem::class, $resolvedLineItem); - self::assertEquals($variant->getPrice(), $resolvedLineItem->getPrice()); - } - - public function testResolveLineItemUnsavedOrder(): void - { - $order = new Order(); - $variant = Variant::find()->sku('hct-blue')->one(); - - $resolvedLineItem = $this->service->resolveLineItem($order, $variant->id, ['giftWrapped' => 'no']); - - self::assertInstanceOf(LineItem::class, $resolvedLineItem); - self::assertEquals($variant->getPrice(), $resolvedLineItem->getPrice()); - } - - public function testGetLineItemById(): void - { - $lineItems = $this->fixtureData->getElement('completed-new')->getLineItems(); - $lineItem = $this->service->getLineItemById($lineItems[0]->id); - - self::assertEquals($lineItems[0]->purchasableId, $lineItem->purchasableId); - self::assertEquals($lineItems[0]->qty, $lineItem->qty); - } - - public function testSnapshotUnpacking(): void - { - /** @var Order $order */ - $order = $this->fixtureData->getElement('completed-new'); - $lineItemById = $this->service->getLineItemById($order->getLineItems()[0]->id); - /** @var LineItem $lineItemFromAll */ - $lineItemFromAll = collect($this->service->getAllLineItemsByOrderId($order->id))->firstWhere('id', $lineItemById->id); - - self::assertIsArray($lineItemById->getSnapshot()); - self::assertIsArray($lineItemFromAll->getSnapshot()); - self::assertEquals($lineItemById->getSnapshot(), $lineItemFromAll->getSnapshot()); - } - - public function testCreateLineItem(): void - { - /** @var Order $order */ - $order = $this->fixtureData->getElement('completed-new'); - $lineItem = $order->getLineItems()[0]; - $qty = 4; - $note = 'My note'; - $lineItem = $this->service->create($order, [ - 'purchasableId' => $lineItem->purchasableId, - 'options' => $lineItem->options, - 'qty' => $qty, - 'note' => $note, - ]); - - self::assertInstanceOf(LineItem::class, $lineItem); - self::assertEquals($this->fixtureData->getElement('completed-new')->id, $lineItem->orderId); - self::assertEquals($lineItem->purchasableId, $lineItem->purchasableId); - self::assertEquals($lineItem->options, $lineItem->getOptions()); - self::assertEquals($qty, $lineItem->qty); - self::assertEquals($note, $lineItem->note); - } -} diff --git a/tests/unit/services/OrdersTest.php b/tests/unit/services/OrdersTest.php deleted file mode 100644 index 5b3e9a08f8..0000000000 --- a/tests/unit/services/OrdersTest.php +++ /dev/null @@ -1,112 +0,0 @@ - - * @since 3.2.14 - */ -class OrdersTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var Orders - */ - protected Orders $service; - - /** - * @var OrdersFixture - */ - protected OrdersFixture $fixtureData; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'customer' => [ - 'class' => CustomerFixture::class, - ], - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - protected function _before(): void - { - parent::_before(); - - $this->service = Plugin::getInstance()->getOrders(); - $this->fixtureData = $this->tester->grabFixture('orders'); - } - - public function testGetOrderById(): void - { - $order = $this->service->getOrderById($this->fixtureData->getElement('completed-new')->id); - - self::assertInstanceOf(Order::class, $order); - self::assertEquals($this->fixtureData->getElement('completed-new')->id, $order->id); - } - - public function testGetOrderByNumber(): void - { - $order = $this->service->getOrderByNumber($this->fixtureData->getElement('completed-new')->number); - - self::assertInstanceOf(Order::class, $order); - self::assertEquals($this->fixtureData->getElement('completed-new')->number, $order->number); - self::assertEquals($this->fixtureData->getElement('completed-new')->id, $order->id); - - $order = $this->service->getOrderByNumber('invalid'); - - self::assertNull($order); - } - - public function testGetOrdersByCustomer(): void - { - /** @var User $customer */ - $customer = $this->tester->grabFixture('customer')->getElement('customer1'); - $orders = $this->service->getOrdersByCustomer($customer->id); - - self::assertIsArray($orders); - self::assertCount(3, $orders); - foreach ($orders as $order) { - self::assertContains($order->id, [$this->fixtureData->getElement('completed-new')->id, $this->fixtureData->getElement('completed-new-past')->id, $this->fixtureData->getElement('completed-shipped')->id]); - } - } - - public function testGetOrdersByEmail(): void - { - /** @var Order $orderFixture */ - $orderFixture = $this->fixtureData->getElement('completed-new'); - $email = $orderFixture->getEmail(); - $orders = $this->service->getOrdersByEmail($email); - - self::assertIsArray($orders); - self::assertCount(3, $orders); - foreach ($orders as $order) { - self::assertEquals($email, $order->getEmail()); - } - } -} diff --git a/tests/unit/services/PaymentCurrenciesTest.php b/tests/unit/services/PaymentCurrenciesTest.php deleted file mode 100644 index 7988b7385b..0000000000 --- a/tests/unit/services/PaymentCurrenciesTest.php +++ /dev/null @@ -1,268 +0,0 @@ - - * @since 3.2.14 - */ -class PaymentCurrenciesTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var PaymentCurrencies $pc - */ - protected PaymentCurrencies $pc; - - /** - * @var PaymentCurrenciesFixture - */ - protected PaymentCurrenciesFixture $fixtureData; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'payment-currencies' => [ - 'class' => PaymentCurrenciesFixture::class, - ], - ]; - } - - /** - * - */ - protected function _before(): void - { - parent::_before(); - - $this->pc = Plugin::getInstance()->getPaymentCurrencies(); - $this->fixtureData = $this->tester->grabFixture('payment-currencies'); - } - - /** - * @throws CurrencyException - * @throws InvalidConfigException - * @group PaymentCurrencies - */ - public function testGetPaymentCurrenciesData(): void - { - $eurCurrencyModel = $this->pc->getPaymentCurrencyByIso('EUR'); - $audCurrencyModel = $this->pc->getPaymentCurrencyByIso('AUD'); - - // Install's USD, plus 2 additional currencies in fixture data. - self::assertCount(3, $this->pc->getAllPaymentCurrencies()); - - // $this->assertSame(1, $getAllCallCount, 'Test memoization of get all call.'); - self::assertNotNull($eurCurrencyModel); - self::assertEquals('EUR', $eurCurrencyModel->iso); - self::assertNotNull($audCurrencyModel); - self::assertEquals('AUD', $audCurrencyModel->iso); - - // Deafult install has a USD primary currency - $iso = $this->pc->getPrimaryPaymentCurrencyIso(); - self::assertNotNull($iso); - self::assertEquals('USD', $iso); - } - - /** - * @group PaymentCurrencies - */ - public function testConvert(): void - { - $eurCurrencyModel = $this->pc->getPaymentCurrencyByIso('EUR'); - $audCurrencyModel = $this->pc->getPaymentCurrencyByIso('AUD'); - - // Converting to the same base currency - $iso = $this->pc->getPrimaryPaymentCurrencyIso(); - $converted = $this->pc->convert(10, $iso); - self::assertEquals($converted, 10); - - // Converting to the EUR currency - $iso = $eurCurrencyModel->iso; - $converted = $this->pc->convert(10, $iso); - self::assertEquals($converted, 5); - - // Converting to the AUD currency - $iso = $audCurrencyModel->iso; - $converted = $this->pc->convert(10, $iso); - self::assertEquals($converted, 13); - } - - /** - * @group PaymentCurrencies - */ - public function testConvertCurrency(): void - { - $eurCurrencyModel = $this->pc->getPaymentCurrencyByIso('EUR'); - $audCurrencyModel = $this->pc->getPaymentCurrencyByIso('AUD'); - - // Converting from EUR to USD and back - $converted = $this->pc->convertCurrency(20, $eurCurrencyModel->iso, $this->pc->getPrimaryPaymentCurrencyIso()); - self::assertEquals($converted, 40); - $converted = $this->pc->convertCurrency(40, $this->pc->getPrimaryPaymentCurrencyIso(), $eurCurrencyModel->iso); - self::assertEquals($converted, 20); - - // Converting from AUD to USD and back - $converted = $this->pc->convertCurrency(13, $audCurrencyModel->iso, $this->pc->getPrimaryPaymentCurrencyIso()); - self::assertEquals($converted, 10); - $converted = $this->pc->convertCurrency(10, $this->pc->getPrimaryPaymentCurrencyIso(), $audCurrencyModel->iso); - self::assertEquals($converted, 13); - - // Converting from AUD to EUR and back - $converted = $this->pc->convertCurrency(13, $audCurrencyModel->iso, $eurCurrencyModel->iso); - self::assertEquals($converted, 5); - $converted = $this->pc->convertCurrency(5, $eurCurrencyModel->iso, $audCurrencyModel->iso); - self::assertEquals($converted, 13); - } - - /** - * @group PaymentCurrencies - */ - public function testConvertCurrencyException(): void - { - $this->expectException(CurrencyException::class); - $this->pc->convertCurrency(20, 'aaa', 'bbb'); - } - - /** - * @group PaymentCurrencies - */ - public function testConvertException(): void - { - $this->expectException(CurrencyException::class); - $this->pc->convert(20, 'aaa'); - } - - /** - * @group PaymentCurrencies - */ - public function testGetRateForReturnsRawRateWithoutHandler(): void - { - $eur = $this->pc->getPaymentCurrencyByIso('EUR'); - self::assertSame(0.5, $this->pc->getRateFor($eur)); - } - - /** - * @group PaymentCurrencies - */ - public function testGetRateForReturnsEventRate(): void - { - $handler = static function(PaymentCurrencyRateEvent $event) { - if ($event->paymentCurrency->iso === 'EUR') { - $event->rate = 0.25; - } - }; - Event::on(PaymentCurrencies::class, PaymentCurrencies::EVENT_DEFINE_PAYMENT_CURRENCY_RATE, $handler); - - try { - $eur = $this->pc->getPaymentCurrencyByIso('EUR'); - self::assertSame(0.25, $this->pc->getRateFor($eur)); - - $aud = $this->pc->getPaymentCurrencyByIso('AUD'); - self::assertSame(1.3, $this->pc->getRateFor($aud), 'Untouched currencies fall through to the raw rate.'); - } finally { - Event::off(PaymentCurrencies::class, PaymentCurrencies::EVENT_DEFINE_PAYMENT_CURRENCY_RATE, $handler); - } - } - - /** - * @group PaymentCurrencies - */ - public function testConvertCurrencyUsesEventRate(): void - { - $handler = static function(PaymentCurrencyRateEvent $event) { - if ($event->paymentCurrency->iso === 'EUR') { - $event->rate = 0.25; - } - }; - Event::on(PaymentCurrencies::class, PaymentCurrencies::EVENT_DEFINE_PAYMENT_CURRENCY_RATE, $handler); - - try { - $converted = $this->pc->convertCurrency(40, $this->pc->getPrimaryPaymentCurrencyIso(), 'EUR'); - self::assertSame(10.0, $converted); - } finally { - Event::off(PaymentCurrencies::class, PaymentCurrencies::EVENT_DEFINE_PAYMENT_CURRENCY_RATE, $handler); - } - } - - /** - * @group PaymentCurrencies - */ - public function testConvertAmountUsesEventRate(): void - { - $handler = static function(PaymentCurrencyRateEvent $event) { - if ($event->paymentCurrency->iso === 'EUR') { - $event->rate = 0.25; - } - }; - Event::on(PaymentCurrencies::class, PaymentCurrencies::EVENT_DEFINE_PAYMENT_CURRENCY_RATE, $handler); - - try { - $usd = new Money(4000, new Currency('USD')); - $converted = $this->pc->convertAmount($usd, 'EUR'); - self::assertSame('EUR', $converted->getCurrency()->getCode()); - self::assertSame('1000', $converted->getAmount()); - } finally { - Event::off(PaymentCurrencies::class, PaymentCurrencies::EVENT_DEFINE_PAYMENT_CURRENCY_RATE, $handler); - } - } - - /** - * The event must not affect the rate that gets persisted when saving a - * payment currency — saving isn't a conversion. - * - * @group PaymentCurrencies - */ - public function testSavePaymentCurrencyIgnoresEventRate(): void - { - $handler = static function(PaymentCurrencyRateEvent $event) { - $event->rate = 999.0; - }; - Event::on(PaymentCurrencies::class, PaymentCurrencies::EVENT_DEFINE_PAYMENT_CURRENCY_RATE, $handler); - - try { - $eur = $this->pc->getPaymentCurrencyByIso('EUR'); - $originalRate = $eur->rate; - $eur->rate = 0.75; - - self::assertTrue($this->pc->savePaymentCurrency($eur)); - - $record = PaymentCurrencyRecord::findOne(['id' => $eur->id]); - self::assertNotNull($record); - self::assertEquals(0.75, $record->rate, 'Raw admin-entered rate is persisted, not the event rate.'); - - // Restore for any tests that run after this one without isolation. - $eur->rate = $originalRate; - $this->pc->savePaymentCurrency($eur); - } finally { - Event::off(PaymentCurrencies::class, PaymentCurrencies::EVENT_DEFINE_PAYMENT_CURRENCY_RATE, $handler); - } - } -} diff --git a/tests/unit/services/PlansTest.php b/tests/unit/services/PlansTest.php deleted file mode 100644 index 2f4986e850..0000000000 --- a/tests/unit/services/PlansTest.php +++ /dev/null @@ -1,232 +0,0 @@ - - * @since 4.0.5 - */ -class PlansTest extends Unit -{ - /** - * @var UnitTester|UnitTesterActions - */ - protected UnitTester $tester; - - /** - * @var Plans - */ - protected Plans $service; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'plans' => [ - 'class' => SubscriptionPlansFixture::class, - ], - ]; - } - - /** - * @return void - */ - public function testGetAllPlans(): void - { - $plans = $this->service->getAllPlans(); - - self::assertCount(2, $plans); - self::assertEquals(['monthlySubscription', 'weeklySubscription'], ArrayHelper::getColumn($plans, 'handle', false)); - } - - /** - * @return void - */ - public function testGetAllEnabledPlans(): void - { - $plans = $this->service->getAllEnabledPlans(); - - self::assertCount(1, $plans); - self::assertEquals(['monthlySubscription'], ArrayHelper::getColumn($plans, 'handle', false)); - } - - /** - * @param int $gatewayId - * @param int $count - * @return void - * @dataProvider getPlansByGatewayIdDataProvider - */ - public function testGetPlansByGatewayId(int $gatewayId, int $count): void - { - $plans = $this->service->getPlansByGatewayId($gatewayId); - - self::assertCount($count, $plans); - } - - /** - * @return \int[][] - */ - public function getPlansByGatewayIdDataProvider(): array - { - return [ - 'dummy-gateway' => [1, 2], - 'non-existent-gateway' => [99, 0], - ]; - } - - /** - * @return void - */ - public function testGetPlanById(): void - { - /** @var Plan $monthlyPlan */ - $monthlyPlan = $this->tester->grabFixture('plans', 'monthly'); - $plan = $this->service->getPlanById($monthlyPlan->id); - - self::assertInstanceOf(Plan::class, $plan); - self::assertEquals($monthlyPlan->name, $plan->name); - } - - /** - * @return void - */ - public function testGetPlanByUid(): void - { - /** @var Plan $monthlyPlan */ - $monthlyPlan = $this->tester->grabFixture('plans', 'monthly'); - $plan = $this->service->getPlanByUid($monthlyPlan->uid); - - self::assertInstanceOf(Plan::class, $plan); - self::assertEquals($monthlyPlan->name, $plan->name); - } - - /** - * @return void - */ - public function testGetPlanByHandle(): void - { - $plan = $this->service->getPlanByHandle('monthlySubscription'); - self::assertEquals('Monthly Subscription', $plan->name); - - $plan = $this->service->getPlanByHandle('weeklySubscription'); - self::assertEquals('Weekly Subscription', $plan->name); - } - - /** - * @return void - */ - public function testGetPlanByReference(): void - { - $plan = $this->service->getPlanByReference('monthly_sub'); - self::assertEquals('Monthly Subscription', $plan->name); - - $plan = $this->service->getPlanByReference('weekly_sub'); - self::assertEquals('Weekly Subscription', $plan->name); - } - - /** - * @return void - * @throws InvalidConfigException - */ - public function testSavePlan(): void - { - $plan = $this->service->getPlanByHandle('monthlySubscription'); - - $plan->name .= ' foo'; - - $result = $this->service->savePlan($plan, false); - - self::assertTrue($result); - self::assertEquals('Monthly Subscription foo', $plan->name); - $dbRow = (new Query()) - ->from(Table::PLANS) - ->select(['id', 'name']) - ->where(['name' => 'Monthly Subscription foo']) - ->one(); - self::assertEquals('Monthly Subscription foo', $dbRow['name']); - self::assertEquals($plan->id, $dbRow['id']); - } - - - /** - * @return void - * @throws InvalidConfigException - */ - public function testArchivePlanById(): void - { - /** @var Plan $monthlyPlan */ - $monthlyPlan = $this->tester->grabFixture('plans', 'monthly'); - $result = $this->service->archivePlanById($monthlyPlan->id); - - self::assertTrue($result); - $dbRow = (new Query()) - ->from(Table::PLANS) - ->select(['id', 'name', 'isArchived']) - ->where(['isArchived' => true]) - ->andWhere(['id' => $monthlyPlan->id]) - ->one(); - self::assertEquals('Monthly Subscription', $dbRow['name']); - self::assertEquals($monthlyPlan->id, $dbRow['id']); - self::assertEquals(true, $dbRow['isArchived']); - - $allPlans = $this->service->getAllPlans(); - $allEnabledPlans = $this->service->getAllEnabledPlans(); - self::assertNull(ArrayHelper::firstWhere($allPlans, 'name', $monthlyPlan->name)); - self::assertNull(ArrayHelper::firstWhere($allEnabledPlans, 'name', $monthlyPlan->name)); - } - - /** - * @return void - * @throws Exception - */ - public function testReorderPlans(): void - { - $plans = ArrayHelper::getColumn($this->service->getAllPlans(), 'id', false); - - $result = $this->service->reorderPlans(array_reverse($plans)); - self::assertTrue($result); - $dbRows = (new Query()) - ->from(Table::PLANS) - ->select(['id', 'sortOrder']) - ->orderBy(['sortOrder' => SORT_ASC]) - ->all(); - $previousSortOrder = -1; - foreach (array_reverse($plans) as $key => $id) { - self::assertEquals($id, $dbRows[$key]['id']); - self::assertGreaterThan($previousSortOrder, $dbRows[$key]['sortOrder']); - $previousSortOrder = $dbRows[$key]['sortOrder']; - } - } - - /** - * - */ - public function _before(): void - { - parent::_before(); - - $this->service = Plugin::getInstance()->getPlans(); - } -} diff --git a/tests/unit/services/ProductPermissionTest.php b/tests/unit/services/ProductPermissionTest.php deleted file mode 100644 index 995919428b..0000000000 --- a/tests/unit/services/ProductPermissionTest.php +++ /dev/null @@ -1,241 +0,0 @@ - - * @since 3.1.4 - */ -class ProductPermissionTest extends Unit -{ - /** - * @var UnitTester - */ - protected $tester; - - public function testCanViewWithNoPermissions() - { - [$user, $product] = $this->_existingProduct(); - - $this->mockPermissions([]); - $this->assertFalse($product->canView($user)); - } - - public function testCanViewWithViewPermission() - { - [$user, $product] = $this->_existingProduct(); - - $this->mockPermissions(['commerce-viewproducttype:randomuid']); - $this->assertTrue($product->canView($user)); - } - - public function testCanViewWithWrongProductType() - { - [$user, $product] = $this->_existingProduct(); - - $this->mockPermissions(['commerce-viewproducttype:anotherrandomuid']); - $this->assertFalse($product->canView($user)); - } - - public function testCanViewWithOnlySavePermission() - { - [$user, $product] = $this->_existingProduct(); - - // Save without view should not grant view - $this->mockPermissions(['commerce-saveproducttype:randomuid']); - $this->assertFalse($product->canView($user)); - } - - public function testCanSaveExistingProductWithSavePermission() - { - [$user, $product] = $this->_existingProduct(); - - $this->mockPermissions(['commerce-viewproducttype:randomuid', 'commerce-saveproducttype:randomuid']); - $this->assertTrue($product->canSave($user)); - } - - public function testCannotSaveExistingProductWithViewOnly() - { - [$user, $product] = $this->_existingProduct(); - - $this->mockPermissions(['commerce-viewproducttype:randomuid']); - $this->assertFalse($product->canSave($user)); - } - - public function testCannotSaveExistingProductWithCreatePermission() - { - [$user, $product] = $this->_existingProduct(); - - // Create permission does not grant save on existing products - $this->mockPermissions(['commerce-viewproducttype:randomuid', 'commerce-createproducttype:randomuid']); - $this->assertFalse($product->canSave($user)); - } - - public function testCanSaveNewProductWithCreatePermission() - { - [$user, $product] = $this->_newProduct(); - - $this->mockPermissions(['commerce-viewproducttype:randomuid', 'commerce-createproducttype:randomuid']); - $this->assertTrue($product->canSave($user)); - } - - public function testCannotSaveNewProductWithViewOnly() - { - [$user, $product] = $this->_newProduct(); - - $this->mockPermissions(['commerce-viewproducttype:randomuid']); - $this->assertFalse($product->canSave($user)); - } - - public function testCannotSaveNewProductWithSavePermission() - { - [$user, $product] = $this->_newProduct(); - - // Save permission does not grant create on new products - $this->mockPermissions(['commerce-viewproducttype:randomuid', 'commerce-saveproducttype:randomuid']); - $this->assertFalse($product->canSave($user)); - } - - public function testCanDeleteWithDeletePermission() - { - [$user, $product] = $this->_existingProduct(); - - $this->mockPermissions(['commerce-viewproducttype:randomuid', 'commerce-deleteproducttype:randomuid']); - $this->assertTrue($product->canDelete($user)); - } - - public function testCannotDeleteWithViewOnly() - { - [$user, $product] = $this->_existingProduct(); - - $this->mockPermissions(['commerce-viewproducttype:randomuid']); - $this->assertFalse($product->canDelete($user)); - } - - public function testCannotDeleteWithSavePermission() - { - [$user, $product] = $this->_existingProduct(); - - // Save permission does not grant delete - $this->mockPermissions(['commerce-viewproducttype:randomuid', 'commerce-saveproducttype:randomuid']); - $this->assertFalse($product->canDelete($user)); - } - - public function testCanDuplicateWithCreateAndSave() - { - [$user, $product] = $this->_existingProduct(); - - $this->mockPermissions([ - 'commerce-viewproducttype:randomuid', - 'commerce-createproducttype:randomuid', - 'commerce-saveproducttype:randomuid', - ]); - $this->assertTrue($product->canDuplicate($user)); - } - - public function testCannotDuplicateWithCreateOnly() - { - [$user, $product] = $this->_existingProduct(); - - $this->mockPermissions(['commerce-viewproducttype:randomuid', 'commerce-createproducttype:randomuid']); - $this->assertFalse($product->canDuplicate($user)); - } - - public function testCannotDuplicateWithSaveOnly() - { - [$user, $product] = $this->_existingProduct(); - - $this->mockPermissions(['commerce-viewproducttype:randomuid', 'commerce-saveproducttype:randomuid']); - $this->assertFalse($product->canDuplicate($user)); - } - - public function testCanCreateDraftsAlwaysReturnsTrue() - { - [$user, $product] = $this->_existingProduct(); - - $this->mockPermissions([]); - $this->assertTrue($product->canCreateDrafts($user)); - } - - public function testAdminBypassesAllPermissions() - { - $user = new User(); - $user->id = 1; - $user->admin = true; - - $product = $this->make(Product::class, [ - 'id' => 100, - 'getType' => $this->_makeProductType(), - ]); - - $this->mockPermissions([]); - $this->assertTrue($product->canView($user)); - $this->assertTrue($product->canSave($user)); - $this->assertTrue($product->canDelete($user)); - $this->assertTrue($product->canDuplicate($user)); - } - - /** - * @return array{User, Product} - */ - private function _existingProduct(): array - { - $user = new User(); - $user->id = 1; - $user->admin = false; - - $product = $this->make(Product::class, [ - 'id' => 100, - 'getType' => $this->_makeProductType(), - ]); - - return [$user, $product]; - } - - /** - * @return array{User, Product} - */ - private function _newProduct(): array - { - $user = new User(); - $user->id = 1; - $user->admin = false; - - $product = $this->make(Product::class, [ - 'getType' => $this->_makeProductType(), - ]); - - return [$user, $product]; - } - - private function _makeProductType(): ProductType - { - return $this->make(ProductType::class, ['id' => 1, 'uid' => 'randomuid']); - } - - private function mockPermissions(array $permissions = []): void - { - $this->tester->mockMethods( - Craft::$app, - 'userPermissions', - [ - 'getPermissionsByUserId' => fn() => $permissions, - ], - [] - ); - } -} diff --git a/tests/unit/services/SalesTest.php b/tests/unit/services/SalesTest.php deleted file mode 100644 index 88123798f3..0000000000 --- a/tests/unit/services/SalesTest.php +++ /dev/null @@ -1,245 +0,0 @@ - - * @since 3.1.4 - */ -class SalesTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var Sales $sales - */ - protected Sales $sales; - - /** - * @var SalesFixture - */ - protected SalesFixture $salesData; - - /** - * @var string|null - */ - private ?string $_originalEdition = null; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'customer' => [ - 'class' => CustomerFixture::class, - ], - 'sales' => [ - 'class' => SalesFixture::class, - ], - ]; - } - - /* - * - */ - protected function _before(): void - { - parent::_before(); - - $this->_originalEdition = Craft::$app->getEdition(); - Craft::$app->setEdition(Craft::Pro); - $this->sales = Plugin::getInstance()->getSales(); - $this->salesData = $this->tester->grabFixture('sales'); - } - - /** - * - */ - protected function _after() - { - parent::_after(); - - Craft::$app->setEdition($this->_originalEdition); - $this->_originalEdition = null; - } - - /** - * - */ - public function testGetAllSales(): void - { - $sales = $this->sales->getAllSales(); - self::assertCount(2, $sales); - - /** @var Sale $firstSale */ - $firstSale = $sales[$this->salesData->data['percentageSale']['id']] ?? null; - self::assertNotNull($firstSale); - self::assertSame($this->salesData->data['percentageSale']['name'], $firstSale->name); - - $variant = Variant::find()->sku('rad-hood')->one(); - self::assertSame([(int)$variant->id], array_map('intval', $firstSale->getPurchasableIds())); - self::assertSame([], $firstSale->getUserGroupIds()); - self::assertSame([], $firstSale->getCategoryIds()); - } - - /** - * - */ - public function testGetSaleById(): void - { - $sale = $this->sales->getSaleById($this->salesData['percentageSale']['id']); - self::assertSame($this->salesData['percentageSale']['name'], $sale->name); - - $noSale = $this->sales->getSaleById(999); - self::assertNull($noSale); - } - - /** - * - */ - public function testGetSalesForPurchasable(): void - { - $variant = Variant::find()->sku('rad-hood')->one(); - $sale = $this->sales->getSaleById($this->salesData['percentageSale']['id']); - - self::assertSame([$sale], $this->sales->getSalesForPurchasable($variant)); - } - - /** - * - */ - public function testGetSalesRelatedToPurchasable(): void - { - $variant = Variant::find()->sku('hct-white')->one(); - $sale = $this->sales->getSaleById($this->salesData['allRelationships']['id']); - - self::assertSame([$sale], $this->sales->getSalesRelatedToPurchasable($variant)); - } - - /** - * @throws InvalidConfigException - */ - public function testGetSalePriceForPurchasable(): void - { - $originalIdentity = Craft::$app->getUser()->getIdentity(); - Craft::$app->getUser()->setIdentity( - $this->tester->grabFixture('customer')->getElement('customer1') - ); - Craft::$app->getUser()->getIdentity()->password = '$2y$13$tAtJfYFSRrnOkIbkruGGEu7TPh0Ixvxq0r.XgWqIgNWuWpxpA7SxK'; - $variant = Variant::find()->sku('rad-hood')->one(); - $salePrice = $this->sales->getSalePriceForPurchasable($variant); - - self::assertNotSame($variant->getPrice(), $salePrice); - self::assertEquals(111.59, $salePrice); - - $variant = Variant::find()->sku('hct-white')->one(); - $salePrice = $this->sales->getSalePriceForPurchasable($variant); - - self::assertNotSame($variant->getPrice(), $salePrice); - self::assertEquals(15.99, $salePrice); - Craft::$app->getUser()->setIdentity($originalIdentity); - } - - /** - * @throws Exception - */ - public function testSaveSale(): void - { - $sale = $this->sales->getSaleById($this->salesData['allRelationships']['id']); - $originalName = $sale->name; - $originalDateUpdated = (new Query()) - ->select('dateUpdated') - ->from(Table::SALES) - ->where(['id' => $sale->id]) - ->scalar(); - $sale->name = 'CHANGED'; - - // Absolutely make sure enough time has passed - sleep(1); - $saveResult = $this->sales->saveSale($sale); - $newDateUpdated = (new Query()) - ->select('dateUpdated') - ->from(Table::SALES) - ->where(['id' => $sale->id]) - ->scalar(); - - self::assertFalse($sale->hasErrors()); - self::assertTrue($saveResult); - self::assertNotSame($originalName, $sale->name); - self::assertSame('CHANGED', $sale->name); - self::assertGreaterThan($originalDateUpdated, $newDateUpdated); - } - - /** - * - */ - public function testReorderSales(): void - { - $sales = $this->sales->getAllSales(); - $originalOrder = ArrayHelper::getColumn($sales, 'id', false); - $newOrder = array_reverse($originalOrder); - - $reorderResult = $this->sales->reorderSales($newOrder); - - self::assertTrue($reorderResult, 'Reorder sales completed'); - - $dbOrder = (new Query()) - ->select(['id']) - ->from(Table::SALES) - ->orderBy('sortOrder asc') - ->all(); - $dbOrder = ArrayHelper::getColumn($dbOrder, 'id', false); - self::assertNotEquals($originalOrder, $dbOrder); - self::assertEquals($newOrder, $dbOrder); - - // Make sure the order has updated if we retrieve the sales again in the same request - $sales = $this->sales->getAllSales(); - $newOrderFromGetSales = ArrayHelper::getColumn($sales, 'id', false); - self::assertEquals($newOrderFromGetSales, $dbOrder); - } - - /** - * @throws Throwable - * @throws StaleObjectException - */ - public function testDeleteSaleById(): void - { - // Pre-get sales to test the memoization - /** @noinspection PhpUnusedLocalVariableInspection */ - $originalSales = $this->sales->getAllSales(); - $id = $this->salesData['percentageSale']['id']; - $deleteResult = $this->sales->deleteSaleById($id); - - self::assertTrue($deleteResult); - self::assertNull($this->sales->getSaleById($id)); - self::assertFalse(array_key_exists($id, $this->sales->getAllSales())); - } -} diff --git a/tests/unit/services/ShippingCategoryTest.php b/tests/unit/services/ShippingCategoryTest.php deleted file mode 100644 index 21124d663e..0000000000 --- a/tests/unit/services/ShippingCategoryTest.php +++ /dev/null @@ -1,77 +0,0 @@ - [ - 'class' => ProductFixture::class, - ], - ]; - } - - public function _before() - { - parent::_before(); - - $this->shippingCategories = Plugin::getInstance()->getShippingCategories(); - } - - public function testDeleteShippingCategory() - { - // Get the non-default shipping category from fixtures (anotherShippingCategory) - $shippingCategory = ShippingCategory::find() - ->where(['handle' => 'anotherShippingCategory']) - ->one(); - - $this->assertNotNull($shippingCategory, 'anotherShippingCategory fixture should exist'); - $this->assertFalse((bool)$shippingCategory->default, 'Test shipping category should not be default'); - - $shippingCategoryId = $shippingCategory->id; - - $result = $this->shippingCategories->deleteShippingCategoryById($shippingCategoryId); - - $this->assertTrue($result); - - $shippingCategory = ShippingCategory::findOne($shippingCategoryId); - - $this->assertNull($shippingCategory); - - $shippingCategory = ShippingCategory::findTrashed()->where(['id' => $shippingCategoryId])->one(); - - $this->assertInstanceOf(ShippingCategory::class, $shippingCategory); - - // Return shipping category to normal - Db::update(Table::SHIPPINGCATEGORIES, ['dateDeleted' => null], ['id' => $shippingCategoryId]); - } -} diff --git a/tests/unit/services/ShippingMethodsTest.php b/tests/unit/services/ShippingMethodsTest.php deleted file mode 100644 index f139213c1c..0000000000 --- a/tests/unit/services/ShippingMethodsTest.php +++ /dev/null @@ -1,77 +0,0 @@ - - */ -class ShippingMethodsTest extends Unit -{ - /** - * @var UnitTester - */ - protected $tester; - - /** - * @var ShippingMethods - */ - protected $shippingMethods; - - - public function _before() - { - parent::_before(); - - $this->shippingMethods = Plugin::getInstance()->getShippingMethods(); - } - - public function testGetMatchingShippingMethods(): void - { - $order = new Order(); - - Event::on(ShippingMethods::class, ShippingMethods::EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS, function(RegisterAvailableShippingMethodsEvent $event) { - $shippingMethods = $event->getShippingMethods(); - - $shippingMethods->push($this->make(ShippingMethod::class, [ - 'name' => 'First', - 'handle' => 'first', - 'getPriceForOrder' => 12.34, - 'getIsEnabled' => true, - 'matchOrder' => true, - ])); - $shippingMethods->push($this->make(ShippingMethod::class, [ - 'name' => 'Second', - 'handle' => 'second', - 'getPriceForOrder' => 12.35, - 'getIsEnabled' => true, - 'matchOrder' => true, - ])); - $shippingMethods->push($this->make(ShippingMethod::class, [ - 'name' => 'Really First', - 'handle' => 'reallyFirst', - 'getPriceForOrder' => 12.33, - 'getIsEnabled' => true, - 'matchOrder' => true, - ])); - }); - - $matchingMethods = $this->shippingMethods->getMatchingShippingMethods($order); - - self::assertEquals(['reallyFirst', 'first', 'second'], array_keys($matchingMethods)); - } -} diff --git a/tests/unit/services/ShippingRulesTest.php b/tests/unit/services/ShippingRulesTest.php deleted file mode 100644 index 197f3ba5ac..0000000000 --- a/tests/unit/services/ShippingRulesTest.php +++ /dev/null @@ -1,135 +0,0 @@ - - */ -class ShippingRulesTest extends Unit -{ - /** - * @var UnitTester - */ - protected $tester; - - /** - * @var ShippingRules - */ - protected ShippingRules $shippingRules; - - /** - * @var ShippingRuleCategories - */ - protected ShippingRuleCategories $shippingRuleCategories; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'shipping' => [ - 'class' => ShippingFixture::class, - ], - ]; - } - - public function _before(): void - { - parent::_before(); - - $this->shippingRules = Plugin::getInstance()->getShippingRules(); - $this->shippingRuleCategories = Plugin::getInstance()->getShippingRuleCategories(); - } - - public function testGetShippingRuleCategoriesByRuleIds(): void - { - $allRules = $this->shippingRules->getAllShippingRules(); - $ruleIds = $allRules->pluck('id')->filter()->all(); - - // Skip if no rules exist - if (empty($ruleIds)) { - $this->markTestSkipped('No shipping rules exist to test'); - } - - $categoriesByRuleId = $this->shippingRuleCategories->getShippingRuleCategoriesByRuleIds($ruleIds); - - // Result should be an array indexed by rule ID - $this->assertIsArray($categoriesByRuleId); - - // Each rule that has categories should have them indexed by category ID - foreach ($categoriesByRuleId as $ruleId => $categories) { - $this->assertContains($ruleId, $ruleIds, 'Rule ID in result should be one of the requested IDs'); - $this->assertIsArray($categories); - } - } - - public function testGetShippingRuleCategoriesByRuleIdsWithEmptyArray(): void - { - $result = $this->shippingRuleCategories->getShippingRuleCategoriesByRuleIds([]); - - $this->assertIsArray($result); - $this->assertEmpty($result); - } - - public function testEagerLoadingPopulatesShippingRuleCategories(): void - { - // Get all shipping rules - this should eager load categories - $allRules = $this->shippingRules->getAllShippingRules(); - - // Skip if no rules exist - if ($allRules->isEmpty()) { - $this->markTestSkipped('No shipping rules exist to test'); - } - - // Each rule should have its categories already loaded (not null) - // We verify this by checking that getShippingRuleCategories returns - // without triggering additional queries - foreach ($allRules as $rule) { - $categories = $rule->getShippingRuleCategories(); - $this->assertIsArray($categories); - } - } - - public function testBulkFetchMatchesSingleFetch(): void - { - $allRules = $this->shippingRules->getAllShippingRules(); - - // Skip if no rules exist - if ($allRules->isEmpty()) { - $this->markTestSkipped('No shipping rules exist to test'); - } - - $ruleIds = $allRules->pluck('id')->filter()->all(); - - // Fetch all categories in bulk - $bulkCategories = $this->shippingRuleCategories->getShippingRuleCategoriesByRuleIds($ruleIds); - - // Fetch categories one by one and compare - foreach ($ruleIds as $ruleId) { - $singleCategories = $this->shippingRuleCategories->getShippingRuleCategoriesByRuleId($ruleId); - $bulkForRule = $bulkCategories[$ruleId] ?? []; - - // Both should have the same category IDs - $this->assertEquals( - array_keys($singleCategories), - array_keys($bulkForRule), - "Categories for rule $ruleId should match between bulk and single fetch" - ); - } - } -} diff --git a/tests/unit/services/StoreTest.php b/tests/unit/services/StoreTest.php deleted file mode 100644 index e4479a4d27..0000000000 --- a/tests/unit/services/StoreTest.php +++ /dev/null @@ -1,64 +0,0 @@ - - * @since 4.0 - */ -class StoreTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var Store - */ - protected Stores $service; - - public function testGetStore(): void - { - $store = $this->service->getPrimaryStore(); - - self::assertInstanceOf(Address::class, $store->getSettings()->getLocationAddress()); - self::assertEquals('US', $store->getSettings()->getLocationAddress()->countryCode); - self::assertEquals('Store', $store->getSettings()->getLocationAddress()->title); - } - - public function testGetAllEnabledCountriesAsList(): void - { - $store = $this->service->getPrimaryStore(); - $store->getSettings()->setCountries(['US', 'AU', 'PH', 'GB']); - $countriesAsList = $store->getSettings()->getCountriesList(); - - self::assertIsArray($countriesAsList); - self::assertArrayHasKey('US', $countriesAsList); - self::assertSame('United States', $countriesAsList['US']); - self::assertCount(4, $countriesAsList); - } - - /** - * - */ - public function _before(): void - { - parent::_before(); - - $this->service = Plugin::getInstance()->getStores(); - } -} diff --git a/tests/unit/services/StoresTest.php b/tests/unit/services/StoresTest.php deleted file mode 100644 index 4aa9415a22..0000000000 --- a/tests/unit/services/StoresTest.php +++ /dev/null @@ -1,171 +0,0 @@ - - * @since 5.0.0 - */ -class StoresTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var Stores - */ - protected Stores $service; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'stores' => [ - 'class' => StoreFixture::class, - ], - ]; - } - - /** - * @return void - */ - public function testGetAllStores(): void - { - $stores = $this->service->getAllStores(); - - self::assertCount(3, $stores); - self::assertInstanceOf(Collection::class, $stores); - self::assertEquals('primary', $stores->firstWhere('primary', true)->handle); - self::assertCount(2, $stores->where('primary', false)->all()); - } - - /** - * @param int $siteId - * @param string|null $storeHandle - * @return void - * @dataProvider getStoreBySiteIdDataProvider - */ - public function testGetStoreBySiteId(int $siteId, ?string $storeHandle): void - { - $store = $this->service->getStoreBySiteId($siteId); - - if ($storeHandle === null) { - self::assertNull($store); - } else { - self::assertEquals($storeHandle, $store->handle); - } - } - - /** - * @return array[] - */ - public function getStoreBySiteIdDataProvider(): array - { - return [ - 'us' => [1000, 'primary'], - 'nl' => [1001, 'euStore'], - 'uk' => [1002, 'ukStore'], - 'nonExistent' => [1003, null], - ]; - } - - /** - * While a project config apply (e.g. `craft up`) is in progress, the incoming sitestores - * config is responsible for creating the mapping via handleChangedSiteStore(). If - * afterSaveCraftSiteHandler() also created one here, it would assign the wrong (primary) - * store and trigger an unwanted project config write. - * - * @return void - */ - public function testAfterSaveCraftSiteHandlerSkipsWhileApplyingExternalChanges(): void - { - $site = Craft::$app->getSites()->getSiteById(1002); - - // Remove the fixture's existing mapping so the handler would normally recreate one. - SiteStoreRecord::deleteAll(['siteId' => $site->id]); - - $this->_withApplyingExternalChanges(true, function() use ($site) { - $this->service->afterSaveCraftSiteHandler(new SiteEvent(['site' => $site])); - }); - - self::assertNull(SiteStoreRecord::findOne(['siteId' => $site->id])); - } - - /** - * Outside of a project config apply, the handler should still create a mapping to the - * primary store for a site that doesn't have one yet. - * - * @return void - */ - public function testAfterSaveCraftSiteHandlerCreatesMappingOutsideOfApply(): void - { - $site = Craft::$app->getSites()->getSiteById(1002); - - SiteStoreRecord::deleteAll(['siteId' => $site->id]); - - $this->_withApplyingExternalChanges(false, function() use ($site) { - $this->service->afterSaveCraftSiteHandler(new SiteEvent(['site' => $site])); - }); - - $siteStore = SiteStoreRecord::findOne(['siteId' => $site->id]); - self::assertNotNull($siteStore); - self::assertEquals($this->service->getPrimaryStore()->id, $siteStore->storeId); - } - - /** - * Runs $callback with ProjectConfig::getIsApplyingExternalChanges() forced to $isApplying, - * restoring its original value afterwards. - * - * @param bool $isApplying - * @param callable $callback - * @return void - */ - private function _withApplyingExternalChanges(bool $isApplying, callable $callback): void - { - $projectConfig = Craft::$app->getProjectConfig(); - $prop = (new ReflectionClass(ProjectConfig::class))->getProperty('_applyingExternalChanges'); - $prop->setAccessible(true); - $originalValue = $prop->getValue($projectConfig); - - $prop->setValue($projectConfig, $isApplying); - - try { - $callback(); - } finally { - $prop->setValue($projectConfig, $originalValue); - } - } - - /** - * - */ - public function _before(): void - { - parent::_before(); - - $this->service = Plugin::getInstance()->getStores(); - } -} diff --git a/tests/unit/services/TaxCategoryTest.php b/tests/unit/services/TaxCategoryTest.php deleted file mode 100644 index 126bcf06b2..0000000000 --- a/tests/unit/services/TaxCategoryTest.php +++ /dev/null @@ -1,73 +0,0 @@ - [ - 'class' => ProductFixture::class, - ], - ]; - } - - public function _before() - { - parent::_before(); - - $this->taxCategories = Plugin::getInstance()->getTaxCategories(); - } - - public function testDeleteTaxCategory() - { - $product = Product::find()->where(['slug' => 'rad-hoodie'])->one(); - - $variant = $product->getVariants()->first(); - $taxCategoryId = $variant->getTaxCategory()->id; - - $result = $this->taxCategories->deleteTaxCategoryById($taxCategoryId); - - $this->assertTrue($result); - - $taxCategory = TaxCategory::findOne($taxCategoryId); - - $this->assertNull($taxCategory); - - $taxCategory = TaxCategory::findTrashed()->where(['id' => $taxCategoryId])->one(); - - $this->assertInstanceOf(TaxCategory::class, $taxCategory); - - // Return tax category to normal - Db::update(Table::TAXCATEGORIES, ['dateDeleted' => null], ['id' => $taxCategoryId]); - } -} diff --git a/tests/unit/services/UserGroupConditionDiscountTest.php b/tests/unit/services/UserGroupConditionDiscountTest.php deleted file mode 100644 index c1219afc81..0000000000 --- a/tests/unit/services/UserGroupConditionDiscountTest.php +++ /dev/null @@ -1,145 +0,0 @@ - - * @author Global Network Group | Giel Tettelaar - * @since 2.1 - */ -class UserGroupConditionDiscountTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * - */ - protected function _before(): void - { - parent::_before(); - } - -// public function testIsUserGroupsConditionAnyOrNoneValid(): void -// { -// $this->_mockCustomers(); -// -// $mockDiscount = $this->_getMockDiscount([3, 4]); -// -// $discountAdjuster = new Discounts(); -// -// $mockDiscount->userGroupsCondition = DiscountRecord::CONDITION_USER_GROUPS_ANY_OR_NONE; -// $isValid = $discountAdjuster->isDiscountUserGroupValid($mockDiscount, new User()); -// self::assertTrue($isValid); -// } -// -// public function testIsUserGroupsConditionIncludeAllValid(): void -// { -// $discountAdjuster = new Discounts(); -// $this->_mockCustomers(); -// -// $mockDiscount = $this->_getMockDiscount([3, 4]); -// -// -// $mockDiscount->userGroupsCondition = DiscountRecord::CONDITION_USER_GROUPS_INCLUDE_ALL; -// $isValid = $discountAdjuster->isDiscountUserGroupValid($mockDiscount, new User()); -// self::assertFalse($isValid); -// -// $mockDiscount = $this->_getMockDiscount([2, 3]); -// -// $mockDiscount->userGroupsCondition = DiscountRecord::CONDITION_USER_GROUPS_INCLUDE_ALL; -// $isValid = $discountAdjuster->isDiscountUserGroupValid($mockDiscount, new User()); -// self::assertFalse($isValid); -// -// $this->_mockCustomers([2]); -// $mockDiscount = $this->_getMockDiscount([2, 1]); -// -// $mockDiscount->userGroupsCondition = DiscountRecord::CONDITION_USER_GROUPS_INCLUDE_ALL; -// $isValid = $discountAdjuster->isDiscountUserGroupValid($mockDiscount, new User()); -// self::assertFalse($isValid); -// } -// -// public function testUserGroupsConditionIncludeAnyValid(): void -// { -// $this->_mockCustomers(); -// -// $mockDiscount = $this->_getMockDiscount([2, 3]); -// -// $discountAdjuster = new Discounts(); -// -// $mockDiscount->userGroupsCondition = DiscountRecord::CONDITION_USER_GROUPS_INCLUDE_ANY; -// -// $isValid = $discountAdjuster->isDiscountUserGroupValid($mockDiscount, new User()); -// self::assertTrue($isValid); -// -// $mockDiscount = $this->_getMockDiscount([3, 4]); -// $mockDiscount->userGroupsCondition = DiscountRecord::CONDITION_USER_GROUPS_INCLUDE_ANY; -// $isValid = $discountAdjuster->isDiscountUserGroupValid($mockDiscount, new User()); -// self::assertFalse($isValid); -// } -// -// public function testIsUserGroupsConditionExcludeValid(): void -// { -// $discountAdjuster = new Discounts(); -// $this->_mockCustomers(); -// -// $mockDiscount = $this->_getMockDiscount([3, 4]); -// $mockDiscount->userGroupsCondition = DiscountRecord::CONDITION_USER_GROUPS_EXCLUDE; -// -// -// $isValid = $discountAdjuster->isDiscountUserGroupValid($mockDiscount, new User()); -// self::assertTrue($isValid); -// -// $mockDiscount = $this->_getMockDiscount([2, 4]); -// $mockDiscount->userGroupsCondition = DiscountRecord::CONDITION_USER_GROUPS_EXCLUDE; -// $isValid = $discountAdjuster->isDiscountUserGroupValid($mockDiscount, new User()); -// self::assertFalse($isValid); -// -// } - - /** - * @param array $ids - * @return void - */ - public function _mockCustomers(array $ids = [1, 2]): void - { - $mockCustomers = $this->make(Users::class, [ - 'getUserGroupIdsByUser' => $ids, - ]); - - Plugin::getInstance()->set('customers', $mockCustomers); - } - - /** - * @param array $ids - * @return Discount|mixed|MockObject - * @throws Exception - */ - public function _getMockDiscount(array $ids) - { - /** @var Discount $mockDiscount */ - return $this->make(Discount::class, [ - 'getUserGroupIds' => $ids, - ]); - } -} diff --git a/tests/unit/stats/AverageOrderTotalTest.php b/tests/unit/stats/AverageOrderTotalTest.php deleted file mode 100644 index 4366330d73..0000000000 --- a/tests/unit/stats/AverageOrderTotalTest.php +++ /dev/null @@ -1,84 +0,0 @@ - - * @since 3.3.2 - */ -class AverageOrderTotalTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @dataProvider getDataDataProvider - * - * @param string $dateRange - * @param DateTime $startDate - * @param DateTime $endDate - * @param float|null $average - */ - public function testGetData(string $dateRange, DateTime $startDate, DateTime $endDate, $average): void - { - $storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - $stat = new AverageOrderTotal($dateRange, $startDate, $endDate, $storeId); - $data = $stat->get(); - - if ($average === null) { - self::assertEquals($average, $data); - } else { - self::assertIsNumeric($data); - } - self::assertEquals($average, $data); - } - - /** - * @return array[] - */ - public function getDataDataProvider(): array - { - return [ - [ - AverageOrderTotal::DATE_RANGE_TODAY, - (new DateTime('now', new \DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('now', new \DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 63.97, - ], - [ - AverageOrderTotal::DATE_RANGE_CUSTOM, - (new DateTime('7 days ago', new \DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('5 days ago', new \DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - null, - ], - ]; - } -} diff --git a/tests/unit/stats/NewCustomersTest.php b/tests/unit/stats/NewCustomersTest.php deleted file mode 100644 index 82b7ea34ee..0000000000 --- a/tests/unit/stats/NewCustomersTest.php +++ /dev/null @@ -1,90 +0,0 @@ - - * @since 3.3.2 - */ -class NewCustomersTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @dataProvider getDataDataProvider - * - * @param string $dateRange - * @param DateTime $startDate - * @param DateTime $endDate - * @param float|null $count - * @throws \yii\base\Exception - */ - public function testGetData(string $dateRange, DateTime $startDate, DateTime $endDate, ?float $count): void - { - $storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - $stat = new NewCustomers($dateRange, $startDate, $endDate, $storeId); - $data = $stat->get(); - - self::assertIsNumeric($data); - self::assertEquals($count, $data); - } - - /** - * @return array[] - * @throws Exception - */ - public function getDataDataProvider(): array - { - return [ - [ - NewCustomers::DATE_RANGE_CUSTOM, - (new DateTime('2 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('0 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 1, - ], - [ - NewCustomers::DATE_RANGE_TODAY, - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 0, - ], - [ - NewCustomers::DATE_RANGE_CUSTOM, - (new DateTime('7 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('5 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 0, - ], - ]; - } -} diff --git a/tests/unit/stats/RepeatCustomersTest.php b/tests/unit/stats/RepeatCustomersTest.php deleted file mode 100644 index 1843cf8c6d..0000000000 --- a/tests/unit/stats/RepeatCustomersTest.php +++ /dev/null @@ -1,92 +0,0 @@ - - * @since 3.3.2 - */ -class RepeatCustomersTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @dataProvider getDataDataProvider - * - * @param string $dateRange - * @param DateTime $startDate - * @param DateTime $endDate - * @param int $total - * @param int $repeat - * @param int $percentage - * @throws \yii\base\Exception - */ - public function testGetData(string $dateRange, DateTime $startDate, DateTime $endDate, int $total, int $repeat, int $percentage): void - { - $storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - $stat = new RepeatCustomers($dateRange, $startDate, $endDate, $storeId); - $data = $stat->get(); - - self::assertIsArray($data); - self::assertEquals($total, $data['total']); - self::assertEquals($repeat, $data['repeat']); - self::assertEquals($percentage, $data['percentage']); - } - - /** - * @return array[] - * @throws Exception - */ - public function getDataDataProvider(): array - { - return [ - [ - RepeatCustomers::DATE_RANGE_TODAY, - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 1, - 1, - 100, - ], - [ - RepeatCustomers::DATE_RANGE_CUSTOM, - (new DateTime('7 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('5 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 0, - 0, - 0, - ], - ]; - } -} diff --git a/tests/unit/stats/StatTest.php b/tests/unit/stats/StatTest.php deleted file mode 100644 index a7a8bc6074..0000000000 --- a/tests/unit/stats/StatTest.php +++ /dev/null @@ -1,201 +0,0 @@ - - * @since 3.3.2 - */ -class StatTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @var DateTime - */ - protected DateTime $today; - - /** - * @var DateTime - */ - protected DateTime $yesterday; - - /** - * @dataProvider instantiateDatesDataProvider - * - * @param string $dateRange - * @param DateTime $startDate - * @param DateTime $endDate - * @throws Exception - */ - public function testInstantiateDates(string $dateRange, DateTime $startDate, DateTime $endDate): void - { - $storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - $stat = $this->_createStatClass($dateRange, $startDate, $endDate, $storeId); - - $data = $stat->get(); - - self::assertArrayHasKey($startDate->format('Y-m-d'), $data); - self::assertArrayHasKey($endDate->format('Y-m-d'), $data); - self::assertCount(2, $data); - } - - /** - * @dataProvider predefinedDateRangesDataProvider - * - * @param string $dateRange - * @param DateTime $startDate - * @param DateTime $endDate - * @param int $keysCount - * @param bool $keyedByDays - * @throws Exception - */ - public function testPredefinedDateRanges(string $dateRange, DateTime $startDate, DateTime $endDate, int $keysCount, bool $keyedByDays = true): void - { - $format = $keyedByDays ? 'Y-m-d' : 'Y-n'; - $storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - $stat = $this->_createStatClass($dateRange, $startDate, $endDate, $storeId); - - $data = $stat->get(); - - while ($startDate <= $endDate) { - self::assertArrayHasKey($startDate->format($format), $data); - - if ($keyedByDays) { - $startDate->add(new DateInterval('P1D')); - } else { - $startDate->add(new DateInterval('P1M')); - } - } - - self::assertCount($keysCount, $data); - } - - /** - * Create an anonymous stat class for testing generic features - * @param $range - * @param $start - * @param $end - * @return Stat - */ - private function _createStatClass($range, $start, $end, $storeId): Stat - { - return new class($range, $start, $end, $storeId) extends Stat { - // Prevent caching - public bool $cache = false; - - // Implement getData method - public function getData(): mixed - { - return $this->_createChartQuery(); - } - }; - } - - /** - * @before createDates - * - * @return array - * @throws \Exception - */ - public function instantiateDatesDataProvider(): array - { - // @TODO Source the timezone from the test Craft app instead of hardcoding it; data provider runs before the app is instantiated #COM-54 - $tz = new DateTimeZone('America/Los_Angeles'); - - return [ - [ - Stat::DATE_RANGE_CUSTOM, - (new DateTime('yesterday', $tz))->setTime(0, 0), - (new DateTime('now', $tz))->setTime(0, 0), - ], - ]; - } - - /** - * @before createDates - * - * @return array - * @throws \Exception - */ - public function predefinedDateRangesDataProvider(): array - { - - // @TODO Source the timezone from the test Craft app instead of hardcoding it; data provider runs before the app is instantiated. Consider storing `tz` in a class property set in a @before hook #COM-54 - - $tz = new DateTimeZone('America/Los_Angeles'); - $today = (new DateTime('now', $tz))->setTime(0, 0); - - return [ - Stat::DATE_RANGE_TODAY => [ - Stat::DATE_RANGE_TODAY, - clone $today, - clone $today, - 1, - ], - Stat::DATE_RANGE_PAST7DAYS => [ - Stat::DATE_RANGE_PAST7DAYS, - (new DateTime('6 days ago', $tz))->setTime(0, 0), - clone $today, - 7, - ], - Stat::DATE_RANGE_PAST30DAYS => [ - Stat::DATE_RANGE_PAST30DAYS, - (new DateTime('29 days ago', $tz))->setTime(0, 0), - clone $today, - 30, - ], - Stat::DATE_RANGE_PAST90DAYS => [ - Stat::DATE_RANGE_PAST90DAYS, - (new DateTime('89 days ago', $tz))->setTime(0, 0), - clone $today, - 90, - ], - Stat::DATE_RANGE_PASTYEAR => [ - Stat::DATE_RANGE_PASTYEAR, - (new DateTime('11 months ago', $tz))->setTime(0, 0), - clone $today, - 12, - false, - ], - Stat::DATE_RANGE_THISMONTH => [ - Stat::DATE_RANGE_THISMONTH, - (new DateTime('now', $tz))->setDate($today->format('Y'), $today->format('n'), 1)->setTime(0, 0), - clone $today, - (int)$today->format('t'), - ], - Stat::DATE_RANGE_THISWEEK => [ - Stat::DATE_RANGE_THISWEEK, - (new DateTime('Monday this week', $tz))->setTime(0, 0), - clone $today, - 7, - ], - Stat::DATE_RANGE_THISYEAR => [ - Stat::DATE_RANGE_THISYEAR, - (new DateTime('first day of January ' . $today->format('Y'), $tz))->setTime(0, 0), - clone $today, - (int)($today->diff((new DateTime('first day of January ' . $today->format('Y'), $tz))->setTime(0, 0))->format('%m')) + 1, - false, - ], - ]; - } -} diff --git a/tests/unit/stats/TopCustomersTest.php b/tests/unit/stats/TopCustomersTest.php deleted file mode 100644 index fecc866e8b..0000000000 --- a/tests/unit/stats/TopCustomersTest.php +++ /dev/null @@ -1,115 +0,0 @@ - - * @since 3.3.2 - */ -class TopCustomersTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @dataProvider getDataDataProvider - * - * @param string $dateRange - * @param string $type - * @param DateTime $startDate - * @param DateTime $endDate - * @param $customerData - * @throws \yii\base\Exception - */ - public function testGetData(string $dateRange, string $type, DateTime $startDate, DateTime $endDate, mixed $count, $customerData): void - { - $storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - $stat = new TopCustomers($dateRange, $type, $startDate, $endDate, $storeId); - $data = $stat->get(); - - self::assertIsArray($data); - self::assertCount($count, $data); - - if ($count !== 0) { - $topCustomer = array_shift($data); - - $testKeys = ['total', 'average', 'customerId', 'email', 'count', 'customer']; - foreach ($testKeys as $testKey) { - self::assertArrayHasKey($testKey, $topCustomer); - - if ($testKey === 'customer') { - self::assertInstanceOf(User::class, $topCustomer[$testKey]); - } else { - self::assertEquals($customerData()[$testKey], $topCustomer[$testKey]); - } - } - } - } - - /** - * @return array[] - * @throws Exception - */ - public function getDataDataProvider(): array - { - return [ - [ - TopCustomers::DATE_RANGE_TODAY, - 'total', - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 1, - function() { - $user = Craft::$app->getUsers()->getUserByUsernameOrEmail('customer1@crafttest.com'); - return [ - 'total' => 127.94, - 'average' => 63.97, - 'customerId' => $user->id, - 'email' => $user->email, - 'count' => 2, - ]; - }, - ], - [ - TopCustomers::DATE_RANGE_CUSTOM, - 'total', - (new DateTime('7 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('5 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 0, - [], - ], - ]; - } -} diff --git a/tests/unit/stats/TopProductTypesTest.php b/tests/unit/stats/TopProductTypesTest.php deleted file mode 100644 index c8c95e68f3..0000000000 --- a/tests/unit/stats/TopProductTypesTest.php +++ /dev/null @@ -1,127 +0,0 @@ - - * @since 3.3.2 - */ -class TopProductTypesTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @dataProvider getDataDataProvider - * - * @param string $dateRange - * @param string $type - * @param DateTime $startDate - * @param DateTime $endDate - * @param int $count - * @param array $productTypeData - * @throws \yii\base\Exception - */ - public function testGetData(string $dateRange, string $type, DateTime $startDate, DateTime $endDate, int $count, array $productTypeData): void - { - $storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - $this->_mockUser(); - $stat = new TopProductTypes($dateRange, $type, $startDate, $endDate, $storeId); - $data = $stat->get(); - - self::assertIsArray($data); - self::assertCount($count, $data); - - if ($count !== 0) { - $topProductType = array_shift($data); - - $testKeys = ['id', 'name', 'qty', 'revenue', 'productType']; - foreach ($testKeys as $testKey) { - self::assertArrayHasKey($testKey, $topProductType); - - if ($testKey === 'productType') { - self::assertInstanceOf(ProductType::class, $topProductType[$testKey]); - } else { - self::assertEquals($productTypeData[$testKey], $topProductType[$testKey]); - } - } - } - } - - /** - * @return array[] - * @throws Exception - */ - public function getDataDataProvider(): array - { - return [ - [ - TopProducts::DATE_RANGE_TODAY, - 'revenue', - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 1, - [ - 'id' => 2001, - 'name' => 'T-Shirts', - 'qty' => 6, - 'revenue' => 127.94, - ], - ], - [ - TopProducts::DATE_RANGE_CUSTOM, - 'revenue', - (new DateTime('7 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('5 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 0, - [], - ], - ]; - } - - public function _mockUser(): void - { - $user = new User(); - $user->id = 1; - $user->admin = true; - - $mockUser = $this->make(\craft\web\User::class, [ - 'getIdentity' => $user, - ]); - - \Craft::$app->set('user', $mockUser); - } -} diff --git a/tests/unit/stats/TopProductsTest.php b/tests/unit/stats/TopProductsTest.php deleted file mode 100644 index 5eec7798e6..0000000000 --- a/tests/unit/stats/TopProductsTest.php +++ /dev/null @@ -1,116 +0,0 @@ - - * @since 3.3.2 - */ -class TopProductsTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @dataProvider getDataDataProvider - * - * @param string $dateRange - * @param string $type - * @param DateTime $startDate - * @param DateTime $endDate - * @param int $count - * @param $productDataFunction - * @throws \yii\base\Exception - */ - public function testGetData(string $dateRange, string $type, DateTime $startDate, DateTime $endDate, int $count, $productDataFunction): void - { - $storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - $stat = new TopProducts($dateRange, $type, $startDate, $endDate, storeId: $storeId); - $data = $stat->get(); - - self::assertIsArray($data); - self::assertCount($count, $data); - - if ($count !== 0) { - $topProduct = array_shift($data); - $productData = $productDataFunction(); - - $testKeys = ['id', 'title', 'qty', 'revenue', 'product']; - foreach ($testKeys as $testKey) { - self::assertArrayHasKey($testKey, $topProduct); - - if ($testKey === 'product') { - self::assertInstanceOf(Product::class, $topProduct[$testKey]); - } else { - self::assertEquals($productData[$testKey], $topProduct[$testKey]); - } - } - } - } - - /** - * @return array[] - * @throws Exception - */ - public function getDataDataProvider(): array - { - return [ - [ - TopProducts::DATE_RANGE_TODAY, - 'revenue', - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 1, - function() { - $product = Product::find()->title('Hypercolor T-shirt')->one(); - - return [ - 'id' => $product->id, - 'title' => 'Hypercolor T-Shirt', - 'qty' => 6, - 'revenue' => 127.94, - ]; - }, - ], - [ - TopProducts::DATE_RANGE_CUSTOM, - 'revenue', - (new DateTime('7 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('5 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 0, - fn() => [], - ], - ]; - } -} diff --git a/tests/unit/stats/TopPurchasablesTest.php b/tests/unit/stats/TopPurchasablesTest.php deleted file mode 100644 index 2a4fc59a0b..0000000000 --- a/tests/unit/stats/TopPurchasablesTest.php +++ /dev/null @@ -1,120 +0,0 @@ - - * @since 3.3.2 - */ -class TopPurchasablesTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @dataProvider getDataDataProvider - * - * @param string $dateRange - * @param string $type - * @param DateTime $startDate - * @param DateTime $endDate - * @param int $count - * @param $getVariantData - * @throws \yii\base\Exception - */ - public function testGetData(string $dateRange, string $type, DateTime $startDate, DateTime $endDate, int $count, $getVariantData): void - { - $storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - Craft::$app->getUser()->setIdentity( - Craft::$app->getUsers()->getUserById('1') - ); - $stat = new TopPurchasables($dateRange, $type, $startDate, $endDate, $storeId); - $data = $stat->get(); - - self::assertIsArray($data); - self::assertCount($count, $data); - - if ($count !== 0) { - $topPurchasable = array_shift($data); - - $testKeys = ['purchasableId', 'description', 'sku', 'qty', 'revenue']; - $purchasableData = $getVariantData(Variant::find()); - foreach ($testKeys as $testKey) { - self::assertArrayHasKey($testKey, $topPurchasable); - - self::assertEquals($purchasableData[$testKey], $topPurchasable[$testKey], 'Assert ' . $testKey); - } - } - } - - /** - * @return array[] - * @throws Exception - */ - public function getDataDataProvider(): array - { - return [ - 'date-today' => [ - TopPurchasables::DATE_RANGE_TODAY, - 'revenue', - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 2, - function(VariantQuery $query) { - /** @var Purchasable $purchasable */ - $variant = $query->sku('hct-blue')->one(); - - return [ - 'purchasableId' => $variant->id ?? null, - 'description' => $variant ? $variant->getDescription() : null, - 'sku' => 'hct-blue', - 'qty' => 4, - 'revenue' => 87.96, - ]; - }, - ], - 'date-custom' => [ - TopPurchasables::DATE_RANGE_CUSTOM, - 'qty', - (new DateTime('7 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('5 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 0, - null, - ], - ]; - } -} diff --git a/tests/unit/stats/TotalOrdersByCountryTest.php b/tests/unit/stats/TotalOrdersByCountryTest.php deleted file mode 100644 index f58c6da695..0000000000 --- a/tests/unit/stats/TotalOrdersByCountryTest.php +++ /dev/null @@ -1,103 +0,0 @@ - - * @since 3.3.2 - */ -class TotalOrdersByCountryTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @dataProvider getDataDataProvider - * - * @param string $dateRange - * @param string $type - * @param DateTime $startDate - * @param DateTime $endDate - * @param int $count - * @param array $countryData - * @throws \yii\base\Exception - */ - public function testGetData(string $dateRange, string $type, DateTime $startDate, DateTime $endDate, int $count, array $countryData): void - { - $storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - $stat = new TotalOrdersByCountry($dateRange, $type, $startDate, $endDate, $storeId); - $data = $stat->get(); - - self::assertIsArray($data); - self::assertCount($count, $data); - - if ($count !== 0) { - $firstItem = array_shift($data); - - foreach ($countryData as $key => $countryDatum) { - self::assertArrayHasKey($key, $firstItem); - self::assertEquals($countryDatum, $firstItem[$key]); - } - } - } - - /** - * @return array[] - * @throws Exception - */ - public function getDataDataProvider(): array - { - return [ - [ - TotalOrdersByCountry::DATE_RANGE_TODAY, - 'shipping', - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 1, - [ - 'total' => 2, - 'name' => 'United States', - 'countryCode' => 'US', - ], - ], - [ - TotalOrdersByCountry::DATE_RANGE_CUSTOM, - 'shipping', - (new DateTime('7 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('5 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 0, - [], - ], - ]; - } -} diff --git a/tests/unit/stats/TotalOrdersTest.php b/tests/unit/stats/TotalOrdersTest.php deleted file mode 100644 index 820335d4a9..0000000000 --- a/tests/unit/stats/TotalOrdersTest.php +++ /dev/null @@ -1,105 +0,0 @@ - - * @since 3.3.2 - */ -class TotalOrdersTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @dataProvider getDataDataProvider - * - * @param string $dateRange - * @param DateTime $startDate - * @param DateTime $endDate - * @param int $total - * @param int $daysDiff - * @throws \yii\base\Exception - */ - public function testGetData(string $dateRange, DateTime $startDate, DateTime $endDate, int $total, int $daysDiff): void - { - $storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - $stat = new TotalOrders($dateRange, $startDate, $endDate, $storeId); - $data = $stat->get(); - - self::assertIsArray($data); - self::assertArrayHasKey('total', $data); - self::assertEquals($total, $data['total']); - self::assertArrayHasKey('chart', $data); - self::assertIsArray($data['chart']); - self::assertArrayHasKey($startDate->format('Y-m-d'), $data['chart']); - self::assertArrayHasKey($endDate->format('Y-m-d'), $data['chart']); - self::assertCount($daysDiff, $data['chart']); - - $firstItem = array_shift($data['chart']); - self::assertArrayHasKey('total', $firstItem); - self::assertArrayHasKey('datekey', $firstItem); - self::assertEquals($startDate->format('Y-m-d'), $firstItem['datekey']); - self::assertEquals($total, $firstItem['total']); - } - - protected function _before(): void - { - Craft::$app->setTimeZone('America/Los_Angeles'); - } - - /** - * @return array[] - * @throws Exception - */ - public function getDataDataProvider(): array - { - return [ - 'today' => [ - TotalOrders::DATE_RANGE_TODAY, - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 2, - 1, - ], - 'custom' => [ - TotalOrders::DATE_RANGE_CUSTOM, - (new DateTime('7 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('5 days ago', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 0, - 3, - ], - ]; - } -} diff --git a/tests/unit/stats/TotalRevenueTest.php b/tests/unit/stats/TotalRevenueTest.php deleted file mode 100644 index f4f8af7be3..0000000000 --- a/tests/unit/stats/TotalRevenueTest.php +++ /dev/null @@ -1,97 +0,0 @@ - - * @since 3.3.2 - */ -class TotalRevenueTest extends Unit -{ - /** - * @var UnitTester - */ - protected UnitTester $tester; - - /** - * @return array - */ - public function _fixtures(): array - { - return [ - 'orders' => [ - 'class' => OrdersFixture::class, - ], - ]; - } - - /** - * @dataProvider getDataDataProvider - * - * @param string $dateRange - * @param DateTime $startDate - * @param DateTime $endDate - * @param int $count - * @param $revenue - * @param string $type - * @throws \yii\base\Exception - */ - public function testGetData(string $dateRange, DateTime $startDate, DateTime $endDate, int $count, $revenue, string $type): void - { - $storeId = Plugin::getInstance()->getStores()->getPrimaryStore()->id; - $stat = new TotalRevenue($dateRange, $startDate, $endDate, $storeId); - $stat->type = $type; - $data = $stat->get(); - - self::assertIsArray($data); - - $todaysStats = array_pop($data); - self::assertArrayHasKey('count', $todaysStats); - self::assertArrayHasKey('revenue', $todaysStats); - self::assertArrayHasKey('datekey', $todaysStats); - self::assertEquals($count, $todaysStats['count']); - self::assertEquals($revenue, $todaysStats['revenue']); - } - - /** - * @return array[] - * @throws Exception - */ - public function getDataDataProvider(): array - { - return [ - [ - TotalRevenue::DATE_RANGE_TODAY, - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 2, - 127.94, - TotalRevenue::TYPE_TOTAL, - ], - [ - TotalRevenue::DATE_RANGE_TODAY, - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(0, 0), - 2, - 0, - TotalRevenue::TYPE_TOTAL_PAID, - ], - ]; - } -}