Merge pull request #2 from Tipimi-fr/chore/sync-source-rebased

Chore/sync source rebased
This commit is contained in:
Slim Amamou 2024-10-28 09:32:09 +01:00 committed by GitHub
commit b1868fc1f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
267 changed files with 8398 additions and 3592 deletions

5
.env
View file

@ -74,6 +74,7 @@ MAILER_DSN=null://null
# This is main the DNS that is used by the notifier component # This is main the DNS that is used by the notifier component
# the other below are just example for two different third party vendors # the other below are just example for two different third party vendors
#SMS_DSN=twilio://SID:TOKEN@default?from=FROM #SMS_DSN=twilio://SID:TOKEN@default?from=FROM
#SMS_DSN=brevo://API_KEY@default?sender=SENDER
SMS_DSN=null://null SMS_DSN=null://null
###< symfony/notifier ### ###< symfony/notifier ###
@ -106,3 +107,7 @@ STORAGE_USE_PATH_STYLE_ENDPOINT=true
STORAGE_KEY=app STORAGE_KEY=app
STORAGE_SECRET=!ChangeMe! STORAGE_SECRET=!ChangeMe!
###< league/flysystem-bundle ### ###< league/flysystem-bundle ###
###> symfony/brevo-notifier ###
# BREVO_DSN=brevo://API_KEY@default?sender=SENDER
###< symfony/brevo-notifier ###

View file

@ -51,6 +51,6 @@ jobs:
- name: Twig Linter - name: Twig Linter
run: docker compose exec -T php ./vendor/bin/twigcs templates/ --exclude vendor run: docker compose exec -T php ./vendor/bin/twigcs templates/ --exclude vendor
- name: Install eslint - 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 - name: Run eslint on javascript files
run: docker run --rm -w "/usr/app" -v "${PWD}":/usr/app gmolaire/yarn yarn lint run: docker run --rm -w "/usr/app" -v "${PWD}":/usr/app gmolaire/yarn yarn lint

View file

@ -124,6 +124,7 @@ jobs:
--set=ingress.tls[0].secretName=${{ needs.meta.outputs.release_name }}-tls \ --set=ingress.tls[0].secretName=${{ needs.meta.outputs.release_name }}-tls \
--set=ingress.tls[0].hosts[0]=${{ vars.DOMAIN }} \ --set=ingress.tls[0].hosts[0]=${{ vars.DOMAIN }} \
--set=postgresql.url="${{ secrets.database-url }}" \ --set=postgresql.url="${{ secrets.database-url }}" \
--set=postgresql.enabled='${{ github.event_name == 'pull_request' }}' \
--set=payum.apikey="${{ secrets.payum-apikey }}" \ --set=payum.apikey="${{ secrets.payum-apikey }}" \
--set=mailer.dsn="${{ secrets.mailer-dsn }}" \ --set=mailer.dsn="${{ secrets.mailer-dsn }}" \
--set=sms.dsn="${{ secrets.sms-dsn }}" \ --set=sms.dsn="${{ secrets.sms-dsn }}" \

View file

@ -23,7 +23,13 @@ Le code est disponible sous licence AGPL (Affero General Public License). Voir l
## Interface dadministration ## Interface dadministration
La plateforme dispose dune interface dadministration, utilisant EasyAdmin, accessible aux utilisateur·rice·s disposant du rôle “administrateur·rice”. La plateforme dispose dune interface dadministration, utilisant EasyAdmin, accessible aux utilisateur·rice·s disposant du rôle “administrateur·rice”.
Les administrateur·rice·s peuvent être ajoutés ou supprimés via linterface dadministration. Les administrateur·rice·s peuvent être ajoutés ou supprimés via linterface dadministration.
## Paiement de l'adhésion à la plateforme
Il est possible d'activer ou désactiver cette fonctionnalité dans l'interface dadministration.
Si le paiement d'une adhésion à la plateforme est activé, il faut renseigner les tarifs d'adhésion (unique, mensuel, annuel), et le paiement devra se faire par lutilisateur·rice après la création de son compte afin d'accéder aux fonctionnalités proposées (création d'objets, demandes d'emprunts etc.)
Si ladhésion a une date de fin, lors de lexpiration de celle-ci, lutilisateur·rice devra payer à nouveau son adhésion afin d'utiliser la plateforme.
## Utilisateur·rice·s ## Utilisateur·rice·s
Les utilisateur·rice·s peuvent sinscrire librement sur la plateforme avec une adresse e-mail valide. Les utilisateur·rice·s peuvent sinscrire librement sur la plateforme avec une adresse e-mail valide.
@ -80,7 +86,7 @@ Lutilisateur·rice peut également renseigner des périodes dindisponibili
La plateforme propose un traitement similaire aux objets et aux services. La plateforme propose un traitement similaire aux objets et aux services.
Les objets et services peuvent être consultés par les administrateur·rice·s dans linterface dadministration. Les objets et services peuvent être consultés par les administrateur·rice·s dans linterface dadministration.
Les services peuvent être activés ou désactivés pour l'instance, via l'espace d'administration. Les services peuvent être activés ou désactivés pour l'instance, et/ou par groupe et sous-groupe via l'espace d'administration.
### Catégories ### Catégories
@ -159,6 +165,8 @@ La plateforme gère lenvoi de notifications automatiques par e-mail et SMS po
| Nouveau gérant·e ou administrateur·rice de groupe | E-mail et SMS | | Nouveau gérant·e ou administrateur·rice de groupe | E-mail et SMS |
| Invitation dans un groupe | E-mail et SMS | | Invitation dans un groupe | E-mail et SMS |
| Rappel expiration dadhésion à 1 groupe J-7 J | E-mail et SMS | | Rappel expiration dadhésion à 1 groupe J-7 J | E-mail et SMS |
| Confirmation du paiement de l'adhésion à la platefome | E-mail |
| Expiration dadhésion à la platefome | E-mail |
Lenvoi demail devra être configuré pour chaque instance avec un service tiers dédié (envoi serveur, Mailgun, Sendinblue, Mailchimp, …) Lenvoi demail devra être configuré pour chaque instance avec un service tiers dédié (envoi serveur, Mailgun, Sendinblue, Mailchimp, …)
@ -187,12 +195,14 @@ Linterface dadministration permet de configurer les liens du menu. Le pied
## Fonctionnalités configurables ## Fonctionnalités configurables
Les options suivantes peuvent être configurées manuellement dans lespace dadministration : Les options suivantes peuvent être configurées manuellement dans lespace dadministration :
- Activation des services - Activation des services
- Activation du paiement de l'adhésion et configuration des tarifs
- Gestion des administrateur·rice·s de linstance - Gestion des administrateur·rice·s de linstance
- Expéditeur des notifications (e-mail, nom) - Expéditeur des notifications (e-mail, nom)
- Activation du lien de contact dans le menu - Activation du lien de contact dans le menu
- Activation des groupes - Activation des groupes
- Création de groupe : Ouverte à tous ou uniquement par les administrateur·rice·s de l'instance - Création de groupe : Ouverte à tous ou uniquement par les administrateur·rice·s de l'instance
- Création de groupe payante ou gratuite (NB : le paiement se fait hors de la plateforme) - Création de groupe payante ou gratuite (NB : le paiement se fait hors de la plateforme)
- Accès à la fonctionnalité Services pour un groupe ou sous-groupe
- Conversations demprunt visibles ou masquées dans lespace dadministration - Conversations demprunt visibles ou masquées dans lespace dadministration

8
assets/admin.js Normal file
View file

@ -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)

View file

@ -6,6 +6,7 @@
"fetch": "eager", "fetch": "eager",
"autoimport": { "autoimport": {
"tom-select/dist/css/tom-select.default.css": true, "tom-select/dist/css/tom-select.default.css": true,
"tom-select/dist/css/tom-select.bootstrap4.css": false,
"tom-select/dist/css/tom-select.bootstrap5.css": false "tom-select/dist/css/tom-select.bootstrap5.css": false
} }
} }

View file

@ -0,0 +1,241 @@
import { Controller} from '@hotwired/stimulus'
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = ['servicesEnabledField', 'parentField', 'idField', 'ownerField']
connect() {
const parentFields = document.querySelectorAll('[data-label="Parent"]')
const trs = Array.from(parentFields)
.map(e => e.firstElementChild)
.filter(e => e.tagName === 'A')
.map(e => e.closest('tr'))
for (const tr of trs) {
this.checkDisabledServices(tr)
}
}
parentFieldTargetConnected(element) {
// edit page
const parentGroupId = element.value
const servicesEnabledToggle = document.getElementById('Group_servicesEnabled')
this.checkGroupEditDisableServices(parentGroupId, servicesEnabledToggle)
element.addEventListener('change', () => {
const newParentGroupId = element.value
this.checkGroupEditDisableServices(newParentGroupId, servicesEnabledToggle)
})
// list page
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'))
}
const parentFields = document.querySelectorAll('[data-label="Parent"]')
const trs = Array.from(parentFields)
.map(e => e.firstElementChild)
.filter(e => e.tagName === 'A')
.map(e => e.closest('tr'))
for (const tr of trs) {
this.checkDisabledServices(tr)
}
})
}
ownerFieldTargetConnected() {
const initialUserId = this.ownerFieldTarget.value
const groupsField = document.getElementById('Product_groups')
this.replaceGroups(initialUserId, groupsField)
this.ownerFieldTarget.addEventListener('change', () => {
const userId = this.ownerFieldTarget.value
this.replaceGroups(userId, groupsField)
})
}
async checkDisabledServices(tr) {
const id = tr.getAttribute('data-id')
const url = `/api/groups/${id}`
const response = await fetch(url, {method: 'GET'})
if (!response.ok) {
return
}
const group = await response.json()
let disabledTr = false
for (const parentUrl of group.parentsRecursively) {
const parentResponse = await fetch(parentUrl, { method: 'GET' })
if (!response.ok) {
return
}
const parent = await parentResponse.json()
if (!parent.servicesEnabled) {
tr.querySelector('[data-admin-parentgroup-target="servicesEnabledField"]').disabled = true
disabledTr = true
break
}
}
if (!disabledTr) {
tr.querySelector('[data-admin-parentgroup-target="servicesEnabledField"]').disabled = false
}
}
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.childrenRecursively
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
toggle.disabled = true
}
})
}
async replaceGroups(userId, groupsField) {
const url = `/api/groups?user=${userId}&services_enabled=true&admin=0`
const response = await fetch(url, {method: 'GET'})
if (!response.ok) {
return
}
const data = await response.json()
const groups = data['hydra:member']
const parentDiv = groupsField.parentElement
const smallElement = parentDiv.querySelector('small')
const saveContinue = document.getElementsByClassName('action-saveAndContinue')[0]
const saveReturn = document.getElementsByClassName('action-saveAndReturn')[0]
const saveAdd = document.getElementsByClassName('action-saveAndAddAnother')[0]
groupsField.tomselect.clear()
groupsField.tomselect.clearOptions()
if (groups.length === 0) {
groupsField.tomselect.disable()
groupsField.tomselect.lock()
// show helper
if (null !== smallElement) {
smallElement.style.visibility = 'visible'
}
if (undefined !== saveContinue) {
saveContinue.disabled = true
}
if (undefined !== saveAdd) {
saveAdd.disabled = true
}
saveReturn.disabled = true
} else {
groups.map(group => {
groupsField.tomselect.addOption(new Option(group.name, group.id))
})
groupsField.tomselect.enable()
groupsField.tomselect.unlock()
// remove helper
if (null !== smallElement) {
smallElement.style.visibility = 'hidden'
}
if (undefined !== saveContinue) {
saveContinue.disabled = false
}
if (undefined !== saveAdd) {
saveAdd.disabled = false
}
saveReturn.disabled = false
}
}
async checkGroupEditDisableServices(parentGroupId, servicesEnabledToggle) {
const url = `/api/groups/${parentGroupId}`
const response = await fetch(url, {method: 'GET'})
if (!response.ok) {
return
}
const parentGroup = await response.json()
let parentAll = parentGroup.parentsRecursively
parentAll.push(parentGroup['@id'])
const parentDiv = servicesEnabledToggle.parentElement.parentElement
const smallElement = parentDiv.querySelector('small')
let disabled = false
for (const parentUrl of parentAll) {
const parentResponse = await fetch(parentUrl, { method: 'GET' })
if (!response.ok) {
return
}
const parent = await parentResponse.json()
if (!parent.servicesEnabled) {
servicesEnabledToggle.checked = false
servicesEnabledToggle.disabled = true
if (null !== smallElement) {
smallElement.style.visibility = 'visible'
}
disabled = true
break
}
}
if (!disabled) {
servicesEnabledToggle.disabled = false
if (null !== smallElement) {
smallElement.style.visibility = 'hidden'
}
}
}
}

View file

@ -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))
})
}
}

View file

@ -1,5 +1,4 @@
import { startStimulusApp } from '@symfony/stimulus-bridge' import { startStimulusApp } from '@symfony/stimulus-bridge'
import { Application } from '@hotwired/stimulus'
import PasswordVisibility from 'stimulus-password-visibility' import PasswordVisibility from 'stimulus-password-visibility'
import Carousel from 'stimulus-carousel' import Carousel from 'stimulus-carousel'
@ -15,7 +14,5 @@ export const app = startStimulusApp(require.context(
// register any custom, 3rd party controllers here // register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController); // app.register('some_controller_name', SomeImportedController);
const application = Application.start() app.register('carousel', Carousel)
application.register('carousel', Carousel) app.register('password-visibility', PasswordVisibility)
application.register('password-visibility', PasswordVisibility)

View file

@ -45,7 +45,7 @@
} }
&.less { &.less {
max-height: 500px; max-height: 1000px;
transition: max-height .3s ease-in-out; transition: max-height .3s ease-in-out;
} }
} }

View file

@ -109,30 +109,13 @@
} }
} }
p > strong { .page-content {
> a { a {
color: $blue-500;; color: $blue-500;;
position: relative;
text-decoration: none; text-decoration: none;
transition: .5s; }
a:hover {
&:after { text-decoration: underline;
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;
}
} }
} }

4
assets/styles/admin.css Normal file
View file

@ -0,0 +1,4 @@
/* Remove the extra arrow added by Tom Select */
.ts-wrapper.single .ts-control:after {
display: none;
}

3
assets/styles/app.css Normal file
View file

@ -0,0 +1,3 @@
body {
background-color: skyblue;
}

View file

@ -3,7 +3,7 @@
"type": "project", "type": "project",
"license": "MIT", "license": "MIT",
"description": "APES : Platforme d'échange de services et de biens", "description": "APES : Platforme d'échange de services et de biens",
"minimum-stability": "stable", "minimum-stability": "beta",
"prefer-stable": true, "prefer-stable": true,
"repositories": { "repositories": {
"coopTilleuls/payum-mollie": { "coopTilleuls/payum-mollie": {
@ -44,50 +44,53 @@
"odolbeau/phone-number-bundle": "^3.9", "odolbeau/phone-number-bundle": "^3.9",
"payum/offline": "^1.7", "payum/offline": "^1.7",
"payum/payum-bundle": "^2.5", "payum/payum-bundle": "^2.5",
"php-http/message": "^1.16",
"php-http/message-factory": "^1.1",
"phpdocumentor/reflection-docblock": "^5.3", "phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.11", "phpstan/phpdoc-parser": "^1.11",
"sensio/framework-extra-bundle": "^6.2",
"snc/redis-bundle": "^4.3", "snc/redis-bundle": "^4.3",
"stof/doctrine-extensions-bundle": "^1.7", "stof/doctrine-extensions-bundle": "^1.7",
"symfony/asset": "6.2.*", "symfony/asset": "6.4.*",
"symfony/cache": "6.2.*", "symfony/brevo-notifier": "6.4.*",
"symfony/clock": "6.2.*", "symfony/cache": "6.4.*",
"symfony/console": "6.2.*", "symfony/clock": "6.4.*",
"symfony/doctrine-messenger": "6.2.*", "symfony/console": "6.4.*",
"symfony/dotenv": "6.2.*", "symfony/doctrine-messenger": "6.4.*",
"symfony/expression-language": "6.2.*", "symfony/dotenv": "6.4.*",
"symfony/fake-sms-notifier": "6.2.*", "symfony/expression-language": "6.4.*",
"symfony/fake-sms-notifier": "6.4.*",
"symfony/flex": "^2", "symfony/flex": "^2",
"symfony/form": "6.2.*", "symfony/form": "6.4.*",
"symfony/framework-bundle": "6.2.*", "symfony/framework-bundle": "6.4.*",
"symfony/google-mailer": "6.2.*", "symfony/google-mailer": "6.4.*",
"symfony/html-sanitizer": "6.2.*", "symfony/html-sanitizer": "6.4.*",
"symfony/http-client": "6.2.*", "symfony/http-client": "6.4.*",
"symfony/mailer": "6.2.*", "symfony/mailer": "6.4.*",
"symfony/mercure-bundle": "^0.3.5", "symfony/mercure-bundle": "^0.3.5",
"symfony/messenger": "6.2.*", "symfony/messenger": "6.4.*",
"symfony/mime": "6.2.*", "symfony/mime": "6.4.*",
"symfony/monolog-bundle": "^3.8", "symfony/monolog-bundle": "^3.8",
"symfony/notifier": "6.2.*", "symfony/notifier": "6.4.*",
"symfony/ovh-cloud-notifier": "6.2.*", "symfony/ovh-cloud-notifier": "6.4.*",
"symfony/property-access": "6.2.*", "symfony/property-access": "6.4.*",
"symfony/property-info": "6.2.*", "symfony/property-info": "6.4.*",
"symfony/proxy-manager-bridge": "6.2.*", "symfony/proxy-manager-bridge": "6.4.*",
"symfony/rate-limiter": "6.2.*", "symfony/rate-limiter": "6.4.*",
"symfony/requirements-checker": "^2.0", "symfony/requirements-checker": "^2.0",
"symfony/runtime": "6.2.*", "symfony/runtime": "6.4.*",
"symfony/security-bundle": "6.2.*", "symfony/security-bundle": "6.4.*",
"symfony/serializer": "6.2.*", "symfony/serializer": "6.4.*",
"symfony/stimulus-bundle": "^2.14",
"symfony/translation-contracts": "^3.2", "symfony/translation-contracts": "^3.2",
"symfony/twig-bridge": "6.2.*", "symfony/twig-bridge": "6.4.*",
"symfony/twig-bundle": "6.2.*", "symfony/twig-bundle": "6.4.*",
"symfony/twilio-notifier": "6.2.*", "symfony/twilio-notifier": "6.4.*",
"symfony/uid": "6.2.*", "symfony/uid": "6.4.*",
"symfony/ux-autocomplete": "^2.7", "symfony/ux-autocomplete": "^2.7",
"symfony/validator": "6.2.*", "symfony/validator": "6.4.*",
"symfony/webpack-encore-bundle": "^1.16", "symfony/webpack-encore-bundle": "^1.16",
"symfony/workflow": "6.2.*", "symfony/workflow": "6.4.*",
"symfony/yaml": "6.2.*", "symfony/yaml": "6.4.*",
"twig/cssinliner-extra": "^3.4", "twig/cssinliner-extra": "^3.4",
"twig/extra-bundle": "^3.4", "twig/extra-bundle": "^3.4",
"twig/inky-extra": "^3.4", "twig/inky-extra": "^3.4",
@ -151,7 +154,7 @@
"extra": { "extra": {
"symfony": { "symfony": {
"allow-contrib": false, "allow-contrib": false,
"require": "6.2.*", "require": "6.4.*",
"docker": true "docker": true
} }
}, },
@ -168,13 +171,13 @@
"phpunit/phpunit": "^9.5", "phpunit/phpunit": "^9.5",
"rector/rector": "^0.14.5", "rector/rector": "^0.14.5",
"staabm/annotate-pull-request-from-checkstyle": "^1.8", "staabm/annotate-pull-request-from-checkstyle": "^1.8",
"symfony/browser-kit": "6.2.*", "symfony/browser-kit": "6.4.*",
"symfony/css-selector": "6.2.*", "symfony/css-selector": "6.4.*",
"symfony/debug-bundle": "6.2.*", "symfony/debug-bundle": "6.4.*",
"symfony/maker-bundle": "^1.47", "symfony/maker-bundle": "^1.47",
"symfony/panther": "^2.0", "symfony/panther": "^2.0",
"symfony/phpunit-bridge": "^6.1", "symfony/phpunit-bridge": "^6.1",
"symfony/web-profiler-bundle": "6.2.*", "symfony/web-profiler-bundle": "6.4.*",
"zenstruck/messenger-test": "^1.5" "zenstruck/messenger-test": "^1.5"
} }
} }

6771
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1 @@
# Note regarding composer.json # Note regarding composer.json
## Meilisearch
meilisearch/meilisearch-php
Is locked to `v0.26.0` to prevent this error:
Executing script cache:clear [KO]
[KO]
Script cache:clear returned with error code 1
!!
!! In DebugClassLoader.php line 327:
!!
!! Case mismatch between loaded and declared class names: "MeiliSearch\Client"
!! vs "Meilisearch\Client".
!!
!!
!!
Script @auto-scripts was called via post-update-cmd
I have opened a ticket https://github.com/meilisearch/meilisearch-php/issues/452.

View file

@ -28,9 +28,9 @@ return [
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
League\FlysystemBundle\FlysystemBundle::class => ['all' => true], League\FlysystemBundle\FlysystemBundle::class => ['all' => true],
Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true], Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
Symfony\UX\Autocomplete\AutocompleteBundle::class => ['all' => true], Symfony\UX\Autocomplete\AutocompleteBundle::class => ['all' => true],
Payum\Bundle\PayumBundle\PayumBundle::class => ['all' => true], Payum\Bundle\PayumBundle\PayumBundle::class => ['all' => true],
FOS\CKEditorBundle\FOSCKEditorBundle::class => ['all' => true], FOS\CKEditorBundle\FOSCKEditorBundle::class => ['all' => true],
Misd\PhoneNumberBundle\MisdPhoneNumberBundle::class => ['all' => true], Misd\PhoneNumberBundle\MisdPhoneNumberBundle::class => ['all' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
]; ];

View file

@ -6,5 +6,7 @@ twig:
fos_ck_editor: fos_ck_editor:
configs: configs:
default:
versionCheck: false
main_config: main_config:
toolbar: full toolbar: full

View file

@ -47,8 +47,8 @@ security:
access_control: access_control:
- { path: ^/admin, roles: [ROLE_ADMIN, ROLE_GROUP_ADMIN] } - { path: ^/admin, roles: [ROLE_ADMIN, ROLE_GROUP_ADMIN] }
# to synchronize with MyAccountAction # to synchronize with MyAccountAction
- { path: ^/en/my-account/, roles: ROLE_USER } - { path: ^/en/my-account, roles: MEMBERSHIP_PAID }
- { path: ^/fr/mon-compte/, roles: ROLE_USER } - { path: ^/fr/mon-compte, roles: MEMBERSHIP_PAID }
role_hierarchy: role_hierarchy:
ROLE_ADMIN: [ROLE_USER, ROLE_ALLOWED_TO_SWITCH, ROLE_GROUP_ADMIN] ROLE_ADMIN: [ROLE_USER, ROLE_ALLOWED_TO_SWITCH, ROLE_GROUP_ADMIN]

View file

@ -1,7 +0,0 @@
# https://stackoverflow.com/q/69809320/633864
sensio_framework_extra:
router:
annotations: false
request:
converters: false
auto_convert: false

View file

@ -1,5 +1,3 @@
version: "3.4"
# Development environment override # Development environment override
services: services:
# https://localhost/_profiler/phpinfo # https://localhost/_profiler/phpinfo
@ -30,8 +28,13 @@ services:
maildev: maildev:
image: maildev/maildev image: maildev/maildev
ports: ports:
- 1080:1080 - "1080:1080"
- 1025:1025 - "1025:1025"
healthcheck:
test: ["CMD-SHELL", "wget -O - http://0.0.0.0:1080/healthz || exit 1"]
interval: 5s
timeout: 5s
retries: 5
storage: storage:
image: minio/minio image: minio/minio

View file

@ -6,18 +6,8 @@ services:
ports: ports:
- "1081:1080" - "1081:1080"
- "1026:1025" - "1026:1025"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:1080"]
interval: 10s
timeout: 5s
retries: 5
adminer: adminer:
image: adminer image: adminer
ports: ports:
- "8989:8080" - "8989:8080"
healthcheck:
test: ["CMD-SHELL", "curl --silent --fail http://127.0.0.1:8080/ || exit 1"]
interval: 10s
timeout: 30s
retries: 10

View file

@ -1,5 +1,3 @@
version: "3.4"
# Production environment override # Production environment override
services: services:
php: php:

View file

@ -1,5 +1,3 @@
version: "3.4"
services: services:
php: php:
build: build:
@ -89,7 +87,7 @@ services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
ports: ports:
- '6389:6379' - "6389:6379"
healthcheck: healthcheck:
test: ["CMD-SHELL", "redis-cli -h 127.0.0.1 ping | grep 'PONG' || exit 1"] test: ["CMD-SHELL", "redis-cli -h 127.0.0.1 ping | grep 'PONG' || exit 1"]
interval: 10s interval: 10s

View file

@ -21,16 +21,21 @@ Access `https://localhost` in your browser and accept the security risk.
You should have access now to: You should have access now to:
* Main project : https://localhost * The main frontend: https://localhost
* Meilisearch : http://localhost:7700/ * The Meilisearch UI: http://localhost:7700/
To access the dev tools, run= Note that you can also use the [online meilisearch-ui](https://meilisearch-ui.riccox.com).
Be careful it is not an official Meilisearch website, use only for dev data, do not
send cloud credentials.
To access the dev tools, run:
make start-dev make start-dev
You should have access now to: You should have access now to:
* Adminer : http://localhost:8989/?pgsql=database&username=app&db=app&ns=public&select=group * Adminer : http://localhost:8989/?pgsql=database&username=app&db=app&ns=public&select=group
* Mailcatcher : http://localhost:1081/
* Maildev : http://localhost:1080 * Maildev : http://localhost:1080
## Makefile ## Makefile

View file

@ -4,7 +4,6 @@ This page documents the settings that can be configured as environment varibles
for the production environment. for the production environment.
Defaults value are shown in the [.env](../.env) file. Defaults value are shown in the [.env](../.env) file.
## Application ## Application
| name | default value | | name | default value |
@ -47,7 +46,7 @@ For example, to use a standart SMTP server use:
Where mailer is the DSN of your SMTP server and 1025 the port to use. Where mailer is the DSN of your SMTP server and 1025 the port to use.
Yo use Gmail with a secret key use: To use Gmail with a secret key use:
MAILER_DSN=gmail://email@example.com:secretkey@default MAILER_DSN=gmail://email@example.com:secretkey@default
@ -58,14 +57,19 @@ Twilio, Sendgrid, Mailingblue...
SMS_DSN=null://null SMS_DSN=null://null
This is the main parameter to send DNS. If you leave `null://null`, nothing will This is the main parameter to send DNS. If you leave `null://null`, no SMS will
be send without errors. It can be useful when having issues with your SMS provider be sent.
and wanting to disable it temporarly. It may be useful when having issues with your SMS provider and wanting to disable it temporarily.
For example, to use a service like Twilio, the parameters should look like: Below are the supported value templates to use depending on your SMS provider.
SMS_DSN=twilio://AccountSID:AuthToken@default?from=%2BFROMNUMBER ### Brevo
SMS_DSN=brevo://API_KEY@default?sender=PHONE_NUMBER
### Twilio
SMS_DSN=twilio://AccountSID:AuthToken@default?from=PHONE_NUMBER
## Meilisearch ## Meilisearch

View file

@ -4,8 +4,9 @@ App\Entity\Configuration:
features (extends configuration_template): features (extends configuration_template):
configuration: configuration:
services: global:
servicesEnabled: true globalServicesEnabled: true
globalPaidMembership: false
notificationsSender: notificationsSender:
notificationsSenderEmail: info@example.com notificationsSenderEmail: info@example.com
notificationsSenderName: Contact notificationsSenderName: Contact

View file

@ -4,8 +4,10 @@ App\Entity\Configuration:
features (extends configuration_template): features (extends configuration_template):
configuration: configuration:
services: global:
servicesEnabled: true globalName: Echanges de biens et de services
globalServicesEnabled: true
globalPaidMembership: false
notificationsSender: notificationsSender:
notificationsSenderEmail: info@example.com notificationsSenderEmail: info@example.com
notificationsSenderName: Contact notificationsSenderName: Contact

View file

@ -4,11 +4,11 @@ App\Entity\GroupOffer:
# Templates # Templates
group_offer_group_1_template (template, extends group_offer_template): group_offer_group_1_template (template, extends group_offer_template):
group: '@group_1' 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_offer_group_7_template (template, extends group_offer_template):
group: '@group_7' group: '@group_7'
type: !php/enum App\Enum\Group\GroupOfferType::YEARLY type: !php/enum App\Enum\OfferType::YEARLY
# Group 1 # Group 1
group_offer_group_1_10 (extends group_offer_group_1_template): group_offer_group_1_10 (extends group_offer_group_1_template):

View file

@ -68,7 +68,7 @@ App\Entity\Page:
<p>&nbsp;</p> <p>&nbsp;</p>
<hr /> <hr />
<h2>D&eacute;p&ocirc;t</h2> <h2>D&eacute;p&ocirc;t</h2>
<p>Toutes les informations sur le projet et le code sont disponibles sur <strong><u><a href="https://github.com/Apes-HDF/EBS">le d&eacute;p&ocirc;t public</a></u></strong>.</p> <p>Toutes les informations sur le projet et le code sont disponibles sur <strong><a href="https://github.com/Apes-HDF/EBS">le d&eacute;p&ocirc;t public</a></strong>.</p>
<p>&nbsp;</p> <p>&nbsp;</p>
<hr /> <hr />
<h2>D&eacute;mo</h2> <h2>D&eacute;mo</h2>

View file

@ -0,0 +1,14 @@
App\Entity\PlatformOffer:
platform_offer_template (template):
configuration: '@features'
platform_offer_1 (extends platform_offer_template):
id: <uuid('016b2a27-1037-6d47-bcdc-ec5efbd723f2')>
name: Lorem ipsum
price: 2000
type: !php/enum App\Enum\OfferType::YEARLY
platform_offer_2 (extends platform_offer_template):
name: Aliquet risus
price: 200
type: !php/enum App\Enum\OfferType::MONTHLY

View file

@ -8,6 +8,7 @@ App\Entity\Product:
service (template, extends product): service (template, extends product):
type: !php/enum App\Enum\Product\ProductType::SERVICE type: !php/enum App\Enum\Product\ProductType::SERVICE
visibility: !php/enum App\Enum\Product\ProductVisibility::RESTRICTED
# Loic ————————————————————————————————————————————————————————————————————— # Loic —————————————————————————————————————————————————————————————————————
loic_object_1 (extends object): loic_object_1 (extends object):

View file

@ -35,6 +35,10 @@ App\Entity\User:
devAccount: true devAccount: true
address: '@address_loic' address: '@address_loic'
avatar: '7c732ddb-9c13-45eb-aea0-e614f2340e6d.jpg' avatar: '7c732ddb-9c13-45eb-aea0-e614f2340e6d.jpg'
membershipPaid: true
platformOffer: '@platform_offer_1'
startAt: <date_create_immutable('-1 year - 1 day')>
endAt: <date_create_immutable('-1 day')>
admin_kevin (extends admin_template): admin_kevin (extends admin_template):
id: <uuid('1ed69804-eeb9-6c32-990b-632c3a6846ba')> id: <uuid('1ed69804-eeb9-6c32-990b-632c3a6846ba')>
@ -42,6 +46,12 @@ App\Entity\User:
firstname: 'Kevin' firstname: 'Kevin'
lastname: 'Pirouet' lastname: 'Pirouet'
avatar: '7c732ddb-9c13-45eb-aea0-e614f2340e6d.jpg' avatar: '7c732ddb-9c13-45eb-aea0-e614f2340e6d.jpg'
type: !php/enum App\Enum\User\UserType::ADMIN
membershipPaid: true
platformOffer: '@platform_offer_1'
startAt: <date_create_immutable('-1 year + 7 day')>
endAt: <date_create_immutable('+7 day midnight')>
roles: [ !php/const App\Entity\User::ROLE_ADMIN, !php/const App\Entity\User::MEMBERSHIP_PAID]
admin_apes (extends admin_template): admin_apes (extends admin_template):
id: <uuid('1ed69804-eeb9-6e6c-bce0-632c3a6846ba')> id: <uuid('1ed69804-eeb9-6e6c-bce0-632c3a6846ba')>

View file

@ -7,6 +7,7 @@ metadata:
{{- include "plateforme-ebs.labels" . | nindent 4 }} {{- include "plateforme-ebs.labels" . | nindent 4 }}
spec: spec:
schedule: '0 0 * * *' schedule: '0 0 * * *'
timeZone: "Europe/Paris"
jobTemplate: jobTemplate:
metadata: metadata:
annotations: annotations:
@ -23,19 +24,20 @@ spec:
serviceAccountName: {{ include "plateforme-ebs.serviceAccountName" . }} serviceAccountName: {{ include "plateforme-ebs.serviceAccountName" . }}
restartPolicy: Never restartPolicy: Never
containers: 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 }}" image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.php.image.pullPolicy }} imagePullPolicy: {{ .Values.php.image.pullPolicy }}
command: ['/bin/sh', '-c'] command: ['/bin/sh', '-c']
args: [' args: ['
bin/console doctrine:database:drop --if-exists --force; set -ex;
bin/console doctrine:database:create --if-not-exists; bin/console doctrine:database:drop --if-exists --force;
bin/console doctrine:schema:create; bin/console doctrine:database:create --if-not-exists;
bin/console doctrine:schema:validate; bin/console doctrine:schema:create;
bin/console messenger:setup-transports; bin/console doctrine:schema:validate;
bin/console hautelook:fixtures:load --no-interaction -vv --no-bundles; bin/console messenger:setup-transports;
bin/console app:index-products; bin/console hautelook:fixtures:load --no-interaction -vv --no-bundles;
'] bin/console app:index-products;'
]
env: env:
- name: API_ENTRYPOINT_HOST - name: API_ENTRYPOINT_HOST
valueFrom: valueFrom:
@ -198,4 +200,4 @@ spec:
periodSeconds: 3 periodSeconds: 3
resources: resources:
{{- toYaml .Values.resources.fixtures | nindent 16 }} {{- toYaml .Values.resources.fixtures | nindent 16 }}
{{- end }} {{- end }}

View file

@ -7,6 +7,7 @@ metadata:
{{- include "plateforme-ebs.labels" . | nindent 4 }} {{- include "plateforme-ebs.labels" . | nindent 4 }}
spec: spec:
schedule: '6 1 * * *' schedule: '6 1 * * *'
timeZone: "Europe/Paris"
jobTemplate: jobTemplate:
metadata: metadata:
annotations: annotations:
@ -78,6 +79,23 @@ spec:
secretKeyRef: secretKeyRef:
name: {{ include "plateforme-ebs" . }} name: {{ include "plateforme-ebs" . }}
key: database-url key: database-url
- name: MAILER_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mailer-dsn
{{- if .Values.meilisearch.enabled }}
- name: MEILISEARCH_API_KEY
valueFrom:
secretKeyRef:
name: {{ printf "%s-%s" (include "meilisearch.fullname" .Subcharts.meilisearch ) "master-key" }}
key: MEILI_MASTER_KEY
- name: MEILISEARCH_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: meilisearch-url
{{- end }}
- name: MERCURE_URL - name: MERCURE_URL
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:
@ -93,6 +111,11 @@ spec:
secretKeyRef: secretKeyRef:
name: {{ include "plateforme-ebs" . }} name: {{ include "plateforme-ebs" . }}
key: mercure-jwt-secret key: mercure-jwt-secret
- name: SMS_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: sms-dsn
- name: STORAGE_BUCKET - name: STORAGE_BUCKET
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:

View file

@ -0,0 +1,171 @@
{{- if .Values.dailyCronjobs.enabled }}
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "plateforme-ebs" . }}-cronjob-end-p-membership
labels:
{{- include "plateforme-ebs.labels" . | nindent 4 }}
spec:
schedule: '17 1 * * *'
timeZone: "Europe/Paris"
jobTemplate:
metadata:
annotations:
rollme: {{ randAlphaNum 5 | quote }}
labels:
{{- include "plateforme-ebs.selectorLabels" . | nindent 8 }}
spec:
template:
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 12 }}
{{- end }}
serviceAccountName: {{ include "plateforme-ebs.serviceAccountName" . }}
restartPolicy: Never
containers:
- name: {{ .Chart.Name }}-cronjob-end-p-membership
image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.php.image.pullPolicy }}
command: ['/bin/sh', '-c']
args: ['bin/console app:end-platform-membership --env=prod']
env:
- name: API_ENTRYPOINT_HOST
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-host
- name: JWT_PASSPHRASE
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-jwt-passphrase
- name: JWT_PUBLIC_KEY
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-jwt-public-key
- name: JWT_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-jwt-secret-key
- name: TRUSTED_HOSTS
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-trusted-hosts
- name: TRUSTED_PROXIES
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-trusted-proxies
- name: APP_ENV
value: "prod"
- name: APP_DEBUG
value: "0"
- name: APP_SECRET
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-app-secret
- name: CORS_ALLOW_ORIGIN
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-cors-allow-origin
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: database-url
- name: MAILER_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mailer-dsn
{{- if .Values.meilisearch.enabled }}
- name: MEILISEARCH_API_KEY
valueFrom:
secretKeyRef:
name: {{ printf "%s-%s" (include "meilisearch.fullname" .Subcharts.meilisearch ) "master-key" }}
key: MEILI_MASTER_KEY
- name: MEILISEARCH_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: meilisearch-url
{{- end }}
- name: MERCURE_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mercure-url
- name: MERCURE_PUBLIC_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mercure-public-url
- name: MERCURE_JWT_SECRET
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mercure-jwt-secret
- name: SMS_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: sms-dsn
- name: STORAGE_BUCKET
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-bucket
- name: STORAGE_ENDPOINT
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-endpoint
- name: STORAGE_REGION
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-region
- name: STORAGE_USE_PATH_STYLE_ENDPOINT
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-use-path-style-endpoint
- name: STORAGE_KEY
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-key
- name: STORAGE_SECRET
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-secret
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "/bin/sleep 1; kill -QUIT 1"]
startupProbe:
exec:
command:
- docker-healthcheck
failureThreshold: 40
periodSeconds: 3
readinessProbe:
exec:
command:
- docker-healthcheck
periodSeconds: 3
livenessProbe:
exec:
command:
- docker-healthcheck
periodSeconds: 3
resources:
{{- toYaml .Values.resources.fixtures | nindent 16 }}
{{- end }}

View file

@ -7,6 +7,7 @@ metadata:
{{- include "plateforme-ebs.labels" . | nindent 4 }} {{- include "plateforme-ebs.labels" . | nindent 4 }}
spec: spec:
schedule: '12 2 * * *' schedule: '12 2 * * *'
timeZone: "Europe/Paris"
jobTemplate: jobTemplate:
metadata: metadata:
annotations: annotations:
@ -78,6 +79,23 @@ spec:
secretKeyRef: secretKeyRef:
name: {{ include "plateforme-ebs" . }} name: {{ include "plateforme-ebs" . }}
key: database-url key: database-url
- name: MAILER_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mailer-dsn
{{- if .Values.meilisearch.enabled }}
- name: MEILISEARCH_API_KEY
valueFrom:
secretKeyRef:
name: {{ printf "%s-%s" (include "meilisearch.fullname" .Subcharts.meilisearch ) "master-key" }}
key: MEILI_MASTER_KEY
- name: MEILISEARCH_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: meilisearch-url
{{- end }}
- name: MERCURE_URL - name: MERCURE_URL
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:
@ -93,6 +111,11 @@ spec:
secretKeyRef: secretKeyRef:
name: {{ include "plateforme-ebs" . }} name: {{ include "plateforme-ebs" . }}
key: mercure-jwt-secret key: mercure-jwt-secret
- name: SMS_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: sms-dsn
- name: STORAGE_BUCKET - name: STORAGE_BUCKET
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:

View file

@ -7,6 +7,7 @@ metadata:
{{- include "plateforme-ebs.labels" . | nindent 4 }} {{- include "plateforme-ebs.labels" . | nindent 4 }}
spec: spec:
schedule: '3 21 * * *' schedule: '3 21 * * *'
timeZone: "Europe/Paris"
jobTemplate: jobTemplate:
metadata: metadata:
annotations: annotations:
@ -78,6 +79,23 @@ spec:
secretKeyRef: secretKeyRef:
name: {{ include "plateforme-ebs" . }} name: {{ include "plateforme-ebs" . }}
key: database-url key: database-url
- name: MAILER_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mailer-dsn
{{- if .Values.meilisearch.enabled }}
- name: MEILISEARCH_API_KEY
valueFrom:
secretKeyRef:
name: {{ printf "%s-%s" (include "meilisearch.fullname" .Subcharts.meilisearch ) "master-key" }}
key: MEILI_MASTER_KEY
- name: MEILISEARCH_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: meilisearch-url
{{- end }}
- name: MERCURE_URL - name: MERCURE_URL
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:
@ -93,6 +111,11 @@ spec:
secretKeyRef: secretKeyRef:
name: {{ include "plateforme-ebs" . }} name: {{ include "plateforme-ebs" . }}
key: mercure-jwt-secret key: mercure-jwt-secret
- name: SMS_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: sms-dsn
- name: STORAGE_BUCKET - name: STORAGE_BUCKET
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:

View file

@ -0,0 +1,171 @@
{{- if .Values.dailyCronjobs.enabled }}
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "plateforme-ebs" . }}-cronjob-notify-pms-e-1
labels:
{{- include "plateforme-ebs.labels" . | nindent 4 }}
spec:
schedule: '20 2 * * *'
timeZone: "Europe/Paris"
jobTemplate:
metadata:
annotations:
rollme: {{ randAlphaNum 5 | quote }}
labels:
{{- include "plateforme-ebs.selectorLabels" . | nindent 8 }}
spec:
template:
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 12 }}
{{- end }}
serviceAccountName: {{ include "plateforme-ebs.serviceAccountName" . }}
restartPolicy: Never
containers:
- name: {{ .Chart.Name }}-cronjob-notify-pms-e-1
image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.php.image.pullPolicy }}
command: ['/bin/sh', '-c']
args: ['bin/console app:notify-platform-membership-expiration 1 --env=prod']
env:
- name: API_ENTRYPOINT_HOST
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-host
- name: JWT_PASSPHRASE
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-jwt-passphrase
- name: JWT_PUBLIC_KEY
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-jwt-public-key
- name: JWT_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-jwt-secret-key
- name: TRUSTED_HOSTS
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-trusted-hosts
- name: TRUSTED_PROXIES
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-trusted-proxies
- name: APP_ENV
value: "prod"
- name: APP_DEBUG
value: "0"
- name: APP_SECRET
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-app-secret
- name: CORS_ALLOW_ORIGIN
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-cors-allow-origin
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: database-url
- name: MAILER_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mailer-dsn
{{- if .Values.meilisearch.enabled }}
- name: MEILISEARCH_API_KEY
valueFrom:
secretKeyRef:
name: {{ printf "%s-%s" (include "meilisearch.fullname" .Subcharts.meilisearch ) "master-key" }}
key: MEILI_MASTER_KEY
- name: MEILISEARCH_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: meilisearch-url
{{- end }}
- name: MERCURE_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mercure-url
- name: MERCURE_PUBLIC_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mercure-public-url
- name: MERCURE_JWT_SECRET
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mercure-jwt-secret
- name: SMS_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: sms-dsn
- name: STORAGE_BUCKET
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-bucket
- name: STORAGE_ENDPOINT
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-endpoint
- name: STORAGE_REGION
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-region
- name: STORAGE_USE_PATH_STYLE_ENDPOINT
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-use-path-style-endpoint
- name: STORAGE_KEY
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-key
- name: STORAGE_SECRET
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-secret
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "/bin/sleep 1; kill -QUIT 1"]
startupProbe:
exec:
command:
- docker-healthcheck
failureThreshold: 40
periodSeconds: 3
readinessProbe:
exec:
command:
- docker-healthcheck
periodSeconds: 3
livenessProbe:
exec:
command:
- docker-healthcheck
periodSeconds: 3
resources:
{{- toYaml .Values.resources.fixtures | nindent 16 }}
{{- end }}

View file

@ -0,0 +1,171 @@
{{- if .Values.dailyCronjobs.enabled }}
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "plateforme-ebs" . }}-cronjob-notify-pms-e-7
labels:
{{- include "plateforme-ebs.labels" . | nindent 4 }}
spec:
schedule: '10 21 * * *'
timeZone: "Europe/Paris"
jobTemplate:
metadata:
annotations:
rollme: {{ randAlphaNum 5 | quote }}
labels:
{{- include "plateforme-ebs.selectorLabels" . | nindent 8 }}
spec:
template:
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 12 }}
{{- end }}
serviceAccountName: {{ include "plateforme-ebs.serviceAccountName" . }}
restartPolicy: Never
containers:
- name: {{ .Chart.Name }}-cronjob-notify-pms-e-7
image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.php.image.pullPolicy }}
command: ['/bin/sh', '-c']
args: ['bin/console app:notify-platform-membership-expiration 7 --env=prod']
env:
- name: API_ENTRYPOINT_HOST
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-host
- name: JWT_PASSPHRASE
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-jwt-passphrase
- name: JWT_PUBLIC_KEY
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-jwt-public-key
- name: JWT_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-jwt-secret-key
- name: TRUSTED_HOSTS
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-trusted-hosts
- name: TRUSTED_PROXIES
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-trusted-proxies
- name: APP_ENV
value: "prod"
- name: APP_DEBUG
value: "0"
- name: APP_SECRET
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-app-secret
- name: CORS_ALLOW_ORIGIN
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-cors-allow-origin
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: database-url
- name: MAILER_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mailer-dsn
{{- if .Values.meilisearch.enabled }}
- name: MEILISEARCH_API_KEY
valueFrom:
secretKeyRef:
name: {{ printf "%s-%s" (include "meilisearch.fullname" .Subcharts.meilisearch ) "master-key" }}
key: MEILI_MASTER_KEY
- name: MEILISEARCH_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: meilisearch-url
{{- end }}
- name: MERCURE_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mercure-url
- name: MERCURE_PUBLIC_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mercure-public-url
- name: MERCURE_JWT_SECRET
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mercure-jwt-secret
- name: SMS_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: sms-dsn
- name: STORAGE_BUCKET
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-bucket
- name: STORAGE_ENDPOINT
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-endpoint
- name: STORAGE_REGION
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-region
- name: STORAGE_USE_PATH_STYLE_ENDPOINT
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-use-path-style-endpoint
- name: STORAGE_KEY
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-key
- name: STORAGE_SECRET
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: php-storage-secret
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "/bin/sleep 1; kill -QUIT 1"]
startupProbe:
exec:
command:
- docker-healthcheck
failureThreshold: 40
periodSeconds: 3
readinessProbe:
exec:
command:
- docker-healthcheck
periodSeconds: 3
livenessProbe:
exec:
command:
- docker-healthcheck
periodSeconds: 3
resources:
{{- toYaml .Values.resources.fixtures | nindent 16 }}
{{- end }}

View file

@ -7,6 +7,7 @@ metadata:
{{- include "plateforme-ebs.labels" . | nindent 4 }} {{- include "plateforme-ebs.labels" . | nindent 4 }}
spec: spec:
schedule: '44 4 * * *' schedule: '44 4 * * *'
timeZone: "Europe/Paris"
jobTemplate: jobTemplate:
metadata: metadata:
annotations: annotations:
@ -78,6 +79,23 @@ spec:
secretKeyRef: secretKeyRef:
name: {{ include "plateforme-ebs" . }} name: {{ include "plateforme-ebs" . }}
key: database-url key: database-url
- name: MAILER_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mailer-dsn
{{- if .Values.meilisearch.enabled }}
- name: MEILISEARCH_API_KEY
valueFrom:
secretKeyRef:
name: {{ printf "%s-%s" (include "meilisearch.fullname" .Subcharts.meilisearch ) "master-key" }}
key: MEILI_MASTER_KEY
- name: MEILISEARCH_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: meilisearch-url
{{- end }}
- name: MERCURE_URL - name: MERCURE_URL
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:
@ -93,6 +111,11 @@ spec:
secretKeyRef: secretKeyRef:
name: {{ include "plateforme-ebs" . }} name: {{ include "plateforme-ebs" . }}
key: mercure-jwt-secret key: mercure-jwt-secret
- name: SMS_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: sms-dsn
- name: STORAGE_BUCKET - name: STORAGE_BUCKET
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:

View file

@ -7,6 +7,7 @@ metadata:
{{- include "plateforme-ebs.labels" . | nindent 4 }} {{- include "plateforme-ebs.labels" . | nindent 4 }}
spec: spec:
schedule: '2 4 * * *' schedule: '2 4 * * *'
timeZone: "Europe/Paris"
jobTemplate: jobTemplate:
metadata: metadata:
annotations: annotations:
@ -78,6 +79,23 @@ spec:
secretKeyRef: secretKeyRef:
name: {{ include "plateforme-ebs" . }} name: {{ include "plateforme-ebs" . }}
key: database-url key: database-url
- name: MAILER_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: mailer-dsn
{{- if .Values.meilisearch.enabled }}
- name: MEILISEARCH_API_KEY
valueFrom:
secretKeyRef:
name: {{ printf "%s-%s" (include "meilisearch.fullname" .Subcharts.meilisearch ) "master-key" }}
key: MEILI_MASTER_KEY
- name: MEILISEARCH_URL
valueFrom:
configMapKeyRef:
name: {{ include "plateforme-ebs" . }}
key: meilisearch-url
{{- end }}
- name: MERCURE_URL - name: MERCURE_URL
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:
@ -93,6 +111,11 @@ spec:
secretKeyRef: secretKeyRef:
name: {{ include "plateforme-ebs" . }} name: {{ include "plateforme-ebs" . }}
key: mercure-jwt-secret key: mercure-jwt-secret
- name: SMS_DSN
valueFrom:
secretKeyRef:
name: {{ include "plateforme-ebs" . }}
key: sms-dsn
- name: STORAGE_BUCKET - name: STORAGE_BUCKET
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:

View file

@ -33,18 +33,14 @@ spec:
command: ['/bin/sh', '-c'] command: ['/bin/sh', '-c']
args: [' args: ['
set -ex; 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: env:
- name: API_ENTRYPOINT_HOST - name: API_ENTRYPOINT_HOST
valueFrom: valueFrom:

View file

@ -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

View file

@ -29,4 +29,10 @@ redis:
size: "1Gi" size: "1Gi"
postgresql: postgresql:
enabled: false url: change_me
php:
fixtureJob:
enabled: true
fixtureCron:
enabled: true

View file

@ -33,10 +33,13 @@ php:
usePathStyleEndpoint: true usePathStyleEndpoint: true
publicKey: "" publicKey: ""
secret: "" secret: ""
fixtureJob:
enabled: false
fixtureCron: fixtureCron:
enabled: false enabled: false
fixtureJob:
enabled: false
dailyCronjobs:
enabled: true
maildev: maildev:
enabled: false enabled: false
@ -48,9 +51,6 @@ mailer:
sms: sms:
dsn: "null://null" dsn: "null://null"
dailyCronjobs:
enabled: true
consumer: consumer:
# We don't use async for now so consumer isn't needed # We don't use async for now so consumer isn't needed
enabled: false enabled: false
@ -80,6 +80,7 @@ postgresql:
username: "example" username: "example"
password: "!ChangeMe!" password: "!ChangeMe!"
database: "api" database: "api"
postgresPassword: "!ChangeMe!"
# Persistent Volume Storage configuration. # Persistent Volume Storage configuration.
# ref: https://kubernetes.io/docs/user-guide/persistent-volumes # ref: https://kubernetes.io/docs/user-guide/persistent-volumes
pullPolicy: IfNotPresent pullPolicy: IfNotPresent

View file

@ -69,7 +69,7 @@ final class EndMembershipCommand extends Command
/** @var UserGroup $userGroup */ /** @var UserGroup $userGroup */
$user = $userGroup->getUser(); $user = $userGroup->getUser();
$group = $userGroup->getGroup(); $group = $userGroup->getGroup();
$io->comment(sprintf(' > deleting membership for %s of %s (%s) (%s)', $io->comment(\sprintf(' > deleting membership for %s of %s (%s) (%s)',
$group->getName(), $group->getName(),
$user->getDisplayName(), $user->getDisplayName(),
$userGroup->getMembership()->value, $userGroup->getMembership()->value,
@ -88,7 +88,7 @@ final class EndMembershipCommand extends Command
++$count; ++$count;
} }
$io->note(sprintf(' > %d deletion(s) done.', $count)); $io->note(\sprintf(' > %d deletion(s) done.', $count));
$this->memoryReport($io); $this->memoryReport($io);
$this->done($io); $this->done($io);

View file

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Doctrine\Manager\UserManager;
use App\Entity\User;
use App\Enum\OfferType;
use App\Mailer\AppMailer;
use App\Mailer\Email\Command\EndPlatformMembershipMail;
use App\Notifier\SmsNotifier;
use App\Notifier\SmsNotifierTrait;
use App\Repository\ConfigurationRepository;
use App\Repository\UserRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsCommand(name: self::CMD, description: self::DESCRIPTION)]
class EndPlatformMembershipCommand extends Command
{
use CommandTrait;
use SmsNotifierTrait;
public const CMD = 'app:end-platform-membership';
public const DESCRIPTION = 'Check overdue platform membership and set user as unpaid';
public function __construct(
private readonly UserRepository $userRepository,
#[Autowire('%kernel.environment%')]
private readonly string $environment,
private readonly AppMailer $appMailer,
private readonly ConfigurationRepository $configurationRepository,
private readonly TranslatorInterface $translator,
private readonly SmsNotifier $notifier,
#[Autowire('%brand%')]
private readonly string $brand,
private readonly UserManager $userManager,
) {
parent::__construct();
}
protected function configure(): void
{
$this->configureCommand(self::DESCRIPTION);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$configuration = $this->configurationRepository->getInstanceConfigurationOrCreate();
if (!$configuration->getPaidMembership()) {
return Command::SUCCESS;
}
$platform = $configuration->getPlatformName();
$io->title(self::DESCRIPTION.' ('.$this->environment.' env)');
$this->memoryReport($io);
$io->section('Getting concerned membership...');
$query = $this->userRepository->getExpiredMembership();
$io->section('Processing user updates...');
$count = 0;
/** @var User $user */
foreach ($query->toIterable() as $user) {
if ($user->getPlatformOffer()?->getType() === OfferType::ONESHOT) {
continue;
}
$io->comment(\sprintf(' > ending platform membership expiration for user %s (%s)',
$user->getDisplayName(),
$user->getEmail(),
));
// save it here for the mail before setting it back to null
$endAt = $user->getEndAt();
$user->setMembershipPaid(false)
->setEndAt(null)
->setPayedAt(null)
->setStartAt(null)
->setPlatformOffer(null);
$this->userManager->save($user, true);
$this->appMailer->send(EndPlatformMembershipMail::class, compact('user', 'platform', 'endAt'));
$this->sendSms($user, EndPlatformMembershipMail::class, ['%platform%' => $platform]);
++$count;
}
$io->note(\sprintf(' > %d update(s) done.', $count));
$this->memoryReport($io);
$this->done($io);
return Command::SUCCESS;
}
}

View file

@ -64,7 +64,7 @@ final class NotifyMembershipExpirationCommand extends Command
$days = $input->getArgument('days'); $days = $input->getArgument('days');
$days = max(1, (int) $days); $days = max(1, (int) $days);
$io->section(sprintf('Getting membership expiring in %d days...', $days)); $io->section(\sprintf('Getting membership expiring in %d days...', $days));
$query = $this->userGroupRepository->getExpiring($days); $query = $this->userGroupRepository->getExpiring($days);
$io->section('Sending notificaitons...'); $io->section('Sending notificaitons...');
$count = 0; $count = 0;
@ -72,7 +72,7 @@ final class NotifyMembershipExpirationCommand extends Command
/** @var UserGroup $userGroup */ /** @var UserGroup $userGroup */
$user = $userGroup->getUser(); $user = $userGroup->getUser();
$group = $userGroup->getGroup(); $group = $userGroup->getGroup();
$io->comment(sprintf(' > notifying membership for %s of %s/%s (%s) (%s)', $io->comment(\sprintf(' > notifying membership for %s of %s/%s (%s) (%s)',
$group->getName(), $group->getName(),
$user->getDisplayName(), $user->getDisplayName(),
$userGroup->getEndAt()?->format('Y-m-d'), $userGroup->getEndAt()?->format('Y-m-d'),
@ -88,7 +88,7 @@ final class NotifyMembershipExpirationCommand extends Command
++$count; ++$count;
} }
$io->note(sprintf(' > %d notification(s) sent.', $count)); $io->note(\sprintf(' > %d notification(s) sent.', $count));
$this->memoryReport($io); $this->memoryReport($io);
$this->done($io); $this->done($io);

View file

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Entity\User;
use App\Enum\OfferType;
use App\Mailer\AppMailer;
use App\Mailer\Email\Command\NotifyPlatformMembershipExpirationMail;
use App\Notifier\SmsNotifier;
use App\Notifier\SmsNotifierTrait;
use App\Repository\ConfigurationRepository;
use App\Repository\UserRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsCommand(name: self::CMD, description: self::DESCRIPTION)]
class NotifyPlatformMembershipExpirationCommand extends Command
{
use CommandTrait;
use SmsNotifierTrait;
public const CMD = 'app:notify-platform-membership-expiration';
public const DESCRIPTION = 'Notify expiring platform membership.';
public function __construct(
private readonly UserRepository $userRepository,
private readonly TranslatorInterface $translator,
private readonly AppMailer $appMailer,
private readonly SmsNotifier $notifier,
#[Autowire('%kernel.environment%')]
private readonly string $environment,
#[Autowire('%brand%')]
private readonly string $brand,
private readonly ConfigurationRepository $configurationRepository,
) {
parent::__construct();
}
protected function configure(): void
{
$this->configureCommand(self::DESCRIPTION);
$this->addArgument('days', InputArgument::REQUIRED, 'Number of days from tomorrow (1 = notify members expiring tomorrow)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$configuration = $this->configurationRepository->getInstanceConfigurationOrCreate();
if (!$configuration->getPaidMembership()) {
return Command::SUCCESS;
}
$platform = $configuration->getPlatformName();
$io->title(self::DESCRIPTION.' ('.$this->environment.' env)');
$this->memoryReport($io);
/** @var string $days */
$days = $input->getArgument('days');
$days = max(1, (int) $days);
$io->section(\sprintf('Getting platform membership expiring in %d days...', $days));
$query = $this->userRepository->getExpiring($days);
$io->section('Sending notifications...');
$count = 0;
/** @var User $user */
foreach ($query->toIterable() as $user) {
if ($user->getPlatformOffer()?->getType() === OfferType::ONESHOT) {
continue;
}
$io->comment(\sprintf(' > notifying platform membership expiration for user %s (%s)',
$user->getDisplayName(),
$user->getEmail(),
));
$this->appMailer->send(NotifyPlatformMembershipExpirationMail::class, compact('user', 'days', 'platform'));
$this->sendSms($user, NotifyPlatformMembershipExpirationMail::class, ['%days%' => $days, '%platform%' => $platform]);
++$count;
}
$io->note(\sprintf(' > %d notification(s) sent.', $count));
$this->memoryReport($io);
$this->done($io);
return Command::SUCCESS;
}
}

View file

@ -84,7 +84,7 @@ final class NotifyServiceRequestDatesCommand extends Command
$serviceRequest->getStartAt() : $serviceRequest->getStartAt() :
$serviceRequest->getEndAt() $serviceRequest->getEndAt()
; ;
$io->comment(sprintf(' > notifying owner and recipient for service request %s (%s) starting on %s.', $io->comment(\sprintf(' > notifying owner and recipient for service request %s (%s) starting on %s.',
$serviceRequest->getId(), $serviceRequest->getId(),
$serviceRequest->getStatus()->value, $serviceRequest->getStatus()->value,
$referenceDate->format('Y-m-d') $referenceDate->format('Y-m-d')
@ -106,7 +106,7 @@ final class NotifyServiceRequestDatesCommand extends Command
++$count; ++$count;
} }
$io->note(sprintf(' > %d notification(s) sent.', $count * 2)); // owner and recipient $io->note(\sprintf(' > %d notification(s) sent.', $count * 2)); // owner and recipient
$this->memoryReport($io); $this->memoryReport($io);
$this->done($io); $this->done($io);

View file

@ -245,7 +245,7 @@ abstract class AbstractCategoryCrudController extends AbstractCrudController imp
$idField, $idField,
$createdAt, $createdAt,
$updatedAt, $updatedAt,
]; ];
} }
public function moveUp(AdminContext $context): Response public function moveUp(AdminContext $context): Response

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Controller\i18nTrait;
use App\EasyAdmin\Field\FieldTrait; use App\EasyAdmin\Field\FieldTrait;
use App\EasyAdmin\Filter\EnumFilter; use App\EasyAdmin\Filter\EnumFilter;
use App\EasyAdmin\Filter\UuidFilter; use App\EasyAdmin\Filter\UuidFilter;
@ -15,8 +16,10 @@ use App\Enum\Product\ProductType;
use App\Enum\Product\ProductVisibility; use App\Enum\Product\ProductVisibility;
use App\Flysystem\EasyAdminHelper; use App\Flysystem\EasyAdminHelper;
use App\Flysystem\MediaManager; use App\Flysystem\MediaManager;
use App\Form\Type\Product\AbstractProductFormType;
use App\Helper\CsvExporter; use App\Helper\CsvExporter;
use App\Repository\CategoryRepository; use App\Repository\CategoryRepository;
use App\Repository\GroupRepository;
use App\Repository\ProductRepository; use App\Repository\ProductRepository;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection; use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
@ -54,6 +57,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
abstract class AbstractProductCrudController extends AbstractCrudController implements AdminSecuredCrudControllerInterface abstract class AbstractProductCrudController extends AbstractCrudController implements AdminSecuredCrudControllerInterface
{ {
use FieldTrait; use FieldTrait;
use i18nTrait;
abstract public function getProductType(): ProductType; abstract public function getProductType(): ProductType;
@ -74,6 +78,7 @@ abstract class AbstractProductCrudController extends AbstractCrudController impl
private readonly TranslatorInterface $translator, private readonly TranslatorInterface $translator,
private readonly FilterFactory $filterFactory, private readonly FilterFactory $filterFactory,
private readonly SluggerInterface $slugger, private readonly SluggerInterface $slugger,
protected readonly GroupRepository $groupRepository,
) { ) {
} }
@ -88,6 +93,9 @@ abstract class AbstractProductCrudController extends AbstractCrudController impl
'@EasyAdmin/crud/form_theme.html.twig', '@EasyAdmin/crud/form_theme.html.twig',
'easy_admin/crud/form_theme.html.twig', 'easy_admin/crud/form_theme.html.twig',
]) ])
->setFormOptions([
'validation_groups' => [AbstractProductFormType::class],
])
; ;
} }
@ -241,9 +249,15 @@ abstract class AbstractProductCrudController extends AbstractCrudController impl
->setFormType(EnumType::class) ->setFormType(EnumType::class)
->setFormTypeOption('class', ProductVisibility::class) ->setFormTypeOption('class', ProductVisibility::class)
->setChoices(ProductVisibility::getAsArray()); ->setChoices(ProductVisibility::getAsArray());
$groupsField = CollectionField::new('groups'); $groupsField = AssociationField::new('groups')->onlyOnForms();
$groupsFieldList = CollectionField::new('groups')->hideOnForm();
$ownerField = AssociationField::new('owner'); $ownerField = AssociationField::new('owner')
->setFormTypeOption('attr', [
'data-controller' => 'admin-parentgroup',
'data-admin-parentgroup-target' => 'ownerField',
])
->addWebpackEncoreEntries('admin');
$categoryField = AssociationField::new('category') $categoryField = AssociationField::new('category')
->setQueryBuilder(function (QueryBuilder $queryBuilder) { ->setQueryBuilder(function (QueryBuilder $queryBuilder) {
return $this->categoryRepository->addTypeFilter($queryBuilder, $this->getProductType()); return $this->categoryRepository->addTypeFilter($queryBuilder, $this->getProductType());
@ -300,6 +314,7 @@ abstract class AbstractProductCrudController extends AbstractCrudController impl
'statusField', 'statusField',
'visibilityField', 'visibilityField',
'groupsField', 'groupsField',
'groupsFieldList',
'ownerField', 'ownerField',
'categoryField', 'categoryField',
'nameField', 'nameField',

View file

@ -17,6 +17,7 @@ use App\Flysystem\MediaManager;
use App\Helper\CsvExporter; use App\Helper\CsvExporter;
use App\Mailer\AppMailer; use App\Mailer\AppMailer;
use App\Mailer\Email\Admin\PromoteToAdmin\PromoteToAdminEmail; use App\Mailer\Email\Admin\PromoteToAdmin\PromoteToAdminEmail;
use App\Repository\ConfigurationRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection; use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
@ -37,10 +38,12 @@ use EasyCorp\Bundle\EasyAdminBundle\Factory\FormFactory;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField; use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField; use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField; use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField; use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField; use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField; use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField; use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Filter\DateTimeFilter; use EasyCorp\Bundle\EasyAdminBundle\Filter\DateTimeFilter;
@ -89,6 +92,7 @@ abstract class AbstractUserCrudController extends AbstractCrudController impleme
#[Autowire('%user_base_path%')] #[Autowire('%user_base_path%')]
private readonly string $userBasePath, private readonly string $userBasePath,
AppMailer $mailer, AppMailer $mailer,
private readonly ConfigurationRepository $configurationRepository,
) { ) {
$this->mailer = $mailer; $this->mailer = $mailer;
} }
@ -355,6 +359,20 @@ abstract class AbstractUserCrudController extends AbstractCrudController impleme
$vacationModeField = BooleanField::new('vacationMode'); $vacationModeField = BooleanField::new('vacationMode');
$addressField = AssociationField::new('address'); $addressField = AssociationField::new('address');
$groupsCountField = AssociationField::new('userGroups')->setLabel('Groups number'); $groupsCountField = AssociationField::new('userGroups')->setLabel('Groups number');
$membershipPaidField = $this->getSimpleBooleanField('membershipPaid');
$startAt = DateField::new('startAt');
$endAt = DateField::new('endAt');
$expiresInField = IntegerField::new('expiresIn')
->formatValue(function ($value) {
return $value !== null ? $this->translator->trans($this->getI18nPrefix().'.expires_in.formatted_value', ['%days%' => $value], 'admin') : '';
})
->setFormTypeOptions([
'attr' => ['readonly' => 'readonly'],
'required' => false,
])
;
$payedAt = DateTimeField::new('payedAt');
$offerField = AssociationField::new('platformOffer');
return compact( return compact(
'idField', 'idField',
@ -378,6 +396,12 @@ abstract class AbstractUserCrudController extends AbstractCrudController impleme
'vacationModeField', 'vacationModeField',
'addressField', 'addressField',
'groupsCountField', 'groupsCountField',
'membershipPaidField',
'startAt',
'endAt',
'expiresInField',
'payedAt',
'offerField',
); );
} }
@ -445,4 +469,9 @@ abstract class AbstractUserCrudController extends AbstractCrudController impleme
return $builder; return $builder;
} }
public function platformRequiresGlobalPayment(): bool
{
return $this->configurationRepository->getInstanceConfigurationOrCreate()->getPaidMembership();
}
} }

View file

@ -22,15 +22,18 @@ use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem; use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator; use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* All /admin routes are protected at the security.yaml level. * All /admin routes are protected at the security.yaml level.
* ROLE_ADMIN inherits the ROLE_GROUP_ADMIN role.
*
* @see security.yaml
*/ */
#[Security("is_granted('".User::ROLE_ADMIN."') or is_granted('".User::ROLE_GROUP_ADMIN."')")] #[IsGranted(User::ROLE_GROUP_ADMIN)]
final class DashboardController extends AbstractDashboardController final class DashboardController extends AbstractDashboardController
{ {
public const DOMAIN = 'admin'; public const DOMAIN = 'admin';
@ -60,7 +63,7 @@ final class DashboardController extends AbstractDashboardController
private readonly AdminUrlGenerator $adminUrlGenerator, private readonly AdminUrlGenerator $adminUrlGenerator,
private readonly AuthorizationChecker $authorizationChecker, private readonly AuthorizationChecker $authorizationChecker,
private readonly UserRepository $userRepository, private readonly UserRepository $userRepository,
private readonly ServiceRequestRepository $requestRepository private readonly ServiceRequestRepository $requestRepository,
) { ) {
} }
@ -159,8 +162,8 @@ final class DashboardController extends AbstractDashboardController
yield MenuItem::linkToCrud('menu.pages', 'fas fa-hat-wizard', Page::class)->setPermission(User::ROLE_ADMIN); yield MenuItem::linkToCrud('menu.pages', 'fas fa-hat-wizard', Page::class)->setPermission(User::ROLE_ADMIN);
yield MenuItem::subMenu('menu.categories', 'fa-solid fa-folder')->setSubItems([ yield MenuItem::subMenu('menu.categories', 'fa-solid fa-folder')->setSubItems([
MenuItem::linkToUrl('menu.objects', 'fa-solid fa-box', $categoryObjectUrl)->setPermission(User::ROLE_ADMIN), MenuItem::linkToUrl('menu.objects', 'fa-solid fa-box', $categoryObjectUrl)->setPermission(User::ROLE_ADMIN),
MenuItem::linkToUrl('menu.services', 'fa-regular fa-handshake', $categoryServiceUrl)->setPermission(User::ROLE_ADMIN), MenuItem::linkToUrl('menu.services', 'fa-regular fa-handshake', $categoryServiceUrl)->setPermission(User::ROLE_ADMIN),
])->setPermission(User::ROLE_ADMIN); ])->setPermission(User::ROLE_ADMIN);
// ————————————————————————————————————————————————————————————————————— // —————————————————————————————————————————————————————————————————————

View file

@ -19,6 +19,7 @@ use App\Form\Type\Security\GroupInvitationFormType;
use App\Helper\CsvExporter; use App\Helper\CsvExporter;
use App\Message\Command\Group\CreateGroupInvitationMessage; use App\Message\Command\Group\CreateGroupInvitationMessage;
use App\MessageBus\CommandBus; use App\MessageBus\CommandBus;
use App\Repository\ConfigurationRepository;
use App\Security\Checker\AuthorizationChecker; use App\Security\Checker\AuthorizationChecker;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection; use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
@ -66,6 +67,7 @@ final class GroupCrudController extends AbstractCrudController implements GroupA
private readonly TranslatorInterface $translator, private readonly TranslatorInterface $translator,
private readonly FilterFactory $filterFactory, private readonly FilterFactory $filterFactory,
private readonly SluggerInterface $slugger, private readonly SluggerInterface $slugger,
private readonly ConfigurationRepository $configurationRepository,
) { ) {
} }
@ -75,6 +77,7 @@ final class GroupCrudController extends AbstractCrudController implements GroupA
->setEntityLabelInPlural('groups') ->setEntityLabelInPlural('groups')
->setSearchFields(['name', 'description']) ->setSearchFields(['name', 'description'])
->setDefaultSort(['id' => 'ASC']) ->setDefaultSort(['id' => 'ASC'])
->overrideTemplate('crud/field/boolean', 'admin/field/services_enabled.html.twig')
; ;
} }
@ -209,7 +212,7 @@ final class GroupCrudController extends AbstractCrudController implements GroupA
/** @var User $user */ /** @var User $user */
$user = $this->getUser(); $user = $this->getUser();
$qb->andWhere(sprintf('%s.id IN (:groups)', $qb->getRootAliases()[0] ?? '')) $qb->andWhere(\sprintf('%s.id IN (:groups)', $qb->getRootAliases()[0] ?? ''))
->setParameter(':groups', $user->getMyGroupsAsAdmin()); ->setParameter(':groups', $user->getMyGroupsAsAdmin());
return $qb; return $qb;
@ -230,8 +233,24 @@ final class GroupCrudController extends AbstractCrudController implements GroupA
->setFormTypeOption('class', GroupMembership::class) ->setFormTypeOption('class', GroupMembership::class)
->setChoices(GroupMembership::getAsArray()); ->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') $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'); $childrenField = AssociationField::new('children');
$usersField = AssociationField::new('userGroups') $usersField = AssociationField::new('userGroups')
->setTemplatePath('admin/group/user_groups_field.html.twig'); ->setTemplatePath('admin/group/user_groups_field.html.twig');
@ -247,14 +266,20 @@ final class GroupCrudController extends AbstractCrudController implements GroupA
$panels = $this->getPanels(); $panels = $this->getPanels();
if ($pageName === Crud::PAGE_INDEX) { 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) { if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) {
$typeField->setChoices(GroupType::cases()); $typeField->setChoices(GroupType::cases());
$membershipField->setChoices(GroupMembership::cases()); $membershipField->setChoices(GroupMembership::cases());
return [ $fields = [
$nameField, $nameField,
$typeField, $typeField,
$membershipField, $membershipField,
@ -264,11 +289,29 @@ final class GroupCrudController extends AbstractCrudController implements GroupA
$invitationByAdminField, $invitationByAdminField,
$membershipField, $membershipField,
]; ];
if ($this->configurationRepository->getInstanceConfigurationOrCreate()->getServicesEnabled()) {
$i18prefix = $this->getI18nPrefix(self::class);
/** @var Group|null $group */
$group = $this->getContext()?->getEntity()?->getInstance();
if (null !== $group) {
foreach ($group->getParentsRecursively() as $parent) {
if (!$parent->getServicesEnabled()) {
$servicesEnabledField->setDisabled();
$servicesEnabledField->setHelp($i18prefix.'.field.services_enabled.parent_disabled');
break;
}
}
}
array_splice($fields, 4, 0, [$servicesEnabledField]);
}
return $fields;
} }
// show // show
return [ $fields = [
$panels['information'], $panels['information'],
$nameField, $nameField,
$parentField, $parentField,
@ -283,6 +326,12 @@ final class GroupCrudController extends AbstractCrudController implements GroupA
$updatedAt, $updatedAt,
$createdAt, $createdAt,
]; ];
if ($this->configurationRepository->getInstanceConfigurationOrCreate()->getServicesEnabled()) {
array_splice($fields, 3, 0, [$servicesEnabledField]);
}
return $fields;
} }
/** /**
@ -309,7 +358,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 public function export(AdminContext $context): Response
{ {

View file

@ -10,12 +10,12 @@ use App\EasyAdmin\Field\FieldTrait;
use App\EasyAdmin\Filter\EnumFilter; use App\EasyAdmin\Filter\EnumFilter;
use App\EasyAdmin\Filter\UserGroup\MyGroupFilter; use App\EasyAdmin\Filter\UserGroup\MyGroupFilter;
use App\EasyAdmin\Filter\UuidFilter; use App\EasyAdmin\Filter\UuidFilter;
use App\EasyAdmin\Form\Type\GroupOfferTypeType; use App\EasyAdmin\Form\Type\OfferTypeType;
use App\Entity\GroupOffer; use App\Entity\GroupOffer;
use App\Entity\User; use App\Entity\User;
use App\Enum\Group\GroupMembership; use App\Enum\Group\GroupMembership;
use App\Enum\Group\GroupOfferType;
use App\Enum\Group\UserMembership; use App\Enum\Group\UserMembership;
use App\Enum\OfferType;
use App\Security\Checker\AuthorizationChecker; use App\Security\Checker\AuthorizationChecker;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection; use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
@ -65,7 +65,7 @@ final class GroupOfferCrudController extends AbstractCrudController implements G
return $filters return $filters
->add(UuidFilter::new('id')) ->add(UuidFilter::new('id'))
->add(MyGroupFilter::new('group')) ->add(MyGroupFilter::new('group'))
->add(EnumFilter::new('membership', GroupOfferTypeType::class)) ->add(EnumFilter::new('membership', OfferTypeType::class))
->add('name') ->add('name')
->add('active') ->add('active')
; ;
@ -98,7 +98,7 @@ final class GroupOfferCrudController extends AbstractCrudController implements G
/** @var User $user */ /** @var User $user */
$user = $this->getUser(); $user = $this->getUser();
$qb->andWhere(sprintf('%s.group IN (:groups)', $qb->getRootAliases()[0] ?? '')) $qb->andWhere(\sprintf('%s.group IN (:groups)', $qb->getRootAliases()[0] ?? ''))
->setParameter(':groups', $user->getMyGroupsAsAdmin()); ->setParameter(':groups', $user->getMyGroupsAsAdmin());
return $qb; return $qb;
@ -129,8 +129,8 @@ final class GroupOfferCrudController extends AbstractCrudController implements G
$nameField = TextField::new('name'); $nameField = TextField::new('name');
$typeField = ChoiceField::new('type') $typeField = ChoiceField::new('type')
->setFormType(EnumType::class) ->setFormType(EnumType::class)
->setFormTypeOption('class', GroupOfferType::class) ->setFormTypeOption('class', OfferType::class)
->setChoices(GroupOfferType::getAsArray()); ->setChoices(OfferType::getAsArray());
$priceField = MoneyField::new('price') $priceField = MoneyField::new('price')
->setCurrencyPropertyPath('currency') ->setCurrencyPropertyPath('currency')
@ -149,7 +149,7 @@ final class GroupOfferCrudController extends AbstractCrudController implements G
} }
if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) {
$typeField->setChoices(GroupOfferType::cases()); $typeField->setChoices(OfferType::cases());
return [ return [
$groupField, $groupField,

View file

@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Controller\FlashTrait;
use App\Controller\i18nTrait;
use App\EasyAdmin\Field\FieldTrait;
use App\EasyAdmin\Filter\EnumFilter;
use App\EasyAdmin\Filter\UuidFilter;
use App\EasyAdmin\Form\Type\OfferTypeType;
use App\Entity\PlatformOffer;
use App\Enum\OfferType;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
use EasyCorp\Bundle\EasyAdminBundle\Field\CurrencyField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\MoneyField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
final class PlatformOfferCrudController extends AbstractCrudController implements AdminSecuredCrudControllerInterface
{
use FlashTrait;
use FieldTrait;
use i18nTrait;
public function configureCrud(Crud $crud): Crud
{
return $crud
->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,
];
}
}

View file

@ -5,10 +5,13 @@ declare(strict_types=1);
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Product; use App\Entity\Product;
use App\Enum\Group\UserMembership;
use App\Enum\Product\ProductStatus; use App\Enum\Product\ProductStatus;
use App\Enum\Product\ProductType; use App\Enum\Product\ProductType;
use App\Enum\Product\ProductVisibility; use App\Enum\Product\ProductVisibility;
use Doctrine\ORM\QueryBuilder;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField; use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField; use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
@ -36,6 +39,7 @@ final class ServiceCrudController extends AbstractProductCrudController
{ {
$product = parent::createEntity($entityFqcn); $product = parent::createEntity($entityFqcn);
$product->setCurrency(null); // remove the default value which is not needed here $product->setCurrency(null); // remove the default value which is not needed here
$product->setVisibility(ProductVisibility::RESTRICTED);
return $product; return $product;
} }
@ -49,6 +53,8 @@ final class ServiceCrudController extends AbstractProductCrudController
'typeField' => $typeField, 'typeField' => $typeField,
'statusField' => $statusField, 'statusField' => $statusField,
'visibilityField' => $visibilityField, 'visibilityField' => $visibilityField,
'groupsField' => $groupsField,
'groupsFieldList' => $groupsFieldList,
'ownerField' => $ownerField, 'ownerField' => $ownerField,
'categoryField' => $categoryField, 'categoryField' => $categoryField,
'nameField' => $nameField, 'nameField' => $nameField,
@ -61,7 +67,7 @@ final class ServiceCrudController extends AbstractProductCrudController
// list // list
if ($pageName === Crud::PAGE_INDEX) { if ($pageName === Crud::PAGE_INDEX) {
return [$nameField, $ownerField, $categoryField, $statusField, $visibilityField, $imageField, $createdAt]; return [$nameField, $ownerField, $categoryField, $statusField, $visibilityField, $groupsFieldList, $imageField, $createdAt];
} }
/** @var ImageField $imageField */ /** @var ImageField $imageField */
@ -71,10 +77,35 @@ final class ServiceCrudController extends AbstractProductCrudController
if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) {
/** @var ChoiceField $statusField */ /** @var ChoiceField $statusField */
$statusField->setChoices(ProductStatus::cases()); $statusField->setChoices(ProductStatus::cases());
/** @var ChoiceField $visibilityField */
$visibilityField->setChoices(ProductVisibility::cases());
return [$nameField, $ownerField, $categoryField, $statusField, $visibilityField, $descriptionField, $imageField, $durationField]; $i18prefix = $this->getI18nPrefix(self::class);
/** @var AssociationField $groupsField */
$groupsField->setHelp($i18prefix.'.field.groups.help');
if ($pageName === Crud::PAGE_NEW) {
return [$nameField, $ownerField, $categoryField, $statusField, $groupsField, $descriptionField, $imageField, $durationField];
}
/** @var Product|null $product */
$product = $this->getContext()?->getEntity()?->getInstance();
$owner = $product?->getOwner();
if (null !== $owner && !$owner->getUserGroupsConfirmedWithServices()->isEmpty()) {
$groupsField->setQueryBuilder(function (QueryBuilder $queryBuilder) use ($owner) {
return $queryBuilder
->join('entity.userGroups', 'ug')
->andWhere('ug.membership != :membership')
->andWhere('ug.user = :user')
->andWhere('entity.servicesEnabled = :true')
->setParameter('user', $owner)
->setParameter('membership', UserMembership::INVITATION)
->setParameter('true', true)
;
});
} else {
$groupsField->setDisabled();
}
return [$nameField, $ownerField, $categoryField, $statusField, $groupsField, $descriptionField, $imageField, $durationField];
} }
// detail // detail
@ -85,6 +116,7 @@ final class ServiceCrudController extends AbstractProductCrudController
$categoryField, $categoryField,
$statusField, $statusField,
$visibilityField, $visibilityField,
$groupsFieldList,
$nameField, $nameField,
$descriptionField, $descriptionField,
$durationField, $durationField,

View file

@ -48,16 +48,27 @@ final class UserCrudController extends AbstractUserCrudController
'vacationModeField' => $vacationModeField, 'vacationModeField' => $vacationModeField,
'addressField' => $addressField, 'addressField' => $addressField,
'groupsCountField' => $groupsCountField, 'groupsCountField' => $groupsCountField,
'membershipPaidField' => $membershipPaidField,
'startAt' => $startAt,
'endAt' => $endAt,
'expiresInField' => $expiresInField,
'payedAt' => $payedAt,
'offerField' => $offerField,
] = $this->getFields($pageName); ] = $this->getFields($pageName);
if ($pageName === Crud::PAGE_INDEX) { if ($pageName === Crud::PAGE_INDEX) {
return [$emailField, $firstNameField, $lastNameField, $enabledField, $emailConfirmedField, $avatarField, $createdAt, $updatedAt, $loginAt, $groupsCountField]; $listFields = [$emailField, $firstNameField, $lastNameField, $enabledField, $emailConfirmedField, $avatarField, $createdAt, $updatedAt, $loginAt, $groupsCountField];
if ($this->platformRequiresGlobalPayment()) {
$listFields[] = $membershipPaidField;
}
return $listFields;
} }
$panels = $this->getPanels(); $panels = $this->getPanels();
if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) {
return [ $editFields = [
$panels['information'], $panels['information'],
$emailField, $emailField,
$firstNameField, $firstNameField,
@ -74,9 +85,21 @@ final class UserCrudController extends AbstractUserCrudController
$enabledField, $enabledField,
$emailConfirmedField, $emailConfirmedField,
]; ];
if ($this->platformRequiresGlobalPayment()) {
$editFields = array_merge($editFields, [
$panels['payment_information'],
$membershipPaidField,
$offerField,
$startAt,
$endAt,
$payedAt,
]);
}
return $editFields;
} }
return [ $showFields = [
$panels['information'], $panels['information'],
$emailField, $emailField,
$firstNameField, $firstNameField,
@ -97,5 +120,17 @@ final class UserCrudController extends AbstractUserCrudController
$updatedAt, $updatedAt,
$loginAt, $loginAt,
]; ];
if ($this->platformRequiresGlobalPayment()) {
$showFields = array_merge($showFields, [
$panels['payment_information'],
$membershipPaidField,
$startAt,
$endAt,
$payedAt,
$expiresInField,
]);
}
return $showFields;
} }
} }

View file

@ -130,7 +130,7 @@ final class UserGroupCrudController extends AbstractCrudController implements Gr
/** @var User $user */ /** @var User $user */
$user = $this->getUser(); $user = $this->getUser();
$qb->andWhere(sprintf('%s.group IN (:groups)', $qb->getRootAliases()[0] ?? '')) $qb->andWhere(\sprintf('%s.group IN (:groups)', $qb->getRootAliases()[0] ?? ''))
->setParameter(':groups', $user->getMyGroupsAsAdmin()); ->setParameter(':groups', $user->getMyGroupsAsAdmin());
return $qb; return $qb;

View file

@ -39,8 +39,6 @@ final class CreateGroupAction extends AbstractController
use FlashTrait; use FlashTrait;
use GroupTrait; use GroupTrait;
public const MAX_ELEMENT_BY_PAGE = 20;
public function __construct( public function __construct(
private readonly QueryBus $queryBus, private readonly QueryBus $queryBus,
private readonly GroupRepository $groupRepository, private readonly GroupRepository $groupRepository,
@ -52,7 +50,7 @@ final class CreateGroupAction extends AbstractController
) { ) {
} }
#[isGranted(User::ROLE_USER)] #[IsGranted(User::ROLE_USER)]
#[Route([ #[Route([
'en' => MyAccountAction::BASE_URL_EN.'/groups/create-my-group', 'en' => MyAccountAction::BASE_URL_EN.'/groups/create-my-group',
'fr' => MyAccountAction::BASE_URL_FR.'/groupes/creer-mon-groupe', 'fr' => MyAccountAction::BASE_URL_FR.'/groupes/creer-mon-groupe',
@ -61,6 +59,16 @@ final class CreateGroupAction extends AbstractController
)] )]
public function createGroup(Request $request, #[CurrentUser] User $user): Response public function createGroup(Request $request, #[CurrentUser] User $user): Response
{ {
// Admin must use the admin interface
if ($user->isAdmin()) {
return $this->redirect(
$this->adminUrlGenerator
->setController(GroupCrudController::class)
->set('crudAction', Crud::PAGE_NEW)
->generateUrl()
);
}
$configuration = $this->configurationRepository->getInstanceConfigurationOrCreate(); $configuration = $this->configurationRepository->getInstanceConfigurationOrCreate();
if (!$configuration->isGroupsCreationForAll()) { if (!$configuration->isGroupsCreationForAll()) {
throw $this->createAccessDeniedException('Cannot create group with current settings.'); throw $this->createAccessDeniedException('Cannot create group with current settings.');

View file

@ -21,7 +21,7 @@ trait PaginationTrait
* *
* @implements PaginationInterface<int,Product> * @implements PaginationInterface<int,Product>
* *
* @return PaginationInterface<int,Product> * @return PaginationInterface<int,mixed>
*/ */
private function paginate(SearchResult $searchResult): PaginationInterface private function paginate(SearchResult $searchResult): PaginationInterface
{ {

View file

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Controller\Payment; namespace App\Controller\Payment\Group;
use App\Controller\FlashTrait; use App\Controller\FlashTrait;
use App\Controller\i18nTrait; use App\Controller\i18nTrait;
@ -18,7 +18,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement; use Symfony\Component\Routing\Requirement\Requirement;
use Symfony\Component\Security\Http\Attribute\CurrentUser; use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
@ -67,6 +67,7 @@ final class DoneAction extends AbstractController
$this->addFlashSuccess($this->translator->trans($this->getI18nPrefix().'.flash.success', [ $this->addFlashSuccess($this->translator->trans($this->getI18nPrefix().'.flash.success', [
'%group%' => $groupOffer->getGroup()->getName()], '%group%' => $groupOffer->getGroup()->getName()],
)); ));
$request->getSession()->remove('payment_in_progress');
} else { } else {
$this->addFlashWarning($this->translator->trans($this->getI18nPrefix().'.status.'.$status->getValue())); $this->addFlashWarning($this->translator->trans($this->getI18nPrefix().'.status.'.$status->getValue()));
} }

View file

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Controller\Payment; namespace App\Controller\Payment\Group;
use App\Entity\GroupOffer; use App\Entity\GroupOffer;
use App\Repository\GroupOfferRepository; use App\Repository\GroupOfferRepository;

View file

@ -2,14 +2,13 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Controller\Payment; namespace App\Controller\Payment\Group;
use App\Controller\User\MyAccountAction; use App\Controller\User\MyAccountAction;
use App\Entity\User; use App\Entity\User;
use App\Payment\PayumManager; use App\Payment\PayumManager;
use App\Repository\GroupOfferRepository; use App\Repository\GroupOfferRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
// use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Controller\Payment\PlatformMembership;
use App\Controller\FlashTrait;
use App\Controller\i18nTrait;
use App\Entity\PaymentToken;
use App\Entity\PlatformOffer;
use App\Entity\User;
use App\Message\Command\Payment\PlatformMembershipPaidCommand;
use App\MessageBus\CommandBusInterface;
use Payum\Core\Payum;
use Payum\Core\Request\GetHumanStatus;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Translation\TranslatorInterface;
#[IsGranted(User::ROLE_USER)]
final class DoneAction extends AbstractController
{
use i18nTrait;
use FlashTrait;
public const ROUTE_NAME = 'app_platform_payment_done';
public function __construct(
private readonly CommandBusInterface $commandBus,
private readonly Payum $payum,
private readonly TranslatorInterface $translator,
private readonly LoggerInterface $logger,
) {
}
/**
* @see https://github.com/Payum/Payum/blob/master/docs/symfony/get-it-started.md#payment-is-done
*/
#[Route(
path: '/payment/{id}/done',
name: self::ROUTE_NAME,
requirements: ['id' => Requirement::UUID_V6],
)]
public function __invoke(Request $request, #[MapEntity(expr: 'repository.findOneActive(id)')] PlatformOffer $platformOffer, #[CurrentUser] User $user): Response
{
try {
/** @var PaymentToken $token */
$token = $this->payum->getHttpRequestVerifier()->verify($request);
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
throw new UnprocessableEntityHttpException('Cannot verify Payum token.');
}
/** @var GetHumanStatus $status */
$status = $this->commandBus->dispatch(new PlatformMembershipPaidCommand($platformOffer->getId(), $user->getId(), $token));
$request->getSession()->remove('payment_in_progress');
// Not captured
if (!$status->isCaptured()) {
$this->addFlashWarning($this->translator->trans($this->getI18nPrefix().'.status.'.$status->getValue()));
return $this->redirectToRoute('app_user_my_account');
}
$this->addFlashSuccess($this->translator->trans($this->getI18nPrefix().'.flash.success', [
'%platform%' => $platformOffer->getConfiguration()?->getPlatformName()],
));
$group = $user->getMyGroupsAsInvited()->first();
if ($group !== false) {
return $this->redirectToRoute('app_group_show_logged', $group->getRoutingParameters());
}
return $this->redirectToRoute('app_user_my_account');
}
}

View file

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Controller\Payment\PlatformMembership;
use App\Entity\PlatformOffer;
use App\Entity\User;
use App\Payment\PayumManager;
use App\Repository\ConfigurationRepository;
use App\Repository\PlatformOfferRepository;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Requirement\Requirement;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted(User::ROLE_USER)]
final class PrepareAction extends AbstractController
{
public const ROUTE_NAME = 'app_platform_payment_prepare';
/**
* @see https://github.com/Payum/Payum/blob/master/docs/symfony/get-it-started.md#prepare-order)
*/
#[Route(
path: '/payment/{id}/prepare',
name: self::ROUTE_NAME,
requirements: ['id' => Requirement::UUID_V6],
methods: ['POST'],
)]
public function preparePayment(Request $request, #[MapEntity(expr: 'repository.findOneActive(id)')] PlatformOffer $platformOffer, #[CurrentUser] User $user, PayumManager $payumManager): Response
{
/** @var ?string $token */
$token = $request->request->get('token');
if (!$this->isCsrfTokenValid('payment_prepare', $token)) {
throw new UnprocessableEntityHttpException('Invalid CSRF token');
}
$request->getSession()->set('payment_in_progress', true);
// create and save the payment main reference
$payment = $payumManager->getPayment($platformOffer, $user);
// create the capture token and redirect to the capture action
$captureToken = $payumManager->getCaptureToken($payment, DoneAction::ROUTE_NAME, [
'id' => (string) $platformOffer->getId(),
]);
return $this->redirect($captureToken->getTargetUrl());
}
#[Route(path: [
'en' => '/en/subcription',
'fr' => '/fr/abonnement',
], name: 'redirect_to_payment')]
public function redirectToPayment(PlatformOfferRepository $platformOfferRepository, 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', 'lowOffer', 'platformName'));
}
}

View file

@ -19,6 +19,7 @@ use App\Message\Query\Security\GetUserByTokenQuery;
use App\MessageBus\CommandBus; use App\MessageBus\CommandBus;
use App\MessageBus\QueryBus; use App\MessageBus\QueryBus;
use App\MessageHandler\Command\Security\AccountCreateStep1CommandHandler; use App\MessageHandler\Command\Security\AccountCreateStep1CommandHandler;
use App\Repository\ConfigurationRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -41,6 +42,7 @@ final class AccountCreateController extends AbstractController
private readonly QueryBus $queryBus, private readonly QueryBus $queryBus,
private readonly CommandBus $commandBus, private readonly CommandBus $commandBus,
private readonly Security $security, private readonly Security $security,
private readonly ConfigurationRepository $configurationRepository,
) { ) {
$this->i18nPrefix = $this->getI18nPrefix(); $this->i18nPrefix = $this->getI18nPrefix();
} }
@ -97,6 +99,7 @@ final class AccountCreateController extends AbstractController
return $this->redirectToRoute('app_login'); return $this->redirectToRoute('app_login');
} }
$configuration = $this->configurationRepository->getInstanceConfigurationOrCreate();
// nominal case: user found and token not expired // nominal case: user found and token not expired
$form = $this->createForm(AccountCreateStep2FormType::class, $user->setStep2Defaults())->handleRequest($request); $form = $this->createForm(AccountCreateStep2FormType::class, $user->setStep2Defaults())->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
@ -110,13 +113,26 @@ final class AccountCreateController extends AbstractController
// page group. // page group.
$group = $user->getMyGroupsAsInvited()->first(); $group = $user->getMyGroupsAsInvited()->first();
if ($group !== false) { if ($group !== false) {
$this->addFlashSuccess($this->i18nPrefix.'.step2.with_invitation.flash.success'); // If platform needs payment, redirect to payment
if ($configuration->getPaidMembership()) {
$successMessage = $this->i18nPrefix.'.step2.with_invitation.global_paid_membership.flash.success';
$this->addFlashSuccess($successMessage);
return $this->redirectToRoute('redirect_to_payment');
}
$successMessage = $this->i18nPrefix.'.step2.with_invitation.flash.success';
$this->addFlashSuccess($successMessage);
return $this->redirectToRoute('app_group_show_logged', $group->getRoutingParameters()); return $this->redirectToRoute('app_group_show_logged', $group->getRoutingParameters());
} }
if ($configuration->getPaidMembership()) {
$successMessage = $this->i18nPrefix.'.step2.global_paid_membership.flash.success';
} else {
$successMessage = $this->i18nPrefix.'.step2.flash.success';
}
// otherwise go to the address form // otherwise go to the address form
$this->addFlashSuccess($this->i18nPrefix.'.step2.flash.success'); $this->addFlashSuccess($successMessage);
return $this->redirectToRoute(MyAccountAction::ROUTE); return $this->redirectToRoute(MyAccountAction::ROUTE);
} }

View file

@ -45,7 +45,7 @@ final class AddressController extends AbstractController
/** /**
* @see UserAddressQueryHandler * @see UserAddressQueryHandler
*/ */
#[isGranted(User::ROLE_USER)] #[IsGranted(User::ROLE_USER)]
#[Route(path: [ #[Route(path: [
'en' => MyAccountAction::BASE_URL_EN.'/my-address/step-1', 'en' => MyAccountAction::BASE_URL_EN.'/my-address/step-1',
'fr' => MyAccountAction::BASE_URL_FR.'/mon-adresse/etape-1', 'fr' => MyAccountAction::BASE_URL_FR.'/mon-adresse/etape-1',
@ -75,7 +75,7 @@ final class AddressController extends AbstractController
return $this->render('pages/account/address/step1.html.twig', compact('form')); return $this->render('pages/account/address/step1.html.twig', compact('form'));
} }
#[isGranted(User::ROLE_USER)] #[IsGranted(User::ROLE_USER)]
#[Route(path: [ #[Route(path: [
'en' => MyAccountAction::BASE_URL_EN.'/my-address/step-2', 'en' => MyAccountAction::BASE_URL_EN.'/my-address/step-2',
'fr' => MyAccountAction::BASE_URL_FR.'/mon-adresse/etape-2', 'fr' => MyAccountAction::BASE_URL_FR.'/mon-adresse/etape-2',

View file

@ -29,7 +29,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* @see UserGroupControllerTest * @see UserGroupControllerTest
*/ */
#[isGranted(User::ROLE_USER)] #[IsGranted(User::ROLE_USER)]
final class UserGroupController extends AbstractController final class UserGroupController extends AbstractController
{ {
use SecurityTrait; use SecurityTrait;
@ -39,7 +39,7 @@ final class UserGroupController extends AbstractController
public function __construct( public function __construct(
private readonly QueryBus $queryBus, private readonly QueryBus $queryBus,
private readonly CommandBus $commandBus private readonly CommandBus $commandBus,
) { ) {
} }

View file

@ -32,7 +32,7 @@ final class MyAccountAction extends AbstractController
) { ) {
} }
#[isGranted(User::ROLE_USER)] #[IsGranted(User::ROLE_USER)]
#[Route(path: [ #[Route(path: [
'en' => self::BASE_URL_EN, 'en' => self::BASE_URL_EN,
'fr' => self::BASE_URL_FR, 'fr' => self::BASE_URL_FR,

View file

@ -35,9 +35,9 @@ final class DeleteProductUnavailabilityAction extends AbstractController
} }
#[Route(path: [ #[Route(path: [
'en' => MyAccountAction::BASE_URL_EN.'/products/unavailability/{id}/delete', 'en' => MyAccountAction::BASE_URL_EN.'/products/unavailability/{id}/delete',
'fr' => MyAccountAction::BASE_URL_FR.'/produits/indisponibilite/{id}/supprimer', 'fr' => MyAccountAction::BASE_URL_FR.'/produits/indisponibilite/{id}/supprimer',
], ],
name: 'app_user_product_delete_availability', name: 'app_user_product_delete_availability',
requirements: [ requirements: [
'id' => Requirement::UUID_V6, 'id' => Requirement::UUID_V6,

View file

@ -35,13 +35,13 @@ final class ProductAvailabilityController extends AbstractController
public const ROUTE = 'app_user_product_availabilities'; public const ROUTE = 'app_user_product_availabilities';
public function __construct( public function __construct(
public readonly ProductRepository $productRepository, public readonly ProductRepository $productRepository,
private readonly QueryBus $queryBus, private readonly QueryBus $queryBus,
private readonly CommandBus $commandBus, private readonly CommandBus $commandBus,
) { ) {
} }
#[isGranted(User::ROLE_USER)] #[IsGranted(User::ROLE_USER)]
#[Route(path: [ #[Route(path: [
'en' => MyAccountAction::BASE_URL_EN.'/my-products/{id}/availabilities', 'en' => MyAccountAction::BASE_URL_EN.'/my-products/{id}/availabilities',
'fr' => MyAccountAction::BASE_URL_FR.'/mes-produits/{id}/disponibilites', 'fr' => MyAccountAction::BASE_URL_FR.'/mes-produits/{id}/disponibilites',

View file

@ -9,6 +9,7 @@ use App\Controller\User\MyAccountAction;
use App\Doctrine\Manager\ProductManager; use App\Doctrine\Manager\ProductManager;
use App\Entity\Product; use App\Entity\Product;
use App\Entity\User; use App\Entity\User;
use App\Enum\Product\ProductVisibility;
use App\Form\Type\Product\ServiceFormType; use App\Form\Type\Product\ServiceFormType;
use App\MessageBus\QueryBus; use App\MessageBus\QueryBus;
use App\Repository\ConfigurationRepository; use App\Repository\ConfigurationRepository;
@ -56,6 +57,7 @@ final class ServiceController extends AbstractController
{ {
if ($this->configurationRepository->getServicesParameter()) { if ($this->configurationRepository->getServicesParameter()) {
$product = $this->productManager->initService($user); $product = $this->productManager->initService($user);
$product->setVisibility(ProductVisibility::RESTRICTED);
$form = $this->getForm($product, $request); $form = $this->getForm($product, $request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
/** @var array<UploadedFile>|null $images */ /** @var array<UploadedFile>|null $images */

View file

@ -30,7 +30,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* @see UserProductsControllerTest * @see UserProductsControllerTest
*/ */
#[isGranted(User::ROLE_USER)] #[IsGranted(User::ROLE_USER)]
#[Route(name: 'app_user_')] #[Route(name: 'app_user_')]
final class UserProductsController extends AbstractController final class UserProductsController extends AbstractController
{ {
@ -49,7 +49,7 @@ final class UserProductsController extends AbstractController
/** /**
* @implements PaginationInterface<int,Product> * @implements PaginationInterface<int,Product>
* *
* @return PaginationInterface<int,Product> * @return PaginationInterface<int,mixed>
*/ */
private function paginate(Query $query, int $page): PaginationInterface private function paginate(Query $query, int $page): PaginationInterface
{ {

View file

@ -27,7 +27,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
/** /**
* @see ConversationControllerTest * @see ConversationControllerTest
*/ */
#[isGranted(User::ROLE_USER)] #[IsGranted(User::ROLE_USER)]
class ConversationController extends AbstractController class ConversationController extends AbstractController
{ {
use FlashTrait; use FlashTrait;

View file

@ -37,7 +37,7 @@ final class MyLendingsAction extends AbstractController
) { ) {
} }
#[isGranted(User::ROLE_USER)] #[IsGranted(User::ROLE_USER)]
#[Route(path: [ #[Route(path: [
'en' => MyAccountAction::BASE_URL_EN.'/my-lendings', 'en' => MyAccountAction::BASE_URL_EN.'/my-lendings',
'fr' => MyAccountAction::BASE_URL_FR.'/mes-prets', 'fr' => MyAccountAction::BASE_URL_FR.'/mes-prets',

View file

@ -35,7 +35,7 @@ class MyLoansAction extends AbstractController
) { ) {
} }
#[isGranted(User::ROLE_USER)] #[IsGranted(User::ROLE_USER)]
#[Route(path: [ #[Route(path: [
'en' => MyAccountAction::BASE_URL_EN.'/my-loans', 'en' => MyAccountAction::BASE_URL_EN.'/my-loans',
'fr' => MyAccountAction::BASE_URL_FR.'/mes-emprunts', 'fr' => MyAccountAction::BASE_URL_FR.'/mes-emprunts',

View file

@ -29,7 +29,7 @@ use Symfony\Component\Uid\Uuid;
/** /**
* @see ServiceRequestControllerTest * @see ServiceRequestControllerTest
*/ */
#[isGranted(User::ROLE_USER)] #[IsGranted(User::ROLE_USER)]
class ServiceRequestController extends AbstractController class ServiceRequestController extends AbstractController
{ {
use FlashTrait; use FlashTrait;

View file

@ -29,7 +29,7 @@ use function Symfony\Component\String\u;
/** /**
* @see ServiceRequestStatusWorkflowControllerTest * @see ServiceRequestStatusWorkflowControllerTest
*/ */
#[isGranted(User::ROLE_USER)] #[IsGranted(User::ROLE_USER)]
class ServiceRequestStatusWorkflowController extends AbstractController class ServiceRequestStatusWorkflowController extends AbstractController
{ {
use FlashTrait; use FlashTrait;

View file

@ -14,7 +14,7 @@ trait i18nTrait
* Get the i18n prefix of a given class to have a consistent key naming in i18n * Get the i18n prefix of a given class to have a consistent key naming in i18n
* files. * files.
*/ */
public function getI18nPrefix(string $class = null): string public function getI18nPrefix(?string $class = null): string
{ {
$class = $class ?? $this::class; $class = $class ?? $this::class;

View file

@ -11,7 +11,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
final class ValidationProcessor implements ProcessorInterface final class ValidationProcessor implements ProcessorInterface
{ {
public function __construct( public function __construct(
private readonly ValidatorInterface $validator private readonly ValidatorInterface $validator,
) { ) {
} }
@ -20,7 +20,7 @@ final class ValidationProcessor implements ProcessorInterface
/** @var ConstraintViolationList $violations */ /** @var ConstraintViolationList $violations */
$violations = $this->validator->validate($object); $violations = $this->validator->validate($object);
if ($violations->count() > 0) { if ($violations->count() > 0) {
$message = sprintf("Error when validating fixture \"%s\", violation(s) detected:\n%s", $id, $violations); $message = \sprintf("Error when validating fixture \"%s\", violation(s) detected:\n%s", $id, $violations);
throw new \DomainException($message); throw new \DomainException($message);
} }
} }

View file

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace App\Doctrine\Behavior;
use App\Enum\OfferType;
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\MappedSuperclass]
abstract class AbstractOfferEntity implements \Stringable
{
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)]
protected Uuid $id;
/**
* Short name of the offer.
*/
#[ORM\Column(type: Types::STRING, length: 255, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(max: 255)]
protected string $name;
/**
* Type of offer.
*/
#[ORM\Column(name: 'type', type: 'string', nullable: false, enumType: OfferType::class)]
#[Assert\NotBlank]
protected OfferType $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 used to deactivate offers
* for some time.
*/
#[ORM\Column(type: 'boolean', nullable: false)]
protected bool $active = true;
public function __toString(): string
{
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 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;
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Doctrine\Listener;
use App\Entity\User;
use App\Repository\ConfigurationRepository;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\Routing\RouterInterface;
#[AsEventListener(event: ExceptionEvent::class, method: 'onKernelException')]
final class MembershipPaidListener
{
public function __construct(
private readonly ConfigurationRepository $configurationRepository,
private readonly Security $security,
private readonly RouterInterface $router,
) {
}
public function onKernelException(ExceptionEvent $event): void
{
$user = $this->security->getUser();
if (!$user instanceof User) {
return;
}
$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')));
}
}
}

View file

@ -19,7 +19,7 @@ use function Symfony\Component\String\u;
final class UserListener final class UserListener
{ {
public function __construct( public function __construct(
private readonly UserManager $userManager private readonly UserManager $userManager,
) { ) {
} }

View file

@ -43,7 +43,7 @@ final class MessageManager
ServiceRequest $serviceRequest, ServiceRequest $serviceRequest,
string $messageTemplate, string $messageTemplate,
array $messageParameters = [], array $messageParameters = [],
\DateTimeImmutable $createdAt = null, ?\DateTimeImmutable $createdAt = null,
): Message { ): Message {
$message = (new Message()) $message = (new Message())
->setServiceRequest($serviceRequest) ->setServiceRequest($serviceRequest)
@ -61,7 +61,7 @@ final class MessageManager
return $message; return $message;
} }
public function createFromRecipientMessage(ServiceRequest $serviceRequest, string $message, \DateTimeImmutable $createdAt = null): Message public function createFromRecipientMessage(ServiceRequest $serviceRequest, string $message, ?\DateTimeImmutable $createdAt = null): Message
{ {
$message = (new Message()) $message = (new Message())
->setServiceRequest($serviceRequest) ->setServiceRequest($serviceRequest)

View file

@ -66,7 +66,7 @@ class ProductManager
try { try {
$this->productStorage->delete($image); $this->productStorage->delete($image);
} catch (FilesystemException $e) { } catch (FilesystemException $e) {
$this->logger->warning(sprintf('Unable to delete product (%s) image %s: %s', $product->getId(), $image, $e->getMessage())); $this->logger->warning(\sprintf('Unable to delete product (%s) image %s: %s', $product->getId(), $image, $e->getMessage()));
} }
$product->deleteImage($image); $product->deleteImage($image);

View file

@ -186,7 +186,7 @@ final class UserManager
try { try {
$this->userStorage->delete((string) $user->getAvatar()); $this->userStorage->delete((string) $user->getAvatar());
} catch (FilesystemException $e) { } catch (FilesystemException $e) {
$this->logger->warning(sprintf('Unable to avatar of user (%s) image %s: %s', $user->getId(), $user->getAvatar(), $e->getMessage())); $this->logger->warning(\sprintf('Unable to avatar of user (%s) image %s: %s', $user->getId(), $user->getAvatar(), $e->getMessage()));
} }
$user->deleteAvatar(); $user->deleteAvatar();
$this->save($user, true); $this->save($user, true);

View file

@ -14,7 +14,7 @@ use App\Form\Type\Product\SearchFormType;
*/ */
final class Search final class Search
{ {
public function __construct(string $q, int $page = 1, User $user = null) public function __construct(string $q, int $page = 1, ?User $user = null)
{ {
$this->q = $q; $this->q = $q;
$this->page = $page; $this->page = $page;
@ -78,9 +78,9 @@ final class Search
*/ */
public function hasProximity(): bool public function hasProximity(): bool
{ {
return $this->hasCity() && return $this->hasCity()
($this->city?->hasLocality() ?? false) && && ($this->city?->hasLocality() ?? false)
$this->hasDistance() && $this->hasDistance()
; ;
} }

View file

@ -15,7 +15,7 @@ trait FieldTrait
/** /**
* Render the boolean without switch: . * Render the boolean without switch: .
*/ */
public function getSimpleBooleanField(string $propertyName, string $label = null): BooleanField public function getSimpleBooleanField(string $propertyName, ?string $label = null): BooleanField
{ {
return BooleanField::new($propertyName, $label) return BooleanField::new($propertyName, $label)
->renderAsSwitch(false) ->renderAsSwitch(false)
@ -30,6 +30,7 @@ trait FieldTrait
return [ return [
'information' => FormField::addPanel('panel.information', 'fas fa-info-circle'), 'information' => FormField::addPanel('panel.information', 'fas fa-info-circle'),
'tech_information' => FormField::addPanel('panel.tech_information', 'fas fa-history'), 'tech_information' => FormField::addPanel('panel.tech_information', 'fas fa-history'),
'payment_information' => FormField::addPanel('panel.payment_information', 'fas fa-dollar-sign'),
]; ];
} }
} }

View file

@ -18,7 +18,7 @@ final class EnumFilter implements FilterInterface
{ {
use FilterTrait; use FilterTrait;
public static function new(string $propertyName, string $formType, string $label = null): self public static function new(string $propertyName, string $formType, ?string $label = null): self
{ {
return (new self()) return (new self())
->setFilterFqcn(__CLASS__) ->setFilterFqcn(__CLASS__)
@ -32,7 +32,7 @@ final class EnumFilter implements FilterInterface
*/ */
public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void
{ {
$queryBuilder->andWhere(sprintf('%s.%s = :value', $filterDataDto->getEntityAlias(), $filterDataDto->getProperty())) $queryBuilder->andWhere(\sprintf('%s.%s = :value', $filterDataDto->getEntityAlias(), $filterDataDto->getProperty()))
->setParameter('value', $filterDataDto->getValue()); ->setParameter('value', $filterDataDto->getValue());
} }
} }

View file

@ -20,7 +20,7 @@ final class GroupFilter implements FilterInterface
{ {
use FilterTrait; use FilterTrait;
public static function new(string $propertyName, string $label = null): self public static function new(string $propertyName, ?string $label = null): self
{ {
return (new self()) return (new self())
->setFilterFqcn(__CLASS__) ->setFilterFqcn(__CLASS__)
@ -37,7 +37,7 @@ final class GroupFilter implements FilterInterface
/** @var Group $group */ /** @var Group $group */
$group = $filterDataDto->getValue(); $group = $filterDataDto->getValue();
$queryBuilder $queryBuilder
->innerJoin(sprintf('%s.userGroups', $filterDataDto->getEntityAlias()), 'ug') ->innerJoin(\sprintf('%s.userGroups', $filterDataDto->getEntityAlias()), 'ug')
->andWhere('ug.group = :group') ->andWhere('ug.group = :group')
->setParameter(':group', $group->getId()) ->setParameter(':group', $group->getId())
; ;

View file

@ -20,7 +20,7 @@ final class MyUsersFilter implements FilterInterface
{ {
use FilterTrait; use FilterTrait;
public static function new(string $propertyName, string $label = null): self public static function new(string $propertyName, ?string $label = null): self
{ {
return (new self()) return (new self())
->setFilterFqcn(__CLASS__) ->setFilterFqcn(__CLASS__)

View file

@ -20,7 +20,7 @@ final class MyGroupFilter implements FilterInterface
{ {
use FilterTrait; use FilterTrait;
public static function new(string $propertyName, string $label = null): self public static function new(string $propertyName, ?string $label = null): self
{ {
return (new self()) return (new self())
->setFilterFqcn(__CLASS__) ->setFilterFqcn(__CLASS__)

View file

@ -22,7 +22,7 @@ final class UuidFilter implements FilterInterface
{ {
use FilterTrait; use FilterTrait;
public static function new(string $propertyName, string $label = null): self public static function new(string $propertyName, ?string $label = null): self
{ {
return (new self()) return (new self())
->setFilterFqcn(__CLASS__) ->setFilterFqcn(__CLASS__)
@ -37,7 +37,7 @@ final class UuidFilter implements FilterInterface
*/ */
public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void
{ {
$queryBuilder->andWhere(sprintf('%s.%s = :value', $filterDataDto->getEntityAlias(), $filterDataDto->getProperty())) $queryBuilder->andWhere(\sprintf('%s.%s = :value', $filterDataDto->getEntityAlias(), $filterDataDto->getProperty()))
->setParameter('value', $filterDataDto->getValue()); ->setParameter('value', $filterDataDto->getValue());
} }
} }

View file

@ -21,7 +21,7 @@ class GroupType extends AbstractType
{ {
public function __construct( public function __construct(
private readonly Security $security, private readonly Security $security,
private readonly AuthorizationChecker $authorizationChecker private readonly AuthorizationChecker $authorizationChecker,
) { ) {
} }

View file

@ -5,20 +5,20 @@ declare(strict_types=1);
namespace App\EasyAdmin\Form\Type; namespace App\EasyAdmin\Form\Type;
use App\Controller\Admin\DashboardController; use App\Controller\Admin\DashboardController;
use App\Enum\Group\GroupOfferType; use App\Enum\OfferType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver; 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 public function configureOptions(OptionsResolver $resolver): void
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'choices' => GroupOfferType::getAsArray(), 'choices' => OfferType::getAsArray(),
'translation_domain' => DashboardController::DOMAIN, 'translation_domain' => DashboardController::DOMAIN,
]); ]);
} }

View file

@ -20,7 +20,7 @@ class UserType extends AbstractType
{ {
public function __construct( public function __construct(
private readonly Security $security, private readonly Security $security,
private readonly AuthorizationChecker $authorizationChecker private readonly AuthorizationChecker $authorizationChecker,
) { ) {
} }

View file

@ -45,7 +45,7 @@ class Address implements \Stringable
#[ORM\Column(type: Types::STRING, length: 255, nullable: false)] #[ORM\Column(type: Types::STRING, length: 255, nullable: false)]
#[Assert\NotBlank] #[Assert\NotBlank]
#[Assert\Length(max: 255)] #[Assert\Length(max: 255)]
private string $address; private ?string $address = null;
/** /**
* Additional information for the address, eg: APT 555. * Additional information for the address, eg: APT 555.
@ -90,7 +90,7 @@ class Address implements \Stringable
#[ORM\Column(type: Types::STRING, length: 255, nullable: false)] #[ORM\Column(type: Types::STRING, length: 255, nullable: false)]
#[Assert\NotBlank] #[Assert\NotBlank]
#[Assert\Length(max: 255)] #[Assert\Length(max: 255)]
private string $locality = ''; private ?string $locality = '';
/** /**
* Postal code, eg: "59160". * Postal code, eg: "59160".
@ -98,7 +98,7 @@ class Address implements \Stringable
#[ORM\Column(type: Types::STRING, length: 10, nullable: false)] #[ORM\Column(type: Types::STRING, length: 10, nullable: false)]
#[Assert\NotBlank] #[Assert\NotBlank]
#[Assert\Length(max: 20)] #[Assert\Length(max: 20)]
private string $postalCode; private ?string $postalCode = null;
/** /**
* ISO code of the country, eg: "FR". * ISO code of the country, eg: "FR".
@ -108,7 +108,7 @@ class Address implements \Stringable
#[ORM\Column(type: Types::STRING, length: 2, nullable: false)] #[ORM\Column(type: Types::STRING, length: 2, nullable: false)]
#[Assert\NotBlank] #[Assert\NotBlank]
#[Assert\Country] #[Assert\Country]
private string $country; private ?string $country = null;
/** /**
* Latitude of the address (north/south), eg: "50.6322562". * Latitude of the address (north/south), eg: "50.6322562".
@ -148,7 +148,7 @@ class Address implements \Stringable
* OpenStreetMap identifier, usefull to create link to maps. * OpenStreetMap identifier, usefull to create link to maps.
*/ */
#[ORM\Column(type: Types::BIGINT, nullable: true)] #[ORM\Column(type: Types::BIGINT, nullable: true)]
private int $osmId; private string $osmId;
public function __toString(): string public function __toString(): string
{ {
@ -167,12 +167,12 @@ class Address implements \Stringable
return $this; return $this;
} }
public function getAddress(): string public function getAddress(): ?string
{ {
return $this->address; return $this->address;
} }
public function setAddress(string $address): self public function setAddress(?string $address): self
{ {
$this->address = $address; $this->address = $address;
@ -227,7 +227,7 @@ class Address implements \Stringable
return $this; return $this;
} }
public function getLocality(): string public function getLocality(): ?string
{ {
return $this->locality; return $this->locality;
} }
@ -237,7 +237,7 @@ class Address implements \Stringable
return $this->locality !== ''; return $this->locality !== '';
} }
public function setLocality(string $locality): self public function setLocality(?string $locality): self
{ {
$this->locality = $locality; $this->locality = $locality;
@ -256,24 +256,24 @@ class Address implements \Stringable
return $this; return $this;
} }
public function getPostalCode(): string public function getPostalCode(): ?string
{ {
return $this->postalCode; return $this->postalCode;
} }
public function setPostalCode(string $postalCode): self public function setPostalCode(?string $postalCode): self
{ {
$this->postalCode = $postalCode; $this->postalCode = $postalCode;
return $this; return $this;
} }
public function getCountry(): string public function getCountry(): ?string
{ {
return $this->country; return $this->country;
} }
public function setCountry(string $country): self public function setCountry(?string $country): self
{ {
$this->country = $country; $this->country = $country;
@ -340,12 +340,12 @@ class Address implements \Stringable
return $this; return $this;
} }
public function getOsmId(): int public function getOsmId(): string
{ {
return $this->osmId; return $this->osmId;
} }
public function setOsmId(int $osmId): Address public function setOsmId(string $osmId): Address
{ {
$this->osmId = $osmId; $this->osmId = $osmId;
@ -378,7 +378,7 @@ class Address implements \Stringable
return $this; return $this;
} }
public function getSubAndLocality(): string public function getSubAndLocality(): ?string
{ {
if (u($this->subLocality)->isEmpty()) { if (u($this->subLocality)->isEmpty()) {
return $this->locality; return $this->locality;

Some files were not shown because too many files have changed in this diff Show more