This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A Sylius e-commerce plugin for abandoned cart recovery. It detects idle shopping carts and sends re-engagement emails with cart recovery URLs and unsubscribe options.
- Do NOT commit unless explicitly asked
- Do NOT push unless explicitly asked
- Use relative paths in bash commands, not absolute paths
- We use OpenSpec for structured change management. The
openspec/directory contains specs, change artifacts, and archives. Always includeopenspec/files when committing.
Follow clean code principles and SOLID design patterns:
- Write clean, readable, and maintainable code
- Apply SOLID principles (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion)
- Use meaningful variable and method names
- Keep methods and classes focused on a single responsibility
- Favor composition over inheritance
# Run tests
composer phpunit
# Run a single test file
vendor/bin/phpunit tests/Path/To/TestFile.php
# Run a single test method
vendor/bin/phpunit --filter testMethodName
# Static analysis (PHPStan at max level)
composer analyse
# Check coding standards (ECS)
composer check-style
# Fix coding standards
composer fix-style
# Start the test application dev server (from tests/Application/)
symfony server:start
# Check server status
(cd tests/Application && symfony server:status)The plugin uses Symfony Workflow (state machine type) to manage notification lifecycle (src/Workflow/NotificationWorkflow.php):
pending → processing → sent
↘ ineligible
(any state) → failed
Initial marking: pending
Transitions:
process:pending→processingsend:processing→sentfail_eligibility_check:processing→ineligiblefail: any state →failed
- Creator (
src/Creator/NotificationCreator) - UsesIdleCartDataProviderto find idle carts and createsNotificationentities for each - Processor (
src/Processor/NotificationProcessor) - Iterates pending notifications viaPendingNotificationDataProvider, runs eligibility checks, sends email, and transitions the state machine - EligibilityChecker (
src/EligibilityChecker/) - Composite checker pattern; validates if notification should be sent - Mailer (
src/Mailer/EmailManager) - Sends the actual email with recovery/unsubscribe URLs
IdleCartDataProvider(src/DataProvider/) - Queries idle carts using configurableidle_thresholdandlookback_window, returns a batch iterator. DispatchesQueryBuilderForIdleCartsCreatedevent for query customization.PendingNotificationDataProvider(src/DataProvider/) - Queries pending notifications whose carts are still in the cart state
Notification(src/Model/) - Tracks abandoned cart notification state (linked to Order). Hasstate,processingErrors,sentAt,lastClickedAt. Uses optimistic locking (version).UnsubscribedCustomer(src/Model/) - Records customers who opted out, keyed by unique email
CartRecoveryUrlGenerator- Generates links to restore customer's cart (with UTM tracking params)UnsubscribeUrlGenerator- Generates secure unsubscribe links using configurable salt + SHA256 hash viaEmailHasher
RecoverCartAction(src/Controller/Action/) - Recovers a cart by token value, setslastClickedAton the notification for engagement tracking, redirects to cart summaryUnsubscribeCustomerAction(src/Controller/Action/) - Validates email + hash, createsUnsubscribedCustomerentity
NotificationFactory- Creates notifications linked to an orderUnsubscribedCustomerFactory- Creates unsubscribed customer records from emailOrderFactory- Decorates Sylius order factory to assign tokens on creation
TokenValueBasedCartContext(src/Context/) - Cart context using token value from request (priority 100)AdminMenuListener(src/Menu/) - Adds "Abandoned Cart" under Marketing in admin menuPruner(src/Pruner/) - Deletes notifications older thanprune_older_thanminutesEmailHasher(src/Hasher/) - SHA256 email hashing for secure unsubscribe linksSetSentAtSubscriber/ResetProcessingErrorsSubscriber(src/EventSubscriber/Workflow/) - Workflow event subscribers
setono:sylius-abandoned-cart:create-notifications- Finds idle carts and creates notification entities (supports--dry-run)setono:sylius-abandoned-cart:process-notifications- Processes pending notifications (eligibility check + send email)setono:sylius-abandoned-cart:prune-notifications- Cleanup old notifications
Services are defined in XML under src/Resources/config/services/. The DI extension conditionally loads eligibility checkers from services/conditional/ based on plugin config. Grids, workflow, and mailer config are prepended in the extension's prepend() method.
The plugin is configured via setono_sylius_abandoned_cart in Symfony config:
setono_sylius_abandoned_cart:
salt: 's3cr3t' # Secret for unsubscribe URL hashing (required, change in production)
idle_threshold: 60 # Minutes before a cart is considered idle (default: 60)
lookback_window: 15 # Minutes lookback window for notification creation (default: 15)
prune_older_than: 43200 # Prune notifications older than N minutes (default: 43200 = 30 days)
eligibility_checkers:
unsubscribed_customer: true # Skip customers who unsubscribed (default: true)
subscribed_to_newsletter: false # Only notify newsletter subscribers (default: false)Tests are in tests/ with a full Sylius test application in tests/Application/. The test app bootstraps via tests/Application/config/bootstrap.php. The test environment requires Sylius fixtures to be loaded for functional tests that render shop templates (they need a Channel):
(cd tests/Application && bin/console sylius:fixtures:load -n --env=test)- BDD-style naming: Use
it_prefix for test methods (e.g.,it_returns_eligible_when_email_is_null) - Use
@testannotation: Methods use@testdocblock annotation - Prophecy for mocking: Use
ProphecyTraitand$this->prophesize()for all mocks, NOT PHPUnit'screateMock()
Example:
use Prophecy\PhpUnit\ProphecyTrait;
final class MyTest extends TestCase
{
use ProphecyTrait;
/** @test */
public function it_does_something(): void
{
$dependency = $this->prophesize(DependencyInterface::class);
$dependency->method()->willReturn('value');
$sut = new MyClass($dependency->reveal());
}
}- PHP 8.1+ required
- PHPStan at
maxlevel with Symfony and Doctrine integrations - ECS follows
sylius-labs/coding-standard - Strict typing enforced (
declare(strict_types=1)) - Rector configured for PHP 8.1 level
- Infection mutation testing with min MSI of 37.33 and 100% covered MSI
Translation files are in src/Resources/translations/ (domain: messages):
- Available languages: English (en), Danish (da), French (fr)
- Key prefixes:
setono_sylius_abandoned_cart.emails.*- Email template stringssetono_sylius_abandoned_cart.form.*- Form field labelssetono_sylius_abandoned_cart.ui.*- Admin UI labels and messages
Use the right tool for the job when executing bash commands:
- Finding files → Use
fd(fast file finder) - Finding text/strings → Use
rg(ripgrep for text search) - Finding code structure → Use
ast-grep(syntax-aware code search) - Selecting from multiple results → Pipe to
fzf(interactive fuzzy finder) - Interacting with JSON → Use
jq(JSON processor) - Interacting with YAML or XML → Use
yq(YAML/XML processor)
Examples:
fd "*.php" | fzf- Find PHP files and interactively select onerg "function.*validate" | fzf- Search for validation functions and selectast-grep --lang php -p 'class $name extends $parent'- Find class inheritance patterns