5 Commits

Author SHA1 Message Date
de35d553c5 Merge branch 'paddle' into 'master'
Paddle

See merge request kbr4/riskletpy!69
2025-10-20 17:58:27 +00:00
de9f072044 Merge branch 'paddle' of gitlab.com:kbr4/riskletpy into paddle 2025-10-20 19:56:24 +02:00
7f9483c2bf added paddle 2025-10-20 19:52:11 +02:00
b583584a7d Added paddle 2025-10-20 19:50:39 +02:00
6093b24734 Merge branch 'network-infrastructure' into 'master'
Primary network infrastructure limitiran samo na 1 izbor

See merge request kbr4/riskletpy!67
2025-10-08 16:41:00 +00:00
9 changed files with 312 additions and 151 deletions

View File

@@ -35,7 +35,7 @@ class EmailTests(TestCase):
) )
self.document = Document.objects.create(organization=self.organization) 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): def test_send_confirmation_email(self, mock_send_mail):
confirmation = EmailConfirmation.objects.create(email=self.email, uuid=uuid.uuid4(), created_at=now()) confirmation = EmailConfirmation.objects.create(email=self.email, uuid=uuid.uuid4(), created_at=now())
send_confirmation_email(self.email) send_confirmation_email(self.email)
@@ -44,7 +44,7 @@ class EmailTests(TestCase):
self.assertIsNotNone(confirmation.uuid) self.assertIsNotNone(confirmation.uuid)
self.assertEqual(mock_send_mail.call_count, 1) 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): def test_send_payment_email(self, mock_send_mail):
send_payment_email(self.email) send_payment_email(self.email)
self.assertEqual(mock_send_mail.call_count, 1) self.assertEqual(mock_send_mail.call_count, 1)

View File

@@ -1,5 +1,5 @@
from django.contrib import admin 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.urls import reverse, path
from django.utils.html import format_html 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 from .utils import generate_demo_code, get_top_risk, get_controls_for_risk, generate_key_findings, generate_recommendations
@@ -95,12 +95,12 @@ class DocumentAdmin(admin.ModelAdmin):
search_fields = ['organization__name', 'organization__email'] search_fields = ['organization__name', 'organization__email']
readonly_fields = ( readonly_fields = (
'created_at', 'modified_at', 'regen_note_action', 'created_at', 'modified_at', 'regen_note_action',
'regen_document_action', 'regen_top_risks_action','update_risk_content_action', 'regen_controls_action', 'regen_keyfindings_action', 'regen_recommendations_action', 'regen_document_action', 'regen_top_risks_action', 'regen_controls_action', 'regen_keyfindings_action', 'regen_recommendations_action',
) )
fieldsets = ( fieldsets = (
('Organization & Risks', { ('Organization & Risks', {
'fields': ('organization', 'regen_note_action', 'regen_document_action', 'organization_risks','update_risk_content_action', 'risk_explanations', 'regen_top_risks_action') 'fields': ('organization', 'regen_note_action', 'regen_document_action', 'organization_risks', 'risk_explanations', 'regen_top_risks_action')
}), }),
('Key Findings', { ('Key Findings', {
'fields': ('key_findings', 'regen_keyfindings_action') 'fields': ('key_findings', 'regen_keyfindings_action')
@@ -157,16 +157,6 @@ class DocumentAdmin(admin.ModelAdmin):
) )
regen_document_action.short_description = '' regen_document_action.short_description = ''
def update_risk_content_action(self, obj):
return format_html(
'<div class="ai-callout ai-callout-info">'
'🔁 <strong>Update existing content:</strong> synchronize risks, controls, and explanations without regenerating everything from scratch.'
'</div>'
'<br/>'
'<button type="submit" name="_update_risk_content" class="button">🌀 Update Risk Content</button>'
)
update_risk_content_action.short_description = ''
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
org_risks = form.cleaned_data.get('organization_risks') org_risks = form.cleaned_data.get('organization_risks')
@@ -308,7 +298,7 @@ class DocumentAdmin(admin.ModelAdmin):
return False return False
def changeform_view(self, request, object_id=None, form_url='', extra_context=None): def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
if request.method == 'POST' and any(k in request.POST for k in ("_regen_controls", "_regen_key_findings", "_regen_recommendations", "_regen_top_risks", "_regen_document", "_update_risk_content")): if request.method == 'POST' and any(k in request.POST for k in ("_regen_controls", "_regen_key_findings", "_regen_recommendations", "_regen_top_risks", "_regen_document")):
obj = self.get_object(request, object_id) obj = self.get_object(request, object_id)
if obj is None: if obj is None:
return super().changeform_view(request, object_id, form_url, extra_context) return super().changeform_view(request, object_id, form_url, extra_context)
@@ -322,13 +312,6 @@ class DocumentAdmin(admin.ModelAdmin):
self.message_user(request, "Top risks regenerated and risk segment updated.") self.message_user(request, "Top risks regenerated and risk segment updated.")
else: else:
self.message_user(request, "Top risks could not be generated.", level=messages.WARNING) self.message_user(request, "Top risks could not be generated.", level=messages.WARNING)
elif "_update_risk_content" in request.POST:
try:
self._update_risk_content(obj)
self.message_user(request, "Risk content successfully synchronized (kept existing explanations and controls).")
except Exception as e:
logger.exception("Update risk content failed")
self.message_user(request, f"Risk content update failed: {e}", level=messages.ERROR)
elif "_regen_controls" in request.POST: elif "_regen_controls" in request.POST:
self._regen_controls(obj) self._regen_controls(obj)
self.message_user(request, "Risks and controls regenerated successfully.") self.message_user(request, "Risks and controls regenerated successfully.")
@@ -420,40 +403,6 @@ class DocumentAdmin(admin.ModelAdmin):
except Exception: except Exception:
logger.exception("Failed to refresh segments from current mappings for document %s", getattr(obj, 'pk', None)) logger.exception("Failed to refresh segments from current mappings for document %s", getattr(obj, 'pk', None))
def _update_risk_content(self, obj):
"""
Synchronizes the document risks and explanations with the organization's current risks.
Keeps existing explanations for unchanged risks, adds blanks for new ones,
and removes explanations + controls for deleted risks.
Does NOT auto-generate new controls for added risks.
"""
org = obj.organization
if not org:
raise ValueError("Organization not set for this document.")
existing_explanations = obj.risk_explanations or {}
existing_risk_ids = set(map(int, existing_explanations.keys()))
actual_risk_ids = set(org.risks.values_list('pk', flat=True))
removed_risks = existing_risk_ids - actual_risk_ids
added_risks = actual_risk_ids - existing_risk_ids
new_explanations = {}
for risk_id in actual_risk_ids & existing_risk_ids:
new_explanations[str(risk_id)] = existing_explanations.get(str(risk_id), "")
for risk_id in added_risks:
new_explanations[str(risk_id)] = ""
obj.risk_explanations = new_explanations
obj.save(update_fields=["risk_explanations", "modified_at"])
if removed_risks:
control = DocumentRiskControl.objects.filter(document=obj, risk_id__in=removed_risks)
for c in control:
c.risk = None
self._refresh_segments_from_current_mappings(obj)
class DocumentTemplateAdmin(admin.ModelAdmin): class DocumentTemplateAdmin(admin.ModelAdmin):
list_display = ['name', 'created_at', 'updated_at', 'preview_button'] list_display = ['name', 'created_at', 'updated_at', 'preview_button']
@@ -511,7 +460,13 @@ class DemoCodeAdmin(admin.ModelAdmin):
form = GenerateCodesForm() form = GenerateCodesForm()
return render(request, 'admin/generate_codes.html', {'form': form}) 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(Document, DocumentAdmin)
admin.site.register(Organization, OrganizationAdmin) admin.site.register(Organization, OrganizationAdmin)
admin.site.register(Risk ,RiskAdmin) admin.site.register(Risk ,RiskAdmin)

View File

@@ -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')),
],
),
]

View File

@@ -200,3 +200,14 @@ class DemoCode(models.Model):
def __str__(self): def __str__(self):
return (f"{self.code} - {'Used' if self.used else 'Available'}") 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}"

View File

@@ -24,7 +24,6 @@
.form-row.field-regen_keyfindings_action label, .form-row.field-regen_keyfindings_action label,
.form-row.field-regen_document_action label, .form-row.field-regen_document_action label,
.form-row.field-regen_note_action label, .form-row.field-regen_note_action label,
.form-row.field-update_risk_content_action label,
.form-row.field-regen_recommendations_action label { display: none; } .form-row.field-regen_recommendations_action label { display: none; }
.form-row.field-regen_top_risks_action label { display: none; } .form-row.field-regen_top_risks_action label { display: none; }
.form-row.field-regen_controls_action { display: none !important; } .form-row.field-regen_controls_action { display: none !important; }

View File

@@ -1,41 +1,60 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<section class="py-24 bg-gradient-to-br from-teal-100 to-primary flex items-center justify-center"> <section class="py-24 bg-gradient-to-br from-teal-100 to-primary flex items-center justify-center">
<div class="max-w-md w-full mx-auto text-center shadow-2xl border border-accent rounded-2xl p-4 bg-white/90 backdrop-blur"> <div class="max-w-md w-full mx-auto shadow-2xl border border-accent rounded-2xl p-4 bg-white/90 backdrop-blur">
<h2 class="text-3xl font-extrabold mb-6 text-accent">Payment</h2> <h2 class="text-3xl font-extrabold mb-6 text-accent text-center">Payment</h2>
<p class="text-lg text-gray-700 mb-6">Please enter your demo code to proceed to document.</p>
{% if success %} <!-- Tabs -->
<p class="text-green-600 font-semibold mb-4">{{ success }}</p> <div class="flex justify-center mb-6 border-b border-accent">
{% endif %} <button id="tab-demo" class="px-4 py-2 text-accent font-semibold border-b-2 border-accent">Demo Code</button>
<form method="post" class="space-y-6"> <button id="tab-paddle" class="px-4 py-2 text-gray-500 hover:text-accent font-semibold">Paddle</button>
{% csrf_token %} </div>
<div class="relative flex items-center">
<input <!-- Demo Code Tab -->
type="text" <div id="demo-tab-content">
id="code-input" <p class="text-lg text-gray-700 mb-4">Enter your demo code to access the document:</p>
name="code" <form method="post" class="space-y-6">
maxlength="10" {% csrf_token %}
class="w-full px-4 py-3 border-2 border-accent rounded-lg focus:outline-none focus:ring-2 focus:ring-accent text-lg tracking-widest text-center font-mono mb-2" <div class="relative flex items-center">
placeholder="Enter your code" <input
required type="text"
autocomplete="off" id="code-input"
name="code"
maxlength="10"
class="w-full px-4 py-3 border-2 border-accent rounded-lg focus:outline-none focus:ring-2 focus:ring-accent text-lg tracking-widest text-center font-mono mb-2"
placeholder="Enter your code"
required
autocomplete="off"
>
<span id="code-status" class="absolute right-3 top-1/2 -translate-y-1/2 text-2xl"></span>
</div>
<button
type="submit"
class="w-full bg-accent text-primary hover:bg-yellow-400 font-bold py-3 px-8 rounded-lg shadow-lg text-lg transition-all duration-200 ease-in-out transform hover:scale-105"
id="submit-btn"
disabled
> >
<span id="code-status" class="absolute right-3 top-1/2 -translate-y-1/2 text-2xl"></span> Enter Code
</div> </button>
<p id="code-error" class="mt-2 font-semibold text-lg"></p>
{% if error %}
<p id="backend-error" class="text-red-600 mt-2 font-semibold text-lg">{{ error }}</p>
{% endif %}
</form>
</div>
<!-- Paddle Tab -->
<div id="paddle-tab-content" class="hidden text-center">
<p class="text-lg text-gray-700 mb-4">Pay with Paddle to access the document:</p>
<button <button
type="submit" id="paddle-button"
class="w-full bg-accent text-primary hover:bg-yellow-400 font-bold py-3 px-8 rounded-lg shadow-lg text-lg transition-all duration-200 ease-in-out transform hover:scale-105" class="mt-4 bg-green-600 text-white font-bold py-3 px-8 rounded-lg shadow-lg text-lg transition-all duration-200 ease-in-out transform hover:scale-105"
id="submit-btn" data-client="{{ pdl_client }}"
disabled data-price="{{ pdl_price }}"
> >
Enter Code Buy Now
</button> </button>
</form> </div>
<p id="code-error" class="mt-6 font-semibold text-lg"></p>
{% if error %}
<p id="backend-error" class="text-red-600 mt-6 font-semibold text-lg">{{ error }}</p>
{% endif %}
</div> </div>
</section> </section>
<script> <script>
@@ -101,4 +120,76 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
</script> </script>
<script>
const demoTabBtn = document.getElementById('tab-demo');
const paddleTabBtn = document.getElementById('tab-paddle');
const demoContent = document.getElementById('demo-tab-content');
const paddleContent = document.getElementById('paddle-tab-content');
demoTabBtn.addEventListener('click', () => {
demoContent.classList.remove('hidden');
paddleContent.classList.add('hidden');
demoTabBtn.classList.add('border-b-2', 'border-accent', 'text-accent');
paddleTabBtn.classList.remove('border-b-2', 'border-accent', 'text-accent');
paddleTabBtn.classList.add('text-gray-500');
});
paddleTabBtn.addEventListener('click', () => {
paddleContent.classList.remove('hidden');
demoContent.classList.add('hidden');
paddleTabBtn.classList.add('border-b-2', 'border-accent', 'text-accent');
demoTabBtn.classList.remove('border-b-2', 'border-accent', 'text-accent');
demoTabBtn.classList.add('text-gray-500');
});
</script>
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
<script>
const paddleBtn = document.getElementById('paddle-button');
const email = "{{ email|escapejs }}";
const client = paddleBtn.dataset.client;
const price = paddleBtn.dataset.price;
Paddle.Environment.set('sandbox');
Paddle.Initialize({
token: client,
eventCallback: function(event) {
if (event.name === 'checkout.completed') {
handlePaymentSuccess(event.data);
}
}
});
paddleBtn.addEventListener('click', function() {
Paddle.Checkout.open({
customer: { email: email },
items: [{ priceId: price, quantity: 1 }]
});
});
function handlePaymentSuccess(data) {
const payload = {
transaction_id: data.transaction_id,
customer_email: data.customer.email,
amount: data.totals.total,
currency: data.currency_code
};
fetch("{% url 'core:payment_page' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}',
},
body: JSON.stringify(payload)
})
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(result => {
if (result.redirect_url) window.location.href = result.redirect_url;
})
.catch(error => console.error('Fetch error:', error));
}
</script>
{% endblock %} {% endblock %}

View File

@@ -11,11 +11,11 @@ urlpatterns = [
# url document/ recieves a parameter named 'uuid' and passes it to the view # url document/ recieves a parameter named 'uuid' and passes it to the view
path('document/<uuid:document_id>/', v.document, name='document'), path('document/<uuid:document_id>/', v.document, name='document'),
path('preview/<str:name>/', v.template_preview, name='template_preview'), path('preview/<str:name>/', 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/<uuid:document_id>/', v.pdf_view, name='pdf_view'), path('pdf/<uuid:document_id>/', v.pdf_view, name='pdf_view'),
path('api/validate_form_fields/', v.validate_form_fields, name='validate_form_fields'), 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('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('terms-and-conditions/', v.terms_and_conditions, name='terms_and_conditions'),
path('refund-policy/', v.refund_policy, name='refund_policy'), path('refund-policy/', v.refund_policy, name='refund_policy'),
path('privacy-policy/', v.privacy_policy, name='privacy_policy'), path('privacy-policy/', v.privacy_policy, name='privacy_policy'),

View File

@@ -6,7 +6,7 @@ import time
from django.shortcuts import render, redirect , get_object_or_404 from django.shortcuts import render, redirect , get_object_or_404
from .forms import OrganizationForm, ContactForm 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 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 django.contrib.admin.views.decorators import staff_member_required
from .utils import generate_pdf, generate_risk_graph, generate_residual_risk_graph 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 django.views.decorators.csrf import csrf_exempt
from backend.accounts.models import ExpertAnalysisEmails from backend.accounts.models import ExpertAnalysisEmails
from django.core.mail import send_mail 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 # @login_required
# def index(request): # def index(request):
@@ -118,57 +122,141 @@ def pdf_view(request, document_id):
document = get_object_or_404(Document, id=document_id) document = get_object_or_404(Document, id=document_id)
return generate_pdf(document) return generate_pdf(document)
def payment_page(request): class PaymentView(View):
error = None
email = request.GET.get('email') template_name = 'payment.html'
if request.method == 'POST':
import re 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] code = re.sub(r'\s+', '', request.POST.get('code', '')).upper()[:10]
try: try:
payment_code = DemoCode.objects.get(code=code) payment_code = DemoCode.objects.get(code=code)
if payment_code.used: if payment_code.used:
error = "CODE INVALID" error = "Invalid demo code"
else: return render(request, self.template_name, {'email': email, 'error': error})
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()
# If document is incomplete, notify reviewers org = Organization.objects.filter(email__iexact=email).first()
if document.status == Document.STATUS_INCOMPLETE: payment_code.used = True
if expert_emails_qs: payment_code.used_at = timezone.now()
for email_addr in expert_emails_qs: payment_code.company = org
try: payment_code.save()
send_document_to_reviewer(email_addr, document)
except Exception: print(org, email)
logger.exception("Failed to send incomplete document email to %s", email_addr) document = Document.objects.get(organization=org)
return render(request, 'payment_review.html', {'email': email, 'document': document}) return self._handle_document_flow(org, document, email)
# 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)
except DemoCode.DoesNotExist: except DemoCode.DoesNotExist:
error = "Invalid code" error = "Invalid demo code"
return render(request, 'payment.html', {'error': error}) 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): def no_confidential_data(request):
return render(request, "no_confidential_data.html") 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' response['Content-Disposition'] = f'inline; filename=demo_codes_{timezone.now().strftime("%Y%m%d_%H%M%S")}.pdf'
return response 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})

View File

@@ -22,6 +22,10 @@ load_dotenv()
#API key #API key
OPENAI_API_KEY = os.getenv("OPENAI_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'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@@ -172,6 +176,7 @@ CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json' CELERY_TASK_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC' CELERY_TIMEZONE = 'UTC'
SECURE_SSL_REDIRECT = config('SECURE_SSL_REDIRECT', default=False, cast=bool) SECURE_SSL_REDIRECT = config('SECURE_SSL_REDIRECT', default=False, cast=bool)
SESSION_COOKIE_SECURE = config('SESSION_COOKIE_SECURE', 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) CSRF_COOKIE_SECURE = config('CSRF_COOKIE_SECURE', default=False, cast=bool)