Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions packages/core/config/customers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

return [
/*
|--------------------------------------------------------------------------
| Customer Deletion Strategy
|--------------------------------------------------------------------------
|
| This option controls the behavior when deleting a customer. You can choose
| between 'anonymize' (recommended for GDPR compliance) or 'delete' (hard delete).
| When using 'anonymize', customer data is anonymized but records are preserved
| for data integrity. When using 'delete', customer data is permanently removed.
|
| Supported: "anonymize", "delete"
|
*/
'deletion_strategy' => env('LUNAR_CUSTOMER_DELETION_STRATEGY', 'anonymize'),

/*
|--------------------------------------------------------------------------
| Anonymization Fields
|--------------------------------------------------------------------------
|
| Define the fields and their replacement values when anonymizing customer data.
| Use :id as a placeholder for the customer ID in replacement values.
|
*/
'anonymization_fields' => [
'first_name' => 'Anonymous',
'last_name' => 'Customer',
'company_name' => null,
'tax_identifier' => null,
'account_ref' => null,
'meta' => [],
'attribute_data' => [],
],

/*
|--------------------------------------------------------------------------
| Preserve Order Data
|--------------------------------------------------------------------------
|
| When true, order data will be preserved even when deleting a customer.
| This is often required for legal and accounting purposes. Orders will
| be anonymized but not deleted.
|
*/
'preserve_order_data' => true,

/*
|--------------------------------------------------------------------------
| Delete Orphaned Users
|--------------------------------------------------------------------------
|
| When true, if a user is only associated with the customer being deleted,
| the user account will also be deleted to free up the email address.
|
*/
'delete_orphaned_users' => true,

/*
|--------------------------------------------------------------------------
| Handle Cart Data
|--------------------------------------------------------------------------
|
| Define how to handle cart data when deleting a customer.
| Options: "delete" (remove carts), "anonymize" (keep but anonymize)
|
*/
'cart_handling' => 'delete',
];
166 changes: 166 additions & 0 deletions packages/core/src/Actions/Customers/AnonymizeCustomer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

namespace Lunar\Actions\Customers;

use Illuminate\Support\Facades\DB;
use Lunar\Actions\AbstractAction;
use Lunar\Models\Address;
use Lunar\Models\Cart;
use Lunar\Models\Customer;
use Lunar\Models\Order;

class AnonymizeCustomer extends AbstractAction
{
/**
* Execute the action.
*/
public function execute(Customer $customer): self
{
$this->passThrough = DB::transaction(function () use ($customer) {
$this->anonymizeRelatedData($customer);
$this->anonymizeCustomerData($customer);
$this->handleOrphanedUsers($customer);

return $customer->fresh();
});

return $this;
}

/**
* Anonymize customer's personal data.
*/
protected function anonymizeCustomerData(Customer $customer): void
{
$fields = config('lunar.customers.anonymization_fields', [
'first_name' => 'Anonymous',
'last_name' => 'Customer',
'company_name' => null,
'tax_identifier' => null,
'account_ref' => null,
'meta' => [],
'attribute_data' => [],
]);

$updateData = collect($fields)->map(function ($value) use ($customer) {
if (is_string($value) && $value === ':id') {
return "customer_{$customer->id}";
}
if (is_string($value) && str_contains($value, ':id')) {
return str_replace(':id', "customer_{$customer->id}", $value);
}

return $value;
})->all();

// Add anonymization timestamp to meta
$meta = $updateData['meta'] ?? [];
if (is_array($meta)) {
$meta['anonymized_at'] = now()->toIso8601String();
$meta['anonymized'] = true;
$updateData['meta'] = $meta;
}

$customer->update($updateData);
}

/**
* Anonymize related data.
*/
protected function anonymizeRelatedData(Customer $customer): void
{
// Handle carts
$cartHandling = config('lunar.customers.cart_handling', 'delete');
if ($cartHandling === 'delete') {
$customer->carts()->each(function (Cart $cart) {
$cart->lines()->delete();
$cart->forceDelete();
});
} else {
$customer->carts()->update(['customer_id' => null]);
}

// Handle orders
if (config('lunar.customers.preserve_order_data', true)) {
$customer->orders()->each(function (Order $order) {
$order->update([
'customer_id' => null,
'user_id' => null,
]);

// Anonymize order addresses
if ($order->shippingAddress) {
$this->anonymizeAddress($order->shippingAddress);
}
if ($order->billingAddress) {
$this->anonymizeAddress($order->billingAddress);
}
});
}

// Handle customer addresses
$customer->addresses()->each(function (Address $address) {
$this->anonymizeAddress($address);
});

// Detach relationships
$customer->customerGroups()->detach();
$customer->discounts()->detach();

// Handle wishlists if available
if (method_exists($customer, 'wishlists')) {
$customer->wishlists()->delete();
}
}

/**
* Anonymize an address record.
*/
protected function anonymizeAddress(Address $address): void
{
$address->update([
'first_name' => 'Anonymous',
'last_name' => 'Customer',
'company_name' => null,
'line_one' => 'Anonymized',
'line_two' => null,
'line_three' => null,
'city' => 'Anonymized',
'state' => null,
'postcode' => '00000',
'delivery_instructions' => null,
'contact_email' => null,
'contact_phone' => null,
'meta' => [
'anonymized_at' => now()->toIso8601String(),
'anonymized' => true,
],
]);
}

/**
* Handle orphaned users.
*/
protected function handleOrphanedUsers(Customer $customer): void
{
if (! config('lunar.customers.delete_orphaned_users', true)) {
$customer->users()->detach();

return;
}

$customer->users->each(function ($user) use ($customer) {
// Check if user is associated with other customers
$hasOtherCustomers = Customer::whereHas('users', function ($query) use ($user) {
$query->where('user_id', $user->id);
})->where('id', '!=', $customer->id)->exists();

if (! $hasOtherCustomers) {
$user->delete();
} else {
// Just detach from this customer
$customer->users()->detach($user->id);
}
});
}
}
100 changes: 100 additions & 0 deletions packages/core/src/Actions/Customers/DeleteCustomer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

namespace Lunar\Actions\Customers;

use Illuminate\Support\Facades\DB;
use Lunar\Actions\AbstractAction;
use Lunar\Models\Cart;
use Lunar\Models\Customer;
use Lunar\Models\Order;

class DeleteCustomer extends AbstractAction
{
/**
* Execute the action.
*/
public function execute(Customer $customer): self
{
DB::transaction(function () use ($customer) {
// Handle all related data cleanup
$this->deleteRelatedData($customer);
$this->handleOrphanedUsers($customer);

// Delete the customer without triggering observer events to avoid duplication
// The observer does basic cleanup, but we've already done comprehensive cleanup
$customer::withoutEvents(function () use ($customer) {
$customer->forceDelete();
});
});

$this->passThrough = true;

return $this;
}

/**
* Delete related data.
*/
protected function deleteRelatedData(Customer $customer): void
{
// Delete carts and their lines
$customer->carts()->each(function (Cart $cart) {
$cart->lines()->delete();
$cart->forceDelete();
});

// Handle orders based on configuration
if (! config('lunar.customers.preserve_order_data', true)) {
$customer->orders()->each(function (Order $order) {
$order->lines()->delete();
$order->transactions()->delete();
$order->delete();
});
} else {
// Even in delete mode, preserve orders by anonymizing
$customer->orders()->update([
'customer_id' => null,
'user_id' => null,
]);
}

// Delete addresses
$customer->addresses()->delete();

// Detach many-to-many relationships
$customer->customerGroups()->detach();
$customer->discounts()->detach();

// Delete wishlists if available
if (method_exists($customer, 'wishlists')) {
$customer->wishlists()->delete();
}
}

/**
* Handle orphaned users cleanup.
*/
protected function handleOrphanedUsers(Customer $customer): void
{
if (! config('lunar.customers.delete_orphaned_users', true)) {
$customer->users()->detach();

return;
}

$customer->users->each(function ($user) use ($customer) {
// Check if user is associated with other customers
$hasOtherCustomers = Customer::whereHas('users', function ($query) use ($user) {
$query->where('user_id', $user->id);
})->where('id', '!=', $customer->id)->exists();

if (! $hasOtherCustomers) {
// This user is only associated with this customer, delete it
$user->delete();
} else {
// Just detach from this customer
$customer->users()->detach($user->id);
}
});
}
}
38 changes: 38 additions & 0 deletions packages/core/src/Actions/Customers/ProcessCustomerDeletion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Lunar\Actions\Customers;

use Lunar\Actions\AbstractAction;
use Lunar\Events\Customers\CustomerDeleted;
use Lunar\Events\Customers\CustomerDeleting;
use Lunar\Models\Customer;

class ProcessCustomerDeletion extends AbstractAction
{
/**
* Execute the action.
*/
public function execute(Customer $customer, ?string $strategy = null): self
{
// Use provided strategy or fall back to config
$strategy = $strategy ?: config('lunar.customers.deletion_strategy', 'anonymize');

// Dispatch deleting event
CustomerDeleting::dispatch($customer, $strategy);

// Execute the appropriate strategy
if ($strategy === 'delete') {
DeleteCustomer::run($customer);
} else {
AnonymizeCustomer::run($customer);
}

// Set passthrough before dispatching final event
$this->passThrough = $strategy === 'delete' ? true : $customer->fresh();

// Dispatch deleted event
CustomerDeleted::dispatch($strategy === 'delete' ? null : $customer, $strategy);

return $this;
}
}
Loading
Loading