update to provisioner code

This commit is contained in:
Mark Gregory
2026-09-30 12:38:04 +01:00
parent 15b8d9b75b
commit 5b3413549d
@@ -3,33 +3,74 @@
namespace App\Services\Accounts;
use App\Models\User;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use RuntimeException;
final class PurchasedAccountProvisioner
{
public function ensure(?string $email, ?string $name = null, bool $sendSetupLink = true): ?User
{
public function ensure(
?string $email,
?string $name = null,
bool $sendSetupLink = true
): ?User {
$email = strtolower(trim((string) $email));
if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
return null;
}
$user = User::query()->whereRaw('lower(email) = ?', [$email])->first();
/*
* Fast path for the normal case.
*
* Paddle can deliver several related webhook events concurrently, so
* this lookup alone cannot guarantee that another request will not
* create the user between this SELECT and our INSERT.
*/
$user = $this->findByEmail($email);
if ($user) {
return $user;
}
$user = User::create([
'name' => $this->displayName($name, $email),
'email' => $email,
'email_verified_at' => now(),
'password' => Str::random(64),
'is_admin' => false,
]);
$created = false;
if ($sendSetupLink) {
try {
$user = User::create([
'name' => $this->displayName($name, $email),
'email' => $email,
'email_verified_at' => now(),
'password' => Str::random(64),
'is_admin' => false,
]);
$created = true;
} catch (UniqueConstraintViolationException $exception) {
/*
* Another webhook may have created this account after our SELECT
* but before our INSERT. In that case the unique email constraint
* correctly rejects our INSERT; simply reuse the account that won
* the race.
*/
$user = $this->findByEmail($email);
if (! $user) {
/*
* The unique violation was not caused by the email account
* appearing concurrently, so preserve the original failure.
*/
throw $exception;
}
}
/*
* Only the request that actually created the account should send the
* account setup/reset link. Concurrent Paddle events must not result
* in several setup emails being sent to the customer.
*/
if ($created && $sendSetupLink) {
DB::afterCommit(static function () use ($email): void {
Password::sendResetLink(['email' => $email]);
});
@@ -38,9 +79,17 @@ final class PurchasedAccountProvisioner
return $user;
}
private function findByEmail(string $email): ?User
{
return User::query()
->whereRaw('LOWER(email) = ?', [$email])
->first();
}
private function displayName(?string $name, string $email): string
{
$name = trim((string) $name);
if ($name !== '') {
return mb_substr($name, 0, 120);
}
@@ -48,6 +97,12 @@ final class PurchasedAccountProvisioner
$localPart = strstr($email, '@', true) ?: 'SonarDesk Customer';
$localPart = trim(str_replace(['.', '_', '-'], ' ', $localPart));
return mb_substr($localPart !== '' ? ucwords($localPart) : 'SonarDesk Customer', 0, 120);
return mb_substr(
$localPart !== ''
? ucwords($localPart)
: 'SonarDesk Customer',
0,
120
);
}
}