Skip to content

Replace request-time Partner Ads notification with conversion entity + cron command - #55

Open
loevgaard wants to merge 2 commits into
3.xfrom
conversion-based-notifications
Open

Replace request-time Partner Ads notification with conversion entity + cron command#55
loevgaard wants to merge 2 commits into
3.xfrom
conversion-based-notifications

Conversation

@loevgaard

@loevgaard loevgaard commented Aug 24, 2026

Copy link
Copy Markdown
Member

Fixes #49, closes #50.

What changed

The Partner Ads notification no longer happens inside the customer's thank-you page request. Instead:

  1. A Conversion entity (new table setono_sylius_partner_ads__conversion) is created when a referred customer completes an order: it holds the order (one-to-one, unique), the partner id from the cookie, a state (pending / notified / failed), a tries counter, the last error, and notifiedAt.
  2. CreateConversionSubscriber listens on sylius.order.post_complete and stores the pending conversion — no HTTP involved. It skips non-positive partner ids from tampered/mangled cookies (partially addresses Query parameter 'paid' is not validated: garbage overwrites valid attribution, array input causes 400 on any shop URL #51) and is idempotent (checks for an existing conversion; the DB unique constraint backs this up).
  3. A new console command, setono:sylius-partner-ads:process-conversions (meant to run via cron every 5–15 minutes), picks up pending conversions whose order payment state is paid and notifies Partner Ads. Failures are caught per conversion: the error is recorded, the conversion stays pending and is retried on the next run, and after --max-tries (default 10) it is marked failed.
  4. The Buzz fallback client now gets an explicit 30s timeout — Buzz's default is no timeout at all.

Why

Removed (breaking changes)

Since the CLI send is already out-of-band, the async messenger machinery is no longer needed and has been removed:

  • NotifySubscriber (thank-you page listener), Notify message, NotifyHandler, RegisterCommandBusPass
  • ProgramContext / ProgramContextInterface (only consumer was NotifySubscriber)
  • The messenger configuration node (command_bus, transport) — apps with this config must drop it on upgrade

Upgrade notes

  • Run doctrine:migrations:diff + migrate to create the conversion table.
  • Schedule setono:sylius-partner-ads:process-conversions via cron.
  • Remove any setono_sylius_partner_ads.messenger config.
  • Behavioral change: notifications now fire after payment (on the next cron run), not on thank-you page view.

Admin UI

The conversion is exposed as a full Sylius resource in the admin: a grid under Marketing → Partner Ads Conversions showing order number, partner id, state, tries, last error, and timestamps, with a state filter (pending / notified / failed) and a delete action. Index and delete routes only — conversions are system-created, so there is no create/update. English and Danish translations included.

Testing

  • New unit tests for Conversion, CreateConversionSubscriber, and ProcessConversionsCommand; removed tests for deleted classes.
  • ECS, PHPStan (level max), and PHPUnit (46 tests) all pass; Doctrine mapping validated via the test app.

https://claude.ai/code/session_01Mt12J8vdGwwWg4V23uoEf9

…mand

Instead of sending the Partner Ads notification during the thank-you page
request, a pending Conversion (order, partner id, state) is stored when a
referred order is completed, and a new console command -
setono:sylius-partner-ads:process-conversions, meant to be run via cron -
notifies Partner Ads about conversions whose orders have been paid, with
automatic retries and a failed state after too many tries.

This means a slow or failing Partner Ads endpoint can never affect
checkout, unpaid orders are no longer reported, and the unique order
constraint prevents duplicate notifications. The Buzz fallback client
also gets an explicit timeout instead of Buzz's default of none.

The messenger machinery (Notify command, handler, command bus compiler
pass, messenger config) and the thank-you page subscriber are removed,
along with ProgramContext, whose only consumer was that subscriber.

Fixes #49, closes #50
Claude-Session: https://claude.ai/code/session_01Mt12J8vdGwwWg4V23uoEf9
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.00000% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.75%. Comparing base (4a2da04) to head (6608958).

Files with missing lines Patch % Lines
src/Doctrine/ORM/ConversionRepository.php 0.00% 24 Missing ⚠️
src/Command/ProcessConversionsCommand.php 87.27% 7 Missing ⚠️
src/Model/Conversion.php 93.33% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##                3.x      #55      +/-   ##
============================================
- Coverage     86.84%   84.75%   -2.10%     
- Complexity       80       94      +14     
============================================
  Files            21       20       -1     
  Lines           365      492     +127     
============================================
+ Hits            317      417     +100     
- Misses           48       75      +27     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">

<entity name="Setono\SyliusPartnerAdsPlugin\Model\Conversion" table="setono_sylius_partner_ads__conversion">

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be mapped superclass. The resource bundle will take care of it

Registers a ResourceController for the conversion resource and adds
admin routes (index and delete only - conversions are system-created),
a grid with a state filter, a marketing menu item, and English and
Danish translations. This gives merchants visibility into pending,
notified, and failed conversions, including the last error.

Claude-Session: https://claude.ai/code/session_01Mt12J8vdGwwWg4V23uoEf9
Comment thread config/routes/admin.yaml
permission: true
templates: "@SyliusAdmin\\shared\\crud"
grid: setono_sylius_partner_ads_admin_conversion
only: ['index', 'delete']

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if I delete a conversion?

Comment thread config/services.php
$services->alias(ProgramContextInterface::class, ProgramContext::class);
service('request_stack'),
service(CookieHandlerInterface::class),
// factory and repository services created by AbstractResourceExtension::registerResources()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment or change it to something about the resource bundle instead

private readonly ProgramRepositoryInterface $programRepository,
private readonly ClientInterface $client,
private readonly OrderTotalCalculatorInterface $orderTotalCalculator,
private readonly ObjectManager $conversionManager,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use https://github.com/Setono/doctrine-orm-trait/ instead of injecting entity manager

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant