fix: platform membership fixes (#740)
This commit is contained in:
parent
97a839c491
commit
eea58804cd
43 changed files with 1668 additions and 35 deletions
|
|
@ -35,6 +35,10 @@ App\Entity\User:
|
|||
devAccount: true
|
||||
address: '@address_loic'
|
||||
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):
|
||||
id: <uuid('1ed69804-eeb9-6c32-990b-632c3a6846ba')>
|
||||
|
|
@ -42,8 +46,11 @@ App\Entity\User:
|
|||
firstname: 'Kevin'
|
||||
lastname: 'Pirouet'
|
||||
avatar: '7c732ddb-9c13-45eb-aea0-e614f2340e6d.jpg'
|
||||
membershipPaid: true
|
||||
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):
|
||||
|
|
|
|||
148
helm/chart/templates/cronjobs/cronjobEndPlatformMembership.yaml
Normal file
148
helm/chart/templates/cronjobs/cronjobEndPlatformMembership.yaml
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
{{- if .Values.dailyCronjobs.enabled }}
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: {{ include "plateforme-ebs" . }}-cronjob-end-membership
|
||||
labels:
|
||||
{{- include "plateforme-ebs.labels" . | nindent 4 }}
|
||||
spec:
|
||||
schedule: '15 1 * * *'
|
||||
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-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: 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: 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 }}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
{{- if .Values.dailyCronjobs.enabled }}
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: {{ include "plateforme-ebs" . }}-cronjob-notify-ms-e-1
|
||||
labels:
|
||||
{{- include "plateforme-ebs.labels" . | nindent 4 }}
|
||||
spec:
|
||||
schedule: '20 2 * * *'
|
||||
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-ms-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: 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: 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 }}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
{{- if .Values.dailyCronjobs.enabled }}
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: {{ include "plateforme-ebs" . }}-cronjob-notify-ms-e-7
|
||||
labels:
|
||||
{{- include "plateforme-ebs.labels" . | nindent 4 }}
|
||||
spec:
|
||||
schedule: '10 21 * * *'
|
||||
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-ms-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: 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: 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 }}
|
||||
99
src/Command/EndPlatformMembershipCommand.php
Normal file
99
src/Command/EndPlatformMembershipCommand.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?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(),
|
||||
));
|
||||
|
||||
$user->setMembershipPaid(false)
|
||||
->setEndAt(null)
|
||||
->setPayedAt(null)
|
||||
->setStartAt(null)
|
||||
->setPlatformOffer(null);
|
||||
|
||||
$this->userManager->save($user, true);
|
||||
|
||||
$this->appMailer->send(EndPlatformMembershipMail::class, compact('user', 'platform'));
|
||||
$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;
|
||||
}
|
||||
}
|
||||
95
src/Command/NotifyPlatformMembershipExpirationCommand.php
Normal file
95
src/Command/NotifyPlatformMembershipExpirationCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ use App\Flysystem\MediaManager;
|
|||
use App\Helper\CsvExporter;
|
||||
use App\Mailer\AppMailer;
|
||||
use App\Mailer\Email\Admin\PromoteToAdmin\PromoteToAdminEmail;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
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\BooleanField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\DateField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
|
||||
use EasyCorp\Bundle\EasyAdminBundle\Filter\DateTimeFilter;
|
||||
|
|
@ -89,6 +92,7 @@ abstract class AbstractUserCrudController extends AbstractCrudController impleme
|
|||
#[Autowire('%user_base_path%')]
|
||||
private readonly string $userBasePath,
|
||||
AppMailer $mailer,
|
||||
private readonly ConfigurationRepository $configurationRepository,
|
||||
) {
|
||||
$this->mailer = $mailer;
|
||||
}
|
||||
|
|
@ -355,6 +359,20 @@ abstract class AbstractUserCrudController extends AbstractCrudController impleme
|
|||
$vacationModeField = BooleanField::new('vacationMode');
|
||||
$addressField = AssociationField::new('address');
|
||||
$groupsCountField = AssociationField::new('userGroups')->setLabel('Groups number');
|
||||
$membershipPaidField = BooleanField::new('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(
|
||||
'idField',
|
||||
|
|
@ -378,6 +396,12 @@ abstract class AbstractUserCrudController extends AbstractCrudController impleme
|
|||
'vacationModeField',
|
||||
'addressField',
|
||||
'groupsCountField',
|
||||
'membershipPaidField',
|
||||
'startAt',
|
||||
'endAt',
|
||||
'expiresInField',
|
||||
'payedAt',
|
||||
'offerField',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -445,4 +469,9 @@ abstract class AbstractUserCrudController extends AbstractCrudController impleme
|
|||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
public function platformRequiresGlobalPayment(): bool
|
||||
{
|
||||
return $this->configurationRepository->getInstanceConfigurationOrCreate()->getPaidMembership();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,16 +48,27 @@ final class UserCrudController extends AbstractUserCrudController
|
|||
'vacationModeField' => $vacationModeField,
|
||||
'addressField' => $addressField,
|
||||
'groupsCountField' => $groupsCountField,
|
||||
'membershipPaidField' => $membershipPaidField,
|
||||
'startAt' => $startAt,
|
||||
'endAt' => $endAt,
|
||||
'expiresInField' => $expiresInField,
|
||||
'payedAt' => $payedAt,
|
||||
'offerField' => $offerField,
|
||||
] = $this->getFields($pageName);
|
||||
|
||||
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();
|
||||
|
||||
if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) {
|
||||
return [
|
||||
$editFields = [
|
||||
$panels['information'],
|
||||
$emailField,
|
||||
$firstNameField,
|
||||
|
|
@ -74,9 +85,21 @@ final class UserCrudController extends AbstractUserCrudController
|
|||
$enabledField,
|
||||
$emailConfirmedField,
|
||||
];
|
||||
if ($this->platformRequiresGlobalPayment()) {
|
||||
$editFields = array_merge($editFields, [
|
||||
$panels['payment_information'],
|
||||
$membershipPaidField,
|
||||
$offerField,
|
||||
$startAt,
|
||||
$endAt,
|
||||
$payedAt,
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
return $editFields;
|
||||
}
|
||||
|
||||
$showFields = [
|
||||
$panels['information'],
|
||||
$emailField,
|
||||
$firstNameField,
|
||||
|
|
@ -97,5 +120,17 @@ final class UserCrudController extends AbstractUserCrudController
|
|||
$updatedAt,
|
||||
$loginAt,
|
||||
];
|
||||
if ($this->platformRequiresGlobalPayment()) {
|
||||
$showFields = array_merge($showFields, [
|
||||
$panels['payment_information'],
|
||||
$membershipPaidField,
|
||||
$startAt,
|
||||
$endAt,
|
||||
$payedAt,
|
||||
$expiresInField,
|
||||
]);
|
||||
}
|
||||
|
||||
return $showFields;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ 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\Attribute\Route;
|
||||
use Symfony\Component\Routing\Requirement\Requirement;
|
||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ namespace App\Controller\Payment\PlatformMembership;
|
|||
|
||||
use App\Controller\FlashTrait;
|
||||
use App\Controller\i18nTrait;
|
||||
use App\Doctrine\Manager\UserManager;
|
||||
use App\Entity\PaymentToken;
|
||||
use App\Entity\PlatformOffer;
|
||||
use App\Entity\User;
|
||||
use Carbon\CarbonImmutable;
|
||||
use App\Message\Command\Payment\PlatformMembershipPaidCommand;
|
||||
use App\MessageBus\CommandBusInterface;
|
||||
use Payum\Core\Payum;
|
||||
use Payum\Core\Request\GetHumanStatus;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
|
@ -19,7 +19,7 @@ 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\Attribute\Route;
|
||||
use Symfony\Component\Routing\Requirement\Requirement;
|
||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
|
@ -33,6 +33,14 @@ final class DoneAction extends AbstractController
|
|||
|
||||
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
|
||||
*/
|
||||
|
|
@ -41,41 +49,37 @@ final class DoneAction extends AbstractController
|
|||
name: self::ROUTE_NAME,
|
||||
requirements: ['id' => Requirement::UUID_V6],
|
||||
)]
|
||||
public function __invoke(Request $request, #[MapEntity(expr: 'repository.findOneActive(id)')] PlatformOffer $platformOffer, #[CurrentUser] User $user, Payum $payum, TranslatorInterface $translator, UserManager $userManager, LoggerInterface $logger): Response
|
||||
public function __invoke(Request $request, #[MapEntity(expr: 'repository.findOneActive(id)')] PlatformOffer $platformOffer, #[CurrentUser] User $user): Response
|
||||
{
|
||||
try {
|
||||
/** @var PaymentToken $token */
|
||||
$token = $payum->getHttpRequestVerifier()->verify($request);
|
||||
$token = $this->payum->getHttpRequestVerifier()->verify($request);
|
||||
} catch (\Exception $e) {
|
||||
$logger->error($e->getMessage());
|
||||
$this->logger->error($e->getMessage());
|
||||
throw new UnprocessableEntityHttpException('Cannot verify Payum token.');
|
||||
}
|
||||
|
||||
$gateway = $payum->getGateway($token->getGatewayName());
|
||||
$status = new GetHumanStatus($token);
|
||||
$gateway->execute($status);
|
||||
/** @var GetHumanStatus $status */
|
||||
$status = $this->commandBus->dispatch(new PlatformMembershipPaidCommand($platformOffer->getId(), $user->getId(), $token));
|
||||
|
||||
// Not captured
|
||||
if (!$status->isCaptured()) {
|
||||
$this->addFlashWarning($translator->trans($this->getI18nPrefix().'.status.'.$status->getValue()));
|
||||
$this->addFlashWarning($this->translator->trans($this->getI18nPrefix().'.status.'.$status->getValue()));
|
||||
|
||||
return $this->redirectToRoute('app_user_my_account');
|
||||
}
|
||||
|
||||
$user
|
||||
->setMembershipPaid(true)
|
||||
->setStartAt(CarbonImmutable::today())
|
||||
->setPayedAt(CarbonImmutable::now())
|
||||
;
|
||||
if (($offerType = $platformOffer->getType())->isRecurring()) {
|
||||
$user->setEndAt(new CarbonImmutable($offerType->getEndAtInterval()));
|
||||
}
|
||||
|
||||
$userManager->save($user, true);
|
||||
|
||||
$this->addFlashSuccess($translator->trans($this->getI18nPrefix().'.flash.success', [
|
||||
$this->addFlashSuccess($this->translator->trans($this->getI18nPrefix().'.flash.success', [
|
||||
'%platform%' => $platformOffer->getConfiguration()?->getPlatformName()],
|
||||
));
|
||||
|
||||
$request->getSession()->remove('payment_in_progress');
|
||||
|
||||
$group = $user->getMyGroupsAsInvited()->first();
|
||||
if ($group !== false) {
|
||||
return $this->redirectToRoute('app_group_show_logged', $group->getRoutingParameters());
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_user_my_account');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ use App\Message\Query\Security\GetUserByTokenQuery;
|
|||
use App\MessageBus\CommandBus;
|
||||
use App\MessageBus\QueryBus;
|
||||
use App\MessageHandler\Command\Security\AccountCreateStep1CommandHandler;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
|
@ -41,6 +42,7 @@ final class AccountCreateController extends AbstractController
|
|||
private readonly QueryBus $queryBus,
|
||||
private readonly CommandBus $commandBus,
|
||||
private readonly Security $security,
|
||||
private readonly ConfigurationRepository $configurationRepository,
|
||||
) {
|
||||
$this->i18nPrefix = $this->getI18nPrefix();
|
||||
}
|
||||
|
|
@ -97,6 +99,7 @@ final class AccountCreateController extends AbstractController
|
|||
return $this->redirectToRoute('app_login');
|
||||
}
|
||||
|
||||
$configuration = $this->configurationRepository->getInstanceConfigurationOrCreate();
|
||||
// nominal case: user found and token not expired
|
||||
$form = $this->createForm(AccountCreateStep2FormType::class, $user->setStep2Defaults())->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
|
|
@ -110,13 +113,26 @@ final class AccountCreateController extends AbstractController
|
|||
// page group.
|
||||
$group = $user->getMyGroupsAsInvited()->first();
|
||||
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());
|
||||
}
|
||||
|
||||
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
|
||||
$this->addFlashSuccess($this->i18nPrefix.'.step2.flash.success');
|
||||
$this->addFlashSuccess($successMessage);
|
||||
|
||||
return $this->redirectToRoute(MyAccountAction::ROUTE);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ final class MembershipPaidListener
|
|||
|
||||
public function onKernelException(ExceptionEvent $event): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->security->getUser();
|
||||
if (!$user instanceof User) {
|
||||
return;
|
||||
}
|
||||
$config = $this->configurationRepository->getInstanceConfigurationOrCreate();
|
||||
$session = $event->getRequest()->getSession();
|
||||
/** @var bool $isPaymentInProgress */
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ trait FieldTrait
|
|||
return [
|
||||
'information' => FormField::addPanel('panel.information', 'fas fa-info-circle'),
|
||||
'tech_information' => FormField::addPanel('panel.tech_information', 'fas fa-history'),
|
||||
'payment_information' => FormField::addPanel('panel.payment_information', 'fas fa-dollar-sign'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ use App\Form\Type\User\ChangeLoginFormType;
|
|||
use App\Form\Type\User\ChangePasswordFormType;
|
||||
use App\Form\Type\User\EditProfileFormType;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Validator\Constraints\User\MembershipPaid;
|
||||
use Carbon\Carbon;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
|
|
@ -26,6 +28,7 @@ use libphonenumber\PhoneNumberUtil;
|
|||
use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber as AssertPhoneNumber;
|
||||
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Security\Core\User\EquatableInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\Validator\Constraints as SecurityAssert;
|
||||
|
|
@ -42,7 +45,8 @@ use function Symfony\Component\String\u;
|
|||
#[ORM\Table(name: '`user`')] // we also need escaping here
|
||||
#[ORM\EntityListeners([UserListener::class])]
|
||||
#[UniqueEntity('email', groups: [AccountCreateStep1FormType::class, ChangeLoginFormType::class, 'Default'])]
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface, ImageInterface
|
||||
#[MembershipPaid]
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface, ImageInterface, EquatableInterface
|
||||
{
|
||||
use UserConfirmationTrait;
|
||||
use UserLostPasswordTrait;
|
||||
|
|
@ -272,6 +276,10 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, ImageIn
|
|||
#[ORM\Column(type: 'boolean', nullable: false)]
|
||||
private bool $membershipPaid = false;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: PlatformOffer::class)]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'id', onDelete: 'SET NULL')]
|
||||
private ?PlatformOffer $platformOffer = null;
|
||||
|
||||
/**
|
||||
* Starting date of a paying membership. The starting date of a free membership
|
||||
* is stored in the creation date.
|
||||
|
|
@ -992,4 +1000,35 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, ImageIn
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function expiresIn(): ?int
|
||||
{
|
||||
$today = Carbon::today();
|
||||
if ($this->endAt === null || $this->endAt < $today) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$endAt = new Carbon($this->endAt);
|
||||
|
||||
return $today->diffInDays($endAt);
|
||||
}
|
||||
|
||||
public function getPlatformOffer(): ?PlatformOffer
|
||||
{
|
||||
return $this->platformOffer;
|
||||
}
|
||||
|
||||
public function setPlatformOffer(?PlatformOffer $platformOffer): void
|
||||
{
|
||||
$this->platformOffer = $platformOffer;
|
||||
}
|
||||
|
||||
public function isEqualTo(UserInterface $user): bool
|
||||
{
|
||||
if (!$user instanceof self) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->email === $user->getUserIdentifier() && $this->password === $user->getPassword();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
src/Mailer/Email/Command/EndPlatformMembershipMail.php
Normal file
50
src/Mailer/Email/Command/EndPlatformMembershipMail.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mailer\Email\Command;
|
||||
|
||||
use App\Controller\i18nTrait;
|
||||
use App\Entity\User;
|
||||
use App\Mailer\AppMailer;
|
||||
use App\Mailer\Email\EmailInterface;
|
||||
use App\Mailer\Email\EmailTrait;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class EndPlatformMembershipMail implements EmailInterface
|
||||
{
|
||||
use EmailTrait;
|
||||
use i18nTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly TranslatorInterface $translator,
|
||||
#[Autowire('%brand%')]
|
||||
private readonly string $brand,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function getEmail(array $context): TemplatedEmail
|
||||
{
|
||||
/** @var ?User $user */
|
||||
$user = $context['user'] ?? null;
|
||||
Assert::isInstanceOf($user, User::class);
|
||||
|
||||
return (new TemplatedEmail())
|
||||
->to($user->getEmail())
|
||||
->priority(Email::PRIORITY_HIGH)
|
||||
->subject($this->translator->trans($this->getI18nPrefix().'.subject', [
|
||||
'%brand%' => $this->brand,
|
||||
'%platform%' => $context['platform'],
|
||||
], AppMailer::TR_DOMAIN))
|
||||
->htmlTemplate('email/command/end_platform_membership.html.twig')
|
||||
->context($context)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mailer\Email\Command;
|
||||
|
||||
use App\Controller\i18nTrait;
|
||||
use App\Entity\User;
|
||||
use App\Mailer\AppMailer;
|
||||
use App\Mailer\Email\EmailInterface;
|
||||
use App\Mailer\Email\EmailTrait;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class NotifyPlatformMembershipExpirationMail implements EmailInterface
|
||||
{
|
||||
use EmailTrait;
|
||||
use i18nTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly TranslatorInterface $translator,
|
||||
#[Autowire('%brand%')]
|
||||
private readonly string $brand,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function getEmail(array $context): TemplatedEmail
|
||||
{
|
||||
/** @var ?User $user */
|
||||
$user = $context['user'] ?? null;
|
||||
Assert::isInstanceOf($user, User::class);
|
||||
|
||||
return (new TemplatedEmail())
|
||||
->to($user->getEmail())
|
||||
->priority(Email::PRIORITY_HIGH)
|
||||
->subject($this->translator->trans($this->getI18nPrefix().'.subject', [
|
||||
'%brand%' => $this->brand,
|
||||
'%days%' => $context['days'],
|
||||
'%platform%' => $context['platform'],
|
||||
], AppMailer::TR_DOMAIN))
|
||||
->htmlTemplate('email/command/notify_platform_membership_expiration.html.twig')
|
||||
->context($context)
|
||||
;
|
||||
}
|
||||
}
|
||||
50
src/Mailer/Email/Payment/PlatformMembershipPaidMail.php
Normal file
50
src/Mailer/Email/Payment/PlatformMembershipPaidMail.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mailer\Email\Payment;
|
||||
|
||||
use App\Controller\i18nTrait;
|
||||
use App\Entity\User;
|
||||
use App\Mailer\AppMailer;
|
||||
use App\Mailer\Email\EmailInterface;
|
||||
use App\Mailer\Email\EmailTrait;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class PlatformMembershipPaidMail implements EmailInterface
|
||||
{
|
||||
use EmailTrait;
|
||||
use i18nTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly TranslatorInterface $translator,
|
||||
#[Autowire('%brand%')]
|
||||
private readonly string $brand,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function getEmail(array $context): TemplatedEmail
|
||||
{
|
||||
/** @var ?User $user */
|
||||
$user = $context['user'] ?? null;
|
||||
Assert::isInstanceOf($user, User::class);
|
||||
|
||||
return (new TemplatedEmail())
|
||||
->to($user->getEmail())
|
||||
->priority(Email::PRIORITY_HIGH)
|
||||
->subject($this->translator->trans($this->getI18nPrefix().'.subject', [
|
||||
'%brand%' => $this->brand,
|
||||
'%platform%' => $context['platform'],
|
||||
], AppMailer::TR_DOMAIN))
|
||||
->htmlTemplate('email/payment/platform_membership_paid.html.twig')
|
||||
->context($context)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Message\Command\Payment;
|
||||
|
||||
use App\Entity\PaymentToken;
|
||||
use App\MessageHandler\Command\Payment\PlatformMembershipPaidCommandHandler;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
/**
|
||||
* @see PlatformMembershipPaidCommandHandler
|
||||
*/
|
||||
final class PlatformMembershipPaidCommand
|
||||
{
|
||||
public function __construct(
|
||||
public readonly Uuid $platformOfferId,
|
||||
public readonly Uuid $userId,
|
||||
public readonly PaymentToken $paymentToken,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\MessageHandler\Command\Payment;
|
||||
|
||||
use App\Doctrine\Manager\UserManager;
|
||||
use App\Mailer\AppMailer;
|
||||
use App\Mailer\Email\Payment\PlatformMembershipPaidMail;
|
||||
use App\Message\Command\Payment\PlatformMembershipPaidCommand;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
use App\Repository\PlatformOfferRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Payum\Core\Payum;
|
||||
use Payum\Core\Request\GetHumanStatus;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final class PlatformMembershipPaidCommandHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PlatformOfferRepository $platformOfferRepository,
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly UserManager $userManager,
|
||||
private readonly Payum $payum,
|
||||
private readonly AppMailer $mailer,
|
||||
private readonly ConfigurationRepository $configurationRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(PlatformMembershipPaidCommand $message): GetHumanStatus
|
||||
{
|
||||
$platformOffer = $this->platformOfferRepository->get($message->platformOfferId);
|
||||
$user = $this->userRepository->get($message->userId);
|
||||
|
||||
$gateway = $this->payum->getGateway($message->paymentToken->getGatewayName());
|
||||
$status = new GetHumanStatus($message->paymentToken);
|
||||
$gateway->execute($status);
|
||||
|
||||
// Not captured
|
||||
if (!$status->isCaptured()) {
|
||||
return $status;
|
||||
}
|
||||
|
||||
$user
|
||||
->setMembershipPaid(true)
|
||||
->setStartAt(CarbonImmutable::today())
|
||||
->setPayedAt(CarbonImmutable::now())
|
||||
->setPlatformOffer($platformOffer)
|
||||
;
|
||||
if (($offerType = $platformOffer->getType())->isRecurring()) {
|
||||
$user->setEndAt(new CarbonImmutable($offerType->getEndAtInterval()));
|
||||
}
|
||||
|
||||
$this->userManager->save($user, true);
|
||||
|
||||
// payment was captured and membership is saved so invalidate the token
|
||||
$this->payum->getHttpRequestVerifier()->invalidate($message->paymentToken);
|
||||
|
||||
// send confirmation email
|
||||
$configuration = $this->configurationRepository->getInstanceConfigurationOrCreate();
|
||||
$platform = $configuration->getPlatformName();
|
||||
$this->mailer->send(PlatformMembershipPaidMail::class, [
|
||||
'platform' => $platform,
|
||||
'user' => $user,
|
||||
]);
|
||||
|
||||
return $status;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,11 @@ namespace App\Repository;
|
|||
|
||||
use App\Entity\User;
|
||||
use App\Enum\User\UserType;
|
||||
use Carbon\Carbon;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
|
|
@ -147,4 +149,32 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
|
|||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function getExpiredMembership(): Query
|
||||
{
|
||||
$today = Carbon::today();
|
||||
$qb = $this
|
||||
->createQueryBuilder('u')
|
||||
->andWhere('u.endAt < :date')
|
||||
->setParameter('date', $today->format('Y-m-d'))
|
||||
;
|
||||
|
||||
return $qb->getQuery();
|
||||
}
|
||||
|
||||
public function getExpiring(int $days): Query
|
||||
{
|
||||
$from = new \DateTimeImmutable(\sprintf('+%d days midnight', $days));
|
||||
$to = $from->modify('+ 1 day'); // just add one day for the end limit
|
||||
|
||||
$qb = $this
|
||||
->createQueryBuilder('u')
|
||||
->andWhere('u.endAt >= :from')
|
||||
->andWhere('u.endAt < :to')
|
||||
->setParameter('from', $from->format('Y-m-d'))
|
||||
->setParameter('to', $to->format('Y-m-d'))
|
||||
;
|
||||
|
||||
return $qb->getQuery();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use App\Repository\MenuItemRepository;
|
|||
use App\Repository\MenuRepository;
|
||||
use App\Repository\MessageRepository;
|
||||
use App\Repository\PaymentRepository;
|
||||
use App\Repository\PlatformOfferRepository;
|
||||
use App\Repository\ProductAvailabilityRepository;
|
||||
use App\Repository\ProductRepository;
|
||||
use App\Repository\ServiceRequestRepository;
|
||||
|
|
@ -113,4 +114,9 @@ trait ContainerRepositoryTrait
|
|||
{
|
||||
return self::getContainer()->get(ServiceRequestRepository::class);
|
||||
}
|
||||
|
||||
public function getPlatformOfferRepository(): PlatformOfferRepository
|
||||
{
|
||||
return self::getContainer()->get(PlatformOfferRepository::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
src/Validator/Constraints/User/MembershipPaid.php
Normal file
18
src/Validator/Constraints/User/MembershipPaid.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Validator\Constraints\User;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
class MembershipPaid extends Constraint
|
||||
{
|
||||
public string $message = 'validator.user.membership_paid';
|
||||
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
58
src/Validator/Constraints/User/MembershipPaidValidator.php
Normal file
58
src/Validator/Constraints/User/MembershipPaidValidator.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Validator\Constraints\User;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Enum\OfferType;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||
|
||||
class MembershipPaidValidator extends ConstraintValidator
|
||||
{
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!$constraint instanceof MembershipPaid) {
|
||||
throw new UnexpectedTypeException($constraint, MembershipPaid::class);
|
||||
}
|
||||
|
||||
if (!$value instanceof User) {
|
||||
throw new UnexpectedValueException($value, User::class);
|
||||
}
|
||||
|
||||
if (!$value->isMembershipPaid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$platformOffer = $value->getPlatformOffer();
|
||||
if (null === $platformOffer) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->atPath('platformOffer')
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
if (null === $value->getStartAt()) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->atPath('startAt')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
match ($platformOffer->getType()) {
|
||||
OfferType::YEARLY, OfferType::MONTHLY => $this->checkEndAt($value, $constraint),
|
||||
OfferType::ONESHOT => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function checkEndAt(User $value, MembershipPaid $constraint): void
|
||||
{
|
||||
if (null === $value->getEndAt()) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->atPath('endAt')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
templates/email/command/end_platform_membership.html.twig
Normal file
11
templates/email/command/end_platform_membership.html.twig
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{% trans_default_domain 'email' %}
|
||||
|
||||
{% set i18n_prefix = _self|i18n_prefix %}
|
||||
|
||||
<p>{{ (i18n_prefix ~ '.h1')|trans }}</p>
|
||||
|
||||
<p>{{ (i18n_prefix ~ '.p1')|trans({'%endAt%': user.endAt, '%platform%': platform}) }}</p>
|
||||
|
||||
<a href="{{ url('app_login') }}">{{ (i18n_prefix ~ '.loginLink')|trans }}</a>
|
||||
|
||||
<p>{{ brand }}</p>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{% trans_default_domain 'email' %}
|
||||
|
||||
{% set i18n_prefix = _self|i18n_prefix %}
|
||||
|
||||
<p>{{ (i18n_prefix ~ '.h1')|trans }}</p>
|
||||
|
||||
<p>{{ (i18n_prefix ~ '.p1')|trans({'%platform%': platform, '%days%': days}) }}</p>
|
||||
|
||||
<p>{{ brand }}</p>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{% trans_default_domain 'email' %}
|
||||
|
||||
{% set i18n_prefix = _self|i18n_prefix %}
|
||||
|
||||
<p>{{ (i18n_prefix ~ '.h1')|trans }}</p>
|
||||
|
||||
<p>{{ (i18n_prefix ~ '.p1')|trans({'%startAt%': user.startAt, '%endAt%': user.endAt, '%platform%': platform}) }}</p>
|
||||
|
||||
<p>{{ brand }}</p>
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Integration\Command;
|
||||
|
||||
use App\Command\EndPlatformMembershipCommand;
|
||||
use App\Test\ContainerRepositoryTrait;
|
||||
use App\Test\ContainerTrait;
|
||||
use Hautelook\AliceBundle\PhpUnit\RefreshDatabaseTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
final class EndPlatformMembershipCommandTest extends KernelTestCase
|
||||
{
|
||||
use ContainerTrait;
|
||||
use ContainerRepositoryTrait;
|
||||
use RefreshDatabaseTrait;
|
||||
|
||||
public function testExecute(): void
|
||||
{
|
||||
$kernel = self::bootKernel();
|
||||
$this->fixDoctrineBug($kernel->getContainer());
|
||||
|
||||
// temporarily set global configuration as globalPaidMembership = true
|
||||
$configuration = $this->getConfigurationRepository()->getInstanceConfigurationOrCreate();
|
||||
$newConfig = $configuration->getConfiguration();
|
||||
$newConfig['global']['globalPaidMembership'] = true;
|
||||
$configuration->setConfiguration($newConfig);
|
||||
$this->getConfigurationRepository()->save($configuration, true);
|
||||
|
||||
$application = new Application($kernel);
|
||||
$command = $application->find(EndPlatformMembershipCommand::CMD);
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->execute([]);
|
||||
$commandTester->assertCommandIsSuccessful();
|
||||
self::assertEmailCount(1);
|
||||
self::assertNotificationCount(1);
|
||||
$output = $commandTester->getDisplay();
|
||||
self::assertStringContainsString(\sprintf('%d update', 1), $output);
|
||||
|
||||
// already deleted
|
||||
$commandTester->execute([]);
|
||||
$commandTester->assertCommandIsSuccessful();
|
||||
self::assertEmailCount(1); // not +1
|
||||
self::assertNotificationCount(1);
|
||||
$output = $commandTester->getDisplay();
|
||||
self::assertStringContainsString(\sprintf('%d update', 0), $output);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Integration\Command;
|
||||
|
||||
use App\Command\NotifyPlatformMembershipExpirationCommand;
|
||||
use App\Test\ContainerRepositoryTrait;
|
||||
use Hautelook\AliceBundle\PhpUnit\RefreshDatabaseTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
final class NotifyPlatformMembershipExpirationCommandTest extends KernelTestCase
|
||||
{
|
||||
use ContainerRepositoryTrait;
|
||||
use RefreshDatabaseTrait;
|
||||
|
||||
public function testExecute(): void
|
||||
{
|
||||
$kernel = self::bootKernel();
|
||||
// temporarily set global configuration as globalPaidMembership = true
|
||||
$configuration = $this->getConfigurationRepository()->getInstanceConfigurationOrCreate();
|
||||
$newConfig = $configuration->getConfiguration();
|
||||
$newConfig['global']['globalPaidMembership'] = true;
|
||||
$configuration->setConfiguration($newConfig);
|
||||
$this->getConfigurationRepository()->save($configuration, true);
|
||||
|
||||
$application = new Application($kernel);
|
||||
$command = $application->find(NotifyPlatformMembershipExpirationCommand::CMD);
|
||||
$commandTester = new CommandTester($command);
|
||||
|
||||
// in one week
|
||||
$commandTester->execute([
|
||||
'days' => 7,
|
||||
]);
|
||||
$commandTester->assertCommandIsSuccessful();
|
||||
$output = $commandTester->getDisplay();
|
||||
self::assertStringContainsString(\sprintf('%d notification', 1), $output);
|
||||
self::assertStringContainsString(\sprintf('notifying platform membership expiration for user %s', 'Kevin'), $output);
|
||||
self::assertEmailCount(1);
|
||||
self::assertNotificationCount(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Integration\MessageHandler\Payment;
|
||||
|
||||
use App\Entity\Payment;
|
||||
use App\Entity\PaymentToken;
|
||||
use App\Entity\User;
|
||||
use App\Message\Command\Payment\PlatformMembershipPaidCommand;
|
||||
use App\MessageHandler\Command\Payment\PlatformMembershipPaidCommandHandler;
|
||||
use App\Test\ContainerRepositoryTrait;
|
||||
use App\Tests\TestReference;
|
||||
use Hautelook\AliceBundle\PhpUnit\RefreshDatabaseTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
final class PlatformMembershipPaidCommandHandlerTest extends KernelTestCase
|
||||
{
|
||||
use RefreshDatabaseTrait;
|
||||
use ContainerRepositoryTrait;
|
||||
|
||||
/**
|
||||
* Just to test when the status is not "captured".
|
||||
*/
|
||||
public function testDoneStatusFailed(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$handler = self::getContainer()->get(PlatformMembershipPaidCommandHandler::class);
|
||||
self::assertInstanceOf(PlatformMembershipPaidCommandHandler::class, $handler);
|
||||
|
||||
$platformOffer = $this->getPlatformOfferRepository()->get(TestReference::PLATFORM_OFFER_1);
|
||||
$user = $this->getUserRepository()->get(TestReference::ADMIN_LOIC);
|
||||
|
||||
$message = new PlatformMembershipPaidCommand($platformOffer->getId(), $user->getId(), $this->getToken($user));
|
||||
$status = $handler($message);
|
||||
self::assertTrue($status->isNew());
|
||||
}
|
||||
|
||||
private function getToken(User $user): PaymentToken
|
||||
{
|
||||
$token = new PaymentToken();
|
||||
$token->setGatewayName('offline');
|
||||
$payment = new Payment();
|
||||
$payment->setUser($user);
|
||||
$token->setDetails($payment);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* No error 500 if the user is already a member.
|
||||
*/
|
||||
public function testDoneAlreadyMember(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$handler = self::getContainer()->get(PlatformMembershipPaidCommandHandler::class);
|
||||
self::assertInstanceOf(PlatformMembershipPaidCommandHandler::class, $handler);
|
||||
|
||||
$platformOffer = $this->getPlatformOfferRepository()->get(TestReference::PLATFORM_OFFER_1);
|
||||
$user = $this->getUserRepository()->get(TestReference::ADMIN_LOIC);
|
||||
$user->setMembershipPaid(false); // juste for the test
|
||||
$this->getUserManager()->save($user, true);
|
||||
$payment = $this->getPaymentRepository()->get(TestReference::PAYMENT_USER_16_1);
|
||||
$token = new PaymentToken();
|
||||
$token->setGatewayName('offline');
|
||||
$payment->setUser($user);
|
||||
$token->setDetails($payment);
|
||||
|
||||
$message = new PlatformMembershipPaidCommand($platformOffer->getId(), $user->getId(), $token);
|
||||
$status = $handler($message);
|
||||
self::assertTrue($status->isCaptured());
|
||||
self::assertEmailCount(1);
|
||||
self::assertTrue($user->isMembershipPaid());
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,6 @@ final class MeilisearchTest extends KernelTestCase
|
|||
$meilisearch->indexProducts([$object, $service]);
|
||||
$searchDto = new Search('vélo');
|
||||
$results = $meilisearch->search($searchDto);
|
||||
self::assertNotEmpty($results->getHitsCount());
|
||||
self::assertNotEmpty($results);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Controller\Payment;
|
||||
namespace App\Tests\Unit\Controller\Payment\Group;
|
||||
|
||||
use App\Controller\Payment\Group\DoneAction;
|
||||
use App\Entity\Group;
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Controller\Payment\PlatformMembership;
|
||||
|
||||
use App\Controller\Payment\PlatformMembership\DoneAction;
|
||||
use App\Entity\PaymentToken;
|
||||
use App\Entity\PlatformOffer;
|
||||
use App\Entity\User;
|
||||
use App\MessageBus\CommandBusInterface;
|
||||
use Payum\Core\Payum;
|
||||
use Payum\Core\Request\GetHumanStatus;
|
||||
use Payum\Core\Security\HttpRequestVerifierInterface;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag;
|
||||
use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class DoneActionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Too complicated, the controller should be refactored.
|
||||
*/
|
||||
public function testUnprocessableEntityHttpException(): void
|
||||
{
|
||||
$httpRequestVerifierInterface = $this->getMockBuilder(HttpRequestVerifierInterface::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$httpRequestVerifierInterface->method('verify')
|
||||
->willThrowException(new UnprocessableEntityHttpException());
|
||||
|
||||
$payum = $this->getMockBuilder(Payum::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$payum->method('getHttpRequestVerifier')
|
||||
->willReturn($httpRequestVerifierInterface);
|
||||
|
||||
$translator = $this->getMockBuilder(TranslatorInterface::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$doneAction = new DoneAction(
|
||||
$this->getCommandBus(),
|
||||
$payum,
|
||||
$translator,
|
||||
$this->getLogger(),
|
||||
);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Cannot verify Payum token');
|
||||
|
||||
$doneAction->__invoke(new Request(), $this->getPlatformOffer(), $this->getUser());
|
||||
}
|
||||
|
||||
/**
|
||||
* All this to test a line :/.
|
||||
*/
|
||||
public function testFlashWarning(): void
|
||||
{
|
||||
$httpRequestVerifierInterface = $this->getMockBuilder(HttpRequestVerifierInterface::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$httpRequestVerifierInterface->method('verify')
|
||||
->willReturn(new PaymentToken());
|
||||
|
||||
$payum = $this->getMockBuilder(Payum::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$payum->method('getHttpRequestVerifier')
|
||||
->willReturn($httpRequestVerifierInterface);
|
||||
|
||||
$translator = $this->getMockBuilder(TranslatorInterface::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$comandBus = $this->getCommandBus();
|
||||
$comandBus->method('dispatch')->willReturn(new GetHumanStatus(new PaymentToken()));
|
||||
|
||||
$doneAction = new DoneAction(
|
||||
$this->getCommandBus(),
|
||||
$payum,
|
||||
$translator,
|
||||
$this->getLogger(),
|
||||
);
|
||||
|
||||
// set session!
|
||||
$session = $this->getMockBuilder(FlashBagAwareSessionInterface::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$session->method('getFlashBag')->willReturn(new FlashBag());
|
||||
|
||||
$requesStack = $this->getMockBuilder(RequestStack::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$requesStack->method('getSession')->willReturn($session);
|
||||
$container = $this->getMockBuilder(ContainerInterface::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$container->method('get')->willReturn($requesStack);
|
||||
$doneAction->setContainer($container);
|
||||
|
||||
$this->expectException(\Error::class); // or more mock are needed. To clean up later
|
||||
|
||||
$doneAction->__invoke(new Request(), $this->getPlatformOffer(), $this->getUser());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CommandBusInterface&MockObject
|
||||
*/
|
||||
private function getCommandBus(): MockObject
|
||||
{
|
||||
return $this->getMockBuilder(CommandBusInterface::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
}
|
||||
|
||||
private function getLogger(): MockObject&LoggerInterface
|
||||
{
|
||||
return $this->getMockBuilder(LoggerInterface::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
}
|
||||
|
||||
private function getUser(): User
|
||||
{
|
||||
return (new User())
|
||||
->setId($this->getUuid());
|
||||
}
|
||||
|
||||
private function getUuid(): Uuid
|
||||
{
|
||||
return Uuid::v6();
|
||||
}
|
||||
|
||||
private function getPlatformOffer(): PlatformOffer
|
||||
{
|
||||
$platformOffer = new PlatformOffer();
|
||||
$platformOffer->setId($this->getUuid());
|
||||
|
||||
return $platformOffer;
|
||||
}
|
||||
}
|
||||
|
|
@ -373,6 +373,11 @@
|
|||
<target>Nombre de groupes</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="mFV6gsn" resname="Membership Paid">
|
||||
<source>Membership Paid</source>
|
||||
<target>Abonnement à la plateforme payé</target>
|
||||
</trans-unit>
|
||||
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
|
||||
<header>
|
||||
<tool tool-id="symfony" tool-name="Symfony" />
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="Zs247Lc" resname="app.controller.admin.user_crud_controller.expires_in.formatted_value">
|
||||
<source>app.controller.admin.user_crud_controller.expires_in.formatted_value</source>
|
||||
<target>%days% jour(s).</target>
|
||||
</trans-unit>
|
||||
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
<body>
|
||||
<trans-unit id="HkHjuo7" resname="app.controller.payment.platform_membership.done_action.flash.success">
|
||||
<source>app.controller.payment.platform_membership.done_action.flash.success</source>
|
||||
<target>Paiement réussi. Vous êtes désormais membre de la plateforme %platform%.</target>
|
||||
<target>Paiement réussi. Vous êtes désormais membre de la plateforme %platform%. Veuillez renseigner votre adresse afin de pouvoir utiliser les différents services.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="iikpmn8" resname="app.controller.payment.platform_membership.done_action.status.new">
|
||||
|
|
|
|||
|
|
@ -15,11 +15,21 @@
|
|||
<target>Votre compte a bien été créé. Veuillez renseigner votre adresse afin de pouvoir utiliser les différents services.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="" resname="app.controller.security.account_create_controller.step2.with_invitation.flash.success">
|
||||
<trans-unit id="jaHXivi" resname="app.controller.security.account_create_controller.step2.global_paid_membership.flash.success">
|
||||
<source>app.controller.security.account_create_controller.step2.global_paid_membership.flash.success</source>
|
||||
<target>Votre compte a bien été créé. Pour accéder à la plateforme, veuillez payer l'adhésion.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="ueGTY73" resname="app.controller.security.account_create_controller.step2.with_invitation.flash.success">
|
||||
<source>app.controller.security.account_create_controller.step2.with_invitation.flash.success</source>
|
||||
<target>Votre compte a bien été créé. Vous pouvez accepter l'invitation sur la page du groupe.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="MWzA9dr" resname="app.controller.security.account_create_controller.step2.with_invitation.global_paid_membership.flash.success">
|
||||
<source>app.controller.security.account_create_controller.step2.with_invitation.global_paid_membership.flash.success</source>
|
||||
<target>Votre compte a bien été créé. Pour accéder à la plateforme, veuillez payer l'adhésion. Vous pourrez alors accepter l'invitation sur la page du groupe.</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="jDAjgu1" resname="app.controller.security.account_create_controller.step2.user_not_found.warning">
|
||||
<source>app.controller.security.account_create_controller.step2.user_not_found.warning</source>
|
||||
<target>Aucun·e utilisateur·rice correspondant à ce code n'a été trouvé·e. Si votre compte est déjà confirmé veuillez vous connecter.</target>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
|
||||
<header>
|
||||
<tool tool-id="symfony" tool-name="Symfony" />
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="DdQRDUh" resname="app.mailer.email.command.end_platform_membership_mail.subject">
|
||||
<source>app.mailer.email.command.end_platform_membership_mail.subject</source>
|
||||
<target>%brand% : Expiration de votre adhésion à la plateforme %platform%.</target>
|
||||
</trans-unit>
|
||||
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
|
||||
<header>
|
||||
<tool tool-id="symfony" tool-name="Symfony" />
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="6AMBY16" resname="app.mailer.email.command.notify_platform_membership_expiration_mail.subject">
|
||||
<source>app.mailer.email.command.notify_platform_membership_expiration_mail.subject</source>
|
||||
<target>%brand% : expiration de l'adhésion à la plateforme %platform% dans %days% jour(s).</target>
|
||||
</trans-unit>
|
||||
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
|
||||
<header>
|
||||
<tool tool-id="symfony" tool-name="Symfony" />
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="P5fekcX" resname="app.mailer.email.payment.platform_membership_paid_mail.subject">
|
||||
<source>app.mailer.email.command.platform_membership_paid_mail.subject</source>
|
||||
<target>%brand% : Confirmation de votre adhésion à la plateforme %platform%.</target>
|
||||
</trans-unit>
|
||||
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
|
||||
<header>
|
||||
<tool tool-id="symfony" tool-name="Symfony" />
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="dMFTkCX" resname="templates.email.command.end_platform_membership.h1">
|
||||
<source>emplates.email.command.end_platform_membership.h1</source>
|
||||
<target>Bonjour !</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="nAjYzR4" resname="templates.email.command.end_platform_membership.p1">
|
||||
<source>templates.email.command.end_platform_membership.p1</source>
|
||||
<target>Votre adhésion à la plateforme %platform% a expiré le %endAt%.
|
||||
Connectez-vous à votre compte pour renouveler votre adhésion. </target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="kcvM4iP" resname="templates.email.command.end_platform_membership.loginLink">
|
||||
<source>templates.email.command.end_platform_membership.loginLink</source>
|
||||
<target>Me connecter</target>
|
||||
</trans-unit>
|
||||
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
|
||||
<header>
|
||||
<tool tool-id="symfony" tool-name="Symfony" />
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="cmeQr4r" resname="templates.email.command.notify_membership_expiration.h1">
|
||||
<source>templates.email.command.notify_membership_expiration.h1</source>
|
||||
<target>Bonjour !</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="YUhVmRj" resname="templates.email.command.notify_membership_expiration.p1">
|
||||
<source>templates.email.command.notify_membership_expiration.p1</source>
|
||||
<target>Votre adhésion à la plateform %platform% expire dans %days% jour(s).</target>
|
||||
</trans-unit>
|
||||
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
|
||||
<header>
|
||||
<tool tool-id="symfony" tool-name="Symfony" />
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="n6g8FMV" resname="templates.email.payment.platform_membership_paid.h1">
|
||||
<source>emplates.email.payment.platform_membership_paid.h1</source>
|
||||
<target>Bonjour !</target>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="8yoMsLT" resname="templates.email.payment.platform_membership_paid.p1">
|
||||
<source>templates.email.payment.platform_membership_paid.p1</source>
|
||||
<target>Votre adhésion à la plateforme %platform% a bien été pris en compte le %startAt%.
|
||||
Il expirera le %endAt%</target>
|
||||
</trans-unit>
|
||||
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
16
translations/user/validators.fr.xlf
Normal file
16
translations/user/validators.fr.xlf
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
|
||||
<header>
|
||||
<tool tool-id="symfony" tool-name="Symfony" />
|
||||
</header>
|
||||
<body>
|
||||
|
||||
<trans-unit id="qTjiw69" resname="validator.user.membership_paid">
|
||||
<source>validator.user.membership_paid</source>
|
||||
<target>Veuillez remplir ce champ</target>
|
||||
</trans-unit>
|
||||
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
Loading…
Reference in a new issue