Feature: Adding Customer Recovery Consent to core, assigning it to the customer

This commit is contained in:
2026-09-25 14:12:55 +03:00
parent 8fdaeda0ba
commit 935b1d02f9
4 changed files with 83 additions and 0 deletions
@@ -0,0 +1,25 @@
<?php
namespace Modules\Core\Customer\Events;
use Illuminate\Contracts\Auth\Authenticatable;
use Modules\Core\Customer\Models\Customer;
/**
* Customer-side sibling of Checkout\Events\RecoveryConsentSet — dispatched
* by CustomerAccountService::setRecoveryConsent() every time the account's
* standing promotional/abandoned-cart-recovery opt-in changes, including
* an explicit opt-OUT, not just an opt-in. $consent is the new value,
* already written to Customer::meta by the time this fires. Distinct from
* RecoveryConsentSet, which fires for the current CART's own opt-in
* (CheckoutService::setRecoveryConsent()) — the two write the same meta
* shape onto different models and can fire independently of each other.
*/
class CustomerRecoveryConsentSet
{
public function __construct(
public readonly Customer $customer,
public readonly bool $consent,
public readonly Authenticatable $causer,
) {}
}
@@ -8,6 +8,7 @@ use Modules\Core\Customer\Events\CustomerAddressCreated;
use Modules\Core\Customer\Events\CustomerAddressDeleted; use Modules\Core\Customer\Events\CustomerAddressDeleted;
use Modules\Core\Customer\Events\CustomerAddressUpdated; use Modules\Core\Customer\Events\CustomerAddressUpdated;
use Modules\Core\Customer\Events\CustomerProfileUpdated; use Modules\Core\Customer\Events\CustomerProfileUpdated;
use Modules\Core\Customer\Events\CustomerRecoveryConsentSet;
use Modules\Core\Logging\ActivityLogService; use Modules\Core\Logging\ActivityLogService;
/** /**
@@ -62,4 +63,21 @@ class LogCustomerAccountActivity implements ShouldQueue
$event->causer, $event->causer,
); );
} }
/**
* CustomerRecoveryConsentSet carries only the new value, not a
* before/after snapshot the way CustomerProfileUpdated does — but
* CustomerAccountService::setRecoveryConsent() only ever dispatches it
* once the value has actually changed, so "old" is trivially the
* opposite of $event->consent.
*/
public function handleRecoveryConsentSet(CustomerRecoveryConsentSet $event): void
{
$this->activityLog->updated(
$event->customer,
['recovery_consent' => ! $event->consent],
['recovery_consent' => $event->consent],
$event->causer,
);
}
} }
@@ -13,6 +13,7 @@ use Modules\Core\Customer\Events\CustomerAddressCreated;
use Modules\Core\Customer\Events\CustomerAddressDeleted; use Modules\Core\Customer\Events\CustomerAddressDeleted;
use Modules\Core\Customer\Events\CustomerAddressUpdated; use Modules\Core\Customer\Events\CustomerAddressUpdated;
use Modules\Core\Customer\Events\CustomerProfileUpdated; use Modules\Core\Customer\Events\CustomerProfileUpdated;
use Modules\Core\Customer\Events\CustomerRecoveryConsentSet;
use Modules\Core\Customer\Exceptions\AddressNotFoundException; use Modules\Core\Customer\Exceptions\AddressNotFoundException;
use Modules\Core\Customer\Exceptions\OrderNotFoundException; use Modules\Core\Customer\Exceptions\OrderNotFoundException;
use Modules\Core\Customer\Models\Customer; use Modules\Core\Customer\Models\Customer;
@@ -235,6 +236,43 @@ class CustomerAccountService
return $customer; return $customer;
} }
/**
* The account's standing "email me a reminder if I don't finish my
* order" opt-in — same meta shape Checkout\Services\CheckoutService::
* setRecoveryConsent() writes on the current CART (recovery_consent,
* recovery_consent_at, recovery_consent_policy_version), written here
* onto the CUSTOMER instead, so it survives across carts/sessions as a
* standing account preference. The two are independent: opting out on
* the customer doesn't retroactively change a cart already opted in,
* and vice versa — a caller that wants both kept in sync (e.g. 3dealer
* applying a customer's standing preference to the current cart too)
* calls both services itself.
*
* A no-op (no write, no event) when $consent already matches what's
* stored — unlike updateProfile()'s address/profile writes, which
* always write and dispatch even when nothing actually changed.
*/
public function setRecoveryConsent(Authenticatable $user, bool $consent): Customer
{
$customer = $this->customerOrFail($user);
if ((bool) data_get($customer->meta, 'recovery_consent') === $consent) {
return $customer;
}
$customer->meta = [
...($customer->meta?->toArray() ?? []),
'recovery_consent' => $consent,
'recovery_consent_at' => $consent ? now()->toIso8601String() : null,
'recovery_consent_policy_version' => $consent ? config('legal.privacy_policy_version') : null,
];
$customer->save();
Event::dispatch(new CustomerRecoveryConsentSet($customer, $consent, $user));
return $customer;
}
/** /**
* @throws LogicException if $user has no paired Customer at all — * @throws LogicException if $user has no paired Customer at all —
* distinct from AddressNotFoundException/OrderNotFoundException * distinct from AddressNotFoundException/OrderNotFoundException
@@ -12,6 +12,7 @@ use Modules\Core\Customer\Events\CustomerAddressCreated;
use Modules\Core\Customer\Events\CustomerAddressDeleted; use Modules\Core\Customer\Events\CustomerAddressDeleted;
use Modules\Core\Customer\Events\CustomerAddressUpdated; use Modules\Core\Customer\Events\CustomerAddressUpdated;
use Modules\Core\Customer\Events\CustomerProfileUpdated; use Modules\Core\Customer\Events\CustomerProfileUpdated;
use Modules\Core\Customer\Events\CustomerRecoveryConsentSet;
use Modules\Core\Customer\Listeners\ClaimGuestOrdersOnLogin; use Modules\Core\Customer\Listeners\ClaimGuestOrdersOnLogin;
use Modules\Core\Customer\Listeners\CreateCustomerForUser; use Modules\Core\Customer\Listeners\CreateCustomerForUser;
use Modules\Core\Customer\Listeners\LogCustomerAccountActivity; use Modules\Core\Customer\Listeners\LogCustomerAccountActivity;
@@ -43,5 +44,6 @@ class CustomerServiceProvider extends ServiceProvider
Event::listen(CustomerAddressUpdated::class, [LogCustomerAccountActivity::class, 'handleAddressUpdated']); Event::listen(CustomerAddressUpdated::class, [LogCustomerAccountActivity::class, 'handleAddressUpdated']);
Event::listen(CustomerAddressDeleted::class, [LogCustomerAccountActivity::class, 'handleAddressDeleted']); Event::listen(CustomerAddressDeleted::class, [LogCustomerAccountActivity::class, 'handleAddressDeleted']);
Event::listen(CustomerProfileUpdated::class, [LogCustomerAccountActivity::class, 'handleProfileUpdated']); Event::listen(CustomerProfileUpdated::class, [LogCustomerAccountActivity::class, 'handleProfileUpdated']);
Event::listen(CustomerRecoveryConsentSet::class, [LogCustomerAccountActivity::class, 'handleRecoveryConsentSet']);
} }
} }