From 7f9483c2bfea51bf16812428e6d9f323266978bd Mon Sep 17 00:00:00 2001 From: Amir Date: Mon, 20 Oct 2025 19:52:11 +0200 Subject: [PATCH] added paddle --- backend/accounts/tests/test_utils.py | 4 +- backend/core/admin.py | 8 +- backend/core/migrations/0029_paymentrecord.py | 25 +++ backend/core/models.py | 11 + backend/core/templates/payment.html | 155 +++++++++++--- backend/core/urls.py | 4 +- backend/core/views.py | 193 ++++++++++++------ backend/settings.py | 5 + 8 files changed, 309 insertions(+), 96 deletions(-) create mode 100644 backend/core/migrations/0029_paymentrecord.py diff --git a/backend/accounts/tests/test_utils.py b/backend/accounts/tests/test_utils.py index a897bfc..6d150c0 100644 --- a/backend/accounts/tests/test_utils.py +++ b/backend/accounts/tests/test_utils.py @@ -35,7 +35,7 @@ class EmailTests(TestCase): ) self.document = Document.objects.create(organization=self.organization) - @patch("backend.accounts.utils.send_mail") + @patch('backend.accounts.utils.EmailMultiAlternatives') def test_send_confirmation_email(self, mock_send_mail): confirmation = EmailConfirmation.objects.create(email=self.email, uuid=uuid.uuid4(), created_at=now()) send_confirmation_email(self.email) @@ -44,7 +44,7 @@ class EmailTests(TestCase): self.assertIsNotNone(confirmation.uuid) self.assertEqual(mock_send_mail.call_count, 1) - @patch("backend.accounts.utils.send_mail") + @patch('backend.accounts.utils.EmailMultiAlternatives') def test_send_payment_email(self, mock_send_mail): send_payment_email(self.email) self.assertEqual(mock_send_mail.call_count, 1) diff --git a/backend/core/admin.py b/backend/core/admin.py index 90f8b3e..4fd9c76 100644 --- a/backend/core/admin.py +++ b/backend/core/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Document, DocumentSegment, Organization, Risk, Control, DocumentTemplate, DocumentRiskControl, DemoCode +from .models import Document, DocumentSegment, Organization, Risk, Control, DocumentTemplate, DocumentRiskControl, DemoCode, PaymentRecord from django.urls import reverse, path from django.utils.html import format_html from .utils import generate_demo_code, get_top_risk, get_controls_for_risk, generate_key_findings, generate_recommendations @@ -459,8 +459,14 @@ class DemoCodeAdmin(admin.ModelAdmin): else: form = GenerateCodesForm() return render(request, 'admin/generate_codes.html', {'form': form}) + +class PaymentRecordAdmin(admin.ModelAdmin): + list_display = ('company', 'amount', 'currency', 'payment_date', 'transaction_id') + search_fields = ('company__name', 'transaction_id') + list_filter = ('payment_date',) +admin.site.register(PaymentRecord, PaymentRecordAdmin) admin.site.register(Document, DocumentAdmin) admin.site.register(Organization, OrganizationAdmin) admin.site.register(Risk ,RiskAdmin) diff --git a/backend/core/migrations/0029_paymentrecord.py b/backend/core/migrations/0029_paymentrecord.py new file mode 100644 index 0000000..36cf6a8 --- /dev/null +++ b/backend/core/migrations/0029_paymentrecord.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.3 on 2025-10-20 13:14 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0028_document_risk_explanations'), + ] + + operations = [ + migrations.CreateModel( + name='PaymentRecord', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=10)), + ('currency', models.CharField(max_length=10)), + ('payment_date', models.DateTimeField(auto_now_add=True)), + ('transaction_id', models.CharField(max_length=255, unique=True)), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.organization')), + ], + ), + ] diff --git a/backend/core/models.py b/backend/core/models.py index db274ab..07d1ecd 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -200,3 +200,14 @@ class DemoCode(models.Model): def __str__(self): return (f"{self.code} - {'Used' if self.used else 'Available'}") + +class PaymentRecord(models.Model): + company = models.ForeignKey(Organization, on_delete=models.CASCADE) + amount = models.DecimalField(max_digits=10, decimal_places=2) + currency = models.CharField(max_length=10) + payment_date = models.DateTimeField(auto_now_add=True) + transaction_id = models.CharField(max_length=255, unique=True) + + def __str__(self): + return f"{self.company} - {self.amount} {self.currency}" + diff --git a/backend/core/templates/payment.html b/backend/core/templates/payment.html index e0dca02..3e5d40e 100644 --- a/backend/core/templates/payment.html +++ b/backend/core/templates/payment.html @@ -1,41 +1,60 @@ {% extends 'base.html' %} {% block content %}
-
-

Payment

-

Please enter your demo code to proceed to document.

- - {% if success %} -

{{ success }}

- {% endif %} -
- {% csrf_token %} -
- +

Payment

+ + +
+ + +
+ + +
+

Enter your demo code to access the document:

+ + {% csrf_token %} +
+ + +
+
+ Enter Code + +

+ {% if error %} +

{{ error }}

+ {% endif %} + +
+ + +
-{% endblock %} \ No newline at end of file + + + +{% endblock %} diff --git a/backend/core/urls.py b/backend/core/urls.py index 0928b57..3b33f77 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -11,11 +11,11 @@ urlpatterns = [ # url document/ recieves a parameter named 'uuid' and passes it to the view path('document//', v.document, name='document'), path('preview//', v.template_preview, name='template_preview'), - path("payment/", v.payment_page, name="payment_page"), + path("payment/", v.PaymentView.as_view(), name="payment_page"), path('pdf//', v.pdf_view, name='pdf_view'), path('api/validate_form_fields/', v.validate_form_fields, name='validate_form_fields'), path('no_confidential_data/', v.no_confidential_data, name='no_confidential_data'), - path('validate_code/', v.validate_code, name='validate_code'), + path('validate_code/', v.PaymentView.validate_code, name='validate_code'), path('terms-and-conditions/', v.terms_and_conditions, name='terms_and_conditions'), path('refund-policy/', v.refund_policy, name='refund_policy'), path('privacy-policy/', v.privacy_policy, name='privacy_policy'), diff --git a/backend/core/views.py b/backend/core/views.py index 6527c08..4fad812 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -6,7 +6,7 @@ import time from django.shortcuts import render, redirect , get_object_or_404 from .forms import OrganizationForm, ContactForm -from .models import Organization,Document, DocumentTemplate, DemoCode, DocumentRiskControl, Risk, Control +from .models import Organization,Document, DocumentTemplate, DemoCode, PaymentRecord, DocumentRiskControl, Risk, Control from backend.accounts.utils import send_confirmation_email, send_document_email, send_documet_to_expert, send_document_to_reviewer from django.contrib.admin.views.decorators import staff_member_required from .utils import generate_pdf, generate_risk_graph, generate_residual_risk_graph @@ -22,7 +22,11 @@ from django.template.loader import render_to_string from django.views.decorators.csrf import csrf_exempt from backend.accounts.models import ExpertAnalysisEmails from django.core.mail import send_mail - +from django.views import View +import re +from decimal import Decimal +from django.urls import reverse +from urllib.parse import urlencode # @login_required # def index(request): @@ -118,57 +122,141 @@ def pdf_view(request, document_id): document = get_object_or_404(Document, id=document_id) return generate_pdf(document) -def payment_page(request): - error = None - email = request.GET.get('email') - if request.method == 'POST': - import re +class PaymentView(View): + + template_name = 'payment.html' + + def get(self,request): + email = request.GET.get('email','').strip() + status_view = request.GET.get('status') + + pdl_client = settings.PADDLE_CLIENT_ID + pdl_price = settings.PADDLE_PRICE_ID + + if status_view == 'review': + document = Document.objects.filter(organization__email__iexact=email).first() + return render(request, 'payment_review.html', {'email': email, 'document': document}) + + if status_view == 'expert': + document = Document.objects.filter(organization__email__iexact=email).first() + return render(request, 'payment_expert_analysis.html', {'email': email, 'document': document}) + + return render(request, self.template_name, { + 'email': email, + 'pdl_client': pdl_client, + 'pdl_price': pdl_price}) + + def post(self, request): + content_type = request.META.get('CONTENT_TYPE', '') + if 'application/json' in content_type: + try: + payload = json.loads(request.body) + return self._handle_paddle_payment(payload) + except json.JSONDecodeError: + return JsonResponse({'status': 'error', 'message': 'Invalid JSON'}, status=400) + else: + return self._handle_demo_code(request) + + + def _handle_demo_code(self, request): + email = request.GET.get('email', '').strip() + error = None code = re.sub(r'\s+', '', request.POST.get('code', '')).upper()[:10] try: payment_code = DemoCode.objects.get(code=code) if payment_code.used: - error = "CODE INVALID" - else: - org = Organization.objects.filter(email__iexact=email).first() - payment_code.used = True - payment_code.used_at = timezone.now() - payment_code.company = org - payment_code.save() - document = Document.objects.get(organization = org) - url = f"{site_domain}/pdf/{document.id}/" - expert_emails_qs = ExpertAnalysisEmails.objects.values_list('email', flat=True).distinct() + error = "Invalid demo code" + return render(request, self.template_name, {'email': email, 'error': error}) - # If document is incomplete, notify reviewers - if document.status == Document.STATUS_INCOMPLETE: - if expert_emails_qs: - for email_addr in expert_emails_qs: - try: - send_document_to_reviewer(email_addr, document) - except Exception: - logger.exception("Failed to send incomplete document email to %s", email_addr) - return render(request, 'payment_review.html', {'email': email, 'document': document}) + org = Organization.objects.filter(email__iexact=email).first() + payment_code.used = True + payment_code.used_at = timezone.now() + payment_code.company = org + payment_code.save() - # If organization requested expert analysis, mark waiting and notify experts - if org.expert_analysis: - document.status = Document.STATUS_WAITING - document.save(update_fields=['status', 'modified_at']) - expert_emails = [e for e in expert_emails_qs if e] - if expert_emails: - for email_addr in expert_emails: - try: - send_documet_to_expert(email_addr, document) - except Exception: - logger.exception("Failed to send expert analysis email to %s", email_addr) - else: - logger.info("No expert emails configured; skipping expert notifications.") - return render(request, 'payment_expert_analysis.html', {'email': email, 'document': document}) - send_document_email(email, url, document) - document.status = Document.STATUS_DONE - document.save(update_fields=['status', 'modified_at']) - return redirect(url) + print(org, email) + document = Document.objects.get(organization=org) + return self._handle_document_flow(org, document, email) + except DemoCode.DoesNotExist: - error = "❌ Invalid code" - return render(request, 'payment.html', {'error': error}) + error = "Invalid demo code" + return render(request, self.template_name, {'email': email, 'error': error}) + + def _handle_paddle_payment(self, data): + try: + customer_email = data.get('customer_email') + transaction_id = data.get('transaction_id') + amount = Decimal(data.get('amount', 0)) + currency = data.get('currency', 'EUR') + + org = Organization.objects.filter(email__iexact=customer_email).first() + if not org: + return JsonResponse({'status': 'error', 'message': 'Organization not found'}, status=404) + + PaymentRecord.objects.create( + company=org, + amount=amount, + currency=currency, + transaction_id=transaction_id, + payment_date=timezone.now() + ) + document = Document.objects.get(organization=org) + return self._handle_document_flow(org, document, customer_email, json_response=True) + + except Exception as e: + logger.exception("Error saving Paddle payment: %s", e) + return JsonResponse({'status': 'error', 'message': str(e)}, status=500) + + def validate_code(request): + if request.method == "POST": + try: + data = json.loads(request.body) + code = data.get("code", "").strip().upper() + valid = DemoCode.objects.filter(code=code, used=False).exists() + time.sleep(1) + return JsonResponse({"valid": valid}) + except Exception: + return JsonResponse({"valid": False}) + return JsonResponse({"valid": False}) + + def _handle_document_flow(self, org, document, email, json_response=False): + site_domain = settings.SITE_DOMAIN + url = f"{site_domain}/pdf/{document.id}/" + expert_emails_qs = ExpertAnalysisEmails.objects.values_list('email', flat=True).distinct() + + if document.status == Document.STATUS_INCOMPLETE: + review_url = f"{reverse('core:payment_page')}?{urlencode({'email': email, 'status': 'review'})}" + for email_addr in expert_emails_qs: + try: + send_document_to_reviewer(email_addr, document) + except Exception: + logger.exception("Failed to send incomplete document email to %s", email_addr) + if json_response: + return JsonResponse({'status': 'review', 'redirect_url': review_url}) + return render(self.request, 'payment_review.html', {'email': email, 'document': document}) + + if org.expert_analysis: + document.status = Document.STATUS_WAITING + document.save(update_fields=['status', 'modified_at']) + expert_url = f"{reverse('core:payment_page')}?{urlencode({'email': email, 'status': 'expert'})}" + expert_emails = [e for e in expert_emails_qs if e] + for email_addr in expert_emails: + try: + send_documet_to_expert(email_addr, document) + except Exception: + logger.exception("Failed to send expert analysis email to %s", email_addr) + if json_response: + return JsonResponse({'status': 'expert', 'redirect_url': expert_url}) + return render(self.request, 'payment_expert_analysis.html', {'email': email, 'document': document}) + + send_document_email(email, url, document) + document.status = Document.STATUS_DONE + document.save(update_fields=['status', 'modified_at']) + + if json_response: + return JsonResponse({'status': 'ok', 'redirect_url': url}) + return redirect(url) + def no_confidential_data(request): return render(request, "no_confidential_data.html") @@ -217,16 +305,3 @@ def demo_codes_pdf_view(request): response['Content-Disposition'] = f'inline; filename=demo_codes_{timezone.now().strftime("%Y%m%d_%H%M%S")}.pdf' return response -@csrf_exempt -def validate_code(request): - if request.method == "POST": - try: - data = json.loads(request.body) - code = data.get("code", "").strip().upper() - from .models import DemoCode - valid = DemoCode.objects.filter(code=code, used=False).exists() - time.sleep(3) - return JsonResponse({"valid": valid}) - except Exception: - return JsonResponse({"valid": False}) - return JsonResponse({"valid": False}) \ No newline at end of file diff --git a/backend/settings.py b/backend/settings.py index 3b533d3..93856b4 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -22,6 +22,10 @@ load_dotenv() #API key OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +# Paddle Keys +PADDLE_CLIENT_ID = config('PADDLE_CLIENT_ID','') +PADDLE_PRICE_ID = config('PADDLE_PRICE_ID','') + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -172,6 +176,7 @@ CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' CELERY_TIMEZONE = 'UTC' + SECURE_SSL_REDIRECT = config('SECURE_SSL_REDIRECT', default=False, cast=bool) SESSION_COOKIE_SECURE = config('SESSION_COOKIE_SECURE', default=False, cast=bool) CSRF_COOKIE_SECURE = config('CSRF_COOKIE_SECURE', default=False, cast=bool)