diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 4f085c8f..e90cf808 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -2,6 +2,7 @@ name: "build"
on:
push:
branches:
+ - "1.x"
- "0.*.x"
paths-ignore:
- "**/*.md"
@@ -17,7 +18,7 @@ env:
jobs:
coding-standards:
- name: "Coding Standards (PHP ${{ matrix.php-version }} | Dependency versions: ${{ matrix.dependencies }})"
+ name: "Coding Standards (PHP ${{ matrix.php-version }})"
runs-on: "ubuntu-latest"
@@ -26,9 +27,6 @@ jobs:
php-version:
- "8.1" # Always use the lowest version of PHP since a higher version would create actual syntax errors in lower versions
- dependencies:
- - "highest"
-
steps:
- name: "Checkout"
uses: "actions/checkout@v4"
@@ -43,7 +41,7 @@ jobs:
- name: "Install composer dependencies"
uses: "ramsey/composer-install@v3"
with:
- dependency-versions: "${{ matrix.dependencies }}"
+ dependency-versions: "highest"
- name: "Validate composer"
run: "composer validate --strict"
@@ -54,6 +52,9 @@ jobs:
- name: "Check style"
run: "composer check-style"
+ - name: "Run Rector"
+ run: "vendor/bin/rector --dry-run"
+
- name: "Lint yaml files"
run: "(cd tests/Application && bin/console lint:yaml ../../src/Resources)"
@@ -61,7 +62,7 @@ jobs:
run: "(cd tests/Application && bin/console lint:twig ../../src/Resources)"
dependency-analysis:
- name: "Dependency Analysis (PHP ${{ matrix.php-version }} | Dependency versions: ${{ matrix.dependencies }} | Symfony: ${{ matrix.symfony }})"
+ name: "Dependency Analysis (PHP ${{ matrix.php-version }} | Deps: ${{ matrix.dependencies }})"
runs-on: "ubuntu-latest"
@@ -69,16 +70,12 @@ jobs:
matrix:
php-version:
- "8.1"
- - "8.2"
+ - "8.3"
dependencies:
- "lowest"
- "highest"
- symfony:
- - "~5.4.0"
- - "~6.4.0"
-
steps:
- name: "Checkout"
uses: "actions/checkout@v4"
@@ -89,26 +86,24 @@ jobs:
coverage: "none"
extensions: "${{ env.PHP_EXTENSIONS }}"
php-version: "${{ matrix.php-version }}"
- tools: "composer-require-checker, composer-unused, flex"
+ tools: "flex"
- name: "Remove require-dev section in composer.json"
run: "composer config --unset require-dev"
-
+
+ - name: "Add composer-dependency-analyser"
+ run: "composer require --no-update --no-plugins --no-scripts --dev shipmonk/composer-dependency-analyser"
+
- name: "Install composer dependencies"
uses: "ramsey/composer-install@v3"
- env:
- SYMFONY_REQUIRE: "${{ matrix.symfony }}"
with:
dependency-versions: "${{ matrix.dependencies }}"
-
- - name: "Run maglnet/composer-require-checker"
- run: "composer-require-checker check --config-file=$(pwd)/composer-require-checker.json"
- - name: "Run composer-unused/composer-unused"
- run: "composer-unused"
+ - name: "Run composer-dependency-analyser"
+ run: "vendor/bin/composer-dependency-analyser"
static-code-analysis:
- name: "Static Code Analysis (PHP ${{ matrix.php-version }} | Dependency versions: ${{ matrix.dependencies }} | Symfony: ${{ matrix.symfony }})"
+ name: "Static Code Analysis (PHP ${{ matrix.php-version }} | Deps: ${{ matrix.dependencies }} | Sylius: ${{ matrix.sylius }})"
runs-on: "ubuntu-latest"
@@ -117,14 +112,14 @@ jobs:
php-version:
- "8.1"
- "8.2"
+ - "8.3"
dependencies:
- - "lowest"
- "highest"
- symfony:
- - "~5.4.0"
- - "~6.4.0"
+ sylius:
+ - "~1.13.0"
+ - "~1.14.0"
steps:
- name: "Checkout"
@@ -138,21 +133,19 @@ jobs:
php-version: "${{ matrix.php-version }}"
tools: "flex"
- - name: "Remove sylius/sylius from composer.json"
- run: "composer remove --dev --no-update --no-plugins --no-scripts sylius/sylius"
-
+ - name: "Require matching Sylius version"
+ run: "composer require --dev --no-update sylius/sylius:\"${{ matrix.sylius }}\""
+
- name: "Install composer dependencies"
uses: "ramsey/composer-install@v3"
- env:
- SYMFONY_REQUIRE: "${{ matrix.symfony }}"
with:
dependency-versions: "${{ matrix.dependencies }}"
- name: "Static analysis"
- run: "vendor/bin/psalm --php-version=${{ matrix.php-version }}"
+ run: "composer analyse"
unit-tests:
- name: "Unit tests (PHP ${{ matrix.php-version }} | Dependency versions: ${{ matrix.dependencies }} | Symfony: ${{ matrix.symfony }})"
+ name: "Unit tests (PHP ${{ matrix.php-version }} | Deps: ${{ matrix.dependencies }} | Sylius: ${{ matrix.sylius }})"
runs-on: "ubuntu-latest"
@@ -161,13 +154,14 @@ jobs:
php-version:
- "8.1"
- "8.2"
+ - "8.3"
dependencies:
- "highest"
- symfony:
- - "~5.4.0"
- - "~6.4.0"
+ sylius:
+ - "~1.13.0"
+ - "~1.14.0"
steps:
- name: "Checkout"
@@ -181,21 +175,19 @@ jobs:
php-version: "${{ matrix.php-version }}"
tools: "flex"
- - name: "Remove sylius/sylius from composer.json"
- run: "composer remove --dev --no-update --no-plugins --no-scripts sylius/sylius"
-
+ - name: "Require matching Sylius version"
+ run: "composer require --dev --no-update sylius/sylius:\"${{ matrix.sylius }}\""
+
- name: "Install composer dependencies"
uses: "ramsey/composer-install@v3"
- env:
- SYMFONY_REQUIRE: "${{ matrix.symfony }}"
with:
dependency-versions: "${{ matrix.dependencies }}"
- - name: "Run phpunit"
- run: "composer phpunit"
+ - name: "Run unit tests"
+ run: "vendor/bin/phpunit --testsuite unit"
- integration-tests:
- name: "Integration tests (PHP ${{ matrix.php-version }} | Dependency versions: ${{ matrix.dependencies }} | Symfony: ${{ matrix.symfony }})"
+ functional-tests:
+ name: "Functional tests (PHP ${{ matrix.php-version }} | Sylius: ${{ matrix.sylius }})"
runs-on: "ubuntu-latest"
@@ -203,13 +195,11 @@ jobs:
matrix:
php-version:
- "8.1"
- - "8.2"
+ - "8.3"
- dependencies:
- - "highest"
-
- symfony:
- - "~6.4"
+ sylius:
+ - "~1.13.0"
+ - "~1.14.0"
steps:
- name: "Start MySQL"
@@ -224,25 +214,18 @@ jobs:
coverage: "none"
extensions: "${{ env.PHP_EXTENSIONS }}"
php-version: "${{ matrix.php-version }}"
- tools: "flex,symfony"
+ tools: "flex"
- - name: "Setup node"
- uses: "actions/setup-node@v4"
- with:
- node-version: "18"
+ - name: "Require matching Sylius version"
+ run: "composer require --dev --no-update sylius/sylius:\"${{ matrix.sylius }}\""
- name: "Install composer dependencies"
uses: "ramsey/composer-install@v3"
- env:
- SYMFONY_REQUIRE: "${{ matrix.symfony }}"
with:
- dependency-versions: "${{ matrix.dependencies }}"
-
- - name: "Install wkhtmltopdf"
- run: "sudo apt-get install wkhtmltopdf"
+ dependency-versions: "highest"
- name: "Lint container"
- run: '(cd tests/Application && bin/console lint:container)'
+ run: "(cd tests/Application && bin/console lint:container)"
- name: "Create database"
run: "(cd tests/Application && bin/console doctrine:database:create)"
@@ -253,77 +236,23 @@ jobs:
- name: "Validate Doctrine mapping"
run: "(cd tests/Application && bin/console doctrine:schema:validate)"
- - name: "Get Yarn cache directory"
- id: "yarn-cache"
- run: "echo \"::set-output name=dir::$(yarn cache dir)\""
-
- - name: "Cache Yarn"
- uses: "actions/cache@v2"
- with:
- path: "${{ steps.yarn-cache.outputs.dir }}"
- key: "yarn-${{ hashFiles('**/package.json **/yarn.lock') }}"
- restore-keys: "yarn-"
-
- - name: "Install JS dependencies"
- run: "(cd tests/Application && yarn install)"
-
- - name: "Install assets"
- run: "(cd tests/Application && bin/console assets:install public -vvv)"
-
- - name: "Build assets"
- run: "(cd tests/Application && yarn build)"
-
- - name: "Output PHP version for Symfony CLI"
- run: "php -v | head -n 1 | awk '{ print $2 }' > .php-version"
-
- - name: "Install certificates"
- run: "symfony server:ca:install"
-
- - name: "Run Chrome Headless"
- run: "google-chrome-stable --enable-automation --disable-background-networking --no-default-browser-check --no-first-run --disable-popup-blocking --disable-default-apps --allow-insecure-localhost --disable-translate --disable-extensions --no-sandbox --enable-features=Metal --headless --remote-debugging-port=9222 --window-size=2880,1800 --proxy-server='direct://' --proxy-bypass-list='*' http://127.0.0.1 > /dev/null 2>&1 &"
-
- - name: "Wait for Chrome to start"
- run: |
- until curl -s http://127.0.0.1:9222/json/version | grep "Browser" > /dev/null 2>&1
- do
- sleep 1
- done
-
- - name: "Run webserver"
- run: "(cd tests/Application && symfony server:start --port=8080 --dir=public --daemon)"
-
- - name: "Wait for webserver to start"
- run: |
- until symfony server:list | grep /public > /dev/null 2>&1
- do
- sleep 1
- done
-
- - name: "Run behat"
- run: "vendor/bin/behat --colors --strict -vvv --no-interaction || vendor/bin/behat --colors --strict -vvv --no-interaction --rerun"
-
- - name: "Upload Behat logs"
- uses: "actions/upload-artifact@v4"
- if: "failure()"
- with:
- name: "Behat logs"
- path: "etc/build/"
- if-no-files-found: "ignore"
+ - name: "Run functional tests"
+ run: "vendor/bin/phpunit --testsuite functional"
code-coverage:
- name: "Code Coverage (PHP ${{ matrix.php-version }} | Dependency versions: ${{ matrix.dependencies }})"
+ name: "Code Coverage (PHP ${{ matrix.php-version }})"
runs-on: "ubuntu-latest"
strategy:
matrix:
php-version:
- - "8.2"
-
- dependencies:
- - "highest"
+ - "8.3"
steps:
+ - name: "Start MySQL"
+ run: "sudo /etc/init.d/mysql start"
+
- name: "Checkout"
uses: "actions/checkout@v4"
@@ -337,12 +266,18 @@ jobs:
- name: "Install composer dependencies"
uses: "ramsey/composer-install@v3"
with:
- dependency-versions: "${{ matrix.dependencies }}"
+ dependency-versions: "highest"
+
+ - name: "Create database"
+ run: "(cd tests/Application && bin/console doctrine:database:create)"
+
+ - name: "Create database schema"
+ run: "(cd tests/Application && bin/console doctrine:schema:create)"
- name: "Collect code coverage with pcov and phpunit/phpunit"
run: "vendor/bin/phpunit --coverage-clover=.build/logs/clover.xml"
- name: "Send code coverage report to Codecov.io"
+ uses: "codecov/codecov-action@v5"
env:
CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}"
- run: "bash <(curl -s https://codecov.io/bash)"
diff --git a/.gitignore b/.gitignore
index 82b272e5..2d912387 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,12 +2,10 @@
/node_modules/
/composer.lock
-/etc/build/*
-!/etc/build/.gitignore
-
-/tests/Application/yarn.lock
-
-/behat.yml
-/phpspec.yml
+/.build/
/.phpunit.result.cache
.php-version
+
+/tests/Application/yarn.lock
+/.playwright-mcp/
+/tests/Application/public/build/
diff --git a/.mcp.json b/.mcp.json
new file mode 100644
index 00000000..37cce59c
--- /dev/null
+++ b/.mcp.json
@@ -0,0 +1,12 @@
+{
+ "mcpServers": {
+ "playwright": {
+ "type": "stdio",
+ "command": "npx",
+ "args": [
+ "@playwright/mcp@latest"
+ ],
+ "env": {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index 17d78a86..ccba3099 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,55 +4,54 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project
-`setono/sylius-gift-card-plugin` — a Sylius plugin adding gift card functionality (buy gift cards, spend them on orders, check balances, admin management, API Platform support). PHP >= 8.1, Symfony 5.4 || 6.4, Sylius ~1.12. Development branches follow the `0.*.x` naming scheme (current: `0.12.x`).
+`setono/sylius-gift-card-plugin` — a Sylius plugin adding gift card functionality. Version 1.x (branch `1.x`) is a full rewrite targeting Sylius 1.13/1.14, PHP >= 8.1, Symfony ^6.4. See `REWRITE.md` for the complete architecture plan, decisions, and progress log of the rewrite.
+
+Key feature set: customers buy gift cards choosing the amount themselves (virtual = email delivery, physical = shipped + design chosen by the customer); redeeming a gift card produces either an order adjustment or a real Payment entity depending on the `setono_sylius_gift_card.redemption.mode` config (`adjustment` | `payment`). No API layer.
## Commands
```bash
-composer analyse # Psalm static analysis (psalm.xml, baseline in psalm-baseline.xml)
-composer check-style # ECS check (sylius-labs coding standard, config in ecs.php)
+composer analyse # PHPStan at max level (phpstan.neon)
+composer check-style # ECS check (ecs.php)
composer fix-style # ECS auto-fix
-composer phpunit # PHPUnit test suite (tests/Unit)
+composer phpunit # full PHPUnit suite
+vendor/bin/phpunit --testsuite unit # unit tests only (no database needed)
+vendor/bin/phpunit --testsuite functional # functional tests (require MySQL, see below)
vendor/bin/phpunit tests/Unit/Path/To/SomeTest.php # single test file
-vendor/bin/phpunit --filter testMethodName # single test method
-vendor/bin/phpspec run # phpspec specs (spec/ directory)
-vendor/bin/behat # Behat acceptance tests (requires running test app, see below)
+vendor/bin/phpunit --filter testMethodName # single test method
+vendor/bin/rector --dry-run # rector check (CI runs this)
```
-Note: the README mentions `composer tests`, `composer try` and `composer all` — those scripts do not exist in composer.json; use the commands above.
-
-### Test application
-
-`tests/Application/` contains a full Sylius app used as the kernel for PHPUnit (bootstrap `tests/Application/config/bootstrap.php`) and Behat. Run its console with `(cd tests/Application && bin/console ...)`. Behat (behat.yml.dist) expects the app served at `http://localhost:8080`, a MySQL database, built assets (`yarn install && yarn build` inside tests/Application), and headless Chrome for `@javascript` scenarios — see the `integration-tests` job in `.github/workflows/build.yaml` for the full setup sequence.
-
-CI additionally runs `composer validate --strict`, `composer normalize --dry-run`, `composer-require-checker`, `composer-unused`, yaml/twig/container lints, and Doctrine schema validation.
+## Test application
-## Architecture
-
-Namespace `Setono\SyliusGiftCardPlugin\` maps to `src/`; tests are `Setono\SyliusGiftCardPlugin\Tests\` in `tests/`. Bundle class is `src/SetonoSyliusGiftCardPlugin.php`; services are XML files under `src/Resources/config/services/` (one per area: applicator, factory, order_processor, ...) imported by `services.xml`. Resource/grid/route/serializer/API configs also live under `src/Resources/config/`. The bundle must be registered before SyliusGridBundle in host apps (parameter resolution order).
+`tests/Application/` contains a full Sylius app used as the kernel for PHPUnit (bootstrap `tests/Application/config/bootstrap.php`) and for manual/browser verification. Run its console with `(cd tests/Application && bin/console ...)`. It needs a MySQL database (`DATABASE_URL` in `tests/Application/.env`) and built assets (`yarn install && yarn build` inside tests/Application). Admin credentials after fixtures: username `sylius`, password `sylius`.
-### Domain model
+**Always run the web server with `symfony serve`** (e.g. `(cd tests/Application && symfony serve -d --port=8080)`) — never `php -S` / `symfony php -S ... router.php`. `symfony serve` handles routing itself (no `router.php` needed) and serves from `public/` automatically.
-`GiftCard` holds a code, `amount`/`initialAmount` (integers, minor units per Sylius money convention), currency, channel, optional customer/expiry, and an enabled toggle. When bought in the shop it is linked 1:1 to an `OrderItemUnit`. `GiftCardConfiguration` (+ `GiftCardChannelConfiguration` join entity) defines per-channel/locale settings: PDF template, default validity period, images.
+**Assets: use Node 20** (`tests/Application/.nvmrc` pins it — run `nvm use` before `yarn install`/`yarn build`). The frontend uses `@sylius-ui/frontend` (Dart Sass); Node 22+ breaks the build. `package.json` also pins `jquery` via `resolutions` so the admin JS (`jquery.dirtyforms`) loads — without it the admin console throws `jQuery.dirtyForms is not a function` and JS-driven form submits fail.
-Host applications integrate by applying the plugin's traits/interfaces to their entities — `ProductTrait` (adds `isGiftCard` and `giftCardAmountConfigurable` flags), `OrderTrait` (applied gift cards collection), `OrderItemTrait`, `OrderItemUnitTrait` (gift card relation), and repository traits in `src/Doctrine/ORM/`. The README documents the exact setup; `tests/Application/` shows a working example.
+## Testing conventions
-### Two distinct gift card flows
+- Unit tests live in `tests/Unit`, functional tests (KernelTestCase/WebTestCase booting the test app) in `tests/Functional`.
+- Use a BDD-style naming convention for test methods (`it_does_something`) with the `@test` annotation or `test` prefix.
+- Use Prophecy for mocking (phpspec/prophecy-phpunit), not PHPUnit mock objects.
+- Form type tests extend `Symfony\Component\Form\Test\TypeTestCase`.
-**Buying a gift card** (product flagged as gift card): the gift card entity is created at add-to-cart time via `GiftCardFactory::createFromOrderItemUnitAndCart()` — called from `Form/Extension/AddToCartTypeExtension` (shop form flow, POST_SUBMIT listener) and `Api/CommandHandler/AddItemToCartHandler` (API flow). The card starts disabled; winzou state machine callbacks (`src/Resources/config/state_machine/`) drive its lifecycle through `Operator/OrderGiftCardOperator`: checkout complete → `associateToCustomer`, payment paid → `enable` + `send` (email with PDF), order cancel → `disable`. "Configurable" gift card products let the customer choose the amount.
+## UI verification with Playwright MCP
-**Spending a gift card**: `Applicator/GiftCardApplicator` validates (enabled, not expired, channel matches) and attaches the card to the order, then reprocesses it. `OrderProcessor/OrderGiftCardProcessor` (a Sylius order processor) converts each applied card into a negative order adjustment (`AdjustmentInterface::ORDER_GIFT_CARD_ADJUSTMENT`, origin code = gift card code) capped at the eligible order total. Actual balance mutation happens via state machine callbacks on `Modifier/OrderGiftCardAmountModifier`: decrement on order create, increment back on cancel. Both flows can coexist on one order.
+All UI changes MUST be verified with the Playwright MCP tools (configured in `.mcp.json`): run the test application, then use browser navigation and screenshots to confirm the change renders and behaves correctly — product page gift card form (amount, message, design picker, live preview), cart, checkout in both redemption modes, and the admin panel (gift cards, designs, balance dashboard).
-### API
-
-API Platform resources are declared in `src/Resources/config/api_resources/` with messenger commands/handlers in `src/Api/Command` and `src/Api/CommandHandler`. The plugin replaces the input of Sylius' shop `add item to cart` operation with its own `AddItemToCart` command (carrying gift card info); host apps must copy/adjust `Order.xml` as described in the README.
+## Architecture
-### PDF rendering
+Namespace `Setono\SyliusGiftCardPlugin\` maps to `src/`; tests are `Setono\SyliusGiftCardPlugin\Tests\` in `tests/`. Bundle class `src/SetonoSyliusGiftCardPlugin.php`; services are XML files under `src/Resources/config/services/` imported by `services.xml`. The DI extension prepends configuration for other bundles (winzou state machine, sylius_ui, sylius_grid, liip_imagine, sylius_mailer) from YAML files in `src/Resources/config/prepend/` — host apps do not import plugin config manually. Register the bundle before SyliusGridBundle.
-Gift cards render to PDF via knp-snappy/wkhtmltopdf (`Renderer/PdfRenderer`), with template content and rendering options coming from the gift card's configuration (`Provider/`). Admin supports live preview of example PDFs.
+Only doctrine/orm is supported. Resources: `gift_card`, `gift_card_design` (translatable, images with front|back types), `gift_card_transaction` (append-only balance ledger, written only by the balance operator).
-### Test layout
+### Domain rules
-- `tests/Unit/` — PHPUnit (includes DI/config tests using matthiasnoback's symfony-config-test / dependency-injection-test)
-- `spec/` — phpspec, mirrors `src/` structure
-- `features/` + `tests/Behat/` — Behat contexts/pages built on the Sylius Behat pack
+- `GiftCard.amount`/`initialAmount` are integers in minor units (Sylius money convention). `initialAmount` is set explicitly — no implicit seeding.
+- A disabled, "pending" GiftCard is created at add-to-cart (one per OrderItemUnit) carrying amount/message/design/deliveryType; a reconciliation pass at checkout complete creates cards for quantity-bumped units, removes stale ones, and re-snapshots final amounts from unit totals. Cards are enabled on payment and emailed (all delivery types); disabled on order cancel.
+- `deliveryType` (virtual|physical) is derived from `variant->isShippingRequired()` — never from product structure assumptions.
+- Balance mutations go through the balance operator exclusively, which writes `GiftCardTransaction` ledger rows (idempotency via nullable-unique `idempotencyKey`). Nothing below controllers flushes.
+- Redemption is strategy-based: `adjustment` mode creates negative `order_gift_card` adjustments; `payment` mode creates one Payment per card (offline gateway payment method, lazily created). Balance is committed at order placement, restored on cancel/refund.
+- Gift cards cannot pay for gift-card line items (EligibleTotalCalculator default).
diff --git a/README.md b/README.md
index 89ed61ab..a97ba517 100644
--- a/README.md
+++ b/README.md
@@ -4,78 +4,65 @@
[![Software License][ico-license]](LICENSE)
[![Build Status][ico-github-actions]][link-github-actions]
-Add gift card functionality to your store:
+Add gift card functionality to your Sylius store:
-- Buy gift cards
-- Use gift cards to purchase products
-- See the balance of a gift card by looking up the gift card code
+- **Buy gift cards** — customers choose the amount, a design and an optional message, and pick whether the gift card is **virtual** (delivered by email as a PDF) or **physical** (shipped like a normal product).
+- **Redeem gift cards** — customers apply a gift card code in the cart. You choose, via configuration, whether redeeming a gift card creates an **order adjustment** or a **real payment entity**.
+- **Admin management** — a gift card grid, gift card designs, a one-click "create gift card product" scaffold, manual balance adjustments (with an audit ledger), and an outstanding-balance dashboard.
-The administrator will have access to a dashboard showing the total outstanding balance of gift cards which
-can be used for auditing.
+> This is the `1.x` line for **Sylius 1.13 / 1.14**. It is a ground-up rewrite of the `0.12.x` plugin. There is **no API layer** in 1.x — see [`UPGRADE-1.0.md`](UPGRADE-1.0.md) if you are coming from `0.12.x`.
-## Screenshots
+## Table of contents
-▶▶ [Skip screenshots and go to installation](#Installation)
+- [How it works](#how-it-works)
+- [Requirements](#requirements)
+- [Installation](#installation)
+- [Configuration](#configuration)
+- [Customization](#customization)
+- [Development](#development)
+- [License](#license)
-
+## How it works
-
+### Virtual vs physical
-## Api platform support
+Whether a gift card is virtual or physical is derived from the chosen product variant's `shipping required` flag — there is no special product type. The recommended setup is a single gift card product with a "delivery" product option producing a non-shippable *Virtual* variant and a shippable *Physical* variant. Virtual-only stores work too: just create a single non-shippable variant and the delivery selector disappears. Use the **Create gift card product** button in the admin gift card list to scaffold this in one click.
-Everything related to Gift Card can be done via API. Whether it is admin or shop actions
+### Buying a gift card
-## Installation
+The customer chooses the amount, a design and an optional message on the product page (with a live preview). A disabled gift card is created per order item unit at add-to-cart time; at checkout completion it is reconciled against the final amounts, and when the order is paid it is enabled and emailed (with a PDF attachment) to the customer.
-### Require plugin with composer:
+### Redeeming a gift card
-```bash
-$ composer require setono/sylius-gift-card-plugin
-```
+The customer enters a gift card code in the cart. Depending on `setono_sylius_gift_card.redemption.mode`:
-### Import configuration:
+- **`adjustment`** (default): applied gift cards become negative order adjustments, reducing the order total. The gateway then charges the reduced total.
+- **`payment`**: the order total stays intact; each applied gift card becomes a completed [`Payment`](https://docs.sylius.com/the-book/carts-and-orders/payments) using a lazily-created *offline* gift card payment method, and the remainder is charged through the normal gateway. The payment step is skipped automatically when gift cards cover the whole order.
-```yaml
-# config/packages/setono_sylius_gift_card.yaml
-imports:
- # ...
- - { resource: "@SetonoSyliusGiftCardPlugin/Resources/config/app/config.yaml" }
-```
+In both modes gift cards cannot be used to buy other gift cards, balances are committed when the order is placed and restored when it is cancelled/refunded, and every balance change is recorded in an append-only ledger.
-### (Optional) Import fixtures
+## Requirements
-If you wish to have some gift cards to play with in your application during development.
+| Requirement | Version |
+|-------------|--------------------------------------------|
+| PHP | >= 8.1 |
+| Sylius | 1.13 / 1.14 |
+| Symfony | ^6.4 |
+| ORM | doctrine/orm (the only supported driver) |
-```yaml
-# config/packages/setono_sylius_gift_card.yaml
-imports:
- # ...
- - { resource: "@SetonoSyliusGiftCardPlugin/Resources/config/app/fixtures.yaml" }
-```
+## Installation
-### Import routing:
-
-```yaml
-# config/routes.yaml
-setono_sylius_gift_card:
- resource: "@SetonoSyliusGiftCardPlugin/Resources/config/routes.yaml"
-```
+### Require the plugin with composer
-or if your app doesn't use locales:
-
-```yaml
-# config/routes.yaml
-setono_sylius_gift_card:
- resource: "@SetonoSyliusGiftCardPlugin/Resources/config/routes_no_locale.yaml"
+```bash
+composer require setono/sylius-gift-card-plugin
```
-### Add plugin class to your `bundles.php`:
+### Register the plugin
-Make sure you add it before `SyliusGridBundle`, otherwise you'll get
-`You have requested a non-existent parameter "setono_sylius_gift_card.model.gift_card.class".` exception.
+Add it to `config/bundles.php` **before** `SyliusGridBundle`:
```php
- ['all' => true],
@@ -84,286 +71,162 @@ $bundles = [
];
```
-### Copy templates
-
-You will find the templates you need to override in the [test application](https://github.com/Setono/SyliusGiftCardPlugin/tree/master/tests/Application/templates).
+The plugin auto-configures the state machine, grids, UI events, email templates and image filters for you — you do **not** need to import any bundle configuration manually.
-### Extend entities
+### Import routing
-**Extend `Product`**
-```php
-__giftCardTraitConstruct();
-
parent::__construct();
}
}
```
-**Extend `OrderItem`**
-
```php
-
-