gestion améliorée du retour de paiement.

Ajout de la source du paiement stripe.
This commit is contained in:
Jonas 12t 2021-10-26 16:32:51 +04:00
parent 9f76969c5c
commit 290ca513ca
12 changed files with 271 additions and 87 deletions

View File

@ -214,10 +214,6 @@ staff_admin_site.register(Product, ProductAdmin)
staff_admin_site.register(LigneArticle, admin.ModelAdmin)
staff_admin_site.register(OptionGenerale, OptionGeneraleAdmin) staff_admin_site.register(OptionGenerale, OptionGeneraleAdmin)
staff_admin_site.register(Price, admin.ModelAdmin) staff_admin_site.register(Price, admin.ModelAdmin)
@ -230,6 +226,7 @@ class PaiementStripeAdmin(admin.ModelAdmin):
'total', 'total',
'order_date', 'order_date',
'status', 'status',
'source',
) )
ordering = ('-order_date',) ordering = ('-order_date',)
@ -237,4 +234,16 @@ class PaiementStripeAdmin(admin.ModelAdmin):
staff_admin_site.register(Paiement_stripe, PaiementStripeAdmin) staff_admin_site.register(Paiement_stripe, PaiementStripeAdmin)
class LigneArticleAdmin(admin.ModelAdmin):
list_display = (
'datetime',
'price',
'qty',
'carte',
'status',
'paiement_stripe',
'status_stripe'
)
ordering = ('-datetime',)
staff_admin_site.register(LigneArticle, LigneArticleAdmin)

View File

@ -241,7 +241,6 @@ class ReservationValidator(serializers.Serializer):
lignes_article = [] lignes_article = []
for price in self.prices_list: for price in self.prices_list:
ligne_article = LigneArticle.objects.create( ligne_article = LigneArticle.objects.create(
reservation=reservation,
price=price.get('price'), price=price.get('price'),
qty=price.get('qty'), qty=price.get('qty'),
) )
@ -264,6 +263,8 @@ class ReservationValidator(serializers.Serializer):
) )
if new_paiement_stripe.is_valid(): if new_paiement_stripe.is_valid():
reservation.paiement = new_paiement_stripe.paiement_stripe_db
reservation.save()
print(new_paiement_stripe.checkout_session.stripe_id) print(new_paiement_stripe.checkout_session.stripe_id)
# return new_paiement_stripe.redirect_to_stripe() # return new_paiement_stripe.redirect_to_stripe()
self.checkout_session = new_paiement_stripe.checkout_session self.checkout_session = new_paiement_stripe.checkout_session

View File

@ -0,0 +1,27 @@
# Generated by Django 2.2 on 2021-10-26 10:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('BaseBillet', '0010_auto_20211025_1002'),
]
operations = [
migrations.RemoveField(
model_name='lignearticle',
name='reservation',
),
migrations.AlterField(
model_name='configuration',
name='name_required_for_ticket',
field=models.BooleanField(default=False, verbose_name='Billet nominatifs'),
),
migrations.AlterField(
model_name='ticket',
name='scan_status',
field=models.CharField(choices=[('N', 'Non actif'), ('K', 'Non scanné'), ('S', 'scanné')], default='N', max_length=1, verbose_name='Status du scan'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2 on 2021-10-26 11:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('PaiementStripe', '0001_initial'),
('BaseBillet', '0011_auto_20211026_1459'),
]
operations = [
migrations.AddField(
model_name='reservation',
name='paiement',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='reservation', to='PaiementStripe.Paiement_stripe'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2 on 2021-10-26 11:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('BaseBillet', '0012_reservation_paiement'),
]
operations = [
migrations.AddField(
model_name='lignearticle',
name='status',
field=models.CharField(choices=[('C', 'Annulée'), ('N', 'Non payée'), ('P', 'Payée'), ('V', 'Validée par serveur cashless')], default='N', max_length=3, verbose_name='Status de ligne article'),
),
]

View File

@ -1,11 +1,14 @@
import uuid import uuid
import requests
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
# Create your models here. # Create your models here.
from django.db.models.signals import post_save from django.db.models import Q
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone
from solo.models import SingletonModel from solo.models import SingletonModel
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from stdimage import StdImageField from stdimage import StdImageField
@ -328,11 +331,14 @@ class Reservation(models.Model):
status = models.CharField(max_length=3, choices=TYPE_CHOICES, default=UNPAID, status = models.CharField(max_length=3, choices=TYPE_CHOICES, default=UNPAID,
verbose_name=_("Status de la réservation")) verbose_name=_("Status de la réservation"))
paiement = models.OneToOneField(Paiement_stripe, on_delete=models.PROTECT, blank=True, null=True, related_name='reservation')
options = models.ManyToManyField(OptionGenerale) options = models.ManyToManyField(OptionGenerale)
def user_mail(self): def user_mail(self):
return self.user_commande.email return self.user_commande.email
#
# def total_billet(self): # def total_billet(self):
# total = 0 # total = 0
# for ligne in self.paiements.all(): # for ligne in self.paiements.all():
@ -388,7 +394,81 @@ class LigneArticle(models.Model):
qty = models.SmallIntegerField() qty = models.SmallIntegerField()
reservation = models.ForeignKey(Reservation, on_delete=models.CASCADE, blank=True, null=True, related_name='paiements')
carte = models.ForeignKey(CarteCashless, on_delete=models.PROTECT, blank=True, null=True) carte = models.ForeignKey(CarteCashless, on_delete=models.PROTECT, blank=True, null=True)
paiement_stripe = models.ForeignKey(Paiement_stripe, on_delete=models.PROTECT, blank=True, null=True) paiement_stripe = models.ForeignKey(Paiement_stripe, on_delete=models.PROTECT, blank=True, null=True)
CANCELED, UNPAID, PAID, VALID, = 'C', 'N', 'P', 'V'
TYPE_CHOICES = [
(CANCELED, _('Annulée')),
(UNPAID, _('Non payée')),
(PAID, _('Payée')),
(VALID, _('Validée par serveur cashless')),
]
status = models.CharField(max_length=3, choices=TYPE_CHOICES, default=UNPAID,
verbose_name=_("Status de ligne article"))
def status_stripe(self):
if self.paiement_stripe:
return self.paiement_stripe.status
else:
return _('no stripe send')
@receiver(post_save, sender=LigneArticle)
def check_status_stripe(sender, instance: LigneArticle, created, **kwargs):
if instance.paiement_stripe :
lignes_dans_paiement_stripe = instance.paiement_stripe.lignearticle_set.all()
if len(lignes_dans_paiement_stripe) == len(lignes_dans_paiement_stripe.filter(status=LigneArticle.VALID)):
# toute les lignes d'article sont VALID
# on passe le status du paiement stripe en VALID
instance.paiement_stripe.status = Paiement_stripe.VALID
instance.paiement_stripe.save()
@receiver(pre_save, sender=Paiement_stripe)
def send_to_cashless(sender, instance: Paiement_stripe, update_fields=None, **kwargs):
# si l'instance vient d'être créé, ne rien faire :
if instance.pk is None:
pass
else:
paiementStripe = instance
if paiementStripe.status == Paiement_stripe.PAID:
data_pour_serveur_cashless = {'uuid_commande': paiementStripe.uuid}
for ligne_article in paiementStripe.lignearticle_set.all():
if ligne_article.carte:
data_pour_serveur_cashless['uuid'] = ligne_article.carte.uuid
if ligne_article.price.product.categorie_article == Product.RECHARGE_CASHLESS:
data_pour_serveur_cashless['recharge_qty'] = ligne_article.price.prix
if ligne_article.price.product.categorie_article == Product.ADHESION:
data_pour_serveur_cashless['tarif_adhesion'] = ligne_article.price.prix
# si il y a autre chose que uuid_commande :
if len(data_pour_serveur_cashless) > 1:
sess = requests.Session()
configuration = Configuration.get_solo()
r = sess.post(
f'{configuration.server_cashless}/api/billetterie_endpoint',
headers={
'Authorization': f'Api-Key {configuration.key_cashless}'
},
data=data_pour_serveur_cashless,
)
sess.close()
print(
f"{timezone.now()} demande au serveur cashless pour un rechargement. réponse : {r.status_code} ")
if r.status_code == 200:
# la commande a été envoyé au serveur cashless et validé.
for ligne_article in paiementStripe.lignearticle_set.filter(
Q(price__product__categorie_article=Product.RECHARGE_CASHLESS) |
Q(price__product__categorie_article=Product.ADHESION)):
ligne_article.status = LigneArticle.VALID
ligne_article.save()

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2 on 2021-10-26 12:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('PaiementStripe', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='paiement_stripe',
name='source',
field=models.CharField(choices=[('Q', 'Depuis scan QR-Code'), ('B', 'Depuis billetterie')], default='N', max_length=1, verbose_name='Statut de la commande'),
),
migrations.AlterField(
model_name='paiement_stripe',
name='status',
field=models.CharField(choices=[('N', 'Lien de paiement non créé'), ('O', 'Envoyée a Stripe'), ('W', 'En attente de paiement'), ('E', 'Expiré'), ('P', 'Payée'), ('V', 'Payée et validée'), ('C', 'Annulée')], default='N', max_length=1, verbose_name='Statut de la commande'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2 on 2021-10-26 12:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('PaiementStripe', '0002_auto_20211026_1617'),
]
operations = [
migrations.AlterField(
model_name='paiement_stripe',
name='source',
field=models.CharField(choices=[('Q', 'Depuis scan QR-Code'), ('B', 'Depuis billetterie')], default='B', max_length=1, verbose_name='Statut de la commande'),
),
]

View File

@ -1,6 +1,8 @@
from django.db import models from django.db import models
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
import uuid import uuid
from django.utils.translation import gettext, gettext_lazy as _
# Create your models here. # Create your models here.
from TiBillet import settings from TiBillet import settings
@ -20,7 +22,7 @@ class Paiement_stripe(models.Model):
NON, OPEN, PENDING, EXPIRE, PAID, VALID, CANCELED = 'N', 'O', 'W', 'E', 'P', 'V', 'C' NON, OPEN, PENDING, EXPIRE, PAID, VALID, CANCELED = 'N', 'O', 'W', 'E', 'P', 'V', 'C'
STATUT_CHOICES = ( STATUT_CHOICES = (
(NON, 'Lien de paiement non crée'), (NON, 'Lien de paiement non créé'),
(OPEN, 'Envoyée a Stripe'), (OPEN, 'Envoyée a Stripe'),
(PENDING, 'En attente de paiement'), (PENDING, 'En attente de paiement'),
(EXPIRE, 'Expiré'), (EXPIRE, 'Expiré'),
@ -31,8 +33,14 @@ class Paiement_stripe(models.Model):
status = models.CharField(max_length=1, choices=STATUT_CHOICES, default=NON, verbose_name="Statut de la commande") status = models.CharField(max_length=1, choices=STATUT_CHOICES, default=NON, verbose_name="Statut de la commande")
total = models.FloatField(default=0) QRCODE, BILLETTERIE = 'Q', 'B'
SOURCE_CHOICES = (
(QRCODE, _('Depuis scan QR-Code')),
(BILLETTERIE, _('Depuis billetterie')),
)
source = models.CharField(max_length=1, choices=SOURCE_CHOICES, default=BILLETTERIE, verbose_name="Source de la commande")
total = models.FloatField(default=0)
def uuid_8(self): def uuid_8(self):
return f"{self.uuid}".partition('-')[0] return f"{self.uuid}".partition('-')[0]
@ -41,8 +49,8 @@ class Paiement_stripe(models.Model):
return self.uuid_8() return self.uuid_8()
def articles(self): def articles(self):
return " - ".join([ f"{ligne.product.name} {ligne.qty * ligne.product.prix }" for ligne in self.lignearticle_set.all()]) return " - ".join(
[f"{ligne.product.name} {ligne.qty * ligne.product.prix}" for ligne in self.lignearticle_set.all()])
''' RECEIVER PRESAVE DANS LE VIEW QRCODECASHELESS ''' RECEIVER PRESAVE DANS LE VIEW QRCODECASHELESS

View File

@ -26,6 +26,7 @@ class creation_paiement_stripe():
email_paiement: str, email_paiement: str,
liste_ligne_article: list, liste_ligne_article: list,
metadata: dict, metadata: dict,
source: str,
absolute_domain: str absolute_domain: str
) -> None: ) -> None:
@ -33,6 +34,7 @@ class creation_paiement_stripe():
self.liste_ligne_article = liste_ligne_article self.liste_ligne_article = liste_ligne_article
self.email_paiement = email_paiement self.email_paiement = email_paiement
self.metadata = metadata self.metadata = metadata
self.source = source
self.configuration = Configuration.get_solo() self.configuration = Configuration.get_solo()
self.user = self._user_paiement() self.user = self._user_paiement()
@ -73,9 +75,10 @@ class creation_paiement_stripe():
user=self.user, user=self.user,
total=self.total, total=self.total,
metadata_stripe=self.metadata_json, metadata_stripe=self.metadata_json,
source=self.source,
) )
for ligne_article in self.liste_ligne_article : for ligne_article in self.liste_ligne_article:
ligne_article: LigneArticle ligne_article: LigneArticle
ligne_article.paiement_stripe = paiementStripeDb ligne_article.paiement_stripe = paiementStripeDb
ligne_article.save() ligne_article.save()
@ -123,7 +126,7 @@ class creation_paiement_stripe():
def is_valid(self): def is_valid(self):
if self.checkout_session.id and \ if self.checkout_session.id and \
self.checkout_session.url : self.checkout_session.url:
return True return True
else: else:
@ -133,26 +136,26 @@ class creation_paiement_stripe():
return HttpResponseRedirect(self.checkout_session.url) return HttpResponseRedirect(self.checkout_session.url)
# On vérifie que les métatada soient les meme dans la DB et chez Stripe. # On vérifie que les métatada soient les meme dans la DB et chez Stripe.
def metatadata_valid(paiement_stripe_db: Paiement_stripe, checkout_session): def metatadata_valid(paiement_stripe_db: Paiement_stripe, checkout_session):
metadata_stripe_json = checkout_session.metadata metadata_stripe_json = checkout_session.metadata
metadata_stripe = json.loads(str(metadata_stripe_json)) metadata_stripe = json.loads(str(metadata_stripe_json))
metadata_db_json = paiement_stripe_db.metadata_stripe metadata_db_json = paiement_stripe_db.metadata_stripe
metadata_db = json.loads(metadata_db_json) metadata_db = json.loads(metadata_db_json)
try:
assert metadata_stripe == metadata_db
assert set(metadata_db.keys()) == set(metadata_stripe.keys())
for key in set(metadata_stripe.keys()):
assert metadata_db[key] == metadata_stripe[key]
return True
except:
logger.error(f"{timezone.now()} "
f"retour_stripe {paiement_stripe_db.uuid} : "
f"metadata ne correspondent pas : {metadata_stripe} {metadata_db}")
return False
try:
assert metadata_stripe == metadata_db
assert set(metadata_db.keys()) == set(metadata_stripe.keys())
for key in set(metadata_stripe.keys()):
assert metadata_db[key] == metadata_stripe[key]
return True
except:
logger.error(f"{timezone.now()} "
f"retour_stripe {paiement_stripe_db.uuid} : "
f"metadata ne correspondent pas : {metadata_stripe} {metadata_db}")
return False
class retour_stripe(View): class retour_stripe(View):
@ -170,8 +173,8 @@ class retour_stripe(View):
checkout_session = stripe.checkout.Session.retrieve(paiement_stripe.id_stripe) checkout_session = stripe.checkout.Session.retrieve(paiement_stripe.id_stripe)
# on vérfie que les metatada soient cohérente. #NTUI ! # on vérfie que les metatada soient cohérentes. #NTUI !
if metatadata_valid(paiement_stripe , checkout_session): if metatadata_valid(paiement_stripe, checkout_session):
if checkout_session.payment_status == "unpaid": if checkout_session.payment_status == "unpaid":
paiement_stripe.status = Paiement_stripe.PENDING paiement_stripe.status = Paiement_stripe.PENDING
@ -181,7 +184,14 @@ class retour_stripe(View):
elif checkout_session.payment_status == "paid": elif checkout_session.payment_status == "paid":
paiement_stripe.status = Paiement_stripe.PAID paiement_stripe.status = Paiement_stripe.PAID
# le .save() lance le process pre_save dans le view QrcodeCashless qui peut modifier son status
# le .save() lance le process pre_save BaseBillet.models.send_to_cashless
# qui modifie le status de chaque ligne
# et envoie les informations au serveur cashless.
# si validé par le serveur cashless, alors la ligne sera VALID.
# Si toute les lignes sont VALID, le paiement_stripe sera aussi VALID
# grace au post_save BaseBillet.models.check_status_stripe
paiement_stripe.save() paiement_stripe.save()
else: else:
@ -192,19 +202,28 @@ class retour_stripe(View):
# on vérifie que le status n'ai pas changé # on vérifie que le status n'ai pas changé
paiement_stripe.refresh_from_db() paiement_stripe.refresh_from_db()
if paiement_stripe.status == Paiement_stripe.VALID:
for ligne_article in paiement_stripe.lignearticle_set.all():
if ligne_article.carte :
messages.success(request, f"Paiement validé. Merci !")
return HttpResponseRedirect(f"/qr/{ligne_article.carte.uuid}#success")
else : # si c'est depuis le qrcode, on renvoie vers la vue mobile :
for ligne_article in paiement_stripe.lignearticle_set.all(): if paiement_stripe.source == Paiement_stripe.QRCODE :
if ligne_article.carte : if paiement_stripe.status == Paiement_stripe.VALID :
messages.error(request, f"Un problème de validation de paiement a été detecté. Merci de vérifier votre moyen de paiement ou contactez un responsable.") # on boucle ici pour récuperer l'uuid de la carte.
return HttpResponseRedirect(f"/qr/{ligne_article.carte.uuid}#error") for ligne_article in paiement_stripe.lignearticle_set.all():
if ligne_article.carte:
messages.success(request, f"Paiement validé. Merci !")
return HttpResponseRedirect(f"/qr/{ligne_article.carte.uuid}#success")
return HttpResponse('Un problème de validation de paiement a été detecté. Merci de vérifier votre moyen de paiement ou contactez un responsable.') else :
# on boucle ici pour récuperer l'uuid de la carte.
for ligne_article in paiement_stripe.lignearticle_set.all():
if ligne_article.carte:
messages.error(request,
f"Un problème de validation de paiement a été detecté. "
f"Merci de vérifier votre moyen de paiement ou contactez un responsable.")
return HttpResponseRedirect(f"/qr/{ligne_article.carte.uuid}#error")
else:
return HttpResponse(
'Un problème de validation de paiement a été detecté. Merci de vérifier votre moyen de paiement ou contactez un responsable.')
# return HttpResponseRedirect("/") # return HttpResponseRedirect("/")

View File

@ -34,7 +34,7 @@
<li><a href="#recharger">Recharger</a></li> <li><a href="#recharger">Recharger</a></li>
{% if history %} {% if history %}
<li><a href="#historique">Historique</a></li> <li><a href="#historique">Solde</a></li>
{% endif %} {% endif %}
{% if carte_resto %} {% if carte_resto %}
@ -130,7 +130,7 @@
</tfoot> </tfoot>
</table> </table>
</div> </div>
<h2 class="major">Historique</h2> <h2 class="major">Historique 24h</h2>
<div class="table-wrapper"> <div class="table-wrapper">

View File

@ -3,6 +3,7 @@ from datetime import datetime
import requests, json import requests, json
from django.contrib import messages from django.contrib import messages
from django.db import connection from django.db import connection
from django.db.models import Q
from django.http import HttpResponseRedirect, HttpResponse, Http404 from django.http import HttpResponseRedirect, HttpResponse, Http404
from django.shortcuts import render from django.shortcuts import render
from django.utils import timezone from django.utils import timezone
@ -201,6 +202,7 @@ class index_scan(View):
email_paiement=data.get('email'), email_paiement=data.get('email'),
liste_ligne_article=ligne_articles, liste_ligne_article=ligne_articles,
metadata=metadata, metadata=metadata,
source=Paiement_stripe.QRCODE,
absolute_domain=request.build_absolute_uri().partition('/qr')[0], absolute_domain=request.build_absolute_uri().partition('/qr')[0],
) )
@ -256,44 +258,3 @@ class index_scan(View):
else: else:
messages.error(request, f'Erreur {r.status_code} {r.text}') messages.error(request, f'Erreur {r.status_code} {r.text}')
return HttpResponseRedirect(f'#erreur') return HttpResponseRedirect(f'#erreur')
@receiver(pre_save, sender=Paiement_stripe)
def changement_paid_to_valid(sender, instance: Paiement_stripe, update_fields=None, **kwargs):
# si l'instance vient d'être créé, ne rien faire :
if instance.pk is None:
pass
else:
paiementStripe = instance
if paiementStripe.status == Paiement_stripe.PAID:
data_pour_serveur_cashless = {'uuid_commande': paiementStripe.uuid}
for ligne_article in paiementStripe.lignearticle_set.all():
if ligne_article.carte:
data_pour_serveur_cashless['uuid'] = ligne_article.carte.uuid
if ligne_article.price.product.categorie_article == Product.RECHARGE_CASHLESS:
data_pour_serveur_cashless['recharge_qty'] = ligne_article.price.prix
if ligne_article.price.product.categorie_article == Product.ADHESION:
data_pour_serveur_cashless['tarif_adhesion'] = ligne_article.price.prix
# si il y a autre chose que uuid_commande :
if len(data_pour_serveur_cashless) > 1:
sess = requests.Session()
configuration = Configuration.get_solo()
r = sess.post(
f'{configuration.server_cashless}/api/billetterie_endpoint',
headers={
'Authorization': f'Api-Key {configuration.key_cashless}'
},
data=data_pour_serveur_cashless,
)
sess.close()
print(
f"{timezone.now()} demande au serveur cashless pour un rechargement. réponse : {r.status_code} ")
if r.status_code == 200:
# la commande a été envoyé au serveur cashless et validé.
paiementStripe.status = Paiement_stripe.VALID