*/ #[\Override] protected $fillable = [ 'email', ]; /** * The attributes that should be cast. * * @var array */ #[\Override] protected $casts = [ 'last_connection' => 'datetime', 'email_verified_at' => 'datetime', ]; // * EVENTS /** * Perform any actions required after the model boots. */ protected static function booted(): void { // * EVENT CLOSURES. static::creating(function (self $model): void { self::assertEmailIsUnique($model->id, $model->email); }); static::updating(function (self $model): void { self::assertEmailIsUnique($model->id, $model->email); }); static::deleting(function (self $model): void { throw_if($model->existsInCrmAndIsActivated(), RuntimeException::class, 'This cannot be done because of MesBr'); }); static::deleted(function (self $model): void { if ($model->forceDeleting) { if ($model->existsInCrm()) { $model->api_data->delete(); } $model->notify(new DestroyedCustomer()); } else { $model->notify(new DestroyCustomer()); } }); static::restored(function (): void { \session()->flash( 'warning', \trans('Votre compte à été réactivé.') ); }); } /** * Set email attribute in lower case. */ protected function email(): Attribute { return Attribute::make(set: fn (string $value): array => ['email' => Str::lower($value)]); } /** * Has Many Participation relation * * @return HasMany */ public function participations(): HasMany { return $this->hasMany(Participation::class, 'customer_id'); } /** * Activities logs One-to-many relationship. * * @return HasMany */ public function activityLogs(): HasMany { return $this->hasMany(ActivityLog::class); } // * METHODS /** * Supprimer les comptes inactivés depuis plus d'une semaine * de manière permanente. * * ! Les comptes non activés depuis une semaine ou les comptes supprimés depuis une semaine. */ public function prunable(): Builder { return static::query()->where(function (Builder $q): void { $q->where('updated_at', '<=', now()->subWeek()) ->whereNull('email_verified_at'); })->orWhere(function (Builder $q): void { $q->where('deleted_at', '<=', now()->subWeek()); }); } /** * Asserts using validation that the email is unique * * @throws ValidationException If user email as already used. */ private static function assertEmailIsUnique(mixed $id, mixed $email): void { self::assertFieldIsUnique($id, 'email', $email); } /** * Asserts that the field is unique * * @throws ValidationException If the field is not unique. */ private static function assertFieldIsUnique(mixed $id, string $field, mixed $value): void { Validator::make([$field => $value], [ $field => Rule::unique('customers', $field)->ignore($id)->withoutTrashed(), ], [ $field . '.unique' => trans('La valeur :value pour le champ :attribute est déjà utilisé', [ 'attribute' => $field, 'value' => $value, ]), ])->validate(); } // * --- AUTH --- /** * Send the email verification notification. */ public function sendEmailVerificationNotification(): void { $this->deleteIfDoesNotExistsInCrm(); $this->notify(new VerifyEmail()); } /** * Mark the given user's email as verified. * * @return bool * * @throws CrmException If an error occurs on server side or on getting response. * @throws ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs. * * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber */ public function markEmailAsVerified() { $this->deleteIfDoesNotExistsInCrm(); try { return $this->api_data->activate() && parent::markEmailAsVerified(); } catch (ApiErrorException $e) { if ($e->getApiError()->equals(ApiError::customerAlreadyActive)) { return parent::markEmailAsVerified(); } throw $e; } } /** * Send the password reset notification. */ public function sendPasswordResetNotification(mixed $token): void { $this->deleteIfDoesNotExistsInCrm(); /** @var string $token */ $this->notify(new ResetPassword($token)); } public function existsInCrmAndIsActivated(): bool { $crmCustomer = $this->getCrmCustomer(); return $crmCustomer instanceof CrmCustomer && $crmCustomer->status; } public function existsInCrm(): bool { return $this->getCrmCustomer() instanceof CrmCustomer; } /** * Force delete and logout if is logged in case the customer * does not exists in CRM. * * @throws ValidationException If the account is deleted because does not exists in CRM. */ private function deleteIfDoesNotExistsInCrm(): void { if ($this->existsInCrm()) { return; } /** @var static|null $loggedCustomer */ $loggedCustomer = \auth('frontend')->user(); $isLogged = $loggedCustomer?->id === $this->id && ! empty($this->id); if ($isLogged) { \auth('frontend')->logout(); } $this->delete(); throw ValidationException::withMessages([ 'error' => trans('Le compte \'existe pas.'), ]); } private function getCrmCustomer(): ?CrmCustomer { if (! $this->hashcode) { return null; } try { return CrmCustomer::find($this->hashcode); } catch (CrmException | ApiErrorException $e) { report($e); return null; } } /** * Get the password for the user. */ public function getAuthPassword(): string { return ''; } // * --- END AUTH --- /** * Get api_data attribute, the crm Data of a customer */ protected function apiData(): Attribute { return Attribute::make( get: fn (): CrmCustomer => self::cacheCrm($this->hashcode, 'apiData', function () { /** @var CrmApiV4 */ $crm = resolve('crmAPI'); $crmCustomer = $crm->getCustomer($this->hashcode); throw_unless($crmCustomer, CrmException::class, 'CRM get Customer returned null'); return $crmCustomer; }) ); } /** * Set the crm Data of a customer * * * @throws CrmException If an error occurs on server side or on getting response. * @throws ApiErrorException If any error or \LDC\LDCCRM\Enums\ApiError occurs. * * @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber */ public function updateApiData(array $data): bool { // phpcs:enable /** @var \LDC\LDCCRM\Models\Customer|null $crmCustomer */ $crmCustomer = $this->api_data; throw_unless($crmCustomer, CrmException::class, 'CRM get Customer returned null'); $crmCustomer->fill($data); $result = $crmCustomer->save(); self::clearCrmCached($crmCustomer->hashcode, 'apiData'); return $result; } // * CRM CACHE /** * Clear cached apiData */ public function clearApiDataCache(): void { /** @var \LDC\LDCCRM\Models\Customer */ $crmCustomer = $this->api_data; self::clearCrmCached($crmCustomer->hashcode, 'apiData'); } /** * Put customer crm part in cache * * @template T * * @param string $uuid The customer uuid. * @param string $part_name The part name for the cache path. * @param Closure():T $part The part to cache. * @param int $ttl Time to live in seconds for the cache part. * @return T * * @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint */ public static function cacheCrm(string $uuid, string $part_name, Closure $part, int $ttl = 60 * 60) { // phpcs:enable return Cache::remember("customers.{$uuid}.{$part_name}", $ttl, $part); } /** * Clear customer crm cache part or totally */ public static function clearCrmCached(string $uuid, string $part_name): void { $cache = "customers.{$uuid}"; if ($part_name) { $cache .= ".{$part_name}"; } Cache::forget($cache); } // * --- END CRM CACHE /** * This mutate Customer attributes to array * including only non critical properties and crm values * (One that can be send to front office without security breach) * * @return array */ public function toArray() { if ($this->hashcode) { return collect(array_merge( parent::toArray(), (array) $this->api_data ))->intersectByKeys([ 'email' => '', 'firstname' => '', 'lastname' => '', 'address' => '', 'address2' => '', 'postcode' => '', 'birth_date' => '', 'city' => '', 'country' => '', 'opt_in' => '', 'phone' => '', ])->toArray(); } return parent::toArray(); } }