integration stripe et mise au norme API cashless

This commit is contained in:
Jonas 12t 2021-09-25 19:21:35 +04:00
parent b9f8324333
commit ba7d4af4d6
17 changed files with 542 additions and 83 deletions

View File

@ -7,6 +7,7 @@ from django.contrib.auth.admin import UserAdmin, GroupAdmin
from Customers.models import Client, Domain
from AuthBillet.models import TibilletUser, HumanUser, TermUser, SuperHumanUser
from django.utils.translation import gettext, gettext_lazy as _
from QrcodeCashless.models import Detail, CarteCashless
# from boutique.models import Category, Product, Tag, VAT, Event, LandingPageContent, Billet
@ -128,5 +129,35 @@ class ClientAdmin(admin.ModelAdmin):
'created_on',
)
public_admin_site.register(Client, ClientAdmin)
class DetailAdmin(admin.ModelAdmin):
list_display = (
'base_url',
'origine',
'generation',
'img_url',
'img',
)
public_admin_site.register(Detail, DetailAdmin)
class CarteCashlessAdmin(admin.ModelAdmin):
list_display = (
'tag_id',
'uuid',
'number',
'get_origin',
)
def get_origin(self, obj):
return obj.detail.origine
get_origin.short_description = 'Origine'
search_fields = ('tag_id', 'uuid', 'number')
list_filter = ('tag_id', 'uuid', 'number')
public_admin_site.register(CarteCashless, CarteCashlessAdmin)

View File

@ -8,6 +8,7 @@ from BaseBillet.models import Configuration, Event, OptionGenerale, Article, Bil
from django.contrib.auth.admin import UserAdmin
from Customers.models import Client
from PaiementStripe.models import Paiement_stripe
class StaffAdminSite(AdminSite):
@ -117,6 +118,9 @@ class ConfigurationAdmin(SingletonModelAdmin):
('Paiements', {
'fields': (
'mollie_api_key',
'stripe_api_key',
'stripe_test_api_key',
'stripe_mode_test',
),
}),
('Billetterie', {
@ -172,13 +176,51 @@ class ReservationAdmin(admin.ModelAdmin):
staff_admin_site.register(Reservation, ReservationAdmin)
class ArticleAdmin(admin.ModelAdmin):
list_display = (
'name',
'prix',
'stock',
'reservation_par_user_max',
'vat',
'publish',
)
list_editable = (
'prix',
'stock',
'reservation_par_user_max',
'vat',
'publish',
)
staff_admin_site.register(OptionGenerale, OptionGeneraleAdmin)
staff_admin_site.register(Billet, admin.ModelAdmin)
staff_admin_site.register(Article, admin.ModelAdmin)
staff_admin_site.register(Article, ArticleAdmin)
staff_admin_site.register(LigneArticle, admin.ModelAdmin)
staff_admin_site.register(OptionGenerale, OptionGeneraleAdmin)
staff_admin_site.register(Billet, admin.ModelAdmin)
class PaiementStripeAdmin(admin.ModelAdmin):
list_display = (
'detail',
'total',
'order_date',
'user',
'status',
)
ordering = ('-order_date',)
# readonly_fields = (
# 'reservations',
# )
staff_admin_site.register(Paiement_stripe, PaiementStripeAdmin)

View File

@ -169,7 +169,6 @@ class HumanUserManager(TibilletManager):
is_superuser=False,
client_achat__id__in=[connection.tenant.id, ],
)
# .distinct() ???
class HumanUser(TibilletUser):

View File

@ -67,6 +67,10 @@ class Configuration(SingletonModel):
mollie_api_key = models.CharField(max_length=50,
blank=True, null=True)
stripe_api_key = models.CharField(max_length=110, blank=True, null=True)
stripe_test_api_key = models.CharField(max_length=110, blank=True, null=True)
stripe_mode_test = models.BooleanField(default=True)
jauge_max = models.PositiveSmallIntegerField(default=50)
option_generale_radio = models.ManyToManyField(OptionGenerale,
@ -105,6 +109,18 @@ class Billet(models.Model):
def __str__(self):
return f"{self.name}"
class VAT(models.Model):
"""
Les différents taux de TVA sont associés à des produits.
"""
percent = models.FloatField(verbose_name="Taux de TVA (%)")
class Meta:
verbose_name = _('TVA')
verbose_name_plural = _('TVA')
def __str__(self):
return f"{self.percent}%"
class Article(models.Model):
name = models.CharField(max_length=50,
@ -113,6 +129,9 @@ class Article(models.Model):
stock = models.SmallIntegerField(blank=True, null=True)
reservation_par_user_max = models.PositiveSmallIntegerField(default=10)
vat = models.ForeignKey(VAT, on_delete=models.PROTECT, verbose_name="TVA", null=True, blank=True)
publish = models.BooleanField(default=False)
def range_max(self):
return range(self.reservation_par_user_max + 1)
@ -224,7 +243,7 @@ class LigneArticle(models.Model):
qty = models.SmallIntegerField()
reste = models.SmallIntegerField()
paiement_stripe = models.ForeignKey(Paiement_stripe, on_delete=models.PROTECT, blank=True, null=True)
datetime = models.DateTimeField(auto_now=True)
# def __str__(self):
# if self.reservation :
# if self.article :

View File

@ -1,6 +1,9 @@
from django.db import models
import uuid
# Create your models here.
from TiBillet import settings
# class Configuration_stripe(models.Model):
class Paiement_stripe(models.Model):
@ -8,34 +11,29 @@ class Paiement_stripe(models.Model):
La commande
"""
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True, db_index=True)
detail = models.CharField(max_length=50, blank=True, null=True)
id_stripe = models.CharField(max_length=20, blank=True, null=True)
id_stripe = models.CharField(max_length=80, blank=True, null=True)
order_date = models.DateTimeField(auto_now=True, verbose_name="Date")
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, blank=True, null=True)
NON, OPEN, PENDING, PAID, VALID, CANCELED = 'N', 'O', 'W', 'P', 'V', 'C'
NON, OPEN, PENDING, EXPIRE, PAID, VALID, CANCELED = 'N', 'O', 'W', 'E', 'P', 'V', 'C'
STATUT_CHOICES = (
(NON, 'Lien de paiement non crée'),
(OPEN, 'Envoyée a Stripe'),
(PENDING, 'En attente de paiement'),
(EXPIRE, 'Expiré'),
(PAID, 'Payée'),
(VALID, 'Payée et validée'), # envoyé sur serveur cashless
(CANCELED, 'Annulée'),
)
status = models.CharField(max_length=1, choices=STATUT_CHOICES, default=NON, verbose_name="Statut de la commande")
# a remplir par default sur le front par User.email.
email_billet = models.CharField(max_length=30, verbose_name="Email de récéption des billets", blank=True)
# def total(self):
# total = 0
# for article in ArticleCommande.objects.filter(commande=self):
# total += article.total()
#
# return total
total = models.FloatField(default=0)
def __str__(self):
return self.status
return f"{self.detail} - {self.status}"

View File

@ -0,0 +1,8 @@
from django.urls import include, path, re_path
from .views import retour_stripe
urlpatterns = [
path('return/<uuid:uuid>', retour_stripe.as_view()),
path('webhook_stripe', retour_stripe.as_view()),
]

View File

@ -1,3 +1,67 @@
from django.shortcuts import render
from datetime import datetime
from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404
import stripe
# Create your views here.
from django.views import View
from BaseBillet.models import Configuration
from PaiementStripe.models import Paiement_stripe
class retour_stripe(View):
def get(self, request, uuid):
configuration = Configuration.get_solo()
paiement_stripe = get_object_or_404(Paiement_stripe, uuid=uuid)
if configuration.stripe_mode_test:
stripe.api_key = configuration.stripe_test_api_key
else:
stripe.api_key = configuration.stripe_api_key
if paiement_stripe.status != Paiement_stripe.VALID :
checkout_session = stripe.checkout.Session.retrieve(paiement_stripe.id_stripe)
if checkout_session.payment_status == "unpaid":
paiement_stripe.status = Paiement_stripe.PENDING
if checkout_session.expires_at > datetime.now().timestamp() :
paiement_stripe.status = Paiement_stripe.EXPIRE
elif checkout_session.payment_status == "paid":
paiement_stripe.status = Paiement_stripe.PAID
else:
paiement_stripe.status = Paiement_stripe.CANCELED
paiement_stripe.save()
return HttpResponse(f'ok {uuid}')
class webhook_stripe(View):
def get(self, request):
print(f"webhook_stripe GET")
return HttpResponse(f'ok')
def post(self, request):
endpoint_secret = 'whsec_1Urn98yUMsgwdXA7vhN5dwDTRQLD2vmD'
event = None
payload = request.data
sig_header = request.headers['STRIPE_SIGNATURE']
try:
event = stripe.Webhook.construct_event(
payload, sig_header, endpoint_secret
)
except ValueError as e:
# Invalid payload
raise e
except stripe.error.SignatureVerificationError as e:
# Invalid signature
raise e
# Handle the event
print('Unhandled event type {}'.format(event['type']))
print(f"webhook_stripe POST {event}")
return HttpResponse(f'ok {event}')

View File

@ -0,0 +1,83 @@
import os
from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand
from Customers.models import Client, Domain
from QrcodeCashless.models import Detail, CarteCashless
from django.core.validators import URLValidator
import csv, uuid
class Command(BaseCommand):
def is_string_an_url(self, url_string):
validate_url = URLValidator()
try:
validate_url(url_string)
except ValidationError as e:
return False
return True
def handle(self, *args, **options):
for client in Client.objects.all():
print (client.schema_name)
input_client = input('quel client ? \n')
client_tenant = Client.objects.get(schema_name=input_client)
print(' ')
input_generation = input('quelle génération ? \n')
print(' ')
print('url, numéro imprimé len(8), fisrt tag id len(8)')
input_fichier_csv = input('path fichier csv ? \n')
file = open(input_fichier_csv)
# file = open('data/raffinerie_1_RETOUR_USINE.csv')
csv_parser = csv.reader(file)
list_csv = []
for line in csv_parser:
list_csv.append(line)
# on saucissonne l'url d'une ligne au pif :
part = list_csv[10][0].partition('/qr/')
base_url = f"{part[0]}{part[1]}"
if self.is_string_an_url(base_url) :
detail_carte, created = Detail.objects.get_or_create(
base_url=base_url,
origine=client_tenant,
generation=input_generation,
)
numline = 1
for line in list_csv:
print(numline)
part = line[0].partition('/qr/')
try:
uuid_url = uuid.UUID(part[2])
print(f"uuid_url : {uuid_url}")
print(f"number : {line[1]}")
print(f"tag_id : {line[2]}")
if str(uuid_url).partition('-')[0].upper() != line[1]:
print('ERROR PRINT != uuid')
break
carte, created = CarteCashless.objects.get_or_create(
tag_id=line[2],
uuid=uuid_url,
number=line[1],
detail=detail_carte,
)
numline += 1
except:
pass

View File

@ -0,0 +1,68 @@
import json
import os
from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand
from Customers.models import Client, Domain
from QrcodeCashless.models import Detail, CarteCashless
from django.core.validators import URLValidator
import csv, uuid
class Command(BaseCommand):
def is_string_an_url(self, url_string):
validate_url = URLValidator()
try:
validate_url(url_string)
except ValidationError as e:
return False
return True
def handle(self, *args, **options):
for client in Client.objects.all():
print (client.schema_name)
input_client = input('quel client ? \n')
client_tenant = Client.objects.get(schema_name=input_client)
print(' ')
print('url, numéro imprimé len(8), fisrt tag id len(8)')
input_generation = input('quelle génération ? \n')
print(' ')
detail_carte, created = Detail.objects.get_or_create(
base_url='https://m.tibillet.re/',
origine=client_tenant,
generation=input_generation,
)
file = open('data/CarteCashlessBisik.json')
json_dict = json.load(file)
for card in json_dict:
tag_id = card['fields']['tag_id']
number = card['fields']['number']
if tag_id and number:
# on va generer un faux uuid pour le bisik
# Namespace hardcodé volontairement pour vérification
namespace = uuid.UUID('6ba7b811-9dad-11d1-80b4-00c04fd430c8')
gen_uuid = uuid.uuid5(namespace, number)
print(tag_id)
print(number)
print(gen_uuid)
carte, created = CarteCashless.objects.get_or_create(
tag_id=tag_id,
uuid=gen_uuid,
number=number,
detail=detail_carte,
)

View File

@ -1,8 +1,29 @@
from django.db import models
# Create your models here.
from stdimage import StdImageField
from stdimage.validators import MaxSizeValidator
from Customers.models import Client as Customers_Client
class Detail(models.Model):
img = StdImageField(upload_to='images/',
null=True, blank=True,
validators=[MaxSizeValidator(1920, 1920)],
variations={
'med': (480, 480),
'thumbnail': (150, 90),
},
delete_orphans=True,
verbose_name='Recto de la carte'
)
img_url = models.URLField(null=True, blank=True)
base_url = models.CharField(max_length=60, null=True, blank=True)
origine = models.ForeignKey(Customers_Client, on_delete=models.PROTECT, null=True, blank=True)
generation = models.SmallIntegerField()
def __str__(self):
return self.base_url
class CarteCashless(models.Model):
tag_id = models.CharField(
@ -11,7 +32,7 @@ class CarteCashless(models.Model):
unique=True
)
uuid_qrcode = models.UUIDField(
uuid = models.UUIDField(
blank=True, null=True,
verbose_name='Uuid',
)
@ -19,9 +40,9 @@ class CarteCashless(models.Model):
number = models.CharField(
db_index=True,
max_length=8,
blank=True,
null=True,
unique=True
)
origine = models.ForeignKey(Customers_Client, on_delete=models.PROTECT)
detail = models.ForeignKey(Detail, on_delete=models.CASCADE, null=True, blank=True)

View File

@ -1,7 +1,8 @@
from django.urls import include, path, re_path
from .views import index_scan
from .views import index_scan, gen_one_bisik
urlpatterns = [
path('<uuid:uuid>', index_scan.as_view()),
path('', gen_one_bisik.as_view())
]

View File

@ -1,19 +1,47 @@
import requests, json
from django.http import HttpResponseRedirect, HttpResponse
from django.contrib.auth import get_user_model
from django.db import connection
from django.http import HttpResponseRedirect, HttpResponse, Http404
from django.shortcuts import render
from rest_framework.generics import get_object_or_404
import stripe
# Create your views here.
from django.views import View
from rest_framework import status
from rest_framework.response import Response
from BaseBillet.models import Configuration
from AuthBillet.models import TibilletUser, HumanUser
from BaseBillet.models import Configuration, Article
from PaiementStripe.models import Paiement_stripe
from QrcodeCashless.models import CarteCashless
def get_domain(request):
absolute_uri = request.build_absolute_uri()
for domain in request.tenant.domains.all():
if domain.domain in absolute_uri:
return domain.domain
raise Http404
class gen_one_bisik(View):
def get(self, request, numero_carte):
print(numero_carte)
carte = get_object_or_404(CarteCashless, number=numero_carte)
address = request.build_absolute_uri()
return HttpResponseRedirect(address.replace("://m.", "://bisik.").replace(f"{carte.number}", f"qr/{carte.uuid}"))
class index_scan(View):
template_name = "RechargementWebUuid.html"
def check_carte(self, uuid):
def check_carte_local(self, uuid):
carte = get_object_or_404(CarteCashless, uuid=uuid)
return carte
def check_carte_serveur_cashless(self, uuid):
configuration = Configuration.get_solo()
# on questionne le serveur cashless pour voir si la carte existe :
@ -28,81 +56,47 @@ class index_scan(View):
'uuid': f'{uuid}',
})
except requests.exceptions.ConnectionError:
reponse = HttpResponse("Serveur non disponible. Merci de revenir ultérieurement.", status=status.HTTP_503_SERVICE_UNAVAILABLE)
reponse = HttpResponse("Serveur non disponible. Merci de revenir ultérieurement.",
status=status.HTTP_503_SERVICE_UNAVAILABLE)
sess.close()
return reponse
def post(self, request, uuid):
data = request.POST
reponse_server_cashless = self.check_carte(uuid)
if data.get('numero_carte_cashless') == str(uuid).split('-')[0].upper() and \
reponse_server_cashless.status_code == 200:
userWeb, created = User.objects.get_or_create(
username="RechargementWeb",
email="rechargementweb@tibillet.re")
vat, created = VAT.objects.get_or_create(percent=0)
commande = Commande.objects.create(
user=userWeb,
email_billet=data.get('email'),
)
art, created = Product.objects.get_or_create(
name="CashlessRechargementWeb",
price_ttc="1",
publish=False,
vat=vat
)
ArticleCommande.objects.create(
product=art,
quantity=data.get('thune'),
commande=commande,
)
domain = get_domain(request)
sub_domain = str(domain).split('.')[0]
absolute_domain = request.build_absolute_uri().partition('/qr')[0]
Paiement = CreationPaiementMollie(commande, domain,
description=f"Rechargez votre carte {sub_domain.capitalize()}",
redirectUrl=f"{absolute_domain}/RechargementWebAfterMollie/{commande.uuid}",
webhookUrl=f"{absolute_domain}/RechargementWebAfterMollie/{commande.uuid}",
numero_carte_cashless=data.get('numero_carte_cashless'))
if Paiement.is_send():
return HttpResponseRedirect(Paiement.is_send())
def get(self, request, uuid):
carte = self.check_carte_local(uuid)
if carte.detail.origine != connection.tenant :
raise Http404
# dette technique ...
# pour rediriger les premières générations de qrcode
# m.tibillet.re et raffinerie
# pour rediriger les carte imprimés a la raffinerie vers le bon tenant.
address = request.build_absolute_uri()
host = address.partition('://')[2]
sub_addr = host.partition('.')[0]
if sub_addr == "m":
return HttpResponseRedirect(address.replace("://m.", "://raffinerie."))
configuration = Configuration.get_solo()
if not configuration.server_cashless:
return HttpResponse(
"L'adresse du serveur cashless n'est pas renseignée dans la configuration de la billetterie.")
if not configuration.stripe_api_key or not configuration.stripe_test_api_key:
return HttpResponse(
"Pas d'information de configuration pour paiement en ligne.")
reponse_server_cashless = self.check_carte(uuid)
reponse_server_cashless = self.check_carte_serveur_cashless(carte.uuid)
if reponse_server_cashless.status_code == 200:
json_reponse = json.loads(reponse_server_cashless.json())
liste_assets = json_reponse.get('liste_assets')
email = json_reponse.get('email')
if reponse_server_cashless.status_code == 200:
return render(
request,
self.template_name,
{
'numero_carte': str(uuid).split('-')[0].upper(),
'numero_carte': carte.number,
'domain': sub_addr,
'informations_carte': reponse_server_cashless.text,
'liste_assets': liste_assets,
@ -117,3 +111,126 @@ class index_scan(View):
elif reponse_server_cashless.status_code == 500:
# Serveur cashless hors ligne
return reponse_server_cashless
def post(self, request, uuid):
carte = self.check_carte_local(uuid)
if carte.detail.origine != connection.tenant :
raise Http404
data = request.POST
reponse_server_cashless = self.check_carte_serveur_cashless(carte.uuid)
montant_recharge = float(data.get('thune'))
configuration = Configuration.get_solo()
if data.get('numero_carte_cashless') == str(uuid).split('-')[0].upper() and \
reponse_server_cashless.status_code == 200 and \
montant_recharge > 0:
User = get_user_model()
user_recharge, created = User.objects.get_or_create(
email=data.get('email'))
if created:
user_recharge: HumanUser
user_recharge.client_source = connection.tenant
user_recharge.client_achat.add(connection.tenant)
user_recharge.is_active = False
else:
user_recharge.client_achat.add(connection.tenant)
user_recharge.save()
art, created = Article.objects.get_or_create(
name="Recharge Stripe",
prix="1",
publish=False,
)
paiementStripe = Paiement_stripe.objects.create(
user=user_recharge,
detail=f"{art.name}",
total=float("{0:.2f}".format(montant_recharge)),
)
domain = get_domain(request)
absolute_domain = request.build_absolute_uri().partition('/qr')[0]
if configuration.stripe_mode_test:
stripe.api_key = configuration.stripe_test_api_key
else:
stripe.api_key = configuration.stripe_api_key
checkout_session = stripe.checkout.Session.create(
customer_email=f'{user_recharge.email}',
line_items=[{
'price_data': {
'currency': 'eur',
'product_data': {
'name': 'Recharge Cashless',
"images": [f'{carte.detail.img_url}', ],
},
'unit_amount': int("{0:.2f}".format(montant_recharge).replace('.', '')),
},
'quantity': 1,
}],
payment_method_types=[
'card',
],
mode='payment',
metadata={
'Carte': str(uuid).split('-')[0].upper()
},
success_url=f'{absolute_domain}/stripe/return/{paiementStripe.uuid}',
cancel_url=f'{absolute_domain}/stripe/return/{paiementStripe.uuid}',
# submit_type='Go go go',
client_reference_id=f"{data.get('numero_carte_cashless')}",
)
print(checkout_session.id)
paiementStripe.id_stripe = checkout_session.id
paiementStripe.status = Paiement_stripe.PENDING
paiementStripe.save()
return HttpResponseRedirect(checkout_session.url)
# Paiement = CreationPaiementMollie(commande, domain,
# description=f"Rechargez votre carte {sub_domain.capitalize()}",
# redirectUrl=f"{absolute_domain}/RechargementWebAfterMollie/{commande.uuid}",
# webhookUrl=f"{absolute_domain}/RechargementWebAfterMollie/{commande.uuid}",
# numero_carte_cashless=data.get('numero_carte_cashless'))
#
# if Paiement.is_send():
# return HttpResponseRedirect(Paiement.is_send())
#
# def postPaimentRecharge(paiementStripe: Paiement_stripe):
#
# if paiementStripe.status == Paiement_stripe.PAID :
#
# sess = requests.Session()
# configuration = Configuration.get_solo()
# r = sess.post(f'{configuration.server_cashless}/wv/rechargementWeb',
# data={
# 'number': numero_carte_cashless,
# 'qty': commande.total(),
# 'uuid_commande': commande_uuid,
# 'API_TIBILLET_BILLETERIE_VERS_CASHLESS': os.getenv(
# 'API_TIBILLET_BILLETERIE_VERS_CASHLESS'),
# })
#
# sess.close()
#
# if r.status_code == 200:
# # la commande a été envoyé au serveur cashless, on la met en validée
# commande.status = 'V'
# commande.save()
#
# return HttpResponse(
# f"<center><h1>Paiement validé, vous avez rechargé votre carte ! Disponible : {r.text}</h1></center>")
#
# elif commande.status == "V":
# # Le paiement a bien été accepté par le passé et envoyé au serveur cashless.
# return HttpResponse(
# f"<center><h1>Paiement bien validé, vous avez rechargé votre carte !</h1></center>")
#
# return HttpResponse("<center><h1>Paiement non valide. Contactez un responsable.<h1></center>")

View File

@ -34,6 +34,11 @@ urlpatterns = [
re_path(r'api/', include('ApiBillet.urls')),
re_path(r'qr/', include('QrcodeCashless.urls')),
# pour carte GEN1 Bisik
re_path(r'(?P<numero_carte>^\w{5}$)', include('QrcodeCashless.urls')),
re_path(r'stripe/', include('PaiementStripe.urls')),
path('', include('BaseBillet.urls')),

View File

@ -4,6 +4,8 @@ services:
container_name: tibillet_postgres
image: postgres:11.5-alpine
restart: always
ports:
- 5432:5432
volumes:
- "../../Postgres/dbdata:/var/lib/postgresql/data"
- "/etc/localtime:/etc/localtime:ro"

View File

@ -58,6 +58,7 @@ RUN pip install django-stdimage
# RUN pip install -U social-auth-app-django
# RUN pip install -U django-templated-mail
RUN pip install stripe