109 lines
3.1 KiB
PHP
109 lines
3.1 KiB
PHP
<?php
|
|
|
|
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 {
|
|
$email = strtolower(trim((string) $email));
|
|
|
|
if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
return null;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
}
|
|
|
|
$created = false;
|
|
|
|
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]);
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
$localPart = strstr($email, '@', true) ?: 'SonarDesk Customer';
|
|
$localPart = trim(str_replace(['.', '_', '-'], ' ', $localPart));
|
|
|
|
return mb_substr(
|
|
$localPart !== ''
|
|
? ucwords($localPart)
|
|
: 'SonarDesk Customer',
|
|
0,
|
|
120
|
|
);
|
|
}
|
|
}
|