/home/alixis5/integrate.sellofinance.com/src/Services
Edit: /home/alixis5/integrate.sellofinance.com/src/Services/AuthService.php (12728B)
accountRepo->create([
'name' => $fullName,
'type' => Account::TYPE_CUSTOMER,
'emailAddress' => $validated['emailAddress'],
]);
$accountId = $account->id;
} catch (EspoDuplicateException $e) {
$hasCustomerDup = false;
foreach ($e->getDuplicates() as $dup) {
if (($dup['type'] ?? '') === Account::TYPE_CUSTOMER) {
$hasCustomerDup = true;
break;
}
}
if ($hasCustomerDup) {
throw new \RuntimeException('DUPLICATE_EMAIL', 409, $e);
}
$account = $this->accountRepo->create([
'name' => $fullName,
'type' => Account::TYPE_CUSTOMER,
'emailAddress' => $validated['emailAddress'],
], true);
$accountId = $account->id;
}
// Step 2: Create AppUser — deduplicates on emailAddress and/or phoneNumber
$appUserData = [
'firstName' => $validated['firstName'] ?? null,
'lastName' => $validated['lastName'],
'emailAddress' => $validated['emailAddress'],
'phoneNumber' => $validated['phoneNumber'] ?? null,
'password' => $this->hashPassword($validated['password']),
'userType' => AppUser::TYPE_CUSTOMER,
'status' => AppUser::STATUS_PENDING,
'signupType' => $validated['signupType'] ?? AppUser::SIGNUP_EMAIL,
'preferredLanguage' => $validated['preferredLanguage'] ?? AppUser::LANG_EN,
'emailVerified' => false,
'mobileVerified' => false,
'mfaEnabled' => false,
'accountId' => $accountId,
];
try {
$appUser = $this->appUserRepo->create($appUserData);
$appUserId = $appUser->id;
} catch (EspoDuplicateException $e) {
$hasCustomerDup = false;
foreach ($e->getDuplicates() as $dup) {
if (($dup['userType'] ?? '') === AppUser::TYPE_CUSTOMER) {
$hasCustomerDup = true;
break;
}
}
if ($hasCustomerDup) {
try { $this->accountRepo->delete($accountId); } catch (\Throwable) {}
$fields = $e->getDuplicateFields($appUserData, ['phoneNumber', 'emailAddress']);
if (\in_array('phoneNumber', $fields, true)) throw new \RuntimeException('DUPLICATE_PHONE', 409, $e);
throw new \RuntimeException('DUPLICATE_EMAIL', 409, $e);
}
try {
$appUser = $this->appUserRepo->create($appUserData, true);
$appUserId = $appUser->id;
} catch (\Throwable $err) {
try { $this->accountRepo->delete($accountId); } catch (\Throwable) {}
throw new \RuntimeException($err->getMessage(), (int) $err->getCode(), $err);
}
} catch (EspoApiException $e) {
try { $this->accountRepo->delete($accountId); } catch (\Throwable) {}
if ($e->getCode() === 403) throw new \RuntimeException('ESPO_VALIDATION:' . $e->getMessage(), 403, $e);
throw new \RuntimeException($e->getMessage(), (int) $e->getCode(), $e);
}
// Step 3: Create CustomerProfile
try {
$profile = $this->profileRepo->create([
'name' => $fullName,
'appUserId' => $appUserId,
'accountId' => $accountId,
'profileType' => CustomerProfile::TYPE_INDIVIDUAL,
'status' => CustomerProfile::STATUS_DRAFT,
'onboardingStatus' => CustomerProfile::ONBOARDING_NOT_STARTED,
'preferredCurrency' => 'AED',
'creditConsentGiven' => false,
'marketingConsentGiven'=> false,
'existingBankCustomer' => false,
]);
} catch (\Throwable $e) {
try { $this->appUserRepo->delete($appUserId); } catch (\Throwable) {}
try { $this->accountRepo->delete($accountId); } catch (\Throwable) {}
throw new \RuntimeException($e->getMessage(), (int) $e->getCode(), $e);
}
$token = $this->jwt->issue([
'sub' => $appUserId,
'accountId' => $accountId,
'profileId' => $profile->id,
'userType' => AppUser::TYPE_CUSTOMER,
]);
return [
'token' => $token,
'expiresIn' => (int) $_ENV['JWT_EXPIRY_SECONDS'],
'user' => $this->buildUserResponse($appUser, $profile),
];
}
// ── Login ──────────────────────────────────────────────────────
public function login(array $validated): array
{
$appUser = $this->appUserRepo->findByEmail($validated['emailAddress']);
if ($appUser === null) {
throw new \RuntimeException('INVALID_CREDENTIALS');
}
// Customer portal only accepts customer-type users
if ($appUser->userType !== AppUser::TYPE_CUSTOMER) {
throw new \RuntimeException('INVALID_CREDENTIALS');
}
if (!$this->verifyPassword($validated['password'], $appUser->password ?? '')) {
$this->appUserRepo->incrementFailedLogins($appUser->id);
throw new \RuntimeException('INVALID_CREDENTIALS');
}
if ($appUser->status === AppUser::STATUS_SUSPENDED) {
throw new \RuntimeException('ACCOUNT_SUSPENDED');
}
if ($appUser->status === AppUser::STATUS_LOCKED) {
throw new \RuntimeException('ACCOUNT_LOCKED');
}
$this->appUserRepo->recordSuccessfulLogin($appUser->id);
$profile = $this->profileRepo->findByAppUserId($appUser->id);
// Auto-create a profile if one doesn't exist (e.g. user created directly in EspoCRM)
if ($profile === null && $appUser->userType === AppUser::TYPE_CUSTOMER) {
$fullName = trim(($appUser->firstName ?? '') . ' ' . $appUser->lastName);
$profile = $this->profileRepo->create([
'name' => $fullName,
'appUserId' => $appUser->id,
'accountId' => $appUser->accountId,
'profileType' => CustomerProfile::TYPE_INDIVIDUAL,
'status' => CustomerProfile::STATUS_DRAFT,
'onboardingStatus' => CustomerProfile::ONBOARDING_NOT_STARTED,
'preferredCurrency' => 'AED',
'creditConsentGiven' => false,
'marketingConsentGiven' => false,
'existingBankCustomer' => false,
]);
}
$token = $this->jwt->issue([
'sub' => $appUser->id,
'accountId' => $appUser->accountId,
'profileId' => $profile?->id,
'userType' => $appUser->userType,
]);
return [
'token' => $token,
'expiresIn' => (int) $_ENV['JWT_EXPIRY_SECONDS'],
'user' => $this->buildUserResponse($appUser, $profile),
];
}
/**
* Update the authenticated user's editable AppUser fields.
* Whitelisted: firstName, lastName, phoneNumber, preferredLanguage.
* Returns the same shape as login() to make client-side state replacement trivial.
*
* @param array
$patch
* @return array { user: ... }
* @throws \RuntimeException 404 if user not found
*/
public function updateMe(string $appUserId, array $patch): array
{
$allowed = array_intersect_key($patch, array_flip([
'firstName', 'lastName', 'phoneNumber', 'preferredLanguage',
]));
if (empty($allowed)) {
// Nothing valid to update — return current state without a write.
$user = $this->appUserRepo->findById($appUserId);
if ($user === null) {
throw new \RuntimeException('User not found', 404);
}
$profile = $this->profileRepo->findByAppUserId($appUserId);
return ['user' => $this->buildUserResponse($user, $profile)];
}
$updated = $this->appUserRepo->update($appUserId, $allowed);
$profile = $this->profileRepo->findByAppUserId($appUserId);
return ['user' => $this->buildUserResponse($updated, $profile)];
}
// ── Private helpers ────────────────────────────────────────────
private function buildUserResponse(AppUser $user, ?CustomerProfile $profile): array
{
return [
'id' => $user->id,
'accountId' => $user->accountId, // Persisted to localStorage by Angular
'firstName' => $user->firstName,
'lastName' => $user->lastName,
'emailAddress' => $user->emailAddress,
'phoneNumber' => $user->phoneNumber,
'emailVerified' => $user->emailVerified,
'mobileVerified' => $user->mobileVerified,
'userType' => $user->userType,
'status' => $user->status,
'preferredLanguage' => $user->preferredLanguage,
'ref' => $user->ref,
'createdAt' => $user->createdAt,
'profile' => $profile ? [
'id' => $profile->id,
'onboardingStatus' => $profile->onboardingStatus,
'onboardingStep' => $profile->onboardingStep,
'profileCompletenessPercent' => $profile->profileCompletenessPercent,
'profileType' => $profile->profileType,
] : null,
];
}
private function hashPassword(string $plaintext): string
{
return password_hash($plaintext, PASSWORD_BCRYPT, ['cost' => 12]);
}
private function verifyPassword(string $plaintext, string $hash): bool
{
return password_verify($plaintext, $hash);
}
}