Compare commits

..

No commits in common. "master" and "single-optins" have entirely different histories.

1153 changed files with 132150 additions and 177681 deletions

View file

@ -3,9 +3,9 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.md] [*.md]
@ -14,8 +14,8 @@ trim_trailing_whitespace = false
[*.{yml,yaml}] [*.{yml,yaml}]
indent_size = 2 indent_size = 2
[docker-compose.yml]
indent_size = 4
[*.vue] [*.vue]
indent_size = 2 indent_size = 2
[docker-compose.yml]
indent_size = 4

View file

@ -14,9 +14,18 @@ DB_DATABASE=laravel
DB_USERNAME=root DB_USERNAME=root
DB_PASSWORD= DB_PASSWORD=
DB_CONNECTION_OLD=mysql
DB_HOST_OLD=127.0.0.1
DB_PORT_OLD=3306
DB_DATABASE_OLD=ldccrm_12_2020
DB_USERNAME_OLD=root
DB_PASSWORD_OLD=
MIGRATIONPAGINATION=20000
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
CACHE_DRIVER=file CACHE_DRIVER=file
QUEUE_CONNECTION=database QUEUE_CONNECTION=sync
SESSION_DRIVER=file SESSION_DRIVER=file
SESSION_LIFETIME=120 SESSION_LIFETIME=120
@ -25,27 +34,23 @@ REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_MAILER=smtp MAIL_MAILER=smtp
MAIL_HOST=dev-mailpit MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=1025 MAIL_PORT=2525
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
MAIL_DIGITAL_SERVICE=""
# Sentry AWS_ACCESS_KEY_ID=
SENTRY_LARAVEL_DSN= AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
# UWIL ADMIN SECURE PUSHER_APP_ID=
# - LIST SEPARATED WITH ',' (Required) PUSHER_APP_KEY=
UWIL_ADMIN_SECURE_CIDR_LIST="" PUSHER_APP_SECRET=
# - LIST SEPARATED WITH ',' (Optional) PUSHER_APP_CLUSTER=mt1
UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST=""
# SAML MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
SAML2_DEFAULT_TENANT_UUID= MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
SAML2_SP_CERT_x509=
SAML2_SP_CERT_PRIVATEKEY=
SAML2_CONTACT_TECHNICAL_EMAIL=
SAML2_CONTACT_SUPPORT_EMAIL=

View file

@ -14,6 +14,15 @@ DB_DATABASE=testing_ldccrm_12_2020
DB_USERNAME=root DB_USERNAME=root
DB_PASSWORD=DBPASSWORD DB_PASSWORD=DBPASSWORD
DB_CONNECTION_OLD=mysql
DB_HOST_OLD=127.0.0.1
DB_PORT_OLD=3306
DB_DATABASE_OLD="ldc-crm"
DB_USERNAME_OLD=root
DB_PASSWORD_OLD=
MIGRATIONPAGINATION=20000
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
CACHE_DRIVER=file CACHE_DRIVER=file
QUEUE_CONNECTION=database QUEUE_CONNECTION=database

View file

@ -37,19 +37,5 @@
"semi": ["error", "always"], "semi": ["error", "always"],
"vue/script-indent": ["error", 2, { "baseIndent": 0, "switchCase": 1 }], "vue/script-indent": ["error", 2, { "baseIndent": 0, "switchCase": 1 }],
"vue/one-component-per-file": "off" "vue/one-component-per-file": "off"
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".vue", ".ts", ".d.ts"]
},
"alias": {
"extensions": [".vue", ".js", ".ts", ".scss", ".d.ts"],
"map": [
["~", "./resources/assets"],
["@", "./node_modules"]
]
}
}
} }
} }

14
.gitattributes vendored
View file

@ -1,11 +1,5 @@
* text=auto eol=lf * text=auto
*.css linguist-vendored
*.blade.php diff=html *.scss linguist-vendored
*.css diff=css *.js linguist-vendored
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore CHANGELOG.md export-ignore
.styleci.yml export-ignore

41
.gitignore vendored
View file

@ -1,31 +1,26 @@
/node_modules
/public/hot
/public/storage
/public/build
/public/medias
/public/uploads
/public/cache
/public/sitemap.xml
/storage/*.key
/vendor
.vscode/
.idea/
.env .env
.env.testing .env.testing
.env.backup .env.backup
.env.production
.phpunit.result.cache .phpunit.result.cache
.stylelintcache
/.fleet
/.idea
/.phpunit.cache
/.vscode
/node_modules
/bootstrap
/public/build
/public/cache
/public/hot
/public/sitemap.xml
/public/storage
/storage/*.key
/vendor
auth.json
docker-compose.override.yml
Homestead.json Homestead.json
Homestead.yaml Homestead.yaml
npm-debug.log npm-debug.log
yarn-error.log yarn-error.log
.npmrc storage/crm_migration.csv
.phpdoc MIG/errors_sql.txt
.zshrc public/js
.npm/ public/css
.composer/ public/assets
tests/coverage auth.json

View file

@ -1,23 +1,19 @@
include: include:
- project: "ldc-web/shared-ci" - project: 'ldc-web/shared-ci'
ref: "main" ref: 'main'
file: "lint/lint.yml" file: 'lint/phpcs7.3.yml'
- project: "ldc-web/shared-ci" - project: 'ldc-web/shared-ci'
ref: "main" ref: 'main'
file: "security/security.yml" file: 'security/composer7.3.yml'
- project: "ldc-web/shared-ci" - project: 'ldc-web/shared-ci'
ref: "main" ref: 'main'
file: "build/docker-multistage.yml" file: 'build/composer7.3.yml'
- project: "ldc-web/shared-ci" - project: 'ldc-web/shared-ci'
ref: "main" ref: 'main'
file: "test/test.yml" file: 'test/test.yml'
- project: "ldc-web/shared-ci"
ref: "main"
file: "deploy/deploy.yml"
stages: stages:
- lint - lint
- security - security
- build - build
- test - test
- deploy

4
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "client"] [submodule "clients/V2"]
path = client path = clients/V2
url = git@lab.s-web.fr:ldc-web/projets/crm-client.git url = git@lab.s-web.fr:ldc-web/projets/crm-client.git

View file

@ -1,3 +0,0 @@
#!/bin/sh
npx --no -- commitlint --config node_modules/@uwil/husky-config/commitlint.config.cjs --edit $1

View file

@ -1,16 +1,12 @@
{ {
"extends": [ "extends": [
"stylelint-config-prettier",
"stylelint-config-standard-scss", "stylelint-config-standard-scss",
"stylelint-config-sass-guidelines", "stylelint-config-sass-guidelines",
"stylelint-config-idiomatic-order" "stylelint-config-idiomatic-order"
], ],
"plugins": [ "plugins": ["stylelint-scss"],
"stylelint-scss" "files": ["**/*.css", "**/*.scss"],
],
"files": [
"**/*.css",
"**/*.scss"
],
"ignoreFiles": [ "ignoreFiles": [
"**/vendor/*.css", "**/vendor/*.css",
"**/vendor/**/*.css", "**/vendor/**/*.css",
@ -23,14 +19,16 @@
], ],
"customSyntax": "postcss-scss", "customSyntax": "postcss-scss",
"rules": { "rules": {
"declaration-property-value-no-unknown": true, "indentation": 4,
"string-quotes": "double",
"selector-max-id": null, "selector-max-id": null,
"color-no-invalid-hex": true, "color-no-invalid-hex": true,
"order/properties-alphabetical-order": null, "order/properties-alphabetical-order": null,
"max-nesting-depth": 5, "max-nesting-depth": 5,
"selector-max-compound-selectors": 5, "selector-max-compound-selectors": 5,
"declaration-colon-space-after": "always-single-line",
"function-parentheses-space-inside": "never-single-line",
"declaration-colon-newline-after": null, "declaration-colon-newline-after": null,
"value-list-comma-newline-after": null,
"scss/double-slash-comment-empty-line-before": [ "scss/double-slash-comment-empty-line-before": [
"always", "always",
{ {

View file

@ -1,20 +0,0 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"rvest.vs-code-prettier-eslint",
"onecentlin.laravel-extension-pack",
"johnpapa.vscode-peacock",
"xdebug.php-debug",
"neilbrayfield.php-docblocker",
"bmewburn.vscode-intelephense-client",
"valeryanm.vscode-phpsab",
"syler.sass-indented",
"sibiraj-s.vscode-scss-formatter",
"mrmlnc.vscode-scss",
"stylelint.vscode-stylelint",
"vue.vscode-typescript-vue-plugin",
"wscats.vue",
"vue.volar",
"misterj.vue-volar-extention-pack"
]
}

View file

@ -1,5 +1,6 @@
{ {
"phpcs.executablePath": "vendor/bin/phpcs",
"workbench.colorCustomizations": { "workbench.colorCustomizations": {
"activityBar.activeBackground": "#8dc952", "activityBar.activeBackground": "#8dc952",
"activityBar.activeBorder": "#3d7fc2", "activityBar.activeBorder": "#3d7fc2",
@ -21,5 +22,5 @@
"commandCenter.border": "#15202b99" "commandCenter.border": "#15202b99"
}, },
"peacock.remoteColor": "#73b137", "peacock.remoteColor": "#73b137",
"intelephense.environment.phpVersion": "8.5" "intelephense.environment.phpVersion": "7.4"
} }

View file

@ -1,50 +1,7 @@
ARG PHP_8_5_TAG=latest FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/ldcweb-production-7.3:latest
COPY --chown=www-data:www-data . /app/srcs
FROM composer:2.8 AS vendor COPY --chown=www-data:www-data ./public /app/srcs/public
COPY --chown=www-data:www-data ./vendor /app/srcs/vendor
WORKDIR /app
ARG GITLAB_USER
ARG GITLAB_PASSWORD
COPY database/ database/
COPY composer.json composer.json
COPY composer.lock composer.lock
RUN composer config gitlab-token.lab.s-web.fr "${GITLAB_USER}" "${GITLAB_PASSWORD}"
RUN composer install \
--ignore-platform-reqs \
--no-interaction \
--no-plugins \
--no-scripts \
--no-dev \
--prefer-dist
FROM node:20.18 AS frontend
ARG GITLAB_PASSWORD
RUN mkdir -p /app/public/build
COPY package*.json tsconfig.json vite.config.ts .eslintrc.json .stylelintrc.json /app/
COPY resources/ /app/resources/
WORKDIR /app
RUN npm config set @ldc-web:registry https://lab.s-web.fr/api/v4/packages/npm/
RUN npm config set -- '//lab.s-web.fr/api/v4/packages/npm/:_authToken' "${GITLAB_PASSWORD}"
RUN npm config set @uwil:registry https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/
RUN npm config set -- '//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken' "${GITLAB_PASSWORD}"
RUN npm ci --no-audit --no-fund --loglevel=error && npm run prod
ARG PHP_8_5_TAG
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-production-8.5:${PHP_8_5_TAG}
ARG USER=www-data
COPY --chown=${USER}:${USER} . /app/srcs
COPY --from=vendor --chown=${USER}:${USER} /app/vendor/ /app/srcs/vendor/
COPY --from=frontend --chown=${USER}:${USER} /app/public/build/ /app/srcs/public/build/
USER root USER root
RUN chmod -R 755 /app/srcs/storage RUN chmod -R 755 /app/srcs/storage
USER ${USER} USER www-data

View file

@ -0,0 +1,69 @@
<?php
use phpseclib3\Crypt\Rijndael;
class CPlatosChiffrement
{
private static $cipher = MCRYPT_RIJNDAEL_128; // Algorithme utilisé pour le cryptage des blocs
//private static $key = "yY75y2kLQ"; // Clé de cryptage PROD
private static $key = "123456789"; // Clé de cryptage DEV
private static $mode = 'cbc'; // Mode opératoire (traitement des blocs)
public static function crypt($data)
{
$keyHash = md5(self::$key);
$data = mcrypt_encrypt(self::$cipher, self::getKey($keyHash), $data, self::$mode, self::getIv($keyHash));
return base64_encode($data);
}
public static function decrypt($data)
{
$keyHash = md5(self::$key);
$data = base64_decode($data);
$data = mcrypt_decrypt(self::$cipher, self::getKey($keyHash), $data, self::$mode, self::getIv($keyHash));
return rtrim($data);
}
public static function getIv($keyHash)
{
$iv = substr($keyHash, 0, mcrypt_get_block_size(self::$cipher, self::$mode));
return $iv;
}
public static function getKey($keyHash)
{
$key = substr($keyHash, 0, mcrypt_get_key_size(self::$cipher, self::$mode));
return $key;
}
private function encryptRJ256($key, $iv, $text)
{
$cipher = new Rijndael('cbc');
$cipher->setBlockLength(128);
// keys are null-padded to the closest valid size
// longer than the longest key and it's truncated
$cipher->setKeyLength(128);
$cipher->setKey($key);
// the IV defaults to all-NULLs if not explicitly defined
$cipher->setIV($iv);
$cipher->disablePadding();
$length = strlen($text);
$pad = 32 - ($length % 32);
$text = str_pad($text, $length + $pad, chr(0));
return base64_encode($cipher->encrypt($text));
}
private function decryptRJ256($key, $iv, $text)
{
$cipher = new Rijndael('cbc'); // could use CRYPT_RIJNDAEL_MODE_CBC
$cipher->setBlockLength(128);
// keys are null-padded to the closest valid size
// longer than the longest key and it's truncated
$cipher->setKeyLength(128);
$cipher->setKey($key);
// the IV defaults to all-NULLs if not explicitly defined
$cipher->setIV($iv);
$cipher->disablePadding();
return $cipher->decrypt(base64_decode($text));
}
}

96
MIG/Db.php Normal file
View file

@ -0,0 +1,96 @@
<?php
class Database
{
static $instance;
private static $host;
private static $database;
private static $user;
private static $password;
public function __construct($host, $database, $user, $password)
{
self::$host = $host;
self::$database = $database;
self::$user = $user;
self::$password = $password;
}
public static function Instance()
{
if (!self::$instance) {
try {
self::$instance = new \PDO('mysql:host=' . self::$host . ';dbname=' . self::$database . ';charset=UTF8', self::$user, self::$password, array(\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION));
} catch (\Exception $e) {
die($e->getMessage());
}
}
return self::$instance;
}
public function request($statement, $params = false)
{
try {
$db = self::Instance();
$query = $db->prepare($statement);
if ($params) {
$query->execute($params);
} else {
$query->execute();
}
} catch (\PDOException $e) {
die($e->getMessage());
}
$query->setFetchMode(\PDO::FETCH_ASSOC);
$result = $query->fetch();
$query->closeCursor();
return ($result);
}
public function requestAll($statement, $params = false)
{
try {
$db = self::Instance();
$query = $db->prepare($statement);
if ($params) {
$query->execute($params);
} else {
$query->execute();
}
} catch (\PDOException $e) {
die($e->getMessage());
}
$query->setFetchMode(\PDO::FETCH_ASSOC);
$result = $query->fetchAll();
$query->closeCursor();
return ($result);
}
public function requestPush($statement, $params = false)
{
try {
$db = self::Instance();
$query = $db->prepare($statement);
if ($params)
$query->execute($params);
else
$query->execute();
return true;
} catch (\PDOException $e) {
die($e->getMessage());
}
}
public function requestPushInsert($statement, $params)
{
try {
$db = self::Instance();
$query = $db->prepare($statement);
$query->execute($params);
return $db->lastInsertId();
} catch (\PDOException $e) {
file_put_contents("errors_sql.txt", implode(',', $params) . PHP_EOL, FILE_APPEND);
}
}
}

90
MIG/export.php Normal file
View file

@ -0,0 +1,90 @@
<?php
ini_set("memory_limit", "-1");
set_time_limit(0);
error_reporting(E_ALL);
require_once 'Db.php';
require_once 'CPlatosChiffrement.class.php';
require_once 'LDCCRM_V1.php';
$db = new Database('localhost', 'ldc-crm', 'ldc-crmadm', 'wai6dahB');
//$db = new Database('192.168.30.3', 'ldc-crm', 'ldc-crmadm', 'wai6dahB');
$apiCrm = new \LDCCRM\ApiV1('http://crm.test', 'giu6OCyG6FuJvM56zFntDcrtDI23GU7WNnkvOk9rPPTOzKArI8IV9nqgEAnsA8wEQUDnKheQ9FDfKUwG');
$siteId = 17;
$siteName = "REGHALAL";
$accounts = $db->requestAll("SELECT *
FROM useraccount
INNER JOIN siteuser su ON useraccount.user_id = su.siteuser_useraccountId
WHERE su.siteuser_siteId = :siteid
AND user_status = 1
AND user_lastUpdateDate >= '2018-03-01'
AND (user_emailCheckStatus <> 0 OR user_emailCheckStatus IS NULL)
AND (user_lastCnxDateHeure IS NOT NULL AND user_lastCnxDateHeure <> '0000-00-00 00:00:00')", ['siteid' => $siteId]);
foreach ($accounts as $account) {
$isSingleOptin = false;
$oldCRMid = $account['user_id'];
$civility = $account['user_civilite'];
$firstname = $account['user_prenom'];
$lastname = $account['user_nom'];
$birthdate = $account['user_dateNaissance'];
$address1 = !empty($account['user_adresse1']) ? $account['user_adresse1'] : null;
$address2 = !empty($account['user_adresse2']) ? $account['user_adresse2'] : null;
$postcode = (!empty($account['user_codePostal']) && $account['user_codePostal'] !== 0 ? $account['user_codePostal'] : null);
$city = !empty($account['user_ville']) ? $account['user_ville'] : null;
$optin = $account['user_inscritNewsletter'];
$haschcode = $account['user_hashcode'];
$status = $account['user_status'];
$created_at = $account['user_creationDate'];
$updated_at = $account['user_lastUpdateDate'];
$email = $account['user_email'];
$password = CPlatosChiffrement::decrypt($account['user_pwd']);
$last_cnx = $account['user_lastCnxDateHeure'];
if (empty($firstname) && empty($lastname))
$isSingleOptin = true;
// Récupération des champs complémentaires
$fields = [['fieldkey' => 'created_at', 'fieldvalue' => $created_at], ['fieldkey' => 'updated_at', 'fieldvalue' => $updated_at], ['fieldkey' => 'last_cnx', 'fieldvalue' => $last_cnx]];
if (!$isSingleOptin) {
$crmUser = $apiCrm->createCustomer($email, $password, $firstname, $lastname, $optin, $birthdate, null, $address1, $address2, $city, $postcode, "France");
if ($crmUser->status === 'OK') {
$newCRMId = $crmUser->user->crm_id;
$activate = $apiCrm->activateCustomer($newCRMId);
if ($activate->status === 'NOK') {
file_put_contents("Export_errors.txt", "OLD CRM ID : $oldCRMid - NEW CRM ID : $newCRMId - ACTIVATE API ERROR : " . $activate->code . " - " . $activate->message . PHP_EOL, FILE_APPEND);
}
} else {
file_put_contents("Export_errors.txt", "OLD CRM ID : $oldCRMid - API ERROR : " . $crmUser->code . " - " . $crmUser->message . PHP_EOL, FILE_APPEND);
continue;
}
} else {
$result = $apiCrm->createSingleOptin($email);
if ($result->status === 'OK') {
$newCRMId = "SINGLEOPTIN";
} else {
file_put_contents("Export_errors.txt", "OLD CRM ID : $oldCRMid - API ERROR : " . $crmUser->code . " - " . $crmUser->message . PHP_EOL, FILE_APPEND);
continue;
}
}
write("$oldCRMid;$newCRMId;$email;" . json_encode($fields) . PHP_EOL, $siteName);
}
function write($text, $siteName)
{
if (!file_exists("Export_$siteName.csv"))
file_put_contents("Export_$siteName.csv", "\xEF\xBB\xBF", FILE_APPEND); // BOM UTF-8
file_put_contents("Export_$siteName.csv", $text, FILE_APPEND);
}

9
MIG/make_tmp_table.sql Normal file
View file

@ -0,0 +1,9 @@
CREATE TABLE `legaulois_migration` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`user_id` BIGINT(20) NOT NULL DEFAULT '0',
`pwd` VARCHAR(255) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
)
COLLATE='utf8_unicode_ci'
ENGINE=InnoDB
AUTO_INCREMENT=0;

80
MIG/migrate.php Normal file
View file

@ -0,0 +1,80 @@
<?php
ini_set("memory_limit", "-1");
set_time_limit(0);
error_reporting(E_ALL);
require_once 'Db.php';
$db = new Database('192.168.31.102', 'ldccrm_12_2020', 'ldccrmadm', 'wai6dahB'); // INFOS NOUVELLE CRM
$newBrandId = 2; // BRAND ID de la migration
/**
* @return Generator
*/
$fileData = function() {
$file = fopen(__DIR__ . '/Export_REGHALAL.csv', 'r');
if (!$file)
die('file does not exist or cannot be opened');
while (($line = fgets($file)) !== false) {
yield $line;
}
fclose($file);
};
foreach ($fileData() as $line) {
$isSingleOptin = false;
$datas = explode(";", $line);
$oldCrmId = $datas[0];
$newCrmId = $datas[1];
$email = $datas[2];
$otherFields = json_decode($datas[3]);
if ($newCrmId === 'SINGLEOPTIN')
$isSingleOptin = true;
foreach ($otherFields as $otherField){
// created_at - updated_at - last_cnx ne bougent pas car génériques et db sur nouvelle CRM sinon DB sur nouveau site.
switch($otherField->fieldkey){
case 'created_at':
if (!$isSingleOptin) {
$query = "UPDATE customers SET created_at = :nvalue WHERE id = :newid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId]);
$query = "UPDATE customer_infos SET created_at = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
$query = "UPDATE customer_login SET created_at = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
$query = "UPDATE customer_opt_in_history SET created_at = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
}else{
$query = "UPDATE single_optin SET created_at = :nvalue, updated_at = :nvalue WHERE email = :email AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'email' => $email, 'brandid' => $newBrandId]);
}
break;
case 'updated_at':
if (!$isSingleOptin) {
$query = "UPDATE customers SET updated_at = :nvalue WHERE id = :newid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId]);
$query = "UPDATE customer_infos SET updated_at = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
$query = "UPDATE customer_login SET updated_at = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
$query = "UPDATE customer_opt_in_history SET updated_at = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
}
break;
case 'last_cnx':
if (!$isSingleOptin) {
$query = "UPDATE customer_login SET last_connection = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
}
break;
}
//var_dump($otherField);
}
}

89
MIG/pwd.php Normal file
View file

@ -0,0 +1,89 @@
<?php
ini_set("memory_limit", "-1");
set_time_limit(0);
error_reporting(E_ALL);
require_once 'Db.php';
require_once 'CPlatosChiffrement.class.php';
$db = new Database('localhost', 'ldc-crm', 'root', '');
//$db = new Database('192.168.34.2', 'ldc-crm', 'ldc-crmadm', 'wai6dahB');
if (!isset($argv[1]) || !isset($argv[2]))
die('params missing');
$name = $argv[1];
$siteId = $argv[2];
$table = $name."_migration";
$maxdate = date("Y-m-d", strtotime("-36 month", time()));
$db->requestPush("CREATE TABLE IF NOT EXISTS $table (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`user_id` BIGINT(20) NOT NULL DEFAULT '0',
`firstname` VARCHAR(255) NOT NULL DEFAULT '0',
`lastname` VARCHAR(255) NOT NULL DEFAULT '0',
`birthdate` VARCHAR(255) NULL DEFAULT '0',
`address` VARCHAR(255) NULL DEFAULT '0',
`address2` VARCHAR(255) NULL DEFAULT '0',
`city` VARCHAR(255) NULL DEFAULT '0',
`postcode` VARCHAR(255) NULL DEFAULT '0',
`optin` INT(11) NOT NULL DEFAULT '0',
`email` VARCHAR(255) NOT NULL DEFAULT '0',
`pwd` VARCHAR(255) NOT NULL DEFAULT '0',
`created_at` TIMESTAMP NOT NULL,
`updated_at` TIMESTAMP NOT NULL,
`last_cnx` TIMESTAMP NOT NULL,
PRIMARY KEY (`id`)
) COLLATE='utf8_unicode_ci' ENGINE=InnoDB AUTO_INCREMENT=0;");
$getAccounts = function ($skip) use ($db, $siteId, $maxdate){
return $db->requestAll("SELECT *
FROM useraccount
INNER JOIN siteuser su ON useraccount.user_id = su.siteuser_useraccountId
WHERE su.siteuser_siteId = :siteid
AND user_id >= 701697
AND user_status = 1
AND user_lastUpdateDate >= :maxdate
AND (user_emailCheckStatus <> 0 OR user_emailCheckStatus IS NULL)
AND (user_lastCnxDateHeure IS NOT NULL AND user_lastCnxDateHeure <> '0000-00-00 00:00:00')
AND user_lastCnxDateHeure >= :maxdate
AND user_email IS NOT NULL LIMIT 4600 OFFSET ". ($skip * 4600), ['siteid' => $siteId, 'maxdate' => $maxdate]);
};
$k = 0;
while (count($accounts = $getAccounts($k++))) {
$sql = [];
$toto = "";
foreach ($accounts as $account){
$crm_id = $account['user_id'];
$civility = $account['user_civilite'];
$firstname = ucfirst($account['user_prenom']);
$lastname = ucfirst($account['user_nom']);
$birthdate = $account['user_dateNaissance'];
$address1 = !empty($account['user_adresse1']) ? $account['user_adresse1'] : null;
$address2 = !empty($account['user_adresse2']) ? $account['user_adresse2'] : null;
$postcode = (!empty($account['user_codePostal']) && $account['user_codePostal'] !== 0 ? $account['user_codePostal'] : null);
$city = !empty($account['user_ville']) ? $account['user_ville'] : null;
$optin = $account['user_inscritNewsletter'];
$haschcode = $account['user_hashcode'];
$status = $account['user_status'];
$created_at = $account['user_creationDate'];
$updated_at = $account['user_lastUpdateDate'];
$email = strtolower($account['user_email']);
//$password = CPlatosChiffrement::decrypt($account['user_pwd']);
$password = "toto";
$last_cnx = $account['user_lastCnxDateHeure'];
$datas = [
$crm_id,
$firstname,
$lastname,
$birthdate,
$address1,
$address2,
$city,
$postcode,
$optin,
$email,
$password,
$created_at,
$updated_at,
$last_cnx
];
$toto = "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$db->requestPushInsert("INSERT INTO $table (user_id, firstname, lastname, birthdate, address, address2, city, postcode, optin, email, pwd, created_at, updated_at, last_cnx) VALUES $toto", $datas);
}
}

View file

@ -1,10 +1,52 @@
# CRM # Comment lancer les tests unitaires ?
## Documentation ### Attention, si vous utilisez juste un .env à la place d'un .env.testing votre base de donnée de developpement sera effacée à chaque lancement des tests unitaires
https://cdlfr.sharepoint.com/:b:/r/sites/LDCWEB/Documents%20partages/Documentations/Projets/CRM/FICHE%20FONCTIONNELLE%20OUTIL%20R%C3%89CLAMATIONS%20CONSO.pdf?csf=1&web=1&e=qktL4V
## Tests unitaire Etapes :
- cp .env.testing.example .env.testing (spécial tests unitaires)
- php artisan key:generate
- parameter les BDD dans le .env.testing
- php artisan config:cache
- git submodule init
- git submodule update (récupérer le client)
- composer install
- service start mysql (votre base de donnée accessible sur 127.0.0.1:3306)
- chmod u+x ./test.sh
- php artisan optimize
- composer run test
### Comment lancer les tests unitaires ? Config Supervisor 2 Fichiers :
[Suivre le wiki dédié au tests unitaires](https://lab.s-web.fr/ldc-web/projets/crm/-/wikis/Tests-unitaires) `ldccrm-worker.conf`
**Pour les jobs en général**
[program:ldccrm-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /u1/DATA01/webldc/Apps/ldccrm/ldccrm-12-2020/srcs/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
startasgroup=true
killasgroup=true
user=www-data
umask=077
numprocs=8
redirect_stderr=true
stdout_logfile=/u1/DATA01/webldc/Apps/ldccrm/ldccrm-12-2020/srcs/storage/logs/worker.log
stopwaitsecs=3600
`ldccrm-stats-worker.conf`
**Pour les jobs de stats uniquement**
[program:ldccrm-stats-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /u1/DATA01/webldc/Apps/ldccrm/ldccrm-12-2020/srcs/artisan queue:work --queue=stats --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
startasgroup=true
killasgroup=true
user=www-data
umask=077
numprocs=1
redirect_stderr=true
stdout_logfile=/u1/DATA01/webldc/Apps/ldccrm/ldccrm-12-2020/srcs/storage/logs/worker.log
stopwaitsecs=3600

View file

@ -35,11 +35,10 @@ class DeleteUnconfirmedCustomers extends Command
* *
* @return integer * @return integer
*/ */
public function handle(): int public function handle()
{ {
/** @var \Illuminate\Database\Eloquent\Collection $customersLogins */ /** @var \Illuminate\Database\Eloquent\Collection $customersLogins */
$customersLogins = CustomerLogin::query() $customersLogins = CustomerLogin::whereStatus(false)
->where('status', false)
->where('created_at', '<', Carbon::now()->subDay(1)) ->where('created_at', '<', Carbon::now()->subDay(1))
->with(['brand', 'customer']) ->with(['brand', 'customer'])
->get(); ->get();

View file

@ -7,20 +7,18 @@ use App\Enums\ApiStatus;
use App\Enums\ArcadiaOrderType; use App\Enums\ArcadiaOrderType;
use App\Enums\Country; use App\Enums\Country;
use App\Enums\QuestionType; use App\Enums\QuestionType;
use App\Exceptions\Api\V4\ApiErrorException; use App\Exceptions\Api\V2\ApiErrorException;
use App\Exceptions\Api\V4\CrmException; use App\Exceptions\Api\V2\CrmException;
use App\Traits\Enums\BaseEnum;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
/** /**
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
class GenerateClient extends Command class GenerateCrmClient extends Command
{ {
private const TARGET = 'client/src'; private const TARGET = 'clients/V2/src';
private const CLIENTNAMESPACE = 'LDC\\LDCCRM'; private const CLIENTNAMESPACE = 'LDC\\LDCCRM';
/** /**
@ -35,16 +33,15 @@ class GenerateClient extends Command
* *
* @var string * @var string
*/ */
protected $description = 'Generate the crm V4 client in client folder'; protected $description = 'Generate the crm v2 client in clients/V2 folder';
/** /**
* Execute the console command. * Execute the console command.
* *
* @return integer * @return integer
*/ */
public function handle(): int public function handle()
{ {
$this->copyTraits();
$this->copyEnums(); $this->copyEnums();
$this->copyExceptions(); $this->copyExceptions();
$this->copyRoutesList(); $this->copyRoutesList();
@ -57,7 +54,7 @@ class GenerateClient extends Command
* *
* @return void * @return void
*/ */
private function copyApiConfigInfo(): void private function copyApiConfigInfo()
{ {
$this->info('Copy Api Config'); $this->info('Copy Api Config');
$rootTarget = app_path('../' . self::TARGET); $rootTarget = app_path('../' . self::TARGET);
@ -75,13 +72,13 @@ class GenerateClient extends Command
* *
* @return void * @return void
*/ */
private function copyRoutesList(): void private function copyRoutesList()
{ {
$this->info('Copy Routes'); $this->info('Copy Routes');
$routes = []; $routes = [];
/** @var \Illuminate\Routing\Route */ /** @var \Illuminate\Routing\Route */
foreach (Route::getRoutes() as $route) { foreach (Route::getRoutes() as $route) {
if (\strpos($route->getPrefix(), 'v4') !== false) { if (\strpos($route->getPrefix(), 'v2') !== false) {
$routes[$route->getName()] = [ $routes[$route->getName()] = [
'uri' => $route->uri(), 'uri' => $route->uri(),
'methods' => $route->methods() 'methods' => $route->methods()
@ -107,36 +104,15 @@ class GenerateClient extends Command
{ {
$this->info('Copy Exceptions'); $this->info('Copy Exceptions');
$rootTarget = app_path('../' . self::TARGET); $rootTarget = app_path('../' . self::TARGET);
$classes = [ $this->copyToFolder([
CrmException::class, CrmException::class,
ApiErrorException::class ApiErrorException::class
]; ], 'Exceptions');
$this->copyToFolder($classes, 'Exceptions'); $this->replaceInPhpFile(
foreach ($classes as $class) { "$rootTarget/Exceptions/ApiErrorException.php",
$classBasename = class_basename($class); 'App\Enums\ApiError',
$dest = "$rootTarget/Exceptions/$classBasename.php"; 'LDC\LDCCRM\Enums\ApiError'
$this->replaceInPhpFile($dest, 'App\\', self::CLIENTNAMESPACE . '\\'); );
}
}
/**
* Copy base enums classes
*
* @return void
*/
private function copyTraits(): void
{
$this->info('Copy Traits');
$rootTarget = app_path('../' . self::TARGET);
$classes = [
BaseEnum::class
];
$this->copyToFolder($classes, 'Traits');
foreach ($classes as $class) {
$classBasename = class_basename($class);
$dest = "$rootTarget/Traits/$classBasename.php";
$this->replaceInPhpFile($dest, 'App\\', self::CLIENTNAMESPACE . '\\');
}
} }
/** /**
@ -147,8 +123,7 @@ class GenerateClient extends Command
private function copyEnums(): void private function copyEnums(): void
{ {
$this->info('Copy Enums'); $this->info('Copy Enums');
$rootTarget = app_path('../' . self::TARGET); $classes = [
$classes = [
ApiError::class, ApiError::class,
ApiStatus::class, ApiStatus::class,
ArcadiaOrderType::class, ArcadiaOrderType::class,
@ -156,10 +131,11 @@ class GenerateClient extends Command
QuestionType::class QuestionType::class
]; ];
$this->copyToFolder($classes, 'Enums'); $this->copyToFolder($classes, 'Enums');
$rootTarget = app_path('../' . self::TARGET);
foreach ($classes as $class) { foreach ($classes as $class) {
$classBasename = class_basename($class); $classBasename = class_basename($class);
$dest = "$rootTarget/Enums/$classBasename.php"; $dest = "$rootTarget/Enums/$classBasename.php";
$this->replaceInPhpFile($dest, 'App\\', self::CLIENTNAMESPACE . '\\'); $this->replaceInPhpFile($dest, 'BaseEnumRoutable', 'BaseEnum');
} }
} }
@ -172,10 +148,9 @@ class GenerateClient extends Command
*/ */
private function copyToFolder(array $classes, string $folder): void private function copyToFolder(array $classes, string $folder): void
{ {
$rootTarget = app_path('../' . self::TARGET); $rootTarget = app_path('../' . self::TARGET);
$folderNamespace = \str_replace('/', '\\', $folder);
foreach ($classes as $class) { foreach ($classes as $class) {
$classFileName = \str_replace("App\\$folderNamespace\\", '', sprintf('%s.php', $class)); $classFileName = \str_replace("App\\$folder\\", '', sprintf('%s.php', $class));
$classFileName = \str_replace('\\', '/', $classFileName); $classFileName = \str_replace('\\', '/', $classFileName);
$classBasename = class_basename($class); $classBasename = class_basename($class);
$source = \app_path("$folder/$classFileName"); $source = \app_path("$folder/$classFileName");
@ -200,15 +175,11 @@ class GenerateClient extends Command
{ {
$fContent = File::get($source); $fContent = File::get($source);
// Remove trailing \Api\V4 . // Remove trailing \Api\V2 .
if ($pos = strpos($fContent, '\Api\V4')) { if ($pos = strpos($fContent, '\Api\V2')) {
$fContent = substr_replace($fContent, '', $pos, strlen('\Api\V4')); $fContent = substr_replace($fContent, '', $pos, strlen('\Api\V2'));
} }
// Remove trailing \Api .
$fContent = Str::of($fContent)->replaceMatches("/\\\(Api)([\s;\\\])/", '$2');
$fContent = Str::of($fContent)->replaceMatches("/\\\Traits\\\Enums/", '\Traits');
// Replace namespace top level. // Replace namespace top level.
$pos = strpos($fContent, $search); $pos = strpos($fContent, $search);
if ($pos !== false) { if ($pos !== false) {

View file

@ -0,0 +1,78 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
/**
* Command Name: LDC Web Status Checker
* Description: Envoi des infos Serveur/Laravel vers LDC Web
* Version: 1.1
* Author: Vincent BRIDIER
* Author URI: vincent.bridier@ldc.fr
*
* @codeCoverageIgnore
*/
class LdcWebStatusChecker extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ldcweb:statuschecker';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Envoi des infos Serveur/Laravel vers LDC Web';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$data = [];
$data['uuid'] = config('ldcweb.project_uuid');
// Sitemap.xml ?
if (file_exists(public_path() . '/sitemap.xml')) {
$data['sitemap'] = 'true';
} else {
$data['sitemap'] = 'false';
}
// Robots.txt ?
if (file_exists(public_path() . '/robots.txt')) {
$data['robots'] = 'true';
} else {
$data['robots'] = 'false';
}
// Check logs.
$data['logs'] = 0;
$patherror = storage_path('/logs');
$files = glob($patherror . '/*.log');
if (count($files) > 0) {
foreach ($files as $file) {
$data['logs'] = $data['logs'] + filesize($file);
}
}
$data['code']['version'] = app()->version();
// Send by cURL.
$url = 'https://ldc-web.ldc.fr/api/status-checker';
$ch = curl_init($url);
$payload = ['token' => 'CE8S52Qq7stf5k8frF4T', 'datas' => json_encode($data)];
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);
return 0;
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace App\Console\Commands;
use App\Lib\MigrateOldData;
use App\Models\Brand;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* @codeCoverageIgnore
*/
class MigrateCrmUsers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'migrate:crm';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate crm users';
/**
* Execute the console command.
*
* @return integer
*/
public function handle()
{
try {
$table = $this->ask('What is the tmp table name with users data ?');
try {
DB::connection(config('database.oldDB'))->table($table)->exists();
} catch (Exception $e) {
$this->error(sprintf('%s table not found on old crm database', $table));
return -1;
}
$brandname = $this->ask('What is the brand name ?');
$brand = Brand::where('name', 'LIKE', "%$brandname%")->first();
if (!$brand) {
if (!$this->confirm(sprintf('Would you like to use %s ? ', $brandname))) {
$this->info('Command cancelled');
return 0;
}
if ($this->confirm('Would you like to create a new one ?')) {
$brand = new Brand();
$brand->name = $brandname;
$brand->save();
} else {
$this->info('Command cancelled');
return 0;
}
}
$mgOldData = new MigrateOldData();
$mgOldData->runMigration($brand, $table);
} catch (Exception $e) {
$this->error(sprintf('Failed %s', $e));
return -1;
}//end try
return 0;
}
}

View file

@ -29,7 +29,7 @@ class StartStatJobs extends Command
* *
* @return integer * @return integer
*/ */
public function handle(): int public function handle()
{ {
\dispatch(new JobsStartStatJobs())->afterResponse(); \dispatch(new JobsStartStatJobs())->afterResponse();
return 0; return 0;

View file

@ -1,35 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class TestMail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'uwil:mail:test';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send test email to equipe.web@ldc.fr';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
Mail::raw(sprintf('UWIL mail-test from %s %s', \config('app.name'), \config('app.url')), function ($msg) {
$msg->to('equipe.web@ldc.fr')->subject('Test Email');
});
}
}

View file

@ -2,14 +2,13 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Enums\Users\RoleEnum;
use App\Models\User; use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class UserCreate extends Command class UserCreate extends Command
{ {
/** /**
@ -17,189 +16,35 @@ class UserCreate extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'uwil:user:create'; protected $signature = 'user:create';
/** /**
* The console command description. * The console command description.
* *
* @var string * @var string
*/ */
protected $description = 'Create a backend admin user'; protected $description = 'Command description';
/**
* The first name of the new user.
*
* @var string|null
*/
protected $firstName;
/**
* The last name of the new user.
*
* @var string|null
*/
protected $lastName;
/**
* The email of the new user.
*
* @var string|null
*/
protected $email;
/**
* The role of the new user.
*
* @var string|null
*/
protected $role;
/** /**
* Execute the console command. * Execute the console command.
* *
* @return void * @return integer
*/ */
public function handle(): void public function handle()
{ {
$this->firstName = null; $firstname = $this->ask('What is the firstname?');
$this->addFirstName(); $lastname = $this->ask('What is the lastname?');
$email = $this->ask('What is the email?');
$pwd = $this->secret('What is the password?');
$this->lastName = null; $user = new User();
$this->addLastName(); $user->firstname = $firstname;
$user->lastname = $lastname;
$user->email = $email;
$user->password = Hash::make($pwd);
$user->save();
$this->email = null; $this->info('User created.');
$this->addEmail(); return 0;
$this->role = null;
$this->addRole();
$user = new User([
'first_name' => $this->firstName,
'last_name' => $this->lastName,
'email' => $this->email,
'role' => $this->role,
'published' => true,
'published_at' => Carbon::now(),
]);
$user->saveOrFail();
$this->info('User created 👌');
}
/**
* Add a first name to the user.
*
* @return void
*/
private function addFirstName(): void
{
while (is_null($this->firstName)) {
try {
$tmp = $this->ask('Type the wanted user first name', 'Admin');
$validator = Validator::make(['first_name' => $tmp], [
'first_name' => 'required|min:3|max:255'
]);
$validator->validate();
$this->firstName = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid first name %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
}
/**
* Add a last name to the user.
*
* @return void
*/
private function addLastName(): void
{
while (is_null($this->lastName)) {
try {
$tmp = $this->ask('Type the wanted user last name', 'Saïtama');
$validator = Validator::make(['last_name' => $tmp], [
'last_name' => 'required|min:3|max:255'
]);
$validator->validate();
$this->lastName = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid last_name %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
}
/**
* Add an email to the user.
*
* @return void
*/
private function addEmail(): void
{
while (is_null($this->email)) {
try {
$tmp = $this->ask('Type the wanted user email', 'equipe.web@ldc.fr');
$validator = Validator::make(['email' => $tmp], [
'email' => 'required|unique:users,email|email:rfc,dns'
]);
$validator->validate();
$this->email = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid email %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
}
/**
* Add a role to the user.
*
* @return void
*/
private function addRole(): void
{
while (is_null($this->role)) {
try {
$tmp = $this->choice(
'Select his role',
[
Str::ucFirst(RoleEnum::conceptor->label()),
Str::ucFirst(RoleEnum::admin->label()),
Str::ucFirst(RoleEnum::marketing->label())
],
Str::ucFirst(RoleEnum::marketing->label()),
);
if ($tmp === Str::ucFirst(RoleEnum::conceptor->label())) {
$tmp = RoleEnum::conceptor;
}
if ($tmp === Str::ucFirst(RoleEnum::admin->label())) {
$tmp = RoleEnum::admin;
}
if ($tmp === Str::ucFirst(RoleEnum::marketing->label())) {
$tmp = RoleEnum::marketing;
}
$validator = Validator::make(['role' => $tmp], [
'role' => 'required'
]);
$validator->validate();
$this->role = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please choose from the selection %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
} }
} }

View file

@ -3,11 +3,13 @@
namespace App\Console; namespace App\Console;
use App\Console\Commands\DeleteUnconfirmedCustomers; use App\Console\Commands\DeleteUnconfirmedCustomers;
use App\Jobs\SendTransferredToDigitalCountMail;
use App\Jobs\StartStatJobs; use App\Jobs\StartStatJobs;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
/**
* @codeCoverageIgnore
*/
class Kernel extends ConsoleKernel class Kernel extends ConsoleKernel
{ {
/** /**
@ -23,13 +25,10 @@ class Kernel extends ConsoleKernel
* @param \Illuminate\Console\Scheduling\Schedule $schedule * @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void * @return void
*/ */
protected function schedule(Schedule $schedule): void protected function schedule(Schedule $schedule)
{ {
// @codeCoverageIgnoreStart
$schedule->job(StartStatJobs::class)->dailyAt('03:30'); $schedule->job(StartStatJobs::class)->dailyAt('03:30');
$schedule->job(SendTransferredToDigitalCountMail::class)->weekdays()->at('07:00');
$schedule->command(DeleteUnconfirmedCustomers::class)->dailyAt('05:30'); $schedule->command(DeleteUnconfirmedCustomers::class)->dailyAt('05:30');
// @codeCoverageIgnoreEnd
} }
/** /**
@ -37,7 +36,7 @@ class Kernel extends ConsoleKernel
* *
* @return void * @return void
*/ */
protected function commands(): void protected function commands()
{ {
$this->load(__DIR__ . '/Commands'); $this->load(__DIR__ . '/Commands');

View file

@ -1,46 +0,0 @@
<?php
namespace App\Enums\ActivityLogs;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ActivityLogsEventEnum: int
{
use BaseEnum;
case created = 1;
case updated = 2;
case duplicated = 3;
case deleted = 4;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::created->name => 'Création',
self::updated->name => 'Mise à jour',
self::duplicated->name => 'Duplication',
self::deleted->name => 'Suppression',
];
/**
* Custom added classes definition
*/
private const BOOTSTRAPCLASS = [
self::created->name => 'success',
self::updated->name => 'warning',
self::duplicated->name => 'secondary',
self::deleted->name => 'danger',
];
/**
* Get Class
*
* @return string
*/
public function bootstrapClass(): string
{
return self::BOOTSTRAPCLASS[$this->name];
}
}

View file

@ -2,43 +2,113 @@
namespace App\Enums; namespace App\Enums;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum; use Kwaadpepper\Enum\BaseEnumRoutable;
enum ApiError: int /**
* This describes an API error.
*
* @method static self modelNotFound()
* The request model was not found (typically the customer)
*
* @method static self validationFailed()
* This means some data could not be validated,
* you may have to look into message to get the details
*
* @method static self customerExists()
* The customer already exists
*
* @method static self customerNotActivated()
* The customer account is not activated and cannot perform
* this action
*
* @method static self customerAlreadyActive()
* You have requested to activate a customer account,
* but it is already active
*
* @method static self singleOptinCustomerExists()
* You have requested to create a single optin for an email,
* but it already exists
*
* @method static self apiAuthFailed()
* The authentification to the api failed. You may have to
* check into config or .env file for your service credentials
*
* @method static self authFailed()
* The authentification for this customer failed because of wrong credentials
*
* @method static self methodNotAllowed()
* This http method is not allowed
*
* @method static self wrongTimeStamp()
* The timestamp given is wrong. This would happen in case if
* a replay attack scenario. Timestamps are provided in headers
* and in the http body
*
* @method static self missingParameter()
* The request is missing some parameters
*
* @method static self throttleRequestsException()
* Too many requests have been attempted. The is a throttle security.
* Normally you should not trigger this.
* If this happens fill a ticket on https://supportweb.ldc.fr
* with some technical details
*
* @method static self authorizationException()
* You are maybe trying to access data that is out of your range.
* This should not happen normally, it is here to allow you to handle this case.
* If this happens fill a ticket on https://supportweb.ldc.fr
* with some technical details
*
* @package LDCCrmClient
* @subpackage enums
*/
class ApiError extends BaseEnumRoutable
{ {
use BaseEnum; /**
* Valeurs des possibilités de cet énumération (value)
case modelNotFound = 101; *
case validationFailed = 102; * @return array
case customerExists = 103; */
case customerAlreadyActive = 110; public static function values(): array
case customerNotActivated = 116; {
case authFailed = 117; return [
case apiAuthFailed = 119; 'modelNotFound' => 101,
case singleOptinCustomerExists = 120; 'validationFailed' => 102,
case methodNotAllowed = 150; 'customerExists' => 103,
case wrongTimeStamp = 151; 'customerAlreadyActive' => 110,
case missingParameter = 400; 'customerNotActivated' => 116,
case authorizationException = 403; 'authFailed' => 117,
case throttleRequestsException = 429; 'apiAuthFailed' => 119,
'singleOptinCustomerExists' => 120,
'methodNotAllowed' => 150,
'wrongTimeStamp' => 151,
'missingParameter' => 400,
'authorizationException' => 403,
'throttleRequestsException' => 429
];
}
/** /**
* Optionnal labels definition * Libellés des possibilités de cet énumération (label)
*
* @return array
*/ */
private const LABELS = [ public static function labels(): array
self::modelNotFound->name => '%s introuvable.', {
self::validationFailed->name => 'Impossible de valider les données', return [
self::customerExists->name => 'L\'utilisateur est déjà enregistré sur ce site', 'modelNotFound' => '%s introuvable.',
self::customerNotActivated->name => 'Le compte n\'est pas activé', 'validationFailed' => 'Impossible de valider les données',
self::customerAlreadyActive->name => 'Le compte est déjà activé', 'customerExists' => 'L\'utilisateur est déjà enregistré sur ce site',
self::singleOptinCustomerExists->name => 'Le single optin ne peut être ajouté car un compte existe.', 'customerNotActivated' => 'Le compte n\'est pas activé',
self::authFailed->name => 'Echec de l\'authentification du compte', 'customerAlreadyActive' => 'Le compte est déjà activé',
self::apiAuthFailed->name => 'Echec de l\'authentification client à l\'Api LDC-CRM pour ce service', 'singleOptinCustomerExists' => 'Le single optin ne peut être ajouté car un compte existe.',
self::methodNotAllowed->name => 'This method is not allowed for this request', 'authFailed' => 'Echec de l\'authentification du compte',
self::wrongTimeStamp->name => 'Your request is malformed, please check timestamp header and 'apiAuthFailed' => 'Echec de l\'authentification client à l\'Api LDC-CRM pour ce service',
parameter', 'methodNotAllowed' => 'This method is not allowed for this request',
self::missingParameter->name => 'The request is missing some parameters', 'wrongTimeStamp' => 'Your request is malformed, please check timestamp header and parameter',
self::authorizationException->name => 'You are not autorized to access this', 'missingParameter' => 'The request is missing some parameters',
self::throttleRequestsException->name => 'Throttle: too many request have been attempted, please retry later' 'authorizationException' => 'You are not autorized to access this',
]; 'throttleRequestsException' => 'Throttle: too many request have been attempted, please retry later'
];
}
} }

View file

@ -2,12 +2,15 @@
namespace App\Enums; namespace App\Enums;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum; use Kwaadpepper\Enum\BaseEnumRoutable;
enum ApiStatus: int /**
* @method static self OK()
* @method static self KO()
*
* @package LDCCrmClient
* @subpackage enums
*/
class ApiStatus extends BaseEnumRoutable
{ {
use BaseEnum;
case OK = 1;
case KO = 2;
} }

View file

@ -2,12 +2,27 @@
namespace App\Enums; namespace App\Enums;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum; use Kwaadpepper\Enum\BaseEnumRoutable;
enum ArcadiaMailType: int /**
* @method static self default()
* @method static self game()
*
* @package LDCCrmClient
* @subpackage enums
*/
class ArcadiaMailType extends BaseEnumRoutable
{ {
use BaseEnum; /**
* Valeurs des possibilités de cet énumération (value)
case default = 1; *
case game = 2; * @return array
*/
public static function values(): array
{
return [
'default' => 1,
'game' => 2
];
}
} }

View file

@ -2,12 +2,27 @@
namespace App\Enums; namespace App\Enums;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum; use Kwaadpepper\Enum\BaseEnumRoutable;
enum ArcadiaOrderType: int /**
* @method static self cb()
* @method static self check()
*
* @package LDCCrmClient
* @subpackage enums
*/
class ArcadiaOrderType extends BaseEnumRoutable
{ {
use BaseEnum; /**
* Valeurs des possibilités de cet énumération (value)
case cb = 0; *
case check = 1; * @return array
*/
public static function values(): array
{
return [
'cb' => 0,
'check' => 1
];
}
} }

View file

@ -1,21 +0,0 @@
<?php
namespace App\Enums\Claims;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ClaimMessageType: int
{
use BaseEnum;
case system = 1;
case human = 2;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::system->name => 'Système',
self::human->name => 'Humain',
];
}

View file

@ -1,51 +0,0 @@
<?php
namespace App\Enums\Claims;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ClaimResponseType: int
{
use BaseEnum;
case none = 1;
case manual = 2;
case mail = 3;
case email = 4;
case phone = 5;
case other = 99;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::none->name => 'Non défini',
self::manual->name => 'Manuelle',
self::mail->name => 'Courrier',
self::email->name => 'E-mail',
self::phone->name => 'Téléphone',
self::other->name => 'Autre',
];
/**
* Optionnal labels definition.
*/
private const COLORS = [
self::none->name => '#b8b8b8',
self::manual->name => '#a7a7a7',
self::mail->name => '#757575',
self::email->name => '#5e5e5e',
self::phone->name => '#414141',
self::other->name => '#2b2b2b',
];
/**
* Get Class.
*
* @return string
*/
public function color(): string
{
return self::COLORS[$this->name];
}
}

View file

@ -1,48 +0,0 @@
<?php
namespace App\Enums\Claims;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ClaimStatus: int
{
use BaseEnum;
case untreated = 1;
case sentDigital = 2;
case sentMarketing = 3;
case treated = 4;
case refused = 5;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::untreated->name => 'Non traitée',
self::sentDigital->name => 'Transférée au service Digital',
self::sentMarketing->name => 'Retournée au Marketing',
self::treated->name => 'Traitée',
self::refused->name => 'Refusé de répondre',
];
/**
* Optionnal labels definition.
*/
private const COLORS = [
self::untreated->name => '#5a5a5a',
self::sentDigital->name => '#0033c0',
self::sentMarketing->name => '#69077c',
self::treated->name => '#188618',
self::refused->name => '#dc143c',
];
/**
* Get Class.
*
* @return string
*/
public function color(): string
{
return self::COLORS[$this->name];
}
}

View file

@ -1,42 +0,0 @@
<?php
namespace App\Enums\Claims;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ClaimType: int
{
use BaseEnum;
case classic = 1;
case br = 2;
case rgpd = 3;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::classic->name => 'Classique',
self::br->name => 'BR',
self::rgpd->name => 'RGPD',
];
/**
* Optionnal labels definition.
*/
private const COLORS = [
self::classic->name => '#08818a',
self::br->name => '#9021f8',
self::rgpd->name => '#c2237a',
];
/**
* Get Class.
*
* @return string
*/
public function color(): string
{
return self::COLORS[$this->name];
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,23 +2,42 @@
namespace App\Enums; namespace App\Enums;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum; use Kwaadpepper\Enum\BaseEnumRoutable;
enum CustomerExportType: int /**
* @method static self singleOptin()
* @method static self customer()
* @method static self both()
*
* @codeCoverageIgnore
*/
class CustomerExportType extends BaseEnumRoutable
{ {
use BaseEnum; /**
* Valeurs des possibilités de cet énumération (value)
case singleOptin = 1; *
case customer = 2; * @return array
case both = 3; */
public static function values(): array
{
return [
'singleOptin' => 1,
'customer' => 2,
'both' => 3
];
}
/** /**
* Optionnal labels definition * Libellés des possibilités de cet énumération (label)
*
* @return array
*/ */
private const LABELS = [ public static function labels(): array
self::singleOptin->name => 'Les single-optins uniquement', {
self::customer->name => 'Les clients uniquement acceptant de recevoir les newsletters', return [
self::both->name => 'La totalité des single-optins ainsi que des clients acceptant 'singleOptin' => trans('Les single optins uniquement'),
de recevoir les newsletters', 'customer' => trans('Les clients uniquement acceptant de recevoir les newsletters'),
]; 'both' => trans('La totalité des client acceptant de recevoir les newsletters')
];
}
} }

View file

@ -1,85 +0,0 @@
<?php
namespace App\Enums;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ExportFormat: int
{
use BaseEnum;
case csv = 1;
case csvWithoutAttachment = 2;
case pdf = 3;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::csv->name => 'csv',
self::csvWithoutAttachment->name => 'csv sans pièces jointes',
self::pdf->name => 'pdf',
];
/**
* Bootstrap color class to use for the export buttons.
*
* @return string
*/
public function class(): string
{
return match ($this) {
self::csv,
self::csvWithoutAttachment => 'success',
self::pdf => 'danger',
default => 'primary',
};
}
/**
* Font Awesome picto to use for the export buttons.
*
* @return string
*/
public function picto(): string
{
return match ($this) {
self::csv,
self::csvWithoutAttachment => 'fa-solid fa-file-csv',
self::pdf => 'fa-solid fa-file-pdf',
default => 'fa-solid fa-file',
};
}
/**
* File headers to use when the exported file is return with StreamedResponse.
*
* @return array
*/
public function fileHeaders(): array
{
return match ($this) {
self::csv,
self::csvWithoutAttachment => [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="export.csv"',
],
default => [],
};
}
/**
* Export file name.
*
* @return string
*/
public function fileName(): string
{
return match ($this) {
self::csv,
self::csvWithoutAttachment => 'export.csv',
self::pdf => 'export.pdf',
};
}
}

View file

@ -1,16 +0,0 @@
<?php
namespace App\Enums\Pagination;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ItemsPerPaginationEnum: int
{
use BaseEnum;
case five = 5;
case twelve = 12;
case twentyfive = 25;
case fifty = 50;
case onehundred = 100;
}

View file

@ -2,13 +2,29 @@
namespace App\Enums; namespace App\Enums;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum; use Kwaadpepper\Enum\BaseEnumRoutable;
enum QuestionType: int /**
* @method static self select()
* @method static self radio()
* @method static self checkbox()
*
* @package LDCCrmClient
* @subpackage enums
*/
class QuestionType extends BaseEnumRoutable
{ {
use BaseEnum; /**
* Valeurs des possibilités de cet énumération (value)
case select = 1; *
case radio = 2; * @return array
case checkbox = 3; */
public static function values(): array
{
return [
'select' => 1,
'radio' => 2,
'checkbox' => 3
];
}
} }

View file

@ -1,21 +0,0 @@
<?php
namespace App\Enums\Theme;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum BootstrapThemeEnum: int
{
use BaseEnum;
case light = 1;
case dark = 2;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::light->name => 'Light',
self::dark->name => 'Dark',
];
}

View file

@ -1,23 +0,0 @@
<?php
namespace App\Enums\Users;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum RoleEnum: int
{
use BaseEnum;
case conceptor = 1;
case admin = 2;
case marketing = 3;
/**
* Optionnal labels definition.
*/
private const LABELS = [
self::conceptor->name => 'Concepteur',
self::admin->name => 'Administrateur',
self::marketing->name => 'Marketing',
];
}

View file

@ -1,55 +0,0 @@
<?php
namespace App\Events;
use App\Models\Claim;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ClaimModelChanged
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/** @var \App\Models\Claim $claim Current claim modified. */
public $claim;
/** @var string $field Modified field. */
public $field;
/** @var string $label Label of the modified field. */
public $label;
/** @var \App\Models\User|null $user Current authenticated user. */
public $user;
/** @var string|null $content Custom content message. */
public $content;
/**
* Create a new event instance.
*
* @param \App\Models\Claim $claim
* @param string $field
* @param string $label
* @param \App\Models\User|null $user
* @param string|null $content
* @return void
*/
public function __construct(
Claim $claim,
string $field,
string $label,
User|null $user = null,
string|null $content = null,
) {
$this->claim = $claim;
$this->field = $field;
$this->label = $label;
$this->user = $user;
$this->content = $content;
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace App\Exceptions;
use Exception;
/**
* This class Exception is used to skip a row import from
* database migration (2016 -> 2020)
* @author Jérémy Munsch <jeremy.munsch@ldc.fr>
*/
class AbortRowImportException extends Exception
{
}

View file

@ -10,11 +10,11 @@ use Throwable;
/** /**
* This would be thrown if any of the case * This would be thrown if any of the case
* referenced in \App\Enums\ApiError would * referenced in \App\Enums\ApiError would
* occur within the crm. * occur within the crm
* *
* On the client an ApiErrorException would * On the client an ApiErrorException would
* also be throw with the ApiError corresponding to * also be throw with the ApiError corresponding to
* the situation. * the situation
* *
* @package LDCCrmClient * @package LDCCrmClient
* @subpackage exceptions * @subpackage exceptions
@ -34,7 +34,7 @@ class ApiErrorException extends Exception
/** /**
* This is an explicit error message, * This is an explicit error message,
* ou may print this to the front or not depending * ou may print this to the front or not depending
* on $error value. * on $error value
* *
* @var string * @var string
*/ */
@ -52,10 +52,10 @@ class ApiErrorException extends Exception
/** /**
* Create a new exception base on ApiError enum. * Create a new exception base on ApiError enum.
* *
* @param \App\Enums\ApiError $error * @param ApiError $error
* @param integer $httpCode * @param integer $httpCode
* @param string $message * @param string $message
* @param Throwable|null $previous * @param Throwable|null $previous
*/ */
public function __construct(ApiError $error, int $httpCode = 400, string $message = '', Throwable $previous = null) public function __construct(ApiError $error, int $httpCode = 400, string $message = '', Throwable $previous = null)
{ {
@ -63,16 +63,16 @@ class ApiErrorException extends Exception
$this->message = $message; $this->message = $message;
$this->httpCode = $httpCode; $this->httpCode = $httpCode;
parent::__construct( parent::__construct(
strlen($this->message) ? $this->message : $error->label(), strlen($this->message) ? $this->message : $error->label,
$error->value(), $error->value,
$previous $previous
); );
} }
/** /**
* Get the Api Error. * Get the Api Error
* *
* @return \App\Enums\ApiError * @return ApiError
*/ */
public function getApiError(): ApiError public function getApiError(): ApiError
{ {
@ -80,7 +80,7 @@ class ApiErrorException extends Exception
} }
/** /**
* Get the http code. * Get the http code
* *
* @return integer * @return integer
*/ */
@ -95,7 +95,7 @@ class ApiErrorException extends Exception
* *
* @return void * @return void
*/ */
public function report(): void public function report()
{ {
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
Log::debug(sprintf('CRM API: %s', $this->getMessage())); Log::debug(sprintf('CRM API: %s', $this->getMessage()));

View file

@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Log;
* *
* @package LDCCrmClient * @package LDCCrmClient
* @subpackage exceptions * @subpackage exceptions
* @codeCoverageIgnore
*/ */
class CrmException extends Exception class CrmException extends Exception
{ {
@ -21,7 +22,7 @@ class CrmException extends Exception
* *
* @return void * @return void
*/ */
public function report(): void public function report()
{ {
Log::critical(sprintf('CRM API: %s', $this->getMessage())); Log::critical(sprintf('CRM API: %s', $this->getMessage()));
} }

View file

@ -1,102 +0,0 @@
<?php
namespace App\Exceptions\Api\V4;
use App\Enums\ApiError;
use Illuminate\Support\Facades\Log;
/**
* This would be thrown if any of the case
* referenced in \App\Enums\ApiError would
* occur within the crm.
*
* On the client an ApiErrorException would
* also be throw with the ApiError corresponding to
* the situation.
*
* @package LDCCrmClient
* @subpackage exceptions
*/
class ApiErrorException extends \RuntimeException
{
/**
* Api Error
* This value makes the error more explicit.
* Handling these errors are normal as inputs or
* request may be undesired or refused by the crm.
*
* @var \App\Enums\ApiError
*/
protected $error;
/**
* This is an explicit error message,
* ou may print this to the front or not depending
* on $error value.
*
* @var string
*/
protected $message;
/**
* This is the http code returned by the crm.
* This value could be used to handle errors.
* But please take in consideration $error first.
*
* @var integer
*/
protected $httpCode;
/**
* Create a new exception base on ApiError enum.
*
* @param \App\Enums\ApiError $error
* @param integer $httpCode
* @param string $message
* @param \Throwable|null $previous
*/
public function __construct(ApiError $error, int $httpCode = 400, string $message = '', \Throwable $previous = null)
{
$this->error = $error;
$this->message = $message;
$this->httpCode = $httpCode;
parent::__construct(
strlen($this->message) ? $this->message : $error->label(),
$error->value(),
$previous
);
}
/**
* Get the Api Error.
*
* @return \App\Enums\ApiError
*/
public function getApiError(): ApiError
{
return $this->error;
}
/**
* Get the http code.
*
* @return integer
*/
public function getStatusCode(): int
{
return $this->httpCode;
}
/**
* This is used by laravel only.
* It uses Log class to print the error.
*
* @return void
*/
public function report(): void
{
// @codeCoverageIgnoreStart
Log::debug(sprintf('CRM API: %s', $this->getMessage()));
// @codeCoverageIgnoreEnd
}
}

View file

@ -1,28 +0,0 @@
<?php
namespace App\Exceptions\Api\V4;
use Illuminate\Support\Facades\Log;
/**
* This is a critical error.
* The request could not be handled. You may dump any value you can
* get before submitting a ticker to https://support.uwil.fr/
*
* @package LDCCrmClient
* @subpackage exceptions
*
*/
class CrmException extends \RuntimeException
{
/**
* This is used by laravel only.
* It uses Log class to print the error.
*
* @return void
*/
public function report(): void
{
Log::critical(sprintf('CRM API: %s', $this->getMessage()));
}
}

View file

@ -4,10 +4,8 @@ namespace App\Exceptions;
use App\Enums\ApiError; use App\Enums\ApiError;
use App\Enums\ApiStatus; use App\Enums\ApiStatus;
use App\Exceptions\Api\V2\ApiErrorException as V2ApiErrorException; use App\Exceptions\Api\V2\ApiErrorException;
use App\Exceptions\Api\V4\ApiErrorException as V4ApiErrorException;
use App\Http\Resources\Api\V2\ApiResponse as V2ApiResponse; use App\Http\Resources\Api\V2\ApiResponse as V2ApiResponse;
use App\Http\Resources\Api\V4\ApiResponse as V4ApiResponse;
use App\Lib\Helper; use App\Lib\Helper;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
@ -16,8 +14,7 @@ use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Kwaadpepper\ExceptionHandler\Exceptions\ExceptionHandler;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
@ -47,13 +44,8 @@ class Handler extends ExceptionHandler
* *
* @return void * @return void
*/ */
public function register(): void public function register()
{ {
$this->reportable(function (\Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
} }
/** /**
@ -63,10 +55,10 @@ class Handler extends ExceptionHandler
* @param \Throwable $e * @param \Throwable $e
* @return \Symfony\Component\HttpFoundation\Response * @return \Symfony\Component\HttpFoundation\Response
*/ */
public function render(mixed $request, \Throwable $e): Response public function render($request, \Throwable $e)
{ {
if ((Helper::isRequestForApiV2($request) || Helper::isRequestForApiV4($request)) and $request->isJson()) { if (Helper::isRequestForApiV2($request) and $request->isJson()) {
return $this->handleApiException($request, $e); return $this->handleApiV2Exception($request, $e);
} //end if } //end if
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
return parent::render($request, $e); return parent::render($request, $e);
@ -74,13 +66,13 @@ class Handler extends ExceptionHandler
} }
/** /**
* Generate a json response for any apiV4 error case. * Generate a json response for any apiV2 error case
* *
* @param \Illuminate\Http\Request $request * @param Request $request
* @param \Throwable $e * @param \Throwable $e
* @return \Illuminate\Http\JsonResponse * @return JsonResponse
*/ */
private function handleApiException(Request $request, \Throwable $e): JsonResponse private function handleApiV2Exception(Request $request, \Throwable $e): JsonResponse
{ {
switch (get_class($e)) { switch (get_class($e)) {
case AuthenticationException::class: case AuthenticationException::class:
@ -88,24 +80,24 @@ class Handler extends ExceptionHandler
return $this->getReponse( return $this->getReponse(
$request, $request,
401, 401,
ApiError::apiAuthFailed->value(), ApiError::apiAuthFailed()->value,
sprintf('%s, %s', ApiError::apiAuthFailed->label(), $e->getMessage()) sprintf('%s, %s', ApiError::apiAuthFailed()->label, $e->getMessage())
); );
case MethodNotAllowedHttpException::class: case MethodNotAllowedHttpException::class:
/** @var MethodNotAllowedHttpException $e */ /** @var MethodNotAllowedHttpException $e */
return $this->getReponse( return $this->getReponse(
$request, $request,
$e->getStatusCode(), $e->getStatusCode(),
ApiError::methodNotAllowed->value(), ApiError::methodNotAllowed()->value,
sprintf('%s, %s', ApiError::methodNotAllowed->label(), $e->getMessage()) sprintf('%s, %s', ApiError::methodNotAllowed()->label, $e->getMessage())
); );
case ValidationException::class: case ValidationException::class:
/** @var ValidationException $e */ /** @var ValidationException $e */
return $this->getReponse( return $this->getReponse(
$request, $request,
$e->status, $e->status,
ApiError::validationFailed->value(), ApiError::validationFailed()->value,
sprintf(ApiError::validationFailed->label(), $e->getMessage()), sprintf(ApiError::validationFailed()->label, $e->getMessage()),
['errors' => $e->errors()] ['errors' => $e->errors()]
); );
case ModelNotFoundException::class: case ModelNotFoundException::class:
@ -113,22 +105,14 @@ class Handler extends ExceptionHandler
return $this->getReponse( return $this->getReponse(
$request, $request,
404, 404,
ApiError::modelNotFound->value(), ApiError::modelNotFound()->value,
sprintf( sprintf(
ApiError::modelNotFound->label(), ApiError::modelNotFound()->label,
\array_slice(explode('\\', $e->getModel()), -1)[0] \array_slice(explode('\\', $e->getModel()), -1)[0]
) )
); );
case V4ApiErrorException::class: case ApiErrorException::class:
/** @var \App\Exceptions\Api\V4\ApiErrorException $e */ /** @var ApiErrorException $e */
return $this->getReponse(
$request,
$e->getStatusCode(),
$e->getCode(),
$e->getMessage()
);
case V2ApiErrorException::class:
/** @var \App\Exceptions\Api\V2\ApiErrorException $e */
return $this->getReponse( return $this->getReponse(
$request, $request,
$e->getStatusCode(), $e->getStatusCode(),
@ -140,36 +124,36 @@ class Handler extends ExceptionHandler
return $this->getReponse( return $this->getReponse(
$request, $request,
429, 429,
ApiError::throttleRequestsException->value(), ApiError::throttleRequestsException()->value,
ApiError::throttleRequestsException->label() ApiError::throttleRequestsException()->label
); );
case AuthorizationException::class: case AuthorizationException::class:
/** @var AuthorizationException $e */ /** @var AuthorizationException $e */
return $this->getReponse( return $this->getReponse(
$request, $request,
403, 403,
ApiError::authorizationException->value(), ApiError::authorizationException()->value,
ApiError::authorizationException->label() ApiError::authorizationException()->label
); );
} //end switch } //end switch
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
if (config('app.env') === 'testing') { if (config('app.env') === 'testing') {
// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found
print_r((string) $e); print_r((string)$e);
} }
return $this->getReponse($request, 500, \intval($e->getCode()), $e->getMessage()); return $this->getReponse($request, 500, \intval($e->getCode()), $e->getMessage());
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
/** /**
* Generate an ApiV4 json response. * Generate an ApiV2 json response
* *
* @param \Illuminate\Http\Request $request * @param Request $request
* @param integer $httpCode * @param integer $httpCode
* @param integer $apiCode * @param integer $apiCode
* @param string $message * @param string $message
* @param array $data * @param array $data
* @return \Illuminate\Http\JsonResponse * @return JsonResponse
*/ */
private static function getReponse( private static function getReponse(
Request $request, Request $request,
@ -178,15 +162,9 @@ class Handler extends ExceptionHandler
string $message, string $message,
array $data = [] array $data = []
): JsonResponse { ): JsonResponse {
if (Helper::isRequestForApiV2($request)) { return (new V2ApiResponse(
return (new V2ApiResponse( (object)array_merge($data, ['message' => $message, 'code' => $apiCode]),
ApiStatus::KO, ApiStatus::KO()
(object) array_merge($data, ['message' => $message, 'code' => $apiCode]),
))->toResponse($request)->setStatusCode($httpCode);
}
return (new V4ApiResponse(
ApiStatus::KO,
(object) array_merge($data, ['message' => $message, 'code' => $apiCode]),
))->toResponse($request)->setStatusCode($httpCode); ))->toResponse($request)->setStatusCode($httpCode);
} }
} }

View file

@ -1,55 +1,50 @@
<?php <?php
namespace App\Http\Controllers\Bo; namespace App\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Answers\StoreAnswerRequest; use App\Http\Requests\Answers\StoreAnswerRequest;
use App\Http\Requests\Bo\Answers\UpdateAnswerRequest; use App\Http\Requests\Answers\UpdateAnswerRequest;
use App\Models\Answer; use App\Models\Answer;
use App\Models\Question; use App\Models\Question;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class AnswerController extends Controller class AnswerController extends Controller
{ {
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Answer::class);
}
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function index(Request $request): \Illuminate\Contracts\View\View public function index(Request $request)
{ {
/** @var \App\Models\Question $questionModel */ /** @var \App\Models\Question $questionModel */
$questionModel = $request->question; $questionModel = $request->question;
/** @var \Illuminate\Database\Eloquent\Builder $query */ /** @var \Illuminate\Database\Eloquent\Builder $answers */
$query = Answer::query()->where('question_id', $questionModel->id); $answers = Answer::query()->where('question_id', $questionModel->id);
if ($request->search) { if ($request->search) {
$this->searchQuery( $this->searchQuery(
$query, $answers,
$request->search, $request->search,
null, null,
'answer', 'answer',
); );
} }
$this->sortQuery($query); $this->sortQuery($answers);
$searchFields = trans('Réponse'); $searchFields = trans('Réponse');
$answerModels = $query->paginate($this->getPagination()); $answerModels = $answers->paginate(12);
return view('bo.pages.answers.index', compact('answerModels', 'questionModel', 'searchFields')); return view('answers.index', compact('answerModels', 'questionModel', 'searchFields'));
} }
/** /**
@ -57,35 +52,33 @@ class AnswerController extends Controller
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \App\Models\Answer $answer * @param \App\Models\Answer $answer
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function create(Request $request, Answer $answer): \Illuminate\Contracts\View\View public function create(Request $request, Answer $answer)
{ {
/** @var \App\Models\Question $questionModel */ /** @var \App\Models\Question $questionModel */
$questionModel = $request->question; $questionModel = $request->question;
return view('bo.pages.answers.create', [ return view('answers.create', [
'answerModel' => $answer, 'answerModel' => $answer,
'questionModel' => $questionModel, 'questionModel' => $questionModel
]); ]);
} }
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \App\Http\Requests\Bo\Answers\StoreAnswerRequest $request * @param \App\Http\Requests\Answers\StoreAnswerRequest $request
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response
*/ */
public function store(StoreAnswerRequest $request): \Illuminate\Http\RedirectResponse public function store(StoreAnswerRequest $request)
{ {
return DB::transaction(function () use ($request) { return DB::transaction(function () use ($request) {
$answer = new Answer(); $answer = new Answer();
$answer->fill($request->validated()); $answer->fill($request->validated());
$questionModel = $request->route()->parameter('question');
$answer->question()->associate($questionModel->getKey());
if ($answer->saveOrFail()) { if ($answer->saveOrFail()) {
return redirect() return redirect()
->route('bo.questions.answers.edit', ['question' => $questionModel, 'answer' => $answer]) ->route('answers.edit', $answer)
->with('success', trans( ->with('success', trans(
':model a été créé.', ':model a été créé.',
['model' => Str::singular(trans('models.classes.answers'))] ['model' => Str::singular(trans('models.classes.answers'))]
@ -105,34 +98,31 @@ class AnswerController extends Controller
* *
* @param \App\Models\Question $question * @param \App\Models\Question $question
* @param \App\Models\Answer $answers * @param \App\Models\Answer $answers
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function edit(Question $question, Answer $answers): \Illuminate\Contracts\View\View public function edit(Question $question, Answer $answers)
{ {
return view('bo.pages.answers.edit', [ return view('answers.edit', [
'answerModel' => $answers, 'answerModel' => $answers,
'questionModel' => $question, 'questionModel' => $question
]); ]);
} }
/** /**
* Update a spacific resource in storage. * Update a spacific resource in storage.
* *
* @param \App\Http\Requests\Bo\Answers\UpdateAnswerRequest $request * @param \App\Http\Requests\Answers\UpdateAnswerRequest $request
* @param \App\Models\Question $question * @param \App\Models\Question $question
* @param \App\Models\Answer $answer * @param \App\Models\Answer $answer
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response
*/ */
public function update( public function update(UpdateAnswerRequest $request, Question $question, Answer $answer)
UpdateAnswerRequest $request, {
Question $question,
Answer $answer
): \Illuminate\Http\RedirectResponse {
return DB::transaction(function () use ($request, $question, $answer) { return DB::transaction(function () use ($request, $question, $answer) {
$answer->fill($request->validated()); $answer->fill($request->validated());
if ($answer->saveOrFail()) { if ($answer->saveOrFail()) {
return redirect() return redirect()
->route('bo.questions.answers.edit', ['question' => $question, 'answer' => $answer]) ->route('questions.answers.edit', ['question' => $question, 'answer' => $answer])
->with('success', trans( ->with('success', trans(
':model a été mis à jour.', ':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.answers'))] ['model' => Str::singular(trans('models.classes.answers'))]
@ -156,9 +146,8 @@ class AnswerController extends Controller
*/ */
public function destroy(Question $question, Answer $answer): \Illuminate\Http\RedirectResponse public function destroy(Question $question, Answer $answer): \Illuminate\Http\RedirectResponse
{ {
$answer->singleOptins()->detach();
if ($answer->deleteOrFail()) { if ($answer->deleteOrFail()) {
return redirect()->route('bo.questions.answers.index', $question) return redirect()->route('questions.answers.index', $question)
->with('success', trans( ->with('success', trans(
':model a été supprimé.', ':model a été supprimé.',
['model' => Str::singular(trans('models.classes.answers'))] ['model' => Str::singular(trans('models.classes.answers'))]

View file

@ -0,0 +1,363 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Resources\StatusResource;
use App\Mail\CourrierArcadiaError;
use App\Models\Customer;
use DOMDocument;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
/**
* @codeCoverageIgnore
*/
class ArcadiaController extends Controller
{
/**
* @param \Illuminate\Http\Request $request
* @return \App\Http\Resources\StatusResource
*/
public function outgoingMail(Request $request)
{
$validator = Validator::make($request->all(), [
'crm_id' => 'required',
'fields' => 'required|json',
'type' => 'required',
'shipping' => 'required_if:type,default|json'
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO',
'code' => 201,
'message' => 'Error on required fields.',
'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$crmId = $request->crm_id;
$fields = json_decode($request->fields);
$type = $request->type;
$shipping = $type === 'default' ? json_decode($request->shipping) : false;
$customer = Customer::where('id', $crmId)->first();
if (!$customer) {
return new StatusResource(['status' => 'KO', 'code' => 202, 'message' => 'User not found.']);
}
$CustomerInfo = $customer->infos()->where('brand_id', $brandId)->first();
if (!$CustomerInfo) {
return new StatusResource(['status' => 'KO', 'code' => 202, 'message' => 'User not found.']);
}
$check = $this->checkRequiredFields($fields, $type);
if ($check['status'] !== 'OK') {
return new StatusResource($check);
}
if ($type === 'default') {
$check = $this->checkShippingRequiredFields($shipping);
if ($check['status'] !== 'OK') {
return new StatusResource($check);
}
}
$xml = $this->makeXml($fields, $type, $customer, $CustomerInfo, $shipping);
try {
Storage::disk('arcadia')->put(date("Ymd_His") . microtime(true) . ".xml", $xml);
return new StatusResource(['status' => 'OK']);
} catch (\Exception $e) {
Log::critical("ARCADIA XML CREATION FAILED : " . $e->getMessage());
Mail::to(config('mail.MAIL_ADMIN'))->send(new CourrierArcadiaError(
"ARCADIA XML CREATION FAILED",
"Une erreur est survenue lors de la création du fichier XML : " . $e->getMessage(),
$xml ?: ''
));
return new StatusResource(['status' => 'KO', 'code' => 204, 'message' => 'Arcadia XML Creation failed.']);
}
}
/**
* @param mixed $fields
* @return array
*/
private function checkShippingRequiredFields($fields)
{
$test = [
'firstname',
'lastname',
'address',
'city',
'postcode'
];
foreach ($test as $t) {
if (!array_key_exists($t, $fields)) {
$errors[] = ['field' => $t, 'error' => 'required'];
}
}
if (!empty($errors)) {
return ['status' => 'KO', 'code' => 203, 'message' => $errors];
}
return ['status' => 'OK'];
}
/**
*
* @param mixed $fields
* @param mixed $type
* @return array
*/
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, Squiz.Commenting.FunctionComment.Missing
private function checkRequiredFields($fields, $type): array
{
$errors = [];
$key = array_search('operation', array_column($fields->root, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'operation', 'error' => 'required'];
}
$key = array_search('support_code', array_column($fields->orderLine, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'support_code', 'error' => 'required'];
}
$key = array_search('label_type_support', array_column($fields->orderLine, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'label_type_support', 'error' => 'required'];
}
$key = array_search('order_date', array_column($fields->root, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'order_date', 'error' => 'required'];
}
$regex = join('', [
'/^(((0[1-9]|[12]\d|3[01])\/(0[13578]|1[02])\/((19|[2-9]\d)\d{2}))|',
'((0[1-9]|[12]\d|30)\/(0[13456789]|1[012])\/((19|[2-9]\d)\d{2}))',
'|((0[1-9]|1\d|2[0-8])\/02\/((19|[2-9]\d)\d{2}))|(29\/02\/((1[6-9]|[2-9]\d)(0[48]',
'|[2468][048]|[13579][26])|((16|[2468][048]|[3579][26])00))))$/'
]);
if (!preg_match($regex, $fields->root[$key]->fieldValue)) {
$errors[] = ['field' => 'order_date', 'error' => 'format must be DD/MM/YYYY'];
}
if ($type === 'default') {
$key = array_search('order_num', array_column($fields->root, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'order_num', 'error' => 'required for default type'];
}
if (!is_numeric($fields->root[$key]->fieldValue) || strlen($fields->root[$key]->fieldValue) !== 9) {
$errors[] = ['field' => 'order_num', 'error' => 'format must be 9 numeric characters'];
}
$key = array_search('state_order_cb', array_column($fields->root, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'state_order_cb', 'error' => 'required for default type'];
}
if ($fields->root[$key]->fieldValue !== 1 && $fields->root[$key]->fieldValue !== 'Null') {
$errors[] = ['field' => 'state_order_cb', 'error' => 'value must be 1 or Null (Yes Null not NULL)'];
}
$orderCB = $fields->root[$key]->fieldValue;
$key = array_search('state_order_check', array_column($fields->root, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'state_order_check', 'error' => 'required for default type'];
}
if ($fields->root[$key]->fieldValue !== 1 && $fields->root[$key]->fieldValue !== 'Null') {
$errors[] = ['field' => 'state_order_check', 'error' => 'value must be 1 or Null (Yes Null not NULL)'];
}
$orderCheck = $fields->root[$key]->fieldValue;
if ($orderCB === $orderCheck) {
$errors[] = [
'field' => 'state_order',
'error' => 'state_order_cb and state_order_check values must be different'
];
}
$key = array_search('package_unit_weight', array_column($fields->orderLine, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'package_unit_weight', 'error' => 'required for default type'];
}
} elseif ($type === 'game') {
$key = array_search('code_unique', array_column($fields->orderLine, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'code_unique', 'error' => 'required for game type'];
}
}//end if
if (!empty($errors)) {
return ['status' => 'KO', 'code' => 203, 'message' => $errors];
}
return ['status' => 'OK'];
}
/**
* @param mixed $fields
* @param mixed $type
* @param mixed $customer
* @param mixed $customerInfos
* @param mixed $shipping
* @return string|false
*/
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, Squiz.Commenting.FunctionComment.Missing
private function makeXml($fields, $type, $customer, $customerInfos, $shipping)
{
$pivot = [
'order_num' => 'Num_Commande',
// Required if type = default - format 9 numeric characters.
'order_date' => 'Date_Commande',
// Required - format : DD/MM/YYYY .
'operation' => 'Operation',
// Required.
'support_code' => 'Code_Support',
// Required.
'label_type_support' => 'Libelle_Type_Support',
// Required.
'package_unit_weight' => 'Poids_Unitaire_Colis',
// Required if type = default.
'qty' => 'Quantite',
'code_on_pack' => 'Code_on_Pack',
'code_unique' => 'Code_Unique',
// Required if type = game.
// Required if type = default - format 1 ou Null (oui Null et pas NULL).
'state_order_cb' => 'Etat_Commande_CB',
// Required if type = default - format 1 ou Null (oui Null et pas NULL).
'state_order_check' => 'Etat_Commande_Cheque',
];
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$root = $dom->createElement('export_arcadia');
$dom->appendChild($root);
$order = $root->appendChild($dom->createElement('Commande'));
$key = array_search('order_num', array_column($fields->root, 'fieldName'));
if ($key === false) {
$order->appendChild($dom->createElement('Num_Commande'));
} else {
$order->appendChild($dom->createElement('Num_Commande', $fields->root[$key]->fieldValue));
}
foreach ($fields->root as $value) {
// phpcs:ignore PSR2.ControlStructures.ControlStructureSpacing.SpacingAfterOpenBrace
if (
isset($pivot[$value->fieldName]) && $value->fieldName !== 'state_order_cb' &&
$value->fieldName !== 'state_order_check' && $value->fieldName !== 'order_num'
) {
$order->appendChild($dom->createElement($pivot[$value->fieldName], $value->fieldValue));
}
}
if ($type === 'default') {
$key = array_search('state_order_cb', array_column($fields->root, 'fieldName'));
if ($key) {
$order->appendChild($dom->createElement('Etat_Commande_CB', $fields->root[$key]->fieldValue));
}
$key = array_search('state_order_check', array_column($fields->root, 'fieldName'));
if ($key) {
$order->appendChild($dom->createElement('Etat_Commande_Cheque', $fields->root[$key]->fieldValue));
}
}
$order->appendChild($dom->createElement('Email_conso'))
->appendChild($dom->createCDATASection("$customer->email"));
$order->appendChild($dom->createElement('Civilite'));
if ($type === 'default') {
$order->appendChild($dom->createElement('Nom'))
->appendChild($dom->createCDATASection("$shipping->lastname"));
$order->appendChild($dom->createElement('Prenom'))
->appendChild($dom->createCDATASection("$shipping->firstname"));
$order->appendChild($dom->createElement('Adresse'))
->appendChild($dom->createCDATASection("$shipping->address"));
$order->appendChild($dom->createElement('Adresse_Complementaire'));
$order->appendChild($dom->createElement('CP'))
->appendChild($dom->createCDATASection("$shipping->postcode"));
$order->appendChild($dom->createElement('Ville'))->appendChild($dom->createCDATASection("$shipping->city"));
if (isset($shipping->phone) && !empty($shipping->phone)) {
$order->appendChild($dom->createElement('Telephone'))
->appendChild($dom->createCDATASection("$shipping->phone"));
} else {
$order->appendChild($dom->createElement('Telephone'));
}
} else {
$order->appendChild($dom->createElement('Nom'))
->appendChild($dom->createCDATASection("$customerInfos->lastname"));
$order->appendChild($dom->createElement('Prenom'))
->appendChild($dom->createCDATASection("$customerInfos->firstname"));
$order->appendChild($dom->createElement('Adresse'))
->appendChild($dom->createCDATASection("$customerInfos->address"));
$order->appendChild($dom->createElement('Adresse_Complementaire'))
->appendChild($dom->createCDATASection("$customerInfos->address2"));
$order->appendChild($dom->createElement('CP'))
->appendChild($dom->createCDATASection("$customerInfos->postcode"));
$order->appendChild($dom->createElement('Ville'))
->appendChild($dom->createCDATASection("$customerInfos->city"));
$order->appendChild($dom->createElement('Telephone'))
->appendChild($dom->createCDATASection("$customerInfos->phone"));
}//end if
$orderLine = $order->appendChild($dom->createElement('Ligne_de_Commande'));
$key = array_search('qty', array_column($fields->orderLine, 'fieldName'));
if ($key === false) {
$orderLine->appendChild($dom->createElement('Quantite', '1'));
}
$key = array_search('package_unit_weight', array_column($fields->orderLine, 'fieldName'));
if ($key === false) {
$orderLine->appendChild($dom->createElement('Poids_Unitaire_Colis'));
}
foreach ($fields->orderLine as $value) {
// phpcs:ignore PSR2.ControlStructures.ControlStructureSpacing.SpacingAfterOpenBrace
if (
isset($pivot[$value->fieldName]) &&
$value->fieldName !== 'code_on_pack' &&
$value->fieldName !== 'code_unique'
) {
$orderLine->appendChild($dom->createElement($pivot[$value->fieldName], $value->fieldValue));
}
}
if ($type === 'game') {
$key = array_search('code_on_pack', array_column($fields->orderLine, 'fieldName'));
if ($key !== false) {
$orderLine->appendChild(
$dom->createElement('Code_on_Pack')
)->appendChild($dom->createCDATASection($fields->orderLine[$key]->fieldValue));
}
$key = array_search('code_unique', array_column($fields->orderLine, 'fieldName'));
if ($key !== false) {
$orderLine->appendChild(
$dom->createElement('Code_Unique')
)->appendChild($dom->createCDATASection($fields->orderLine[$key]->fieldValue));
}
}
return $dom->saveXML();
}
}

View file

@ -0,0 +1,574 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Enums\Country;
use App\Http\Controllers\Controller;
use App\Http\Resources\CustomerResource;
use App\Http\Resources\StatusResource;
use App\Models\Customer;
use App\Models\CustomerInfo;
use App\Models\CustomerLogin;
use App\Models\CustomerOptInHistory;
use App\Models\SingleOptin;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class CustomerController extends Controller
{
/**
* Store a newly created resource in storage
* @param \Illuminate\Http\Request $request
* @return \App\Http\Resources\CustomerResource|\App\Http\Resources\StatusResource
*/
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, Squiz.Commenting.FunctionComment.Missing
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'password' => 'required',
'firstname' => 'required',
'lastname' => 'required',
'opt_in' => 'required',
'birth_date' => 'date_format:Y-m-d'
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 102,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$email = strtolower(filter_var($request->email, FILTER_SANITIZE_EMAIL));
$firstname = filter_var($request->firstname, FILTER_SANITIZE_STRING);
$lastname = filter_var($request->lastname, FILTER_SANITIZE_STRING);
$optIn = filter_var($request->opt_in, FILTER_SANITIZE_STRING);
$hashcode = Str::uuid();
// Récupère le customer s'il existe déjà.
$customer = Customer::where('email', $email)->first();
// Si le customer n'existe pas déjà.
if (!$customer) {
$customer = new Customer();
$customer->email = $email;
$customer->save();
$singleOptin = SingleOptin::query()->where([
['email', $email], ['brand_id', $brandId]
])->first();
if ($singleOptin) {
$singleOptin->delete();
}
} else {
// Récupère les infos s'ils existent déjà.
$CustomerInfo = CustomerInfo::query()->where([
['customer_id', $customer->id], ['brand_id', $brandId]
])->first();
if ($CustomerInfo) {
return new StatusResource([
'status' => 'KO', 'code' => 103,
'message' => 'User already registered on this site.'
]);
}
}//end if
$customerInfos = new CustomerInfo();
$customerInfos->firstname = ucfirst(strtolower($firstname));
$customerInfos->lastname = ucfirst(strtolower($lastname));
$customerInfos->birth_date = !empty($request->birth_date) ?
filter_var($request->birth_date, FILTER_SANITIZE_STRING) : null;
$customerInfos->address = !empty($request->address) ?
filter_var($request->address, FILTER_SANITIZE_STRING) : null;
$customerInfos->address2 = !empty($request->address2) ?
filter_var($request->address2, FILTER_SANITIZE_STRING) : null;
$customerInfos->phone = !empty($request->phone) ?
filter_var($request->phone, FILTER_SANITIZE_STRING) : null;
try {
$customerInfos->country = Country::make(filter_var($request->country, FILTER_SANITIZE_STRING));
} catch (\Exception $e) {
$customerInfos->country = Country::france();
}
$customerInfos->city = !empty($request->city) ?
filter_var($request->city, FILTER_SANITIZE_STRING) : null;
$customerInfos->postcode = !empty($request->postcode) ?
filter_var($request->postcode, FILTER_SANITIZE_STRING) : null;
$customerInfos->opt_in = $optIn;
$customerInfos->hashcode = $hashcode;
$customerInfos->brand_id = $brandId;
$customerInfos->customer_id = $customer->id;
$customer->infos()->save($customerInfos);
// Création du login pour cet utilisateur sur ce service.
$customerLogin = new CustomerLogin();
$customerLogin->customer_id = $customer->id;
$customerLogin->password = Hash::make(trim($request->password));
$customerLogin->last_connection = now();
$customerLogin->brand_id = $brandId;
$customerLogin->status = 0;
$customer->logins()->save($customerLogin);
// Ajout dans la table d'historique des opt-in de la nouvelle valeur.
$optInHistory = new CustomerOptInHistory();
$optInHistory->customer_id = $customer->id;
$optInHistory->brand_id = $brandId;
$optInHistory->new_value = $optIn;
$optInHistory->save();
return new CustomerResource($customer, $brandId);
}
/**
* Display the specified resource.
*
* @param \Illuminate\Http\Request $request
* @param mixed $type
* @param mixed $value
* @return \App\Http\Resources\CustomerResource|\App\Http\Resources\StatusResource
*/
public function show(Request $request, $type, $value)
{
$brandId = $request->brand_id;
if ($type === 'id') {
$customer = Customer::find($value);
if (!$customer) {
return new StatusResource(['status' => 'KO', 'code' => 101, 'message' => 'User not found.']);
}
} elseif ($type === 'email') {
$customer = Customer::where('email', $value)->first();
if (!$customer) {
return new StatusResource(['status' => 'KO', 'code' => 101, 'message' => 'User not found.']);
}
$CustomerInfo = $customer->infos()->where('brand_id', $brandId)->first();
if (!$CustomerInfo) {
return new StatusResource(['status' => 'KO', 'code' => 101, 'message' => 'User not found.']);
}
} else {
return new StatusResource(['status' => 'KO', 'code' => 101, 'message' => 'User not found.']);
}
return new CustomerResource($customer, $brandId);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param integer $id
* @return \App\Http\Resources\CustomerResource|\App\Http\Resources\StatusResource
*/
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded, Squiz.Commenting.FunctionComment.Missing
public function update(Request $request, $id)
{
$validator = Validator::make($request->all(), [
'birth_date' => 'date_format:Y-m-d'
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 104,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$customer = Customer::find($id);
if (!$customer) {
return new StatusResource(['status' => 'KO', 'code' => 105, 'message' => 'User not found.']);
}
$CustomerInfo = $customer->infos()->where('brand_id', $brandId)->first();
if (!$CustomerInfo) {
return new StatusResource(['status' => 'KO', 'code' => 105, 'message' => 'User not found.']);
}
if ($request->has('firstname')) {
$CustomerInfo->firstname = filter_var($request->firstname, FILTER_SANITIZE_STRING);
}
if ($request->has('lastname')) {
$CustomerInfo->lastname = filter_var($request->lastname, FILTER_SANITIZE_STRING);
}
if ($request->has('birth_date')) {
if (!empty($request->birth_date)) {
$CustomerInfo->birth_date = filter_var($request->birth_date, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->birth_date = null;
}
}
if ($request->has('address')) {
if (!empty($request->address)) {
$CustomerInfo->address = filter_var($request->address, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->address = null;
}
}
if ($request->has('address2')) {
if (!empty($request->address2)) {
$CustomerInfo->address2 = filter_var($request->address2, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->address2 = null;
}
}
if ($request->has('phone')) {
if (!empty($request->phone)) {
$CustomerInfo->phone = filter_var($request->phone, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->phone = null;
}
}
if ($request->has('country')) {
try {
$CustomerInfo->country = Country::make(filter_var($request->country, FILTER_SANITIZE_STRING));
} catch (\Exception $e) {
$CustomerInfo->country = Country::france();
}
}
if ($request->has('city')) {
if (!empty($request->city)) {
$CustomerInfo->city = filter_var($request->city, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->city = null;
}
}
if ($request->has('postcode')) {
if (!empty($request->postcode)) {
$CustomerInfo->postcode = filter_var($request->postcode, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->postcode = null;
}
}
if ($request->has('opt_in')) {
$optIn = filter_var($request->opt_in, FILTER_SANITIZE_STRING);
if ($CustomerInfo->opt_in != $optIn) {
// Ajout dans la table d'historique des opt-in de la nouvelle valeur.
$optInHistory = new CustomerOptInHistory();
$optInHistory->customer_id = $customer->id;
$optInHistory->brand_id = $brandId;
$optInHistory->new_value = $optIn;
$optInHistory->save();
}
$CustomerInfo->opt_in = $optIn;
}
$CustomerInfo->save();
return new CustomerResource($customer, $brandId);
}
/**
* Authenticate the user
* @param \Illuminate\Http\Request $request
* @return \App\Http\Resources\CustomerResource|\App\Http\Resources\StatusResource
*/
public function authenticate(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'password' => 'required',
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 115,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$email = filter_var($request->email, FILTER_SANITIZE_EMAIL);
$password = trim($request->password);
$customer = Customer::where('email', $email)->first();
if ($customer) {
$login = $customer->logins()->where('brand_id', $brandId)->first();
if ($login) {
if (Hash::check($password, $login->password)) {
if ($login->status === false) {
$infos = $customer->infos()->where('brand_id', $brandId)->first();
// "Le compte de l'utilisateur n'est pas activé"
return new StatusResource(
[
'status' => 'KO',
'code' => 116,
'message' => 'Account not activated.',
'user' => [
'crm_id' => $customer->id,
'firstname' => $infos->firstname,
'lastname' => $infos->lastname,
'email' => $customer->email,
'birth_date' => $infos->birth_date,
'address' => $infos->address,
'address2' => $infos->address2,
'phone' => $infos->phone,
'country' => $infos->country->label,
'city' => $infos->city,
'postcode' => $infos->postcode,
'opt_in' => $infos->opt_in,
'hashcode' => $infos->hashcode,
'status' => $login->status,
]
]
);
} //end if
$login->last_connection = now();
$customer->logins()->save($login);
return new CustomerResource($customer, $brandId);
} //end if
// "Mot de passe incorrect"
return new StatusResource(['status' => 'KO', 'code' => 117, 'message' => 'Authentication failed.']);
} else {
// "Ce compte n'est pas enregistré pour cette marque"
return new StatusResource(['status' => 'KO', 'code' => 118, 'message' => 'User not found.']);
} //end if
} else {
// "Ce compte n'existe pas dans la CRM"
return new StatusResource(['status' => 'KO', 'code' => 119, 'message' => 'User not found.']);
} //end if
}
/**
* Change the password of the user for the brand
* @param \Illuminate\Http\Request $request
* @param mixed $id
* @return \App\Http\Resources\StatusResource
*/
public function changePassword(Request $request, $id)
{
$validator = Validator::make($request->all(), [
'password' => 'required',
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 107,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$customer = Customer::find($id);
$password = trim($request->password);
if ($customer) {
$customerLogin = $customer->logins()->where('brand_id', $brandId)->first();
if ($customerLogin) {
// Check if same password.
if (Hash::check($password, $customerLogin->password)) {
return new StatusResource(['status' => 'KO', 'code' => 123, 'message' => 'Same password.']);
}
$customerLogin->password = Hash::make($password);
$customerLogin->save();
} else {
// "Ce compte n'est pas enregistré pour cette marque"
return new StatusResource(['status' => 'KO', 'code' => 108, 'message' => 'User not found.']);
}
} else {
// "Ce compte n'existe pas"
return new StatusResource(['status' => 'KO', 'code' => 109, 'message' => 'User not found.']);
}
return new StatusResource(['status' => 'OK', 'message' => 'Update complete.']);
}
/**
* Activate the account of the user
* @param \Illuminate\Http\Request $request
* @param mixed $id
* @return \App\Http\Resources\StatusResource
*/
public function activateAccount(Request $request, $id)
{
$brandId = $request->brand_id;
$customer = Customer::find($id);
if ($customer) {
$login = $customer->logins()->where('brand_id', $brandId)->first();
if ($login) {
if ($login->status === false) {
$login->status = true;
$login->save();
// "Le compte a bien été activé"
return new StatusResource(['status' => 'OK', 'message' => 'Update complete.']);
} else {
// "Ce compte est déjà activé"
return new StatusResource(['status' => 'KO', 'code' => 110, 'message' => 'User already active.']);
}
} else {
// "Ce compte n'est pas enregistré pour cette marque"
return new StatusResource(['status' => 'KO', 'code' => 111, 'message' => 'User not found.']);
}
} else {
// "Cet utilisateur n'existe pas dans la CRM"
return new StatusResource(['status' => 'KO', 'code' => 112, 'message' => 'User not found.']);
}
}
/**
* Delete the user account for the brand
* @param \Illuminate\Http\Request $request
* @param mixed $id
*/
// phpcs:disable Squiz.PHP.NonExecutableCode.Unreachable, Squiz.Commenting.FunctionComment.Missing
public function delete(Request $request, $id)
{
die("Not implemented yet");
// TODO : Gestion avec mesBR.
$brandId = $request->brand_id;
$customer = Customer::find($id);
if ($customer) {
$customer->infos()->where('brand_id', $brandId)->first()->delete();
$customer->logins()->where('brand_id', $brandId)->first()->delete();
if (count($customer->infos) === 0 && count($customer->logins) === 0) {
$customer->optInHistory()->delete();
$customer->delete();
// Suppression du compte de la CRM terminée.
return new StatusResource(['status' => 'OK', 'message' => 'Deletion complete.']);
} else {
// Suppression des informations du compte pour la marque terminée.
return new StatusResource(['status' => 'OK', 'message' => 'Deletion complete.']);
}
} else {
return new StatusResource(['status' => 'KO', 'code' => 106, 'message' => 'User not found.']);
}
// phpcs:enable Squiz.PHP.NonExecutableCode.Unreachable, Squiz.Commenting.FunctionComment.Missing
}
/**
* Change the status of the opt-in for the customer on this brand
* @param \Illuminate\Http\Request $request
* @param mixed $id
* @return \App\Http\Resources\StatusResource
*/
public function toggleOptin(Request $request, $id)
{
// TODO : Liaison des opt-in avec MailJet.
$brandId = $request->brand_id;
$customer = Customer::find($id);
if ($customer) {
$infos = $customer->infos()->where('brand_id', $brandId)->first();
if ($infos) {
$infos->opt_in = $infos->opt_in === 0 ? 1 : 0;
// On enregistre l'historique du changement d'optin.
$optinHistory = new CustomerOptInHistory();
$optinHistory->customer_id = $customer->id;
$optinHistory->new_value = $infos->opt_in;
$optinHistory->brand_id = $brandId;
$optinHistory->save();
$infos->save();
// "Changement effectué"
return new StatusResource(['status' => 'OK', 'message' => 'Update complete.']);
} else {
// "Ce compte n'a pas d'information liés à cette marque"
return new StatusResource(['status' => 'KO', 'code' => 113, 'message' => 'User not found.']);
}
} else {
// "Cet utilisateur n'existe pas dans la CRM"
return new StatusResource(['status' => 'KO', 'code' => 114, 'message' => 'User not found.']);
} //end if
}
/**
* Remove opt-in for the customer on this brand
* @param \Illuminate\Http\Request $request
* @param mixed $hashcode
* @return \App\Http\Resources\StatusResource
*/
public function removeOptin(Request $request, $hashcode)
{
// TODO : Liaison des opt-in avec MailJet.
$brandId = $request->brand_id;
$infos = CustomerInfo::where([
['brand_id', $brandId],
['hashcode', $hashcode]
])->first();
if ($infos) {
$infos->opt_in = 0;
// On enregistre l'historique du changement d'optin.
$optinHistory = new CustomerOptInHistory();
$optinHistory->customer_id = $infos->customer_id;
$optinHistory->new_value = 0;
$optinHistory->brand_id = $brandId;
$optinHistory->save();
$infos->save();
// "Changement effectué"
return new StatusResource(['status' => 'OK', 'message' => 'Update complete.']);
} else {
// "Ce compte n'a pas d'information liés à cette marque"
return new StatusResource(['status' => 'KO', 'code' => 122, 'message' => 'User not found.']);
}
}
/**
* @param \Illuminate\Http\Request $request
* @return void
*/
// phpcs:ignore Squiz.PHP.NonExecutableCode.Unreachable, Squiz.Commenting.FunctionComment.Missing
public function addSingleOptin(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 120,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$email = filter_var($request->email, FILTER_SANITIZE_EMAIL);
// Check si le customer ou l'optin simple existe avant de l'ajouter en optin simple.
if (
!SingleOptin::query()->where([['email', $email], ['brand_id', $brandId]])->exists() &&
!CustomerInfo::join("customers", "customer_infos.customer_id", "=", "customers.id")->where([
['brand_id', $brandId], ['email', $email]
])->exists()
) {
$singleOptin = new SingleOptin();
$singleOptin->email = $email;
$singleOptin->brand_id = $brandId;
$singleOptin->save();
} else {
// "Cet utilisateur est déjà inscrit à la newsletter pour cette marque"
return new StatusResource([
'status' => 'KO', 'code' => 121,
'message' => 'This user is already subscribed to the newsletter for this brand.'
]);
}
return new StatusResource(['status' => 'OK', 'message' => 'Optin added.']);
}
}

View file

@ -4,43 +4,32 @@ namespace App\Http\Controllers\Api\V2;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V2\StoreCustomerAnswers; use App\Http\Requests\Api\V2\StoreCustomerAnswers;
use App\Models\Answer;
use App\Models\Customer; use App\Models\Customer;
use App\Models\ModelAnswer; use App\Models\Service;
use App\Models\Question; use Illuminate\Http\JsonResponse;
class AnswerController extends Controller class AnswerController extends Controller
{ {
/** /**
* Store the customer answers. * Store the customer answers
* *
* @param \App\Http\Requests\Api\V2\StoreCustomerAnswers $request * @param \App\Http\Requests\Api\V2\StoreCustomerAnswers $request
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer * @param \App\Models\Customer $customer
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function storeCustomerAnswers( public function storeCustomerAnswers(
StoreCustomerAnswers $request, StoreCustomerAnswers $request,
Service $service,
Customer $customer Customer $customer
): \Illuminate\Http\JsonResponse { ): JsonResponse {
// * Make sure we are accessing a customer that belongs to our service. // * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer); $this->authorize('customerBelongsToService', $customer);
$question = Question::find($request->validated('question')); $existingAnswerIds = $customer->getAnswers($service)->pluck('id');
$answersID = Answer::whereIn('id', $request->answers)->pluck('id');
// Column model_id is varchar because of singleoptin uuid, so force cast in string. $customer->answers()->sync($answersID->merge($existingAnswerIds));
ModelAnswer::query()
->where('model_type', Customer::class)
->where('model_id', (string) $customer->getKey())
->whereIn('answer_id', $question->answers->modelKeys())
->delete();
foreach ($request->validated('answers') as $answerId) {
ModelAnswer::create([
'model_type' => Customer::class,
'model_id' => (string) $customer->getKey(),
'answer_id' => $answerId,
]);
}
return response()->json(); return response()->json();
} }
} }

View file

@ -8,7 +8,7 @@ use Illuminate\Http\JsonResponse;
class ApiController extends Controller class ApiController extends Controller
{ {
/** /**
* Just returns HTTP 200 response. * Just returns HTTP 200 response
* *
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */

View file

@ -29,30 +29,30 @@ use Illuminate\Support\Str;
class ArcadiaController extends Controller class ArcadiaController extends Controller
{ {
/** /**
* Send arcadia default mail (reclamations). * Send arcadia default mail (reclamations)
* *
* @param \App\Http\Requests\Api\V2\SendArchadiaDefaultMailRequest $request * @param \App\Http\Requests\Api\V2\SendArchadiaDefaultMailRequest $request
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function outgoingMailDefault(SendArchadiaDefaultMailRequest $request): JsonResponse public function outgoingMailDefault(SendArchadiaDefaultMailRequest $request): JsonResponse
{ {
return $this->handleArcadiaMail($request, ArcadiaMailType::default); return $this->handleArcadiaMail($request, ArcadiaMailType::default());
} }
/** /**
* Send arcadia game mail (games). * Send arcadia game mail (games)
* *
* @param \App\Http\Requests\Api\V2\SendArchadiaGameMailRequest $request * @param \App\Http\Requests\Api\V2\SendArchadiaGameMailRequest $request
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function outgoingMailGame(SendArchadiaGameMailRequest $request): JsonResponse public function outgoingMailGame(SendArchadiaGameMailRequest $request): JsonResponse
{ {
return $this->handleArcadiaMail($request, ArcadiaMailType::game); return $this->handleArcadiaMail($request, ArcadiaMailType::game());
} }
/** /**
* Form xml message and save it in the apropriate storage location * Form xml message and save it in the apropriate storage location
* to be picked up by the arcadia job. * to be picked up by the arcadia job
* *
* @param \App\Http\Requests\Api\V2\BaseApiRequest $request * @param \App\Http\Requests\Api\V2\BaseApiRequest $request
* @param \App\Enums\ArcadiaMailType $type * @param \App\Enums\ArcadiaMailType $type
@ -67,14 +67,14 @@ class ArcadiaController extends Controller
$xml = 'none'; $xml = 'none';
try { try {
$customerInfo = \array_intersect_key($request->all()['deliver'], [ $customerInfo = \array_intersect_key($request->all()['deliver'], [
'email' => '', 'email' => '',
'firstname' => '', 'firstname' => '',
'lastname' => '', 'lastname' => '',
'address' => '', 'address' => '',
'address2' => '', 'address2' => '',
'city' => '', 'city' => '',
'postcode' => '', 'postcode' => '',
'phone' => '' 'phone' => ''
]); ]);
$orderType = ArcadiaOrderType::make(intval($request->state_order)); $orderType = ArcadiaOrderType::make(intval($request->state_order));
$xml = $this->makeXml($request, $type, $orderType, $customerInfo); $xml = $this->makeXml($request, $type, $orderType, $customerInfo);
@ -90,7 +90,7 @@ class ArcadiaController extends Controller
Storage::disk('arcadia')->put(Str::slug(sprintf( Storage::disk('arcadia')->put(Str::slug(sprintf(
'v2_order_%s_%s_%s%s', 'v2_order_%s_%s_%s%s',
$request->order_num, $request->order_num,
$type->label(), $type->label,
date('Ymd_His'), date('Ymd_His'),
\microtime(true) \microtime(true)
)) . '.xml', $xml); )) . '.xml', $xml);
@ -109,11 +109,11 @@ class ArcadiaController extends Controller
} }
throw $e; throw $e;
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} //end try }//end try
} }
/** /**
* Render Arcadia xml for order. * render Arcadia xml for order
* *
* @param \App\Http\Requests\Api\V2\BaseApiRequest $request * @param \App\Http\Requests\Api\V2\BaseApiRequest $request
* @param \App\Enums\ArcadiaMailType $type * @param \App\Enums\ArcadiaMailType $type
@ -127,12 +127,12 @@ class ArcadiaController extends Controller
ArcadiaOrderType $orderType, ArcadiaOrderType $orderType,
array $customerInfo array $customerInfo
): string { ): string {
$arcadiaMailType = $type->equals(ArcadiaMailType::default) ? $arcadiaMailType = $type->equals(ArcadiaMailType::default()) ?
'mail.arcadiaOrders.default' : 'mail.arcadiaOrders.game'; 'mail.arcadiaOrders.default' : 'mail.arcadiaOrders.game';
return view($arcadiaMailType, [ return view($arcadiaMailType, [
'request' => $request, 'request' => $request,
'type' => $type, 'type' => $type,
'orderType' => $orderType, 'orderType' => $orderType,
'customerInfo' => $customerInfo 'customerInfo' => $customerInfo
])->render(); ])->render();
} }

View file

@ -34,10 +34,10 @@ class CustomerController extends Controller
*/ */
public function store(StoreCustomerRequest $request, Service $service): JsonResource public function store(StoreCustomerRequest $request, Service $service): JsonResource
{ {
$data = (object) $request->validated(); $data = (object)$request->validated();
/** @var \App\Models\Customer */ /** @var \App\Models\Customer */
$customer = Customer::query()->where('email', $data->email)->firstOrCreate([ $customer = Customer::where('email', $data->email)->firstOrCreate([
'email' => $data->email 'email' => $data->email
]); ]);
@ -59,7 +59,7 @@ class CustomerController extends Controller
/** @var \App\Models\CustomerInfo */ /** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_NEW); $customerInfo = $customer->info($service->brand, Customer::FIRST_OR_NEW);
if ($customerInfo->exists) { if ($customerInfo->exists) {
throw new ApiErrorException(ApiError::customerExists); throw new ApiErrorException(ApiError::customerExists());
} }
$customerInfo->fill($request->validated()); $customerInfo->fill($request->validated());
$customerInfo->brand()->associate($service->brand); $customerInfo->brand()->associate($service->brand);
@ -84,7 +84,7 @@ class CustomerController extends Controller
} }
/** /**
* Renders the specific customer infos. * Renders the specific customer infos
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @param \App\Models\Customer $customer * @param \App\Models\Customer $customer
@ -99,7 +99,7 @@ class CustomerController extends Controller
} }
/** /**
* Updates information for a specific customer. * Updates information for a specific customer
* *
* @param \App\Http\Requests\Api\V2\UpdateCustomerRequest $request * @param \App\Http\Requests\Api\V2\UpdateCustomerRequest $request
* @param \App\Models\Service $service * @param \App\Models\Service $service
@ -141,7 +141,7 @@ class CustomerController extends Controller
} }
/** /**
* Deletes customer information for a specific service. * Deletes customer information for a specific service
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @param \App\Models\Customer $customer * @param \App\Models\Customer $customer
@ -153,6 +153,7 @@ class CustomerController extends Controller
// * Make sure we are accessing a customer that belongs to our service. // * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer); $this->authorize('customerBelongsToService', $customer);
// TODO: Gestion avec mesBR.
/** @var \App\Models\CustomerInfo */ /** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL); $customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL);
/** @var \App\Models\CustomerLogin */ /** @var \App\Models\CustomerLogin */
@ -192,30 +193,30 @@ class CustomerController extends Controller
} }
/** /**
* Authenticate the user. * Authenticate the user
* *
* @param \App\Http\Requests\Api\V2\AuthenticateRequest $request * @param \App\Http\Requests\Api\V2\AuthenticateRequest $request
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\JsonResource * @return \Illuminate\Http\Resources\Json\JsonResource
* @throws \App\Exceptions\Api\V2\ApiErrorException With ApiError::authFailed in case * @throws \App\Exceptions\Api\V2\ApiErrorException With ApiError::authFailed() in case
* of wrong credentials. * of wrong credentials.
* @throws \App\Exceptions\Api\V2\ApiErrorException With ApiError::customerNotActivated if * @throws \App\Exceptions\Api\V2\ApiErrorException With ApiError::customerNotActivated() if
* customer account is not activated. * customer account is not activated.
*/ */
public function authenticate(AuthenticateRequest $request, Service $service): JsonResource public function authenticate(AuthenticateRequest $request, Service $service): JsonResource
{ {
try { try {
/** @var \App\Models\Customer */ /** @var \App\Models\Customer */
$customer = Customer::query()->where('email', $request->email)->firstOrFail(); $customer = Customer::where('email', $request->email)->firstOrFail();
/** @var \App\Models\CustomerLogin $customerLogin */ /** @var \App\Models\CustomerLogin $customerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL); $customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
if (!$customerLogin->passwordMatch($request->password)) { if (!$customerLogin->passwordMatch($request->password)) {
throw new ApiErrorException(ApiError::authFailed); throw new ApiErrorException(ApiError::authFailed());
} }
if (!$customerLogin->status) { if (!$customerLogin->status) {
throw new ApiErrorException(ApiError::customerNotActivated); throw new ApiErrorException(ApiError::customerNotActivated());
} }
UpdateCustomerLastConnection::dispatch($customer, $customerLogin, now()) UpdateCustomerLastConnection::dispatch($customer, $customerLogin, now())
@ -223,12 +224,12 @@ class CustomerController extends Controller
return new CustomerResource($service->brand, $customer); return new CustomerResource($service->brand, $customer);
} catch (ModelNotFoundException $e) { } catch (ModelNotFoundException $e) {
throw new ApiErrorException(ApiError::authFailed); throw new ApiErrorException(ApiError::authFailed());
} //end try } //end try
} }
/** /**
* Activate the account of the user. * Activate the account of the user
* *
* @param \App\Models\Customer $customer * @param \App\Models\Customer $customer
* @param \App\Models\Service $service * @param \App\Models\Service $service
@ -237,7 +238,7 @@ class CustomerController extends Controller
* if customer account is already active. * if customer account is already active.
* @throws \App\Exceptions\Api\V2\CrmException If saving the customer to DB failed. * @throws \App\Exceptions\Api\V2\CrmException If saving the customer to DB failed.
*/ */
public function activateAccount(Customer $customer, Service $service): JsonResponse public function activateAccount(Customer $customer, Service $service)
{ {
// * Make sure we are accessing a customer that belongs to our service. // * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer); $this->authorize('customerBelongsToService', $customer);
@ -246,7 +247,7 @@ class CustomerController extends Controller
$login = $customer->login($service->brand, Customer::FIRST_OR_FAIL); $login = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
if ($login->status) { if ($login->status) {
throw new ApiErrorException(ApiError::customerAlreadyActive); throw new ApiErrorException(ApiError::customerAlreadyActive());
} }
$login->status = true; $login->status = true;

View file

@ -11,12 +11,13 @@ use App\Http\Requests\Api\V2\SingleOptinRequest;
use App\Http\Requests\Api\V2\SingleOptinRemoveWithHashcodeRequest; use App\Http\Requests\Api\V2\SingleOptinRemoveWithHashcodeRequest;
use App\Models\CustomerInfo; use App\Models\CustomerInfo;
use App\Models\Service; use App\Models\Service;
use Illuminate\Http\JsonResponse;
use App\Models\SingleOptin; use App\Models\SingleOptin;
class OptinController extends Controller class OptinController extends Controller
{ {
/** /**
* Creates a new Single optin or destroys it if exists. * Creates a new Single optin or destroys it if exists
* *
* @param \App\Http\Requests\Api\V2\SingleOptinRequest $request * @param \App\Http\Requests\Api\V2\SingleOptinRequest $request
* @param \App\Models\Service $service * @param \App\Models\Service $service
@ -25,53 +26,42 @@ class OptinController extends Controller
* if this email already has a full account. * if this email already has a full account.
* @throws \App\Exceptions\Api\V2\CrmException If the single optin could not be added or removed. * @throws \App\Exceptions\Api\V2\CrmException If the single optin could not be added or removed.
*/ */
public function optin(SingleOptinRequest $request, Service $service): \Illuminate\Http\JsonResponse public function optin(SingleOptinRequest $request, Service $service): JsonResponse
{ {
$validatedData = $request->validated(); if (CustomerInfo::getFromEmail($service->brand, $request->email)->exists()) {
if (CustomerInfo::getFromEmail($service->brand, $validatedData['email'])->exists()) { throw new ApiErrorException(ApiError::singleOptinCustomerExists());
throw new ApiErrorException(ApiError::singleOptinCustomerExists);
} }
/** @var \App\Models\SingleOptin */ /** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::query() $singleOptin = SingleOptin::query()->where('email', $request->email)
->where('email', $validatedData['email'])
->whereHas('brand', function ($q) use ($service) { ->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand->id); $q->where('id', $service->brand->id);
}) })->firstOrNew();
->firstOrNew();
if (!$this->changeOptin($service, $request, \boolval($validatedData['opt_in']), $singleOptin)) { if (!$this->changeOptin($service, $request, \boolval($request->opt_in), $singleOptin)) {
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
throw new CrmException('Api failed to update single optin.'); throw new CrmException('Api failed to update single optin.');
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
if (isset($validatedData['answer_id'])) {
$singleOptin->answers()->attach($validatedData['answer_id']);
}
return response()->json(); return response()->json();
} }
/** /**
* Creates a new Single optin or destroys it if exists using hashcode. * Creates a new Single optin or destroys it if exists using hashcode
* *
* @param \App\Http\Requests\Api\V2\SingleOptinRemoveWithHashcodeRequest $request * @param \App\Http\Requests\Api\V2\SingleOptinRemoveWithHashcodeRequest $request
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V2\CrmException If the single optin could not be removed. * @throws \App\Exceptions\Api\V2\CrmException If the single optin could not be removed.
*/ */
public function optoutWithHashcode( public function optoutWithHashcode(SingleOptinRemoveWithHashcodeRequest $request, Service $service)
SingleOptinRemoveWithHashcodeRequest $request, {
Service $service
): \Illuminate\Http\JsonResponse {
/** @var \App\Models\SingleOptin */ /** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::query() $singleOptin = SingleOptin::query()->where('hashcode', $request->hashcode)
->where('hashcode', $request->hashcode)
->whereHas('brand', function ($q) use ($service) { ->whereHas('brand', function ($q) use ($service) {
$q->whereId($service->brand->id); $q->whereId($service->brand->id);
}) })->firstOrFail();
->firstOrFail();
if ($singleOptin and !$this->changeOptin($service, $request, \boolval($request->opt_in), $singleOptin)) { if ($singleOptin and !$this->changeOptin($service, $request, \boolval($request->opt_in), $singleOptin)) {
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
@ -83,7 +73,7 @@ class OptinController extends Controller
} }
/** /**
* Changes the optin state. * Changes the optin state
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @param \App\Http\Requests\Api\V2\BaseApiRequest $request * @param \App\Http\Requests\Api\V2\BaseApiRequest $request

View file

@ -52,10 +52,10 @@ class QuestionController extends Controller
// * Make sure we are accessing a customer that belongs to our service. // * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer); $this->authorize('customerBelongsToService', $customer);
$questions = Question::query()->whereHas('brand', function ($q) use ($service) { $questions = Question::whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand->id); $q->where('id', $service->brand->id);
})->whereDoesntHave('answers', function ($q) use ($customer) { })->whereDoesntHave('answers', function ($q) use ($customer) {
$q->whereIn('id', Answer::query()->whereHas('customers', function ($q) use ($customer) { $q->whereIn('id', Answer::whereHas('customers', function ($q) use ($customer) {
$q->where('id', $customer->id); $q->where('id', $customer->id);
})->pluck('id')); })->pluck('id'));
})->with('answers')->get(); })->with('answers')->get();

View file

@ -8,42 +8,39 @@ use App\Http\Requests\Api\V2\GetQuestionStatsRequest;
use App\Http\Resources\Api\V2\BrandStatsResource; use App\Http\Resources\Api\V2\BrandStatsResource;
use App\Http\Resources\Api\V2\QuestionStatsResource; use App\Http\Resources\Api\V2\QuestionStatsResource;
use App\Models\BrandStat; use App\Models\BrandStat;
use App\Models\ModelAnswer; use App\Models\CustomerAnswer;
use App\Models\Question; use App\Models\Question;
use App\Models\Service; use App\Models\Service;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class StatsController extends Controller class StatsController extends Controller
{ {
/** /**
* Get specific brand statistics. * Get specific brand statistics
* *
* @param \App\Http\Requests\Api\V2\GetBrandStatsRequest $request * @param \App\Http\Requests\Api\V2\GetBrandStatsRequest $request
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\ResourceCollection * @return \Illuminate\Http\Resources\Json\ResourceCollection
*/ */
public function brandStats( public function brandStats(GetBrandStatsRequest $request, Service $service): ResourceCollection
GetBrandStatsRequest $request, {
Service $service $stats = BrandStat::whereDate('created_at', '>=', $request->start)
): \Illuminate\Http\Resources\Json\ResourceCollection {
$stats = BrandStat::query()->whereDate('created_at', '>=', $request->start)
->whereDate('created_at', '<=', $request->end) ->whereDate('created_at', '<=', $request->end)
->whereBrandId($service->brand->id)->get(); ->whereBrandId($service->brand->id)->get();
return new BrandStatsResource($stats); return new BrandStatsResource($stats);
} }
/** /**
* Get statistics for a specific question. * Get statistics for a specific question
* *
* @param \App\Http\Requests\Api\V2\GetQuestionStatsRequest $request * @param \App\Http\Requests\Api\V2\GetQuestionStatsRequest $request
* @param \App\Models\Question $question * @param \App\Models\Question $question
* @return \App\Http\Resources\Api\V2\QuestionStatsResource * @return \App\Http\Resources\Api\V2\QuestionStatsResource
*/ */
public function questionStats( public function questionStats(GetQuestionStatsRequest $request, Question $question): QuestionStatsResource
GetQuestionStatsRequest $request, {
Question $question
): \App\Http\Resources\Api\V2\QuestionStatsResource {
// * Make sure we are accessing a customer that belongs to our service. // * Make sure we are accessing a customer that belongs to our service.
$this->authorize('questionBelongsToService', $question); $this->authorize('questionBelongsToService', $question);
@ -58,36 +55,32 @@ class StatsController extends Controller
} }
/** /**
* Total réponses par dates. * Total réponses par dates
* *
* @param \Illuminate\Support\Collection $answers * @param \Illuminate\Support\Collection $answers
* @param \Illuminate\Support\Carbon $start * @param \Illuminate\Support\Carbon $start
* @param \Illuminate\Support\Carbon $end * @param \Illuminate\Support\Carbon $end
* @return \Illuminate\Support\Collection * @return \Illuminate\Support\Collection
*/ */
private function answersByDays(Collection $answers, Carbon $start, Carbon $end): \Illuminate\Support\Collection private function answersByDays(Collection $answers, Carbon $start, Carbon $end): Collection
{ {
// * Totalize by date all answers. // * Totalize by date all answers.
$answersByDays = ModelAnswer::query() $answersByDays = CustomerAnswer::whereDate('created_at', '>=', $start)
->whereDate('created_at', '>=', $start)
->whereDate('created_at', '<=', $end) ->whereDate('created_at', '<=', $end)
->whereIn('answer_id', $answers->pluck('id')) ->whereIn('answer_id', $answers->pluck('id'))
->get() ->get()->map(function ($custAnswer) {
->map(function ($custAnswer) {
return [ return [
'answer_id' => $custAnswer->answer_id, 'answer_id' => $custAnswer->answer_id,
'created_at' => $custAnswer->created_at->isoFormat('YYYY-MM-DD'), 'created_at' => $custAnswer->created_at->format('Y-m-d'),
]; ];
}) })->groupBy(['created_at', 'answer_id'])->map(function ($day) {
->groupBy(['created_at', 'answer_id'])
->map(function ($day) {
return $day->map(function ($custAnswers) { return $day->map(function ($custAnswers) {
return $custAnswers->count(); return $custAnswers->count();
}); });
}); });
do { do {
$currentDate = $start->isoFormat('YYYY-MM-DD'); $currentDate = $start->format('Y-m-d');
/** @var \Illuminate\Support\Collection $answersByDays */ /** @var \Illuminate\Support\Collection $answersByDays */
if (!$answersByDays->has($currentDate)) { if (!$answersByDays->has($currentDate)) {
$answersByDays->put( $answersByDays->put(

View file

@ -1,46 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\StoreCustomerAnswers;
use App\Models\Customer;
use App\Models\ModelAnswer;
use App\Models\Question;
class AnswerController extends Controller
{
/**
* Store the customer answers.
*
* @param \App\Http\Requests\Api\V4\StoreCustomerAnswers $request
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\JsonResponse
*/
public function storeCustomerAnswers(
StoreCustomerAnswers $request,
Customer $customer
): \Illuminate\Http\JsonResponse {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
$question = Question::find($request->validated('question'));
// Column model_id is varchar because of singleoptin uuid, so force cast in string.
ModelAnswer::query()
->where('model_type', Customer::class)
->where('model_id', (string) $customer->getKey())
->whereIn('answer_id', $question->answers->modelKeys())
->delete();
foreach ($request->validated('answers') as $answerId) {
ModelAnswer::create([
'model_type' => Customer::class,
'model_id' => (string) $customer->getKey(),
'answer_id' => $answerId,
]);
}
return response()->json();
}
}

View file

@ -1,19 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
class ApiController extends Controller
{
/**
* Just returns HTTP 200 response.
*
* @return \Illuminate\Http\JsonResponse
*/
public function ping(): JsonResponse
{
return response()->json();
}
}

View file

@ -1,139 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Enums\ArcadiaMailType;
use App\Enums\ArcadiaOrderType;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\BaseApiRequest;
use App\Http\Requests\Api\V4\SendArchadiaDefaultMailRequest;
use App\Http\Requests\Api\V4\SendArchadiaGameMailRequest;
use App\Mail\CourrierArcadiaError;
use DOMDocument;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* ! Ici c'est du sale à cause d'arcadia
* Ils ont fait le choix de rajouter une fonctionnalité
* sur une methode existante en rajoutant des conditions
* sales. Ils ont donc une seule méthode pour envoyer plusieurs
* types de réclas avec des champs différents...
*
* Donc une séparation est faite sur les endpoints CRM et dans le client
* pour éviter une confusion de nos utilisateurs
*/
class ArcadiaController extends Controller
{
/**
* Send arcadia default mail (reclamations).
*
* @param \App\Http\Requests\Api\V4\SendArchadiaDefaultMailRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function outgoingMailDefault(SendArchadiaDefaultMailRequest $request): JsonResponse
{
return $this->handleArcadiaMail($request, ArcadiaMailType::default);
}
/**
* Send arcadia game mail (games).
*
* @param \App\Http\Requests\Api\V4\SendArchadiaGameMailRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function outgoingMailGame(SendArchadiaGameMailRequest $request): JsonResponse
{
return $this->handleArcadiaMail($request, ArcadiaMailType::game);
}
/**
* Form xml message and save it in the apropriate storage location
* to be picked up by the arcadia job.
*
* @param \App\Http\Requests\Api\V4\BaseApiRequest $request
* @param \App\Enums\ArcadiaMailType $type
* @return \Illuminate\Http\JsonResponse
* @throws \Exception If an error occurs while generating and saving the xml file.
*/
private function handleArcadiaMail(BaseApiRequest $request, ArcadiaMailType $type): JsonResponse
{
$request = $request->merge([
'order_num' => str_pad(\intval($request->order_num), 9, '0', STR_PAD_LEFT)
]);
$xml = 'none';
try {
$customerInfo = \array_intersect_key($request->all()['deliver'], [
'email' => '',
'firstname' => '',
'lastname' => '',
'address' => '',
'address2' => '',
'city' => '',
'postcode' => '',
'phone' => ''
]);
$orderType = ArcadiaOrderType::make(intval($request->state_order));
$xml = $this->makeXml($request, $type, $orderType, $customerInfo);
// * Format xml.
$doc = new DOMDocument('1.0', 'utf-8');
$doc->xmlStandalone = true;
$doc->preserveWhiteSpace = false;
$doc->formatOutput = true;
$doc->loadXML($xml);
$xml = $doc->saveXML();
Storage::disk('arcadia')->put(Str::slug(sprintf(
'v4_order_%s_%s_%s%s',
$request->order_num,
$type->label(),
date('Ymd_His'),
\microtime(true)
)) . '.xml', $xml);
return response()->json();
// @codeCoverageIgnoreStart
} catch (\Exception $e) {
Log::critical("ARCADIA XML CREATION FAILED FOR $request->order_num : " . $e->getMessage());
if (!config('app.debug')) {
dispatch(function () use ($xml, $e, $request) {
Mail::to(config('mail.MAIL_ADMIN'))->send(new CourrierArcadiaError(
"ARCADIA XML CREATION FAILED FOR $request->order_num",
"Une erreur est survenue lors de la création du fichier XML : " . $e->getMessage(),
$xml ?: ''
));
})->afterResponse();
}
throw $e;
// @codeCoverageIgnoreEnd
} //end try
}
/**
* Render Arcadia xml for order.
*
* @param \App\Http\Requests\Api\V4\BaseApiRequest $request
* @param \App\Enums\ArcadiaMailType $type
* @param \App\Enums\ArcadiaOrderType $orderType
* @param array $customerInfo
* @return string
*/
private function makeXml(
BaseApiRequest $request,
ArcadiaMailType $type,
ArcadiaOrderType $orderType,
array $customerInfo
): string {
$arcadiaMailType = $type->equals(ArcadiaMailType::default) ?
'mail.arcadiaOrders.default' : 'mail.arcadiaOrders.game';
return view($arcadiaMailType, [
'request' => $request,
'type' => $type,
'orderType' => $orderType,
'customerInfo' => $customerInfo
])->render();
}
}

View file

@ -1,51 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Exceptions\Api\V4\CrmException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\SendClaimRequest;
use App\Models\Claim;
use App\Models\CustomerInfo;
use App\Models\Service;
use Illuminate\Support\Arr;
use Uwil\LaravelCommon\Helpers\FileHelper;
class ClaimController extends Controller
{
/**
* Just returns HTTP 200 response.
*
* @param \App\Http\Requests\Api\V4\SendClaimRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V4\CrmException If the single optin could not be added or removed.
*/
public function store(SendClaimRequest $request, Service $service): \Illuminate\Http\JsonResponse
{
$claimModel = (new Claim())->fill(Arr::except($request->validated(), 'file'));
if ($request->filled('file')) {
$claimModel->file = FileHelper::storeBase64File(
tableName: 'claim',
fileBase64: $request->file,
private: true,
);
}
// Add brand id from service.
$claimModel->brand_id = $service->brand->getKey();
// Add customer relation if exist.
$crmCustomer = CustomerInfo::getFromEmail($claimModel->brand, $claimModel->email)->first();
if (!is_null($crmCustomer)) {
$claimModel->hashcode = $crmCustomer->hashcode;
}
if (!$claimModel->saveOrFail()) {
throw new CrmException('Api failed to store claim.');
}
$claimModel->brand()->associate($service->brand);
return response()->json();
}
}

View file

@ -1,260 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Enums\ApiError;
use App\Exceptions\Api\V4\ApiErrorException;
use App\Exceptions\Api\V4\CrmException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\AuthenticateRequest;
use App\Http\Requests\Api\V4\StoreCustomerRequest;
use App\Http\Requests\Api\V4\UpdateCustomerRequest;
use App\Http\Resources\Api\V4\CustomerResource;
use App\Jobs\UpdateCustomerLastConnection;
use App\Models\Customer;
use App\Models\Service;
use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\DB;
class CustomerController extends Controller
{
/**
* Creates a new customer for the specified service.
*
* @param \App\Http\Requests\Api\V4\StoreCustomerRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\JsonResource
* @throws \App\Exceptions\Api\V4\ApiErrorException With ApiError::customerExists if
* the customer with this email already exists.
* @throws \App\Exceptions\Api\V4\CrmException If saving the customer to DB failed.
*/
public function store(StoreCustomerRequest $request, Service $service): JsonResource
{
$data = (object) $request->validated();
/** @var \App\Models\Customer */
$customer = Customer::query()->where('email', $data->email)->firstOrCreate([
'email' => $data->email
]);
DB::transaction(function () use (&$customer, $data, $request, $service) {
$saved = $customer->save();
$customer->setOptIn($service->brand, $data->opt_in);
// * Delete Single optin if exists.
/** @var \App\Models\SingleOptin|null $singleOptin */
$singleOptin = SingleOptin::query()->where('email', $data->email)
->whereHas('brand', function (Builder $q) use ($service) {
$q->where('id', $service->brand->id);
})->first();
if ($singleOptin) {
$saved = $singleOptin->delete() && $saved;
}
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_NEW);
if ($customerInfo->exists) {
throw new ApiErrorException(ApiError::customerExists);
}
$customerInfo->fill($request->validated());
$customerInfo->brand()->associate($service->brand);
$customerInfo->customer()->associate($customer);
$saved = $customerInfo->save() && $saved;
/** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_NEW);
$customerLogin->fill($request->validated());
$customerLogin->brand()->associate($service->brand);
$customerLogin->customer()->associate($customer);
$saved = $customerLogin->save() && $saved;
if (!$saved) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to store customer.');
// @codeCoverageIgnoreEnd
}
});
return new CustomerResource($service->brand, $customer);
}
/**
* Renders the specific customer infos.
*
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\JsonResource
*/
public function show(Service $service, Customer $customer): JsonResource
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
return new CustomerResource($service->brand, $customer);
}
/**
* Updates information for a specific customer.
*
* @param \App\Http\Requests\Api\V4\UpdateCustomerRequest $request
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\JsonResource
* @throws \App\Exceptions\Api\V4\CrmException If saving the customer to DB failed.
*/
public function update(UpdateCustomerRequest $request, Service $service, Customer $customer): JsonResource
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
DB::transaction(function () use ($request, $customer, $service) {
// To correct with create.
$customer->fill($request->validated());
$saved = $customer->save();
if ($request->has('opt_in')) {
$customer->setOptIn($service->brand, $request->opt_in);
}
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL);
$customerInfo->fill($request->validated());
$saved = $customerInfo->save() && $saved;
/** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
$customerLogin->fill($request->validated());
$saved = $customerLogin->save() && $saved;
if (!$saved) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update customer.');
// @codeCoverageIgnoreEnd
}
});
return new CustomerResource($service->brand, $customer);
}
/**
* Deletes customer information for a specific service.
*
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V4\CrmException If removing the customer off the DB failed.
*/
public function destroy(Service $service, Customer $customer): JsonResponse
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL);
/** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
DB::transaction(function () use ($service, $customer, $customerLogin, $customerInfo) {
if (
$customerInfo->delete() === false ||
$customerLogin->delete() === false
) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to delete customer.');
// @codeCoverageIgnoreEnd
};
$customer->optInHistory()->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand_id);
})->delete();
// * Force loading real relation counts.
$customer->unsetRelation('logins');
$customer->unsetRelation('infos');
$logins = $customer->logins()->count();
$infos = $customer->infos()->count();
// If customer is not associated with any brand then delete.
if (!$logins and !$infos) {
if ($customer->delete() === false) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update customer.');
// @codeCoverageIgnoreEnd
}
}
});
return response()->json();
}
/**
* Authenticate the user.
*
* @param \App\Http\Requests\Api\V4\AuthenticateRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\JsonResource
* @throws \App\Exceptions\Api\V4\ApiErrorException With ApiError::authFailed in case
* of wrong credentials.
* @throws \App\Exceptions\Api\V4\ApiErrorException With ApiError::customerNotActivated if
* customer account is not activated.
*/
public function authenticate(AuthenticateRequest $request, Service $service): JsonResource
{
try {
/** @var \App\Models\Customer */
$customer = Customer::query()->where('email', $request->email)->firstOrFail();
/** @var \App\Models\CustomerLogin $customerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
if (!$customerLogin->passwordMatch($request->password)) {
throw new ApiErrorException(ApiError::authFailed);
}
if (!$customerLogin->status) {
throw new ApiErrorException(ApiError::customerNotActivated);
}
UpdateCustomerLastConnection::dispatch($customer, $customerLogin, now())
->onConnection('database')->afterResponse();
return new CustomerResource($service->brand, $customer);
} catch (ModelNotFoundException $e) {
throw new ApiErrorException(ApiError::authFailed);
} //end try
}
/**
* Activate the account of the user.
*
* @param \App\Models\Customer $customer
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V4\ApiErrorException With ApiError::customerAlreadyActive
* if customer account is already active.
* @throws \App\Exceptions\Api\V4\CrmException If saving the customer to DB failed.
*/
public function activateAccount(Customer $customer, Service $service): JsonResponse
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
/** @var \App\Models\CustomerLogin $login */
$login = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
if ($login->status) {
throw new ApiErrorException(ApiError::customerAlreadyActive);
}
$login->status = true;
if (!$login->save()) {
// @codeCoverageIgnoreStart
throw new CrmException('Failed to save login');
// @codeCoverageIgnoreEnd
}
return response()->json();
}
}

View file

@ -1,130 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Exceptions\Api\V4\CrmException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\BaseApiRequest;
use App\Http\Requests\Api\V4\SingleOptinRequest;
use App\Http\Requests\Api\V4\SingleOptinRemoveWithHashcodeRequest;
use App\Models\CustomerInfo;
use App\Models\Service;
use App\Models\SingleOptin;
class OptinController extends Controller
{
/**
* Creates a new Single optin or destroys it if exists.
*
* @param \App\Http\Requests\Api\V4\SingleOptinRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V4\CrmException If the single optin could not be added or removed.
*/
public function optin(SingleOptinRequest $request, Service $service): \Illuminate\Http\JsonResponse
{
$validatedData = $request->validated();
$crmCustomer = CustomerInfo::getFromEmail($service->brand, $validatedData['email'])->first();
if ($crmCustomer) {
// Update the optin field.
$crmCustomer->opt_in = \boolval($validatedData['opt_in']);
// Check the saving.
if (!$crmCustomer->saveOrFail()) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update customer optin.');
// @codeCoverageIgnoreEnd
}
// Associate answers if presents.
if (isset($validatedData['answer_id'])) {
$crmCustomer->answers()->attach($validatedData['answer_id']);
}
} else {
// Get the single optin if exist or create it.
/** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::query()
->where('email', $validatedData['email'])
->whereHas('brand', function ($q) use ($service) {
$q->whereKey($service->brand->getKey());
})
->firstOrNew();
// Update the optin field and check the saving.
if (!$this->changeOptin($service, $request, \boolval($validatedData['opt_in']), $singleOptin)) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update single optin.');
// @codeCoverageIgnoreEnd
}
// Associate answers if presents.
if (isset($validatedData['answer_id'])) {
$singleOptin->answers()->attach($validatedData['answer_id']);
}
} //end if
return response()->json();
}
/**
* Creates a new Single optin or destroys it if exists using hashcode.
*
* @param \App\Http\Requests\Api\V4\SingleOptinRemoveWithHashcodeRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V4\CrmException If the single optin could not be removed.
*/
public function optoutWithHashcode(
SingleOptinRemoveWithHashcodeRequest $request,
Service $service
): \Illuminate\Http\JsonResponse {
$validatedData = $request->validated();
$crmCustomer = CustomerInfo::getFromHashcode($service->brand, $validatedData['hashcode'])->first();
if ($crmCustomer) {
// Update the optin field.
$crmCustomer->opt_in = false;
// Check the saving.
if (!$crmCustomer->saveOrFail()) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update customer optin.');
// @codeCoverageIgnoreEnd
}
} else {
// Get the single optin if exist or create it.
/** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::query()->where('hashcode', $validatedData['hashcode'])
->whereHas('brand', function ($q) use ($service) {
$q->whereId($service->brand->getKey());
})->firstOrFail();
// Update the optin field and check the saving.
if ($singleOptin and !$this->changeOptin($service, $request, false, $singleOptin)) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update single optin.');
// @codeCoverageIgnoreEnd
}
} //end if
return response()->json();
}
/**
* Changes the optin state.
*
* @param \App\Models\Service $service
* @param \App\Http\Requests\Api\V4\BaseApiRequest $request
* @param boolean $optin
* @param \App\Models\SingleOptin $singleOptin
* @return boolean Returns false in case of failure.
*/
private function changeOptin(Service $service, BaseApiRequest $request, bool $optin, SingleOptin $singleOptin): bool
{
if ($optin) {
$singleOptin->fill($request->validated());
$singleOptin->brand()->associate($service->brand);
return $singleOptin->save();
}
return $singleOptin->delete() !== false;
}
}

View file

@ -1,68 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Http\Controllers\Controller;
use App\Http\Resources\Api\V4\QuestionsResource;
use App\Models\Answer;
use App\Models\Customer;
use App\Models\Question;
use App\Models\Service;
class QuestionController extends Controller
{
/**
* Returns all service questions.
*
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function index(Service $service): \Illuminate\Http\Resources\Json\ResourceCollection
{
return new QuestionsResource($service->brand->questions);
}
/**
* Returns all service questions with user answers for all of them.
*
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function indexWithResponse(
Service $service,
Customer $customer
): \Illuminate\Http\Resources\Json\ResourceCollection {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
$customerAnswers = $customer->getAnswers($service);
return new QuestionsResource($service->brand->questions, $customerAnswers);
}
/**
* Returns all service questions that was not answered by a user.
*
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\JsonResource
*/
public function unAnsweredBy(Service $service, Customer $customer): \Illuminate\Http\Resources\Json\JsonResource
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
$questions = Question::query()
->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand->id);
})
->whereDoesntHave('answers', function ($q) use ($customer) {
$q->whereIn('id', Answer::query()->whereHas('customers', function ($q) use ($customer) {
$q->where('id', $customer->id);
})->pluck('id'));
})
->with('answers')
->get();
return new QuestionsResource($questions);
}
}

View file

@ -1,116 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\GetBrandStatsRequest;
use App\Http\Requests\Api\V4\GetQuestionStatsRequest;
use App\Http\Resources\Api\V4\BrandStatsResource;
use App\Http\Resources\Api\V4\QuestionStatsResource;
use App\Models\BrandStat;
use App\Models\ModelAnswer;
use App\Models\Question;
use App\Models\Service;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
class StatsController extends Controller
{
/**
* Get specific brand statistics.
*
* @param \App\Http\Requests\Api\V4\GetBrandStatsRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function brandStats(
GetBrandStatsRequest $request,
Service $service
): \Illuminate\Http\Resources\Json\ResourceCollection {
$stats = BrandStat::query()->whereDate('created_at', '>=', $request->start)
->whereDate('created_at', '<=', $request->end)
->whereBrandId($service->brand->id)->get();
return new BrandStatsResource($stats);
}
/**
* Get statistics for a specific question.
*
* @param \App\Http\Requests\Api\V4\GetQuestionStatsRequest $request
* @param \App\Models\Question $question
* @return \App\Http\Resources\Api\V4\QuestionStatsResource
*/
public function questionStats(
GetQuestionStatsRequest $request,
Question $question
): \App\Http\Resources\Api\V4\QuestionStatsResource {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('questionBelongsToService', $question);
$answers = $question->answers;
$start = Carbon::parse($request->start);
$end = Carbon::parse($request->end);
return new QuestionStatsResource(
$this->answersByDays($answers, $start, $end),
$question
);
}
/**
* Total réponses par dates.
*
* @param \Illuminate\Support\Collection $answers
* @param \Illuminate\Support\Carbon $start
* @param \Illuminate\Support\Carbon $end
* @return \Illuminate\Support\Collection
*/
private function answersByDays(Collection $answers, Carbon $start, Carbon $end): \Illuminate\Support\Collection
{
// * Totalize by date all answers.
$answersByDays = ModelAnswer::query()
->whereDate('created_at', '>=', $start)
->whereDate('created_at', '<=', $end)
->whereIn('answer_id', $answers->pluck('id'))
->get()
->map(function ($custAnswer) {
return [
'answer_id' => $custAnswer->answer_id,
'created_at' => $custAnswer->created_at->isoFormat('YYYY-MM-DD'),
];
})
->groupBy(['created_at', 'answer_id'])
->map(function ($day) {
return $day->map(function ($custAnswers) {
return $custAnswers->count();
});
});
do {
$currentDate = $start->isoFormat('YYYY-MM-DD');
/** @var \Illuminate\Support\Collection $answersByDays */
if (!$answersByDays->has($currentDate)) {
$answersByDays->put(
$currentDate,
$answers->mapWithKeys(function ($answer) {
return [$answer->id => 0];
})
);
} else {
$curentAnswers = $answersByDays->pull($currentDate);
$answersByDays->put(
$currentDate,
$answers->mapWithKeys(function ($answer) use ($curentAnswers) {
return [$answer->id =>
$curentAnswers->has($answer->id) ?
$curentAnswers->get($answer->id) : 0];
})
);
}
$start = $start->addDay();
} while ($start->lessThanOrEqualTo($end));
return $answersByDays;
}
}

View file

@ -1,79 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Traits\Controllers\ChangesModelOrder;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Http\Request;
class ActivityLogsController extends Controller
{
use ChangesModelOrder;
use UpdateModelPublished;
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(ActivityLog::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
$this->authorize('isConceptor');
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = ActivityLog::query()->with('user');
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'model_class',
);
}
$searchFields = \implode(', ', [
trans('validation.custom.target'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$activitylogModels = $query->paginate($this->getPagination());
return view('bo.pages.activity_logs.index', compact('activitylogModels', 'search', 'searchFields'));
}
/**
* Show the form for editing the specified resource.
*
* @param App\Models\ActivityLog $activity_log
* @return \Illuminate\Contracts\View\View
*/
public function show(ActivityLog $activity_log): \Illuminate\Contracts\View\View
{
$this->authorize('isConceptor');
$targetModelClass = $activity_log->model_class;
$targetModel = $activity_log->model_class::where(
(new $targetModelClass())->getRouteKeyName(),
$activity_log->model_id
)->first();
return view('bo.pages.activity_logs.show', [
'activitylogModel' => $activity_log,
'targetModel' => $targetModel,
]);
}
}

View file

@ -1,72 +0,0 @@
<?php
namespace App\Http\Controllers\Bo\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Contracts\View\View;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
use AuthenticatesUsers;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:backend')->except('logout');
}
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard(): StatefulGuard
{
return Auth::guard('backend');
}
/**
* Show the application's login form.
*
* @return \Illuminate\Contracts\View\View
*/
public function showLoginForm(): View
{
return view('bo.auth.login');
}
/**
* Where to redirect users after login.
*
* @return string
*/
protected function redirectTo(): string
{
return response()->redirectToIntended()->getTargetUrl();
}
/**
* Attempt to log the user into the application.
*
* @param \Illuminate\Http\Request $request
* @return boolean
*/
protected function attemptLogin(Request $request)
{
$credentials = $this->credentials($request);
// * Attempt login only using published users.
$credentials['published'] = true;
return $this->guard()->attempt(
$credentials,
$request->boolean('remember')
);
}
}

View file

@ -1,33 +0,0 @@
<?php
namespace App\Http\Controllers\Bo\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Support\Facades\Auth;
class ResetPasswordController extends Controller
{
use ResetsPasswords;
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard(): StatefulGuard
{
return Auth::guard('backend');
}
/**
* Where to redirect users after login.
*
* @return string
*/
protected function redirectTo(): string
{
return \route('bo.login');
}
}

View file

@ -1,298 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Enums\Claims\ClaimResponseType;
use App\Enums\Claims\ClaimStatus;
use App\Enums\ExportFormat;
use App\Enums\Users\RoleEnum;
use App\Events\ClaimModelChanged;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Claims\ExportClaimRequest;
use App\Http\Requests\Bo\Claims\SendClaimAnswerRequest;
use App\Http\Requests\Bo\Claims\UpdateClaimRequest;
use App\Http\Requests\Bo\Claims\ViewClaimAttachmentRequest;
use App\Http\Requests\Bo\PaginateModelsRequest;
use App\Lib\Helpers\FileHelper;
use App\Mail\ClaimAnswerMail;
use App\Models\Brand;
use App\Models\Claim;
use App\Models\ClaimMailTemplate;
use App\Models\ClaimMessage;
use App\Models\ClaimSubject;
use App\Traits\Controllers\ExportDatas;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class ClaimController extends Controller
{
use UpdateModelPublished;
use ExportDatas;
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Claim::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \App\Models\User $userModel */
$userModel = auth('backend')->user();
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Claim::query()
->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
)->when(
$userModel->role->equals(RoleEnum::marketing),
fn($q) => $q->whereHas('brand', fn($q) => $q->whereIn('id', $userModel->brands->modelKeys()))
)
->when(
$request->filled('subject'),
fn($q) => $q->whereHas('claimSubject', fn($q) => $q->where('id', $request->subject))
)
->when(
$request->filled('status'),
fn($q) => $q->whereIn('status', $request->status),
fn($q) => $q->whereNotIn('status', [ClaimStatus::treated, ClaimStatus::refused])
);
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'reference',
'first_name',
'last_name',
'email',
);
}
$searchFields = \implode(', ', [
trans('validation.custom.reference'),
trans('validation.attributes.first_name'),
trans('validation.attributes.last_name'),
trans('validation.attributes.email'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$claimModels = $query->simplePaginate($this->getPagination());
/** Define request brand filter if user only has 1 brand */
if (auth('backend')->user()->brands()->count() === 1) {
$request->merge(['brand' => auth('backend')->user()->brands()->value('id')]);
};
$claimSubjectModels = ClaimSubject::query()
->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand)),
)
->when(
auth('backend')->user()->brands()->count() === 1,
fn($q) => $q->whereHas(
'brand',
fn($q) => $q->where('id', auth('backend')->user()->brands()->pluck('id'))
)
);
$claimSubjectModels = $request->filled('brand') || (auth('backend')->user()->brands()->count() === 1) ?
$claimSubjectModels->orderBy('label', 'ASC')->get() :
null;
return view('bo.pages.claims.index', compact('claimModels', 'search', 'searchFields', 'claimSubjectModels'));
}
/**
* Show the form for editing the specified resource.
*
* @param App\Models\Claim $claim
* @return \Illuminate\Contracts\View\View
*/
public function edit(Claim $claim): \Illuminate\Contracts\View\View
{
$customerClaimModels = !is_null($claim->customerInfos) ? Claim::query()
->where('hashcode', $claim->customerInfos->hashcode)
->whereHas('brand', fn($q) => $q->whereKey($claim->brand->getKey()))
->whereNot('id', $claim->getKey())
->paginate($this->jsonItemPerPagination) : collect();
$brandModels = CollectionHelper::paginate(
Brand::getUserAssociatedBrands(),
$this->jsonItemPerPagination,
);
$claimMessageModels = ClaimMessage::getAllModelsFromClaim($claim->getKey());
$claimSubjectModels = CollectionHelper::paginate(
ClaimSubject::getAllModelsFromBrand($claim->brand->getKey()),
$this->jsonItemPerPagination,
);
$claimTemplateModels = CollectionHelper::paginate(
ClaimMailTemplate::getAllModelsFromBrand($claim->brand->getKey()),
$this->jsonItemPerPagination,
);
return view('bo.pages.claims.edit', [
'claimModel' => $claim->load(['brand', 'claimSubject']),
'customerClaimModels' => $customerClaimModels,
'brandModels' => $brandModels,
'claimMessageModels' => $claimMessageModels,
'claimSubjectModels' => $claimSubjectModels,
'claimTemplateModels' => $claimTemplateModels,
'customerInfoModel' => $claim->customerInfos,
]);
}
/**
* Update the specified resource.
*
* @param \App\Http\Requests\Bo\Claims\UpdateClaimRequest $request
* @param \App\Models\Claim $claim
* @return \Illuminate\Http\JsonResponse
*/
public function update(UpdateClaimRequest $request, Claim $claim): \Illuminate\Http\JsonResponse
{
return DB::transaction(function () use ($request, $claim) {
$claim->fill($request->validated());
if ($claim->saveOrFail()) {
$claimMessageModels = ClaimMessage::getAllModelsFromClaim($claim->getKey());
return response()->json($claimMessageModels);
}
return response()->json([], 500);
});
}
/**
* Send the claim answer.
*
* @param \App\Http\Requests\Bo\Claims\SendClaimAnswerRequest $request
* @param \App\Models\Claim $claim
* @return \Illuminate\Http\JsonResponse
*/
public function answer(SendClaimAnswerRequest $request, Claim $claim): \Illuminate\Http\JsonResponse
{
return DB::transaction(function () use ($request, $claim) {
/** @var \App\Models\User $userModel */
$userModel = auth()->user();
$enum = ClaimResponseType::manual;
$claim->response_type = $enum;
$claim->status = ClaimStatus::treated;
if ($claim->saveOrFail()) {
// Get only the content of the response.
$content = $request->validated()['content'];
// Send the response to the claim author.
Mail::to($claim->email)->send(new ClaimAnswerMail($claim, $content));
// Create custom event with content message.
// @phpcs:disable Generic.Files.LineLength.TooLong
$content = sprintf(
"<b>%s %s</b> %s <i class='badge' style='background-color:{$enum->color()}'>%s</i><hr class='my-2'>%s",
$userModel->first_name,
$userModel->last_name,
trans('a modifié :field en', ['field' => trans('validation.custom.response_type')]),
$claim->response_type->label(),
clean($content),
);
event(new ClaimModelChanged($claim, '', '', null, $content));
// @phpcs:enable
// Return conversation.
$claimMessageModels = ClaimMessage::getAllModelsFromClaim($claim->getKey());
return response()->json($claimMessageModels);
}
return response()->json([], 500);
});
}
/**
* Undocumented function
*
* @param ViewClaimAttachmentRequest $request
* @param Claim $claim
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*/
public function viewAttachment(
ViewClaimAttachmentRequest $request,
Claim $claim
): \Symfony\Component\HttpFoundation\BinaryFileResponse {
// phpcs:enable
return response()->file(Storage::disk('private')->path(FileHelper::getPrivateFilePath($claim->file)));
}
/**
* Get a paginate resource.
* Needed to overheight the controller method and add a brand condition.
*
* @param \App\Http\Requests\Bo\PaginateModelsRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function jsonClaimPaginateModels(PaginateModelsRequest $request): \Illuminate\Http\JsonResponse
{
$data = $request->validated();
$brandModelKey = $request->route()->parameter('brand')->getKey();
$models = CollectionHelper::paginateManually(
$data['targetModel']::getAllModelsFromBrand($brandModelKey),
$this->jsonItemPerPagination,
$request->validated('page'),
);
return \response()->json($models);
}
// * EXPORT
/**
* Export claim datas to a given format
*
* @param ExportClaimRequest $request
* @return \Illuminate\Http\Response|Symfony\Component\HttpFoundation\StreamedResponse|\Illuminate\Http\RedirectResponse
*/
public function export(ExportClaimRequest $request)
{
/** @var \Illuminate\Database\Eloquent\Collection<int, \App\Models\Claim> $claims*/
$claims = Claim::query()->whereIn('id', $request->validated('claimList'))->get();
$format = ExportFormat::from($request->validated('format'));
// * CSV
if ($format->equals(ExportFormat::csvWithoutAttachment, ExportFormat::csv)) {
$csv = $this->generateCsv($claims);
if (!$this->needsZip($format, $claims)) {
return new StreamedResponse(function () use ($format, $csv) {
echo $csv;
}, 200, $this->getFileHeaders($format));
}
return $this->returnExportZip($format, $claims, $csv);
// * PDF
} elseif ($format->equals(ExportFormat::pdf)) {
$pdf = $this->generatePdf($claims);
if (!$this->needsZip(ExportFormat::pdf, $claims)) {
return $pdf->download('export.pdf');
}
return $this->returnExportZip($format, $claims, $pdf->output());
}
}
}

View file

@ -1,183 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Enums\Users\RoleEnum;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\ClaimMailTemplates\ClaimMailTemplateRequest;
use App\Models\Brand;
use App\Models\ClaimMailTemplate;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class ClaimMailTemplateController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(ClaimMailTemplate::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \App\Models\User $userModel */
$userModel = auth('backend')->user();
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = ClaimMailTemplate::query()
->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
)->when(
$userModel->role->equals(RoleEnum::marketing),
fn($q) => $q->whereHas('brand', fn($q) => $q->whereIn('id', $userModel->brands->modelKeys()))
);
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'label',
);
}
$this->sortQuery($query);
$searchFields = trans('validation.custom.label');
$claimMailTemplateModels = $query->paginate($this->getPagination());
return view('bo.pages.claim_mail_templates.index', compact('claimMailTemplateModels', 'searchFields'));
}
/**
* Show the specified resource.
*
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Contracts\View\View
*/
public function show(ClaimMailTemplate $claim_mail_template): \Illuminate\Contracts\View\View
{
return view('bo.pages.claim_mail_templates.show', [
'claimMailTemplateModel' => $claim_mail_template,
]);
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Contracts\View\View
*/
public function create(ClaimMailTemplate $claim_mail_template): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.claim_mail_templates.create', [
'claimMailTemplateModel' => $claim_mail_template,
'brandModels' => $brandModels,
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\ClaimMailTemplates\ClaimMailTemplateRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(ClaimMailTemplateRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$claimMailTemplateModel = new ClaimMailTemplate();
$claimMailTemplateModel->fill($request->validated());
if ($claimMailTemplateModel->saveOrFail()) {
return redirect()
->route('bo.claim_mail_templates.edit', $claimMailTemplateModel)
->with('success', trans(
':model a été créé.',
['model' => Str::singular(trans('models.classes.claimmailtemplates'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé.',
['model' => Str::singular(trans('models.classes.claimmailtemplates'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Contracts\View\View
*/
public function edit(ClaimMailTemplate $claim_mail_template): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.claim_mail_templates.edit', [
'claimMailTemplateModel' => $claim_mail_template,
'brandModels' => $brandModels,
]);
}
/**
* Update a spacific resource in storage.
*
* @param \App\Http\Requests\Bo\ClaimSubjects\ClaimMailTemplateRequest $request
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Http\RedirectResponse
*/
public function update(
ClaimMailTemplateRequest $request,
ClaimMailTemplate $claim_mail_template
): \Illuminate\Http\RedirectResponse {
return DB::transaction(function () use ($request, $claim_mail_template) {
$claim_mail_template->fill($request->validated());
if ($claim_mail_template->saveOrFail()) {
return redirect()
->route('bo.claim_mail_templates.edit', $claim_mail_template)
->with('success', trans(
':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.claimmailtemplates'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour.',
['model' => Str::singular(trans('models.classes.claimmailtemplates'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(ClaimMailTemplate $claim_mail_template): \Illuminate\Http\RedirectResponse
{
if ($claim_mail_template->deleteOrFail()) {
return redirect()->route('bo.claim_mail_templates.index')
->with('success', __('crud.messages.has_been_deleted', [
'model' => Str::of(__('models.classes.claimmailtemplates'))->singular()
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_deleted', [
'model' => Str::of(__('models.classes.claimmailtemplates'))->singular()
]));
}
}

View file

@ -1,53 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\ClaimMessages\StoreMessageRequest;
use App\Http\Requests\Bo\ClaimMessages\UpdateMessageRequest;
use App\Models\ClaimMessage;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Support\Facades\DB;
class ClaimMessageController extends Controller
{
use UpdateModelPublished;
/**
* Store a new specific resource.
*
* @param \App\Http\Requests\Bo\ClaimMessages\StoreMessageRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function store(StoreMessageRequest $request): \Illuminate\Http\JsonResponse
{
$this->authorize('store', ClaimMessage::class);
$validatedData = $request->validated();
return DB::transaction(function () use ($validatedData) {
$claimMessage = (new ClaimMessage())->fill($validatedData);
if ($claimMessage->saveOrFail()) {
$claimMessageModels = ClaimMessage::getAllModelsFromClaim($claimMessage->claim->getKey());
return response()->json($claimMessageModels);
}
return response()->json([], 500);
});
}
/**
* Update a specified resource.
*
* @param \App\Http\Requests\Bo\ClaimMessages\UpdateMessageRequest $request
* @param \App\Models\ClaimMessage $claim_message
* @return \Illuminate\Http\JsonResponse
*/
public function update(UpdateMessageRequest $request, ClaimMessage $claim_message): \Illuminate\Http\JsonResponse
{
$this->authorize('update', ClaimMessage::class);
$claim_message->content = $request->validated()['content'];
$claim_message->saveOrFail();
return response()->json($claim_message);
}
}

View file

@ -1,173 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\ClaimSubjects\ClaimSubjectRequest;
use App\Models\Brand;
use App\Models\ClaimSubject;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class ClaimSubjectController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(ClaimSubject::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = ClaimSubject::query()->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
);
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'label',
);
}
$this->sortQuery($query);
$searchFields = trans('validation.custom.label');
$claimSubjectModels = $query->paginate($this->getPagination());
return view('bo.pages.claim_subjects.index', compact('claimSubjectModels', 'searchFields'));
}
/**
* Show the specified resource.
*
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Contracts\View\View
*/
public function show(ClaimSubject $claim_subject): \Illuminate\Contracts\View\View
{
return view('bo.pages.claim_subjects.show', [
'claimSubjectModel' => $claim_subject
]);
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Contracts\View\View
*/
public function create(ClaimSubject $claim_subject): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.claim_subjects.create', [
'claimSubjectModel' => $claim_subject,
'brandModels' => $brandModels,
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\ClaimSubjects\ClaimSubjectRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(ClaimSubjectRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$claimSubjectModel = new ClaimSubject();
$claimSubjectModel->fill($request->validated());
if ($claimSubjectModel->saveOrFail()) {
return redirect()
->route('bo.claim_subjects.edit', $claimSubjectModel)
->with('success', trans(
':model a été créé.',
['model' => Str::singular(trans('models.classes.claimsubjects'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé.',
['model' => Str::singular(trans('models.classes.claimsubjects'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Contracts\View\View
*/
public function edit(ClaimSubject $claim_subject): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.claim_subjects.edit', [
'claimSubjectModel' => $claim_subject,
'brandModels' => $brandModels,
]);
}
/**
* Update a spacific resource in storage.
*
* @param \App\Http\Requests\Bo\ClaimSubjects\ClaimSubjectRequest $request
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Http\RedirectResponse
*/
public function update(ClaimSubjectRequest $request, ClaimSubject $claim_subject): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $claim_subject) {
$claim_subject->fill($request->validated());
if ($claim_subject->saveOrFail()) {
return redirect()
->route('bo.claim_subjects.edit', $claim_subject)
->with('success', trans(
':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.claimsubjects'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour.',
['model' => Str::singular(trans('models.classes.claimsubjects'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(ClaimSubject $claim_subject): \Illuminate\Http\RedirectResponse
{
if ($claim_subject->deleteOrFail()) {
return redirect()->route('bo.claim_subjects.index')
->with('success', __('crud.messages.has_been_deleted', [
'model' => Str::of(__('models.classes.claimsubjects'))->singular()
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_deleted', [
'model' => Str::of(__('models.classes.claimsubjects'))->singular()
]));
}
}

View file

@ -1,422 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Enums\CustomerExportType;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\CustomerExport;
use App\Models\Answer;
use App\Models\Brand;
use App\Models\Customer;
use App\Models\CustomerInfo;
use App\Models\CustomerLogin;
use App\Models\Question;
use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use League\Csv\Writer;
use Symfony\Component\HttpFoundation\StreamedResponse;
class CustomerController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Customer::class);
}
/**
* View all customers.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
$modelClass = Customer::class;
$modelTable = (new Customer())->getTable();
$questionModels = null;
$answerModels = null;
$searchFields = trans('Email, Prénom, Nom');
// Reset session sorts.
if ($request->rstSrts) {
$this->resetSessionSorts($modelTable);
}
// Reset session filters.
if ($request->rstFltrs) {
$this->resetSessionFilters($modelTable);
}
if ($request->filled('search') || Session::get("crud.$modelTable.search")) {
$columns = ['customer_infos.lastname', 'customer_infos.firstname'];
$query = $this->buildCustomerQuery();
// Apply filters.
$this->applyBrandFilter($request, $query, $modelTable, $questionModels, $answerModels);
$this->applySearchFilter($request, $query, $modelTable, $columns);
$this->applySort($request, $query, $modelTable);
$customerModels = $query->with(['customer', 'brand'])->simplePaginate($this->getPagination());
} //end if
return view('bo.pages.customers.index', [
'modelClass' => $modelClass,
'modelTable' => $modelTable,
'customerModels' => $customerModels ?? collect(),
'searchFields' => $searchFields,
'questionModels' => $questionModels,
'answerModels' => $answerModels,
]);
}
/**
* Show a specific custmer.
*
* @param \App\Models\Customer $customer
* @param \App\Models\Brand $brand
* @return \Illuminate\Contracts\View\View
*/
public function show(Customer $customer, Brand $brand): \Illuminate\Contracts\View\View
{
$customerInfos = CustomerInfo::query()->where('customer_id', $customer->id)
->where('brand_id', $brand->id)->firstOrFail();
$customerLogin = CustomerLogin::query()->where('customer_id', $customer->id)
->where('brand_id', $brand->id)->first();
return view('bo.pages.customers.show', compact(
'customerInfos',
'customerLogin'
));
}
/**
* Delete a specific customer info.
*
* @param \App\Models\Customer $customer
* @param \App\Models\Brand $brand
* @return \Illuminate\Http\RedirectResponse
*/
public function delete(Customer $customer, Brand $brand): \Illuminate\Http\RedirectResponse
{
try {
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($brand, Customer::FIRST_OR_FAIL);
/** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($brand, Customer::FIRST_OR_FAIL);
DB::transaction(function () use (
$brand,
$customer,
$customerLogin,
$customerInfo
) {
if (
$customerInfo->delete() === false ||
$customerLogin->delete() === false
) {
throw new \ErrorException('Api failed to delete customer.');
};
$customer->optInHistory()->whereHas('brand', function ($q) use ($brand) {
$q->where('id', $brand->id);
})->delete();
$logins = $customer->logins()->count();
$infos = $customer->infos()->count();
$history = $customer->optInHistory()->count();
// If customer is not associated with any brand then delete.
if (!$logins and !$infos and !$history) {
if ($customer->delete() === false) {
throw new \ErrorException('Api failed to update customer.');
}
}
});
} catch (\ErrorException $e) {
return redirect()->route('bo.customers.index')
->with('error', trans('Le compte n\'a pas pu être supprimé.'));
} //end try
return redirect()->route('bo.customers.index')->with('warning', trans('Compte supprimé.'));
}
/**
* Show export the customers form.
*
* @return \Illuminate\Contracts\View\View
*/
public function export(): \Illuminate\Contracts\View\View
{
$this->authorize('export', Customer::class);
$brands = Brand::all();
return view('bo.pages.customers.export', compact(
'brands'
));
}
/**
* Exports customers following the requested export type.
*
* @param \App\Http\Requests\Bo\CustomerExport $request
* @return \Symfony\Component\HttpFoundation\StreamedResponse
* @throws \Exception If the request export type in not handled.
*/
public function exportSubmit(CustomerExport $request): \Symfony\Component\HttpFoundation\StreamedResponse
{
$pagination = 2000;
$header = '"lastname";"firstname";"email";"hashcode"';
$w = Writer::createFromString($header . PHP_EOL);
$w->setDelimiter(';');
$w->setEnclosure('"');
$w->setOutputBOM(Writer::BOM_UTF8);
$exportType = CustomerExportType::make(\intval($request->export_type));
// * We send the CSV using Symfony StreamedResponse.
$response = new StreamedResponse();
$response->headers->set('Content-Encoding', 'none');
$response->headers->set('Content-Type', 'text/csv; charset=UTF-8');
$response->headers->set('Description', 'File Transfer');
$response->headers->set(
'Content-Disposition',
sprintf(
'attachment; filename="Export pour newsletter %s %s.csv"',
Carbon::now()->isoFormat('DD-MM-YYYY_HH-mm'),
Str::slug($exportType->label())
)
);
switch ($exportType) {
case CustomerExportType::both:
$continue = true;
// ! Continue
case CustomerExportType::customer:
$page = 1;
/** @var \Illuminate\Database\Eloquent\Builder $req */
$req = CustomerInfo::query()->where('brand_id', $request->brand)
->where('opt_in', true);
$total = $req->count();
$pages = \ceil($total / $pagination);
do {
/** @var \Illuminate\Pagination\LengthAwarePaginator $items */
$items = $req->with('customer', function ($q) {
$q->select(['id', 'email']);
})->paginate(
$pagination,
['lastname', 'firstname', 'hashcode', 'customer_id'],
'page',
$page
);
$items->map(function (CustomerInfo $customerInfo) use ($w) {
$w->insertOne([
'lastname' => $customerInfo->lastname,
'firstname' => $customerInfo->firstname,
'email' => $customerInfo->customer->email,
'hashcode' => $customerInfo->hashcode
]);
});
} while ($page++ <= $pages);
if (!isset($continue)) {
break;
}
// ! Continue
case CustomerExportType::singleOptin:
SingleOptin::query()->whereHas('brand', function (Builder $q) use ($request) {
$q->where('id', $request->brand);
})->get()->map(function (SingleOptin $singleOptin) use ($w) {
$w->insertOne([
'lastname' => '',
'firstname' => '',
'email' => $singleOptin->email,
'hashcode' => $singleOptin->hashcode
]);
});
break;
default:
throw new \Exception(sprintf('Unhandled CustomerExportType %s', $exportType->label()));
} //end switch
/*
We create a callable to output the CSV in chunk
with Symfony StreamResponse you can flush the body content if necessary
see Symfony documentation for more information
*/
// * The flush value should depend on your CSV size.
$flush_threshold = 1000;
$response->setCallback(function () use ($w, $flush_threshold) {
foreach ($w->chunk(1024) as $offset => $chunk) {
echo $chunk;
if ($offset % $flush_threshold === 0) {
flush();
}
}
});
return $response;
}
/**
* Reset all session sorts.
*
* @param string $table
* @return void
*/
private function resetSessionSorts(string $table): void
{
Session::forget([
"crud.$table.sort_col",
"crud.$table.sort_way",
]);
}
/**
* Reset all session filters.
*
* @param string $table
* @return void
*/
private function resetSessionFilters(string $table): void
{
Session::forget([
"crud.$table.brand",
"crud.$table.question",
"crud.$table.answer",
"crud.$table.search",
]);
}
/**
* Build the customer query.
*
* @return Builder
*/
private function buildCustomerQuery(): Builder
{
return CustomerInfo::query()
->join('customer_logins', function (JoinClause $join) {
$join->on('customer_logins.brand_id', '=', 'customer_infos.brand_id')
->on('customer_logins.customer_id', '=', 'customer_infos.customer_id');
})
->join('customers', fn($join) => $join->on('customers.id', '=', 'customer_infos.customer_id'))
->join('brands', fn($join) => $join->on('brands.id', '=', 'customer_infos.brand_id'));
}
/**
* Apply all filters (brand, question, answer).
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $table
* @param \Illuminate\Support\Collection|null $questionModels
* @param \Illuminate\Support\Collection|null $answerModels
* @return void
*/
private function applyBrandFilter(
Request $request,
Builder &$query,
string $table,
?Collection &$questionModels,
?Collection &$answerModels
): void {
$brand = $request->brand ?? Session::get("crud.$table.brand");
if (!(int) $brand) {
return;
}
Session::put("crud.$table.brand", $brand);
$query->where('customer_infos.brand_id', $brand);
$questionModels = Question::getAllModelsFromBrand($brand);
if (($request->filled('question') || Session::has("crud.$table.question")) && $questionModels->count()) {
$question = $request->question ?? Session::get("crud.$table.question");
Session::put("crud.$table.question", $question);
$answerModels = Answer::getAllAnswersFromQuestion($question);
$questionAnswers = Question::find($question)?->answers->modelKeys() ?? [];
$query->whereHas('customer.answers', fn($q) => $q->whereIn('answer_id', $questionAnswers));
if (
($request->filled('answer') || Session::has("crud.$table.answer"))
&& $questionModels->count()
&& $answerModels->count()
) {
$answer = $request->answer ?? Session::get("crud.$table.answer");
Session::put("crud.$table.answer", $answer);
$query->whereHas('customer.answers', fn($q) => $q->whereKey($answer));
}
}
}
/**
* Apply the search filter.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $table
* @param array $columns
* @return void
*/
private function applySearchFilter(Request $request, Builder &$query, string $table, array $columns): void
{
$search = $request->search ?? Session::get("crud.$table.search");
Session::put("crud.$table.search", $search);
$query->where(function ($q) use ($columns, $search) {
foreach ($columns as $column) {
$q->orWhere($column, 'LIKE', "%$search%");
}
$q->orWhere('customers.email', 'LIKE', "%$search%")
->orWhere('customer_logins.last_connection', 'LIKE', "%$search%");
});
}
/**
* Apply sorts.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $table
* @return void
*/
private function applySort(Request $request, Builder &$query, string $table): void
{
$sortCol = $request->sort_col ?? Session::get("crud.$table.sort_col");
$sortWay = $request->sort_way ?? Session::get("crud.$table.sort_way");
if ($sortCol) {
Session::put("crud.$table.sort_col", $sortCol);
}
if ($sortWay) {
Session::put("crud.$table.sort_way", $sortWay);
}
$sortable = [
'brands.name',
'customers.email',
'customer_infos.firstname',
'customer_infos.lastname',
'customer_infos.opt_in',
'customer_logins.last_connection',
'customer_logins.status',
];
if ($sortCol && $sortWay && (Schema::hasColumn($table, $sortCol) || in_array($sortCol, $sortable))) {
$query->orderBy($sortCol, $sortWay);
}
}
}

View file

@ -1,18 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
class DashboardController extends Controller
{
/**
* Dashboard page.
*
* @return \Illuminate\Contracts\View\View
*/
public function index(): \Illuminate\Contracts\View\View
{
return view('bo.pages.dashboard');
}
}

View file

@ -1,133 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Models\Answer;
use App\Models\Question;
use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class SingleOptinController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(SingleOptin::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
$questionModels = null;
$answerModels = null;
$searchFields = trans('E-mail');
if ($request->filled('search')) {
$query = SingleOptin::query();
$this->applyBrandAndAnswerFilters($request, $query, $questionModels, $answerModels);
$this->applySearch($request, $query);
$this->sortQuery($query);
$query->with('brand');
$singleoptinModels = $query->simplePaginate($this->getPagination());
}
return view('bo.pages.singleoptins.index', [
'singleoptinModels' => $singleoptinModels ?? collect(),
'searchFields' => $searchFields,
'questionModels' => $questionModels,
'answerModels' => $answerModels,
]);
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\SingleOptin $singleoptin
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(SingleOptin $singleoptin): \Illuminate\Http\RedirectResponse
{
$singleoptin->answers()->detach();
if ($singleoptin->deleteOrFail()) {
return redirect()->route('bo.single_optins.index')
->with('success', trans(
':model a été supprimé.',
['model' => Str::singular(trans('models.classes.singleoptins'))]
));
}
return redirect()->back()
->with('error', trans(
':model n\'a pas pu être supprimé.',
['model' => Str::singular(trans('models.classes.singleoptins'))]
));
}
/**
* Undocumented function
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Support\Collection|null $questionModels
* @param \Illuminate\Support\Collection|null $answerModels
* @return void
*/
private function applyBrandAndAnswerFilters(
Request $request,
Builder $query,
?Collection &$questionModels,
?Collection &$answerModels
): void {
if (!$request->filled('brand') || !(int) $request->brand) {
return;
}
$brandId = $request->brand;
$query->whereHas('brand', fn($q) => $q->whereKey($brandId));
$questionModels = Question::getAllModelsFromBrand($brandId);
if (!$request->filled('question')) {
return;
}
$questionId = $request->question;
$question = Question::find($questionId);
if ($question) {
$questionAnswers = $question->answers->modelKeys();
$query->whereHas('answers', fn($q) => $q->whereIn('answer_id', $questionAnswers));
$answerModels = Answer::getAllAnswersFromQuestion($questionId);
}
if ($request->filled('answer')) {
$query->whereHas('answers', fn($q) => $q->whereKey($request->answer));
}
}
/**
* Apply search filter.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
private function applySearch(Request $request, Builder $query): void
{
if ($request->filled('search')) {
$this->searchQuery($query, $request->search, null, 'email');
}
}
}

View file

@ -1,175 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Users\StoreUserRequest;
use App\Http\Requests\Bo\Users\UpdateUserRequest;
use App\Models\Brand;
use App\Models\User;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class UserController extends Controller
{
use UpdateModelPublished;
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(User::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = User::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'first_name',
'last_name',
'email',
'role',
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.first_name'),
trans('validation.attributes.last_name'),
trans('validation.attributes.email'),
trans('validation.attributes.role'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$userModels = $query->paginate($this->getPagination());
return view('bo.pages.users.index', compact('userModels', 'search', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\User $user
* @return \Illuminate\Contracts\View\View
*/
public function create(User $user): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.users.create', ['userModel' => $user, 'brandModels' => $brandModels]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Users\StoreUserRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreUserRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$user = new User();
$user->fill($request->validated());
if ($user->saveOrFail()) {
$user->brands()->sync(collect($request->validated()['brand_id'])->pluck('id')->toArray());
return redirect()->route('bo.users.edit', $user)
->with('success', __('crud.messages.has_been_created', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_created', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
});
}
/**
* Show the form for editing the specified resource.
*
* @param App\Models\User $user
* @return \Illuminate\Contracts\View\View
*/
public function edit(User $user): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.users.edit', ['userModel' => $user, 'brandModels' => $brandModels]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Users\UpdateUserRequest $request
* @param \App\Models\User $user
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateUserRequest $request, User $user): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $user) {
$user->fill($request->validated());
if ($user->saveOrFail()) {
$user->brands()->sync(collect($request->validated()['brand_id'])->pluck('id')->toArray());
return redirect()->route('bo.users.edit', $user)
->with('success', __('crud.messages.has_been_updated', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
}
return redirect()->route('bo.users.edit', $user)
->with('error', __('crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\User $user
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(User $user): \Illuminate\Http\RedirectResponse
{
if ($user->deleteOrFail()) {
return redirect()->route('bo.users.index')
->with('success', __('crud.messages.has_been_deleted', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
}
/**
* Duplicate the specified resource.
*
* @param \App\Models\User $user
* @return \Illuminate\Contracts\View\View
*/
public function duplicate(User $user): \Illuminate\Contracts\View\View
{
return $this->create($user->replicate());
}
}

View file

@ -1,35 +1,28 @@
<?php <?php
namespace App\Http\Controllers\Bo; namespace App\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Brands\StoreBrandRequest; use App\Http\Requests\Brands\StoreBrandRequest;
use App\Http\Requests\Bo\Brands\UpdateBrandRequest; use App\Http\Requests\Brands\UpdateBrandRequest;
use App\Http\Requests\Bo\PaginateModelsRequest;
use App\Models\Brand; use App\Models\Brand;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
/**
* @codeCoverageIgnore
*/
class BrandController extends Controller class BrandController extends Controller
{ {
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Brand::class);
}
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function index(Request $request): \Illuminate\Contracts\View\View public function index(Request $request)
{ {
/** @var \Illuminate\Database\Eloquent\Builder $query */ /** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Brand::query(); $query = Brand::query();
@ -50,20 +43,20 @@ class BrandController extends Controller
$q->where('opt_in', true); $q->where('opt_in', true);
}]); }]);
$brandModels = $query->paginate($this->getPagination()); $brandModels = $query->paginate(12);
return view('bo.pages.brands.index', compact('brandModels', 'searchFields')); return view('brands.index', compact('brandModels', 'searchFields'));
} }
/** /**
* Show the form for creating a new resource. * Show the form for creating a new resource.
* *
* @param \App\Models\Brand $brands * @param \App\Models\Brand $brands
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function create(Brand $brands): \Illuminate\Contracts\View\View public function create(Brand $brands)
{ {
return view('bo.pages.brands.create', [ return view('brands.create', [
'brandModel' => $brands 'brandModel' => $brands
]); ]);
} }
@ -71,17 +64,17 @@ class BrandController extends Controller
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \App\Http\Requests\Brands\Bo\StoreBrandRequest $request * @param \App\Http\Requests\Brands\StoreBrandRequest $request
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response
*/ */
public function store(StoreBrandRequest $request): \Illuminate\Http\RedirectResponse public function store(StoreBrandRequest $request)
{ {
return DB::transaction(function () use ($request) { return DB::transaction(function () use ($request) {
$brand = new Brand(); $brand = new Brand();
$brand->fill($request->validated()); $brand->fill($request->validated());
if ($brand->saveOrFail()) { if ($brand->saveOrFail()) {
return redirect() return redirect()
->route('bo.brands.edit', $brand) ->route('brands.edit', $brand)
->with('success', trans( ->with('success', trans(
':model a été créé.', ':model a été créé.',
['model' => Str::singular(trans('models.classes.brands'))] ['model' => Str::singular(trans('models.classes.brands'))]
@ -100,11 +93,11 @@ class BrandController extends Controller
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
* *
* @param \App\Models\Brand $brands * @param \App\Models\Brand $brands
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function edit(Brand $brands): \Illuminate\Contracts\View\View public function edit(Brand $brands)
{ {
return view('bo.pages.brands.edit', [ return view('brands.edit', [
'brandModel' => $brands 'brandModel' => $brands
]); ]);
} }
@ -112,17 +105,17 @@ class BrandController extends Controller
/** /**
* Update a spacific resource in storage. * Update a spacific resource in storage.
* *
* @param \App\Http\Requests\Brands\Bo\UpdateBrandRequest $request * @param \App\Http\Requests\Brands\UpdateBrandRequest $request
* @param \App\Models\Brand $brand * @param \App\Models\Brand $brand
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response
*/ */
public function update(UpdateBrandRequest $request, Brand $brand): \Illuminate\Http\RedirectResponse public function update(UpdateBrandRequest $request, Brand $brand)
{ {
return DB::transaction(function () use ($request, $brand) { return DB::transaction(function () use ($request, $brand) {
$brand->fill($request->validated()); $brand->fill($request->validated());
if ($brand->saveOrFail()) { if ($brand->saveOrFail()) {
return redirect() return redirect()
->route('bo.brands.edit', $brand) ->route('brands.edit', $brand)
->with('success', trans( ->with('success', trans(
':model a été mis à jour.', ':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.brands'))] ['model' => Str::singular(trans('models.classes.brands'))]
@ -138,18 +131,16 @@ class BrandController extends Controller
} }
/** /**
* Get a paginate resource. * Store a newly created resource in storage.
* *
* @param \App\Http\Requests\Bo\PaginateModelsRequest $request * @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*/ */
public function jsonPaginateModels(PaginateModelsRequest $request): \Illuminate\Http\JsonResponse public function jsonPaginate()
{ {
$models = CollectionHelper::paginateManually( return DB::transaction(function () {
Brand::getUserAssociatedBrands(), /** @var \App\Models\Tag */
$this->jsonItemPerPagination, $brandModels = Brand::paginate(12);
(int) $request->validated('page'), return \response()->json($brandModels ?? 500);
); });
return \response()->json($models);
} }
} }

View file

@ -2,28 +2,23 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\Pagination\ItemsPerPaginationEnum;
use App\Enums\Theme\BootstrapThemeEnum;
use App\Http\Requests\Bo\PaginateModelsRequest;
use App\Http\Requests\Bo\ThemeRequest;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class Controller extends BaseController class Controller extends BaseController
{ {
use AuthorizesRequests; use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests; use ValidatesRequests;
/** @var integer $itemPerPagination */
public $jsonItemPerPagination = 5;
/** /**
* Build Search query on specified fields * Build Search query on specified fields
* splitting search into words or using whole sentence * splitting search into words or using whole sentence
@ -41,7 +36,7 @@ class Controller extends BaseController
string $search, string $search,
callable $searchQuery = null, callable $searchQuery = null,
string ...$fields string ...$fields
): \Illuminate\Database\Eloquent\Builder { ): Builder {
if (count($fields)) { if (count($fields)) {
// Search using words. // Search using words.
$words = explode(' ', $search); $words = explode(' ', $search);
@ -70,85 +65,36 @@ class Controller extends BaseController
* @param array $fields * @param array $fields
* @return \Illuminate\Database\Eloquent\Builder * @return \Illuminate\Database\Eloquent\Builder
*/ */
private function searchOnFields( private function searchOnFields(Builder $query, string $search, array $fields): Builder
Builder $query, {
string $search,
array $fields
): \Illuminate\Database\Eloquent\Builder {
$table = $query->getModel()->getTable(); $table = $query->getModel()->getTable();
foreach ($fields as $field) { foreach ($fields as $f) {
$field = str_replace('?', '\?', $field); $f = str_replace('?', '\?', $f);
if (is_array($field)) { if (is_array($f)) {
$query->orWhereRaw( $query->orWhereRaw(
sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($field)->map(function ($q) use ($table) { sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($f)->map(function ($q) use ($table) {
return "`$table`.`$q`"; return "`$table`.`$q`";
})->implode(', \' \', ')), })->implode(', \' \', ')),
['%' . htmlspecialchars($search) . '%'] ['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%']
);
} else {
$query->orWhereRaw(
"UPPER(`$table`.`$f`) LIKE UPPER(?)",
['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%']
); );
return $query;
} }
// Get models woth enum's label. }
if (is_array($this->getSearchValue($query, $search, $field))) {
foreach ($this->getSearchValue($query, $search, $field) as $array) {
$query->orWhereRaw(
"UPPER(`$table`.`$field`) LIKE UPPER(?)",
['%' . htmlspecialchars($array->value) . '%']
);
}
return $query;
}
// Get models with relation's name/label.
if (Str::endsWith($field, '_id') && $query->getModel()->has(Str::remove('_id', $field))) {
$query->orWhereHas(Str::remove('_id', $field), function ($queryRelation) use ($field, $search) {
if (Schema::hasColumn(Str::remove('_id', $field) . 's', 'name')) {
$queryRelation->where('name', 'like', '%' . $search . '%');
} elseif (Schema::hasColumn(Str::remove('_id', $field) . 's', 'label')) {
$queryRelation->where('label', 'like', '%' . $search . '%');
}
});
return $query;
}
$query->orWhereRaw(
"UPPER(`$table`.`$field`) LIKE UPPER(?)",
['%' . htmlspecialchars($this->getSearchValue($query, $search, $field)) . '%']
);
} //end foreach
return $query; return $query;
} }
/** /**
* Get the search value (on string or Enum). * Sort columns with a query
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $search
* @param string $field
* @return string|array
*/
protected function getSearchValue(Builder $query, string $search, string $field): string|array
{
if (
count($query->getModel()->getCasts()) and // Check if the model has casts.
isset($query->getModel()->getCasts()[$field]) and // Check if there is cast about specific field.
enum_exists($query->getModel()->getCasts()[$field]) // Check if the cast is an enum.
) {
$cases = collect($query->getModel()->getCasts()[$field]::toArray());
$enum = $cases->filter(function ($item) use ($search) {
return preg_match('/' . Str::lower($search) . '/', Str::lower($item->label));
});
return $enum->toArray();
} else {
return $search;
}
}
/**
* Sort columns with a query.
* *
* @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @param array $ignore * @param array $ignore
* @return \Illuminate\Database\Eloquent\Builder * @return \Illuminate\Database\Eloquent\Builder
*/ */
protected function sortQuery(Builder $query, array $ignore = []): \Illuminate\Database\Eloquent\Builder protected function sortQuery(Builder $query, array $ignore = []): Builder
{ {
$rName = request()->route()->getName(); $rName = request()->route()->getName();
$table = $query->getModel()->getTable(); $table = $query->getModel()->getTable();
@ -160,7 +106,7 @@ class Controller extends BaseController
Session::put("$rName.sort_way", request()->sort_way); Session::put("$rName.sort_way", request()->sort_way);
Session::put("$rName.sorted", true); Session::put("$rName.sorted", true);
} }
if (request()->rstSrts) { if (request()->rst) {
Session::remove("$rName.sorted"); Session::remove("$rName.sorted");
Session::remove("$rName.sort_col"); Session::remove("$rName.sort_col");
Session::remove("$rName.sort_way"); Session::remove("$rName.sort_way");
@ -176,72 +122,7 @@ class Controller extends BaseController
$query = $query->orderBy('order', 'ASC'); $query = $query->orderBy('order', 'ASC');
Session::put("$rName.sort_col", 'order'); Session::put("$rName.sort_col", 'order');
Session::put("$rName.sort_way", 'asc'); Session::put("$rName.sort_way", 'asc');
} elseif (Schema::hasColumn($table, 'updated_at')) {
$query = $query->orderBy('updated_at', 'DESC');
Session::put("$rName.sort_col", 'updated_at');
Session::put("$rName.sort_way", 'desc');
} elseif (Schema::hasColumn($table, 'created_at')) {
$query = $query->orderBy('created_at', 'DESC');
Session::put("$rName.sort_col", 'created_at');
Session::put("$rName.sort_way", 'desc');
} }
return $query; return $query;
} }
/**
* Get the number of items per page.
*
* @param \App\Enums\Pagination\ItemsPerPaginationEnum|null $pagination
* @return integer
*/
protected function getPagination(ItemsPerPaginationEnum $pagination = null): int
{
$currentRoutePath = Str::slug(request()->route()->getName());
$cacheKey = "pagination.{$currentRoutePath}";
$pagination = $pagination ?? ItemsPerPaginationEnum::twelve;
$pagination = ToolboxHelper::getValidatedEnum(
Cache::get($cacheKey, $pagination->value()),
'pagination',
'\App\Enums\Pagination\ItemsPerPaginationEnum',
);
if (Cache::get($cacheKey) !== $pagination) {
Cache::put($cacheKey, $pagination);
}
return $pagination;
}
/**
* Get a paginate resource.
*
* @param \App\Http\Requests\Bo\PaginateModelsRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function jsonPaginateModels(PaginateModelsRequest $request): \Illuminate\Http\JsonResponse
{
/** @var \Illuminate\Database\Eloquent\Model $models */
$models = $request->targetModel::query()
->where($request->input('fieldName') ?? 'name', 'like', "%{$request->input('search')}%")
->orderBy($request->input('fieldName') ?? 'name', 'ASC')
->paginate($request->input('paginate') ?? 10);
return \response()->json($models);
}
/**
* Set the bootstrap theme or light (default).
*
* @param \App\Http\Requests\Bo\ThemeRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
protected function setTheme(ThemeRequest $request): \Illuminate\Http\RedirectResponse
{
$theme = ToolboxHelper::getValidatedEnum(
$request->validated('theme') ?: Cache::get('theme', BootstrapThemeEnum::light->value()),
'theme',
'\App\Http\Controllers\BootstrapThemeEnum',
);
if (Cache::get("theme") !== $theme) {
Cache::put("theme", $theme);
}
return redirect()->back()->with('success', __('crud.messages.theme_updated'));
}
} }

View file

@ -0,0 +1,309 @@
<?php
namespace App\Http\Controllers;
use App\Enums\CustomerExportType;
use App\Http\Requests\CustomerExport;
use App\Models\Brand;
use App\Models\Customer;
use App\Models\CustomerInfo;
use App\Models\CustomerLogin;
use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use League\Csv\Writer;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* @codeCoverageIgnore
*/
class CustomerController extends Controller
{
/**
* View all customers
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$modelTable = (new Customer())->getTable();
$modelClass = Customer::class;
$brands = Brand::select('id', 'name')->get();
$columns = ['customer_infos.lastname', 'customer_infos.firstname'];
// * Jointure pour pouvoir trier les colonnes.
/** @var \Illuminate\Database\Eloquent\Builder */
$query = CustomerInfo::join('customer_logins', function (JoinClause $join) {
$join->on('customer_logins.brand_id', '=', 'customer_infos.brand_id')
->on('customer_logins.customer_id', '=', 'customer_infos.customer_id');
})
->join('customers', function (JoinClause $join) {
$join->on('customers.id', '=', 'customer_infos.customer_id');
})
->join('brands', function (JoinClause $join) {
$join->on('brands.id', '=', 'customer_infos.brand_id');
});
if ($request->rst) {
Session::remove("crud.$modelTable.brand");
Session::remove("crud.$modelTable.search");
Session::remove("crud.$modelTable.sort_way");
Session::remove("crud.$modelTable.sort_col");
}
if (request()->brand or Session::has("crud.$modelTable.brand")) {
$brand = $request->brand ?? Session::get("crud.$modelTable.brand");
Session::put("crud.$modelTable.brand", $brand);
$query = $query->where('customer_infos.brand_id', $brand);
}
// * Search on crud Label column.
if ($request->search or Session::has("crud.$modelTable.search")) {
$search = $request->search ?? Session::get("crud.$modelTable.search");
Session::put("crud.$modelTable.search", $search);
$query->where(function ($q) use ($columns, $search) {
foreach ($columns as $column) {
$q = $q->orWhere($column, 'LIKE', "%$search%");
}
$q = $q->orWhere('customers.email', 'LIKE', "%$search%");
$q = $q->orWhere('customer_logins.last_connection', 'LIKE', "%$search%");
});
}
// * Sort columns.
if ($request->sort_col) {
Session::put("crud.$modelTable.sort_col", $request->sort_col);
}
if ($request->sort_way) {
Session::put("crud.$modelTable.sort_way", $request->sort_way);
}
if (
Session::has("crud.$modelTable.sort_col") and
Session::has("crud.$modelTable.sort_way") and
(Schema::hasColumn($modelTable, Session::get("crud.$modelTable.sort_col")) or
in_array(Session::get("crud.$modelTable.sort_col"), [
'brands.name',
'customers.email',
'customer_infos.firstname',
'customer_infos.lastname',
'customer_infos.opt_in',
'customer_logins.last_connection',
'customer_logins.status'
]))
) {
$query = $query->orderBy(
Session::get("crud.$modelTable.sort_col"),
Session::get("crud.$modelTable.sort_way")
);
}
$searchFields = trans('Email, Prénom, Nom');
/** @var \Illuminate\Pagination\AbstractPaginator */
$customerModels = $query->paginate(50);
return view('customers.index', compact(
'modelClass',
'modelTable',
'customerModels',
'brands',
'searchFields'
));
}
/**
* Show a specific custmer
*
* @param \App\Models\Customer $customer
* @param \App\Models\Brand $brand
* @return \Illuminate\Http\Response
*/
public function show(Customer $customer, Brand $brand)
{
$customerInfos = CustomerInfo::where('customer_id', $customer->id)
->where('brand_id', $brand->id)->firstOrFail();
$customerLogin = CustomerLogin::where('customer_id', $customer->id)
->where('brand_id', $brand->id)->first();
return view('customers.show', compact(
'customerInfos',
'customerLogin'
));
}
/**
* Delete a specific customer info
*
* @param \App\Models\Customer $customer
* @param \App\Models\Brand $brand
* @return \Illuminate\Http\RedirectResponse
*/
public function delete(Customer $customer, Brand $brand)
{
try {
// TODO: Gestion avec mesBR.
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($brand, Customer::FIRST_OR_FAIL);
/** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($brand, Customer::FIRST_OR_FAIL);
DB::transaction(function () use (
$brand,
$customer,
$customerLogin,
$customerInfo
) {
if (
$customerInfo->delete() === false ||
$customerLogin->delete() === false
) {
throw new \ErrorException('Api failed to delete customer.');
};
$customer->optInHistory()->whereHas('brand', function ($q) use ($brand) {
$q->where('id', $brand->id);
})->delete();
$logins = $customer->logins()->count();
$infos = $customer->infos()->count();
$history = $customer->optInHistory()->count();
// If customer is not associated with any brand then delete.
if (!$logins and !$infos and !$history) {
if ($customer->delete() === false) {
throw new \ErrorException('Api failed to update customer.');
}
}
});
} catch (\ErrorException $e) {
return redirect()->route('customers.index')
->with('error', trans('Le compte n\'a pas pu être supprimé.'));
} //end try
return redirect()->route('customers.index')->with('warning', trans('Compte supprimé.'));
}
/**
* Show export the customers form
*
* @return \Illuminate\Http\Response
*/
public function export()
{
$brands = Brand::all();
return view('customers.export', compact(
'brands'
));
}
/**
* Exports customers following the requested export type
*
* @param \App\Http\Requests\CustomerExport $request
* @return \Symfony\Component\HttpFoundation\StreamedResponse
* @throws \Exception If the request export type in not handled.
*/
public function exportSubmit(CustomerExport $request)
{
$pagination = 2000;
$header = '"lastname";"firstname";"email";"hashcode"';
$w = Writer::createFromString($header . PHP_EOL);
$w->setDelimiter(';');
$w->setEnclosure('"');
$w->setOutputBOM(Writer::BOM_UTF8);
$exportType = CustomerExportType::make(\intval($request->export_type));
// * We send the CSV using Symfony StreamedResponse.
$response = new StreamedResponse();
$response->headers->set('Content-Encoding', 'none');
$response->headers->set('Content-Type', 'text/csv; charset=UTF-8');
$response->headers->set('Description', 'File Transfer');
$response->headers->set(
'Content-Disposition',
sprintf(
'attachment; filename="Export pour newsletter %s %s.csv"',
Carbon::now()->format('d-m-Y_H-i'),
Str::slug($exportType)
)
);
switch ($exportType) {
case CustomerExportType::both():
$continue = true;
// ! Continue
case CustomerExportType::customer():
$page = 1;
/** @var \Illuminate\Database\Eloquent\Builder $req */
$req = CustomerInfo::where('brand_id', $request->brand)
->where('opt_in', true);
$total = $req->count();
$pages = \ceil($total / $pagination);
do {
/** @var \Illuminate\Pagination\LengthAwarePaginator $items */
$items = $req->with('customer', function ($q) {
$q->select(['id', 'email']);
})->paginate(
$pagination,
['lastname', 'firstname', 'hashcode', 'customer_id'],
'page',
$page
);
$items->map(function (CustomerInfo $customerInfo) use ($w) {
$w->insertOne([
'lastname' => $customerInfo->lastname,
'firstname' => $customerInfo->firstname,
'email' => $customerInfo->customer->email,
'hashcode' => $customerInfo->hashcode
]);
});
} while ($page++ <= $pages);
if (!isset($continue)) {
break;
}
// ! Continue
case CustomerExportType::singleOptin():
SingleOptin::query()->whereHas('brand', function (Builder $q) use ($request) {
$q->where('id', $request->brand);
})->get()->map(function (SingleOptin $singleOptin) use ($w) {
$w->insertOne([
'lastname' => '',
'firstname' => '',
'email' => $singleOptin->email,
'hashcode' => $singleOptin->hashcode
]);
});
break;
default:
throw new \Exception(sprintf('Unhandled CustomerExportType %s', $exportType->label));
} //end switch
/*
We create a callable to output the CSV in chunk
with Symfony StreamResponse you can flush the body content if necessary
see Symfony documentation for more information
*/
// * The flush value should depend on your CSV size.
$flush_threshold = 1000;
$response->setCallback(function () use ($w, $flush_threshold) {
foreach ($w->chunk(1024) as $offset => $chunk) {
echo $chunk;
if ($offset % $flush_threshold === 0) {
flush();
}
}
});
return $response;
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
/**
* @codeCoverageIgnore
*/
class LoginController extends Controller
{
/**
* Shows the login view
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('login');
}
/**
* Undocumented function
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function authenticate(Request $request)
{
if (
Auth::attempt(
["email" => $request->email, "password" => $request->password],
$request->remember !== null ? true : false
)
) {
return redirect()->route('customers.index');
} else {
return back()->withInput($request->except("password"))
->withErrors("Les identifiants entrés sont incorrects, veuillez réessayer.");
}
}
/**
* Undocumented function
*
* @return \Illuminate\Http\Response
*/
public function logout()
{
Auth::logout();
return redirect()->route('login');
}
}

View file

@ -1,13 +1,12 @@
<?php <?php
namespace App\Http\Controllers\Bo; namespace App\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\QuestionExport; use App\Http\Requests\QuestionExport;
use App\Http\Requests\Bo\Questions\StoreQuestionRequest; use App\Http\Requests\Questions\StoreQuestionRequest;
use App\Http\Requests\Bo\Questions\UpdateQuestionRequest; use App\Http\Requests\Questions\UpdateQuestionRequest;
use App\Models\Brand; use App\Models\Brand;
use App\Models\Customer;
use App\Models\Question; use App\Models\Question;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -16,30 +15,25 @@ use Illuminate\Support\Str;
use League\Csv\Writer; use League\Csv\Writer;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* @codeCoverageIgnore
*/
class QuestionController extends Controller class QuestionController extends Controller
{ {
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Question::class);
}
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function index(Request $request): \Illuminate\Contracts\View\View public function index(Request $request)
{ {
/** @var \Illuminate\Database\Eloquent\Builder $query */ /** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Question::query()->when( $query = Question::query();
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
);
if ($request->brand) {
$query = $query->where('brand_id', '=', request()->brand);
}
if ($request->search) { if ($request->search) {
$this->searchQuery( $this->searchQuery(
$query, $query,
@ -52,26 +46,25 @@ class QuestionController extends Controller
$this->sortQuery($query); $this->sortQuery($query);
$searchFields = trans('Question'); $searchFields = trans('Question');
$query->with(['brand', 'answers']);
/** @var \Illuminate\Pagination\AbstractPaginator */ /** @var \Illuminate\Pagination\AbstractPaginator */
$questionModels = $query->paginate($this->getPagination()); $questionModels = $query->paginate(12);
return view('bo.pages.questions.index', compact('questionModels', 'searchFields')); return view('questions.index', compact('questionModels', 'searchFields'));
} }
/** /**
* Show the form for creating a new resource. * Show the form for creating a new resource.
* *
* @param \App\Models\Question $question * @param \App\Models\Question $question
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function create(Question $question): \Illuminate\Contracts\View\View public function create(Question $question)
{ {
$brandModels = Brand::query()->orderBy('name', 'ASC')->paginate($this->jsonItemPerPagination); $brandModels = Brand::get();
return view('bo.pages.questions.create', [
return view('questions.create', [
'questionModel' => $question, 'questionModel' => $question,
'brandModels' => $brandModels 'brandModels' => $brandModels
]); ]);
} }
@ -79,16 +72,16 @@ class QuestionController extends Controller
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \App\Http\Requests\Questions\StoreQuestionRequest $request * @param \App\Http\Requests\Questions\StoreQuestionRequest $request
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response
*/ */
public function store(StoreQuestionRequest $request): \Illuminate\Http\RedirectResponse public function store(StoreQuestionRequest $request)
{ {
return DB::transaction(function () use ($request) { return DB::transaction(function () use ($request) {
$question = new Question(); $question = new Question();
$question->fill($request->validated()); $question->fill($request->validated());
if ($question->saveOrFail()) { if ($question->saveOrFail()) {
return redirect() return redirect()
->route('bo.questions.edit', $question) ->route('questions.edit', $question)
->with('success', trans( ->with('success', trans(
':model a été créé.', ':model a été créé.',
['model' => Str::singular(trans('models.classes.questions'))] ['model' => Str::singular(trans('models.classes.questions'))]
@ -107,13 +100,13 @@ class QuestionController extends Controller
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
* *
* @param \App\Models\Question $question * @param \App\Models\Question $question
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function edit(Question $question): \Illuminate\Contracts\View\View public function edit(Question $question)
{ {
$brandModels = Brand::query()->orderBy('name', 'ASC')->paginate($this->jsonItemPerPagination); $brandModels = Brand::get();
$question->load('brand');
return view('bo.pages.questions.edit', [ return view('questions.edit', [
'questionModel' => $question, 'questionModel' => $question,
'brandModels' => $brandModels 'brandModels' => $brandModels
]); ]);
@ -124,15 +117,15 @@ class QuestionController extends Controller
* *
* @param \App\Http\Requests\Questions\UpdateQuestionRequest $request * @param \App\Http\Requests\Questions\UpdateQuestionRequest $request
* @param \App\Models\Question $question * @param \App\Models\Question $question
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response
*/ */
public function update(UpdateQuestionRequest $request, Question $question): \Illuminate\Http\RedirectResponse public function update(UpdateQuestionRequest $request, Question $question)
{ {
return DB::transaction(function () use ($request, $question) { return DB::transaction(function () use ($request, $question) {
$question->fill($request->validated()); $question->fill($request->validated());
if ($question->saveOrFail()) { if ($question->saveOrFail()) {
return redirect() return redirect()
->route('bo.questions.edit', $question) ->route('questions.edit', $question)
->with('success', trans( ->with('success', trans(
':model a été mis à jour.', ':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.questions'))] ['model' => Str::singular(trans('models.classes.questions'))]
@ -151,11 +144,11 @@ class QuestionController extends Controller
* Show the specified resource. * Show the specified resource.
* *
* @param \App\Models\Question $question * @param \App\Models\Question $question
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function show(Question $question): \Illuminate\Contracts\View\View public function show(Question $question)
{ {
return view('bo.pages.questions.show', [ return view('questions.show', [
'questionModel' => $question 'questionModel' => $question
]); ]);
} }
@ -169,7 +162,7 @@ class QuestionController extends Controller
public function destroy(Question $question): \Illuminate\Http\RedirectResponse public function destroy(Question $question): \Illuminate\Http\RedirectResponse
{ {
if ($question->deleteOrFail()) { if ($question->deleteOrFail()) {
return redirect()->route('bo.questions.index') return redirect()->route('questions.index')
->with('success', trans( ->with('success', trans(
':model a été supprimé.', ':model a été supprimé.',
['model' => Str::singular(trans('models.classes.questions'))] ['model' => Str::singular(trans('models.classes.questions'))]
@ -185,13 +178,12 @@ class QuestionController extends Controller
/** /**
* Shows the question export form * Shows the question export form
* *
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function export(): \Illuminate\Contracts\View\View public function export()
{ {
$this->authorize('export', Question::class);
$brands = Brand::all(); $brands = Brand::all();
return view('bo.pages.questions.export', compact('brands')); return view('questions.export', compact('brands'));
} }
/** /**
@ -200,22 +192,19 @@ class QuestionController extends Controller
* @param \App\Http\Requests\QuestionExport $request * @param \App\Http\Requests\QuestionExport $request
* @return \Symfony\Component\HttpFoundation\StreamedResponse * @return \Symfony\Component\HttpFoundation\StreamedResponse
*/ */
public function exportSubmit(QuestionExport $request): \Symfony\Component\HttpFoundation\StreamedResponse public function exportSubmit(QuestionExport $request)
{ {
$results = []; $results = [];
/** @var \App\Models\Brand $brand */ $brand = Brand::findOrFail($request->brand);
$brand = Brand::query()->where('id', $request->brand)->firstOrFail();
$questions = $brand->questions->load('answers'); $questions = $brand->questions->load('answers');
foreach ($questions as $question) { foreach ($questions as $question) {
$answersCounts = DB::query() $answersCounts = DB::query()
->selectRaw('count(*) as total, answer_id') ->selectRaw('count(*) as total, answer_id')
->from('model_answer') ->from('customer_answer')
->where('model_type', '=', Customer::class) ->whereIn('answer_id', $question->answers->pluck('id'))
->whereIn('answer_id', $question->answers->modelKeys())
->groupBy('answer_id') ->groupBy('answer_id')
->get() ->get()->keyBy('answer_id');
->keyBy('answer_id');
$results[$question->id] = [ $results[$question->id] = [
'question' => $question->question, 'question' => $question->question,
@ -228,7 +217,7 @@ class QuestionController extends Controller
$answersCounts[$answer->id]->total ?? 0 $answersCounts[$answer->id]->total ?? 0
]; ];
} }
} //end foreach }
$w = Writer::createFromString(''); $w = Writer::createFromString('');
$w->setDelimiter(';'); $w->setDelimiter(';');
@ -244,7 +233,7 @@ class QuestionController extends Controller
'Content-Disposition', 'Content-Disposition',
sprintf( sprintf(
'attachment; filename="Export pour newsletter %s %s.csv"', 'attachment; filename="Export pour newsletter %s %s.csv"',
Carbon::now()->isoFormat('DD-MM-YYYY_HH-mm'), Carbon::now()->format('d-m-Y_H-i'),
Str::slug($brand->name) Str::slug($brand->name)
) )
); );
@ -259,13 +248,6 @@ class QuestionController extends Controller
$w->insertOne([]); $w->insertOne([]);
} }
// * Push empty line if nothing to export
if (empty($results)) {
$w->insertOne([
'Aucune question pour cette marque'
]);
}
/* /*
We create a callable to output the CSV in chunk We create a callable to output the CSV in chunk
with Symfony StreamResponse you can flush the body content if necessary with Symfony StreamResponse you can flush the body content if necessary

View file

@ -1,11 +1,10 @@
<?php <?php
namespace App\Http\Controllers\Bo; namespace App\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Requests\GenerateServiceToken;
use App\Http\Requests\Bo\GenerateServiceToken; use App\Http\Requests\Services\StoreServiceRequest;
use App\Http\Requests\Bo\Services\StoreServiceRequest; use App\Http\Requests\Services\UpdateServiceRequest;
use App\Http\Requests\Bo\Services\UpdateServiceRequest;
use App\Mail\ServiceCredentialRenewed; use App\Mail\ServiceCredentialRenewed;
use App\Models\Brand; use App\Models\Brand;
use App\Models\Service; use App\Models\Service;
@ -14,32 +13,26 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
/**
* @codeCoverageIgnore
*/
class ServiceController extends Controller class ServiceController extends Controller
{ {
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Service::class);
}
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function index(Request $request): \Illuminate\Contracts\View\View public function index(Request $request)
{ {
/** @var \Illuminate\Database\Eloquent\Builder $query */ /** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Service::query()->when( $query = Service::query();
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
);
if ($request->brand) {
$query = $query->where('brand_id', '=', request()->brand);
}
if ($request->search) { if ($request->search) {
$this->searchQuery( $this->searchQuery(
$query, $query,
@ -52,23 +45,22 @@ class ServiceController extends Controller
$this->sortQuery($query); $this->sortQuery($query);
$searchFields = trans('Nom'); $searchFields = trans('Nom');
$query->with(['brand']); $serviceModels = $query->paginate(12);
$serviceModels = $query->paginate($this->getPagination()); return view('services.index', compact('serviceModels', 'searchFields'));
return view('bo.pages.services.index', compact('serviceModels', 'searchFields'));
} }
/** /**
* Show the form for creating a new resource. * Show the form for creating a new resource.
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function create(Service $service): \Illuminate\Contracts\View\View public function create(Service $service)
{ {
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination); $brandModels = Brand::get();
return view('bo.pages.services.create', [
return view('services.create', [
'serviceModel' => $service, 'serviceModel' => $service,
'brandModels' => $brandModels 'brandModels' => $brandModels
]); ]);
@ -77,10 +69,10 @@ class ServiceController extends Controller
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \App\Http\Requests\Bo\Services\StoreServiceRequest $request * @param \App\Http\Requests\Services\StoreServiceRequest $request
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response
*/ */
public function store(StoreServiceRequest $request): \Illuminate\Http\RedirectResponse public function store(StoreServiceRequest $request)
{ {
return DB::transaction(function () use ($request) { return DB::transaction(function () use ($request) {
$service = new Service(); $service = new Service();
@ -89,7 +81,7 @@ class ServiceController extends Controller
$this->generateServicePassword($service); $this->generateServicePassword($service);
if ($service->saveOrFail()) { if ($service->saveOrFail()) {
return redirect() return redirect()
->route('bo.services.edit', $service) ->route('services.edit', $service)
->with('success', trans( ->with('success', trans(
':model a été créé.', ':model a été créé.',
['model' => Str::singular(trans('models.classes.services'))] ['model' => Str::singular(trans('models.classes.services'))]
@ -108,12 +100,13 @@ class ServiceController extends Controller
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function edit(Service $service): \Illuminate\Contracts\View\View public function edit(Service $service)
{ {
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination); $brandModels = Brand::get();
return view('bo.pages.services.edit', [
return view('services.edit', [
'serviceModel' => $service, 'serviceModel' => $service,
'brandModels' => $brandModels 'brandModels' => $brandModels
]); ]);
@ -122,17 +115,17 @@ class ServiceController extends Controller
/** /**
* Update a spacific resource in storage. * Update a spacific resource in storage.
* *
* @param \App\Http\Requests\Services\Bo\UpdateServiceRequest $request * @param \App\Http\Requests\Services\UpdateServiceRequest $request
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response
*/ */
public function update(UpdateServiceRequest $request, Service $service): \Illuminate\Http\RedirectResponse public function update(UpdateServiceRequest $request, Service $service)
{ {
return DB::transaction(function () use ($request, $service) { return DB::transaction(function () use ($request, $service) {
$service->fill($request->validated()); $service->fill($request->validated());
if ($service->saveOrFail()) { if ($service->saveOrFail()) {
return redirect() return redirect()
->route('bo.services.edit', $service) ->route('services.edit', $service)
->with('success', trans( ->with('success', trans(
':model a été mis à jour.', ':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.services'))] ['model' => Str::singular(trans('models.classes.services'))]
@ -151,11 +144,11 @@ class ServiceController extends Controller
* Show the specified resource. * Show the specified resource.
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function show(Service $service): \Illuminate\Contracts\View\View public function show(Service $service)
{ {
return view('bo.pages.services.show', [ return view('services.show', [
'serviceModel' => $service 'serviceModel' => $service
]); ]);
} }
@ -169,7 +162,7 @@ class ServiceController extends Controller
public function destroy(Service $service): \Illuminate\Http\RedirectResponse public function destroy(Service $service): \Illuminate\Http\RedirectResponse
{ {
if ($service->deleteOrFail()) { if ($service->deleteOrFail()) {
return redirect()->route('bo.services.index') return redirect()->route('services.index')
->with('success', trans( ->with('success', trans(
':model a été supprimé.', ':model a été supprimé.',
['model' => Str::singular(trans('models.classes.services'))] ['model' => Str::singular(trans('models.classes.services'))]
@ -183,19 +176,17 @@ class ServiceController extends Controller
} }
/** /**
* Regen service credentials. * Regen service credentials
* *
* @param \App\Http\Requests\Bo\GenerateServiceToken $request * @param \App\Http\Requests\GenerateServiceToken $request
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*/ */
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed, Squiz.Commenting.FunctionComment.Missing // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed, Squiz.Commenting.FunctionComment.Missing
public function generateServicePassword( public function generateServicePassword(Service $service, GenerateServiceToken $request = null)
Service $service, {
GenerateServiceToken $request = null
): \Illuminate\Http\RedirectResponse {
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = auth('backend')->user(); $user = auth()->user();
// * Special Condition for migration from v2 to v3. // * Special Condition for migration from v2 to v3.
$service->bearer_token = !Hash::needsRehash($service->bearer_token) ? $service->bearer_token = !Hash::needsRehash($service->bearer_token) ?

View file

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class SingleOptinController extends Controller
{
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = SingleOptin::query();
if ($request->brand) {
$query = $query->whereHas('brand', function (Builder $q) {
$q->where('id', request()->brand);
});
}
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'email',
);
}
$this->sortQuery($query);
$searchFields = trans('E-mail');
/** @var \Illuminate\Pagination\AbstractPaginator */
$singleoptinModels = $query->paginate(12);
return view('singleoptins.index', compact('singleoptinModels', 'searchFields'));
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\SingleOptin $singleoptin
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(SingleOptin $singleoptin): \Illuminate\Http\RedirectResponse
{
if ($singleoptin->deleteOrFail()) {
return redirect()->route('single_optins.index')
->with('success', trans(
':model a été supprimé.',
['model' => Str::singular(trans('models.classes.singleoptins'))]
));
}
return redirect()->back()
->with('error', trans(
':model n\'a pas pu être supprimé.',
['model' => Str::singular(trans('models.classes.singleoptins'))]
));
}
}

View file

@ -1,50 +1,50 @@
<?php <?php
namespace App\Http\Controllers\Bo; namespace App\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Requests\StatsByBrandRequest;
use App\Http\Requests\Bo\StatsByBrandRequest; use App\Http\Requests\StatsByQuestionRequest;
use App\Http\Requests\Bo\StatsByQuestionRequest;
use App\Models\Brand; use App\Models\Brand;
use App\Models\BrandStat; use App\Models\BrandStat;
use App\Models\ModelAnswer; use App\Models\CustomerAnswer;
use App\Models\Question; use App\Models\Question;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use League\Csv\Writer; use League\Csv\Writer;
/**
* @codeCoverageIgnore
*/
class StatsController extends Controller class StatsController extends Controller
{ {
/** /**
* Shows stats by brand. * Shows stats by brand
* *
* @param \App\Http\Requests\Bo\StatsByBrandRequest $request * @param \App\Http\Requests\StatsByBrandRequest $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function viewStatsByBrand(StatsByBrandRequest $request) public function viewStatsByBrand(StatsByBrandRequest $request)
{ {
Gate::authorize('viewStatsByBrand');
$stats = null; $stats = null;
if ($request->post()) { if ($request->post()) {
$stats = BrandStat::query()->whereDate('created_at', '>=', $request->start) $stats = BrandStat::whereDate('created_at', '>=', $request->start)
->whereDate('created_at', '<=', $request->end) ->whereDate('created_at', '<=', $request->end)
->whereBrandId($request->brand)->get(); ->whereBrandId($request->brand)->get();
} }
return view('bo.pages.stats.view-stats-by-brands', \compact('stats')); return view('stats.view-stats-by-brands', \compact('stats'));
} }
/** /**
* Exports stats by brand as a csv file. * Exports stats by brand as a csv file
* *
* @param \App\Http\Requests\Bo\StatsByBrandRequest $request * @param \App\Http\Requests\StatsByBrandRequest $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function exportByBrandCsv(StatsByBrandRequest $request): \Illuminate\Http\Response public function exportByBrandCsv(StatsByBrandRequest $request)
{ {
$brand = Brand::query()->findOrFail($request->brand); $brand = Brand::findOrFail($request->brand);
$stats = BrandStat::query()->with('brand')->whereDate('created_at', '>=', $request->start) $stats = BrandStat::with('brand')->whereDate('created_at', '>=', $request->start)
->whereDate('created_at', '<=', $request->end) ->whereDate('created_at', '<=', $request->end)
->whereBrandId($request->brand)->get(); ->whereBrandId($request->brand)->get();
@ -62,7 +62,7 @@ class StatsController extends Controller
foreach ($stats as $stat) { foreach ($stats as $stat) {
$cols = [ $cols = [
$stat->created_at->isoFormat('YYYY-MM-DD'), $stat->created_at->format('Y-m-d'),
$stat->brand->name $stat->brand->name
]; ];
foreach ($columns as $colname) { foreach ($columns as $colname) {
@ -70,7 +70,7 @@ class StatsController extends Controller
} }
$w->insertOne($cols); $w->insertOne($cols);
} }
return response((string) $w, 200, [ return response((string)$w, 200, [
'Content-Encoding' => 'none', 'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8', 'Content-Type' => 'text/csv; charset=UTF-8',
// phpcs:ignore Generic.Files.LineLength.TooLong // phpcs:ignore Generic.Files.LineLength.TooLong
@ -80,19 +80,18 @@ class StatsController extends Controller
} }
/** /**
* Views stats for a specific question. * Views stats for a specific question
* *
* @param \App\Http\Requests\Bo\StatsByQuestionRequest $request * @param \App\Http\Requests\StatsByQuestionRequest $request
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Http\Response
*/ */
public function viewByQuestion(StatsByQuestionRequest $request): \Illuminate\Contracts\View\View public function viewByQuestion(StatsByQuestionRequest $request)
{ {
Gate::authorize('viewByQuestion');
$question = null; $question = null;
$answers = null; $answers = null;
$answersByDays = null; $answersByDays = null;
if ($request->post()) { if ($request->post()) {
$question = Question::query()->findOrFail($request->question); $question = Question::findOrFail($request->question);
$answers = $question->answers; $answers = $question->answers;
$start = Carbon::parse($request->start); $start = Carbon::parse($request->start);
$end = Carbon::parse($request->end); $end = Carbon::parse($request->end);
@ -100,18 +99,18 @@ class StatsController extends Controller
$answersByDays = $this->answersByDays($answers, $start, $end); $answersByDays = $this->answersByDays($answers, $start, $end);
} }
return view('bo.pages.stats.view-stats-by-questions', \compact('question', 'answers', 'answersByDays')); return view('stats.view-stats-by-questions', \compact('question', 'answers', 'answersByDays'));
} }
/** /**
* Exports stats for a specific question as csv. * Exports stats for a specific question as csv
* *
* @param \App\Http\Requests\Bo\StatsByQuestionRequest $request * @param \App\Http\Requests\StatsByQuestionRequest $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function exportByQuestionCsv(StatsByQuestionRequest $request): \Illuminate\Http\Response public function exportByQuestionCsv(StatsByQuestionRequest $request)
{ {
$question = Question::query()->findOrFail($request->question); $question = Question::findOrFail($request->question);
$answers = $question->answers; $answers = $question->answers;
$start = Carbon::parse($request->start); $start = Carbon::parse($request->start);
$end = Carbon::parse($request->end); $end = Carbon::parse($request->end);
@ -135,7 +134,7 @@ class StatsController extends Controller
$dayAnswers->values()->all() $dayAnswers->values()->all()
)); ));
} }
return response((string) $w, 200, [ return response((string)$w, 200, [
'Content-Encoding' => 'none', 'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8', 'Content-Type' => 'text/csv; charset=UTF-8',
// phpcs:ignore Generic.Files.LineLength.TooLong // phpcs:ignore Generic.Files.LineLength.TooLong
@ -145,7 +144,7 @@ class StatsController extends Controller
} }
/** /**
* Makes a collection of total users answers by day. * Makes a collection of total users answers by day
* *
* @param \Illuminate\Support\Collection $answers A collection of Answer. * @param \Illuminate\Support\Collection $answers A collection of Answer.
* @param \Illuminate\Support\Carbon $start The wanted start period. * @param \Illuminate\Support\Carbon $start The wanted start period.
@ -153,18 +152,16 @@ class StatsController extends Controller
* @return \Illuminate\Support\Collection A collection with date as Y_m_d index * @return \Illuminate\Support\Collection A collection with date as Y_m_d index
* with array of answers and their total. * with array of answers and their total.
*/ */
private function answersByDays(Collection $answers, Carbon $start, Carbon $end): \Illuminate\Support\Collection private function answersByDays(Collection $answers, Carbon $start, Carbon $end): Collection
{ {
Gate::authorize('answersByDays');
// * Totalize by date all answers. // * Totalize by date all answers.
$answersByDays = ModelAnswer::query() $answersByDays = CustomerAnswer::whereDate('created_at', '>=', $start)
->whereDate('created_at', '>=', $start)
->whereDate('created_at', '<=', $end) ->whereDate('created_at', '<=', $end)
->whereIn('answer_id', $answers->modelKeys()) ->whereIn('answer_id', $answers->pluck('id'))
->get()->map(function ($custAnswer) { ->get()->map(function ($custAnswer) {
return [ return [
'answer_id' => $custAnswer->answer_id, 'answer_id' => $custAnswer->answer_id,
'created_at' => $custAnswer->created_at->isoFormat('YYYY-MM-DD'), 'created_at' => $custAnswer->created_at->format('Y-m-d'),
]; ];
})->groupBy(['created_at', 'answer_id'])->map(function ($day) { })->groupBy(['created_at', 'answer_id'])->map(function ($day) {
return $day->map(function ($custAnswers) { return $day->map(function ($custAnswers) {
@ -173,7 +170,7 @@ class StatsController extends Controller
}); });
do { do {
$currentDate = $start->isoFormat('YYYY-MM-DD'); $currentDate = $start->format('Y-m-d');
/** @var \Illuminate\Support\Collection $answersByDays */ /** @var \Illuminate\Support\Collection $answersByDays */
if (!$answersByDays->has($currentDate)) { if (!$answersByDays->has($currentDate)) {
$answersByDays->put( $answersByDays->put(

View file

@ -14,12 +14,12 @@ class Kernel extends HttpKernel
* @var array * @var array
*/ */
protected $middleware = [ protected $middleware = [
\App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class, \App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class, \Fruitcake\Cors\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class, \App\Http\Middleware\TrimStrings::class,
\App\Http\Middleware\XssSanitizer::class
]; ];
/** /**
@ -38,18 +38,14 @@ class Kernel extends HttpKernel
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SecureHeaders::class \App\Http\Middleware\SecureHeaders::class
], ],
'api' => [ 'api' => [
\Illuminate\Routing\Middleware\ThrottleRequests::class . ':api', 'throttle:api',
\App\Http\Middleware\JsonOnly::class, \App\Http\Middleware\JsonOnly::class,
\App\Http\Middleware\ApiResponse::class, \App\Http\Middleware\ApiResponse::class,
\App\Http\Middleware\ApiCheckTimeStamps::class, \App\Http\Middleware\ApiCheckTimeStamps::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class \Illuminate\Routing\Middleware\SubstituteBindings::class
], ],
'saml' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
],
]; ];
/** /**
@ -60,17 +56,15 @@ class Kernel extends HttpKernel
* @var array * @var array
*/ */
protected $routeMiddleware = [ protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class, 'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class, 'check-brand' => \App\Http\Middleware\CheckBrand::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class,
'logoutusers' => \App\Http\Middleware\LogoutUnpublishedUsers::class,
]; ];
} }

View file

@ -3,7 +3,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Enums\ApiError; use App\Enums\ApiError;
use App\Exceptions\Api\V4\ApiErrorException; use App\Exceptions\Api\V2\ApiErrorException;
use App\Lib\Helper; use App\Lib\Helper;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
@ -21,21 +21,21 @@ class ApiCheckTimeStamps
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
* @throws \App\Exceptions\Api\V4\ApiErrorException With ApiError::wrongTimeStamp * @throws \App\Exceptions\Api\V2\ApiErrorException With ApiError::wrongTimeStamp
* if timestamp has an incorrect * if timestamp has an incorrect
* format or is timed out. * format or is timed out.
*/ */
public function handle(Request $request, \Closure $next): mixed public function handle(Request $request, \Closure $next)
{ {
if (Helper::isRequestForApiV2($request) || Helper::isRequestForApiV4($request)) { if (Helper::isRequestForApiV2($request)) {
if (!\is_string($request->timestamp) or !Carbon::hasFormat($request->timestamp, 'Y-m-d H:i:s')) { if (!\is_string($request->timestamp) or !Carbon::hasFormat($request->timestamp, 'Y-m-d H:i:s')) {
throw new ApiErrorException(ApiError::wrongTimeStamp); throw new ApiErrorException(ApiError::wrongTimeStamp());
} }
if ( if (
Carbon::parse($request->timestamp, 'utc') Carbon::parse($request->timestamp, 'utc')
->isBefore(Carbon::now('UTC')->subSeconds(config('api.timestampTimeout', 10))) ->isBefore(Carbon::now('UTC')->subSeconds(config('api.timestampTimeout', 10)))
) { ) {
throw new ApiErrorException(ApiError::wrongTimeStamp); throw new ApiErrorException(ApiError::wrongTimeStamp());
} }
} }
return $next($request); return $next($request);

View file

@ -4,11 +4,12 @@ namespace App\Http\Middleware;
use App\Enums\ApiStatus; use App\Enums\ApiStatus;
use App\Http\Resources\Api\V2\ApiResponse as V2ApiResponse; use App\Http\Resources\Api\V2\ApiResponse as V2ApiResponse;
use App\Http\Resources\Api\V4\ApiResponse as V4ApiResponse;
use App\Lib\Helper; use App\Lib\Helper;
use App\Models\Service; use App\Models\Service;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ApiResponse class ApiResponse
{ {
@ -20,7 +21,7 @@ class ApiResponse
* @return mixed * @return mixed
* @throws \ErrorException If the received response is not a JsonResponse. * @throws \ErrorException If the received response is not a JsonResponse.
*/ */
public function handle(Request $request, \Closure $next): mixed public function handle(Request $request, \Closure $next)
{ {
/** @var \Illuminate\Http\JsonResponse $response */ /** @var \Illuminate\Http\JsonResponse $response */
$response = $next($request); $response = $next($request);
@ -34,6 +35,19 @@ class ApiResponse
// Set no cache header. // Set no cache header.
$response->header('Cache-Control', 'No-Store'); $response->header('Cache-Control', 'No-Store');
if (Helper::isRequestForApiV1($request)) {
// @codeCoverageIgnoreStart
try {
$bearerToken = Helper::getBearerTokenFromRequest($request) ?? $request->api_token ?? '';
$service = Service::getFromBearerToken($bearerToken);
$service->incrementStat();
} catch (ModelNotFoundException $e) {
Log::error(sprintf('Could not increment stats %s', $e->getMessage()));
}
return $response;
// @codeCoverageIgnoreEnd
}
/** @var \App\Models\Service|null */ /** @var \App\Models\Service|null */
if (\optional($service = app(Service::class))->id) { if (\optional($service = app(Service::class))->id) {
$service->incrementStat(); $service->incrementStat();
@ -42,23 +56,18 @@ class ApiResponse
/** @var object $data */ /** @var object $data */
$data = $response->getData(); $data = $response->getData();
/** Check if it's V2 or V4 response. */ // If reponse is already a V2ApiResponse then just return it. This happens if error handler comes to action.
if ( if (
gettype($data) === "object" and
isset($data->status) and isset($data->status) and
isset($data->data) and isset($data->data) and
isset($data->version) and isset($data->version) and
in_array($data->status, [ApiStatus::OK->value(), ApiStatus::KO->value()]) in_array($data->status, [ApiStatus::OK()->value, ApiStatus::KO()->value])
) { ) {
/** Return direct ApiResponse */
return $response; return $response;
} }
$status = intval($response->getStatusCode() / 100) == 2 ? ApiStatus::OK : ApiStatus::KO; /** @var \App\Enums\ApiStatus $status */
$status = intval($response->getStatusCode() / 100) == 2 ? ApiStatus::OK() : ApiStatus::KO();
if (Helper::isRequestForApiV2($request)) { return $response->setData(new V2ApiResponse((object)$response->getData(), $status));
return $response->setData(new V2ApiResponse($status, (object) $data));
}
return $response->setData(new V4ApiResponse($status, (object) $data));
} }
} }

View file

@ -2,38 +2,57 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Lib\Helper;
use App\Models\Service;
use Closure;
use Illuminate\Auth\Middleware\Authenticate as Middleware; use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
/**
* Updated for ApiV2
*/
class Authenticate extends Middleware class Authenticate extends Middleware
{ {
/** /**
* Get the path the user should be redirected to when they are not authenticated. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request * @param mixed $request
* @return string|null * @param \Closure $next
* @param mixed ...$guards
* @return mixed
*
* @throws \Illuminate\Auth\AuthenticationException If authentification fails.
*/ */
protected function redirectTo(Request $request): ?string public function handle($request, Closure $next, ...$guards)
{ {
$logins = [ if (Helper::isRequestForApiV1($request)) {
'backend' => route('bo.login') // @codeCoverageIgnoreStart
]; // Use patched v1 Auth mecanism (without bearer reimplementation).
if (!$request->expectsJson()) { $bearerToken = Helper::getBearerTokenFromRequest($request) ?? $request->api_token ?? '';
// * Notify must login $service = Service::getFromBearerToken($bearerToken);
Session::flash('warning', trans('Veuillez vous connecter pour continuer')); $request->request->add(['brand_id' => $service->brand->id]);
$response = $next($request);
foreach ($request->route()->gatherMiddleware() as $middleware) { // @codeCoverageIgnoreEnd
if (\is_string($middleware) and strpos($middleware, 'auth:') !== false) { } else {
$route = $logins[str_replace('auth:', '', $middleware)] ?? null; // Use Laravel auth check mecanism.
if ($route) { $response = \call_user_func_array(
return $route; [$this, 'parent::' . __FUNCTION__],
} array_merge([$request, $next], $guards)
} );
}
return route('bo.login');
} }
return null; return $response;
}
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param mixed $request
* @return string|null
* @codeCoverageIgnore
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
} }
} }

View file

@ -0,0 +1,40 @@
<?php
namespace App\Http\Middleware;
use App\Lib\Helper;
use App\Models\Service;
use Closure;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
/**
* @codeCoverageIgnore
*/
class CheckBrand
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$apiToken = Helper::getBearerTokenFromRequest($request) ?? $request->api_token ?? '';
if ($apiToken === null) {
abort(401, 'Le token d\'authentification est incorrect ' . \json_encode($request->all()));
}
try {
$brand = Service::getFromBearerToken($apiToken)->brand;
$request->request->add(['brand_id' => $brand->id]);
} catch (ModelNotFoundException $e) {
abort(401, 'Le token d\'authentification est incorrect ' . $e->getMessage());
}
return $next($request);
}
}

View file

@ -1,63 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Http\Request;
class HtmlSanitizer
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|array ...$params
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(
Request $request,
\Closure $next,
string|array ...$params
): \Symfony\Component\HttpFoundation\Response {
$inputs = $request->all();
/** @var callable|null */
$cleanInput = null;
$cleanInput = function (
null|string|array &$input,
string $name,
?string $rootName = null
) use (
$params,
&$cleanInput
): void {
// * Nested element array
if (is_array($input)) {
array_walk_recursive($input, $cleanInput, $name);
}
if (!is_string($input)) {
return;
}
if (in_array($name, $params) or in_array($rootName ?? $name, $params)) {
// ? Is a json input ?
if ($isJson = \json_decode($input, true) and is_array($isJson)) {
// ! Decode each json field recursive.
\array_walk_recursive($isJson, function (string|array &$item) {
if (\is_string($item)) {
$item = clean($item);
$item = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($item);
}
});
} else {
$input = clean($input);
$input = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($input);
}
} else {
$input = strip_tags($input);
}
};
\array_walk($inputs, $cleanInput);
$request->merge($inputs);
return $next($request);
}
}

View file

@ -2,6 +2,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Lib\Helper;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class JsonOnly class JsonOnly
@ -13,9 +14,11 @@ class JsonOnly
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle(Request $request, \Closure $next): mixed public function handle(Request $request, \Closure $next)
{ {
// TODO remove condition v1 check when dropped.
if ( if (
!Helper::isRequestForApiV1($request) and
(!$request->isJson() or (!$request->isJson() or
!$request->expectsJson() or !$request->expectsJson() or
is_null(json_decode($request->getContent()))) is_null(json_decode($request->getContent())))

View file

@ -1,28 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LogoutUnpublishedUsers
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
/** @var \App\Models\User|null $userModel */
$userModel = Auth::user();
if (!is_null($userModel) && !$userModel->published) {
Auth::logout();
return redirect()->route('bo.login')->with('warning', trans('auth.unpublished_user'));
}
return $next($request);
}
}

View file

@ -9,7 +9,7 @@ class PreventRequestsDuringMaintenance extends Middleware
/** /**
* The URIs that should be reachable while maintenance mode is enabled. * The URIs that should be reachable while maintenance mode is enabled.
* *
* @var array<int, string> * @var array
*/ */
protected $except = []; protected $except = [];
} }

View file

@ -2,39 +2,31 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Illuminate\Support\Facades\Auth;
/**
* @codeCoverageIgnore
*/
class RedirectIfAuthenticated class RedirectIfAuthenticated
{ {
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next * @param \Closure $next
* @param string|null ...$guards * @param string|null ...$guards
* @return \Symfony\Component\HttpFoundation\Response * @return mixed
* @throws \RuntimeException If guard is unhandled.
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/ */
public function handle(Request $request, \Closure $next, ...$guards): Response public function handle(Request $request, Closure $next, ...$guards)
{ {
// phpcs:enable phpcs:disable
$guards = empty($guards) ? [null] : $guards; $guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) { foreach ($guards as $guard) {
if (auth($guard)->check()) { if (Auth::guard($guard)->check()) {
switch ($guard) { return redirect(RouteServiceProvider::HOME);
case 'frontend':
return \redirect()->route('fo.homepage');
case 'backend':
if (!$request->routeIs('bo.*')) {
continue;
}
return \redirect()->route('bo.dashboard');
default:
throw new \RuntimeException("Unhandled guard `{$guard}` .");
}
} }
} }

View file

@ -6,7 +6,6 @@ use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Vite;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
class SecureHeaders class SecureHeaders
@ -34,10 +33,9 @@ class SecureHeaders
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
// phpcs:enable // phpcs:enable
View::share('nonce', $nonce = Vite::useCspNonce()); View::share('nonce', $nonce = \uniqid());
// * For VueJS dev tool to execute // * For VueJS dev tool to execute
$jsDev = config('app.debug') ? "'unsafe-eval' 'unsafe-inline'" : "'nonce-{$nonce}' 'strict-dynamic'"; $jsDev = config('app.debug') ? "'unsafe-eval' 'unsafe-inline'" : "'nonce-{$nonce}' 'strict-dynamic'";
$scheme = config('app.env') === 'local' ? 'http' : 'https';
$reportUri = Route::has('cspReportUri') ? $reportUri = Route::has('cspReportUri') ?
\sprintf(' report-uri %s ;', \parse_url(route('cspReportUri'), \PHP_URL_PATH)) : ''; \sprintf(' report-uri %s ;', \parse_url(route('cspReportUri'), \PHP_URL_PATH)) : '';
$this->removeUnwantedHeaders($this->unwantedHeaderList); $this->removeUnwantedHeaders($this->unwantedHeaderList);
@ -48,12 +46,11 @@ class SecureHeaders
$response->headers->set('X-Frame-Options', 'SAMEORIGIN'); $response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains;'); $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains;');
$cspPolicies = [ $cspPolicies = [
"default-src 'none';",
"manifest-src 'self';", "manifest-src 'self';",
"connect-src 'self' blob:;", "connect-src 'self' blob:;",
"img-src * data: blob:;", "img-src * data: blob:;",
"font-src 'self' data: https://fonts.gstatic.com;", "font-src 'self' data: https://fonts.gstatic.com;",
"style-src 'self' 'unsafe-inline' " . \implode(' ', []) . ';', "style-src 'self' 'unsafe-inline';",
"script-src 'self' {$jsDev} " . \implode(' ', [ "script-src 'self' {$jsDev} " . \implode(' ', [
'https: blob:', 'https: blob:',
]) . ';' ]) . ';'
@ -73,7 +70,7 @@ class SecureHeaders
* @param array $headerList * @param array $headerList
* @return void * @return void
*/ */
private function removeUnwantedHeaders(array $headerList): void private function removeUnwantedHeaders(array $headerList)
{ {
foreach ($headerList as $header) { foreach ($headerList as $header) {
header_remove($header); header_remove($header);

View file

@ -9,10 +9,9 @@ class TrimStrings extends Middleware
/** /**
* The names of the attributes that should not be trimmed. * The names of the attributes that should not be trimmed.
* *
* @var array<int, string> * @var array
*/ */
protected $except = [ protected $except = [
'current_password',
'password', 'password',
'password_confirmation', 'password_confirmation',
]; ];

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