Feat/adhesion payante front (#716)
* list only user groups with enabled services in create and edit service forms * feat: add platform membership payment (wip) * WIP * revert mollie api key * remove all option for visibility in services forms * add quit platform membership * remove quit platform feature * fix good route name for payment * fix review * fix review 2
This commit is contained in:
parent
32ccd2415e
commit
20251f6caf
42 changed files with 705 additions and 100 deletions
|
|
@ -47,8 +47,8 @@ security:
|
|||
access_control:
|
||||
- { path: ^/admin, roles: [ROLE_ADMIN, ROLE_GROUP_ADMIN] }
|
||||
# to synchronize with MyAccountAction
|
||||
- { path: ^/en/my-account/, roles: ROLE_USER }
|
||||
- { path: ^/fr/mon-compte/, roles: ROLE_USER }
|
||||
- { path: ^/en/my-account, roles: MEMBERSHIP_PAID }
|
||||
- { path: ^/fr/mon-compte, roles: MEMBERSHIP_PAID }
|
||||
|
||||
role_hierarchy:
|
||||
ROLE_ADMIN: [ROLE_USER, ROLE_ALLOWED_TO_SWITCH, ROLE_GROUP_ADMIN]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ App\Entity\Configuration:
|
|||
features (extends configuration_template):
|
||||
configuration:
|
||||
global:
|
||||
globalName: Echanges de biens et de services
|
||||
globalServicesEnabled: true
|
||||
globalPaidMembership: false
|
||||
notificationsSender:
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
App\Entity\PlatformOffer:
|
||||
platform_offer_1:
|
||||
id: <uuid('9040b3fb-8a01-4bbf-a228-ca9f90db5034')>
|
||||
platform_offer_template (template):
|
||||
configuration: '@features'
|
||||
|
||||
platform_offer_1 (extends platform_offer_template):
|
||||
id: <uuid('016b2a27-1037-6d47-bcdc-ec5efbd723f2')>
|
||||
name: Lorem ipsum
|
||||
price: 2000
|
||||
type: !php/enum App\Enum\OfferType::YEARLY
|
||||
|
||||
platform_offer_2:
|
||||
platform_offer_2 (extends platform_offer_template):
|
||||
name: Aliquet risus
|
||||
price: 200
|
||||
type: !php/enum App\Enum\OfferType::MONTHLY
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ App\Entity\User:
|
|||
firstname: 'Kevin'
|
||||
lastname: 'Pirouet'
|
||||
avatar: '7c732ddb-9c13-45eb-aea0-e614f2340e6d.jpg'
|
||||
membershipPaid: true
|
||||
type: !php/enum App\Enum\User\UserType::ADMIN
|
||||
roles: [ !php/const App\Entity\User::ROLE_ADMIN, !php/const App\Entity\User::MEMBERSHIP_PAID]
|
||||
|
||||
admin_apes (extends admin_template):
|
||||
id: <uuid('1ed69804-eeb9-6e6c-bce0-632c3a6846ba')>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ final class PlatformOfferCrudController extends AbstractCrudController implement
|
|||
|
||||
public function configureFields(string $pageName): iterable
|
||||
{
|
||||
$idFIeld = IdField::new('id')
|
||||
$idField = IdField::new('id')
|
||||
->setLabel('id')
|
||||
->hideOnForm();
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ final class PlatformOfferCrudController extends AbstractCrudController implement
|
|||
$currencyField,
|
||||
|
||||
$panels['tech_information'],
|
||||
$idFIeld,
|
||||
$idField,
|
||||
$updatedAtField,
|
||||
$createdAtField,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Payment;
|
||||
namespace App\Controller\Payment\Group;
|
||||
|
||||
use App\Controller\FlashTrait;
|
||||
use App\Controller\i18nTrait;
|
||||
|
|
@ -67,6 +67,7 @@ final class DoneAction extends AbstractController
|
|||
$this->addFlashSuccess($this->translator->trans($this->getI18nPrefix().'.flash.success', [
|
||||
'%group%' => $groupOffer->getGroup()->getName()],
|
||||
));
|
||||
$request->getSession()->remove('payment_in_progress');
|
||||
} else {
|
||||
$this->addFlashWarning($this->translator->trans($this->getI18nPrefix().'.status.'.$status->getValue()));
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Payment;
|
||||
namespace App\Controller\Payment\Group;
|
||||
|
||||
use App\Entity\GroupOffer;
|
||||
use App\Repository\GroupOfferRepository;
|
||||
|
|
@ -2,14 +2,13 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Payment;
|
||||
namespace App\Controller\Payment\Group;
|
||||
|
||||
use App\Controller\User\MyAccountAction;
|
||||
use App\Entity\User;
|
||||
use App\Payment\PayumManager;
|
||||
use App\Repository\GroupOfferRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
// use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
81
src/Controller/Payment/PlatformMembership/DoneAction.php
Executable file
81
src/Controller/Payment/PlatformMembership/DoneAction.php
Executable file
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Payment\PlatformMembership;
|
||||
|
||||
use App\Controller\FlashTrait;
|
||||
use App\Controller\i18nTrait;
|
||||
use App\Doctrine\Manager\UserManager;
|
||||
use App\Entity\PaymentToken;
|
||||
use App\Entity\PlatformOffer;
|
||||
use App\Entity\User;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Payum\Core\Payum;
|
||||
use Payum\Core\Request\GetHumanStatus;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Requirement\Requirement;
|
||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[IsGranted(User::ROLE_USER)]
|
||||
final class DoneAction extends AbstractController
|
||||
{
|
||||
use i18nTrait;
|
||||
use FlashTrait;
|
||||
|
||||
public const ROUTE_NAME = 'app_platform_payment_done';
|
||||
|
||||
/**
|
||||
* @see https://github.com/Payum/Payum/blob/master/docs/symfony/get-it-started.md#payment-is-done
|
||||
*/
|
||||
#[Route(
|
||||
path: '/payment/{id}/done',
|
||||
name: self::ROUTE_NAME,
|
||||
requirements: ['id' => Requirement::UUID_V6],
|
||||
)]
|
||||
public function __invoke(Request $request, #[MapEntity(expr: 'repository.findOneActive(id)')] PlatformOffer $platformOffer, #[CurrentUser] User $user, Payum $payum, TranslatorInterface $translator, UserManager $userManager, LoggerInterface $logger): Response
|
||||
{
|
||||
try {
|
||||
/** @var PaymentToken $token */
|
||||
$token = $payum->getHttpRequestVerifier()->verify($request);
|
||||
} catch (\Exception $e) {
|
||||
$logger->error($e->getMessage());
|
||||
throw new UnprocessableEntityHttpException('Cannot verify Payum token.');
|
||||
}
|
||||
|
||||
$gateway = $payum->getGateway($token->getGatewayName());
|
||||
$status = new GetHumanStatus($token);
|
||||
$gateway->execute($status);
|
||||
|
||||
// Not captured
|
||||
if (!$status->isCaptured()) {
|
||||
$this->addFlashWarning($translator->trans($this->getI18nPrefix().'.status.'.$status->getValue()));
|
||||
}
|
||||
|
||||
$user
|
||||
->setMembershipPaid(true)
|
||||
->setStartAt(CarbonImmutable::today())
|
||||
->setPayedAt(CarbonImmutable::now())
|
||||
;
|
||||
if (($offerType = $platformOffer->getType())->isRecurring()) {
|
||||
$user->setEndAt(new CarbonImmutable($offerType->getEndAtInterval()));
|
||||
}
|
||||
|
||||
$userManager->save($user, true);
|
||||
|
||||
$this->addFlashSuccess($translator->trans($this->getI18nPrefix().'.flash.success', [
|
||||
'%platform%' => $platformOffer->getConfiguration()?->getPlatformName()],
|
||||
));
|
||||
$request->getSession()->remove('payment_in_progress');
|
||||
|
||||
return $this->redirectToRoute('app_user_my_account');
|
||||
}
|
||||
}
|
||||
66
src/Controller/Payment/PlatformMembership/PrepareAction.php
Normal file
66
src/Controller/Payment/PlatformMembership/PrepareAction.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Payment\PlatformMembership;
|
||||
|
||||
use App\Entity\PlatformOffer;
|
||||
use App\Entity\User;
|
||||
use App\Payment\PayumManager;
|
||||
use App\Repository\PlatformOfferRepository;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Requirement\Requirement;
|
||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[IsGranted(User::ROLE_USER)]
|
||||
final class PrepareAction extends AbstractController
|
||||
{
|
||||
public const ROUTE_NAME = 'app_platform_payment_prepare';
|
||||
|
||||
/**
|
||||
* @see https://github.com/Payum/Payum/blob/master/docs/symfony/get-it-started.md#prepare-order)
|
||||
*/
|
||||
#[Route(
|
||||
path: '/payment/{id}/prepare',
|
||||
name: self::ROUTE_NAME,
|
||||
requirements: ['id' => Requirement::UUID_V6],
|
||||
methods: ['POST'],
|
||||
)]
|
||||
public function preparePayment(Request $request, #[MapEntity(expr: 'repository.findOneActive(id)')] PlatformOffer $platformOffer, #[CurrentUser] User $user, PayumManager $payumManager): Response
|
||||
{
|
||||
/** @var ?string $token */
|
||||
$token = $request->request->get('token');
|
||||
if (!$this->isCsrfTokenValid('payment_prepare', $token)) {
|
||||
throw new UnprocessableEntityHttpException('Invalid CSRF token');
|
||||
}
|
||||
|
||||
$request->getSession()->set('payment_in_progress', true);
|
||||
|
||||
// create and save the payment main reference
|
||||
$payment = $payumManager->getPayment($platformOffer, $user);
|
||||
|
||||
// create the capture token and redirect to the capture action
|
||||
$captureToken = $payumManager->getCaptureToken($payment, DoneAction::ROUTE_NAME, [
|
||||
'id' => (string) $platformOffer->getId(),
|
||||
]);
|
||||
|
||||
return $this->redirect($captureToken->getTargetUrl());
|
||||
}
|
||||
|
||||
#[Route(path: [
|
||||
'en' => '/en/subcription',
|
||||
'fr' => '/fr/abonnement',
|
||||
], name: 'redirect_to_payment')]
|
||||
public function redirectToPayment(PlatformOfferRepository $platformOfferRepository): Response
|
||||
{
|
||||
$offers = $platformOfferRepository->findBy(['active' => true]);
|
||||
|
||||
return $this->render('pages/redirect_to_payment.html.twig', compact('offers'));
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ use App\Controller\User\MyAccountAction;
|
|||
use App\Doctrine\Manager\ProductManager;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\User;
|
||||
use App\Enum\Product\ProductVisibility;
|
||||
use App\Form\Type\Product\ServiceFormType;
|
||||
use App\MessageBus\QueryBus;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
|
|
@ -56,6 +57,7 @@ final class ServiceController extends AbstractController
|
|||
{
|
||||
if ($this->configurationRepository->getServicesParameter()) {
|
||||
$product = $this->productManager->initService($user);
|
||||
$product->setVisibility(ProductVisibility::RESTRICTED);
|
||||
$form = $this->getForm($product, $request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var array<UploadedFile>|null $images */
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ abstract class AbstractOfferEntity implements \Stringable
|
|||
#[ORM\Column(type: 'boolean', nullable: false)]
|
||||
protected bool $active = true;
|
||||
|
||||
public function __toString()
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name.' ('.$this->type->value.')';
|
||||
}
|
||||
|
|
|
|||
38
src/Doctrine/Listener/MembershipPaidListener.php
Normal file
38
src/Doctrine/Listener/MembershipPaidListener.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Doctrine\Listener;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
#[AsEventListener(event: ExceptionEvent::class, method: 'onKernelException')]
|
||||
final class MembershipPaidListener
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ConfigurationRepository $configurationRepository,
|
||||
private readonly Security $security,
|
||||
private readonly RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
public function onKernelException(ExceptionEvent $event): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->security->getUser();
|
||||
$config = $this->configurationRepository->getInstanceConfigurationOrCreate();
|
||||
$session = $event->getRequest()->getSession();
|
||||
/** @var bool $isPaymentInProgress */
|
||||
$isPaymentInProgress = $session->get('payment_in_progress');
|
||||
|
||||
if ($config->getPaidMembership() && !$user->isMembershipPaid() && !$isPaymentInProgress) {
|
||||
$event->setResponse(new RedirectResponse($this->router->generate('redirect_to_payment')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ use App\Doctrine\Behavior\TimestampableEntity;
|
|||
use App\Enum\ConfigurationType;
|
||||
use App\Message\Command\Admin\ParametersFormCommand;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
|
|
@ -25,6 +27,13 @@ class Configuration
|
|||
#[Assert\NotBlank]
|
||||
protected ConfigurationType $type = ConfigurationType::INSTANCE;
|
||||
|
||||
/**
|
||||
* @var Collection<int, PlatformOffer>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'configuration', targetEntity: PlatformOffer::class, orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['price' => 'ASC'])]
|
||||
private Collection $offers;
|
||||
|
||||
/**
|
||||
* Associative array to store parameters.
|
||||
*
|
||||
|
|
@ -33,6 +42,11 @@ class Configuration
|
|||
#[ORM\Column(type: 'json')]
|
||||
private array $configuration = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->offers = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
|
|
@ -76,28 +90,35 @@ class Configuration
|
|||
/** end of basic getters and setters ------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* @return bool[]
|
||||
* @return array<string, bool|string>
|
||||
*/
|
||||
public function getGlobals(): array
|
||||
{
|
||||
/** @var array<string, bool> $globals */
|
||||
/** @var array<string, bool|string> $globals */
|
||||
$globals = $this->configuration['global'] ?? [];
|
||||
|
||||
return $globals;
|
||||
}
|
||||
|
||||
public function getPlatformName(): string
|
||||
{
|
||||
$globals = $this->getGlobals();
|
||||
|
||||
return (string) ($globals['globalName'] ?? '');
|
||||
}
|
||||
|
||||
public function getServicesEnabled(): bool
|
||||
{
|
||||
$globals = $this->getGlobals();
|
||||
|
||||
return $globals['globalServicesEnabled'];
|
||||
return (bool) $globals['globalServicesEnabled'];
|
||||
}
|
||||
|
||||
public function getPaidMembership(): bool
|
||||
{
|
||||
$globals = $this->getGlobals();
|
||||
|
||||
return $globals['globalPaidMembership'];
|
||||
return (bool) $globals['globalPaidMembership'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -13,4 +13,23 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
#[ORM\Index(columns: ['type'])]
|
||||
class PlatformOffer extends AbstractOfferEntity
|
||||
{
|
||||
/**
|
||||
* Related platform.
|
||||
*/
|
||||
#[ORM\ManyToOne(inversedBy: 'offers')]
|
||||
#[ORM\OrderBy(['createdAt' => 'ASC'])]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')]
|
||||
private ?Configuration $configuration;
|
||||
|
||||
public function getConfiguration(): ?Configuration
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
public function setConfiguration(?Configuration $configuration): self
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ class Product implements \Stringable, ImagesInterface
|
|||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Group::class, inversedBy: 'products')]
|
||||
#[Assert\When(
|
||||
expression: '!this.getVisibility().isPublic()',
|
||||
expression: '!this.getVisibility().isPublic() && !this.getOwner().getUserGroupsConfirmedWithServices().isEmpty()',
|
||||
constraints: [
|
||||
new Assert\Count(min: 1, minMessage: 'app.entity.product.groups.constraints.count.min_message'),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, ImageIn
|
|||
final public const ROLE_USER = 'ROLE_USER';
|
||||
final public const ROLE_ADMIN = 'ROLE_ADMIN';
|
||||
final public const ROLE_GROUP_ADMIN = 'ROLE_GROUP_ADMIN';
|
||||
final public const MEMBERSHIP_PAID = 'MEMBERSHIP_PAID';
|
||||
|
||||
private const EMAIL_MAX_LENGTH = 180;
|
||||
private const NAME_LENGTH = 180;
|
||||
|
|
@ -265,6 +266,32 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, ImageIn
|
|||
#[Assert\IsTrue(groups: [AccountCreateStep2FormType::class])]
|
||||
public bool $gdpr = true;
|
||||
|
||||
/**
|
||||
* Paid for membership of the platform.
|
||||
*/
|
||||
#[ORM\Column(type: 'boolean', nullable: false)]
|
||||
private bool $membershipPaid = false;
|
||||
|
||||
/**
|
||||
* Starting date of a paying membership. The starting date of a free membership
|
||||
* is stored in the creation date.
|
||||
*/
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||
protected ?\DateTimeImmutable $startAt = null;
|
||||
|
||||
/**
|
||||
* Ending date of the paying membership. If it only set for recurring membership.
|
||||
* For one-shot payments, only the start date is filled.
|
||||
*/
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||
protected ?\DateTimeImmutable $endAt = null;
|
||||
|
||||
/**
|
||||
* Date of the last payment of this membership.
|
||||
*/
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||
protected ?\DateTimeImmutable $payedAt = null;
|
||||
|
||||
/**
|
||||
* Local cache to store groups (extracted from related userGroups).
|
||||
*
|
||||
|
|
@ -474,6 +501,10 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, ImageIn
|
|||
}
|
||||
}
|
||||
|
||||
if ($this->isMembershipPaid()) {
|
||||
$roles[] = self::MEMBERSHIP_PAID;
|
||||
}
|
||||
|
||||
return array_unique($roles);
|
||||
}
|
||||
|
||||
|
|
@ -671,6 +702,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, ImageIn
|
|||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, UserGroup>
|
||||
*/
|
||||
public function getUserGroupsConfirmedWithServices(): Collection
|
||||
{
|
||||
/** @var Collection<int, UserGroup> $collection */
|
||||
$collection = $this->userGroups->filter(fn (UserGroup $userGroup) => !$userGroup->getMembership()->isInvited() && $userGroup->getGroup()->getServicesEnabled());
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
|
|
@ -714,6 +756,54 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, ImageIn
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function isMembershipPaid(): bool
|
||||
{
|
||||
return $this->membershipPaid;
|
||||
}
|
||||
|
||||
public function setMembershipPaid(bool $membershipPaid): self
|
||||
{
|
||||
$this->membershipPaid = $membershipPaid;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStartAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->startAt;
|
||||
}
|
||||
|
||||
public function setStartAt(?\DateTimeImmutable $startAt): self
|
||||
{
|
||||
$this->startAt = $startAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEndAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->endAt;
|
||||
}
|
||||
|
||||
public function setEndAt(?\DateTimeImmutable $endAt): self
|
||||
{
|
||||
$this->endAt = $endAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPayedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->payedAt;
|
||||
}
|
||||
|
||||
public function setPayedAt(?\DateTimeImmutable $payedAt): self
|
||||
{
|
||||
$this->payedAt = $payedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// —— end of basic 'etters —————————————————————————————————————————————————
|
||||
|
||||
public function promoteToAdmin(): self
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ enum OfferType: string
|
|||
use AsArrayTrait;
|
||||
|
||||
// The user only to pay once to access the group/platform. In his case the end date is
|
||||
// not set and the membership is valid until it is deleted or a end date is
|
||||
// not set and the membership is valid until it is deleted or an end date is
|
||||
// set. The end date can always be set manually in case of a problem.
|
||||
case ONESHOT = 'oneshot';
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ enum OfferType: string
|
|||
// try a group/platform on the short period before taking a longer subscription.
|
||||
case MONTHLY = 'monthly';
|
||||
|
||||
// Subscription valid for one year. An email will be send a few days before
|
||||
// Subscription valid for one year. An email will be sent a few days before
|
||||
// the end of the membership
|
||||
case YEARLY = 'yearly';
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ final class ParametersFormType extends AbstractType
|
|||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('globalName', TextType::class, [
|
||||
'label' => 'parameter.global_name',
|
||||
])
|
||||
|
||||
->add('globalServicesEnabled', CheckboxType::class, [
|
||||
'label' => 'parameter.services',
|
||||
'label_attr' => [
|
||||
|
|
|
|||
|
|
@ -6,18 +6,14 @@ namespace App\Form\Type\Product;
|
|||
|
||||
use App\Controller\i18nTrait;
|
||||
use App\Entity\Category;
|
||||
use App\Entity\Group;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\User;
|
||||
use App\Enum\Product\ProductType;
|
||||
use App\Enum\Product\ProductVisibility;
|
||||
use App\Flysystem\MediaManager;
|
||||
use App\Repository\CategoryRepository;
|
||||
use App\Repository\GroupRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
|
|
@ -36,7 +32,6 @@ abstract class AbstractProductFormType extends AbstractType
|
|||
|
||||
public function __construct(
|
||||
private readonly MediaManager $mediaManager,
|
||||
private readonly GroupRepository $groupRepository,
|
||||
private readonly Security $security,
|
||||
) {
|
||||
}
|
||||
|
|
@ -87,38 +82,6 @@ abstract class AbstractProductFormType extends AbstractType
|
|||
'attr' => ['class' => 'btn-sm btn-primary'],
|
||||
]);
|
||||
|
||||
// only if the user has groups
|
||||
if (!$user->getUserGroupsConfirmed()->isEmpty()) {
|
||||
$builder
|
||||
->add('visibility', EnumType::class, [
|
||||
'class' => ProductVisibility::class,
|
||||
'label' => 'product.form.visibility',
|
||||
'expanded' => true,
|
||||
'label_attr' => [
|
||||
'class' => 'radio-inline text-black fs-6 fw-normal',
|
||||
],
|
||||
// check if we can do simpler (@see productvisibility_controller.js)
|
||||
'choice_attr' => [
|
||||
0 => [
|
||||
'data-productvisibility-target' => ProductVisibility::PUBLIC->value,
|
||||
'data-action' => 'click->productvisibility#hideGroups',
|
||||
],
|
||||
1 => [
|
||||
'data-productvisibility-target' => ProductVisibility::RESTRICTED->value,
|
||||
'data-action' => 'click->productvisibility#showGroups',
|
||||
],
|
||||
],
|
||||
])
|
||||
->add('groups', EntityType::class, [
|
||||
'class' => Group::class,
|
||||
'query_builder' => $this->groupRepository->getUserGroups($user),
|
||||
'label' => $i18nPrefix.'.form.groups',
|
||||
'expanded' => true,
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
// remove all group associations if public to avoid side effects
|
||||
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
|
||||
/** @var Product $product */
|
||||
|
|
|
|||
|
|
@ -4,8 +4,16 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Form\Type\Product;
|
||||
|
||||
use App\Entity\Group;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\User;
|
||||
use App\Enum\Product\ProductType;
|
||||
use App\Enum\Product\ProductVisibility;
|
||||
use App\Flysystem\MediaManager;
|
||||
use App\Repository\GroupRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
|
@ -13,10 +21,22 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||
|
||||
final class ObjectFormType extends AbstractProductFormType
|
||||
{
|
||||
public function __construct(
|
||||
MediaManager $mediaManager,
|
||||
private readonly GroupRepository $groupRepository,
|
||||
private readonly Security $security
|
||||
) {
|
||||
parent::__construct($mediaManager, $security);
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$i18nPrefix = $this->getI18nPrefix();
|
||||
/** @var User $user */
|
||||
$user = $this->security->getUser();
|
||||
|
||||
$builder
|
||||
->add('age', TextType::class, [
|
||||
'label' => 'object.form.age',
|
||||
|
|
@ -34,6 +54,38 @@ final class ObjectFormType extends AbstractProductFormType
|
|||
'label_attr' => ['class' => 'text-black fs-6 fw-normal'],
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
// only if the user is connected and has groups
|
||||
if (!$user->getUserGroupsConfirmed()->isEmpty()) {
|
||||
$builder
|
||||
->add('visibility', EnumType::class, [
|
||||
'class' => ProductVisibility::class,
|
||||
'label' => 'product.form.visibility',
|
||||
'expanded' => true,
|
||||
'label_attr' => [
|
||||
'class' => 'radio-inline text-black fs-6 fw-normal',
|
||||
],
|
||||
// check if we can do simpler (@see productvisibility_controller.js)
|
||||
'choice_attr' => [
|
||||
0 => [
|
||||
'data-productvisibility-target' => ProductVisibility::PUBLIC->value,
|
||||
'data-action' => 'click->productvisibility#hideGroups',
|
||||
],
|
||||
1 => [
|
||||
'data-productvisibility-target' => ProductVisibility::RESTRICTED->value,
|
||||
'data-action' => 'click->productvisibility#showGroups',
|
||||
],
|
||||
],
|
||||
])
|
||||
->add('groups', EntityType::class, [
|
||||
'class' => Group::class,
|
||||
'query_builder' => $this->groupRepository->getUserGroups($user),
|
||||
'label' => $i18nPrefix.'.form.groups',
|
||||
'expanded' => true,
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getType(): ProductType
|
||||
|
|
|
|||
|
|
@ -4,24 +4,73 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Form\Type\Product;
|
||||
|
||||
use App\Entity\Group;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\User;
|
||||
use App\Enum\Product\ProductType;
|
||||
use App\Enum\Product\ProductVisibility;
|
||||
use App\Flysystem\MediaManager;
|
||||
use App\Repository\GroupRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
final class ServiceFormType extends AbstractProductFormType
|
||||
{
|
||||
public function __construct(
|
||||
MediaManager $mediaManager,
|
||||
private readonly GroupRepository $groupRepository,
|
||||
private readonly Security $security
|
||||
) {
|
||||
parent::__construct($mediaManager, $security);
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$i18nPrefix = $this->getI18nPrefix();
|
||||
/** @var User $user */
|
||||
$user = $this->security->getUser();
|
||||
|
||||
$builder
|
||||
->add('duration', TextType::class, [
|
||||
'label' => 'new_service.form.serviceDuration',
|
||||
'label_attr' => ['class' => 'text-black fs-6 fw-normal'],
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
// only if the user is connected and has groups
|
||||
if (!$user->getUserGroupsConfirmed()->isEmpty()) {
|
||||
$groups = $this->groupRepository->getUserGroupsWithEnabledServices($user);
|
||||
$builder
|
||||
->add('visibility', ChoiceType::class, [
|
||||
'label' => 'product.form.visibility',
|
||||
'expanded' => true,
|
||||
'label_attr' => [
|
||||
'class' => 'radio-inline text-black fs-6 fw-normal',
|
||||
],
|
||||
'choices' => [
|
||||
0 => [
|
||||
'product.service.form.visibility' => ProductVisibility::RESTRICTED,
|
||||
],
|
||||
],
|
||||
'multiple' => false,
|
||||
'data' => ProductVisibility::RESTRICTED,
|
||||
'disabled' => true,
|
||||
])
|
||||
->add('groups', EntityType::class, [
|
||||
'class' => Group::class,
|
||||
'query_builder' => $groups,
|
||||
'label' => [] === $groups->getQuery()->getResult() ? $i18nPrefix.'.form.no_groups' : $i18nPrefix.'.form.groups',
|
||||
'expanded' => true,
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getType(): ProductType
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ final class ParametersFormCommand extends AbstractFormCommand
|
|||
final public const ALL = 'all';
|
||||
|
||||
// global section —————————————————————————————————————————————
|
||||
#[Assert\NotBlank()]
|
||||
public ?string $globalName = null;
|
||||
|
||||
#[Assert\Type('bool')]
|
||||
public bool $globalServicesEnabled = true;
|
||||
|
||||
|
|
@ -79,7 +82,6 @@ final class ParametersFormCommand extends AbstractFormCommand
|
|||
public function hydrate(Configuration $configuration): self
|
||||
{
|
||||
$instanceConfiguration = $configuration->getConfiguration();
|
||||
// dd($instanceConfiguration);
|
||||
foreach (array_keys(get_class_vars($this::class)) as $classVar) {
|
||||
$this->{$classVar} = $instanceConfiguration[$this->getSection($classVar)][$classVar]; // @phpstan-ignore-line
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Payment;
|
||||
|
||||
use App\Controller\Payment\PrepareAction;
|
||||
use App\Controller\Payment\Group\PrepareAction;
|
||||
use App\Entity\GroupOffer;
|
||||
use App\Entity\Payment;
|
||||
use App\Entity\PlatformOffer;
|
||||
use App\Entity\User;
|
||||
use App\Enum\Payment\PaymentMethod;
|
||||
use Payum\Core\Payum;
|
||||
|
|
@ -29,20 +30,27 @@ final class PayumManager
|
|||
*
|
||||
* @see PrepareAction
|
||||
*/
|
||||
public function getPayment(GroupOffer $groupOffer, User $user): Payment
|
||||
public function getPayment(GroupOffer|PlatformOffer $offer, User $user): Payment
|
||||
{
|
||||
$storage = $this->payum->getStorage(Payment::class);
|
||||
|
||||
/** @var Payment $payment */
|
||||
$payment = $storage->create();
|
||||
$payment->setNumber(uniqid('payum_', true));
|
||||
$payment->setCurrencyCode($groupOffer->getCurrency());
|
||||
$payment->setTotalAmount($groupOffer->getPrice());
|
||||
$payment->setDescription($groupOffer->getGroup()->getName().' / '.$groupOffer->getName());
|
||||
$payment->setCurrencyCode($offer->getCurrency());
|
||||
$payment->setTotalAmount($offer->getPrice());
|
||||
|
||||
if ($offer instanceof GroupOffer) {
|
||||
$payment->setDescription($offer->getGroup()->getName().' / '.$offer->getName());
|
||||
}
|
||||
|
||||
if ($offer instanceof PlatformOffer) {
|
||||
$payment->setDescription($offer->getConfiguration()?->getPlatformName().' / '.$offer->getName());
|
||||
}
|
||||
|
||||
$payment->setClientId((string) $user->getId());
|
||||
$payment->setClientEmail($user->getEmail());
|
||||
$payment->setUser($user);
|
||||
$payment->setDetails($this->getGatewayDetails($groupOffer));
|
||||
$payment->setDetails($this->getGatewayDetails($offer));
|
||||
$storage->update($payment);
|
||||
|
||||
return $payment;
|
||||
|
|
@ -61,16 +69,27 @@ final class PayumManager
|
|||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getGatewayDetails(GroupOffer $groupOffer): array
|
||||
private function getGatewayDetails(GroupOffer|PlatformOffer $offer): array
|
||||
{
|
||||
if ($offer instanceof GroupOffer) {
|
||||
return [
|
||||
// method must be set as the default value is not retrieved from the gateway configuration
|
||||
'method' => PaymentMethod::CREDITCARD->value,
|
||||
'metadata' => [
|
||||
'groupId' => (string) $groupOffer->getGroup()->getId(),
|
||||
'groupOfferId' => (string) $groupOffer->getId(),
|
||||
'groupId' => (string) $offer->getGroup()->getId(),
|
||||
'groupOfferId' => (string) $offer->getId(),
|
||||
],
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
// method must be set as the default value is not retrieved from the gateway configuration
|
||||
'method' => PaymentMethod::CREDITCARD->value,
|
||||
'metadata' => [
|
||||
'platformId' => (string) $offer->getConfiguration()?->getId(),
|
||||
'platformOfferId' => (string) $offer->getId(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -80,13 +80,11 @@ final class GroupRepository extends ServiceEntityRepository
|
|||
return $qb->orderBy('g.name', 'ASC')->getQuery();
|
||||
}
|
||||
|
||||
public function getUserGroups(User $user): QueryBuilder
|
||||
public function getUserGroupsWithEnabledServices(User $user): QueryBuilder
|
||||
{
|
||||
return $this->createQueryBuilder('g')
|
||||
->from(UserGroup::class, 'ug')
|
||||
->andWhere('g = ug.group')
|
||||
->andWhere('ug.user = :user')
|
||||
->setParameter('user', $user);
|
||||
return $this->getUserGroups($user)
|
||||
->andWhere('g.servicesEnabled = :enabled')
|
||||
->setParameter('enabled', true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -135,4 +133,13 @@ final class GroupRepository extends ServiceEntityRepository
|
|||
}
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
public function getUserGroups(User $user): QueryBuilder
|
||||
{
|
||||
return $this->createQueryBuilder('g')
|
||||
->from(UserGroup::class, 'ug')
|
||||
->andWhere('g = ug.group')
|
||||
->andWhere('ug.user = :user')
|
||||
->setParameter('user', $user);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,4 +32,9 @@ class PlatformOfferRepository extends ServiceEntityRepository
|
|||
{
|
||||
return $this->find($id, $lockMode, $lockVersion) ?? throw new \LogicException('Platform offer not found.');
|
||||
}
|
||||
|
||||
public function findOneActive(string $id): ?PlatformOffer
|
||||
{
|
||||
return $this->findOneBy(['id' => $id, 'active' => true]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
src/Security/Voter/MembershipPaidVoter.php
Normal file
34
src/Security/Voter/MembershipPaidVoter.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security\Voter;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
class MembershipPaidVoter extends Voter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ConfigurationRepository $configurationRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
return User::MEMBERSHIP_PAID === $attribute;
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !$this->configurationRepository->getInstanceConfigurationOrCreate()->getPaidMembership() || $user->isMembershipPaid();
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@
|
|||
<h2 class="h3 fw-bold mt-3">{{ 'parameters.global.h2'|trans }}</h2>
|
||||
<hr/>
|
||||
|
||||
{{ form_row(form.globalName) }}
|
||||
|
||||
<h2 class="h5 fw-bold my-3">{{ 'parameters.services.h3'|trans }}</h2>
|
||||
|
||||
{{ form_widget(form.globalServicesEnabled) }}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
</span>
|
||||
</div>
|
||||
<div class="col col-lg-5 d-flex align-items-center justify-content-center mt-3 mt-lg-0">
|
||||
<form action="{{ path('app_payment_prepare', {id: offer.id}) }}" method="post">
|
||||
<form action="{{ path(prepare_path, {id: offer.id}) }}" method="post">
|
||||
<input type="hidden" name="token" value="{{ csrf_token('payment_prepare') }}"/>
|
||||
<input class="btn btn-primary" type="submit"
|
||||
value="{{ (i18n_prefix ~ '.payment_prepare.form.submit')|trans }}">
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
{% if not group.activeOffers.isEmpty %}
|
||||
{% include 'components/group/_first_offer.html.twig' with {group} %}
|
||||
{% include 'components/group/_modal_offers.html.twig' with {
|
||||
offers: group.activeOffers
|
||||
offers: group.activeOffers,
|
||||
prepare_path: 'app_payment_prepare'
|
||||
} %}
|
||||
|
||||
{# 2.1.2 otherwise it is a free group #}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@
|
|||
|
||||
{% include 'components/group/_first_offer.html.twig' with {group} %}
|
||||
{% include 'components/group/_modal_offers.html.twig' with {
|
||||
offers: group.activeOffers
|
||||
offers: group.activeOffers,
|
||||
prepare_path: 'app_payment_prepare'
|
||||
} %}
|
||||
|
||||
{# 1.3 group is not correclty configured, it doesn't have any offer configured yet. #}
|
||||
|
|
|
|||
30
templates/pages/redirect_to_payment.html.twig
Normal file
30
templates/pages/redirect_to_payment.html.twig
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{% extends 'layout/base.html.twig' %}
|
||||
|
||||
{% set i18n_prefix = _self|i18n_prefix %}
|
||||
|
||||
{% block body %}
|
||||
<div class="">
|
||||
{% include 'components/layout/_title_3.html.twig' with {
|
||||
name: (i18n_prefix ~ '.title')|trans
|
||||
} %}
|
||||
{% include 'components/layout/_text.html.twig' with {
|
||||
text: (i18n_prefix ~ '.text')|trans
|
||||
} %}
|
||||
<div class="d-grid col col-lg-4 mx-auto">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary my-2 mb-4"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#offersModal"
|
||||
>
|
||||
<i class="bi bi-currency-euro"></i>
|
||||
{{ (i18n_prefix ~ '.button')|trans }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% include 'components/group/_modal_offers.html.twig' with {
|
||||
offers: offers,
|
||||
prepare_path: 'app_platform_payment_prepare'
|
||||
} %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Tests\Functional\Controller\Payment;
|
||||
|
||||
use App\Controller\Payment\PrepareAction;
|
||||
use App\Controller\Payment\Group\PrepareAction;
|
||||
use App\Test\KernelTrait;
|
||||
use App\Tests\TestReference;
|
||||
use Hautelook\AliceBundle\PhpUnit\RefreshDatabaseTrait;
|
||||
|
|
|
|||
|
|
@ -35,8 +35,6 @@ class ServiceControllerTest extends WebTestCase
|
|||
$form->getName().'[name]' => 'jardinage',
|
||||
$form->getName().'[description]' => 'description',
|
||||
$form->getName().'[duration]' => '1 jour',
|
||||
$form->getName().'[visibility]' => 'restricted',
|
||||
$form->getName().'[groups]' => [TestReference::GROUP_1],
|
||||
]);
|
||||
|
||||
$container = $client->getContainer();
|
||||
|
|
@ -49,7 +47,6 @@ class ServiceControllerTest extends WebTestCase
|
|||
self::assertSame('jardinage', $editedService->getName());
|
||||
self::assertSame('description', $editedService->getDescription());
|
||||
self::assertSame('1 jour', $editedService->getDuration());
|
||||
self::assertSame('restricted', $editedService->getVisibility()->value);
|
||||
|
||||
self::assertResponseRedirects();
|
||||
$client->followRedirect();
|
||||
|
|
|
|||
|
|
@ -129,5 +129,5 @@ final class TestReference
|
|||
final public const PAYMENT_USER_16_1 = '1edcefc9-45b3-6a3e-b4a6-db137f56da56';
|
||||
|
||||
// platform offer
|
||||
final public const PLATFORM_OFFER_1 = '9040b3fb-8a01-4bbf-a228-ca9f90db5034';
|
||||
final public const PLATFORM_OFFER_1 = '016b2a27-1037-6d47-bcdc-ec5efbd723f2';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Tests\Unit\Controller\Payment;
|
||||
|
||||
use App\Controller\Payment\DoneAction;
|
||||
use App\Controller\Payment\Group\DoneAction;
|
||||
use App\Entity\Group;
|
||||
use App\Entity\GroupOffer;
|
||||
use App\Entity\PaymentToken;
|
||||
|
|
@ -103,6 +103,7 @@ final class DoneActionTest extends TestCase
|
|||
);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Cannot verify Payum token');
|
||||
|
||||
$doneAction->__invoke(new Request(), TestReference::UUID_404, $this->getUser());
|
||||
}
|
||||
|
||||
|
|
@ -154,6 +155,7 @@ final class DoneActionTest extends TestCase
|
|||
$doneAction->setContainer($container);
|
||||
|
||||
$this->expectException(\Error::class); // or more mock are needed. To clean up later
|
||||
|
||||
$doneAction->__invoke(new Request(), TestReference::UUID_404, $this->getUser());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,28 +5,28 @@
|
|||
<tool tool-id="symfony" tool-name="Symfony" />
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="HVbbDui" resname="app.controller.payment.done_action.flash.success">
|
||||
<source>app.controller.payment.done_action.flash.success</source>
|
||||
<trans-unit id="HVbbDui" resname="app.controller.payment.group.done_action.flash.success">
|
||||
<source>app.controller.payment.group.done_action.flash.success</source>
|
||||
<target>Paiement réussi. Vous êtes désormais membre du groupe %group%.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="ipYvZb8" resname="app.controller.payment.done_action.status.new">
|
||||
<source>app.controller.payment.done_action.status.new</source>
|
||||
<trans-unit id="ipYvZb8" resname="app.controller.payment.group.done_action.status.new">
|
||||
<source>app.controller.payment.group.done_action.status.new</source>
|
||||
<target>Le paiement est en cours mais n'a pas encore abouti.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="K3ZL3UV" resname="app.controller.payment.done_action.status.failed">
|
||||
<source>app.controller.payment.done_action.status.failed</source>
|
||||
<trans-unit id="K3ZL3UV" resname="app.controller.payment.group.done_action.status.failed">
|
||||
<source>app.controller.payment.group.done_action.status.failed</source>
|
||||
<target>Le paiement a échoué, vous n'avez pas été prélevé·e.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="AVy18N2" resname="app.controller.payment.done_action.status.canceled">
|
||||
<source>app.controller.payment.done_action.status.canceled</source>
|
||||
<trans-unit id="AVy18N2" resname="app.controller.payment.group.done_action.status.canceled">
|
||||
<source>app.controller.payment.group.done_action.status.canceled</source>
|
||||
<target>Le paiement a été annulé, vous n'avez pas été prélevé·e.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="D8DSDe6" resname="app.controller.payment.done_action.status.expired">
|
||||
<source>app.controller.payment.done_action.status.expired</source>
|
||||
<trans-unit id="D8DSDe6" resname="app.controller.payment.group.done_action.status.expired">
|
||||
<source>app.controller.payment.group.done_action.status.expired</source>
|
||||
<target>Le paiement a expiré, veuillez réessayer.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
|
||||
<header>
|
||||
<tool tool-id="symfony" tool-name="Symfony" />
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="HkHjuo7" resname="app.controller.payment.platform_membership.done_action.flash.success">
|
||||
<source>app.controller.payment.platform_membership.done_action.flash.success</source>
|
||||
<target>Paiement réussi. Vous êtes désormais membre de la plateforme %platform%.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="iikpmn8" resname="app.controller.payment.platform_membership.done_action.status.new">
|
||||
<source>app.controller.payment.platform_membership.done_action.status.new</source>
|
||||
<target>Le paiement est en cours mais n'a pas encore abouti.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="K3Zg5gn" resname="app.controller.payment.platform_membership.done_action.status.failed">
|
||||
<source>app.controller.payment.platform_membership.done_action.status.failed</source>
|
||||
<target>Le paiement a échoué, vous n'avez pas été prélevé·e.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="AVy1f5d" resname="app.controller.payment.platform_membership.done_action.status.canceled">
|
||||
<source>app.controller.payment.platform_membership.done_action.status.canceled</source>
|
||||
<target>Le paiement a été annulé, vous n'avez pas été prélevé·e.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="D8fvt2w" resname="app.controller.payment.platform_membership.done_action.status.expired">
|
||||
<source>app.controller.payment.platform_membership.done_action.status.expired</source>
|
||||
<target>Le paiement a expiré, veuillez réessayer.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
@ -5,11 +5,15 @@
|
|||
<tool tool-id="symfony" tool-name="Symfony" />
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="" resname="app.form.type.product.service_form_type.form.groups">
|
||||
<trans-unit id="pLw9zsf" resname="app.form.type.product.service_form_type.form.groups">
|
||||
<source>app.form.type.product.service_form_type.form.groups</source>
|
||||
<target>Visible dans mes groupes :</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="bMv5dw8" resname="app.form.type.product.service_form_type.form.no_groups">
|
||||
<source>app.form.type.product.service_form_type.form.no_groups</source>
|
||||
<target>Vous n'avez aucun groupe avec la disponibilité des services activée</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@
|
|||
<body>
|
||||
|
||||
<!-- global section -->
|
||||
<trans-unit id="qlPklB7" resname="parameter.global_name">
|
||||
<source>parameter.global_name</source>
|
||||
<target>Nom de la plateforme</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="qlPbwB3" resname="parameter.services">
|
||||
<source>parameter.services</source>
|
||||
<target>Services activés</target>
|
||||
|
|
|
|||
|
|
@ -105,6 +105,11 @@
|
|||
<target>Uniquement mes groupes</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="BaSmLK9" resname="product.service.form.visibility">
|
||||
<source>product.service.form.visibility</source>
|
||||
<target>Uniquement mes groupes</target>
|
||||
</trans-unit>
|
||||
|
||||
<!-- product forms -->
|
||||
<trans-unit id="X4i3dq6" resname="new_product.available">
|
||||
<source>new_product.available</source>
|
||||
|
|
|
|||
65
translations/templates/pages/messages.fr.xlf
Normal file
65
translations/templates/pages/messages.fr.xlf
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
|
||||
<header>
|
||||
<tool tool-id="symfony" tool-name="Symfony" />
|
||||
</header>
|
||||
<body>
|
||||
|
||||
<trans-unit id="Xwpl7p9" resname="templates.pages.redirect_to_payment.type.yearly">
|
||||
<source>templates.pages.redirect_to_payment.type.yearly</source>
|
||||
<target>Adhésion annuel</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="HJFwmkE" resname="templates.pages.redirect_to_payment.type.monthly">
|
||||
<source>templates.pages.redirect_to_payment.type.monthly</source>
|
||||
<target>Adhésion mensuel</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="Pwx2GJU" resname="templates.pages.redirect_to_payment.type.oneshot">
|
||||
<source>templates.pages.redirect_to_payment.type.oneshot</source>
|
||||
<target>Unique</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="v4xDplQ" resname="templates.pages.redirect_to_payment.choose_offer">
|
||||
<source>templates.pages.redirect_to_payment.choose_offer</source>
|
||||
<target>Rejoindre la plateforme</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="DMvoLvx" resname="templates.pages.redirect_to_payment.close">
|
||||
<source>templates.pages.redirect_to_payment.close</source>
|
||||
<target>Annuler</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="1w4Jeyy" resname="templates.pages.redirect_to_payment.membership">
|
||||
<source>templates.pages.redirect_to_payment.membership</source>
|
||||
<target>Adhésion: </target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="Ee6vfs4" resname="templates.pages.redirect_to_payment.membership.start">
|
||||
<source>templates.pages.redirect_to_payment.membership.start</source>
|
||||
<target>À partir de </target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="QwsvcUp" resname="templates.pages.redirect_to_payment.payment_prepare.form.submit">
|
||||
<source>templates.pages.redirect_to_payment.payment_prepare.form.submit</source>
|
||||
<target>Accéder au paiement</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="QsIj9kp" resname="templates.pages.redirect_to_payment.title">
|
||||
<source>templates.pages.redirect_payment.title</source>
|
||||
<target>Bienvenue</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="wSc8oud" resname="templates.pages.redirect_to_payment.text">
|
||||
<source>templates.pages.redirect_to_payment.text</source>
|
||||
<target>Pour avoir accès aux prêts/emprunts sur notre plateforme, il faut être adhérent.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="wSc6sFt" resname="templates.pages.redirect_to_payment.button">
|
||||
<source>templates.pages.redirect_to_payment.button</source>
|
||||
<target>Payer mon adhésion</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
Loading…
Reference in a new issue