Merge pull request #210 from goteo/feat/enriched-project-supports
feat: Enriched ProjectSupports
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260522111940 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add matchfunding bool property to ProjectSupport';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE project_support ADD matchfunding TINYINT(1) DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE project_support DROP matchfunding');
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,12 @@ use App\ApiResource\Accounting\AccountingApiResource;
|
||||
use App\ApiResource\Accounting\TransactionApiResource;
|
||||
use App\ApiResource\MoneyOutput;
|
||||
use App\Entity\Project\Support;
|
||||
use App\Mapping\Transformer\SupportDisplayNameMapTransformer;
|
||||
use App\Money\Totalization\TotalizedMoney;
|
||||
use App\State\ApiResourceStateProvider;
|
||||
use App\State\MoneyTotalStateProvider;
|
||||
use App\State\Project\SupportStateProcessor;
|
||||
use AutoMapper\Attribute\MapFrom;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
@@ -76,6 +78,10 @@ class SupportApiResource
|
||||
#[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')]
|
||||
public ?AccountingApiResource $origin;
|
||||
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[MapFrom(Support::class, transformer: SupportDisplayNameMapTransformer::class)]
|
||||
public string $displayName;
|
||||
|
||||
/**
|
||||
* The Transactions that were issued to the Project by the origin.
|
||||
*
|
||||
@@ -98,6 +104,13 @@ class SupportApiResource
|
||||
#[API\ApiFilter(BooleanFilter::class)]
|
||||
public bool $anonymous = true;
|
||||
|
||||
/**
|
||||
* If this ProjectSupport comes from a MatchCall this flag will be true.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[API\ApiFilter(BooleanFilter::class)]
|
||||
public bool $matchfunding;
|
||||
|
||||
/**
|
||||
* A message of support from the User to the Project.
|
||||
*/
|
||||
|
||||
@@ -86,7 +86,7 @@ trait TerritoryPumpTrait
|
||||
private const COMMON_VARIATIONS = [
|
||||
'PERÚ' => ['PÉROU'],
|
||||
'ECUADOR' => ['EQUADOR'],
|
||||
'MÉXICO' => ['MESSICO', 'MX'],
|
||||
'MÉXICO' => ['MEXICO', 'MESSICO', 'MX'],
|
||||
'CIUDAD DE MÉXICO' => ['CDMX'],
|
||||
'EUROPA' => ['EUROPEAN UNION', 'EUROPE'],
|
||||
'FRANCIA' => ['FRANCE'],
|
||||
@@ -124,7 +124,7 @@ trait TerritoryPumpTrait
|
||||
'VIZCAYA' => ['BIZKAIA'],
|
||||
'IRÚN' => ['IRUN'],
|
||||
'NAVARRA' => ['NAVARRE', 'NAFARROA'],
|
||||
'PAMLONA' => ['IRUÑEA'],
|
||||
'PAMPLONA' => ['IRUÑEA'],
|
||||
'CANARIAS' => ['ISLAS CANARIAS', 'CANARY ISLANDS'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Entity\Project;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\EmbeddableMoney;
|
||||
use App\Entity\Matchfunding\MatchCall;
|
||||
use App\Mapping\Provider\EntityMapProvider;
|
||||
use App\Repository\Project\SupportRepository;
|
||||
use AutoMapper\Attribute\MapProvider;
|
||||
@@ -50,6 +51,9 @@ class Support
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $message = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $matchfunding = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->transactions = new ArrayCollection();
|
||||
@@ -68,6 +72,7 @@ class Support
|
||||
public function setOrigin(?Accounting $origin): static
|
||||
{
|
||||
$this->origin = $origin;
|
||||
$this->matchfunding = $origin->getOwnerClass() === MatchCall::class;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -143,4 +148,9 @@ class Support
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isMatchfunding(): ?bool
|
||||
{
|
||||
return $this->matchfunding;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account
|
||||
private ?array $links = null;
|
||||
|
||||
#[ORM\Embedded(class: Territory::class)]
|
||||
private ?object $territory = null;
|
||||
private ?Territory $territory = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $description = null;
|
||||
@@ -382,12 +382,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTerritory(): ?object
|
||||
public function getTerritory(): ?Territory
|
||||
{
|
||||
return $this->territory;
|
||||
}
|
||||
|
||||
public function setTerritory(?object $territory): static
|
||||
public function setTerritory(?Territory $territory): static
|
||||
{
|
||||
$this->territory = $territory;
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\Project\Support;
|
||||
use App\Gateway\CheckoutStatus;
|
||||
use App\Service\Project\SupportService;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||
use Doctrine\ORM\Event\PreUpdateEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
#[AsEntityListener(
|
||||
event: Events::preUpdate,
|
||||
method: 'preUpdate',
|
||||
entity: Checkout::class
|
||||
)]
|
||||
class CheckoutSupportsListener
|
||||
{
|
||||
use RecomputingListenerTrait;
|
||||
|
||||
public function __construct(
|
||||
private SupportService $supportService,
|
||||
) {}
|
||||
|
||||
public function preUpdate(Checkout $checkout, PreUpdateEventArgs $event): void
|
||||
{
|
||||
if (!$event->hasChangedField('status')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event->getNewValue('status') !== CheckoutStatus::Charged) {
|
||||
return;
|
||||
}
|
||||
|
||||
$supports = $this->prepareSupports($checkout);
|
||||
|
||||
foreach ($supports as $support) {
|
||||
$this->computeChangeSet($event->getObjectManager(), $support);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Support[]
|
||||
*/
|
||||
public function prepareSupports(Checkout $checkout): array
|
||||
{
|
||||
/** @var Charge[] */
|
||||
$charges = $checkout->getCharges()->toArray();
|
||||
$origin = $checkout->getOrigin();
|
||||
|
||||
$projects = [];
|
||||
$transactionsByProject = [];
|
||||
foreach ($charges as $charge) {
|
||||
$project = $charge->getTarget()->getProject();
|
||||
|
||||
if (!$project) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$projectId = $project->getId();
|
||||
|
||||
$projects[$projectId] = $project;
|
||||
$transactionsByProject[$projectId] = [
|
||||
...$transactionsByProject[$projectId] ?? [],
|
||||
...$charge->getTransactions(),
|
||||
];
|
||||
}
|
||||
|
||||
$supports = [];
|
||||
foreach ($transactionsByProject as $projectId => $transactions) {
|
||||
$support = $this->supportService->getSupport($projects[$projectId], $origin);
|
||||
$support = $this->supportService->withTransactions($support, $transactions);
|
||||
$supports[] = $support;
|
||||
}
|
||||
|
||||
return $supports;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\EmbeddableMoney;
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Gateway\Checkout;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Project\Support;
|
||||
use App\Money\Money;
|
||||
use App\Money\MoneyService;
|
||||
use App\Repository\Project\SupportRepository;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Event\PreFlushEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
#[AsEntityListener(
|
||||
event: Events::preFlush,
|
||||
method: 'preFlush',
|
||||
entity: Checkout::class
|
||||
)]
|
||||
class GatewayCheckoutSupportsListener
|
||||
{
|
||||
public function __construct(
|
||||
private SupportRepository $supportRepository,
|
||||
private MoneyService $moneyService,
|
||||
) {}
|
||||
|
||||
public function preFlush(Checkout $checkout, PreFlushEventArgs $args): void
|
||||
{
|
||||
/** @var EntityManagerInterface */
|
||||
$em = $args->getObjectManager();
|
||||
$uow = $em->getUnitOfWork();
|
||||
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if (!$entity instanceof Checkout) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$changes = $uow->getEntityChangeSet($entity);
|
||||
|
||||
if (!isset($changes['status']) || !$entity->isCharged()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$supports = $this->prepareSupports($entity);
|
||||
|
||||
foreach ($supports as $support) {
|
||||
$em->persist($support);
|
||||
$uow->computeChangeSet($em->getClassMetadata(Support::class), $support);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Support[]
|
||||
*/
|
||||
public function prepareSupports(Checkout $checkout): array
|
||||
{
|
||||
/** @var Charge[] */
|
||||
$charges = $checkout->getCharges()->toArray();
|
||||
$origin = $checkout->getOrigin();
|
||||
|
||||
$projects = [];
|
||||
$transactionsByProject = [];
|
||||
foreach ($charges as $charge) {
|
||||
$project = $charge->getTarget()->getProject();
|
||||
|
||||
if (!$project) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$projectId = $project->getId();
|
||||
|
||||
$projects[$projectId] = $project;
|
||||
$transactionsByProject[$projectId] = [
|
||||
...$transactionsByProject[$projectId] ?? [],
|
||||
...$charge->getTransactions(),
|
||||
];
|
||||
}
|
||||
|
||||
$supports = [];
|
||||
foreach ($transactionsByProject as $projectId => $transactions) {
|
||||
$supports[] = $this->getSupport($projects[$projectId], $origin, $transactions);
|
||||
}
|
||||
|
||||
return $supports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ProjectSupport for the given data.
|
||||
*
|
||||
* @param Transaction[] $transactions
|
||||
*/
|
||||
private function getSupport(Project $project, Accounting $origin, array $transactions): Support
|
||||
{
|
||||
/** @var Support|null */
|
||||
$projectSupport = $this->supportRepository->findOneBy([
|
||||
'project' => $project,
|
||||
'origin' => $origin,
|
||||
]);
|
||||
|
||||
if (!$projectSupport) {
|
||||
$projectSupport = new Support();
|
||||
}
|
||||
|
||||
$projectSupport->setProject($project);
|
||||
$projectSupport->setOrigin($origin);
|
||||
$projectSupport->setAnonymous(false);
|
||||
|
||||
$money = new Money(0, $project->getAccounting()->getCurrency());
|
||||
foreach ($transactions as $transaction) {
|
||||
$money = $this->moneyService->add($transaction->getMoney(), $money);
|
||||
|
||||
$projectSupport->addTransaction($transaction);
|
||||
}
|
||||
|
||||
$projectSupport->setMoney(EmbeddableMoney::of($money));
|
||||
|
||||
return $projectSupport;
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,11 @@
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Gateway\ChargeStatus;
|
||||
use App\Service\Matchfunding\MatchfundingService;
|
||||
use App\Service\Project\SupportService;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Event\PreUpdateEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
@@ -15,52 +16,45 @@ use Doctrine\ORM\Events;
|
||||
method: 'preUpdate',
|
||||
entity: Charge::class
|
||||
)]
|
||||
#[AsEntityListener(
|
||||
event: Events::postUpdate,
|
||||
method: 'postUpdate',
|
||||
entity: Charge::class
|
||||
)]
|
||||
final class MatchfundingListener
|
||||
{
|
||||
private ?Charge $charge = null;
|
||||
use RecomputingListenerTrait;
|
||||
|
||||
public function __construct(
|
||||
private MatchfundingService $matchfunding,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private SupportService $supportService,
|
||||
) {}
|
||||
|
||||
public function preUpdate(Charge $charge, PreUpdateEventArgs $event)
|
||||
public function preUpdate(Charge $charge, PreUpdateEventArgs $event): void
|
||||
{
|
||||
$this->charge = null;
|
||||
|
||||
if (count($event->getEntityChangeSet()) === 0) {
|
||||
if (!$event->hasChangedField('status')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($charge->getStatus() !== ChargeStatus::InCharge) {
|
||||
if ($event->getNewValue('status') !== ChargeStatus::InCharge) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target = $charge->getTarget();
|
||||
if (!$target instanceof Project) {
|
||||
return;
|
||||
}
|
||||
|
||||
$transactions = $this->matchfunding->match($charge);
|
||||
|
||||
if (\count($transactions) > 0) {
|
||||
foreach ($transactions as $transaction) {
|
||||
$charge->addTransaction($transaction);
|
||||
}
|
||||
|
||||
$this->charge = $charge;
|
||||
}
|
||||
}
|
||||
|
||||
public function postUpdate()
|
||||
{
|
||||
if ($this->charge === null) {
|
||||
if ($transactions === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->entityManager->persist($this->charge);
|
||||
$this->entityManager->flush();
|
||||
$em = $event->getObjectManager();
|
||||
foreach ($transactions as $transaction) {
|
||||
$support = $this->supportService->getSupport($target, $charge->getCheckout()->getOrigin());
|
||||
$support = $this->supportService->withTransactions($support, [$transaction]);
|
||||
$this->computeChangeSet($em, $support);
|
||||
|
||||
$this->charge = null;
|
||||
$matchSupport = $this->supportService->getSupport($target, $transaction->getOrigin());
|
||||
$matchSupport = $this->supportService->withTransactions($matchSupport, [$transaction]);
|
||||
$matchSupport->setAnonymous(false);
|
||||
$this->computeChangeSet($em, $matchSupport);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
trait RecomputingListenerTrait
|
||||
{
|
||||
/**
|
||||
* Appropriately updates the Doctrine's Unit of Work and marks entities for persistence.
|
||||
*/
|
||||
private function computeChangeSet(EntityManagerInterface $em, object $entity): void
|
||||
{
|
||||
$uow = $em->getUnitOfWork();
|
||||
$meta = $em->getClassMetadata($entity::class);
|
||||
|
||||
$em->persist($entity);
|
||||
$uow->computeChangeSet($meta, $entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mapping\Transformer;
|
||||
|
||||
use App\Entity\Accounting\AccountingOwnerInterface;
|
||||
use App\Entity\Matchfunding\MatchCall;
|
||||
use App\Entity\Project\Support;
|
||||
use App\Entity\User\User;
|
||||
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface;
|
||||
|
||||
class SupportDisplayNameMapTransformer implements PropertyTransformerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private UserDisplayNameMapTransformer $userDisplayNameMapTransformer,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param Support $source
|
||||
*/
|
||||
public function transform(mixed $value, object|array $source, array $context): mixed
|
||||
{
|
||||
$origin = $source->getOrigin();
|
||||
if (!$origin) {
|
||||
throw new \Exception(\sprintf('The Support with ID %s does not have an origin', $source->getId()));
|
||||
}
|
||||
|
||||
return match ($origin->getOwnerClass()) {
|
||||
User::class => $this->userDisplayNameMapTransformer->transform($value, $origin->getUser(), $context),
|
||||
MatchCall::class => $origin->getMatchCall()->getTitle(),
|
||||
default => $this->attemptDisplayName($origin->getOwner()),
|
||||
};
|
||||
}
|
||||
|
||||
private function attemptDisplayName(AccountingOwnerInterface $owner): string
|
||||
{
|
||||
if (\method_exists($owner, 'getTitle')) {
|
||||
return $owner->getTitle();
|
||||
}
|
||||
|
||||
if (\method_exists($owner, 'getName')) {
|
||||
return $owner->getName();
|
||||
}
|
||||
|
||||
throw new \Exception(\sprintf("AccountingOwnerInterface '%s' does not provide a displayable name", $owner::class));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Service\Matchfunding;
|
||||
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\EmbeddableMoney;
|
||||
use App\Entity\Gateway\Charge;
|
||||
use App\Entity\Matchfunding\MatchAgainst;
|
||||
use App\Entity\Matchfunding\MatchCallSubmissionStatus;
|
||||
@@ -61,7 +62,7 @@ class MatchfundingService
|
||||
->match($strategy->getFactor(), $toBeMatched, $strategy->getLimit());
|
||||
|
||||
$transaction = new Transaction();
|
||||
$transaction->setMoney($matched);
|
||||
$transaction->setMoney(EmbeddableMoney::of($matched));
|
||||
$transaction->setOrigin($submission->getCall()->getAccounting());
|
||||
$transaction->setTarget($submission->getProject()->getAccounting());
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ class NominatimService
|
||||
{
|
||||
public const NOMINATIM_BASE_URI = 'https://nominatim.openstreetmap.org';
|
||||
|
||||
private float $lastRequestTime = 0.0;
|
||||
|
||||
/**
|
||||
* Cache calls to Nominatim for 1 hour.
|
||||
* This ensures freshness of the data but minimizes bulk-operations hit rates to Nominatim.
|
||||
@@ -46,6 +48,7 @@ class NominatimService
|
||||
function (CacheItemInterface $item) use ($endpoint, $options) {
|
||||
$item->expiresAfter(self::NOMINATIM_CACHE_TTL);
|
||||
|
||||
$this->throttle();
|
||||
$response = $this->httpClient->request('GET', $endpoint, $options);
|
||||
|
||||
return \json_decode($response->getContent(), true);
|
||||
@@ -76,4 +79,18 @@ class NominatimService
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function throttle(): void
|
||||
{
|
||||
$minInterval = 1.1;
|
||||
|
||||
$now = microtime(true);
|
||||
$delta = $now - $this->lastRequestTime;
|
||||
|
||||
if ($delta < $minInterval) {
|
||||
usleep((int) (($minInterval - $delta) * 1000000));
|
||||
}
|
||||
|
||||
$this->lastRequestTime = microtime(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Project;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Accounting\Transaction;
|
||||
use App\Entity\EmbeddableMoney;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Project\Support;
|
||||
use App\Money\Money;
|
||||
use App\Money\MoneyService;
|
||||
use App\Repository\Project\SupportRepository;
|
||||
|
||||
/**
|
||||
* Helpers to work with `App\Entity\Project\Support` entities.
|
||||
*/
|
||||
class SupportService
|
||||
{
|
||||
public function __construct(
|
||||
private SupportRepository $supportRepository,
|
||||
private MoneyService $moneyService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Obtain a Support (new or existing) for a Project and origin.
|
||||
*/
|
||||
public function getSupport(
|
||||
Project $project,
|
||||
Accounting $origin,
|
||||
): Support {
|
||||
/** @var Support|null $support */
|
||||
$support = $this->supportRepository->findOneBy([
|
||||
'project' => $project,
|
||||
'origin' => $origin,
|
||||
]);
|
||||
|
||||
if ($support === null) {
|
||||
$support = new Support();
|
||||
$support->setProject($project);
|
||||
$support->setOrigin($origin);
|
||||
}
|
||||
|
||||
return $support;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new Transactions and recalculate the Money for an existing Support.
|
||||
*
|
||||
* @param Transaction[] $transactions
|
||||
*/
|
||||
public function withTransactions(Support $support, array $transactions): Support
|
||||
{
|
||||
foreach ($transactions as $transaction) {
|
||||
$support->addTransaction($transaction);
|
||||
}
|
||||
|
||||
$money = new Money(0, $support->getProject()->getAccounting()->getCurrency());
|
||||
foreach ($support->getTransactions() as $transaction) {
|
||||
$money = $this->moneyService->add($transaction->getMoney(), $money);
|
||||
}
|
||||
|
||||
$support->setMoney(EmbeddableMoney::of($money));
|
||||
|
||||
return $support;
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ class UserService
|
||||
// Only lowercase a-z, numbers, underscore and middle dots in user handles
|
||||
$nval = \preg_replace('/[^a-z0-9_.]|^\.|\.$/', '_', \strtolower($nval));
|
||||
|
||||
$nval = \str_pad($nval, $min, \sprintf('#%s', \hash('md5', $value)));
|
||||
$nval = \str_pad($nval, $min, \sprintf('_%s', \hash('md5', $value)));
|
||||
$nval = \substr($nval, 0, $max);
|
||||
|
||||
if (strlen(str_replace('_', '', $nval)) < 1) {
|
||||
|
||||
Reference in New Issue
Block a user