Commit 10c6b239 by Damien Moulard

Merge branch '5691-paiement-reccurent-payzen' into 'ssa-gironde'

payzen preccurent payment

See merge request cooperatic/kohinos-tav!77
parents 41fca9e5 ff90af24
......@@ -50,6 +50,7 @@ PRESTA_SELF_INIT_AND_EVAL=0
AUTOMATISATION_RECONVERSION=0
PRESTA_EXTRA_DATA=0
HOUSEHOLD_BASED_ALLOWANCE=0
SSA_FRIENDLY_FLUX_TYPE_NAMES=0
EMAIL_ERROR=technique@kohinos.net
EMAIL_USER_FROM=noreply@kohinos.fr
......
......@@ -68,4 +68,5 @@ templates/themes/custom/
.idea/
#dir containg exported reconversions xml
/reconversions/
reconversions/*
!reconversions/.versionme
......@@ -58,6 +58,7 @@ Copier le fichier .env.dist en .env et configurer :
- en environnement TAV, la variable AUTOMATISATION_RECONVERSION permet d'activer l'automatisation des reconversions
- en environnement TAV, la variable PRESTA_EXTRA_DATA permet d'indiquer puis d'afficher publiquement plus de données concernant les prestataires (e.g. familles de produits)
- en environnement TAV, la variable HOUSEHOLD_BASED_ALLOWANCE permet d'activer un mode de cotisations libres et d'allocations basées sur la composition du foyer
- en environnement TAV, la variable SSA_FRIENDLY_FLUX_TYPE_NAMES permet d'adapter le nommage des types de flux à un projet de type ssa
Si vous utilisez Payzen comme moyen de paiement par CB :
......@@ -148,7 +149,9 @@ Une fois tout installé, éventuellement changer le logo et favicon situés dans
Une fois Payzen configuré sur le Kohinos, il faut le configurer sur le Back Office de Payzen :
Paramétrage -> Boutique : configurer les 2 "URL de retour de la boutique" avec la page d'accueil du site.
Paramétrage -> Règles de notification -> URL de notification à la fin du paiement : dans le 2ème encadré, renseigner les 2 champs avec [url_du_site]/payment/done
Paramétrage -> Règles de notification -> URL de notification à la fin du paiement : dans le 2ème encadré, renseigner les 2 champs avec [url_du_site]/payment/notify
Paramétrage -> Règles de notification -> URL de notification à la création d'un abonnement (mal nommé à mon avis car utilisé à chaque échéance) : dans le 2ème encadré, renseigner les 2 champs avec [url_du_site]/payment/notify
Remarque : les URLS de notification sont réécrites par Payum à la même valeur dans le cadre de sa configuration par défaut
Une fois l'installation et la configuration du Kohinos terminées, il est conseillé de réaliser un premier paiement afin de tester le bon fonctionnement de la communication avec Payzen en environnement de production.
......
......@@ -429,3 +429,8 @@ form[name="formEncaissement"] label {
.text-error {
color: #ff4136;
}
.tav-cotisation-payment-form {
display: none;
margin-top: 1rem;
}
\ No newline at end of file
......@@ -806,4 +806,26 @@ $(function() {
$('.prestataire-product-families-select').on('change', onPrestataireProductFamilyChange);
};
$("#add-prestataire-products-family").on("click", addFormToCollection);
$("#tav-cotisation-payment-type").on("change", function() {
$(".tav-cotisation-payment-form").hide();
if (this.value === "instant-payment") {
$("#tav-cotisation-instant-payment-container").show();
} else if (this.value === "recurrent-payment") {
$("#tav-cotisation-recurrent-payment-container").show();
}
});
$("input:text[name='formAchatMonnaieAdherentRecurrent[don][montant]']").on('input', function() {
let montant = 0;
montant = parseFloat($('input#formAchatMonnaieAdherentRecurrent_montant').val());
let montantDon = parseFloat($("input:text[name='formAchatMonnaieAdherentRecurrent[don][montant]']").val().replace(",", "."));
if (isNaN(montantDon)) {
montantDon = 0;
}
var valuetotal = montant + montantDon;
$("span.achat_monnaie_premier_montant_total").text(valuetotal + ' €')
});
});
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b4f4376c4660e8a7ab3c2a419263798f",
"content-hash": "9f1667eb8d3defce6dc7895876f07cd5",
"packages": [
{
"name": "alcohol/iso4217",
......@@ -2525,6 +2525,46 @@
"time": "2021-10-11T09:18:27+00:00"
},
{
"name": "ekyna/payum-payzen",
"version": "dev-5691-paiement-reccurent-payzen",
"dist": {
"type": "path",
"url": "./lib/ekyna/payum-payzen",
"reference": "781b74869b2acc65e7635c5bd5d40c66cf1c2177"
},
"require": {
"payum/core": "^1.5",
"php": "^7.0",
"php-http/guzzle6-adapter": "^2.0",
"psr/log": "~1.0",
"symfony/process": "4.4.*"
},
"type": "component",
"autoload": {
"psr-4": {
"Ekyna\\Component\\Payum\\Payzen\\": ""
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "Etienne Dauvergne",
"homepage": "http://ekyna.com"
}
],
"description": "PayZen Payum gateway",
"homepage": "https://github.com/ekyna/PayumPayzen",
"keywords": [
"payum",
"payzen"
],
"transport-options": {
"relative": true
}
},
{
"name": "exsyst/swagger",
"version": "v0.4.2",
"source": {
......@@ -19601,7 +19641,9 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"ekyna/payum-payzen": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
......
......@@ -16,3 +16,4 @@ twig:
presta_extra_data: '%env(PRESTA_EXTRA_DATA)%'
household_based_allowance: '%env(HOUSEHOLD_BASED_ALLOWANCE)%'
automatisation_reconversion: '%env(AUTOMATISATION_RECONVERSION)%'
ssa_friendly_flux_type_names: '%env(SSA_FRIENDLY_FLUX_TYPE_NAMES)%'
\ No newline at end of file
......@@ -17,6 +17,7 @@ parameters:
automatisation_reconversion: '%env(AUTOMATISATION_RECONVERSION)%'
presta_extra_data: '%env(PRESTA_EXTRA_DATA)%'
household_based_allowance: '%env(HOUSEHOLD_BASED_ALLOWANCE)%'
ssa_friendly_flux_type_names: '%env(SSA_FRIENDLY_FLUX_TYPE_NAMES)%'
# PARAMETRES DES IMPORTS POSSIBLE POUR L'APPLICATION DE GESTION DE MONNAIE LOCALE COMPLEMENTAIRE
......@@ -129,6 +130,10 @@ services:
public: true
arguments: ['@app.utils.custom_entity_manager', '@security.helper', '@app.utils.operations']
app.utils.payment:
class: App\Utils\PaymentUtils
autowire: true
app.twig.main.extension:
class: App\Twig\AppExtension
autowire: false
......
......@@ -61,6 +61,20 @@ class CaptureAction implements ActionInterface, GatewayAwareInterface, GenericTo
);
$model['vads_url_check'] = $notifyToken->getTargetUrl();
}
// Set recurrent payment data if needed
$payment = $request->getFirstModel();
if (true == $payment->getIsRecurrent()) {
$model['vads_page_action'] = 'REGISTER_PAY_SUBSCRIBE';
$model['vads_sub_amount'] = strval($payment->getRecurrenceAmount()); // 1000 for 10.00 EUR
$model['vads_sub_currency'] = $model['vads_currency'];
$model['vads_sub_effect_date'] = (new \DateTime('tomorrow', new \DateTimeZone('UTC')))->format('Ymd'); // tomorrow, to avoid duplicate payment this day
//FOR TEST : $model['vads_sub_desc'] = 'RRULE:FREQ=DAILY;INTERVAL=1;COUNT=2';
$count = $payment->getRecurrenceMonthsCount() - 1; //initial payment is not considered by payzen as the first occurence
$monthDay = $payment->getRecurrenceMonthDay();
$model['vads_sub_desc'] = "RRULE:FREQ=MONTHLY;COUNT={$count};BYMONTHDAY={$monthDay}";
}
}
if (false == $model['vads_trans_id']) {
......
......@@ -126,7 +126,6 @@ class Api
$data = $this
->getRequestOptionsResolver()
->resolve(array_replace($data, [
'vads_page_action' => 'PAYMENT',
'vads_version' => 'V2',
]));
......@@ -353,6 +352,10 @@ class Api
'vads_url_return' => null, // Obligatoire si acquisition de la carte par commerçant
'vads_user_info' => null,
'vads_version' => 'V2',
'vads_sub_amount' => null,
'vads_sub_currency' => null,
'vads_sub_effect_date' => null,
'vads_sub_desc' => null,
])
->setRequired([
'vads_amount',
......@@ -365,7 +368,7 @@ class Api
->setAllowedValues('vads_action_mode', ['SILENT', 'INTERACTIVE'])
->setAllowedValues('vads_currency', $this->getCurrencyCodes())
->setAllowedValues('vads_language', $this->getLanguageCodes())
->setAllowedValues('vads_page_action', 'PAYMENT')
->setAllowedValues('vads_page_action', ['PAYMENT', 'REGISTER_PAY_SUBSCRIBE'])
->setAllowedValues('vads_payment_cards', $this->getCardsCodes())
->setAllowedValues('vads_payment_config', function ($value) {
if ($value === 'SINGLE') {
......
......@@ -13,6 +13,7 @@
<server name="SYMFONY_PHPUNIT_REMOVE" value=""/>
<server name="SYMFONY_PHPUNIT_VERSION" value="7.5"/>
<server name="SYMFONY_DEPRECATIONS_HELPER" value="disabled" />
<env name="KERNEL_CLASS" value="App\Kernel"/>
</php>
<testsuites>
<testsuite name="Project Test Suite">
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -3,10 +3,10 @@
"app": {
"js": [
"/build/runtime.6ad5c9da.js",
"/build/app.27091a64.js"
"/build/app.3644f7b2.js"
],
"css": [
"/build/app.5cdc32e6.css"
"/build/app.8a3f698b.css"
]
},
"admin": {
......
{
"build/app.css": "/build/app.5cdc32e6.css",
"build/app.js": "/build/app.27091a64.js",
"build/app.css": "/build/app.8a3f698b.css",
"build/app.js": "/build/app.3644f7b2.js",
"build/admin.css": "/build/admin.4de55830.css",
"build/admin.js": "/build/admin.86a2d986.js",
"build/runtime.js": "/build/runtime.6ad5c9da.js",
......
(()=>{"use strict";var e={913:()=>{try{self["workbox:core:6.5.1"]&&_()}catch(e){}},977:()=>{try{self["workbox:precaching:6.5.1"]&&_()}catch(e){}},80:()=>{try{self["workbox:routing:6.5.1"]&&_()}catch(e){}},873:()=>{try{self["workbox:strategies:6.5.1"]&&_()}catch(e){}}},t={};function s(a){var n=t[a];if(void 0!==n)return n.exports;var r=t[a]={exports:{}};return e[a](r,r.exports,s),r.exports}(()=>{s(913);const e=(e,...t)=>{let s=e;return t.length>0&&(s+=` :: ${JSON.stringify(t)}`),s};class t extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}const a={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},n=e=>[a.prefix,e,a.suffix].filter((e=>e&&e.length>0)).join("-"),r=e=>e||n(a.precache),i=e=>e||n(a.runtime);function c(e,t){const s=t();return e.waitUntil(s),s}s(977);function o(e){if(!e)throw new t("add-to-cache-list-unexpected-type",{entry:e});if("string"==typeof e){const t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}const{revision:s,url:a}=e;if(!a)throw new t("add-to-cache-list-unexpected-type",{entry:e});if(!s){const e=new URL(a,location.href);return{cacheKey:e.href,url:e.href}}const n=new URL(a,location.href),r=new URL(a,location.href);return n.searchParams.set("__WB_REVISION__",s),{cacheKey:n.href,url:r.href}}class h{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:s})=>{if("install"===e.type&&t&&t.originalRequest&&t.originalRequest instanceof Request){const e=t.originalRequest.url;s?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return s}}}class l{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{const s=(null==t?void 0:t.cacheKey)||this._precacheController.getCacheKeyForURL(e.url);return s?new Request(s,{headers:e.headers}):e},this._precacheController=e}}let u;async function f(e,s){let a=null;if(e.url){a=new URL(e.url).origin}if(a!==self.location.origin)throw new t("cross-origin-copy-response",{origin:a});const n=e.clone(),r={headers:new Headers(n.headers),status:n.status,statusText:n.statusText},i=s?s(r):r,c=function(){if(void 0===u){const e=new Response("");if("body"in e)try{new Response(e.body),u=!0}catch(e){u=!1}u=!1}return u}()?n.body:await n.blob();return new Response(c,i)}function d(e,t){const s=new URL(e);for(const e of t)s.searchParams.delete(e);return s.href}class p{constructor(){this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t}))}}const g=new Set;s(873);function y(e){return"string"==typeof e?new Request(e):e}class w{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new p,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(const e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){const{event:s}=this;let a=y(e);if("navigate"===a.mode&&s instanceof FetchEvent&&s.preloadResponse){const e=await s.preloadResponse;if(e)return e}const n=this.hasCallback("fetchDidFail")?a.clone():null;try{for(const e of this.iterateCallbacks("requestWillFetch"))a=await e({request:a.clone(),event:s})}catch(e){if(e instanceof Error)throw new t("plugin-error-request-will-fetch",{thrownErrorMessage:e.message})}const r=a.clone();try{let e;e=await fetch(a,"navigate"===a.mode?void 0:this._strategy.fetchOptions);for(const t of this.iterateCallbacks("fetchDidSucceed"))e=await t({event:s,request:r,response:e});return e}catch(e){throw n&&await this.runCallbacks("fetchDidFail",{error:e,event:s,originalRequest:n.clone(),request:r.clone()}),e}}async fetchAndCachePut(e){const t=await this.fetch(e),s=t.clone();return this.waitUntil(this.cachePut(e,s)),t}async cacheMatch(e){const t=y(e);let s;const{cacheName:a,matchOptions:n}=this._strategy,r=await this.getCacheKey(t,"read"),i=Object.assign(Object.assign({},n),{cacheName:a});s=await caches.match(r,i);for(const e of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await e({cacheName:a,matchOptions:n,cachedResponse:s,request:r,event:this.event})||void 0;return s}async cachePut(e,s){const a=y(e);var n;await(n=0,new Promise((e=>setTimeout(e,n))));const r=await this.getCacheKey(a,"write");if(!s)throw new t("cache-put-with-no-response",{url:(i=r.url,new URL(String(i),location.href).href.replace(new RegExp(`^${location.origin}`),""))});var i;const c=await this._ensureResponseSafeToCache(s);if(!c)return!1;const{cacheName:o,matchOptions:h}=this._strategy,l=await self.caches.open(o),u=this.hasCallback("cacheDidUpdate"),f=u?await async function(e,t,s,a){const n=d(t.url,s);if(t.url===n)return e.match(t,a);const r=Object.assign(Object.assign({},a),{ignoreSearch:!0}),i=await e.keys(t,r);for(const t of i)if(n===d(t.url,s))return e.match(t,a)}(l,r.clone(),["__WB_REVISION__"],h):null;try{await l.put(r,u?c.clone():c)}catch(e){if(e instanceof Error)throw"QuotaExceededError"===e.name&&await async function(){for(const e of g)await e()}(),e}for(const e of this.iterateCallbacks("cacheDidUpdate"))await e({cacheName:o,oldResponse:f,newResponse:c.clone(),request:r,event:this.event});return!0}async getCacheKey(e,t){const s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let a=e;for(const e of this.iterateCallbacks("cacheKeyWillBeUsed"))a=y(await e({mode:t,request:a,event:this.event,params:this.params}));this._cacheKeys[s]=a}return this._cacheKeys[s]}hasCallback(e){for(const t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(const s of this.iterateCallbacks(e))await s(t)}*iterateCallbacks(e){for(const t of this._strategy.plugins)if("function"==typeof t[e]){const s=this._pluginStateMap.get(t),a=a=>{const n=Object.assign(Object.assign({},a),{state:s});return t[e](n)};yield a}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){let e;for(;e=this._extendLifetimePromises.shift();)await e}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,s=!1;for(const e of this.iterateCallbacks("cacheWillUpdate"))if(t=await e({request:this.request,response:t,event:this.event})||void 0,s=!0,!t)break;return s||t&&200!==t.status&&(t=void 0),t}}class m extends class{constructor(e={}){this.cacheName=i(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){const[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});const t=e.event,s="string"==typeof e.request?new Request(e.request):e.request,a="params"in e?e.params:void 0,n=new w(this,{event:t,request:s,params:a}),r=this._getResponse(n,s,t);return[r,this._awaitComplete(r,n,s,t)]}async _getResponse(e,s,a){let n;await e.runCallbacks("handlerWillStart",{event:a,request:s});try{if(n=await this._handle(s,e),!n||"error"===n.type)throw new t("no-response",{url:s.url})}catch(t){if(t instanceof Error)for(const r of e.iterateCallbacks("handlerDidError"))if(n=await r({error:t,event:a,request:s}),n)break;if(!n)throw t}for(const t of e.iterateCallbacks("handlerWillRespond"))n=await t({event:a,request:s,response:n});return n}async _awaitComplete(e,t,s,a){let n,r;try{n=await e}catch(r){}try{await t.runCallbacks("handlerDidRespond",{event:a,request:s,response:n}),await t.doneWaiting()}catch(e){e instanceof Error&&(r=e)}if(await t.runCallbacks("handlerDidComplete",{event:a,request:s,response:n,error:r}),t.destroy(),r)throw r}}{constructor(e={}){e.cacheName=r(e.cacheName),super(e),this._fallbackToNetwork=!1!==e.fallbackToNetwork,this.plugins.push(m.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){const s=await t.cacheMatch(e);return s||(t.event&&"install"===t.event.type?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,s){let a;const n=s.params||{};if(!this._fallbackToNetwork)throw new t("missing-precache-entry",{cacheName:this.cacheName,url:e.url});{0;const t=n.integrity,r=e.integrity,i=!r||r===t;if(a=await s.fetch(new Request(e,{integrity:r||t})),t&&i){this._useDefaultCacheabilityPluginIfNeeded();await s.cachePut(e,a.clone());0}}return a}async _handleInstall(e,s){this._useDefaultCacheabilityPluginIfNeeded();const a=await s.fetch(e);if(!await s.cachePut(e,a.clone()))throw new t("bad-precaching-response",{url:e.url,status:a.status});return a}_useDefaultCacheabilityPluginIfNeeded(){let e=null,t=0;for(const[s,a]of this.plugins.entries())a!==m.copyRedirectedCacheableResponsesPlugin&&(a===m.defaultPrecacheCacheabilityPlugin&&(e=s),a.cacheWillUpdate&&t++);0===t?this.plugins.push(m.defaultPrecacheCacheabilityPlugin):t>1&&null!==e&&this.plugins.splice(e,1)}}m.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:e})=>!e||e.status>=400?null:e},m.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:e})=>e.redirected?await f(e):e};class _{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:s=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new m({cacheName:r(e),plugins:[...t,new l({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this._installAndActiveListenersAdded=!0)}addToCacheList(e){const s=[];for(const a of e){"string"==typeof a?s.push(a):a&&void 0===a.revision&&s.push(a.url);const{cacheKey:e,url:n}=o(a),r="string"!=typeof a&&a.revision?"reload":"default";if(this._urlsToCacheKeys.has(n)&&this._urlsToCacheKeys.get(n)!==e)throw new t("add-to-cache-list-conflicting-entries",{firstEntry:this._urlsToCacheKeys.get(n),secondEntry:e});if("string"!=typeof a&&a.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==a.integrity)throw new t("add-to-cache-list-conflicting-integrities",{url:n});this._cacheKeysToIntegrities.set(e,a.integrity)}if(this._urlsToCacheKeys.set(n,e),this._urlsToCacheModes.set(n,r),s.length>0){const e=`Workbox is precaching URLs without revision info: ${s.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return c(e,(async()=>{const t=new h;this.strategy.plugins.push(t);for(const[t,s]of this._urlsToCacheKeys){const a=this._cacheKeysToIntegrities.get(s),n=this._urlsToCacheModes.get(t),r=new Request(t,{integrity:a,cache:n,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:s},request:r,event:e}))}const{updatedURLs:s,notUpdatedURLs:a}=t;return{updatedURLs:s,notUpdatedURLs:a}}))}activate(e){return c(e,(async()=>{const e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),s=new Set(this._urlsToCacheKeys.values()),a=[];for(const n of t)s.has(n.url)||(await e.delete(n),a.push(n.url));return{deletedURLs:a}}))}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s){return(await self.caches.open(this.strategy.cacheName)).match(s)}}createHandlerBoundToURL(e){const s=this.getCacheKeyForURL(e);if(!s)throw new t("non-precached-url",{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:s},t.params),this.strategy.handle(t))}}let R;const v=()=>(R||(R=new _),R);s(80);const C=e=>e&&"object"==typeof e?e:{handle:e};class b{constructor(e,t,s="GET"){this.handler=C(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=C(e)}}class q extends b{constructor(e,t,s){super((({url:t})=>{const s=e.exec(t.href);if(s&&(t.origin===location.origin||0===s.index))return s.slice(1)}),t,s)}}class U{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&"CACHE_URLS"===e.data.type){const{payload:t}=e.data;0;const s=Promise.all(t.urlsToCache.map((t=>{"string"==typeof t&&(t=[t]);const s=new Request(...t);return this.handleRequest({request:s,event:e})})));e.waitUntil(s),e.ports&&e.ports[0]&&s.then((()=>e.ports[0].postMessage(!0)))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return void 0;const a=s.origin===location.origin,{params:n,route:r}=this.findMatchingRoute({event:t,request:e,sameOrigin:a,url:s});let i=r&&r.handler;const c=e.method;if(!i&&this._defaultHandlerMap.has(c)&&(i=this._defaultHandlerMap.get(c)),!i)return void 0;let o;try{o=i.handle({url:s,request:e,event:t,params:n})}catch(e){o=Promise.reject(e)}const h=r&&r.catchHandler;return o instanceof Promise&&(this._catchHandler||h)&&(o=o.catch((async a=>{if(h){0;try{return await h.handle({url:s,request:e,event:t,params:n})}catch(e){e instanceof Error&&(a=e)}}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw a}))),o}findMatchingRoute({url:e,sameOrigin:t,request:s,event:a}){const n=this._routes.get(s.method)||[];for(const r of n){let n;const i=r.match({url:e,sameOrigin:t,request:s,event:a});if(i)return n=i,(Array.isArray(n)&&0===n.length||i.constructor===Object&&0===Object.keys(i).length||"boolean"==typeof i)&&(n=void 0),{route:r,params:n}}return{}}setDefaultHandler(e,t="GET"){this._defaultHandlerMap.set(t,C(e))}setCatchHandler(e){this._catchHandler=C(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t("unregister-route-but-not-found-with-method",{method:e.method});const s=this._routes.get(e.method).indexOf(e);if(!(s>-1))throw new t("unregister-route-route-not-registered");this._routes.get(e.method).splice(s,1)}}let L;class k extends b{constructor(e,t){super((({request:s})=>{const a=e.getURLsToCacheKeys();for(const n of function*(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:a=!0,urlManipulation:n}={}){const r=new URL(e,location.href);r.hash="",yield r.href;const i=function(e,t=[]){for(const s of[...e.searchParams.keys()])t.some((e=>e.test(s)))&&e.searchParams.delete(s);return e}(r,t);if(yield i.href,s&&i.pathname.endsWith("/")){const e=new URL(i.href);e.pathname+=s,yield e.href}if(a){const e=new URL(i.href);e.pathname+=".html",yield e.href}if(n){const e=n({url:r});for(const t of e)yield t.href}}(s.url,t)){const t=a.get(n);if(t){return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}}}),e.strategy)}}function K(e){const s=v();!function(e,s,a){let n;if("string"==typeof e){const t=new URL(e,location.href);n=new b((({url:e})=>e.href===t.href),s,a)}else if(e instanceof RegExp)n=new q(e,s,a);else if("function"==typeof e)n=new b(e,s,a);else{if(!(e instanceof b))throw new t("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});n=e}(L||(L=new U,L.addFetchListener(),L.addCacheListener()),L).registerRoute(n)}(new k(s,e))}var T;(function(e){v().precache(e)})([{'revision':null,'url':'/build/admin.4de55830.css'},{'revision':null,'url':'/build/admin.86a2d986.js'},{'revision':'ad79405d542b397996a3079203114b42','url':'/build/admin.86a2d986.js.LICENSE.txt'},{'revision':null,'url':'/build/app.27091a64.js'},{'revision':'4ff7361e3c3e359ae88c5fae5738746d','url':'/build/app.27091a64.js.LICENSE.txt'},{'revision':null,'url':'/build/app.5cdc32e6.css'},{'revision':null,'url':'/build/fonts/fa-brands-400.3dc44d22.woff2'},{'revision':null,'url':'/build/fonts/fa-regular-400.3dc6ca01.woff2'},{'revision':null,'url':'/build/fonts/fa-solid-900.496d5fc1.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-300.d2c7d5c5.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-600.17c0392c.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-600italic.cc34c6e7.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-700.ed37bc60.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-900.476756cd.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-italic.a07cb9c5.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-regular.f74389bd.woff2'},{'revision':null,'url':'/build/images/fa-brands-400.05d20183.svg'},{'revision':null,'url':'/build/images/fa-regular-400.9a0810d6.svg'},{'revision':null,'url':'/build/images/fa-solid-900.a838c42a.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-300.4e7fe004.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-600.cf2758ae.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-600italic.7249d863.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-700.3e4b9e19.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-900.060d8c51.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-italic.08dc9b1c.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-regular.3bb9538c.svg'},{'revision':null,'url':'/build/runtime.6ad5c9da.js'}]),K(T),self.addEventListener("fetch",(function(e){e.respondWith(caches.match(e.request).then((function(t){return t||fetch(e.request)})).catch((function(){return caches.match("/offline.html")})))}))})()})();
\ No newline at end of file
(()=>{"use strict";var e={913:()=>{try{self["workbox:core:6.5.1"]&&_()}catch(e){}},977:()=>{try{self["workbox:precaching:6.5.1"]&&_()}catch(e){}},80:()=>{try{self["workbox:routing:6.5.1"]&&_()}catch(e){}},873:()=>{try{self["workbox:strategies:6.5.1"]&&_()}catch(e){}}},t={};function s(a){var n=t[a];if(void 0!==n)return n.exports;var r=t[a]={exports:{}};return e[a](r,r.exports,s),r.exports}(()=>{s(913);const e=(e,...t)=>{let s=e;return t.length>0&&(s+=` :: ${JSON.stringify(t)}`),s};class t extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}const a={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},n=e=>[a.prefix,e,a.suffix].filter((e=>e&&e.length>0)).join("-"),r=e=>e||n(a.precache),i=e=>e||n(a.runtime);function c(e,t){const s=t();return e.waitUntil(s),s}s(977);function o(e){if(!e)throw new t("add-to-cache-list-unexpected-type",{entry:e});if("string"==typeof e){const t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}const{revision:s,url:a}=e;if(!a)throw new t("add-to-cache-list-unexpected-type",{entry:e});if(!s){const e=new URL(a,location.href);return{cacheKey:e.href,url:e.href}}const n=new URL(a,location.href),r=new URL(a,location.href);return n.searchParams.set("__WB_REVISION__",s),{cacheKey:n.href,url:r.href}}class h{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:s})=>{if("install"===e.type&&t&&t.originalRequest&&t.originalRequest instanceof Request){const e=t.originalRequest.url;s?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return s}}}class l{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{const s=(null==t?void 0:t.cacheKey)||this._precacheController.getCacheKeyForURL(e.url);return s?new Request(s,{headers:e.headers}):e},this._precacheController=e}}let u;async function f(e,s){let a=null;if(e.url){a=new URL(e.url).origin}if(a!==self.location.origin)throw new t("cross-origin-copy-response",{origin:a});const n=e.clone(),r={headers:new Headers(n.headers),status:n.status,statusText:n.statusText},i=s?s(r):r,c=function(){if(void 0===u){const e=new Response("");if("body"in e)try{new Response(e.body),u=!0}catch(e){u=!1}u=!1}return u}()?n.body:await n.blob();return new Response(c,i)}function d(e,t){const s=new URL(e);for(const e of t)s.searchParams.delete(e);return s.href}class p{constructor(){this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t}))}}const g=new Set;s(873);function y(e){return"string"==typeof e?new Request(e):e}class w{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new p,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(const e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){const{event:s}=this;let a=y(e);if("navigate"===a.mode&&s instanceof FetchEvent&&s.preloadResponse){const e=await s.preloadResponse;if(e)return e}const n=this.hasCallback("fetchDidFail")?a.clone():null;try{for(const e of this.iterateCallbacks("requestWillFetch"))a=await e({request:a.clone(),event:s})}catch(e){if(e instanceof Error)throw new t("plugin-error-request-will-fetch",{thrownErrorMessage:e.message})}const r=a.clone();try{let e;e=await fetch(a,"navigate"===a.mode?void 0:this._strategy.fetchOptions);for(const t of this.iterateCallbacks("fetchDidSucceed"))e=await t({event:s,request:r,response:e});return e}catch(e){throw n&&await this.runCallbacks("fetchDidFail",{error:e,event:s,originalRequest:n.clone(),request:r.clone()}),e}}async fetchAndCachePut(e){const t=await this.fetch(e),s=t.clone();return this.waitUntil(this.cachePut(e,s)),t}async cacheMatch(e){const t=y(e);let s;const{cacheName:a,matchOptions:n}=this._strategy,r=await this.getCacheKey(t,"read"),i=Object.assign(Object.assign({},n),{cacheName:a});s=await caches.match(r,i);for(const e of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await e({cacheName:a,matchOptions:n,cachedResponse:s,request:r,event:this.event})||void 0;return s}async cachePut(e,s){const a=y(e);var n;await(n=0,new Promise((e=>setTimeout(e,n))));const r=await this.getCacheKey(a,"write");if(!s)throw new t("cache-put-with-no-response",{url:(i=r.url,new URL(String(i),location.href).href.replace(new RegExp(`^${location.origin}`),""))});var i;const c=await this._ensureResponseSafeToCache(s);if(!c)return!1;const{cacheName:o,matchOptions:h}=this._strategy,l=await self.caches.open(o),u=this.hasCallback("cacheDidUpdate"),f=u?await async function(e,t,s,a){const n=d(t.url,s);if(t.url===n)return e.match(t,a);const r=Object.assign(Object.assign({},a),{ignoreSearch:!0}),i=await e.keys(t,r);for(const t of i)if(n===d(t.url,s))return e.match(t,a)}(l,r.clone(),["__WB_REVISION__"],h):null;try{await l.put(r,u?c.clone():c)}catch(e){if(e instanceof Error)throw"QuotaExceededError"===e.name&&await async function(){for(const e of g)await e()}(),e}for(const e of this.iterateCallbacks("cacheDidUpdate"))await e({cacheName:o,oldResponse:f,newResponse:c.clone(),request:r,event:this.event});return!0}async getCacheKey(e,t){const s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let a=e;for(const e of this.iterateCallbacks("cacheKeyWillBeUsed"))a=y(await e({mode:t,request:a,event:this.event,params:this.params}));this._cacheKeys[s]=a}return this._cacheKeys[s]}hasCallback(e){for(const t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(const s of this.iterateCallbacks(e))await s(t)}*iterateCallbacks(e){for(const t of this._strategy.plugins)if("function"==typeof t[e]){const s=this._pluginStateMap.get(t),a=a=>{const n=Object.assign(Object.assign({},a),{state:s});return t[e](n)};yield a}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){let e;for(;e=this._extendLifetimePromises.shift();)await e}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,s=!1;for(const e of this.iterateCallbacks("cacheWillUpdate"))if(t=await e({request:this.request,response:t,event:this.event})||void 0,s=!0,!t)break;return s||t&&200!==t.status&&(t=void 0),t}}class m extends class{constructor(e={}){this.cacheName=i(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){const[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});const t=e.event,s="string"==typeof e.request?new Request(e.request):e.request,a="params"in e?e.params:void 0,n=new w(this,{event:t,request:s,params:a}),r=this._getResponse(n,s,t);return[r,this._awaitComplete(r,n,s,t)]}async _getResponse(e,s,a){let n;await e.runCallbacks("handlerWillStart",{event:a,request:s});try{if(n=await this._handle(s,e),!n||"error"===n.type)throw new t("no-response",{url:s.url})}catch(t){if(t instanceof Error)for(const r of e.iterateCallbacks("handlerDidError"))if(n=await r({error:t,event:a,request:s}),n)break;if(!n)throw t}for(const t of e.iterateCallbacks("handlerWillRespond"))n=await t({event:a,request:s,response:n});return n}async _awaitComplete(e,t,s,a){let n,r;try{n=await e}catch(r){}try{await t.runCallbacks("handlerDidRespond",{event:a,request:s,response:n}),await t.doneWaiting()}catch(e){e instanceof Error&&(r=e)}if(await t.runCallbacks("handlerDidComplete",{event:a,request:s,response:n,error:r}),t.destroy(),r)throw r}}{constructor(e={}){e.cacheName=r(e.cacheName),super(e),this._fallbackToNetwork=!1!==e.fallbackToNetwork,this.plugins.push(m.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){const s=await t.cacheMatch(e);return s||(t.event&&"install"===t.event.type?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,s){let a;const n=s.params||{};if(!this._fallbackToNetwork)throw new t("missing-precache-entry",{cacheName:this.cacheName,url:e.url});{0;const t=n.integrity,r=e.integrity,i=!r||r===t;if(a=await s.fetch(new Request(e,{integrity:r||t})),t&&i){this._useDefaultCacheabilityPluginIfNeeded();await s.cachePut(e,a.clone());0}}return a}async _handleInstall(e,s){this._useDefaultCacheabilityPluginIfNeeded();const a=await s.fetch(e);if(!await s.cachePut(e,a.clone()))throw new t("bad-precaching-response",{url:e.url,status:a.status});return a}_useDefaultCacheabilityPluginIfNeeded(){let e=null,t=0;for(const[s,a]of this.plugins.entries())a!==m.copyRedirectedCacheableResponsesPlugin&&(a===m.defaultPrecacheCacheabilityPlugin&&(e=s),a.cacheWillUpdate&&t++);0===t?this.plugins.push(m.defaultPrecacheCacheabilityPlugin):t>1&&null!==e&&this.plugins.splice(e,1)}}m.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:e})=>!e||e.status>=400?null:e},m.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:e})=>e.redirected?await f(e):e};class _{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:s=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new m({cacheName:r(e),plugins:[...t,new l({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this._installAndActiveListenersAdded=!0)}addToCacheList(e){const s=[];for(const a of e){"string"==typeof a?s.push(a):a&&void 0===a.revision&&s.push(a.url);const{cacheKey:e,url:n}=o(a),r="string"!=typeof a&&a.revision?"reload":"default";if(this._urlsToCacheKeys.has(n)&&this._urlsToCacheKeys.get(n)!==e)throw new t("add-to-cache-list-conflicting-entries",{firstEntry:this._urlsToCacheKeys.get(n),secondEntry:e});if("string"!=typeof a&&a.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==a.integrity)throw new t("add-to-cache-list-conflicting-integrities",{url:n});this._cacheKeysToIntegrities.set(e,a.integrity)}if(this._urlsToCacheKeys.set(n,e),this._urlsToCacheModes.set(n,r),s.length>0){const e=`Workbox is precaching URLs without revision info: ${s.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return c(e,(async()=>{const t=new h;this.strategy.plugins.push(t);for(const[t,s]of this._urlsToCacheKeys){const a=this._cacheKeysToIntegrities.get(s),n=this._urlsToCacheModes.get(t),r=new Request(t,{integrity:a,cache:n,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:s},request:r,event:e}))}const{updatedURLs:s,notUpdatedURLs:a}=t;return{updatedURLs:s,notUpdatedURLs:a}}))}activate(e){return c(e,(async()=>{const e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),s=new Set(this._urlsToCacheKeys.values()),a=[];for(const n of t)s.has(n.url)||(await e.delete(n),a.push(n.url));return{deletedURLs:a}}))}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s){return(await self.caches.open(this.strategy.cacheName)).match(s)}}createHandlerBoundToURL(e){const s=this.getCacheKeyForURL(e);if(!s)throw new t("non-precached-url",{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:s},t.params),this.strategy.handle(t))}}let R;const v=()=>(R||(R=new _),R);s(80);const C=e=>e&&"object"==typeof e?e:{handle:e};class b{constructor(e,t,s="GET"){this.handler=C(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=C(e)}}class q extends b{constructor(e,t,s){super((({url:t})=>{const s=e.exec(t.href);if(s&&(t.origin===location.origin||0===s.index))return s.slice(1)}),t,s)}}class U{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&"CACHE_URLS"===e.data.type){const{payload:t}=e.data;0;const s=Promise.all(t.urlsToCache.map((t=>{"string"==typeof t&&(t=[t]);const s=new Request(...t);return this.handleRequest({request:s,event:e})})));e.waitUntil(s),e.ports&&e.ports[0]&&s.then((()=>e.ports[0].postMessage(!0)))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return void 0;const a=s.origin===location.origin,{params:n,route:r}=this.findMatchingRoute({event:t,request:e,sameOrigin:a,url:s});let i=r&&r.handler;const c=e.method;if(!i&&this._defaultHandlerMap.has(c)&&(i=this._defaultHandlerMap.get(c)),!i)return void 0;let o;try{o=i.handle({url:s,request:e,event:t,params:n})}catch(e){o=Promise.reject(e)}const h=r&&r.catchHandler;return o instanceof Promise&&(this._catchHandler||h)&&(o=o.catch((async a=>{if(h){0;try{return await h.handle({url:s,request:e,event:t,params:n})}catch(e){e instanceof Error&&(a=e)}}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw a}))),o}findMatchingRoute({url:e,sameOrigin:t,request:s,event:a}){const n=this._routes.get(s.method)||[];for(const r of n){let n;const i=r.match({url:e,sameOrigin:t,request:s,event:a});if(i)return n=i,(Array.isArray(n)&&0===n.length||i.constructor===Object&&0===Object.keys(i).length||"boolean"==typeof i)&&(n=void 0),{route:r,params:n}}return{}}setDefaultHandler(e,t="GET"){this._defaultHandlerMap.set(t,C(e))}setCatchHandler(e){this._catchHandler=C(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t("unregister-route-but-not-found-with-method",{method:e.method});const s=this._routes.get(e.method).indexOf(e);if(!(s>-1))throw new t("unregister-route-route-not-registered");this._routes.get(e.method).splice(s,1)}}let L;class k extends b{constructor(e,t){super((({request:s})=>{const a=e.getURLsToCacheKeys();for(const n of function*(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:a=!0,urlManipulation:n}={}){const r=new URL(e,location.href);r.hash="",yield r.href;const i=function(e,t=[]){for(const s of[...e.searchParams.keys()])t.some((e=>e.test(s)))&&e.searchParams.delete(s);return e}(r,t);if(yield i.href,s&&i.pathname.endsWith("/")){const e=new URL(i.href);e.pathname+=s,yield e.href}if(a){const e=new URL(i.href);e.pathname+=".html",yield e.href}if(n){const e=n({url:r});for(const t of e)yield t.href}}(s.url,t)){const t=a.get(n);if(t){return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}}}),e.strategy)}}function K(e){const s=v();!function(e,s,a){let n;if("string"==typeof e){const t=new URL(e,location.href);n=new b((({url:e})=>e.href===t.href),s,a)}else if(e instanceof RegExp)n=new q(e,s,a);else if("function"==typeof e)n=new b(e,s,a);else{if(!(e instanceof b))throw new t("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});n=e}(L||(L=new U,L.addFetchListener(),L.addCacheListener()),L).registerRoute(n)}(new k(s,e))}var T;(function(e){v().precache(e)})([{'revision':null,'url':'/build/admin.4de55830.css'},{'revision':null,'url':'/build/admin.86a2d986.js'},{'revision':'ad79405d542b397996a3079203114b42','url':'/build/admin.86a2d986.js.LICENSE.txt'},{'revision':null,'url':'/build/app.3644f7b2.js'},{'revision':'4ff7361e3c3e359ae88c5fae5738746d','url':'/build/app.3644f7b2.js.LICENSE.txt'},{'revision':null,'url':'/build/app.8a3f698b.css'},{'revision':null,'url':'/build/fonts/fa-brands-400.3dc44d22.woff2'},{'revision':null,'url':'/build/fonts/fa-regular-400.3dc6ca01.woff2'},{'revision':null,'url':'/build/fonts/fa-solid-900.496d5fc1.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-300.d2c7d5c5.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-600.17c0392c.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-600italic.cc34c6e7.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-700.ed37bc60.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-900.476756cd.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-italic.a07cb9c5.woff2'},{'revision':null,'url':'/build/fonts/source-sans-pro-v14-latin-regular.f74389bd.woff2'},{'revision':null,'url':'/build/images/fa-brands-400.05d20183.svg'},{'revision':null,'url':'/build/images/fa-regular-400.9a0810d6.svg'},{'revision':null,'url':'/build/images/fa-solid-900.a838c42a.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-300.4e7fe004.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-600.cf2758ae.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-600italic.7249d863.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-700.3e4b9e19.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-900.060d8c51.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-italic.08dc9b1c.svg'},{'revision':null,'url':'/build/images/source-sans-pro-v14-latin-regular.3bb9538c.svg'},{'revision':null,'url':'/build/runtime.6ad5c9da.js'}]),K(T),self.addEventListener("fetch",(function(e){e.respondWith(caches.match(e.request).then((function(t){return t||fetch(e.request)})).catch((function(){return caches.match("/offline.html")})))}))})()})();
\ No newline at end of file
......@@ -27,9 +27,17 @@ class AchatMonnaieAdmin extends FluxAdmin
public function createQuery($context = 'list')
{
$query = parent::createQuery($context);
//In mode ssa_friendly_flux_type_names, we display on this page all cotisation
//initial operation (i.e. we display vente_emlc operations in addition to achat_monnaie)
if($this->getConfigurationPool()->getContainer()->getParameter('tav_env')
&& $this->getConfigurationPool()->getContainer()->getParameter('ssa_friendly_flux_type_names')) {
$query->andWhere($query->getRootAliases()[0] . '.parenttype = :parenttype' . ' OR ' . $query->getRootAliases()[0] . '.parenttype = :parenttype2')
->setParameter('parenttype', Flux::TYPE_ACHAT)
->setParameter('parenttype2', Flux::TYPE_VENTE_EMLC);
} else {
$query->andWhere($query->getRootAliases()[0] . '.parenttype = :parenttype')
->setParameter('parenttype', Flux::TYPE_ACHAT);
}
return $query;
}
......@@ -43,6 +51,12 @@ class AchatMonnaieAdmin extends FluxAdmin
*/
protected function configureDatagridFilters(DatagridMapper $datagridMapper): void
{
//In mode ssa_friendly_flux_type_names, we display on this page all cotisation
//initial operation (i.e. we display vente_emlc operations in addition to achat_monnaie)
//therefore filtering by type achat_monnaie_adherent VS achat_monnaie_prestataire
//would be confusing (selecting one of them would hide all vente_emlc operations)
if(!$this->getConfigurationPool()->getContainer()->getParameter('tav_env')
|| !$this->getConfigurationPool()->getContainer()->getParameter('ssa_friendly_flux_type_names')) {
$datagridMapper->add('type', null, [
'advanced_filter' => false,
'show_filter' => true,
......@@ -55,6 +69,7 @@ class AchatMonnaieAdmin extends FluxAdmin
],
]);
}
}
/**
* {@inheritdoc}
......@@ -81,12 +96,24 @@ class AchatMonnaieAdmin extends FluxAdmin
$datagrid->buildPager();
$query = clone $datagrid->getQuery();
if($this->getConfigurationPool()->getContainer()->getParameter('tav_env')
&& $this->getConfigurationPool()->getContainer()->getParameter('ssa_friendly_flux_type_names')) {
$query
->select('SUM( ' . $query->getRootAlias() . '.montant) as total')
->andWhere($query->getRootAlias() . '.parenttype = :parenttype' . ' OR ' . $query->getRootAliases()[0] . '.parenttype = :parenttype2')
->setParameter('parenttype', Flux::TYPE_ACHAT)
->setParameter('parenttype2', Flux::TYPE_VENTE_EMLC)
->setFirstResult(null)
->setMaxResults(null);
} else {
$query
->select('SUM( ' . $query->getRootAlias() . '.montant) as total')
->andWhere($query->getRootAlias() . '.parenttype = :parenttype')
->setParameter('parenttype', Flux::TYPE_ACHAT)
->setFirstResult(null)
->setMaxResults(null);
}
$result = $query->execute([], \Doctrine\ORM\Query::HYDRATE_SINGLE_SCALAR);
......
......@@ -64,6 +64,8 @@ class FluxAdmin extends AbstractAdmin
protected function configureExportFields(): array
{
$ssaFriendlyTypeNames = $this->getConfigurationPool()->getContainer()->getParameter('tav_env')
&& $this->getConfigurationPool()->getContainer()->getParameter('ssa_friendly_flux_type_names');
return [
$this->trans('Date') => 'createdAt',
$this->trans('Expediteur') => 'expediteur',
......@@ -91,10 +93,12 @@ class FluxAdmin extends AbstractAdmin
*/
protected function configureListFields(ListMapper $listMapper)
{
$ssaFriendlyTypeNames = $this->getConfigurationPool()->getContainer()->getParameter('tav_env')
&& $this->getConfigurationPool()->getContainer()->getParameter('ssa_friendly_flux_type_names');
unset($this->listModes['mosaic']);
$listMapper
->add('createdAt', 'datetime', ['label' => 'Date'])
->add('type', 'text', ['label' => 'Type', 'template' => '@kohinos/bundles/SonataAdminBundle/Block/translated_value.html.twig'])
->add($ssaFriendlyTypeNames ? 'ssaFriendlyTypeName' : 'type', 'text', ['label' => 'Type', 'template' => '@kohinos/bundles/SonataAdminBundle/Block/translated_value.html.twig'])
->add('montant', 'decimal', ['label' => 'Montant', 'attributes' => ['fraction_digits' => 2]])
->add('expediteur', null, ['label' => 'Expediteur'])
->add('destinataire', null, ['label' => 'Destinataire'])
......@@ -110,6 +114,11 @@ class FluxAdmin extends AbstractAdmin
*/
protected function configureDatagridFilters(DatagridMapper $datagridMapper): void
{
$ssaFriendlyTypeNames = $this->getConfigurationPool()->getContainer()->getParameter('tav_env')
&& $this->getConfigurationPool()->getContainer()->getParameter('ssa_friendly_flux_type_names');
$choices = $ssaFriendlyTypeNames ? //in ssa mode, remove flux types that are irrelevant or confusing
['Transactions' => Flux::TYPE_TRANSACTION]
: ['Cotisations' => Flux::TYPE_COTISATION, 'Achat de monnaie' => Flux::TYPE_ACHAT, 'Dépôt de billets' => Flux::TYPE_CHANGE, 'Retrait' => Flux::TYPE_RETRAIT, 'Transactions' => Flux::TYPE_TRANSACTION, 'Transferts' => Flux::TYPE_TRANSFERT, 'Vente' => Flux::TYPE_VENTE];
$datagridMapper
->add('transfert_or_transaction', 'doctrine_orm_callback', [
'label' => 'Type',
......@@ -127,17 +136,32 @@ class FluxAdmin extends AbstractAdmin
'show_filter' => true,
'field_type' => SChoiceType::class,
'field_options' => [
'choices' => ['Cotisations' => Flux::TYPE_COTISATION, 'Achat de monnaie' => Flux::TYPE_ACHAT, 'Dépôt de billets' => Flux::TYPE_CHANGE, 'Retrait' => Flux::TYPE_RETRAIT, 'Transactions' => Flux::TYPE_TRANSACTION, 'Transferts' => Flux::TYPE_TRANSFERT, 'Vente' => Flux::TYPE_VENTE],
'choices' => $choices,
'placeholder' => 'Indifférent',
'expanded' => true,
'multiple' => false,
],
])
]);
if ($ssaFriendlyTypeNames) {
$datagridMapper
->add('type', null, [
'label' => 'Type plus précis',
'advanced_filter' => false,
'show_filter' => true,
])
'field_type' => SChoiceType::class,
'field_options' => [
'choices' => array_flip(Flux::ssaUsefulFriendlyTypeNames(true))
]
]);
} else {
$datagridMapper
->add('type', null, [
'label' => 'Type plus précis',
'advanced_filter' => false,
'show_filter' => true,
]);
}
$datagridMapper
->add('operateur', null, [
'label' => 'Operateur',
'advanced_filter' => false,
......@@ -182,9 +206,11 @@ class FluxAdmin extends AbstractAdmin
public function getExportFields()
{
$ssaFriendlyTypeNames = $this->getConfigurationPool()->getContainer()->getParameter('tav_env')
&& $this->getConfigurationPool()->getContainer()->getParameter('ssa_friendly_flux_type_names');
return [
'Date' => 'created_at',
'Type' => 'type',
'Type' => $ssaFriendlyTypeNames ? 'ssaFriendlyTypeName' : 'type',
'Montant' => 'montant',
'Expediteur' => 'expediteur',
'Destinataire' => 'destinataire',
......
......@@ -24,10 +24,14 @@ use Twig\Environment;
*/
class ReconversionMonaPrestatairesCommand extends Command
{
//Call with following crons :
//15 1 14,28 * * kohinos php /home/kohinos/kohinos/bin/console kohinos:ssa:reconversion-prestataires twice_a_month
//17 1 28 * * kohinos php /home/kohinos/kohinos/bin/console kohinos:ssa:reconversion-prestataires once_a_month
//19 1 28 1,3,5,7,9,11 * kohinos php /home/kohinos/kohinos/bin/console kohinos:ssa:reconversion-prestataires once_every_two_month
//Les reconversions doivent avoir lieu les dimanches soir qui précèdent les 14 et 28.
// On utilise les crons suivants :
//15 23 * * 0 [ "$(date +\%d)" -ge 08 -a "$(date +\%d)" -le 14 ] && kohinos php /home/kohinos/kohinos/bin/console kohinos:ssa:reconversion-prestataires twice_a_month
//15 23 * * 0 [ "$(date +\%d)" -ge 22 -a "$(date +\%d)" -le 28 ] && kohinos php /home/kohinos/kohinos/bin/console kohinos:ssa:reconversion-prestataires twice_a_month
//17 23 * * 0 [ "$(date +\%d)" -ge 22 -a "$(date +\%d)" -le 28 ] && kohinos php /home/kohinos/kohinos/bin/console kohinos:ssa:reconversion-prestataires once_a_month
//19 23 * 1,3,5,7,9,11 0 [ "$(date +\%d)" -ge 22 -a "$(date +\%d)" -le 28 ] && kohinos php /home/kohinos/kohinos/bin/console kohinos:ssa:reconversion-prestataires once_every_two_month
protected static $defaultName = 'kohinos:ssa:reconversion-prestataires';
......
......@@ -261,7 +261,7 @@ class FluxController extends AbstractController
//projectDir is composer.json
$dir = $this->getParameter('kernel.project_dir') . "/reconversions";
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
mkdir($dir, 0755, true);
}
$path = $dir . "/" . $filename;
......
......@@ -2,25 +2,26 @@
namespace App\Controller;
use App\Entity\GlobalParameter;
use App\Entity\Payment;
use App\Entity\User;
use App\Security\LoginAuthenticator;
use App\Utils\PaymentUtils;
use Doctrine\ORM\EntityManagerInterface;
use Payum\Core\Payum;
use Payum\Core\Request\GetHumanStatus;
use Payum\Core\Request\Notify;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Translation\TranslatorInterface;
use Payum\Core\Payum;
use Payum\Core\Request\Notify;
use Payum\Core\Request\GetHumanStatus;
use App\Entity\Flux;
use App\Entity\Payment;
use App\Entity\User;
use App\Entity\GlobalParameter;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use App\Security\LoginAuthenticator;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Translation\TranslatorInterface;
/**
* Gestion des paiements avec Payum
* Gestion des paiements avec Payum.
*/
class PaymentController extends AbstractController
{
......@@ -29,22 +30,25 @@ class PaymentController extends AbstractController
protected $payum;
protected $authenticator;
protected $guardHandler;
protected $paymentUtils;
public function __construct(EntityManagerInterface $em,
TranslatorInterface $translator,
LoginAuthenticator $authenticator,
GuardAuthenticatorHandler $guardHandler,
Payum $payum)
Payum $payum,
PaymentUtils $paymentUtils)
{
$this->em = $em;
$this->translator = $translator;
$this->payum = $payum;
$this->authenticator = $authenticator;
$this->guardHandler = $guardHandler;
$this->paymentUtils = $paymentUtils;
}
/**
* Crée une instance de Payment, les tokens associés, et redirige vers la page de paiement
* Crée une instance de Payment, les tokens associés, et redirige vers la page de paiement.
*/
public function preparePaymentAction(Form $form, $type, $extra_data = null)
{
......@@ -70,19 +74,20 @@ class PaymentController extends AbstractController
],
'expediteur' => ['id'],
'destinataire' => ['id'],
'operateur' => ['id']]
'operateur' => ['id'], ],
]);
$jsondata = $serializer->serialize($data, 'json');
// Prepare CB Payment
if ($this->em->getRepository(GlobalParameter::class)->val(GlobalParameter::USE_PAYZEN) === 'true') {
if ('true' === $this->em->getRepository(GlobalParameter::class)->val(GlobalParameter::USE_PAYZEN)) {
$gatewayName = 'payzen';
} else {
$this->addFlash(
'error',
$this->translator->trans('Une erreur est survenue due à la configuration du paiement dans l\'application. Il est pour l\'instant impossible de payer par CB, merci de contacter votre monnaie locale.')
);
return $this->redirectToRoute('index');
}
......@@ -99,8 +104,8 @@ class PaymentController extends AbstractController
$payment->setExtraData($extra_data);
}
if ($type == Payment::TYPE_ADHESION) {
$payment->setTotalAmount($form->get('cotisation')->get('montant')->getData()*100); // 1.23 EUR
if (Payment::TYPE_ADHESION == $type) {
$payment->setTotalAmount($form->get('cotisation')->get('montant')->getData() * 100); // 1.23 EUR
$payment->setClientId('Nouvel adhérent');
$payment->setClientEmail($form->get('user')->get('email')->getData());
} else {
......@@ -114,6 +119,13 @@ class PaymentController extends AbstractController
$payment->setClientEmail($this->getUser()->getEmail());
}
if (Payment::TYPE_PAIEMENT_RECURRENT_COTISATION_TAV == $type) {
$payment->setRecurrenceAmount($form->get('montant')->getData() * 100);
$payment->setIsRecurrent(true);
$payment->setRecurrenceMonthsCount($form->get('nombreMois')->getData());
$payment->setRecurrenceMonthDay($form->get('jourPrelevement')->getData());
}
$storage->update($payment);
$captureToken = $this->payum->getTokenFactory()->createCaptureToken(
......@@ -135,8 +147,112 @@ class PaymentController extends AbstractController
return $this->redirect($captureToken->getTargetUrl());
}
/* COMMENT LES FLUX SONT-ILS CREES SUITE A LA CREATION D'UN PAIEMENT PAYZEN ?
*
*
*
* CAS 1 : Paiement standard
* Deux voies de signalement coexistent :
*
* Voie 1 : via la notification instantannée payzen.
* Un paramètre appelé "URL de notification à la fin du paiement" est configuré dans le backoffice payzen et
* est renseigné à payement/notify. Cette valeur est utilisée par payzen, sauf si la variable vads_url_check
* est transmise, auquel cas c'est cette dernière valeur qui prime.
* Ici, vads_url_check est transmise à la valeur payment/notify/{token}.
* Remarque : son usage systématique n'est pas recommandé par Payzen.
* L'URL est ainsi captée par NotifyController.php.
*
* Voie 2 : via la redirection du client.
* Cette voie n'est pas garantie (si l'utilisateur ferme son navigateur assez tôt, il ne sera pas redirigé).
* A vérifier : l'URL de redirection est la combinaison entre une configuration côté backoffice et
* un complément que nous transmettons.
* Lorsque la redirection a bien lieu, c'est donc le contrôleur doneAction de PaymentController.php qui est sollicité,
* avec une requête au format payement/done/?payum_token={token}
*
* Remarquons dans ces deux stratégie la présence d'un token permettant l'authentification de l'agent Payzen et
* la récupération de l'objet paiement préalablement enregistré en base.
*
*
*
* CAS 2 : Paiement récurrent
*
* Dans le cadre de notre implémentation, un paiement récurrent commence par un paiement initial (occurence n°0).
* Pour cette occurence n°0, au niveau du mode de communication, on est sur un processus en apparence similaire au
* CAS 1. Un examen minutieux de ce process m'a cependant permis de mettre en évidence des comportements que
* je n'ai pas réussi à comprendre :
* En ouvrant les vannes pour tester le déclenchement d'une création de flux à chaque notification rattachée
* à un paiement récurrent, j'ai observé la génération de pas moins de 5 événements, opérant donc
* un accroissement de solde de +750 mona au lieu de +150 mona !!!
* - 2 événements via /payement/notify/{token}
* - 3 événements après l'execution de la ligne $gateway->execute($status = new GetHumanStatus($token)); dans doneAction.
*
* Le fonctionnement du module Payum est sans doute très intelligent mais il est trop complexe à maitriser pour moi.
* Il fait également appel à des mécanismes dont l'usage systématique est déconseillé par Payzen (utilisation de la variable
* vads_url_check).
*
* Bref. De toute façon, la gestion des notifications successives d'un paiement récurrent ne peut pas être gérée
* via les voies 1 ou 2 en l'état.
* En effet la variable vads_url_check ne fonctionne pas en mode récurrent et Payzen nous notifie à l'aide
* d'une requête post au format de l'"URL de notification à la création d'un abonnement" (mal nommé), configurée à payement/notify,
* (mais on aurait pu mettre autre chose pour plus de clarté). Il faut donc créer un controlleur spécifique qui va traiter
* cette notification sans disposer du token utilisé par les voies 1 ou 2.
*
* Voie 3 : le traitement de la requête se fait finalement simplement dans notifyRecurringPaymentAction sans utiliser
* les mécanismes complexes de Payum.
*
* Yvon KERDONCUFF
* 26/03/2024
*/
/**
* Collects payzen recurring payment payment occurence in place of default payum system.
*
* Payum default controler is not able to catch URL recurring payment notifications
* because payzen actual used URL has shape /payment/notify instead of /payment/notify/token.
*
* @param Request $request
*
* @return Response
* @Route("/payment/notify", name="notify_recurring_payment")
*/
public function notifyRecurringPaymentAction(Request $request)
{
$vads_cust_email = $request->request->get('vads_cust_email');
//Look for recurring payments from this client.
$recurringPayments = $this->em->getRepository(Payment::class)->findBy([
'isRecurrent' => true,
'clientEmail' => $vads_cust_email,
]);
if (!$recurringPayments) {
//return error
return new Response('No recurring payments with email ' . $vads_cust_email, 405);
}
$vads_identifier = $request->request->get('vads_identifier');
$vads_trans_status = $request->request->get('vads_trans_status');
$new_status = strtolower($vads_trans_status);
foreach ($recurringPayments as $payment) {
//Just look for one valid payment.
if ($payment->getDetails()['vads_identifier'] == $vads_identifier) {
if (
GetHumanStatus::STATUS_CAPTURED == $new_status
|| GetHumanStatus::STATUS_AUTHORIZED == $new_status
) {
$this->paymentUtils->handlePayzenNotificationCore($payment);
$this->em->flush();
}
return new Response('Recurring payment occurence taken into account.', 200);
}
}
//return error
return new Response('No recurring payments with vads_identifier ' . $vads_identifier, 405);
}
/**
* Fonction de retour sur le site par l'utilisateur après paiement
* Ce contrôleur est sollicité lorsque Payzen renvoie le cotisant sur l'URL de retour,
*
* @Route("/payment/done/", name="payment_done")
*/
......@@ -154,16 +270,14 @@ class PaymentController extends AbstractController
$gateway->execute($status = new GetHumanStatus($token));
$payment = $status->getFirstModel();
if ($payment->getStatus() == GetHumanStatus::STATUS_NEW) {
// No notification arrived at this point: execute Notify action
if (GetHumanStatus::STATUS_NEW == $payment->getStatus()) {
$gateway->execute(new Notify($token));
} else {
// Invalidate token
$this->payum->getHttpRequestVerifier()->invalidate($token);
}
// Set flash message according to payment status
if ($payment->getStatus() == GetHumanStatus::STATUS_CAPTURED || $payment->getStatus() == GetHumanStatus::STATUS_AUTHORIZED) {
if (GetHumanStatus::STATUS_CAPTURED == $payment->getStatus() || GetHumanStatus::STATUS_AUTHORIZED == $payment->getStatus()) {
$type = $payment->getDescription();
if (Payment::TYPE_ACHAT_MONNAIE_ADHERENT == $type || Payment::TYPE_ACHAT_MONNAIE_PRESTA == $type) {
......@@ -171,12 +285,12 @@ class PaymentController extends AbstractController
'success',
$this->translator->trans('Achat de monnaie locale bien effectué !')
);
} else if (Payment::TYPE_COTISATION_ADHERENT == $type || Payment::TYPE_COTISATION_PRESTA == $type) {
} elseif (Payment::TYPE_COTISATION_ADHERENT == $type || Payment::TYPE_COTISATION_PRESTA == $type) {
$this->addFlash(
'success',
$this->translator->trans('Cotisation bien reçue. Merci !')
);
} else if (Payment::TYPE_ADHESION == $type) {
} elseif (Payment::TYPE_ADHESION == $type) {
$this->addFlash(
'success',
$this->translator->trans('Votre adhésion a bien été prise en compte, bienvenue !')
......@@ -185,21 +299,20 @@ class PaymentController extends AbstractController
// Connect new user
return $this->guardHandler
->authenticateUserAndHandleSuccess(
$this->em->getRepository(User::class)->findOneBy(array('id' => $payment->getClientId())),
$this->em->getRepository(User::class)->findOneBy(['id' => $payment->getClientId()]),
$request,
$this->authenticator,
'main'
);
} else if (Payment::TYPE_PAIEMENT_COTISATION_TAV == $type) {
} elseif (Payment::TYPE_PAIEMENT_COTISATION_TAV == $type || Payment::TYPE_PAIEMENT_RECURRENT_COTISATION_TAV) {
$this->addFlash(
'success',
$this->translator->trans('Cotisation payée !')
);
}
} else if ($payment->getStatus() == GetHumanStatus::STATUS_CANCELED ||
$payment->getStatus() == GetHumanStatus::STATUS_EXPIRED ||
$payment->getStatus() == GetHumanStatus::STATUS_FAILED)
{
} elseif (GetHumanStatus::STATUS_CANCELED == $payment->getStatus() ||
GetHumanStatus::STATUS_EXPIRED == $payment->getStatus() ||
GetHumanStatus::STATUS_FAILED == $payment->getStatus()) {
$this->addFlash(
'error',
$this->translator->trans('La transaction a été annulée.')
......@@ -208,5 +321,4 @@ class PaymentController extends AbstractController
return $this->redirectToRoute('index');
}
}
......@@ -11,6 +11,7 @@ use App\Entity\TransactionAdherentAdherent;
use App\Entity\TransactionAdherentPrestataire;
use App\Form\Type\AchatMonnaieAConfirmerAdherentFormType;
use App\Form\Type\AchatMonnaieAdherentFormType;
use App\Form\Type\AchatMonnaieAdherentRecurrentFormType;
use App\Form\Type\AdherentInfosFormType;
use App\Form\Type\TransactionAdherentAdherentFormType;
use App\Form\Type\TransactionAdherentPrestataireFormType;
......@@ -122,6 +123,42 @@ class UserAdherentController extends FluxController
}
/**
* Validations before proceeding to payment.
*
* Returns error message or null if no error.
*/
private function paiementCotisTavValidation($flux) {
//Look for existing recurring payment
if($reason = $this->tavCotisationsUtils->checkExistingRecurringPayment($flux)) {
return $reason;
}
// Look for existing cotisation
if ($this->tavCotisationsUtils->checkExistingCotisation($flux)) {
return "Cotisation déjà payée ce mois-ci.";
}
$destinataire = $flux->getDestinataire();
// Look for cotisation data depending on active process
if (true == $this->getParameter('household_based_allowance')) {
$cotisationAmount = $destinataire->getCotisationAmount();
if (is_null($cotisationAmount) || is_null($destinataire->getHouseholdAdultCount())) {
return "Opération impossible : votre profil est incomplet, informations de cotisation manquantes. Veuillez contacter un administrateur.";
}
} else {
$profile = $destinataire->getProfilDeCotisation();
if (is_null($profile)) {
return "Opération impossible : vous n'avez pas de profil de cotisation associé. Veuillez contacter un administrateur.";
}
}
return null;
}
/**
* @Route("/adherent/paiement-cotis-tav/", name="paiementCotisTav")
* @IsGranted("ROLE_ADHERENT")
*/
......@@ -138,23 +175,11 @@ class UserAdherentController extends FluxController
if ($form->isSubmitted() && $form->isValid()) {
$flux = $form->getData();
// Look for existing cotisation
if ($this->tavCotisationsUtils->checkExistingCotisation($flux)) {
$validationError = $this->paiementCotisTavValidation($flux);
if (!is_null($validationError)) {
$this->addFlash(
'error',
$this->translator->trans('Cotisation déjà payée ce mois-ci.')
);
return $this->redirectToRoute('index');
}
$destinataire = $flux->getDestinataire();
$profile = $destinataire->getProfilDeCotisation();
if (is_null($profile)) {
$this->addFlash(
'error',
$this->translator->trans('Opération impossible : vous n\'avez pas de profil de cotisation associé. Veuillez contacter un administrateur.')
$this->translator->trans($validationError)
);
return $this->redirectToRoute('index');
......@@ -193,6 +218,57 @@ class UserAdherentController extends FluxController
}
/**
* @Route("/adherent/paiement-reccurent-cotis-tav/", name="paiementRecurrentCotisTav")
* @IsGranted("ROLE_ADHERENT")
*/
public function paiementRecurrentCotisTavAction(Request $request)
{
if (empty($this->getUser()) || empty($this->getUser()->getAdherent())) {
return $this->redirectToRoute('index');
}
$entity = new AchatMonnaieAdherent();
$form = $this->createForm(AchatMonnaieAdherentRecurrentFormType::class, $entity);
$form->handleRequest($request);
if (!$form->isValid()) {
foreach($form->getErrors(true) as $error) {
$this->addFlash('error', $error->getMessage());
}
return $this->redirectToRoute('index');
}
if ($form->isSubmitted() && $form->isValid()) {
$flux = $form->getData();
$validationError = $this->paiementCotisTavValidation($flux);
if (!is_null($validationError)) {
$this->addFlash(
'error',
$this->translator->trans($validationError)
);
return $this->redirectToRoute('index');
}
if (null == $flux->getDon() || 0 == $flux->getDon()->getMontant()) {
$flux->setDon(null);
}
// Redirect to payment
return $this->forward('App\Controller\PaymentController::preparePaymentAction', [
'form' => $form,
'type' => Payment::TYPE_PAIEMENT_RECURRENT_COTISATION_TAV
]);
}
return $this->render('@kohinos/flux/transaction.html.twig', [
'form' => $form->createView(),
'title' => $this->translator->trans('Payer sa cotisation'),
]);
}
/**
* @Route("/adherent/demande/achat-monnaie/", name="achatMonnaieAConfirmerAdherent")
* @IsGranted("ROLE_ADHERENT")
*/
......
......@@ -296,7 +296,7 @@ class UserController extends AbstractController
$now = (new \Datetime('now'))->format('d/m/Y H:i:s');
$flux->setReference(
'Remboursement en Monnaie Solidaire du ' . $now . ', annule ' . $transactionAdherentPrestataire->getReference()
'Remboursement en Monnaie Solidaire du ' . $now . ' annule ' . $transactionAdherentPrestataire->getReference()
);
$this->em->persist($flux);
......
......@@ -649,4 +649,27 @@ abstract class Flux implements FluxInterface
return ($this->getCreatedAt() ? $this->getCreatedAt()->format('d/m/Y H:i') . ' | ' : '') . ucwords($this->getParenttype()) . ' : ' . $this->getExpediteur() . ' => ' . $this->getDestinataire() . ' : ' . $this->getMontant() . '€';
}
public function ssaFriendlyTypeName()
{
$friendlyTypeNames = Flux::ssaUsefulFriendlyTypeNames();
return array_key_exists($this->type,$friendlyTypeNames) ?
$friendlyTypeNames[$this->type]
: $this->type;
}
public static function ssaUsefulFriendlyTypeNames($differentiateAchatsDeMonaPayzenOrComptoir = false)
{
return [
VenteEmlc::TYPE_VENTE_EMLC_ADHERENT => "Achat de MonA via cotisation" . ($differentiateAchatsDeMonaPayzenOrComptoir ? " (au comptoir)" : ""),
AchatMonnaie::TYPE_ACHAT_ADHERENT => "Achat de MonA via cotisation" . ($differentiateAchatsDeMonaPayzenOrComptoir ? " (via Payzen)" : ""),
CotisationTavApplication::TYPE_REVERSEMENT_COTISATION_ADHERENT => "Allocation complémentaire de la caisse",
CotisationTavApplication::TYPE_PRELEVEMENT_COTISATION_ADHERENT => "Réduction de l'allocation",
Don::TYPE_DON_ADHERENT => Don::TYPE_DON_ADHERENT,
Don::TYPE_DON_PRESTATAIRE => Don::TYPE_DON_PRESTATAIRE,
Reconversion::TYPE_RECONVERSION_PRESTATAIRE => Reconversion::TYPE_RECONVERSION_PRESTATAIRE,
Transaction::TYPE_TRANSACTION_PRESTATAIRE_ADHERENT => Transaction::TYPE_TRANSACTION_PRESTATAIRE_ADHERENT,
Transaction::TYPE_TRANSACTION_ADHERENT_PRESTATAIRE => Transaction::TYPE_TRANSACTION_ADHERENT_PRESTATAIRE
];
}
}
......@@ -2,6 +2,7 @@
namespace App\Entity;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Payum\Core\Model\Payment as BasePayment;
use Ramsey\Uuid\Doctrine\UuidGenerator;
......@@ -18,6 +19,7 @@ class Payment extends BasePayment
const TYPE_COTISATION_PRESTA = 'cotisation_presta';
const TYPE_ADHESION = 'adhesion';
const TYPE_PAIEMENT_COTISATION_TAV = 'paiement_cotisation_tav';
const TYPE_PAIEMENT_RECURRENT_COTISATION_TAV = 'paiement_recurrent_cotisation_tav';
/**
* @var \Ramsey\Uuid\UuidInterface
......@@ -53,6 +55,26 @@ class Payment extends BasePayment
protected $extra_data;
/**
* @ORM\Column(type="boolean", nullable=True)
*/
private $isRecurrent;
/**
* @ORM\Column(type="integer", nullable=true)
*/
private $recurrenceMonthsCount;
/**
* @ORM\Column(type="integer", nullable=true)
*/
private $recurrenceMonthDay;
/**
* @ORM\Column(type="integer", nullable=true)
*/
private $recurrenceAmount;
/**
* @return string
*/
public function getStatus(): ?string
......@@ -111,4 +133,104 @@ class Payment extends BasePayment
return $this;
}
public function getIsRecurrent(): ?bool
{
return $this->isRecurrent;
}
public function setIsRecurrent(bool $isRecurrent): self
{
$this->isRecurrent = $isRecurrent;
return $this;
}
public function getRecurrenceMonthsCount(): ?int
{
return $this->recurrenceMonthsCount;
}
public function setRecurrenceMonthsCount(?int $recurrenceMonthsCount): self
{
$this->recurrenceMonthsCount = $recurrenceMonthsCount;
return $this;
}
public function getRecurrenceMonthDay(): ?int
{
return $this->recurrenceMonthDay;
}
public function setRecurrenceMonthDay(?int $recurrenceMonthDay): self
{
$this->recurrenceMonthDay = $recurrenceMonthDay;
return $this;
}
public function getRecurrenceAmount(): ?int
{
return $this->recurrenceAmount;
}
public function setRecurrenceAmount(?int $recurrenceAmount): self
{
$this->recurrenceAmount = $recurrenceAmount;
return $this;
}
/**
* Return null in case of error
* Returns true if payment is already ended or CB expired
* Returns false otherwise
*
* @param $reason
* @return bool|null
*/
public function isRecurringPaymentEndedOrExpired(&$reason)
{
if(!$this->details
|| !array_key_exists('vads_effective_creation_date',$this->details)
|| !array_key_exists('vads_expiry_year',$this->details)
|| !array_key_exists('vads_effective_creation_date',$this->details)
) {
$reason = "Attribut détails vide ou clés absentes.";
return null;
}
$firstDayOfCreationMonth = DateTime::createFromFormat(
'Ymd',substr($this->details['vads_effective_creation_date'],0,6) . "01"
);
if(!$firstDayOfCreationMonth) {
$reason = "Error parsing vads_effective_creation_date : " . $this->details['vads_effective_creation_date'];
return null;
}
$day = $this->getRecurrenceMonthDay() ? $this->getRecurrenceMonthDay() - 1 : 0; //if not set assume first day of month (not a big deal anyway)
$dateOfFirstOccurenceAfterInitialPayment = $firstDayOfCreationMonth->modify(
"+" . $day . " days next month"
);
$paymentEndDate = $this->getRecurrenceMonthsCount() ?
$dateOfFirstOccurenceAfterInitialPayment->modify(
"+" . ($this->getRecurrenceMonthsCount() - 1) . " months" //minus one because initial payment is not considered by payzen as the first occurence
)
: null; //assume no end date if recurrenceMonthsCount not set
//Now check expiry date
$paymentEndDateDueToExpiry = new DateTime();
$paymentEndDateDueToExpiry->setDate($this->details['vads_expiry_year'],$this->details['vads_expiry_month'],$day+1);
$now = new DateTime();
if($paymentEndDate && $paymentEndDate < $paymentEndDateDueToExpiry) {
//Compare now with payment end date
$reason = "Paiement récurrent en cours jusqu'à dernière échéance le " . $paymentEndDate->format('d/m/Y') . ".";
return $now >= $paymentEndDate;
} else {
//Compare now with expiry date
$reason = "Paiement récurrent en cours jusqu'à dernière échéance le " . $paymentEndDateDueToExpiry->format('d/m/Y') . " en raison de l'expiration du moyen de paiement.";
return $now >= $paymentEndDateDueToExpiry;
}
}
}
......@@ -2,37 +2,20 @@
namespace App\EventListener;
use App\Utils\OperationUtils;
use App\Utils\PaymentUtils;
use App\Utils\TAVCotisationUtils;
use Doctrine\ORM\EntityManagerInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use Payum\Core\Extension\Context;
use Payum\Core\Extension\ExtensionInterface;
use Payum\Core\Model\PaymentInterface;
use Payum\Core\Request\Generic;
use Payum\Core\Request\GetHumanStatus;
use Payum\Core\Request\GetStatusInterface;
use Payum\Core\Bridge\Symfony\Event\ExecuteEvent;
use Doctrine\ORM\EntityManagerInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Serializer\SerializerInterface;
use App\Events\MLCEvents;
use App\Events\FluxEvent;
use App\Entity\Flux;
use App\Entity\Payment;
use App\Entity\PaymentToken;
use App\Entity\Siege;
use App\Entity\User;
use App\Entity\Adherent;
use App\Entity\Prestataire;
use App\Entity\Geoloc;
use App\Entity\Groupe;
use App\Entity\Usergroup;
use App\Entity\AchatMonnaieAdherent;
use App\Entity\AchatMonnaiePrestataire;
use App\Entity\CotisationAdherent;
use App\Entity\CotisationPrestataire;
use App\Entity\Don;
use App\Utils\OperationUtils;
use App\Utils\TAVCotisationUtils;
class PaymentStatusExtension implements ExtensionInterface
{
......@@ -42,6 +25,8 @@ class PaymentStatusExtension implements ExtensionInterface
private $userManager;
private $operationUtils;
private $tavCotisationsUtils;
private $container;
private $paymentUtils;
/**
* PaymentStatusExtension constructor.
......@@ -54,7 +39,9 @@ class PaymentStatusExtension implements ExtensionInterface
SerializerInterface $serializer,
UserManagerInterface $userManager,
OperationUtils $operationUtils,
TAVCotisationUtils $tavCotisationsUtils
TAVCotisationUtils $tavCotisationsUtils,
ContainerInterface $container,
PaymentUtils $paymentUtils
) {
$this->em = $em;
$this->eventDispatcher = $eventDispatcher;
......@@ -62,6 +49,8 @@ class PaymentStatusExtension implements ExtensionInterface
$this->userManager = $userManager;
$this->operationUtils = $operationUtils;
$this->tavCotisationsUtils = $tavCotisationsUtils;
$this->container = $container;
$this->paymentUtils = $paymentUtils;
}
/**
......@@ -91,213 +80,30 @@ class PaymentStatusExtension implements ExtensionInterface
// Get current & new status
$context->getGateway()->execute($status = new GetHumanStatus($payment));
$current_payment_status = $payment->getStatus();
// Payment can be captured if it hasn't been captured before
if ($current_payment_status !== GetHumanStatus::STATUS_CAPTURED
&& $current_payment_status != GetHumanStatus::STATUS_AUTHORIZED)
{
// If payment succesful, persist serialized 'Flux' stored in payment
if ($status->getValue() == GetHumanStatus::STATUS_CAPTURED
|| $status->getValue() == GetHumanStatus::STATUS_AUTHORIZED)
{
$flux_array = json_decode($payment->getFluxData(), true);
$type = $payment->getDescription();
if (Payment::TYPE_ACHAT_MONNAIE_ADHERENT == $type) {
$flux = $this->serializer->deserialize(
$payment->getFluxData(),
AchatMonnaieAdherent::class,
'json',
['disable_type_enforcement' => true]
);
$exp = $this->em->getRepository(Siege::class)->find($flux_array['expediteur']);
$flux->setExpediteur($exp);
$dest = $this->em->getRepository(Adherent::class)->find($flux_array['destinataire']);
$flux->setDestinataire($dest);
$op = $this->em->getRepository(User::class)->find($flux_array['operateur']);
$flux->setOperateur($op);
$flux->setReconverti(true);
if (null != $flux->getDon()) {
$flux->getDon()->setType(Don::TYPE_DON_ADHERENT);
$flux->getDon()->setOperateur($op);
$flux->getDon()->setExpediteur($dest);
$flux->getDon()->setDestinataire($this->em->getRepository(Prestataire::class)->findOneBy(['mlc' => true]));
}
} else if (Payment::TYPE_ACHAT_MONNAIE_PRESTA == $type) {
$flux = $this->serializer->deserialize(
$payment->getFluxData(),
AchatMonnaiePrestataire::class,
'json',
['disable_type_enforcement' => true]
);
$exp = $this->em->getRepository(Siege::class)->find($flux_array['expediteur']);
$flux->setExpediteur($exp);
$dest = $this->em->getRepository(Prestataire::class)->find($flux_array['destinataire']);
$flux->setDestinataire($dest);
$op = $this->em->getRepository(User::class)->find($flux_array['operateur']);
$flux->setOperateur($op);
$flux->setReconverti(true);
if (null != $flux->getDon()) {
$flux->getDon()->setType(Don::TYPE_DON_PRESTATAIRE);
$flux->getDon()->setOperateur($op);
$flux->getDon()->setExpediteur($dest);
$flux->getDon()->setDestinataire($this->em->getRepository(Prestataire::class)->findOneBy(['mlc' => true]));
}
} else if (Payment::TYPE_COTISATION_ADHERENT == $type) {
$flux = $this->serializer->deserialize(
$payment->getFluxData(),
CotisationAdherent::class,
'json',
['disable_type_enforcement' => true]
);
$exp = $this->em->getRepository(Adherent::class)->find($flux_array['expediteur']);
$flux->setExpediteur($exp);
$dest = $this->em->getRepository(Prestataire::class)->find($flux_array['destinataire']);
$flux->setDestinataire($dest);
$op = $this->em->getRepository(User::class)->find($flux_array['operateur']);
$flux->setOperateur($op);
$flux->setRecu(true);
if (null != $flux->getDon()) {
$flux->getDon()->setType(Don::TYPE_DON_ADHERENT);
$flux->getDon()->setOperateur($op);
$flux->getDon()->setExpediteur($exp);
$flux->getDon()->setDestinataire($this->em->getRepository(Prestataire::class)->findOneBy(['mlc' => true]));
}
} else if (Payment::TYPE_COTISATION_PRESTA == $type) {
$flux = $this->serializer->deserialize(
$payment->getFluxData(),
CotisationPrestataire::class,
'json',
['disable_type_enforcement' => true]
);
$exp = $this->em->getRepository(Prestataire::class)->find($flux_array['expediteur']);
$flux->setExpediteur($exp);
$dest = $this->em->getRepository(Prestataire::class)->find($flux_array['destinataire']);
$flux->setDestinataire($dest);
$op = $this->em->getRepository(User::class)->find($flux_array['operateur']);
$flux->setOperateur($op);
$flux->setRecu(true);
if (null != $flux->getDon()) {
$flux->getDon()->setType(Don::TYPE_DON_PRESTATAIRE);
$flux->getDon()->setOperateur($op);
$flux->getDon()->setExpediteur($exp);
$flux->getDon()->setDestinataire($this->em->getRepository(Prestataire::class)->findOneBy(['mlc' => true]));
}
} else if (Payment::TYPE_ADHESION == $type) {
$new_adherent_data = json_decode($payment->getExtraData());
$adherent = new Adherent();
$user = $this->userManager->createUser();
$usergroup = $this->em->getRepository(Usergroup::class)->findOneByName('Adherent');
$group = $this->em->getRepository(Groupe::class)->findOneBy(array('id' => $new_adherent_data->groupe->id));
$user->setEmail($new_adherent_data->user->email);
$user->setUsername($new_adherent_data->user->username);
$user->setFirstname($new_adherent_data->user->firstname);
$user->setLastname($new_adherent_data->user->lastname);
$user->setPlainPassword($new_adherent_data->user->plainPassword);
$user->setEnabled(true);
$user->addPossiblegroup($usergroup);
$user->addGroup($usergroup);
$user->addRole('ROLE_ADHERENT');
$user->setAdherent($adherent);
$adherent->setEcompte('0');
$adherent->setUser($user);
$adherent->setGroupe($group);
if ($adherent->getGeoloc() == null) {
$geoloc = new Geoloc();
$geoloc->setAdresse($new_adherent_data->geoloc->adresse);
$geoloc->setCpostal($new_adherent_data->geoloc->cpostal);
$geoloc->setVille($new_adherent_data->geoloc->ville);
$adherent->setGeoloc($geoloc);
}
$this->em->persist($adherent);
$this->em->flush();
// Create first cotisation
$flux = $this->serializer->deserialize(
$payment->getFluxData(),
CotisationAdherent::class,
'json',
['disable_type_enforcement' => true]
);
$flux->setOperateur($user);
$flux->setExpediteur($adherent);
$flux->setDestinataire($this->em->getRepository(Prestataire::class)->findOneBy(array('mlc' => true)));
$flux->setRole('Adherent');
$flux->setRecu(true);
// Update payment with new user id, remove user data
$payment->setClientId($user->getId());
$payment->setExtraData('');
$this->em->persist($payment);
} else if (Payment::TYPE_PAIEMENT_COTISATION_TAV == $type) {
$flux = $this->serializer->deserialize(
$payment->getFluxData(),
AchatMonnaieAdherent::class,
'json',
['disable_type_enforcement' => true]
);
$exp = $this->em->getRepository(Siege::class)->find($flux_array['expediteur']);
$flux->setExpediteur($exp);
$dest = $this->em->getRepository(Adherent::class)->find($flux_array['destinataire']);
$flux->setDestinataire($dest);
$op = $this->em->getRepository(User::class)->find($flux_array['operateur']);
$flux->setOperateur($op);
$flux->setReconverti(true);
if (null != $flux->getDon()) {
$flux->getDon()->setType(Don::TYPE_DON_ADHERENT);
$flux->getDon()->setOperateur($op);
$flux->getDon()->setExpediteur($dest);
$flux->getDon()->setDestinataire($this->em->getRepository(Prestataire::class)->findOneBy(['mlc' => true]));
}
} else {
// Bad request
}
$this->em->persist($flux);
$this->operationUtils->executeOperations($flux);
if (Payment::TYPE_PAIEMENT_COTISATION_TAV == $type) {
// Apply cotisation rate, create new flux
$this->tavCotisationsUtils->applyTauxCotisation($flux);
}
$current_payment_status = $payment->getStatus();
$new_status = $status->getValue();
if (
GetHumanStatus::STATUS_CAPTURED !== $current_payment_status
&& GetHumanStatus::STATUS_AUTHORIZED != $current_payment_status
) {
if (
GetHumanStatus::STATUS_CAPTURED == $new_status
|| GetHumanStatus::STATUS_AUTHORIZED == $new_status
) {
$this->paymentUtils->handlePayzenNotificationCore($payment);
// Invalidate (delete) notify token after payment is captured
$this->em->remove($token);
$this->em->flush();
}
}
//Update status
// Update payment status with status received in payzen response
$payment->setStatus($status->getValue());
$payment->setStatus($new_status);
//Flush
$this->em->persist($payment);
$this->em->flush();
}
......
......@@ -28,6 +28,7 @@ use App\Entity\User;
use App\Form\Type\AchatMonnaieAConfirmerAdherentFormType;
use App\Form\Type\AchatMonnaieAConfirmerPrestataireFormType;
use App\Form\Type\AchatMonnaieAdherentFormType;
use App\Form\Type\AchatMonnaieAdherentRecurrentFormType;
use App\Form\Type\AchatMonnaiePrestataireFormType;
use App\Form\Type\AdherentInfosFormType;
use App\Form\Type\ChangeAdherentComptoirFormType;
......@@ -474,6 +475,19 @@ class FormFactory
return $form->createView();
}
public function getPaiementRecurrentCotisationTAVForm(User $user)
{
if (empty($user) || !$user->isGranted('ROLE_ADHERENT')) {
throw new \Exception('[FORM 25] Opération impossible !');
}
$entity = new AchatMonnaieAdherent();
$entity->setOperateur($user);
$form = $this->ff->create(AchatMonnaieAdherentRecurrentFormType::class, $entity, ['action' => $this->router->generate('paiementRecurrentCotisTav')]);
return $form->createView();
}
public function getComptoirEncaisserDonAdherentForm(User $user)
{
if (empty($user) || empty($this->session->get('_comptoirgere'))) {
......
<?php
namespace App\Form\Type;
use App\Entity\AchatMonnaieAdherent;
use App\Entity\Adherent;
use App\Entity\DonAdherent;
use App\Entity\GlobalParameter;
use App\Entity\Prestataire;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
class AchatMonnaieAdherentRecurrentFormType extends AchatMonnaieAdherentFormType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$now = new \DateTime();
//force prelevement to occur on same day as today to make sure next prelevement occurs next month
//if today is after the 28th, use the 28th so that prelevement always occurs on same date
$jourPrelevement = min($now->format('d'),28);
$builder
->add('nombreMois', IntegerType::class, [
'label' => 'Nombre d\'échéances désirées : ',
'required' => true,
'mapped' => false,
'constraints' => [
new GreaterThanOrEqual(['value' => 2, 'message' => "Le nombre d'échéances doit être au moins égal à 2."]),
],
'help' => "Une échéance par mois.
Le premier paiement est inclus dans le nombre d'échéances.
Le nombre d'échéance réel pourra être diminué selon la date d'expiration du moyen de paiement utilisé.
Pour demander une interruption anticipée, merci de contacter la caisse.",
'attr' => ['autocomplete' => 'off']
])
->add('jourPrelevement', ChoiceType::class, [
'label' => 'Jour du prélèvement dans le mois : ',
'choices' => [strval($jourPrelevement) => $jourPrelevement],
'data' => $jourPrelevement,
'required' => true,
'mapped' => false,
'attr' => ['autocomplete' => 'off', 'readonly' => true],
'help' => "Pour simplifier le traitement de l'information, le jour de prélèvement est fixé au jour du premier paiement ou au plus tard le 28 du mois."
])
->remove('saveHelloAsso')
->remove('save')
->add('save', SubmitType::class, [
'label' => 'Payer en CB mon premier paiement et créer un paiement récurrent',
'translation_domain' => 'messages',
'attr' => [
'class' => 'btn-primary btn achatCBSubmit',
],
])
->remove('reference')
->add('reference', HiddenType::class, [
'data' => 'Achat monnaie en CB (récurrent) Adhérent',
])
;
$builder
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'class' => AchatMonnaieAdherent::class,
]);
}
public function getParent()
{
return AchatMonnaieAdherentFormType::class;
}
public function getBlockPrefix()
{
return 'formAchatMonnaieAdherentRecurrent';
}
}
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240321105448 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE payment ADD is_recurrent TINYINT(1) DEFAULT \'0\' NOT NULL, ADD recurrence_months_count INT DEFAULT NULL, ADD recurrence_month_day INT DEFAULT NULL, ADD recurrence_amount INT DEFAULT NULL');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE payment DROP is_recurrent, DROP recurrence_months_count, DROP recurrence_month_day, DROP recurrence_amount');
}
}
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240325100723 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE payment CHANGE is_recurrent is_recurrent TINYINT(1) DEFAULT NULL');
$this->addSql('ALTER TABLE prestataire CHANGE iban iban LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:personal_data)\'');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE payment CHANGE is_recurrent is_recurrent TINYINT(1) DEFAULT \'0\' NOT NULL');
$this->addSql('ALTER TABLE prestataire CHANGE iban iban LONGTEXT CHARACTER SET utf8mb3 DEFAULT NULL COLLATE `utf8mb3_general_ci` COMMENT \'(DC2Type:personal_data)\'');
}
}
......@@ -55,6 +55,7 @@ class FormExtension extends AbstractExtension
new \Twig_SimpleFunction('getSetPaymentCodeForm', [$this, 'getSetPaymentCodeForm']),
new \Twig_SimpleFunction('getComptoirEncaisserCotisationForm', [$this, 'getComptoirEncaisserCotisationForm']),
new \Twig_SimpleFunction('getPayerCotisationTAVForm', [$this, 'getPayerCotisationTAVForm']),
new \Twig_SimpleFunction('getPaiementRecurrentCotisationTAVForm', [$this, 'getPaiementRecurrentCotisationTAVForm']),
new \Twig_SimpleFunction('getComptoirEncaisserDonAdherentForm', [$this, 'getComptoirEncaisserDonAdherentForm'])
];
}
......@@ -219,6 +220,11 @@ class FormExtension extends AbstractExtension
return $this->container->get('app.formfactory')->getPayerCotisationTAVForm($user);
}
public function getPaiementRecurrentCotisationTAVForm(User $user)
{
return $this->container->get('app.formfactory')->getPaiementRecurrentCotisationTAVForm($user);
}
public function getComptoirEncaisserDonAdherentForm(User $user)
{
return $this->container->get('app.formfactory')->getComptoirEncaisserDonAdherentForm($user);
......
<?php
namespace App\Utils;
use App\Entity\AchatMonnaieAdherent;
use App\Entity\AchatMonnaiePrestataire;
use App\Entity\Adherent;
use App\Entity\CotisationAdherent;
use App\Entity\CotisationPrestataire;
use App\Entity\Don;
use App\Entity\Geoloc;
use App\Entity\Groupe;
use App\Entity\Payment;
use App\Entity\Prestataire;
use App\Entity\Siege;
use App\Entity\User;
use App\Entity\Usergroup;
use Doctrine\ORM\EntityManagerInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use Payum\Core\Request\GetHumanStatus;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Serializer\SerializerInterface;
class PaymentUtils
{
private $em;
private $serializer;
private $userManager;
private $operationUtils;
private $tavCotisationsUtils;
private $container;
/**
* PaymentUtils constructor.
*/
public function __construct(
EntityManagerInterface $em,
SerializerInterface $serializer,
UserManagerInterface $userManager,
OperationUtils $operationUtils,
TAVCotisationUtils $tavCotisationsUtils,
ContainerInterface $container
) {
$this->em = $em;
$this->serializer = $serializer;
$this->userManager = $userManager;
$this->operationUtils = $operationUtils;
$this->tavCotisationsUtils = $tavCotisationsUtils;
$this->container = $container;
}
/**
* @param $payment
* @return void
* @throws \Exception
*/
public function handlePayzenNotificationCore($payment): void
{
$current_payment_status = $payment->getStatus();
// Payment can be captured if it hasn't been captured before
// Allow STATUS_CAPTURED and STATUS_AUTHORIZED for recurrent payment only.
$flux_array = json_decode($payment->getFluxData(), true);
$type = $payment->getDescription();
if (Payment::TYPE_ACHAT_MONNAIE_ADHERENT == $type) {
$flux = $this->serializer->deserialize(
$payment->getFluxData(),
AchatMonnaieAdherent::class,
'json',
['disable_type_enforcement' => true]
);
$exp = $this->em->getRepository(Siege::class)->find($flux_array['expediteur']);
$flux->setExpediteur($exp);
$dest = $this->em->getRepository(Adherent::class)->find($flux_array['destinataire']);
$flux->setDestinataire($dest);
$op = $this->em->getRepository(User::class)->find($flux_array['operateur']);
$flux->setOperateur($op);
$flux->setReconverti(true);
if (null != $flux->getDon()) {
$flux->getDon()->setType(Don::TYPE_DON_ADHERENT);
$flux->getDon()->setOperateur($op);
$flux->getDon()->setExpediteur($dest);
$flux->getDon()->setDestinataire($this->em->getRepository(Prestataire::class)->findOneBy(['mlc' => true]));
}
} else if (Payment::TYPE_ACHAT_MONNAIE_PRESTA == $type) {
$flux = $this->serializer->deserialize(
$payment->getFluxData(),
AchatMonnaiePrestataire::class,
'json',
['disable_type_enforcement' => true]
);
$exp = $this->em->getRepository(Siege::class)->find($flux_array['expediteur']);
$flux->setExpediteur($exp);
$dest = $this->em->getRepository(Prestataire::class)->find($flux_array['destinataire']);
$flux->setDestinataire($dest);
$op = $this->em->getRepository(User::class)->find($flux_array['operateur']);
$flux->setOperateur($op);
$flux->setReconverti(true);
if (null != $flux->getDon()) {
$flux->getDon()->setType(Don::TYPE_DON_PRESTATAIRE);
$flux->getDon()->setOperateur($op);
$flux->getDon()->setExpediteur($dest);
$flux->getDon()->setDestinataire($this->em->getRepository(Prestataire::class)->findOneBy(['mlc' => true]));
}
} else if (Payment::TYPE_COTISATION_ADHERENT == $type) {
$flux = $this->serializer->deserialize(
$payment->getFluxData(),
CotisationAdherent::class,
'json',
['disable_type_enforcement' => true]
);
$exp = $this->em->getRepository(Adherent::class)->find($flux_array['expediteur']);
$flux->setExpediteur($exp);
$dest = $this->em->getRepository(Prestataire::class)->find($flux_array['destinataire']);
$flux->setDestinataire($dest);
$op = $this->em->getRepository(User::class)->find($flux_array['operateur']);
$flux->setOperateur($op);
$flux->setRecu(true);
if (null != $flux->getDon()) {
$flux->getDon()->setType(Don::TYPE_DON_ADHERENT);
$flux->getDon()->setOperateur($op);
$flux->getDon()->setExpediteur($exp);
$flux->getDon()->setDestinataire($this->em->getRepository(Prestataire::class)->findOneBy(['mlc' => true]));
}
} else if (Payment::TYPE_COTISATION_PRESTA == $type) {
$flux = $this->serializer->deserialize(
$payment->getFluxData(),
CotisationPrestataire::class,
'json',
['disable_type_enforcement' => true]
);
$exp = $this->em->getRepository(Prestataire::class)->find($flux_array['expediteur']);
$flux->setExpediteur($exp);
$dest = $this->em->getRepository(Prestataire::class)->find($flux_array['destinataire']);
$flux->setDestinataire($dest);
$op = $this->em->getRepository(User::class)->find($flux_array['operateur']);
$flux->setOperateur($op);
$flux->setRecu(true);
if (null != $flux->getDon()) {
$flux->getDon()->setType(Don::TYPE_DON_PRESTATAIRE);
$flux->getDon()->setOperateur($op);
$flux->getDon()->setExpediteur($exp);
$flux->getDon()->setDestinataire($this->em->getRepository(Prestataire::class)->findOneBy(['mlc' => true]));
}
} else if (Payment::TYPE_ADHESION == $type) {
$new_adherent_data = json_decode($payment->getExtraData());
$adherent = new Adherent();
$user = $this->userManager->createUser();
$usergroup = $this->em->getRepository(Usergroup::class)->findOneByName('Adherent');
$group = $this->em->getRepository(Groupe::class)->findOneBy(array('id' => $new_adherent_data->groupe->id));
$user->setEmail($new_adherent_data->user->email);
$user->setUsername($new_adherent_data->user->username);
$user->setFirstname($new_adherent_data->user->firstname);
$user->setLastname($new_adherent_data->user->lastname);
$user->setPlainPassword($new_adherent_data->user->plainPassword);
$user->setEnabled(true);
$user->addPossiblegroup($usergroup);
$user->addGroup($usergroup);
$user->addRole('ROLE_ADHERENT');
$user->setAdherent($adherent);
$adherent->setEcompte('0');
$adherent->setUser($user);
$adherent->setGroupe($group);
if ($adherent->getGeoloc() == null) {
$geoloc = new Geoloc();
$geoloc->setAdresse($new_adherent_data->geoloc->adresse);
$geoloc->setCpostal($new_adherent_data->geoloc->cpostal);
$geoloc->setVille($new_adherent_data->geoloc->ville);
$adherent->setGeoloc($geoloc);
}
$this->em->persist($adherent);
$this->em->flush();
// Create first cotisation
$flux = $this->serializer->deserialize(
$payment->getFluxData(),
CotisationAdherent::class,
'json',
['disable_type_enforcement' => true]
);
$flux->setOperateur($user);
$flux->setExpediteur($adherent);
$flux->setDestinataire($this->em->getRepository(Prestataire::class)->findOneBy(array('mlc' => true)));
$flux->setRole('Adherent');
$flux->setRecu(true);
// Update payment with new user id, remove user data
$payment->setClientId($user->getId());
$payment->setExtraData('');
$this->em->persist($payment);
} else if (Payment::TYPE_PAIEMENT_COTISATION_TAV == $type || Payment::TYPE_PAIEMENT_RECURRENT_COTISATION_TAV) {
$flux = $this->serializer->deserialize(
$payment->getFluxData(),
AchatMonnaieAdherent::class,
'json',
['disable_type_enforcement' => true]
);
$exp = $this->em->getRepository(Siege::class)->find($flux_array['expediteur']);
$flux->setExpediteur($exp);
$dest = $this->em->getRepository(Adherent::class)->find($flux_array['destinataire']);
$flux->setDestinataire($dest);
$op = $this->em->getRepository(User::class)->find($flux_array['operateur']);
$flux->setOperateur($op);
$flux->setReconverti(true);
//For recurrent payment: save don only for first payment, otherwise remove don
//Use current payment status to decide (if first payment -> add don, if not -> remove don)
if (null != $flux->getDon() && $current_payment_status === GetHumanStatus::STATUS_NEW) {
$flux->getDon()->setType(Don::TYPE_DON_ADHERENT);
$flux->getDon()->setOperateur($op);
$flux->getDon()->setExpediteur($dest);
$flux->getDon()->setDestinataire($this->em->getRepository(Prestataire::class)->findOneBy(['mlc' => true]));
} else {
$flux->setDon(null);
}
} else {
// Bad request
}
$this->em->persist($flux);
$this->operationUtils->executeOperations($flux);
if (Payment::TYPE_PAIEMENT_COTISATION_TAV == $type || Payment::TYPE_PAIEMENT_RECURRENT_COTISATION_TAV) {
// Create new flux for cotisation, depending on process
if ($this->container->getParameter('household_based_allowance')) {
$this->tavCotisationsUtils->applyHouseholdAllowance($flux);
} else {
$this->tavCotisationsUtils->applyTauxCotisation($flux);
}
}
}
}
......@@ -3,6 +3,7 @@
namespace App\Utils;
use App\Entity\Adherent;
use App\Entity\Payment;
use App\Entity\Siege;
use App\Entity\Flux;
use App\Entity\CotisationTavReversement;
......@@ -44,6 +45,23 @@ class TAVCotisationUtils
return count($existing) > 0;
}
public function checkExistingRecurringPayment(Flux $flux)
{
$recurringPayments = $this->em->getRepository(Payment::class)->findBy([
'isRecurrent' => true,
'clientEmail' => $flux->getDestinataire()->getUser()->getEmail(),
]);
$res = "";
foreach($recurringPayments as $p) {
$reason = "";
if($p->isRecurringPaymentEndedOrExpired($reason) !== true) {
$res .= ($reason . " ");
}
}
return $res;
}
/**
* First method to calculate allowance:
* according to a contribution rate defined in user's profile (ProfilDeCotisation).
......@@ -168,14 +186,14 @@ class TAVCotisationUtils
$fluxCotis->setExpediteur($siege);
$fluxCotis->setDestinataire($adherent);
$fluxCotis->setMontant($amountDiff);
$fluxCotis->setReference("Reversement du complément de cotisation après paiement de " . $cotisationAmount . "€ pour une allocation de " . $mlcAllowanceAmount . " MonA.");
$fluxCotis->setReference("Versement de l'allocation complémentaire après paiement de " . $cotisationAmount . "€ pour atteindre une allocation de " . $mlcAllowanceAmount . " MonA.");
} else {
// User should receive less than he•she paid: fetch the difference from his account
$fluxCotis = new CotisationTavPrelevement();
$fluxCotis->setExpediteur($adherent);
$fluxCotis->setDestinataire($siege);
$fluxCotis->setMontant(-$amountDiff);
$fluxCotis->setReference("Prélèvement du complément de cotisation après paiement de " . $cotisationAmount . "€ pour une allocation de " . $mlcAllowanceAmount . " MonA.");
$fluxCotis->setReference("Réduction de l'allocation correspondant à un paiement de " . $cotisationAmount . "€ pour atteindre une allocation de " . $mlcAllowanceAmount . " MonA.");
}
$fluxCotis->setOperateur($flux->getOperateur());
......@@ -205,7 +223,7 @@ class TAVCotisationUtils
$flux->setExpediteur($adherent);
$flux->setDestinataire($siege);
$flux->setMontant(-$amountDiff);
$flux->setReference("Prélèvement pour ramener le solde de " . $balance . "MonA sous le plafond de " . $ceiling . " MonA.");
$flux->setReference("Prélèvement pour ramener le solde de " . $balance . " MonA sous le plafond de " . $ceiling . " MonA.");
$flux->setOperateur($this->security->getUser());
$flux->setRole($this->security->getUser()->getGroups()[0]->__toString());
$flux->setMoyen(MoyenEnum::MOYEN_EMLC);
......
......@@ -5,6 +5,7 @@
{% endblock blocktitle %}
{% block blockcontent %}
{% set form = getPayerCotisationTAVForm(app.user) %}
{% set formRecurrentPayment = getPaiementRecurrentCotisationTAVForm(app.user) %}
{% if form.montant.vars.value == false and not household_based_allowance %}
<p>{{ 'Vous n\'avez pas de profil de cotisation associé, vous ne pouvez donc pas payer de cotisation.'|trans }}</p>
......@@ -16,9 +17,15 @@
<p>
{{ 'Montant de la cotisation à payer'|trans }} : <span class="paiement_cotisation_montant">{{ form.montant.vars.value }}</span>
</p>
<select name="tav-cotisation-payment-type" id="tav-cotisation-payment-type" class="form-control" autocomplete="off">
<option value="">Choisir une option de paiement</option>
<option value="instant-payment">Paiement immédiat</option>
<option value="recurrent-payment">Paiement récurrent</option>
</select>
<div id="tav-cotisation-instant-payment-container" class="tav-cotisation-payment-form">
{{form_start(form)}}
{% if form.don is defined %}
{% include '@kohinos/tav/block/adherent_payer_cotisation_don.html.twig' %}
{% include '@kohinos/tav/block/adherent_payer_cotisation_don.html.twig' with {'parentForm': form} %}
<hr/>
<p><b>{{ 'TOTAL A PAYER'|trans }} : <span class="achat_monnaie_montant_total">{{ form.montant.vars.value }}</span></b></p>
{% endif %}
......@@ -29,6 +36,22 @@
{{ form_widget(form.saveHelloAsso) }}
{% endif %}
{{form_end(form)}}
</div>
<div id="tav-cotisation-recurrent-payment-container" class="tav-cotisation-payment-form">
{{form_start(formRecurrentPayment)}}
{{ form_row(formRecurrentPayment.nombreMois) }}
{{ form_row(formRecurrentPayment.jourPrelevement) }}
{% if formRecurrentPayment.don is defined %}
{% include '@kohinos/tav/block/adherent_payer_cotisation_don.html.twig' with {'parentForm': formRecurrentPayment} %}
<hr/>
<p><b>{{ 'TOTAL PREMIER PAIEMENT'|trans }} : <span class="achat_monnaie_premier_montant_total">{{ formRecurrentPayment.montant.vars.value }}</span></b></p>
<p><b>{{ 'ÉCHÉANCES SUIVANTES'|trans }} : <span class="achat_monnaie_montant_total">{{ formRecurrentPayment.montant.vars.value }}</span></b></p>
{% endif %}
{% if formRecurrentPayment.save is defined %}
{{ form_widget(formRecurrentPayment.save) }}
{% endif %}
{{form_end(formRecurrentPayment)}}
</div>
{% endif %}
......
......@@ -4,5 +4,5 @@
En plus de ma cotisation je souhaite faire un don pour ce mois
{% endblock blocktitle %}
{% block blockcontent %}
{{ form_row(form.don, {row_attr:{style: 'max-width: 200px;margin: 0 auto;'}}) }}
{{ form_row(parentForm.don, {row_attr:{style: 'max-width: 200px;margin: 0 auto;'}}) }}
{% endblock blockcontent %}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment