1.x rewrite (Sylius 1.13/1.14, no API layer) - #269
Conversation
Phase 1 (skeleton): - New 1.x branch; drop API layer, GiftCardConfiguration family, Behat, psalm, knp-snappy, safe-writer, jms/lexik - Tooling per SyliusPluginSkeleton 1.14.x: PHPStan (max), Rector, Infection, shipmonk dependency analysis, playwright MCP; Symfony ^6.4, Sylius ~1.13/~1.14 - Bundle extension auto-configures host app via prepend() - Redemption mode config (adjustment|payment) selects service file Phase 2 (domain model): - Reshaped GiftCard (deliveryType enum, design FK, optimistic version, transaction ledger, isUsable/isPending); explicit initialAmount - New GiftCardDesign (translatable, front/back images, channels) + lazy seeding hooks - New GiftCardTransaction append-only balance ledger - Grouped unambiguous code generator + normalizer; SQL balance aggregation - All quality gates green; schema validates; container boots on Sylius 1.14
- GiftCardDesignProvider with lazy Classic seeding (bundled default image, concurrency-guarded) - Admin grid (prepended sylius_grid) with thumbnail field, admin form template, menu entry - Design fixture + example factory (front/back image upload) - Clean 1.x translations - Verified: fixtures load against DB, Classic design + image persist, 20 cards created - All quality gates green
- GiftCardInformation DTO + form (amount, message, design picker); designs unified across virtual/physical - AddToCartTypeExtension + CartGiftCardHandler: pending disabled GiftCard per OrderItemUnit at add-to-cart (units exist at POST_SUBMIT via quantity data mapper) - ValidGiftCardAmount constraint + channel-aware amount limits provider - PendingGiftCardCleanupListener (onFlush) removes pending cards for deleted units - Product-page section (sylius_ui prepend) with live HTML preview (vanilla JS/CSS) - gift_card_product fixture: product + delivery option + virtual/physical variants (verified: correct per-variant shipping_required) - Quality gates green; fixtures load clean
- OrderGiftCardOperator: reconcile (checkout complete), enable (pay), disable (cancel) - Winzou callbacks prepended (verified registered) - reconcile snapshots final amount, creates cards for quantity-bumped units, associates customer - send-on-pay deferred to phase 10 (email), scaffold to phase 11 Also: yarn install + build for the test app (shop assets)
Shared core: - EligibleTotalCalculator (excludes gift-card line items) - GiftCardCoverageCalculator + GiftCardCoverage VO (stacking, caps, skips unusable/mismatched) - GiftCardBalanceOperator: sole balance mutator, append-only ledger, idempotency, InsufficientGiftCardBalanceException, manual adjust() - GiftCardApplicator rewritten with guards, delegates to aliased redemption method - GiftCardRedemptionMethodInterface + abstract RedemptionMethod base - GiftCardIsApplicable compound constraint Adjustment mode: - GiftCardAdjustmentProcessor (negative order_gift_card adjustments from coverage) - AdjustmentRedemptionMethod (commit/rollback via balance operator, idempotency keys) - One pair of winzou callbacks (create->commit, cancel->rollback) for both modes Container lints, callbacks registered, quality gates green
- Apply action (POST + GiftCardIsApplicable validation), remove action (POST + CSRF) - Twig redemption extension/runtime (apply form, coverage, remaining total) - Cart apply box + totals partials via sylius_ui prepend - Playwright-verified end-to-end (adjustment mode): product gift card form + live preview render; add-to-cart creates the pending disabled card with correct amount/deliveryType/design/message; cart apply box attaches a card to the order - Test app: add StateMachineAbstractionBundle, remove stale 0.12.x overrides, fixture channel fallback, encore strict_mode off
- DompdfGiftCardPdfGenerator + pluggable interface; two-page pdf.html.twig - GiftCardEmailManager (Sylius mailer, in-memory PDF attachments via tempfile) - OrderGiftCardOperator.send() + pay->send winzou callback - SendGiftCardEmailSubscriber for admin-created cards - Admin PDF download + design preview-PDF actions/routes - Browser-verified: admin PDF download produces a valid 22KB PDF
…pages render styled
resolve-url-loader 3.x fails with dart-sass ('PostCSS received undefined');
disabling it lets Encore emit the Sylius CSS. Also gitignore the local dev router.php.
- GiftCardPaymentChecker + lazy GiftCardPaymentMethodProvider (offline gateway) - PaymentRedemptionMethod: commit creates completed gift-card payments + redeems balance; rollback refunds + restores (same winzou callbacks as adjustment mode) - GiftCardAwareOrderPaymentProcessor decorates checkout + after_checkout to size the gateway payment to (total - coverage) - Payment-step-skip checker + methods/default resolver decorators hide the gift-card method from checkout - Verified: container boots + lints in payment mode, decorators active
- Gift card admin grid (prepend): code/customer/amount/deliveryType/enabled/createdAt, filters, create/update/download-pdf/delete actions, hides pending cards - Balance dashboard action + template (SQL findBalance aggregation by currency) - Admin gift card create enabled (channel field on new cards) - Design form example-PDF preview button; menu items for designs + balance - Browser-verified: gift card list, balance dashboard render styled
- Gift card grid actions (adjust-balance, download PDF), balance dashboard (SQL) - AdjustGiftCardBalanceAction + form (delta + reason -> ledger via balance operator) - CreateGiftCardProductAction scaffold (verified: creates disabled gift card product with virtual + physical variants, redirects to edit) - Admin gift card create (channel field), design preview-PDF button, menus, translations - Note: local admin form submits blocked by node-sass/arm64 broken admin JS (test-app infra); covered by functional tests
- Unit suite (28): model, code normalizer, eligible-total + coverage calculators, balance operator (redeem/restore/adjust/idempotency/insufficient), configuration - Functional suite (3): balance operator + ledger + findBalance against a real DB - composer phpunit -> OK (31 tests, 53 assertions); PHPStan max / ECS / Rector clean - README rewritten for 1.x; UPGRADE-1.0.md clean-break guide Completes the 1.x rewrite (all 12 phases).
Follows Setono/SyliusPluginSkeleton@80cc9db: - package.json: use @sylius-ui/frontend (Dart Sass) instead of node-sass, which failed to compile against Node 22's V8 API on arm64; pin jquery via resolutions so jquery.dirtyforms loads (fixes 'jQuery.dirtyForms is not a function' console error that broke admin form submits) - .nvmrc: pin Node 20 for the asset build - webpack.config.js: build the vendor shop/admin entries directly (drops the redundant local assets/ re-export files) - Document the Node 20 requirement in CLAUDE.md - Add missing admin CRUD heading translations (edit/create gift card + design) Verified: yarn install + build succeed, admin renders fully styled, console is clean, and the adjust-balance form now submits end-to-end (balance 30000->35000 with a manual ledger row).
- EligibleTotalCalculator: add back applied order_gift_card adjustments so the per-card coverage shown in the cart stays stable once a card is applied. Previously, in adjustment mode, an applied card that fully covered the order drove the total to 0 and its own displayed coverage collapsed to $0.00 (the actual adjustment/ledger were always correct). +unit test. - Remove tests/Application stale 0.12.x Cart/summary.html.twig override that referenced a non-existent setono-sylius-gift-card-add-gift-card-to-order.js (404 + 'jQuery.addGiftCardToOrder is not a function' on the cart page) Both found via Playwright verification of the adjustment-mode redemption flow.
OrderItemTrait::equals() returned false for a gift card item even when compared to itself, so Sylius' OrderItemController::resolveAddedOrderItem() (getItems()->filter(equals)->first()) found nothing and ->first() returned false, raising a TypeError -> 500 on every gift card add-to-cart. Add an identity short-circuit so an item still equals itself while distinct gift card lines remain unmergeable. +regression test. Found via Playwright verification of the gift card purchase flow.
- Design name was required in every locale: a NotBlank form constraint on the translation was applied to all rendered locales. Move it to the GiftCardDesignTranslation entity validation so only the default locale (kept by ResourceTranslationsType) is required. +validators messages. - Design grid 'image' field 500'd (Can't read property 'image'): the twig field passed resource.image; add path: '.' so the template receives the design. - Add missing translations: new_gift_card_design heading, no_image label; drop stale 0.12.x gift_card_configuration/search validator keys. All found via Playwright verification of the design create/edit flow.
Sylius' PaymentMethodFactory::createWithGateway() only sets the gateway config factoryName, leaving gatewayName null. gateway_name is a NOT NULL column, so placing an order in payment mode (which lazily creates the offline gift card payment method) failed with a 500 integrity-constraint violation. Set gatewayName to the payment method code. Found via Playwright verification of payment-mode checkout.
…cale
- Fix GiftCardEmailManager::sendGiftCard(): it did not pass localeCode, so the
Sylius email layout threw 'Variable localeCode does not exist' (breaks admin
resend / single-card send). sendGiftCardsFromOrder was unaffected. Found by
the new functional test.
- Give the PDF attachment a clean customer-facing filename (gift-card-<code>.pdf)
via a per-send temp directory instead of the raw tempnam name.
- Functional tests (KernelTestCase, in-memory MessageEvent capture, CI-friendly):
* GiftCardEmailManagerTest: emails the customer with a valid PDF attachment +
clean filename; skips when there is no customer.
* GiftCardPaymentMethodProviderTest: lazily creates a persistable offline
payment method with gatewayName set (guards the payment-mode 500 regression).
* Extract GiftCardFunctionalTestCase base (schema + channel helpers).
- Verified real delivery + PDF attachment through the docker mailcatcher SMTP.
- docker-compose: drop obsolete 'version' key.
Full suite: 38 tests, 71 assertions.
…gn form Addresses reported issues: - expiresAt is now pinned to 23:59:59 (factory + admin date picker with a model transformer); the admin field is a date picker, not a datetime one. +unit test. - Add a gift card show page with a details panel and the transaction ledger (explicit show route + template + grid Show action). - PDF rendered 3 pages; two full-height cards + html/body height:100% (no explicit page break) now yields exactly 2 pages. +functional test. - Gift card grid defaults to 100 per page (limits [100, 200, 500, 1000]). - Adjust-balance and balance-dashboard pages now use the proper admin layout (header macro + breadcrumbs + form theme). - Design translations use the default Sylius translationForm accordion (locale tabs). - Design image upload: guidelines (dimensions/format, accept=image/*) + a preview thumbnail of already-uploaded images via a custom form widget. Full suite: 41 tests, 76 assertions. All browser-verified.
- Add UniqueDesignImageTypes constraint on GiftCardDesign so a design can have at most one image per type (front/back). Previously two 'Front' images could be added. +constraint validator with unit tests; browser-verified the error. - Restructure the design edit form into two columns (Details | Design images).
Mimics a clean minimalist design: - Front (dark, framed): brand mark + channel name, 'GIFT CARD' tag, 'The gift of choice' eyebrow, large 'Gift Card' heading, VALUE + amount, NO. + code. - Back (cream): dark stripe band, 'How to redeem' instructions, a dashed 'Redemption code' panel, a barcode strip, and a terms + brand/hostname footer (terms note the expiry date when set). - Designs with an uploaded front image still render it full-bleed with a scrim showing the amount + code; back falls back to the cream layout. - New pdf.* translations. Still exactly 2 pages (covered by the functional test).
…tial) - Extract the gift card front into a shared Twig partial (_card.html.twig) + CSS (_cardStyle.html.twig), used by BOTH the PDF and the on-site preview — one source of truth, no html2canvas. - Product page: render the shared card as a prominent, full-width live preview at the top of the gift card section; it updates amount (currency-formatted via Intl), message, and the selected design image live, scaled to fit via a transform. Rewrote product-gift-card.js/.css accordingly. - PDF template consumes the same partial for its front; back unchanged. Still 2 pages (functional test green).
The live preview only handled designs with a front image, so selecting an image-less design left it unchanged. Render both the image and framed layouts in preview mode and toggle .ssgc-card--has-image from JS based on whether the selected design has an image — matching what the PDF produces for that design. Also show the customer message in the image scrim.
templates/bundles/SyliusShopBundle/Taxon/_horizontalMenu.html.twig dropped the 'ui large stackable menu' wrapper (and used taxon.children instead of enabledChildren), so the top navigation rendered as unstyled left-aligned links. Deleting the override restores the vendor default (centered, styled menu).
…nfig - All plugin view folders and files are now snake_case (admin/gift_card/..., shop/gift_card/_card_style.html.twig, email/gift_cards_from_order.html.twig, admin/gift_card/grid/field/delivery_type.html.twig, etc.). Updated every reference (grid/ui/mailer prepend config, routes, pdf service arg, the two balance controllers, and the include/form_theme paths inside templates). - Removed dead 0.12.x leftovers that the extension never loads: grids.yaml + grids/, sylius_ui.yaml (root), routes_no_locale.yaml, state_machine/, and the templates only they referenced (Grid/Action/*, item_units_order, Order/coveredByGiftCards, giftCardBalance, GiftCard/create). Verified: container + twig lint, PHPStan, 43 tests, and browser rendering of the product preview, admin grid, and PDF.
…ntation - Every plugin-defined service id is now its class FQCN, and each interface is aliased directly to the concrete FQCN (autowiring-idiomatic). Updated all argument references, route _controller values, the winzou state-machine callbacks, and test/container references accordingly. - Fixed YAML escaping: the state-machine 'do' service refs use single quotes so the FQCN backslashes aren't treated as escape sequences. - Intentional exceptions kept with dotted ids: the two Sylius service overrides (sylius.factory.add_to_cart_command, sylius.form.type.add_to_cart), the Sylius ImagesUploadListener instance, and the two GiftCardAwareOrderPaymentProcessor decorators (same class -> can't both be the FQCN id). Convenience aliases kept: setono_sylius_gift_card.redemption_method (public, for the state machine) and setono_sylius_gift_card.pdf.generator, both pointing at the concrete FQCN. Verified: container lint, PHPStan, 43 tests, and an end-to-end redemption checkout (winzou -> redemption_method -> operator all resolve; balance debited with a ledger row).
Previously the plugin replaced two Sylius services outright: - sylius.factory.add_to_cart_command was redefined with our factory class - sylius.form.type.add_to_cart was redefined with our command as data_class Both are now non-destructive: - AddToCartCommandFactory decorates sylius.factory.add_to_cart_command and delegates to the inner factory, then wraps the result in our command with the gift card information (also gives it a proper FQCN id). - The form's data_class is set from AddToCartTypeExtension::configureOptions (the extension already extends AddToCartType), so no service override is needed. Verified: container lint, PHPStan, ECS, 43 tests, and browser add-to-cart of both a gift card (pending card created) and a regular product.
The form produced a bare array, which the action then had to describe with an array shape annotation to make anything of. It now binds to AdjustGiftCardBalanceCommand, following AddGiftCardToOrderCommand, and the constraints move onto that class as attributes so the rules travel with the data rather than with the one form that happens to produce it. Record the preference in CLAUDE.md, and drop the comment on GiftCardDesignTranslationType.
Checkout redemption was the last uncovered part of the UI, and the part where the plugin's two modes actually diverge. The mode decides which service file the extension loads, so it is fixed when the container is compiled — an environment variable cannot switch it, as the loader then looks for services/redemption/%env(...)%.xml. Covering both modes therefore means building a second container, which config/redemption_payment.yaml exists for; CI now runs the shop specs a second time against it. The specs assert what genuinely differs rather than asserting the same thing twice. In adjustment mode the gift card is a negative adjustment and reduces the order total. In payment mode it becomes a payment, so the order still costs what it did and a "Remaining to pay" row appears instead — asserting the total dropping there was wrong, and writing the first version that way is how the difference got pinned down. Also seed a gift card with a known code, so the shop specs have something to redeem without reading it out of the admin.
Researching how other platforms handle this found none that make the mechanism configurable. Shopify and BigCommerce treat a gift card as a payment method; Magento treats it as a discount and has a long-standing feature request to stop, because order management and accounting systems expect a payment instrument, and a gift card reducing the subtotal collides with promotions. The payment reading is also the one that matches the accounting: selling a gift card takes money for a liability, and redeeming it settles that liability rather than reducing what the order is worth. So the choice goes rather than the capability. A redeemed gift card is now always a Payment against the order, and the redemption.mode setting, the adjustment redemption method, the adjustment order processor, the order_gift_card adjustment type and the payment-mode-only Twig flag are all removed. The eligible total no longer has to add gift card adjustments back before computing coverage — nothing subtracts them any more, so the order total is already the pre-redemption total. GiftCardRedemptionMethodInterface stays: it is a reasonable seam for an application that wants to substitute its own, it is just no longer a switch the plugin flips. CI drops the second Playwright pass, and the specs assert the remaining-to-pay figure instead of branching on the mode.
…eftovers Everything reacting to a Symfony event is now an event subscriber, so the event name and the priority live in getSubscribedEvents() next to the code that cares about them instead of in a service tag: the six Symfony Workflow handlers, the gift card issuance recorder and the admin menu builder. Renamed and moved to match, since a class called *Listener implementing EventSubscriberInterface reads as a mistake. Two stay listeners because they cannot be anything else. The design image upload is Sylius' own ImagesUploadListener, a vendor class we cannot add the interface to, and PendingGiftCardCleanupListener is on Doctrine's onFlush, which is not a Symfony event at all. Verified through debug:event-dispatcher that all eight land on the same events with the same priorities as before, including where two of them share an event and the order between them matters. Also remove the psalm leftovers: the @psalm-suppress in Configuration, the @psalm-return on OrderInterface::getGiftCards() — replaced by the plain @return Collection<array-key, GiftCardInterface> the rest of the models use — and the export-ignore for a psalm.xml that no longer exists.
The skeleton uses setono/sylius-plugin-pack, and it is the better fit: it keeps php >=8.1 and PHPUnit 9, and it dropped psalm in favour of PHPStan, which is what this plugin has actually been analysed with all along. code-quality-pack still pulls psalm on the 2.x line we were pinned to, and its 3.x line requires php >=8.2, so it could not be upgraded in place. sylius/sylius is no longer required directly either — the pack provides it, along with phpstan and its extensions, rector, infection, prophecy, phpunit and the dependency analyser. The pack pins sylius/sylius to ~1.14.19, so the CI jobs that forced ~1.13.0 can no longer resolve and the sylius matrix dimension is removed. Sylius 1.13 is therefore no longer exercised anywhere, though nothing in the plugin's own constraints forbids it yet. The upgrade brings PHPStan 1 -> 2, Rector 1 -> 2 and adds phpstan-strict-rules, which surfaced 46 errors. All of them are fixed rather than suppressed: generics declared on the form types and repository traits, @var tags that narrowed a native type replaced by real narrowing, mixed values asserted at the point of use, and the fixture factories' create() widened back to the signature the interface declares. The one exception is the traits the plugin ships for host applications to apply, which PHPStan can only ever see as unused; that is ignored by identifier and path, with the reason written down.
Adds the mutation testing job the skeleton has and this repository was missing, even though infection.json.dist has been committed all along and the plugin pack provides infection. It is scoped to the unit suite: infection runs the test suite itself, and the functional one needs a booted kernel and a database that the job deliberately does not provision. Verified locally — 1234 mutants, covered code MSI 100%, and minMsi 0 in the config means it reports rather than gates. Also picks up actions/checkout@v5, `rector process --dry-run` and the verbose flag on doctrine:schema:validate that makes it print the missing SQL. Not adopted: the skeleton's `lowest` dependency dimension on the analysis, unit and integration jobs. On lowest, lexik/jwt-authentication-bundle and sylius-labs/polyfill-symfony-security resolve to versions whose signatures are incompatible with Symfony 6.4, which is why the skeleton pins them in require-dev. Both only reach us transitively through sylius/sylius, and pinning them would mean re-adding dependencies this rewrite deliberately removed with the API layer, purely to satisfy resolution. The dependency analysis job already installs the production dependencies at their lowest versions, which is what actually guards the constraints in `require`. The skeleton's symfony matrix is also skipped: it carries the single value ~6.4.0, and composer.json already constrains Symfony to ^6.4.
Adds the lowest dimension the skeleton has on static analysis, unit and functional tests, so both ends of the declared constraints are exercised rather than only the newest releases that happen to satisfy them. Three dev dependencies are pinned to make that resolve, exactly as the skeleton does. All three arrive transitively through sylius/sylius and are unused here, but at their lowest versions they are incompatible with Symfony 6.4: sylius-labs/polyfill-symfony-security declares getSalt() incompatibly with LegacyPasswordAuthenticatedUserInterface, lexik/jwt-authentication-bundle does the same for AuthenticatorInterface::authenticate(), and api-platform/core wires a service that does not exist. With those resolved, lowest surfaced two real problems. SchemaTool wants a list and getAllMetadata() returns a plain array, which is now wrapped. And Twig runtime extensions are declared as [RuntimeClass::class, 'method'], a form Twig 3 types explicitly but the Twig 2 we still support only documents as callable|null — ignored by path with the reason recorded, since the code is correct on both. Mutation testing stays on highest alone, as in the skeleton.
…balance date Move the command objects' constraints from PHP attributes into src/Resources/config/validation/, where the models already keep theirs. Host applications override plugin validation by pointing at those files, so a constraint declared as an attribute is one they cannot reach. Extract GiftCardProductFactory from GiftCardProductExampleFactory. Building the product — the shared delivery option, a variant per delivery type, channel pricing — is domain logic, so the admin scaffolding action no longer reaches into fixture code to create a live product; the fixture factory is now just the options resolver in front of the same factory. Covered by a Playwright spec. Default findBalance() to now. The usual question is what is outstanding today, so callers only pass a date when they want a different point in time.
sylius/calendar v0.4.0 registers its own Behat contexts as services in the test environment. We do not use Behat, so the container references classes extending an interface that is not installed and lint:container fails on the lowest dependency matrix. v0.5.0 dropped those service definitions.
payum/payum-bundle 2.7.0 shipped the routing configuration as YAML only, but Sylius' ShopBundle imports @PayumBundle/Resources/config/routing/authorize.xml, so booting the kernel fails outright. 2.7.1 restored the XML files, and it is what the lowest dependency build now resolves to. Also apply IfToNullCoalescingAssignRector, which a newer Rector on the highest matrix started reporting.
| /** | ||
| * @Annotation | ||
| */ |
There was a problem hiding this comment.
Is this still supported in the versions we support?
There was a problem hiding this comment.
Good catch — it is not, really. Removed from all four constraints in 3c6ed60.
@Annotation only means something to the Doctrine annotation reader, and Symfony 6.4 (the only Symfony we support) deprecates that path explicitly:
symfony/validator 6.4: "%s uses Doctrine Annotations to configure validation constraints,
which is deprecated. Use PHP attributes instead."
framework.validation.enable_annotations is deprecated in 6.4 too, in favour of enable_attributes, and Symfony 7 drops the reader altogether. Since we require PHP >= 8.1 the #[\Attribute] line underneath was already the only declaration anyone could use — and our own constraints are declared in XML anyway.
(doctrine/annotations 2.0.2 is still installed, via sylius/resource-bundle, so the docblock was not broken — just dead weight pointing at a deprecated mechanism.)
The one thing it changes: an application that wrote @GiftCardIsApplicable in a docblock rather than as an attribute would stop being validated. Deliberate, given we are pre-release and Symfony is removing it regardless.
…e cleanup listener Give AdjustGiftCardBalanceCommand the gift card it adjusts. The command now describes the whole intent, and validation can judge the adjustment against the balance it applies to: deducting more than a card holds reached the balance operator, which asserts and returned a 500. It is ordinary user error, so BalanceAdjustmentIsApplicable reports it as a field error instead. Move the constraint messages into the validators translation domain. That is where Symfony resolves them, so all of them except the design one were rendering as raw keys — customers saw "setono_sylius_gift_card.gift_card.not_enabled" whenever a gift card was rejected in the shop. Scaffold gift card products as gift_card, gift_card_2, ... instead of a random hex suffix, and separate variant codes with an underscore like every other code the plugin generates. Cover PendingGiftCardCleanupListener, which had no tests: a pending card dies with its unit, whether the unit is removed directly or orphaned by removing the order item, and an enabled card survives.
|
Heads up on something that fell out of the balance-adjustment validation above: almost every constraint message in the plugin was rendering as a raw translation key. Symfony resolves constraint messages in the instead of "This gift card is not active." — same for expired, no_balance, channel_mismatch, currency_mismatch, already_applied and both gift card amount limit messages. Only I noticed because my new message did exactly the same thing when I first put it in |
Symfony 6.4 — the only Symfony this plugin supports — deprecates configuring validation constraints through Doctrine Annotations, and Symfony 7 removes the reader entirely. The plugin requires PHP >= 8.1, so the #[\Attribute] declaration below each docblock is the only one that can still be used, and our own constraints are declared in XML regardless.
Summary
This is the ground-up 1.x rewrite of the plugin, targeting Sylius 1.13/1.14, PHP ≥ 8.1, Symfony ^6.4. It replaces the
0.12.xarchitecture (base branch of this PR).The rewrite simplifies aggressively and hardens what remains — see
REWRITE.mdfor the full architecture, decisions and progress log, andUPGRADE-1.0.mdfor the migration inventory.Highlights
variant->isShippingRequired(), not a special product type.OrderItemUnitat add-to-cart, reconciled to final amounts at checkout completion, enabled + emailed (PDF attachment) on payment, disabled on cancel.Paymentagainst the order via a lazily-created offline gift card payment method; the order total stays intact, the remainder goes through the normal gateway, and the payment step is skipped at full coverage. See the note below.GiftCardTransactionledger (audit + idempotency) which accounts for the whole balance, opening balance included —sum(transactions) == amount. Balance mutations go through a single balance operator; nothing below controllers flushes.sylius_core.state_machine.default_adapteris set to.Why redemption is a payment
0.12.x reduced the order total with a negative
order_gift_cardadjustment. 1.x does not: a redeemed gift card is a payment against the order.This started life as a configurable
redemption.modewith both mechanisms, and was cut back to one after checking what other platforms actually do. None of them make it configurable, and the split is instructive: Shopify and BigCommerce treat a gift card as a payment method, while Magento treats it as a discount and has a long-standing feature request to stop — because order management and accounting systems expect a payment instrument, and a gift card reducing the subtotal collides with promotions.It also matches the accounting: selling a gift card takes money for a deferred revenue liability, and redeeming it settles that liability rather than reducing what the order is worth.
This is the sharpest upgrade edge in the PR: orders no longer carry
order_gift_cardadjustments at all, so any host-app template, report or ERP export reading them silently returns nothing.UPGRADE-1.0.mdcalls it out.Testing
The Playwright suite covers the admin (gift cards index/show/edit, designs, balance report, gift card and design preview PDFs, product edit for simple/configurable/gift card products) and the shop (gift card product page, locales, add-to-cart, and redemption in the cart). Specs discover their subjects through the admin grids and the locale switcher rather than hardcoding ids, so they survive a reseed.
Notes for reviewers
1.x rewrite phase N …) followed by polish, review follow-ups and bug fixes.prepend(); host apps only register the bundle, import routes and apply the entity traits.REWRITE.md: [Feature request]: Recipient name and email fields on a gift card #270, [Feature request]: Per-channel default validity periods #271, [Feature request]: Preset amount chips on the gift card product page #272, [Feature request]: Edit a gift card line in the cart #273, [Feature request]: Public gift card balance page #274, [Feature request]: CR80 (credit card sized) print template for gift card PDFs #275, [Feature request]: Expiry reminder emails #276.