Compare commits

..

No commits in common. "e277ec5269d3289e8ef0428998c4e3b73f22425c" and "863aeab25c7253dd70f5f6b802db3ce85594d411" have entirely different histories.

12 changed files with 174 additions and 281 deletions

View File

@ -2133,8 +2133,7 @@ class VatRateForm(forms.ModelForm):
label="VAT Rate", label="VAT Rate",
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
validators=[vat_rate_validator], validators=[vat_rate_validator]
help_text=_("VAT rate as decimal between 0 and 1 (e.g., 0.2 for 20%)"),
) )
class Meta: class Meta:

View File

@ -9,8 +9,6 @@ def check_create_coa_accounts(task):
""" """
Hook to verify account creation and handle failures Hook to verify account creation and handle failures
""" """
from .models import Dealer
if task.success: if task.success:
logger.info("Account creation task completed successfully") logger.info("Account creation task completed successfully")
return return
@ -18,15 +16,14 @@ def check_create_coa_accounts(task):
logger.warning("Account creation task failed, checking status...") logger.warning("Account creation task failed, checking status...")
try: try:
dealer_id = task.kwargs.get('dealer_id',None) dealer_id = task.kwargs.get('dealer_id')
coa_slug = task.kwargs.get('coa_slug', None) coa_slug = task.kwargs.get('coa_slug', None)
logger.info(f"Checking accounts for dealer {dealer_id}")
logger.info(f"COA slug: {coa_slug}")
if not dealer_id: if not dealer_id:
logger.error("No dealer_id in task kwargs") logger.error("No dealer_id in task kwargs")
return return
instance = Dealer.objects.get(id=dealer_id) from .models import Dealer
instance = Dealer.objects.select_related('entity').get(id=dealer_id)
entity = instance.entity entity = instance.entity
if not entity: if not entity:
@ -37,11 +34,11 @@ def check_create_coa_accounts(task):
try: try:
coa = entity.get_coa_model_qs().get(slug=coa_slug) coa = entity.get_coa_model_qs().get(slug=coa_slug)
except Exception as e: except Exception as e:
logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}") logger.error(f"COA with slug {coa_slug} not found for entity {entity.id}: {e}")
else: else:
coa = entity.get_default_coa() coa = entity.get_default_coa()
if not coa: if not coa:
logger.error(f"No COA for entity {entity.pk}") logger.error(f"No COA for entity {entity.id}")
return return
# Check which accounts are missing and create them # Check which accounts are missing and create them
@ -49,7 +46,7 @@ def check_create_coa_accounts(task):
missing_accounts = [] missing_accounts = []
for account_data in get_accounts_data(): for account_data in get_accounts_data():
if not entity.get_all_accounts().filter(coa_model=coa,code=account_data["code"]).exists(): if not entity.get_all_accounts().filter(code=account_data["code"]).exists():
missing_accounts.append(account_data) missing_accounts.append(account_data)
logger.info(f"Missing account: {account_data['code']}") logger.info(f"Missing account: {account_data['code']}")

View File

@ -658,12 +658,16 @@ class Car(Base):
CarMake, CarMake,
models.DO_NOTHING, models.DO_NOTHING,
db_column="id_car_make", db_column="id_car_make",
null=True,
blank=True,
verbose_name=_("Make"), verbose_name=_("Make"),
) )
id_car_model = models.ForeignKey( id_car_model = models.ForeignKey(
CarModel, CarModel,
models.DO_NOTHING, models.DO_NOTHING,
db_column="id_car_model", db_column="id_car_model",
null=True,
blank=True,
verbose_name=_("Model"), verbose_name=_("Model"),
) )
year = models.IntegerField(verbose_name=_("Year")) year = models.IntegerField(verbose_name=_("Year"))

View File

@ -138,36 +138,50 @@ def create_car_location(sender, instance, created, **kwargs):
@receiver(post_save, sender=models.Dealer) @receiver(post_save, sender=models.Dealer)
def create_ledger_entity(sender, instance, created, **kwargs): def create_ledger_entity(sender, instance, created, **kwargs):
if not created: if created:
return try:
# Use transaction to ensure atomicity
with transaction.atomic():
entity_name = instance.user.dealer.name
entity = EntityModel.create_entity(
name=entity_name,
admin=instance.user,
use_accrual_method=True,
fy_start_month=1,
)
try: if entity:
with transaction.atomic(): instance.entity = entity
# Create entity instance.save(update_fields=['entity'])
entity = models.EntityModel.create_entity(
name=instance.user.dealer.name,
admin=instance.user,
use_accrual_method=True,
fy_start_month=1,
)
if not entity:
raise Exception("Entity creation failed")
instance.entity = entity # Create COA synchronously first
instance.save(update_fields=['entity']) coa = entity.create_chart_of_accounts(
assign_as_default=True, commit=True,
coa_name=_(f"{entity_name}-COA")
)
# Create default COA if coa:
entity.create_chart_of_accounts( # Create essential UOMs synchronously
assign_as_default=True, for u in models.UnitOfMeasure.choices:
commit=True, entity.create_uom(name=u[1], unit_abbr=u[0])
coa_name=f"{entity.name}-COA"
# Schedule async task after successful synchronous operations
async_task(
func="inventory.tasks.create_coa_accounts",
dealer_id=instance.id, # Pass ID instead of object
hook="inventory.hooks.check_create_coa_accounts",
ack_failure=True, # Ensure task failures are acknowledged
sync=False # Explicitly set to async
) )
logger.info(f"✅ Setup complete for dealer {instance.id}: entity & COA ready.") except Exception as e:
logger.error(f"Failed to create ledger entity for dealer {instance.id}: {e}")
except Exception as e: # Schedule retry task
logger.error(f"💥 Failed setup for dealer {instance.id}: {e}") async_task(
# Optional: schedule retry or alert func="inventory.tasks.retry_entity_creation",
dealer_id=instance.id,
retry_count=0
)
# Create Entity # Create Entity
# @receiver(post_save, sender=models.Dealer) # @receiver(post_save, sender=models.Dealer)
# def create_ledger_entity(sender, instance, created, **kwargs): # def create_ledger_entity(sender, instance, created, **kwargs):
@ -1392,7 +1406,6 @@ def handle_user_registration(sender, instance, created, **kwargs):
if instance.is_created: if instance.is_created:
logger.info(f"User account created: {instance.email}, sending email") logger.info(f"User account created: {instance.email}, sending email")
# instance.create_account()
send_email( send_email(
settings.DEFAULT_FROM_EMAIL, settings.DEFAULT_FROM_EMAIL,
instance.email, instance.email,
@ -1417,31 +1430,15 @@ def handle_user_registration(sender, instance, created, **kwargs):
@receiver(post_save, sender=ChartOfAccountModel) @receiver(post_save, sender=ChartOfAccountModel)
def handle_chart_of_account(sender, instance, created, **kwargs): def handle_chart_of_account(sender, instance, created, **kwargs):
if created: if created:
entity = instance.entity
dealer = instance.entity.admin.dealer
# Create UOMs (minimal, no logging per item)
if not entity.get_uom_all():
for code, name in models.UnitOfMeasure.choices:
entity.create_uom(name=name, unit_abbr=code)
try: try:
# Schedule async account creation AFTER commit dealer = instance.entity.dealers.first()
transaction.on_commit( async_task(
lambda: async_task( func="inventory.tasks.create_coa_accounts",
"inventory.tasks.create_coa_accounts", dealer_id=dealer.pk, # Pass ID instead of object
dealer_id=dealer.pk, coa_slug=instance.slug,
coa_slug=instance.slug, hook="inventory.hooks.check_create_coa_accounts",
hook="inventory.hooks.check_create_coa_accounts", ack_failure=True, # Ensure task failures are acknowledged
ack_failure=True, sync=False # Explicitly set to async
)
) )
# async_task(
# func="inventory.tasks.create_coa_accounts",
# dealer_id=dealer.pk, # Pass ID instead of object
# coa_slug=instance.slug,
# hook="inventory.hooks.check_create_coa_accounts",
# ack_failure=True, # Ensure task failures are acknowledged
# sync=False # Explicitly set to async
# )
except Exception as e: except Exception as e:
logger.error(f"Error handling chart of account: {e}") logger.error(f"Error handling chart of account: {e}")

View File

@ -18,7 +18,7 @@ from django.core.files.base import ContentFile
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from allauth.account.models import EmailAddress from allauth.account.models import EmailAddress
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
# from .utils import get_accounts_data, create_account from .utils import get_accounts_data, create_account
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User, Group, Permission from django.contrib.auth.models import User, Group, Permission
@ -63,64 +63,70 @@ def create_settings(pk):
) )
def create_coa_accounts(dealer_id,**kwargs): def create_coa_accounts(dealer_id, **kwargs):
""" """
Idempotent: Creates only missing default accounts. Create COA accounts with retry logic and proper error handling
Safe to retry. Returns True if all done.
""" """
from .models import Dealer from .models import Dealer
from .utils import get_accounts_data, create_account from .utils import create_account, get_accounts_data
try:
dealer = Dealer.objects.get(pk=dealer_id)
entity = dealer.entity
coa_slug = kwargs.get('coa_slug', None)
if not entity:
logger.error(f"❌ No entity for dealer {dealer_id}")
return False
if coa_slug: max_retries = 3
try: retry_delay = 2 # seconds
coa = entity.get_coa_model_qs().get(slug=coa_slug) coa_slug = kwargs.get('coa_slug', None)
except Exception as e: logger.info(f"chart of account model slug {coa_slug}")
logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}") logger.info(f"Attempting to create accounts for dealer {dealer_id}")
for attempt in range(max_retries):
try:
logger.info(f"Attempt {attempt + 1} to create accounts for dealer {dealer_id}")
instance = Dealer.objects.get(pk=dealer_id)
entity = instance.entity
if not entity:
logger.error(f"No entity found for dealer {dealer_id}")
return False return False
else:
coa = entity.get_default_coa()
if not coa: if coa_slug:
logger.error(f"❌ No default COA for entity {entity.pk}") try:
return False coa = entity.get_coa_model_qs().get(slug=coa_slug)
logger.info(f"COA with slug {coa_slug} found for entity {entity.pk}")
except Exception as e:
logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}")
else:
coa = entity.get_default_coa()
logger.info(f"Default COA found for entity {entity.pk}")
# Get missing accounts if not coa:
existing_codes = set(entity.get_all_accounts().filter(coa_model=coa).values_list('code', flat=True)) logger.error(f"No COA found for entity {entity.pk}")
accounts_to_create = [ return False
acc for acc in get_accounts_data()
if acc["code"] not in existing_codes
]
if not accounts_to_create: logger.info("Creating default accounts")
logger.info("✅ All default accounts already exist.") accounts_created = 0
with transaction.atomic():
for account_data in get_accounts_data():
logger.info(f"Creating account: {account_data['code']}")
if create_account(entity, coa, account_data):
accounts_created += 1
logger.info(f"Successfully created {accounts_created} accounts")
return True return True
# Create missing ones except Exception as e:
logger.info(f"🔧 Creating {len(accounts_to_create)} missing accounts...") logger.error(f"Attempt {attempt + 1} failed: {e}")
success = True
for acc in accounts_to_create: if attempt < max_retries - 1:
if not create_account(entity, coa, acc): logger.info(f"Retrying in {retry_delay} seconds...")
logger.warning(f"⚠️ Failed to create account: {acc['code']}") time.sleep(retry_delay * (attempt + 1)) # Exponential backoff
success = False # don't fail task, just log else:
logger.error(f"All {max_retries} attempts failed for dealer {dealer_id}")
if success: # Schedule a cleanup or notification task
logger.info("✅ All missing accounts created successfully.") # async_task(
else: # "inventory.tasks.handle_account_creation_failure",
logger.warning("⚠️ Some accounts failed to create — check logs.") # dealer_id=dealer_id,
# error=str(e)
return success # Django-Q will mark as failed if False # )
return False
except Exception as e:
logger.error(f"💥 Task failed for dealer {dealer_id}: {e}")
raise # Let Django-Q handle retry if configured
def retry_entity_creation(dealer_id, retry_count=0): def retry_entity_creation(dealer_id, retry_count=0):
""" """
@ -155,7 +161,7 @@ def retry_entity_creation(dealer_id, retry_count=0):
# Now trigger account creation # Now trigger account creation
async_task( async_task(
"inventory.tasks.create_coa_accounts", "inventory.tasks.create_coa_accounts",
dealer_id=dealer_id, dealer_id=dealer_id
) )
except Exception as e: except Exception as e:

View File

@ -1332,7 +1332,6 @@ urlpatterns = [
path('<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/', views.ticket_detail, name='ticket_detail'), path('<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/', views.ticket_detail, name='ticket_detail'),
path('help_center/tickets/<int:ticket_id>/update/', views.ticket_update, name='ticket_update'), path('help_center/tickets/<int:ticket_id>/update/', views.ticket_update, name='ticket_update'),
# path('help_center/tickets/<int:ticket_id>/ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'), # path('help_center/tickets/<int:ticket_id>/ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'),
path('payment_results/', views.payment_result, name='payment_result'),
] ]

View File

@ -2388,36 +2388,45 @@ def get_accounts_data():
def create_account(entity, coa, account_data): def create_account(entity, coa, account_data):
logger.info(f"Creating account: {account_data['code']}") """
logger.info(f"COA: {coa}") Create account with proper validation and error handling
"""
try: try:
# Skip if exists # existing_account = AccountModel.objects.filter(coa_model=coa,code=account_data["code"])
if coa.get_coa_accounts().filter(code=account_data["code"]).exists(): existing_account = entity.get_all_accounts().filter(
coa_model=coa,
code=account_data["code"]
)
if existing_account:
logger.info(f"Account already exists: {account_data['code']}") logger.info(f"Account already exists: {account_data['code']}")
return True return True
logger.info(f"Account does not exist: {account_data['code']},creating...") logger.info(f"Creating account: {account_data['code']}")
account = coa.create_account( account = entity.create_account(
coa_model=coa,
code=account_data["code"], code=account_data["code"],
name=account_data["name"], name=account_data["name"],
role=account_data["role"], role=account_data["role"],
balance_type=account_data["balance_type"], balance_type=_(account_data["balance_type"]),
active=True, active=True,
) )
logger.info(f"Created account: {account}") logger.info(f"Successfully created account: {account_data['code']}")
if account: if account:
account.role_default = account_data.get("default", False) account.role_default = account_data["default"]
account.save(update_fields=['role_default']) account.save()
logger.info(f"Successfully created account: {account_data['code']}")
return True return True
except IntegrityError: except IntegrityError:
return True # Already created by race condition logger.warning(f"Account {account_data['code']} already exists (IntegrityError)")
return True
except Exception as e: except Exception as e:
logger.error(f"❌ Error creating {account_data['code']}: {e}") logger.error(f"Error creating account {account_data['code']}: {e}")
return False
return False return False
# def create_account(entity, coa, account_data): # def create_account(entity, coa, account_data):
# try: # try:
# account = entity.create_account( # account = entity.create_account(

View File

@ -9867,8 +9867,7 @@ def payment_callback(request, dealer_slug):
payment_status = request.GET.get("status") payment_status = request.GET.get("status")
logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}") logger.info(f"Received payment callback for dealer_slug: {dealer_slug}, payment_id: {payment_id}, status: {payment_status}")
order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW
if history.status == "paid":
return redirect('home')
if payment_status == "paid": if payment_status == "paid":
logger.info(f"Payment successful for transaction ID {payment_id}. Processing order completion.") logger.info(f"Payment successful for transaction ID {payment_id}. Processing order completion.")
@ -11764,9 +11763,3 @@ class CarDealershipSignUpView(CreateView):
response = super().form_valid(form) response = super().form_valid(form)
messages.success(self.request, _('Your request has been submitted. We will contact you soon.')) messages.success(self.request, _('Your request has been submitted. We will contact you soon.'))
return response return response
def payment_result(request):
s = request.GET.get("status")
if s == "success":
return render(request, 'plans/payment_success.html')
return render(request, 'plans/payment_failed.html')

View File

@ -50,6 +50,8 @@ body {
<span id="unread-count-text" class="fw-bold me-1">{{ total_count}}</span> {% trans "Notifications" %} <span id="unread-count-text" class="fw-bold me-1">{{ total_count}}</span> {% trans "Notifications" %}
</span> </span>
<a href="{% url 'mark_all_notifications_as_read' %}" <a href="{% url 'mark_all_notifications_as_read' %}"
hx-get="{% url 'mark_all_notifications_as_read' %}"
hx-swap="none"
class="btn btn-sm btn-outline-secondary rounded-pill fw-bold"> class="btn btn-sm btn-outline-secondary rounded-pill fw-bold">
<i class="fas fa-check-double me-2"></i>{% trans "Mark all read" %} <i class="fas fa-check-double me-2"></i>{% trans "Mark all read" %}
</a> </a>

View File

@ -1,50 +0,0 @@
{% extends "base.html" %}
{% load i18n static %}
{% block content %}
<main class="main">
<section class="py-4">
<div class="container d-flex justify-content-between align-items-center">
<h1 class="h3 mb-0 text-body-emphasis">{% trans "Payment Failed" %}</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item">
<a href="{% url 'home' %}">{% trans "Home" %}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Payment Failed" %}</li>
</ol>
</nav>
</div>
</section>
<section class="d-flex align-items-center justify-content-center py-5">
<div class="container text-center" style="max-width: 32rem;">
<div class="card p-5 shadow-sm border-0 rounded-4" data-aos="fade-up">
<div class="card-body p-0">
<div class="mb-4">
<i class="bi bi-x-circle-fill text-danger" style="font-size: 5rem;"></i>
</div>
<h2 class="mt-4 mb-3 text-body-emphasis fw-bold">{% trans "Payment Failed" %}</h2>
{% if message %}
<p class="lead text-body-secondary">{{ message }}.</p>
{% else %}
<p class="lead text-body-secondary">
{% trans "We couldn't process your payment. Please review your information and try again." %}
</p>
{% endif %}
<div class="d-grid gap-3 col-md-8 mx-auto mt-4">
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-danger btn-lg rounded-pill">
{% trans "Go Back and Try Again" %}
</a>
<a href="{% url 'home' %}" class="btn btn-link text-body-secondary text-decoration-none">
{% trans "Back to Home" %}
</a>
</div>
</div>
</div>
</div>
</section>
</main>
{% endblock content %}

View File

@ -1,50 +0,0 @@
{% extends "base.html" %}
{% load i18n static %}
{% block content %}
<main class="main">
<section class="py-4">
<div class="container d-flex justify-content-between align-items-center">
<h1 class="h3 mb-0 text-body-emphasis">{% trans "Payment Successful" %}</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item">
<a href="{% url 'home' %}">{% trans "Home" %}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{% trans "Success" %}</li>
</ol>
</nav>
</div>
</section>
<section class="d-flex align-items-center justify-content-center py-5">
<div class="container text-center" style="max-width: 32rem;">
<div class="card p-5 shadow-sm border-0 rounded-4" data-aos="fade-up">
<div class="card-body p-0">
<div class="mb-4">
<i class="bi bi-check-circle-fill text-success" style="font-size: 5rem;"></i>
</div>
<h2 class="mt-4 mb-3 text-body-emphasis fw-bold">{{ _("Thank You") }}!</h2>
<p class="lead text-body-secondary">
{{ _("Your payment was successful") }}. {{ _("Your order is being processed") }}.
</p>
<div class="d-grid gap-3 col-md-8 mx-auto mt-4">
{% if invoice %}
<a href="{% url 'invoice_preview_html' invoice.pk %}"
class="btn btn-primary btn-lg rounded-pill">
<i class="fas fa-eye me-2"></i> {{ _("View Invoice") }}
</a>
{% endif %}
<a href="{% url 'home' %}" class="btn btn-link text-body-secondary text-decoration-none">
<i class="fas fa-home me-2"></i> {{ _("Back to Home") }}
</a>
</div>
</div>
</div>
</div>
</section>
</main>
{% endblock content %}

View File

@ -8,7 +8,7 @@
{% block customCSS %} {% block customCSS %}
<link rel="stylesheet" <link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
integrity="sha512-X..." integrity="sha512-X... (Your actual integrity hash)"
crossorigin="anonymous" crossorigin="anonymous"
referrerpolicy="no-referrer" /> referrerpolicy="no-referrer" />
<style> <style>
@ -144,6 +144,7 @@
} }
.summary-box { .summary-box {
background-color: #ffffff;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 2rem; padding: 2rem;
@ -176,9 +177,9 @@
.fa-ul .fa-li { .fa-ul .fa-li {
left: -1em; left: -1em;
} }
</style> </style>
{% endblock customCSS %} {% endblock customCSS %}
{% block content %} {% block content %}
<div class="container py-5" id="pricing_container"> <div class="container py-5" id="pricing_container">
<h1 class="text-center mb-5 text-primary fw-bold">{{ _("Choose Your Plan") }}</h1> <h1 class="text-center mb-5 text-primary fw-bold">{{ _("Choose Your Plan") }}</h1>
@ -255,7 +256,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="step d-none row justify-content-center" id="step2"> <div class="step d-none row justify-content-center" id="step2">
<div class="col-lg-8 col-md-10"> <div class="col-lg-8 col-md-10">
<h4 class="text-center mb-4 text-secondary">{{ _("2. Enter Your Information") }}</h4> <h4 class="text-center mb-4 text-secondary">{{ _("2. Enter Your Information") }}</h4>
@ -335,7 +335,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="step d-none row justify-content-center" id="step3"> <div class="step d-none row justify-content-center" id="step3">
<div class="col-lg-8 col-md-10"> <div class="col-lg-8 col-md-10">
<h4 class="text-center mb-4 text-secondary">{{ _("3. Payment Information") }}</h4> <h4 class="text-center mb-4 text-secondary">{{ _("3. Payment Information") }}</h4>
@ -415,7 +414,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="step d-none row justify-content-center" id="step4"> <div class="step d-none row justify-content-center" id="step4">
<div class="col-lg-8 col-md-10"> <div class="col-lg-8 col-md-10">
<h4 class="text-center mb-4 text-secondary">{{ _("4. Confirm Your Information") }}</h4> <h4 class="text-center mb-4 text-secondary">{{ _("4. Confirm Your Information") }}</h4>
@ -425,10 +423,10 @@
<p class="mb-1"><i class="fas fa-box me-2"></i><strong>{{ _("Plan") }}:</strong> <span id="summary_plan"></span></p> <p class="mb-1"><i class="fas fa-box me-2"></i><strong>{{ _("Plan") }}:</strong> <span id="summary_plan"></span></p>
</div> </div>
<div class="summary-item"> <div class="summary-item">
<p class="mb-1"><i class="fas fa-tag me-2"></i><strong>{{ _("Price (excl. VAT)") }}:</strong> <span id="summary_price"></span> <span class="icon-saudi_riyal"></span></p> <p class="mb-1"><i class="fas fa-tag me-2"></i><strong>{{ _("Price") }}:</strong> <span id="summary_price"></span></p>
</div> </div>
<div class="summary-item"> <div class="summary-item">
<p class="mb-1"><i class="fas fa-receipt me-2"></i><strong>{{ _("VAT") }} (15%):</strong> <span id="summary-tax"></span> <span class="icon-saudi_riyal"></span></p> <p class="mb-1"><i class="fas fa-receipt me-2"></i><strong>{{ _("VAT") }} (15%):</strong> <span id="summary-tax">0.00</span> <span class="icon-saudi_riyal"></span></p>
</div> </div>
<hr class="my-3"> <hr class="my-3">
<div class="summary-item"> <div class="summary-item">
@ -460,7 +458,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="d-flex justify-content-between mt-5"> <div class="d-flex justify-content-between mt-5">
<button type="button" <button type="button"
class="btn btn-lg btn-phoenix-secondary px-5" class="btn btn-lg btn-phoenix-secondary px-5"
@ -479,6 +476,7 @@
{% block customJS %} {% block customJS %}
<script> <script>
// wizard-form.js
document.addEventListener('DOMContentLoaded', initWizardForm); document.addEventListener('DOMContentLoaded', initWizardForm);
document.addEventListener('htmx:afterSwap', initWizardForm); document.addEventListener('htmx:afterSwap', initWizardForm);
@ -486,22 +484,22 @@
const form = document.getElementById('wizardForm'); const form = document.getElementById('wizardForm');
if (!form) return; if (!form) return;
// Remove old event listeners to prevent duplicates // Remove old event listeners to prevent duplicates
form.removeEventListener('submit', handleFormSubmit); form.removeEventListener('submit', handleFormSubmit);
// Add new submit handler // Add new submit handler
form.addEventListener('submit', handleFormSubmit); form.addEventListener('submit', handleFormSubmit);
// Initialize radio button selections // Initialize radio button selections
initPlanSelection(); initPlanSelection();
// Initialize wizard steps // Initialize wizard steps
initWizardSteps(); initWizardSteps();
// Initialize card input formatting // Initialize card input formatting
initCardInputs(); initCardInputs();
// Initialize summary updates // Initialize summary updates
initSummaryUpdates(); initSummaryUpdates();
// Initialize validation // Initialize validation
@ -525,7 +523,7 @@
}); });
} }
// Submit the form after a slight delay // Submit the form after a slight delay
setTimeout(() => { setTimeout(() => {
const form = e.target; const form = e.target;
if (form.reportValidity()) { if (form.reportValidity()) {
@ -539,13 +537,13 @@
function initPlanSelection() { function initPlanSelection() {
const radios = document.querySelectorAll('.btn-check'); const radios = document.querySelectorAll('.btn-check');
radios.forEach(radio => { radios.forEach(radio => {
// Remove old listeners // Remove old listeners
radio.removeEventListener('change', handlePlanChange); radio.removeEventListener('change', handlePlanChange);
// Add new listeners // Add new listeners
radio.addEventListener('change', handlePlanChange); radio.addEventListener('change', handlePlanChange);
// Initialize selected state // Initialize selected state
if (radio.checked) { if (radio.checked) {
updatePlanSelection(radio.id); updatePlanSelection(radio.id);
} }
@ -582,9 +580,8 @@
nextBtn.removeEventListener("click", handleNext); nextBtn.removeEventListener("click", handleNext);
prevBtn.removeEventListener("click", handlePrev); prevBtn.removeEventListener("click", handlePrev);
// Add new listeners nextBtn.addEventListener("click", handleNext);
nextBtn.addEventListener("click", handleNext); prevBtn.addEventListener("click", handlePrev);
prevBtn.addEventListener("click", handlePrev);
function showStep(index) { function showStep(index) {
steps.forEach((step, i) => { steps.forEach((step, i) => {
@ -625,7 +622,11 @@
} }
function populateSummary() { function populateSummary() {
updatePricingSummary(); // Reuse logic for pricing const selectedPlan = document.querySelector('input[name="selected_plan"]:checked');
if (selectedPlan) {
document.getElementById("summary_plan").textContent = selectedPlan.dataset.name;
document.getElementById("summary_price").textContent = selectedPlan.dataset.price;
}
const firstName = document.getElementById("first_name")?.value || ''; const firstName = document.getElementById("first_name")?.value || '';
const lastName = document.getElementById("last_name")?.value || ''; const lastName = document.getElementById("last_name")?.value || '';
@ -647,7 +648,7 @@
return "**** **** **** " + last4; return "**** **** **** " + last4;
} }
// Initialize // Initialize
showStep(currentStep); showStep(currentStep);
} }
@ -696,20 +697,6 @@
updatePricingSummary(); // Initial call updatePricingSummary(); // Initial call
} }
function updatePricingSummary() {
const selectedPlan = document.querySelector('input[name="selected_plan"]:checked');
if (!selectedPlan) return;
const priceWithTax = parseFloat(selectedPlan.dataset.price);
const basePrice = priceWithTax / 1.15;
const vat = priceWithTax - basePrice;
document.getElementById("summary_plan").textContent = selectedPlan.dataset.name;
document.getElementById("summary_price").textContent = basePrice.toFixed(2);
document.getElementById("summary-tax").textContent = vat.toFixed(2);
document.getElementById("summary-total").textContent = priceWithTax.toFixed(2);
}
function initValidation() { function initValidation() {
// Add input event listeners for validation // Add input event listeners for validation
const cardNameInput = document.getElementById("card_name"); const cardNameInput = document.getElementById("card_name");