From b9a87a420bfc130886cad38a49c0e025bbbb7377 Mon Sep 17 00:00:00 2001 From: Sarahshr <51380592+Sarahshr@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:24:09 +0200 Subject: [PATCH 01/28] Feat/disable services option for groups (#711) * add servicesDisabled field for groups and for global configuration * fixup! add servicesDisabled field for groups and for global configuration * comment libcurl upgrade to fix ci temporarly * upgrade caddy version * review * fix test * feat: add paying membership option (#714) * feat: add paying membership option * fix: ci * fix: phpstan + review * fix: eslint ci --- .github/workflows/ci.yml | 2 +- Dockerfile | 8 +- assets/admin.js | 8 + .../admin_parentgroup_controller.js | 77 +++++++++ assets/controllers/parentgroup_controller.js | 36 +++++ assets/stimulus.js | 7 +- assets/styles/admin.css | 4 + assets/styles/app.css | 3 + composer.json | 1 + composer.lock | 73 ++++++++- config/bundles.php | 1 + fixtures/prod-boot/configuration.yaml | 5 +- fixtures/test/configuration.yaml | 5 +- fixtures/test/group_offer.yaml | 4 +- fixtures/test/platform_offer.yaml | 11 ++ src/Controller/Admin/GroupCrudController.php | 47 +++++- .../Admin/GroupOfferCrudController.php | 12 +- .../Admin/PlatformOfferCrudController.php | 121 ++++++++++++++ src/Doctrine/Behavior/AbstractOfferEntity.php | 148 ++++++++++++++++++ ...oupOfferTypeType.php => OfferTypeType.php} | 8 +- src/Entity/Configuration.php | 19 ++- src/Entity/Group.php | 28 ++++ src/Entity/GroupOffer.php | 142 +---------------- src/Entity/PlatformOffer.php | 16 ++ src/Entity/Product.php | 2 +- src/Entity/User.php | 12 +- src/Entity/UserGroup.php | 2 + .../GroupOfferType.php => OfferType.php} | 10 +- src/Form/Type/Admin/ParametersFormType.php | 9 +- src/Form/Type/Group/CreateGroupFormType.php | 52 ++++-- .../Command/Admin/ParametersFormCommand.php | 9 +- .../Admin/ParametersFormCommandHandler.php | 9 +- src/Repository/ConfigurationRepository.php | 4 +- src/Repository/GroupRepository.php | 48 ++++++ src/Repository/PlatformOfferRepository.php | 35 +++++ src/State/GroupsProvider.php | 34 ++++ .../GroupChildServicesEnabledProcessor.php | 31 ++++ symfony.lock | 18 +++ .../admin/field/services_enabled.html.twig | 17 ++ templates/admin/parameters.html.twig | 22 ++- templates/pages/group/create.html.twig | 17 +- .../Admin/PlatformOfferCrudControllerTest.php | 57 +++++++ .../Group/CreateGroupActionTest.php | 2 +- tests/TestReference.php | 3 + .../Controller/Payment/DoneActionTest.php | 2 +- tests/Unit/Enum/Group/GroupOfferTypeTest.php | 6 +- translations/admin.fr.xlf | 14 +- .../PlatformOfferCrudController/admin.fr.xlf | 20 +++ translations/group/admin.fr.xlf | 5 + translations/parameters/admin.fr.xlf | 12 +- .../pages/group/create/messages.fr.xlf | 5 + webpack.config.js | 1 + 52 files changed, 1023 insertions(+), 221 deletions(-) create mode 100644 assets/admin.js create mode 100644 assets/controllers/admin_parentgroup_controller.js create mode 100644 assets/controllers/parentgroup_controller.js create mode 100644 assets/styles/admin.css create mode 100644 assets/styles/app.css create mode 100644 fixtures/test/platform_offer.yaml create mode 100644 src/Controller/Admin/PlatformOfferCrudController.php create mode 100644 src/Doctrine/Behavior/AbstractOfferEntity.php rename src/EasyAdmin/Form/Type/{GroupOfferTypeType.php => OfferTypeType.php} (75%) create mode 100644 src/Entity/PlatformOffer.php rename src/Enum/{Group/GroupOfferType.php => OfferType.php} (82%) create mode 100644 src/Repository/PlatformOfferRepository.php create mode 100644 src/State/GroupsProvider.php create mode 100644 src/State/Processor/GroupChildServicesEnabledProcessor.php create mode 100644 templates/admin/field/services_enabled.html.twig create mode 100644 tests/Functional/Controller/Admin/PlatformOfferCrudControllerTest.php create mode 100644 translations/app/Controller/Admin/PlatformOfferCrudController/admin.fr.xlf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49c0df0..8bebfe9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,6 @@ jobs: - name: Twig Linter run: docker compose exec -T php ./vendor/bin/twigcs templates/ --exclude vendor - name: Install eslint - run: docker run --rm -w "/usr/app" -v "${PWD}":/usr/app gmolaire/yarn yarn add eslint + run: docker run --rm -w "/usr/app" -v "${PWD}":/usr/app gmolaire/yarn yarn add eslint@8.57.0 - name: Run eslint on javascript files run: docker run --rm -w "/usr/app" -v "${PWD}":/usr/app gmolaire/yarn yarn lint diff --git a/Dockerfile b/Dockerfile index becedd2..bc3bb4c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ # https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact ARG PHP_VERSION=8.1 -ARG CADDY_VERSION=2 +ARG CADDY_VERSION=2.8.4 # yarn build FROM gmolaire/yarn AS yarn_build @@ -24,7 +24,7 @@ RUN yarn build FROM php:${PHP_VERSION}-fpm-alpine AS app_php # needed for security update until base image is updated -RUN apk upgrade libcurl curl openssl openssl-dev libressl libcrypto3 libssl3 +#RUN apk upgrade libcurl curl openssl openssl-dev libressl libcrypto3 libssl3 # Allow to use development versions of Symfony ARG STABILITY="stable" @@ -188,7 +188,7 @@ RUN rm -f .env.local.php # Temporary fix for https://github.com/dunglas/mercure/issues/770 # https://github.com/dunglas/symfony-docker/pull/407/files -FROM caddy:2.7-builder-alpine AS app_caddy_builder +FROM caddy:2.8.4-builder-alpine AS app_caddy_builder # RUN xcaddy build \ # --with github.com/dunglas/mercure \ @@ -204,7 +204,7 @@ RUN xcaddy build \ FROM caddy:${CADDY_VERSION} AS app_caddy # needed for security update until base image is updated -RUN apk upgrade libcurl curl openssl openssl-dev libressl libcrypto1.1 libssl1.1 libcrypto3 libssl3 +#RUN apk upgrade libcurl curl openssl openssl-dev libressl libcrypto1.1 libssl1.1 libcrypto3 libssl3 WORKDIR /srv/app diff --git a/assets/admin.js b/assets/admin.js new file mode 100644 index 0000000..b3e40e5 --- /dev/null +++ b/assets/admin.js @@ -0,0 +1,8 @@ +import { startStimulusApp } from '@symfony/stimulus-bridge' +import AdminParentgroup from './controllers/admin_parentgroup_controller' + +import './styles/admin.css' + +const app = startStimulusApp() + +app.register('admin-parentgroup', AdminParentgroup) diff --git a/assets/controllers/admin_parentgroup_controller.js b/assets/controllers/admin_parentgroup_controller.js new file mode 100644 index 0000000..df6af16 --- /dev/null +++ b/assets/controllers/admin_parentgroup_controller.js @@ -0,0 +1,77 @@ +import { Controller} from '@hotwired/stimulus' + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + static targets = ['servicesEnabledField', 'parentField', 'idField'] + + parentFieldTargetConnected(element) { + const observer = new MutationObserver( ( ) => { + if (element.tomselect) { + observer.disconnect() + + const toggle = document.getElementById('Group_servicesEnabled') + toggle.addEventListener('change', () => { + this.updateParentOptions(toggle.checked, this.parentFieldTarget) + }) + } + }) + observer.observe(element, {attributes: true}) + } + + async updateParentOptions(servicesEnabled, parentField) { + const url = `/api/groups?services_enabled=${servicesEnabled}` + + const response = await fetch(url, { method: 'GET' }) + if (!response.ok) { + return + } + const data = await response.json() + const groups = data['hydra:member'] + + // Remove options + parentField.tomselect.clearOptions() + + // Populate with new options + groups.map(group => { + parentField.tomselect.addOption(new Option(group.name, group.id)) + }) + } + + servicesEnabledFieldTargetConnected() { + this.servicesEnabledFieldTarget.addEventListener('change', () => { + if(!this.servicesEnabledFieldTarget.checked) { + const params = new URLSearchParams(this.servicesEnabledFieldTarget.getAttribute('data-toggle-url')) + this.disableServicesForChildGroups(params.get('entityId')) + } + }) + } + + async disableServicesForChildGroups(groupId) { + const url = `/api/groups/${groupId}/disable_child_services` + + const response = await fetch(url, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/merge-patch+json', + }, + }) + if (!response.ok) { + return + } + const data = await response.json() + const groupChild = data.children + + const groupChildId = groupChild.map(group => { + return group.split('/')[3] + }) + + const allToggles = document.querySelectorAll('[data-admin-parentgroup-target="servicesEnabledField"]') + Array.from(allToggles).map(toggle => { + const params = new URLSearchParams(toggle.getAttribute('data-toggle-url')) + if(groupChildId.includes(params.get('entityId'))) { + toggle.checked = false + } + }) + + } +} diff --git a/assets/controllers/parentgroup_controller.js b/assets/controllers/parentgroup_controller.js new file mode 100644 index 0000000..b5577c1 --- /dev/null +++ b/assets/controllers/parentgroup_controller.js @@ -0,0 +1,36 @@ +import { Controller} from '@hotwired/stimulus' + +export default class extends Controller { + static targets = ['servicesEnabledField', 'parentField'] + + connect() { + } + + updateParentOptions() { + const userId = this.servicesEnabledFieldTarget.getAttribute('data-user-id') + const servicesEnabled = this.servicesEnabledFieldTarget.checked + + const url = `/api/groups?user=${userId}&services_enabled=${servicesEnabled}` + this.addGroupsWithEnabledServices(url) + } + + async addGroupsWithEnabledServices(url) { + const response = await fetch(url, { method: 'GET' }) + if (!response.ok) { + return + } + const data = await response.json() + const groups = data['hydra:member'] + + // Remove options and set a default value + Array.from(this.parentFieldTarget.options).map((group) => { + this.parentFieldTarget.remove(group) + }) + this.parentFieldTarget.add(new Option()) + + // Populate with new options + groups.map(group => { + this.parentFieldTarget.add(new Option(group.name, group.id)) + }) + } +} diff --git a/assets/stimulus.js b/assets/stimulus.js index c02d609..352bda5 100644 --- a/assets/stimulus.js +++ b/assets/stimulus.js @@ -1,5 +1,4 @@ import { startStimulusApp } from '@symfony/stimulus-bridge' -import { Application } from '@hotwired/stimulus' import PasswordVisibility from 'stimulus-password-visibility' import Carousel from 'stimulus-carousel' @@ -15,7 +14,5 @@ export const app = startStimulusApp(require.context( // register any custom, 3rd party controllers here // app.register('some_controller_name', SomeImportedController); -const application = Application.start() -application.register('carousel', Carousel) -application.register('password-visibility', PasswordVisibility) - +app.register('carousel', Carousel) +app.register('password-visibility', PasswordVisibility) diff --git a/assets/styles/admin.css b/assets/styles/admin.css new file mode 100644 index 0000000..5862dac --- /dev/null +++ b/assets/styles/admin.css @@ -0,0 +1,4 @@ +/* Remove the extra arrow added by Tom Select */ +.ts-wrapper.single .ts-control:after { + display: none; +} diff --git a/assets/styles/app.css b/assets/styles/app.css new file mode 100644 index 0000000..dd6181a --- /dev/null +++ b/assets/styles/app.css @@ -0,0 +1,3 @@ +body { + background-color: skyblue; +} diff --git a/composer.json b/composer.json index 8f02a41..7899c1e 100644 --- a/composer.json +++ b/composer.json @@ -78,6 +78,7 @@ "symfony/runtime": "6.2.*", "symfony/security-bundle": "6.2.*", "symfony/serializer": "6.2.*", + "symfony/stimulus-bundle": "^2.14", "symfony/translation-contracts": "^3.2", "symfony/twig-bridge": "6.2.*", "symfony/twig-bundle": "6.2.*", diff --git a/composer.lock b/composer.lock index 3c77d18..db0756a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3e1f8a3631e991b528d9f68758299e0e", + "content-hash": "2b97a3771217b9b8a9f07e3f5ec9aec2", "packages": [ { "name": "alcohol/iso4217", @@ -13247,6 +13247,75 @@ ], "time": "2023-03-01T10:32:47+00:00" }, + { + "name": "symfony/stimulus-bundle", + "version": "v2.14.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/stimulus-bundle.git", + "reference": "f775f6e811215156bfe41e6be234272d0c27e02b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/f775f6e811215156bfe41e6be234272d0c27e02b", + "reference": "f775f6e811215156bfe41e6be234272d0c27e02b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/deprecation-contracts": "^2.0|^3.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "twig/twig": "^2.15.3|^3.4.3" + }, + "require-dev": { + "symfony/asset-mapper": "^6.3|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "zenstruck/browser": "^1.4" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\UX\\StimulusBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Integration with your Symfony app & Stimulus!", + "keywords": [ + "symfony-ux" + ], + "support": { + "source": "https://github.com/symfony/stimulus-bundle/tree/v2.14.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-07T20:26:48+00:00" + }, { "name": "symfony/stopwatch", "version": "v6.2.7", @@ -18590,5 +18659,5 @@ "ext-zip": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/config/bundles.php b/config/bundles.php index efd16ea..9f15f1f 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -33,4 +33,5 @@ return [ Payum\Bundle\PayumBundle\PayumBundle::class => ['all' => true], FOS\CKEditorBundle\FOSCKEditorBundle::class => ['all' => true], Misd\PhoneNumberBundle\MisdPhoneNumberBundle::class => ['all' => true], + Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], ]; diff --git a/fixtures/prod-boot/configuration.yaml b/fixtures/prod-boot/configuration.yaml index 4866531..e2facd2 100644 --- a/fixtures/prod-boot/configuration.yaml +++ b/fixtures/prod-boot/configuration.yaml @@ -4,8 +4,9 @@ App\Entity\Configuration: features (extends configuration_template): configuration: - services: - servicesEnabled: true + global: + globalServicesEnabled: true + globalPaidMembership: false notificationsSender: notificationsSenderEmail: info@example.com notificationsSenderName: Contact diff --git a/fixtures/test/configuration.yaml b/fixtures/test/configuration.yaml index 74fe9ee..97062e6 100644 --- a/fixtures/test/configuration.yaml +++ b/fixtures/test/configuration.yaml @@ -4,8 +4,9 @@ App\Entity\Configuration: features (extends configuration_template): configuration: - services: - servicesEnabled: true + global: + globalServicesEnabled: true + globalPaidMembership: false notificationsSender: notificationsSenderEmail: info@example.com notificationsSenderName: Contact diff --git a/fixtures/test/group_offer.yaml b/fixtures/test/group_offer.yaml index 723a1f4..d2881b3 100644 --- a/fixtures/test/group_offer.yaml +++ b/fixtures/test/group_offer.yaml @@ -4,11 +4,11 @@ App\Entity\GroupOffer: # Templates group_offer_group_1_template (template, extends group_offer_template): group: '@group_1' - type: !php/enum App\Enum\Group\GroupOfferType::YEARLY + type: !php/enum App\Enum\OfferType::YEARLY group_offer_group_7_template (template, extends group_offer_template): group: '@group_7' - type: !php/enum App\Enum\Group\GroupOfferType::YEARLY + type: !php/enum App\Enum\OfferType::YEARLY # Group 1 group_offer_group_1_10 (extends group_offer_group_1_template): diff --git a/fixtures/test/platform_offer.yaml b/fixtures/test/platform_offer.yaml new file mode 100644 index 0000000..9a1d756 --- /dev/null +++ b/fixtures/test/platform_offer.yaml @@ -0,0 +1,11 @@ +App\Entity\PlatformOffer: + platform_offer_1: + id: + name: Lorem ipsum + price: 2000 + type: !php/enum App\Enum\OfferType::YEARLY + + platform_offer_2: + name: Aliquet risus + price: 200 + type: !php/enum App\Enum\OfferType::MONTHLY diff --git a/src/Controller/Admin/GroupCrudController.php b/src/Controller/Admin/GroupCrudController.php index fb42db0..04498d2 100755 --- a/src/Controller/Admin/GroupCrudController.php +++ b/src/Controller/Admin/GroupCrudController.php @@ -19,6 +19,7 @@ use App\Form\Type\Security\GroupInvitationFormType; use App\Helper\CsvExporter; use App\Message\Command\Group\CreateGroupInvitationMessage; use App\MessageBus\CommandBus; +use App\Repository\ConfigurationRepository; use App\Security\Checker\AuthorizationChecker; use Doctrine\ORM\QueryBuilder; use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection; @@ -66,6 +67,7 @@ final class GroupCrudController extends AbstractCrudController implements GroupA private readonly TranslatorInterface $translator, private readonly FilterFactory $filterFactory, private readonly SluggerInterface $slugger, + private readonly ConfigurationRepository $configurationRepository ) { } @@ -75,6 +77,7 @@ final class GroupCrudController extends AbstractCrudController implements GroupA ->setEntityLabelInPlural('groups') ->setSearchFields(['name', 'description']) ->setDefaultSort(['id' => 'ASC']) + ->overrideTemplate('crud/field/boolean', 'admin/field/services_enabled.html.twig') ; } @@ -230,8 +233,24 @@ final class GroupCrudController extends AbstractCrudController implements GroupA ->setFormTypeOption('class', GroupMembership::class) ->setChoices(GroupMembership::getAsArray()); + if ($this->configurationRepository->getInstanceConfigurationOrCreate()->getServicesEnabled()) { + $servicesEnabledField = BooleanField::new('servicesEnabled') + ->renderAsSwitch() + ->setFormTypeOption('attr', [ + 'data-controller' => 'admin-parentgroup', + 'data-admin-parentgroup-target' => 'servicesEnabledField', + ]) + ->addWebpackEncoreEntries('admin'); + } + $parentField = AssociationField::new('parent') - ->setRequired(false); + ->setRequired(false) + ->addWebpackEncoreEntries('admin') + ->setFormTypeOption('attr', [ + 'data-controller' => 'admin-parentgroup', + 'data-admin-parentgroup-target' => 'parentField', + ]) + ; $childrenField = AssociationField::new('children'); $usersField = AssociationField::new('userGroups') ->setTemplatePath('admin/group/user_groups_field.html.twig'); @@ -247,14 +266,20 @@ final class GroupCrudController extends AbstractCrudController implements GroupA $panels = $this->getPanels(); if ($pageName === Crud::PAGE_INDEX) { - return [$nameField, $typeField, $parentField, $membershipField, $usersField, $createdAt, $updatedAt]; + $fields = [$nameField, $typeField, $parentField, $membershipField, $usersField, $createdAt, $updatedAt]; + + if ($this->configurationRepository->getInstanceConfigurationOrCreate()->getServicesEnabled()) { + array_splice($fields, 3, 0, [$servicesEnabledField]); + } + + return $fields; } if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { $typeField->setChoices(GroupType::cases()); $membershipField->setChoices(GroupMembership::cases()); - return [ + $fields = [ $nameField, $typeField, $membershipField, @@ -264,11 +289,17 @@ final class GroupCrudController extends AbstractCrudController implements GroupA $invitationByAdminField, $membershipField, ]; + + if ($this->configurationRepository->getInstanceConfigurationOrCreate()->getServicesEnabled()) { + array_splice($fields, 3, 0, [$servicesEnabledField]); + } + + return $fields; } // show - return [ + $fields = [ $panels['information'], $nameField, $parentField, @@ -283,6 +314,12 @@ final class GroupCrudController extends AbstractCrudController implements GroupA $updatedAt, $createdAt, ]; + + if ($this->configurationRepository->getInstanceConfigurationOrCreate()->getServicesEnabled()) { + array_splice($fields, 2, 0, [$servicesEnabledField]); + } + + return $fields; } /** @@ -309,7 +346,7 @@ final class GroupCrudController extends AbstractCrudController implements GroupA } /** - * For now we export exactly what we see in the list to avoid seurity problems. + * For now we export exactly what we see in the list to avoid security problems. */ public function export(AdminContext $context): Response { diff --git a/src/Controller/Admin/GroupOfferCrudController.php b/src/Controller/Admin/GroupOfferCrudController.php index 1597cca..8b93f18 100755 --- a/src/Controller/Admin/GroupOfferCrudController.php +++ b/src/Controller/Admin/GroupOfferCrudController.php @@ -10,12 +10,12 @@ use App\EasyAdmin\Field\FieldTrait; use App\EasyAdmin\Filter\EnumFilter; use App\EasyAdmin\Filter\UserGroup\MyGroupFilter; use App\EasyAdmin\Filter\UuidFilter; -use App\EasyAdmin\Form\Type\GroupOfferTypeType; +use App\EasyAdmin\Form\Type\OfferTypeType; use App\Entity\GroupOffer; use App\Entity\User; use App\Enum\Group\GroupMembership; -use App\Enum\Group\GroupOfferType; use App\Enum\Group\UserMembership; +use App\Enum\OfferType; use App\Security\Checker\AuthorizationChecker; use Doctrine\ORM\QueryBuilder; use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection; @@ -65,7 +65,7 @@ final class GroupOfferCrudController extends AbstractCrudController implements G return $filters ->add(UuidFilter::new('id')) ->add(MyGroupFilter::new('group')) - ->add(EnumFilter::new('membership', GroupOfferTypeType::class)) + ->add(EnumFilter::new('membership', OfferTypeType::class)) ->add('name') ->add('active') ; @@ -129,8 +129,8 @@ final class GroupOfferCrudController extends AbstractCrudController implements G $nameField = TextField::new('name'); $typeField = ChoiceField::new('type') ->setFormType(EnumType::class) - ->setFormTypeOption('class', GroupOfferType::class) - ->setChoices(GroupOfferType::getAsArray()); + ->setFormTypeOption('class', OfferType::class) + ->setChoices(OfferType::getAsArray()); $priceField = MoneyField::new('price') ->setCurrencyPropertyPath('currency') @@ -149,7 +149,7 @@ final class GroupOfferCrudController extends AbstractCrudController implements G } if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { - $typeField->setChoices(GroupOfferType::cases()); + $typeField->setChoices(OfferType::cases()); return [ $groupField, diff --git a/src/Controller/Admin/PlatformOfferCrudController.php b/src/Controller/Admin/PlatformOfferCrudController.php new file mode 100644 index 0000000..0c49a27 --- /dev/null +++ b/src/Controller/Admin/PlatformOfferCrudController.php @@ -0,0 +1,121 @@ +setEntityLabelInPlural('platform_offers') + ->setSearchFields(['name']) + ; + } + + public function configureFilters(Filters $filters): Filters + { + return $filters + ->add(UuidFilter::new('id')) + ->add(EnumFilter::new('type', OfferTypeType::class)) + ->add('name') + ->add('active') + ; + } + + public function configureActions(Actions $actions): Actions + { + return $actions + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::INDEX) + ; + } + + public static function getEntityFqcn(): string + { + return PlatformOffer::class; + } + + public function configureFields(string $pageName): iterable + { + $idFIeld = IdField::new('id') + ->setLabel('id') + ->hideOnForm(); + + $nameField = TextField::new('name'); + $typeField = ChoiceField::new('type') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', OfferType::class) + ->setChoices(OfferType::getAsArray()); + + $priceField = MoneyField::new('price') + ->setCurrencyPropertyPath('currency') + ->setStoredAsCents(); + $currencyField = CurrencyField::new('currency'); + + $activeField = BooleanField::new('active') + ->setTemplatePath('easy_admin/field/boolean.html.twig') + ; + $createdAtField = DateTimeField::new('createdAt'); + $updatedAtField = DateTimeField::new('updatedAt'); + + $panels = $this->getPanels(); + if ($pageName === Crud::PAGE_INDEX) { + return [$nameField, $typeField, $priceField, $activeField, $createdAtField, $updatedAtField]; + } + + if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { + $typeField->setChoices(OfferType::cases()); + + return [ + $nameField, + $typeField, + $priceField, + $currencyField, + $activeField, + ]; + } + + // show + return [ + $panels['information'], + $nameField, + $typeField, + $priceField, + $currencyField, + + $panels['tech_information'], + $idFIeld, + $updatedAtField, + $createdAtField, + ]; + } +} diff --git a/src/Doctrine/Behavior/AbstractOfferEntity.php b/src/Doctrine/Behavior/AbstractOfferEntity.php new file mode 100644 index 0000000..0355573 --- /dev/null +++ b/src/Doctrine/Behavior/AbstractOfferEntity.php @@ -0,0 +1,148 @@ +name.' ('.$this->type->value.')'; + } + + public function getId(): Uuid + { + return $this->id; + } + + public function setId(Uuid $uuid): self + { + $this->id = $uuid; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getType(): OfferType + { + return $this->type; + } + + public function setType(OfferType $type): self + { + $this->type = $type; + + return $this; + } + + public function setPrice(int $price): self + { + $this->price = $price; + + return $this; + } + + public function getPrice(): int + { + return $this->price; + } + + public function getActualPrice(): int + { + return $this->price / 100; + } + + public function getCurrency(): string + { + return $this->currency; + } + + public function setCurrency(string $currency): self + { + $this->currency = $currency; + + return $this; + } + + public function isActive(): bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + + return $this; + } +} diff --git a/src/EasyAdmin/Form/Type/GroupOfferTypeType.php b/src/EasyAdmin/Form/Type/OfferTypeType.php similarity index 75% rename from src/EasyAdmin/Form/Type/GroupOfferTypeType.php rename to src/EasyAdmin/Form/Type/OfferTypeType.php index 719e9fb..2efa4fd 100644 --- a/src/EasyAdmin/Form/Type/GroupOfferTypeType.php +++ b/src/EasyAdmin/Form/Type/OfferTypeType.php @@ -5,20 +5,20 @@ declare(strict_types=1); namespace App\EasyAdmin\Form\Type; use App\Controller\Admin\DashboardController; -use App\Enum\Group\GroupOfferType; +use App\Enum\OfferType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\OptionsResolver\OptionsResolver; /** - * Form type for the GroupOfferType enumeration. + * Form type for the OfferType enumeration. */ -class GroupOfferTypeType extends AbstractType +class OfferTypeType extends AbstractType { public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'choices' => GroupOfferType::getAsArray(), + 'choices' => OfferType::getAsArray(), 'translation_domain' => DashboardController::DOMAIN, ]); } diff --git a/src/Entity/Configuration.php b/src/Entity/Configuration.php index e60559c..fa5d509 100644 --- a/src/Entity/Configuration.php +++ b/src/Entity/Configuration.php @@ -78,19 +78,26 @@ class Configuration /** * @return bool[] */ - public function getServices(): array + public function getGlobals(): array { - /** @var array $services */ - $services = $this->configuration['services'] ?? []; + /** @var array $globals */ + $globals = $this->configuration['global'] ?? []; - return $services; + return $globals; } public function getServicesEnabled(): bool { - $services = $this->getServices(); + $globals = $this->getGlobals(); - return $services['servicesEnabled']; + return $globals['globalServicesEnabled']; + } + + public function getPaidMembership(): bool + { + $globals = $this->getGlobals(); + + return $globals['globalPaidMembership']; } /** diff --git a/src/Entity/Group.php b/src/Entity/Group.php index fa86764..34c5cc1 100755 --- a/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -7,10 +7,15 @@ namespace App\Entity; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; use App\Doctrine\Behavior\TimestampableEntity; use App\Enum\Group\GroupMembership; use App\Enum\Group\GroupType; use App\Repository\GroupRepository; +use App\State\GroupsProvider; +use App\State\Processor\GroupChildServicesEnabledProcessor; use App\Validator as AppAssert; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -26,6 +31,16 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\Index(columns: ['type'])] #[ApiFilter(OrderFilter::class, properties: ['name'])] #[AppAssert\Constraints\Group\GroupParentNotSelf] +#[ApiResource( + operations: [ + new GetCollection(provider: GroupsProvider::class), + new Patch( + uriTemplate: '/groups/{id}/disable_child_services', + input: false, + processor: GroupChildServicesEnabledProcessor::class + ), + ] +)] class Group implements \Stringable { use TimestampableEntity; @@ -123,6 +138,9 @@ class Group implements \Stringable #[ORM\ManyToMany(targetEntity: Product::class, mappedBy: 'groups')] private Collection $products; + #[ORM\Column(type: 'boolean')] + private bool $servicesEnabled = false; + public function __construct() { $this->children = new ArrayCollection(); @@ -340,6 +358,16 @@ class Group implements \Stringable return $this; } + public function getServicesEnabled(): bool + { + return $this->servicesEnabled; + } + + public function setServicesEnabled(bool $servicesEnabled): void + { + $this->servicesEnabled = $servicesEnabled; + } + // End of basic 'etters ---------------------------------------------------- /** diff --git a/src/Entity/GroupOffer.php b/src/Entity/GroupOffer.php index 87263c1..752655e 100644 --- a/src/Entity/GroupOffer.php +++ b/src/Entity/GroupOffer.php @@ -4,33 +4,16 @@ declare(strict_types=1); namespace App\Entity; -use App\Doctrine\Behavior\TimestampableEntity; -use App\Enum\Group\GroupOfferType; +use App\Doctrine\Behavior\AbstractOfferEntity; use App\Repository\GroupOfferRepository; -use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; -use Symfony\Component\Uid\Uuid; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: GroupOfferRepository::class)] #[ORM\Table(name: 'group_offer')] #[ORM\Index(columns: ['type'])] -class GroupOffer implements \Stringable +class GroupOffer extends AbstractOfferEntity { - use TimestampableEntity; - - final public const DEFAULT_CURRENCY = 'EUR'; - - /** - * Generates a V6 uuid. - */ - #[ORM\Id] - #[ORM\Column(type: 'uuid', unique: true)] - #[ORM\GeneratedValue(strategy: 'CUSTOM')] - #[ORM\CustomIdGenerator(class: UuidGenerator::class)] - private Uuid $id; - /** * Related group. */ @@ -39,134 +22,15 @@ class GroupOffer implements \Stringable #[ORM\OrderBy(['createdAt' => 'ASC'])] private Group $group; - /** - * Short name of the offer. - */ - #[ORM\Column(type: Types::STRING, length: 255, nullable: false)] - #[Assert\NotBlank] - #[Assert\Length(max: 255)] - private string $name; - - /** - * Type of offer. - */ - #[ORM\Column(name: 'type', type: 'string', nullable: false, enumType: GroupOfferType::class)] - #[Assert\NotBlank] - protected GroupOfferType $type; - - /** - * Price, we stored the amount multiplied by 100 so we can use an integer for - * this property. - */ - #[ORM\Column(type: Types::INTEGER, nullable: false)] - protected int $price; - - /** - * Associated currency for the price property. - * - * @see https://en.wikipedia.org/wiki/ISO_4217 - */ - #[ORM\Column(type: Types::STRING, nullable: false)] - protected string $currency = self::DEFAULT_CURRENCY; - - /** - * If the offer is visible on the front site. Can be use to deactivate offers - * for some time. - */ - #[ORM\Column(type: 'boolean', nullable: false)] - protected bool $active = true; - - public function __toString() - { - return $this->name.' ('.$this->type->value.')'; - } - - public function getId(): Uuid - { - return $this->id; - } - - public function setId(Uuid $uuid): self - { - $this->id = $uuid; - - return $this; - } - public function getGroup(): Group { return $this->group; } - public function setGroup(Group $group): GroupOffer + public function setGroup(Group $group): self { $this->group = $group; return $this; } - - public function getName(): string - { - return $this->name; - } - - public function setName(string $name): GroupOffer - { - $this->name = $name; - - return $this; - } - - public function getType(): GroupOfferType - { - return $this->type; - } - - public function setType(GroupOfferType $type): GroupOffer - { - $this->type = $type; - - return $this; - } - - public function setPrice(int $price): self - { - $this->price = $price; - - return $this; - } - - public function getPrice(): int - { - return $this->price; - } - - public function getActualPrice(): int - { - return $this->price / 100; - } - - public function getCurrency(): string - { - return $this->currency; - } - - public function setCurrency(string $currency): self - { - $this->currency = $currency; - - return $this; - } - - public function isActive(): bool - { - return $this->active; - } - - public function setActive(bool $active): GroupOffer - { - $this->active = $active; - - return $this; - } } diff --git a/src/Entity/PlatformOffer.php b/src/Entity/PlatformOffer.php new file mode 100644 index 0000000..7462085 --- /dev/null +++ b/src/Entity/PlatformOffer.php @@ -0,0 +1,16 @@ + 'Swicth the status of the product'], + openapiContext: ['summary' => 'Switch the status of the product'], normalizationContext: ['groups' => [ProductSwitchProcessor::class]], security: "is_granted('".ProductVoter::EDIT."', object)", input: false, diff --git a/src/Entity/User.php b/src/Entity/User.php index 132004d..50140db 100755 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -770,15 +770,23 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, ImageIn * * @return Collection */ - public function getMyGroupsAsAdmin(): Collection + public function getMyGroupsAsAdmin(bool $enabledServices = false): Collection { $adminUserGroups = $this->userGroups->filter( static fn (UserGroup $userGroup) => $userGroup->getMembership()->isAdmin() || $userGroup->isMainAdminAccount() ); - return new ArrayCollection( + $groups = new ArrayCollection( array_map(static fn (UserGroup $userGroup) => $userGroup->getGroup(), $adminUserGroups->toArray()) ); + + if ($enabledServices) { + return $groups->filter( + static fn (Group $group) => $group->getServicesEnabled() + ); + } + + return $groups; } /** diff --git a/src/Entity/UserGroup.php b/src/Entity/UserGroup.php index 8400882..d4e38f5 100644 --- a/src/Entity/UserGroup.php +++ b/src/Entity/UserGroup.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Entity; +use ApiPlatform\Metadata\ApiResource; use App\Doctrine\Behavior\TimestampableEntity; use App\Enum\Group\UserMembership; use App\Repository\UserGroupRepository; @@ -16,6 +17,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: UserGroupRepository::class)] #[ORM\UniqueConstraint(columns: ['user', 'group'])] +#[ApiResource] class UserGroup { use TimestampableEntity; diff --git a/src/Enum/Group/GroupOfferType.php b/src/Enum/OfferType.php similarity index 82% rename from src/Enum/Group/GroupOfferType.php rename to src/Enum/OfferType.php index bbf761b..611b55f 100644 --- a/src/Enum/Group/GroupOfferType.php +++ b/src/Enum/OfferType.php @@ -2,22 +2,20 @@ declare(strict_types=1); -namespace App\Enum\Group; +namespace App\Enum; -use App\Enum\AsArrayTrait; - -enum GroupOfferType: string +enum OfferType: string { use AsArrayTrait; - // The user only to pay once to access the group. In his case the end date is + // 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 // set. The end date can always be set manually in case of a problem. case ONESHOT = 'oneshot'; // Monthly subscription. The membership is valid 1 month and the user has to // renew it once the end date is over. This can be useful when a user when to - // try a group on the short period before taking a longer subscription. + // 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 diff --git a/src/Form/Type/Admin/ParametersFormType.php b/src/Form/Type/Admin/ParametersFormType.php index c4e7735..034ae0c 100755 --- a/src/Form/Type/Admin/ParametersFormType.php +++ b/src/Form/Type/Admin/ParametersFormType.php @@ -25,13 +25,20 @@ final class ParametersFormType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $builder - ->add('servicesEnabled', CheckboxType::class, [ + ->add('globalServicesEnabled', CheckboxType::class, [ 'label' => 'parameter.services', 'label_attr' => [ 'class' => 'checkbox-inline checkbox-switch', ], ]) + ->add('globalPaidMembership', CheckboxType::class, [ + 'label' => 'parameter.paid_membership', + 'label_attr' => [ + 'class' => 'checkbox-inline checkbox-switch', + ], + ]) + ->add('notificationsSenderEmail', EmailType::class, [ 'label' => 'parameter.mail', 'label_attr' => ['class' => 'col-sm-2 col-form-label'], diff --git a/src/Form/Type/Group/CreateGroupFormType.php b/src/Form/Type/Group/CreateGroupFormType.php index 565827a..125dbae 100644 --- a/src/Form/Type/Group/CreateGroupFormType.php +++ b/src/Form/Type/Group/CreateGroupFormType.php @@ -4,23 +4,30 @@ declare(strict_types=1); namespace App\Form\Type\Group; +use App\Entity\Configuration; use App\Entity\Group; use App\Entity\User; use App\Enum\Group\GroupMembership; use App\Enum\Group\GroupType; +use App\Repository\ConfigurationRepository; +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\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Webmozart\Assert\Assert; class CreateGroupFormType extends AbstractType { public function __construct( private readonly Security $security, + private readonly ConfigurationRepository $configurationRepository, + private readonly GroupRepository $groupRepository ) { } @@ -28,6 +35,8 @@ class CreateGroupFormType extends AbstractType { /** @var User $user */ $user = $this->security->getUser(); + $configuration = $this->configurationRepository->getInstanceConfiguration(); + $myGroupsWithDisabledServices = $this->groupRepository->getGroupsByEnabledServices(false, $user); $builder ->add('name', TextType::class, [ @@ -43,27 +52,44 @@ class CreateGroupFormType extends AbstractType 'label_attr' => ['class' => 'fs-6 text-black'], 'expanded' => true, 'choice_label' => 'transKey', - ]) - ->add('membership', EnumType::class, [ - 'class' => GroupMembership::class, - 'label' => 'templates.pages.group.create.form.membership', - 'label_attr' => ['class' => 'fs-6 text-black'], - 'expanded' => true, - ]) - ->add('submit', SubmitType::class, [ - 'label' => 'templates.pages.group.create.form.submit', - 'attr' => ['class' => 'btn btn-primary btn-sm d-grid col-12 my-5'], ]); + Assert::isInstanceOf($configuration, Configuration::class); + if ($configuration->getServicesEnabled()) { + $builder + ->add('servicesEnabled', CheckboxType::class, [ + 'label' => 'templates.pages.group.create.form.servicesEnabled', + 'label_attr' => ['class' => 'fs-6 text-black mb-3 switch-custom'], + 'required' => false, + 'attr' => [ + 'data-action' => 'click->parentgroup#updateParentOptions', + 'data-parentgroup-target' => 'servicesEnabledField', + 'data-user-id' => $user->getId(), + ], + ]); + } + $builder + ->add('membership', EnumType::class, [ + 'class' => GroupMembership::class, + 'label' => 'templates.pages.group.create.form.membership', + 'label_attr' => ['class' => 'fs-6 text-black'], + 'expanded' => true, + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'templates.pages.group.create.form.submit', + 'attr' => ['class' => 'btn btn-primary btn-sm d-grid col-12 my-5'], + ]); - $myGroups = $user->getMyGroupsAsAdmin(); - if (!$myGroups->isEmpty()) { + if ([] === $myGroupsWithDisabledServices) { $builder ->add('parent', EntityType::class, [ 'class' => Group::class, - 'choices' => $myGroups, + 'choices' => $myGroupsWithDisabledServices, 'label' => 'templates.pages.group.create.form.subgroup', 'label_attr' => ['class' => 'fs-6 text-black'], 'required' => false, + 'attr' => [ + 'data-parentgroup-target' => 'parentField', + ], ]); } } diff --git a/src/Message/Command/Admin/ParametersFormCommand.php b/src/Message/Command/Admin/ParametersFormCommand.php index c719a45..73ba808 100755 --- a/src/Message/Command/Admin/ParametersFormCommand.php +++ b/src/Message/Command/Admin/ParametersFormCommand.php @@ -17,9 +17,12 @@ final class ParametersFormCommand extends AbstractFormCommand final public const ONLY_ADMIN = 'only_admin'; final public const ALL = 'all'; - // services section ————————————————————————————————————————————— + // global section ————————————————————————————————————————————— #[Assert\Type('bool')] - public bool $servicesEnabled = true; + public bool $globalServicesEnabled = true; + + #[Assert\Type('bool')] + public bool $globalPaidMembership = false; // notificationsSender section ————————————————————————————————————————————— #[Assert\Email()] @@ -59,7 +62,7 @@ final class ParametersFormCommand extends AbstractFormCommand protected function getSections(): array { return [ - 'services', + 'global', 'notificationsSender', 'contact', 'groups', diff --git a/src/MessageHandler/Command/Admin/ParametersFormCommandHandler.php b/src/MessageHandler/Command/Admin/ParametersFormCommandHandler.php index b9d42f0..dcd9c09 100644 --- a/src/MessageHandler/Command/Admin/ParametersFormCommandHandler.php +++ b/src/MessageHandler/Command/Admin/ParametersFormCommandHandler.php @@ -6,13 +6,15 @@ namespace App\MessageHandler\Command\Admin; use App\Message\Command\Admin\ParametersFormCommand; use App\Repository\ConfigurationRepository; +use App\Repository\GroupRepository; use Symfony\Component\Messenger\Attribute\AsMessageHandler; #[AsMessageHandler] final class ParametersFormCommandHandler { public function __construct( - private readonly ConfigurationRepository $configurationRepository + private readonly ConfigurationRepository $configurationRepository, + private readonly GroupRepository $groupRepository, ) { } @@ -24,5 +26,10 @@ final class ParametersFormCommandHandler $configuration = $this->configurationRepository->getInstanceConfigurationOrCreate(); $configuration->setConfiguration($message->toJsonArray()); $this->configurationRepository->save($configuration, true); + + if (!$configuration->getServicesEnabled()) { + $groups = $this->groupRepository->findAll(); + $this->groupRepository->disableServicesForAllGroups($groups); + } } } diff --git a/src/Repository/ConfigurationRepository.php b/src/Repository/ConfigurationRepository.php index 9601154..c2f392d 100644 --- a/src/Repository/ConfigurationRepository.php +++ b/src/Repository/ConfigurationRepository.php @@ -61,13 +61,13 @@ final class ConfigurationRepository extends ServiceEntityRepository public function getServicesParameter(): bool { - /** @var array{configuration: array{ services: array{ servicesEnabled: bool }}} $config */ + /** @var array{configuration: array{ global: array{ globalServicesEnabled: bool }}} $config */ $config = $this ->createQueryBuilder('c') ->select('c.configuration') ->setMaxResults(1) ->getQuery()->getOneOrNullResult(); - return $config['configuration']['services']['servicesEnabled']; + return $config['configuration']['global']['globalServicesEnabled']; } } diff --git a/src/Repository/GroupRepository.php b/src/Repository/GroupRepository.php index 65a01b6..a1c1a5f 100644 --- a/src/Repository/GroupRepository.php +++ b/src/Repository/GroupRepository.php @@ -8,6 +8,7 @@ use App\Entity\Group; use App\Entity\User; use App\Entity\UserGroup; use App\Enum\Group\GroupType; +use App\Enum\Group\UserMembership; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; @@ -87,4 +88,51 @@ final class GroupRepository extends ServiceEntityRepository ->andWhere('ug.user = :user') ->setParameter('user', $user); } + + /** + * @return Group[] + */ + public function getGroupsByEnabledServices(bool $servicesEnabled, ?User $user = null): array + { + $qb = $this->createQueryBuilder('g') + ->andWhere('g.servicesEnabled = :servicesEnabled') + ->setParameter('servicesEnabled', $servicesEnabled); + + if ($user instanceof User) { + $qb + ->leftJoin('g.userGroups', 'gu') + ->andWhere('gu.user = :user') + ->andWhere('gu.mainAdminAccount = :mainAdminAccount OR gu.membership = :membership') + ->setParameter('user', $user) + ->setParameter('mainAdminAccount', true) + ->setParameter('membership', UserMembership::ADMIN); + } + + /** @var Group[] */ + return $qb + ->getQuery() + ->getResult(); + } + + /** + * @param Group[] $groups + */ + public function disableServicesForAllGroups(array $groups): void + { + foreach ($groups as $group) { + $group->setServicesEnabled(false); + $this->getEntityManager()->persist($group); + } + $this->getEntityManager()->flush(); + } + + public function disableServicesForChildGroup(Group $group): void + { + /** @var Group $child */ + foreach ($group->getChildren() as $child) { + $child->setServicesEnabled(false); + $this->getEntityManager()->persist($child); + } + $this->getEntityManager()->flush(); + } } diff --git a/src/Repository/PlatformOfferRepository.php b/src/Repository/PlatformOfferRepository.php new file mode 100644 index 0000000..8ae77f5 --- /dev/null +++ b/src/Repository/PlatformOfferRepository.php @@ -0,0 +1,35 @@ + + * + * @method PlatformOffer|null find($id, $lockMode = null, $lockVersion = null) + * @method PlatformOffer|null findOneBy(array $criteria, array $orderBy = null) + * @method PlatformOffer[] findAll() + * @method PlatformOffer[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class PlatformOfferRepository extends ServiceEntityRepository +{ + private const ENTITY_CLASS = PlatformOffer::class; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, self::ENTITY_CLASS); + } + + /** + * Return an object or throws an exception if not found. + */ + public function get(mixed $id, int|null $lockMode = null, int|null $lockVersion = null): PlatformOffer + { + return $this->find($id, $lockMode, $lockVersion) ?? throw new \LogicException('Platform offer not found.'); + } +} diff --git a/src/State/GroupsProvider.php b/src/State/GroupsProvider.php new file mode 100644 index 0000000..31a1ad7 --- /dev/null +++ b/src/State/GroupsProvider.php @@ -0,0 +1,34 @@ + + */ +class GroupsProvider implements ProviderInterface +{ + public function __construct( + readonly private GroupRepository $groupRepository, + readonly private UserRepository $userRepository + ) { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null // @phpstan-ignore-line + { + if (isset($context['filters']['user'])) { + $user = $this->userRepository->find($context['filters']['user']); + + return $this->groupRepository->getGroupsByEnabledServices($context['filters']['services_enabled'] === 'true', $user); + } + + return $this->groupRepository->getGroupsByEnabledServices($context['filters']['services_enabled'] === 'true'); + } +} diff --git a/src/State/Processor/GroupChildServicesEnabledProcessor.php b/src/State/Processor/GroupChildServicesEnabledProcessor.php new file mode 100644 index 0000000..fa2cd15 --- /dev/null +++ b/src/State/Processor/GroupChildServicesEnabledProcessor.php @@ -0,0 +1,31 @@ + $uriVariables + * @param array $context + */ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Group + { + Assert::isInstanceOf($data, Group::class); + $this->groupRepository->disableServicesForChildGroup($data); + + return $data; + } +} diff --git a/symfony.lock b/symfony.lock index 2073928..edffc86 100644 --- a/symfony.lock +++ b/symfony.lock @@ -185,6 +185,15 @@ "config/packages/http_discovery.yaml" ] }, + "phpstan/phpstan": { + "version": "1.11", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.0", + "ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767" + } + }, "phpunit/phpunit": { "version": "9.5", "recipe": { @@ -444,6 +453,15 @@ "config/packages/security.yaml" ] }, + "symfony/stimulus-bundle": { + "version": "2.18", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.8", + "ref": "9e33a8a3794b603fb4be6c04ee5ecab901ce549e" + } + }, "symfony/translation": { "version": "6.2", "recipe": { diff --git a/templates/admin/field/services_enabled.html.twig b/templates/admin/field/services_enabled.html.twig new file mode 100644 index 0000000..f87d522 --- /dev/null +++ b/templates/admin/field/services_enabled.html.twig @@ -0,0 +1,17 @@ +{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #} +{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #} +{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #} +{% trans_default_domain 'EasyAdminBundle' %} + +{% if ea.crud.currentAction == 'detail' or not field.customOptions.get('renderAsSwitch') %} + + {{ (field.value == true ? 'label.true' : 'label.false')|trans }} + +{% else %} +
+ + +
+{% endif %} diff --git a/templates/admin/parameters.html.twig b/templates/admin/parameters.html.twig index 435fdda..d6354ca 100644 --- a/templates/admin/parameters.html.twig +++ b/templates/admin/parameters.html.twig @@ -14,12 +14,28 @@ {% block main %} {{ form_start(form) }}
-

{{ 'parameters.services.h2'|trans }}

+

{{ 'parameters.global.h2'|trans }}


- {{ form_widget(form.servicesEnabled) }} +

{{ 'parameters.services.h3'|trans }}

-

{{ 'parameters.senders.h2'|trans }}

+ {{ form_widget(form.globalServicesEnabled) }} + +

{{ 'parameters.paid_membership.h3'|trans }}

+ + {{ form_widget(form.globalPaidMembership) }} + + + +

{{ 'parameters.senders.h2'|trans }}


{{ form_row(form.notificationsSenderEmail) }} diff --git a/templates/pages/group/create.html.twig b/templates/pages/group/create.html.twig index 11ee476..a91f317 100644 --- a/templates/pages/group/create.html.twig +++ b/templates/pages/group/create.html.twig @@ -17,9 +17,20 @@ {{ form_row(form.name) }} {{ form_row(form.type) }} {{ form_row(form.membership) }} - {% if form.parent is defined and form.parent is not null %} - {{ form_row(form.parent) }} - {% endif %} + +
+ {% if form.servicesEnabled is defined %} +
+ {{ form_widget(form.servicesEnabled) }} +
+ {% endif %} + {% if form.parent is defined and form.parent is not null %} + {{ form_row(form.parent) }} + {% endif %} +
{{ form_widget(form.submit) }} {{ form_end(form) }}
diff --git a/tests/Functional/Controller/Admin/PlatformOfferCrudControllerTest.php b/tests/Functional/Controller/Admin/PlatformOfferCrudControllerTest.php new file mode 100644 index 0000000..e2a1d71 --- /dev/null +++ b/tests/Functional/Controller/Admin/PlatformOfferCrudControllerTest.php @@ -0,0 +1,57 @@ +loginAsAdmin($client); + + // list+custom filters + $filters = '&filters[type]='.(OfferType::MONTHLY->isMonthly() ? '1' : '0'); + + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', PlatformOfferCrudController::class.'&'.$filters)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', PlatformOfferCrudController::class, TestReference::PLATFORM_OFFER_1)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', PlatformOfferCrudController::class, TestReference::PLATFORM_OFFER_1)); + self::assertResponseIsSuccessful(); + + // new + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', PlatformOfferCrudController::class)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); + $client->submit($form, [ + $form->getName().'[name]' => 'New special offer', + $form->getName().'[type]' => 'yearly', + $form->getName().'[price]' => 490, + $form->getName().'[currency]' => 'EUR', + $form->getName().'[active]' => false, + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Group/CreateGroupActionTest.php b/tests/Functional/Controller/Group/CreateGroupActionTest.php index 046eb95..7cf1226 100644 --- a/tests/Functional/Controller/Group/CreateGroupActionTest.php +++ b/tests/Functional/Controller/Group/CreateGroupActionTest.php @@ -33,7 +33,7 @@ final class CreateGroupActionTest extends WebTestCase $client->submit($form, [ $form->getName().'[name]' => 'Groupe 1', $form->getName().'[type]' => 'public', - $form->getName().'[parent]' => TestReference::GROUP_1, + $form->getName().'[servicesEnabled]' => false, ]); self::assertResponseRedirects(); self::assertTrue(u($client->getResponse()->headers->get('Location'))->startsWith('http://localhost/admin')); diff --git a/tests/TestReference.php b/tests/TestReference.php index dc43b23..2bb1347 100644 --- a/tests/TestReference.php +++ b/tests/TestReference.php @@ -127,4 +127,7 @@ final class TestReference // payments 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'; } diff --git a/tests/Unit/Controller/Payment/DoneActionTest.php b/tests/Unit/Controller/Payment/DoneActionTest.php index bc4c817..1d18ce7 100644 --- a/tests/Unit/Controller/Payment/DoneActionTest.php +++ b/tests/Unit/Controller/Payment/DoneActionTest.php @@ -39,7 +39,7 @@ final class DoneActionTest extends TestCase ->setId($this->getUuid()) ->setSlug('group'); - return (new GroupOffer()) + return (new GroupOffer()) // @phpstan-ignore-line ->setId($this->getUuid()) ->setGroup($group); } diff --git a/tests/Unit/Enum/Group/GroupOfferTypeTest.php b/tests/Unit/Enum/Group/GroupOfferTypeTest.php index 392f199..c69b24a 100644 --- a/tests/Unit/Enum/Group/GroupOfferTypeTest.php +++ b/tests/Unit/Enum/Group/GroupOfferTypeTest.php @@ -4,14 +4,14 @@ declare(strict_types=1); namespace App\Tests\Unit\Enum\Group; -use App\Enum\Group\GroupOfferType; +use App\Enum\OfferType; use PHPUnit\Framework\TestCase; final class GroupOfferTypeTest extends TestCase { public function testGroupOfferType(): void { - self::assertTrue(GroupOfferType::MONTHLY->isMonthly()); - self::assertTrue(GroupOfferType::YEARLY->isYearly()); + self::assertTrue(OfferType::MONTHLY->isMonthly()); + self::assertTrue(OfferType::YEARLY->isYearly()); } } diff --git a/translations/admin.fr.xlf b/translations/admin.fr.xlf index cbd466e..db23fa1 100644 --- a/translations/admin.fr.xlf +++ b/translations/admin.fr.xlf @@ -260,11 +260,21 @@ Paramètres de l'instance - - parameters.services.h2 + + parameters.global.h2 + Paramètres généraux de la plateforme + + + + parameters.services.h3 Services + + parameters.paid_membership.h3 + Adhésion payante + + parameters.senders.h2 Expéditeur·rice des notifications diff --git a/translations/app/Controller/Admin/PlatformOfferCrudController/admin.fr.xlf b/translations/app/Controller/Admin/PlatformOfferCrudController/admin.fr.xlf new file mode 100644 index 0000000..8d81373 --- /dev/null +++ b/translations/app/Controller/Admin/PlatformOfferCrudController/admin.fr.xlf @@ -0,0 +1,20 @@ + + + +
+ +
+ + + platform_offers + Tarifs d'adhésion pour la plateforme + + + + PlatformOffer + Tarifs d'adhésion + + + +
+
diff --git a/translations/group/admin.fr.xlf b/translations/group/admin.fr.xlf index c2b00d8..fd333a5 100644 --- a/translations/group/admin.fr.xlf +++ b/translations/group/admin.fr.xlf @@ -25,6 +25,11 @@ Envoi d'invitations par les admins possible
+ + Services Enabled + Activer la disponibilité des services pour le groupe + + Invitation By Moderator Envoi d'invitations par les modérateur·rice·s possible diff --git a/translations/parameters/admin.fr.xlf b/translations/parameters/admin.fr.xlf index 87cfc04..d7e5243 100644 --- a/translations/parameters/admin.fr.xlf +++ b/translations/parameters/admin.fr.xlf @@ -6,12 +6,22 @@ - + parameter.services Services activés + + parameter.paid_membership + Adhésion à la plateforme payante + + + + parameter.set_price + Configurer le tarif + + parameter.mail diff --git a/translations/templates/pages/group/create/messages.fr.xlf b/translations/templates/pages/group/create/messages.fr.xlf index 5776265..331a35b 100644 --- a/translations/templates/pages/group/create/messages.fr.xlf +++ b/translations/templates/pages/group/create/messages.fr.xlf @@ -26,6 +26,11 @@ Type + + templates.pages.group.create.form.servicesEnabled + Activer la disponibilité des services pour le groupe + + templates.pages.group.create.form.membership Tarif diff --git a/webpack.config.js b/webpack.config.js index fef2287..a6620c4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -21,6 +21,7 @@ Encore * and one CSS file (e.g. app.css) if your JavaScript imports CSS. */ .addEntry('app', './assets/app.js') + .addEntry('admin', './assets/admin.js') // enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js) .enableStimulusBridge('./assets/controllers.json') From d13959ee1f88b6299e10348de6488b021c5586a6 Mon Sep 17 00:00:00 2001 From: Sarahshr <51380592+Sarahshr@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:41:33 +0200 Subject: [PATCH 02/28] Feat/disable services option for groups (#715) * add servicesDisabled field for groups and for global configuration comment libcurl upgrade to fix ci temporarly upgrade caddy version review fix test * feat: add paying membership option (#714) * feat: add paying membership option * fix: ci * fix: phpstan + review * fix: eslint ci From 9344e32cd97dc3225005d295f86f0ee370ce1b27 Mon Sep 17 00:00:00 2001 From: Thomas SAMSON <93578303+ThomasSamson@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:16:00 +0200 Subject: [PATCH 03/28] Re activate fixture job (#717) * Re activate fixture job * restore dailycron values * enable fixture * debug fixture cron * Enable postgres container on PR --- .github/workflows/deploy.yml | 10 +++++ .../templates/cronjob-fixture-reset.yaml | 22 ++++++----- helm/chart/templates/fixtures-job.yaml | 20 +++++----- helm/chart/values-nonprod.yml | 38 +++++++++++++++++++ helm/chart/values-prod.yml | 8 +++- helm/chart/values.yaml | 11 ++++-- 6 files changed, 84 insertions(+), 25 deletions(-) create mode 100644 helm/chart/values-nonprod.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7b66316..0244cc7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,12 +3,20 @@ name: Deploy on: workflow_call: secrets: + kubeconfig: + description: kubeconfig stored as a base64 encrypted secret + required: true domain: description: Main project deploy domain used in URLs required: true payum-apikey: description: Payum API Key required: true + sms-dsn: + description: SMS DSN + required: true + mailer-dsn: + description: MAILER DSN storage-key: description: storage key required: true @@ -117,6 +125,8 @@ jobs: --set=ingress.tls[0].secretName=${{ needs.meta.outputs.release_name }}-tls \ --set=ingress.tls[0].hosts[0]=${{ vars.DOMAIN }} \ --set=postgresql.url="${{ secrets.database-url }}" \ + --set=postgresql.enabled='${{ github.event_name == 'pull_request' }}' \ + --set=sms.dsn="${{ secrets.sms-dsn }}" \ --set=payum.apikey="${{ secrets.payum-apikey }}" \ --set=mailer.dsn="${{ secrets.mailer-dsn }}" \ --set=php.storage.bucket="${{ vars.STORAGE_BUCKET }}" \ diff --git a/helm/chart/templates/cronjob-fixture-reset.yaml b/helm/chart/templates/cronjob-fixture-reset.yaml index fa244e2..f8dc391 100644 --- a/helm/chart/templates/cronjob-fixture-reset.yaml +++ b/helm/chart/templates/cronjob-fixture-reset.yaml @@ -1,3 +1,4 @@ +{{- if .Values.php.fixtureCron.enabled }} apiVersion: batch/v1 kind: CronJob metadata: @@ -22,19 +23,20 @@ spec: serviceAccountName: {{ include "plateforme-ebs.serviceAccountName" . }} restartPolicy: Never containers: - - name: {{ .Chart.Name }}-cronjob-notify-ms-e-7 + - name: {{ .Chart.Name }}-cronjob-fixture-reset image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.php.image.pullPolicy }} command: ['/bin/sh', '-c'] - args: [' - bin/console doctrine:database:drop --if-exists --force; - bin/console doctrine:database:create --if-not-exists; - bin/console doctrine:schema:create; - bin/console doctrine:schema:validate; - bin/console messenger:setup-transports; - bin/console hautelook:fixtures:load --no-interaction -vv --no-bundles; - bin/console app:index-products; - '] + args: [' + set -ex; + bin/console doctrine:database:drop --if-exists --force; + bin/console doctrine:database:create --if-not-exists; + bin/console doctrine:schema:create; + bin/console doctrine:schema:validate; + bin/console messenger:setup-transports; + bin/console hautelook:fixtures:load --no-interaction -vv --no-bundles; + bin/console app:index-products;' + ] env: - name: API_ENTRYPOINT_HOST valueFrom: diff --git a/helm/chart/templates/fixtures-job.yaml b/helm/chart/templates/fixtures-job.yaml index 45f9245..09eca03 100644 --- a/helm/chart/templates/fixtures-job.yaml +++ b/helm/chart/templates/fixtures-job.yaml @@ -1,3 +1,4 @@ +{{- if .Values.php.fixtureJob.enabled -}} apiVersion: batch/v1 kind: Job metadata: @@ -32,18 +33,14 @@ spec: command: ['/bin/sh', '-c'] args: [' set -ex; - echo no fixtures job at the moment + bin/console doctrine:database:drop --if-exists --force; + bin/console doctrine:database:create --if-not-exists; + bin/console doctrine:schema:create; + bin/console doctrine:schema:validate; + bin/console messenger:setup-transports; + bin/console hautelook:fixtures:load --no-interaction -vv --no-bundles; + bin/console app:index-products; '] - # if [ "$APP_ENV" != "prod" ]; then - # composer install --prefer-dist --no-progress --no-suggest --no-interaction; - # fi; - # bin/console doctrine:database:drop --if-exists --force; - # bin/console doctrine:database:create --if-not-exists; - # bin/console doctrine:schema:create; - # bin/console doctrine:schema:validate; - # bin/console messenger:setup-transports; - # bin/console hautelook:fixtures:load --no-interaction -vv --no-bundles; - # bin/console app:index-products; env: - name: API_ENTRYPOINT_HOST valueFrom: @@ -208,3 +205,4 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/chart/values-nonprod.yml b/helm/chart/values-nonprod.yml new file mode 100644 index 0000000..b38ec60 --- /dev/null +++ b/helm/chart/values-nonprod.yml @@ -0,0 +1,38 @@ +imagePullSecrets: + - name: regcred + +ingress: + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production + hosts: + - host: toset + paths: + - path: / + pathType: Prefix + tls: + - secretName: toset + hosts: + - toset + +meilisearch: + persistence: + enabled: true + storageClass: "standard" + size: "1Gi" + +redis: + master: + persistence: + enabled: true + storageClass: "standard" + size: "1Gi" + +postgresql: + url: change_me + +php: + fixtureJob: + enabled: true + fixtureCron: + enabled: true \ No newline at end of file diff --git a/helm/chart/values-prod.yml b/helm/chart/values-prod.yml index 35498ed..c644cc5 100644 --- a/helm/chart/values-prod.yml +++ b/helm/chart/values-prod.yml @@ -29,4 +29,10 @@ redis: size: "1Gi" postgresql: - enabled: false \ No newline at end of file + url: change_me + +php: + fixtureJob: + enabled: true + fixtureCron: + enabled: true diff --git a/helm/chart/values.yaml b/helm/chart/values.yaml index d9a56fe..5eee356 100644 --- a/helm/chart/values.yaml +++ b/helm/chart/values.yaml @@ -30,6 +30,13 @@ php: usePathStyleEndpoint: true publicKey: "" secret: "" + fixtureCron: + enabled: false + fixtureJob: + enabled: false + +dailyCronjobs: + enabled: true maildev: enabled: false @@ -41,9 +48,6 @@ mailer: sms: dsn: "null://null" -dailyCronjobs: - enabled: true - consumer: # We don't use async for now so consumer isn't needed enabled: false @@ -73,6 +77,7 @@ postgresql: username: "example" password: "!ChangeMe!" database: "api" + postgresPassword: "!ChangeMe!" # Persistent Volume Storage configuration. # ref: https://kubernetes.io/docs/user-guide/persistent-volumes pullPolicy: IfNotPresent From 32ccd2415ef69a97802a57d2b1640a043784e692 Mon Sep 17 00:00:00 2001 From: ThomasSamson Date: Tue, 30 Jul 2024 14:36:43 +0200 Subject: [PATCH 04/28] Fix cronjob From 20251f6caff774a6b72dc8739926b625aaa40224 Mon Sep 17 00:00:00 2001 From: Sarahshr <51380592+Sarahshr@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:11:45 +0200 Subject: [PATCH 05/28] 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 --- config/packages/security.yaml | 4 +- fixtures/test/configuration.yaml | 1 + fixtures/test/platform_offer.yaml | 9 +- fixtures/test/user.yaml | 3 + .../Admin/PlatformOfferCrudController.php | 4 +- .../Payment/{ => Group}/DoneAction.php | 3 +- .../Payment/{ => Group}/GroupOfferTrait.php | 2 +- .../Payment/{ => Group}/PrepareAction.php | 3 +- .../Payment/PlatformMembership/DoneAction.php | 81 +++++++++++++++++ .../PlatformMembership/PrepareAction.php | 66 ++++++++++++++ .../User/Product/ServiceController.php | 2 + src/Doctrine/Behavior/AbstractOfferEntity.php | 2 +- .../Listener/MembershipPaidListener.php | 38 ++++++++ src/Entity/Configuration.php | 29 +++++- src/Entity/PlatformOffer.php | 19 ++++ src/Entity/Product.php | 2 +- src/Entity/User.php | 90 +++++++++++++++++++ src/Enum/OfferType.php | 4 +- src/Form/Type/Admin/ParametersFormType.php | 4 + .../Type/Product/AbstractProductFormType.php | 37 -------- src/Form/Type/Product/ObjectFormType.php | 52 +++++++++++ src/Form/Type/Product/ServiceFormType.php | 49 ++++++++++ .../Command/Admin/ParametersFormCommand.php | 4 +- src/Payment/PayumManager.php | 51 +++++++---- src/Repository/GroupRepository.php | 19 ++-- src/Repository/PlatformOfferRepository.php | 5 ++ src/Security/Voter/MembershipPaidVoter.php | 34 +++++++ templates/admin/parameters.html.twig | 2 + .../components/group/_modal_offers.html.twig | 2 +- .../group/show/_logged_with_link.html.twig | 3 +- .../group/show/_logged_without_link.html.twig | 3 +- templates/pages/redirect_to_payment.html.twig | 30 +++++++ .../Controller/Payment/PrepareActionTest.php | 2 +- .../Product/ServiceControllerTest.php | 3 - tests/TestReference.php | 2 +- .../Controller/Payment/DoneActionTest.php | 6 +- .../{ => Group}/DoneAction/messages.fr.xlf | 20 ++--- .../PlatformMembership/messages.fr.xlf | 34 +++++++ .../Product/ServiceFormType/messages.fr.xlf | 6 +- translations/parameters/admin.fr.xlf | 5 ++ translations/product/messages.fr.xlf | 5 ++ translations/templates/pages/messages.fr.xlf | 65 ++++++++++++++ 42 files changed, 705 insertions(+), 100 deletions(-) rename src/Controller/Payment/{ => Group}/DoneAction.php (96%) rename src/Controller/Payment/{ => Group}/GroupOfferTrait.php (93%) rename src/Controller/Payment/{ => Group}/PrepareAction.php (95%) create mode 100755 src/Controller/Payment/PlatformMembership/DoneAction.php create mode 100644 src/Controller/Payment/PlatformMembership/PrepareAction.php create mode 100644 src/Doctrine/Listener/MembershipPaidListener.php create mode 100644 src/Security/Voter/MembershipPaidVoter.php create mode 100644 templates/pages/redirect_to_payment.html.twig rename translations/app/Controller/Payment/{ => Group}/DoneAction/messages.fr.xlf (66%) create mode 100644 translations/app/Controller/Payment/PlatformMembership/messages.fr.xlf create mode 100644 translations/templates/pages/messages.fr.xlf diff --git a/config/packages/security.yaml b/config/packages/security.yaml index b75cfbc..dc04dba 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -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] diff --git a/fixtures/test/configuration.yaml b/fixtures/test/configuration.yaml index 97062e6..6200b9b 100644 --- a/fixtures/test/configuration.yaml +++ b/fixtures/test/configuration.yaml @@ -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: diff --git a/fixtures/test/platform_offer.yaml b/fixtures/test/platform_offer.yaml index 9a1d756..d37d126 100644 --- a/fixtures/test/platform_offer.yaml +++ b/fixtures/test/platform_offer.yaml @@ -1,11 +1,14 @@ App\Entity\PlatformOffer: - platform_offer_1: - id: + platform_offer_template (template): + configuration: '@features' + + platform_offer_1 (extends platform_offer_template): + id: 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 diff --git a/fixtures/test/user.yaml b/fixtures/test/user.yaml index 5fc4cc2..06c9a3d 100644 --- a/fixtures/test/user.yaml +++ b/fixtures/test/user.yaml @@ -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: diff --git a/src/Controller/Admin/PlatformOfferCrudController.php b/src/Controller/Admin/PlatformOfferCrudController.php index 0c49a27..5a599b5 100644 --- a/src/Controller/Admin/PlatformOfferCrudController.php +++ b/src/Controller/Admin/PlatformOfferCrudController.php @@ -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, ]; diff --git a/src/Controller/Payment/DoneAction.php b/src/Controller/Payment/Group/DoneAction.php similarity index 96% rename from src/Controller/Payment/DoneAction.php rename to src/Controller/Payment/Group/DoneAction.php index 13e2d5d..91dfeeb 100755 --- a/src/Controller/Payment/DoneAction.php +++ b/src/Controller/Payment/Group/DoneAction.php @@ -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())); } diff --git a/src/Controller/Payment/GroupOfferTrait.php b/src/Controller/Payment/Group/GroupOfferTrait.php similarity index 93% rename from src/Controller/Payment/GroupOfferTrait.php rename to src/Controller/Payment/Group/GroupOfferTrait.php index c8b623b..5bf260e 100644 --- a/src/Controller/Payment/GroupOfferTrait.php +++ b/src/Controller/Payment/Group/GroupOfferTrait.php @@ -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; diff --git a/src/Controller/Payment/PrepareAction.php b/src/Controller/Payment/Group/PrepareAction.php similarity index 95% rename from src/Controller/Payment/PrepareAction.php rename to src/Controller/Payment/Group/PrepareAction.php index af5e4bc..f72f83b 100755 --- a/src/Controller/Payment/PrepareAction.php +++ b/src/Controller/Payment/Group/PrepareAction.php @@ -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; diff --git a/src/Controller/Payment/PlatformMembership/DoneAction.php b/src/Controller/Payment/PlatformMembership/DoneAction.php new file mode 100755 index 0000000..d79776b --- /dev/null +++ b/src/Controller/Payment/PlatformMembership/DoneAction.php @@ -0,0 +1,81 @@ + 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'); + } +} diff --git a/src/Controller/Payment/PlatformMembership/PrepareAction.php b/src/Controller/Payment/PlatformMembership/PrepareAction.php new file mode 100644 index 0000000..332a808 --- /dev/null +++ b/src/Controller/Payment/PlatformMembership/PrepareAction.php @@ -0,0 +1,66 @@ + 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')); + } +} diff --git a/src/Controller/User/Product/ServiceController.php b/src/Controller/User/Product/ServiceController.php index dad5958..0316c35 100644 --- a/src/Controller/User/Product/ServiceController.php +++ b/src/Controller/User/Product/ServiceController.php @@ -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|null $images */ diff --git a/src/Doctrine/Behavior/AbstractOfferEntity.php b/src/Doctrine/Behavior/AbstractOfferEntity.php index 0355573..9580150 100644 --- a/src/Doctrine/Behavior/AbstractOfferEntity.php +++ b/src/Doctrine/Behavior/AbstractOfferEntity.php @@ -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.')'; } diff --git a/src/Doctrine/Listener/MembershipPaidListener.php b/src/Doctrine/Listener/MembershipPaidListener.php new file mode 100644 index 0000000..0564b13 --- /dev/null +++ b/src/Doctrine/Listener/MembershipPaidListener.php @@ -0,0 +1,38 @@ +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'))); + } + } +} diff --git a/src/Entity/Configuration.php b/src/Entity/Configuration.php index fa5d509..0b1d091 100644 --- a/src/Entity/Configuration.php +++ b/src/Entity/Configuration.php @@ -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 + */ + #[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 */ public function getGlobals(): array { - /** @var array $globals */ + /** @var array $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']; } /** diff --git a/src/Entity/PlatformOffer.php b/src/Entity/PlatformOffer.php index 7462085..b9ed102 100644 --- a/src/Entity/PlatformOffer.php +++ b/src/Entity/PlatformOffer.php @@ -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; + } } diff --git a/src/Entity/Product.php b/src/Entity/Product.php index 6f63b45..96a5369 100644 --- a/src/Entity/Product.php +++ b/src/Entity/Product.php @@ -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'), ], diff --git a/src/Entity/User.php b/src/Entity/User.php index 50140db..bf25ebf 100755 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -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 + */ + public function getUserGroupsConfirmedWithServices(): Collection + { + /** @var Collection $collection */ + $collection = $this->userGroups->filter(fn (UserGroup $userGroup) => !$userGroup->getMembership()->isInvited() && $userGroup->getGroup()->getServicesEnabled()); + + return $collection; + } + /** * @return array */ @@ -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 diff --git a/src/Enum/OfferType.php b/src/Enum/OfferType.php index 611b55f..222acdd 100644 --- a/src/Enum/OfferType.php +++ b/src/Enum/OfferType.php @@ -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'; diff --git a/src/Form/Type/Admin/ParametersFormType.php b/src/Form/Type/Admin/ParametersFormType.php index 034ae0c..173cc84 100755 --- a/src/Form/Type/Admin/ParametersFormType.php +++ b/src/Form/Type/Admin/ParametersFormType.php @@ -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' => [ diff --git a/src/Form/Type/Product/AbstractProductFormType.php b/src/Form/Type/Product/AbstractProductFormType.php index 52dd3b0..1a19598 100644 --- a/src/Form/Type/Product/AbstractProductFormType.php +++ b/src/Form/Type/Product/AbstractProductFormType.php @@ -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 */ diff --git a/src/Form/Type/Product/ObjectFormType.php b/src/Form/Type/Product/ObjectFormType.php index db464e2..1cda787 100644 --- a/src/Form/Type/Product/ObjectFormType.php +++ b/src/Form/Type/Product/ObjectFormType.php @@ -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 diff --git a/src/Form/Type/Product/ServiceFormType.php b/src/Form/Type/Product/ServiceFormType.php index e61dbf9..7597a26 100644 --- a/src/Form/Type/Product/ServiceFormType.php +++ b/src/Form/Type/Product/ServiceFormType.php @@ -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 diff --git a/src/Message/Command/Admin/ParametersFormCommand.php b/src/Message/Command/Admin/ParametersFormCommand.php index 73ba808..d34f2cf 100755 --- a/src/Message/Command/Admin/ParametersFormCommand.php +++ b/src/Message/Command/Admin/ParametersFormCommand.php @@ -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 } diff --git a/src/Payment/PayumManager.php b/src/Payment/PayumManager.php index a7b4bbc..0f0736a 100644 --- a/src/Payment/PayumManager.php +++ b/src/Payment/PayumManager.php @@ -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 */ - private function getGatewayDetails(GroupOffer $groupOffer): array + private function getGatewayDetails(GroupOffer|PlatformOffer $offer): array { - 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(), - ], - ]; + 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) $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(), + ], + ]; + } } /** diff --git a/src/Repository/GroupRepository.php b/src/Repository/GroupRepository.php index a1c1a5f..acd3b48 100644 --- a/src/Repository/GroupRepository.php +++ b/src/Repository/GroupRepository.php @@ -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); + } } diff --git a/src/Repository/PlatformOfferRepository.php b/src/Repository/PlatformOfferRepository.php index 8ae77f5..af8b4ce 100644 --- a/src/Repository/PlatformOfferRepository.php +++ b/src/Repository/PlatformOfferRepository.php @@ -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]); + } } diff --git a/src/Security/Voter/MembershipPaidVoter.php b/src/Security/Voter/MembershipPaidVoter.php new file mode 100644 index 0000000..f97d156 --- /dev/null +++ b/src/Security/Voter/MembershipPaidVoter.php @@ -0,0 +1,34 @@ +getUser(); + + if (!$user instanceof User) { + return false; + } + + return !$this->configurationRepository->getInstanceConfigurationOrCreate()->getPaidMembership() || $user->isMembershipPaid(); + } +} diff --git a/templates/admin/parameters.html.twig b/templates/admin/parameters.html.twig index d6354ca..b2fc959 100644 --- a/templates/admin/parameters.html.twig +++ b/templates/admin/parameters.html.twig @@ -17,6 +17,8 @@

{{ 'parameters.global.h2'|trans }}


+ {{ form_row(form.globalName) }} +

{{ 'parameters.services.h3'|trans }}

{{ form_widget(form.globalServicesEnabled) }} diff --git a/templates/components/group/_modal_offers.html.twig b/templates/components/group/_modal_offers.html.twig index 3d3f2b7..31291b7 100644 --- a/templates/components/group/_modal_offers.html.twig +++ b/templates/components/group/_modal_offers.html.twig @@ -27,7 +27,7 @@
-
+ diff --git a/templates/pages/group/show/_logged_with_link.html.twig b/templates/pages/group/show/_logged_with_link.html.twig index a6eefbc..eb8dc81 100644 --- a/templates/pages/group/show/_logged_with_link.html.twig +++ b/templates/pages/group/show/_logged_with_link.html.twig @@ -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 #} diff --git a/templates/pages/group/show/_logged_without_link.html.twig b/templates/pages/group/show/_logged_without_link.html.twig index 8a6922e..c5ade60 100644 --- a/templates/pages/group/show/_logged_without_link.html.twig +++ b/templates/pages/group/show/_logged_without_link.html.twig @@ -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. #} diff --git a/templates/pages/redirect_to_payment.html.twig b/templates/pages/redirect_to_payment.html.twig new file mode 100644 index 0000000..1d0eeb4 --- /dev/null +++ b/templates/pages/redirect_to_payment.html.twig @@ -0,0 +1,30 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block body %} +
+ {% 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 + } %} +
+ +
+ + {% include 'components/group/_modal_offers.html.twig' with { + offers: offers, + prepare_path: 'app_platform_payment_prepare' + } %} +
+{% endblock %} diff --git a/tests/Functional/Controller/Payment/PrepareActionTest.php b/tests/Functional/Controller/Payment/PrepareActionTest.php index a71cb74..d0c139c 100644 --- a/tests/Functional/Controller/Payment/PrepareActionTest.php +++ b/tests/Functional/Controller/Payment/PrepareActionTest.php @@ -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; diff --git a/tests/Functional/Controller/Product/ServiceControllerTest.php b/tests/Functional/Controller/Product/ServiceControllerTest.php index 82e1148..efa7d39 100644 --- a/tests/Functional/Controller/Product/ServiceControllerTest.php +++ b/tests/Functional/Controller/Product/ServiceControllerTest.php @@ -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(); diff --git a/tests/TestReference.php b/tests/TestReference.php index 2bb1347..c590ab2 100644 --- a/tests/TestReference.php +++ b/tests/TestReference.php @@ -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'; } diff --git a/tests/Unit/Controller/Payment/DoneActionTest.php b/tests/Unit/Controller/Payment/DoneActionTest.php index 1d18ce7..5f4b7c9 100644 --- a/tests/Unit/Controller/Payment/DoneActionTest.php +++ b/tests/Unit/Controller/Payment/DoneActionTest.php @@ -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()); } @@ -153,7 +154,8 @@ final class DoneActionTest extends TestCase $container->method('get')->willReturn($requesStack); $doneAction->setContainer($container); - $this->expectException(\Error::class); // or more mock are needed. To cleanup later + $this->expectException(\Error::class); // or more mock are needed. To clean up later + $doneAction->__invoke(new Request(), TestReference::UUID_404, $this->getUser()); } } diff --git a/translations/app/Controller/Payment/DoneAction/messages.fr.xlf b/translations/app/Controller/Payment/Group/DoneAction/messages.fr.xlf similarity index 66% rename from translations/app/Controller/Payment/DoneAction/messages.fr.xlf rename to translations/app/Controller/Payment/Group/DoneAction/messages.fr.xlf index c0b0d1e..4d87247 100644 --- a/translations/app/Controller/Payment/DoneAction/messages.fr.xlf +++ b/translations/app/Controller/Payment/Group/DoneAction/messages.fr.xlf @@ -5,28 +5,28 @@ - - app.controller.payment.done_action.flash.success + + app.controller.payment.group.done_action.flash.success Paiement réussi. Vous êtes désormais membre du groupe %group%. - - app.controller.payment.done_action.status.new + + app.controller.payment.group.done_action.status.new Le paiement est en cours mais n'a pas encore abouti. - - app.controller.payment.done_action.status.failed + + app.controller.payment.group.done_action.status.failed Le paiement a échoué, vous n'avez pas été prélevé·e. - - app.controller.payment.done_action.status.canceled + + app.controller.payment.group.done_action.status.canceled Le paiement a été annulé, vous n'avez pas été prélevé·e. - - app.controller.payment.done_action.status.expired + + app.controller.payment.group.done_action.status.expired Le paiement a expiré, veuillez réessayer. diff --git a/translations/app/Controller/Payment/PlatformMembership/messages.fr.xlf b/translations/app/Controller/Payment/PlatformMembership/messages.fr.xlf new file mode 100644 index 0000000..da777de --- /dev/null +++ b/translations/app/Controller/Payment/PlatformMembership/messages.fr.xlf @@ -0,0 +1,34 @@ + + + +
+ +
+ + + app.controller.payment.platform_membership.done_action.flash.success + Paiement réussi. Vous êtes désormais membre de la plateforme %platform%. + + + + app.controller.payment.platform_membership.done_action.status.new + Le paiement est en cours mais n'a pas encore abouti. + + + + app.controller.payment.platform_membership.done_action.status.failed + Le paiement a échoué, vous n'avez pas été prélevé·e. + + + + app.controller.payment.platform_membership.done_action.status.canceled + Le paiement a été annulé, vous n'avez pas été prélevé·e. + + + + app.controller.payment.platform_membership.done_action.status.expired + Le paiement a expiré, veuillez réessayer. + + +
+
diff --git a/translations/app/Form/Type/Product/ServiceFormType/messages.fr.xlf b/translations/app/Form/Type/Product/ServiceFormType/messages.fr.xlf index 9c405a3..1026009 100644 --- a/translations/app/Form/Type/Product/ServiceFormType/messages.fr.xlf +++ b/translations/app/Form/Type/Product/ServiceFormType/messages.fr.xlf @@ -5,11 +5,15 @@ - + app.form.type.product.service_form_type.form.groups Visible dans mes groupes : + + app.form.type.product.service_form_type.form.no_groups + Vous n'avez aucun groupe avec la disponibilité des services activée + diff --git a/translations/parameters/admin.fr.xlf b/translations/parameters/admin.fr.xlf index d7e5243..9066ee5 100644 --- a/translations/parameters/admin.fr.xlf +++ b/translations/parameters/admin.fr.xlf @@ -7,6 +7,11 @@ + + parameter.global_name + Nom de la plateforme + + parameter.services Services activés diff --git a/translations/product/messages.fr.xlf b/translations/product/messages.fr.xlf index a165e60..5615560 100644 --- a/translations/product/messages.fr.xlf +++ b/translations/product/messages.fr.xlf @@ -105,6 +105,11 @@ Uniquement mes groupes + + product.service.form.visibility + Uniquement mes groupes + + new_product.available diff --git a/translations/templates/pages/messages.fr.xlf b/translations/templates/pages/messages.fr.xlf new file mode 100644 index 0000000..133f00d --- /dev/null +++ b/translations/templates/pages/messages.fr.xlf @@ -0,0 +1,65 @@ + + + +
+ +
+ + + + templates.pages.redirect_to_payment.type.yearly + Adhésion annuel + + + + templates.pages.redirect_to_payment.type.monthly + Adhésion mensuel + + + + templates.pages.redirect_to_payment.type.oneshot + Unique + + + + templates.pages.redirect_to_payment.choose_offer + Rejoindre la plateforme + + + + templates.pages.redirect_to_payment.close + Annuler + + + + templates.pages.redirect_to_payment.membership + Adhésion: + + + + templates.pages.redirect_to_payment.membership.start + À partir de + + + + templates.pages.redirect_to_payment.payment_prepare.form.submit + Accéder au paiement + + + + templates.pages.redirect_payment.title + Bienvenue + + + + templates.pages.redirect_to_payment.text + Pour avoir accès aux prêts/emprunts sur notre plateforme, il faut être adhérent. + + + + templates.pages.redirect_to_payment.button + Payer mon adhésion + + +
+
From 7fd21b6dda33c97efee7c8d100c444cbead3585e Mon Sep 17 00:00:00 2001 From: Sarahshr <51380592+Sarahshr@users.noreply.github.com> Date: Wed, 31 Jul 2024 08:51:07 +0200 Subject: [PATCH 06/28] Fix/issues 679 and 680 (#718) * fix: increases max height of product description * fix: change link style for pages --- assets/styles/_product.scss | 2 +- assets/styles/_utilities.scss | 27 +++++---------------------- fixtures/test/page.yaml | 2 +- templates/cms/page.html.twig | 2 +- 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/assets/styles/_product.scss b/assets/styles/_product.scss index 528883b..8472e7c 100644 --- a/assets/styles/_product.scss +++ b/assets/styles/_product.scss @@ -45,7 +45,7 @@ } &.less { - max-height: 500px; + max-height: 1000px; transition: max-height .3s ease-in-out; } } diff --git a/assets/styles/_utilities.scss b/assets/styles/_utilities.scss index 9aab182..a9bde42 100644 --- a/assets/styles/_utilities.scss +++ b/assets/styles/_utilities.scss @@ -109,30 +109,13 @@ } } -p > strong { - > a { +.page-content { + a { color: $blue-500;; - position: relative; text-decoration: none; - transition: .5s; - - &:after { - background-color: $blue-500; - content: ""; - height: 2px; - left: 0; - position: absolute; - top: 100%; - transform: scaleX(0); - transform-origin: right; - transition: transform .5s; - width: 100%; - } - - &:hover:after { - transform: scaleX(1); - transform-origin: left; - } + } + a:hover { + text-decoration: underline; } } diff --git a/fixtures/test/page.yaml b/fixtures/test/page.yaml index 54fd53f..117f596 100644 --- a/fixtures/test/page.yaml +++ b/fixtures/test/page.yaml @@ -68,7 +68,7 @@ App\Entity\Page:

 


Dépôt

-

Toutes les informations sur le projet et le code sont disponibles sur le dépôt public.

+

Toutes les informations sur le projet et le code sont disponibles sur le dépôt public.

 


Démo

diff --git a/templates/cms/page.html.twig b/templates/cms/page.html.twig index 9c3ef2d..3076cba 100644 --- a/templates/cms/page.html.twig +++ b/templates/cms/page.html.twig @@ -5,7 +5,7 @@ {% block title %}{{ page is not null ? page.name : brand }}{% endblock %} {% block body %} -
+
{% if page is not null %} {{ page.content|raw }} {% else %} From ac791cfae6f32a46de246ef0c5aa5f77f38ebc3c Mon Sep 17 00:00:00 2001 From: Sarahshr <51380592+Sarahshr@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:05:06 +0200 Subject: [PATCH 07/28] add missing info on redirect to payment page (#729) --- .../Payment/PlatformMembership/PrepareAction.php | 7 +++++-- src/Repository/PlatformOfferRepository.php | 16 ++++++++++++++++ templates/pages/redirect_to_payment.html.twig | 12 +++++++++++- translations/templates/pages/messages.fr.xlf | 2 +- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/Controller/Payment/PlatformMembership/PrepareAction.php b/src/Controller/Payment/PlatformMembership/PrepareAction.php index 332a808..6147de9 100644 --- a/src/Controller/Payment/PlatformMembership/PrepareAction.php +++ b/src/Controller/Payment/PlatformMembership/PrepareAction.php @@ -7,6 +7,7 @@ namespace App\Controller\Payment\PlatformMembership; use App\Entity\PlatformOffer; use App\Entity\User; use App\Payment\PayumManager; +use App\Repository\ConfigurationRepository; use App\Repository\PlatformOfferRepository; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -57,10 +58,12 @@ final class PrepareAction extends AbstractController 'en' => '/en/subcription', 'fr' => '/fr/abonnement', ], name: 'redirect_to_payment')] - public function redirectToPayment(PlatformOfferRepository $platformOfferRepository): Response + public function redirectToPayment(PlatformOfferRepository $platformOfferRepository, ConfigurationRepository $configurationRepository): Response { $offers = $platformOfferRepository->findBy(['active' => true]); + $lowOffer = $platformOfferRepository->findLowOffer(); + $platformName = $configurationRepository->getInstanceConfigurationOrCreate()->getPlatformName(); - return $this->render('pages/redirect_to_payment.html.twig', compact('offers')); + return $this->render('pages/redirect_to_payment.html.twig', compact('offers', 'lowOffer', 'platformName')); } } diff --git a/src/Repository/PlatformOfferRepository.php b/src/Repository/PlatformOfferRepository.php index af8b4ce..9f19744 100644 --- a/src/Repository/PlatformOfferRepository.php +++ b/src/Repository/PlatformOfferRepository.php @@ -6,6 +6,7 @@ namespace App\Repository; use App\Entity\PlatformOffer; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\NonUniqueResultException; use Doctrine\Persistence\ManagerRegistry; /** @@ -37,4 +38,19 @@ class PlatformOfferRepository extends ServiceEntityRepository { return $this->findOneBy(['id' => $id, 'active' => true]); } + + /** + * @throws NonUniqueResultException + */ + public function findLowOffer(): ?PlatformOffer + { + /** @var ?PlatformOffer */ + return $this + ->createQueryBuilder('o') + ->andWhere('o.active = true') + ->orderBy('o.price', 'ASC') + ->getQuery() + ->setMaxResults(1) + ->getOneOrNullResult(); + } } diff --git a/templates/pages/redirect_to_payment.html.twig b/templates/pages/redirect_to_payment.html.twig index 1d0eeb4..7ac3c7a 100644 --- a/templates/pages/redirect_to_payment.html.twig +++ b/templates/pages/redirect_to_payment.html.twig @@ -5,11 +5,21 @@ {% block body %}
{% include 'components/layout/_title_3.html.twig' with { - name: (i18n_prefix ~ '.title')|trans + name: (i18n_prefix ~ '.title')|trans({'%name%': platformName}) } %} {% include 'components/layout/_text.html.twig' with { text: (i18n_prefix ~ '.text')|trans } %} + +
+

+ {{ (i18n_prefix ~ '.membership')|trans }} + + {{ (i18n_prefix ~ '.membership.start')|trans }} + {{ lowOffer.actualPrice|format_currency(lowOffer.currency) }} + +

+