Compare commits

...

5 Commits

12 changed files with 281 additions and 174 deletions

View File

@ -2133,7 +2133,8 @@ 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,6 +9,8 @@ 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
@ -16,14 +18,15 @@ 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') dealer_id = task.kwargs.get('dealer_id',None)
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
from .models import Dealer instance = Dealer.objects.get(id=dealer_id)
instance = Dealer.objects.select_related('entity').get(id=dealer_id)
entity = instance.entity entity = instance.entity
if not entity: if not entity:
@ -34,11 +37,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.id}: {e}") logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {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.id}") logger.error(f"No COA for entity {entity.pk}")
return return
# Check which accounts are missing and create them # Check which accounts are missing and create them
@ -46,7 +49,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(code=account_data["code"]).exists(): if not entity.get_all_accounts().filter(coa_model=coa,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,16 +658,12 @@ 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,50 +138,36 @@ 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 created: if not created:
try: return
# 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,
)
if entity: try:
instance.entity = entity with transaction.atomic():
instance.save(update_fields=['entity']) # Create 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")
# Create COA synchronously first instance.entity = entity
coa = entity.create_chart_of_accounts( instance.save(update_fields=['entity'])
assign_as_default=True, commit=True,
coa_name=_(f"{entity_name}-COA")
)
if coa: # Create default COA
# Create essential UOMs synchronously entity.create_chart_of_accounts(
for u in models.UnitOfMeasure.choices: assign_as_default=True,
entity.create_uom(name=u[1], unit_abbr=u[0]) commit=True,
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
) )
except Exception as e: logger.info(f"✅ Setup complete for dealer {instance.id}: entity & COA ready.")
logger.error(f"Failed to create ledger entity for dealer {instance.id}: {e}")
# Schedule retry task except Exception as e:
async_task( logger.error(f"💥 Failed setup for dealer {instance.id}: {e}")
func="inventory.tasks.retry_entity_creation", # Optional: schedule retry or alert
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):
@ -1406,6 +1392,7 @@ 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,
@ -1430,15 +1417,31 @@ 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:
dealer = instance.entity.dealers.first() # Schedule async account creation AFTER commit
async_task( transaction.on_commit(
func="inventory.tasks.create_coa_accounts", lambda: async_task(
dealer_id=dealer.pk, # Pass ID instead of object "inventory.tasks.create_coa_accounts",
coa_slug=instance.slug, dealer_id=dealer.pk,
hook="inventory.hooks.check_create_coa_accounts", coa_slug=instance.slug,
ack_failure=True, # Ensure task failures are acknowledged hook="inventory.hooks.check_create_coa_accounts",
sync=False # Explicitly set to async ack_failure=True,
)
) )
# 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,70 +63,64 @@ def create_settings(pk):
) )
def create_coa_accounts(dealer_id, **kwargs): def create_coa_accounts(dealer_id,**kwargs):
""" """
Create COA accounts with retry logic and proper error handling Idempotent: Creates only missing default accounts.
Safe to retry. Returns True if all done.
""" """
from .models import Dealer from .models import Dealer
from .utils import create_account, get_accounts_data from .utils import get_accounts_data, create_account
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
max_retries = 3 if coa_slug:
retry_delay = 2 # seconds try:
coa_slug = kwargs.get('coa_slug', None) coa = entity.get_coa_model_qs().get(slug=coa_slug)
logger.info(f"chart of account model slug {coa_slug}") except Exception as e:
logger.info(f"Attempting to create accounts for dealer {dealer_id}") logger.error(f"COA with slug {coa_slug} not found for entity {entity.pk}: {e}")
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 coa_slug: if not coa:
try: logger.error(f"❌ No default COA for entity {entity.pk}")
coa = entity.get_coa_model_qs().get(slug=coa_slug) return False
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}")
if not coa: # Get missing accounts
logger.error(f"No COA found for entity {entity.pk}") existing_codes = set(entity.get_all_accounts().filter(coa_model=coa).values_list('code', flat=True))
return False accounts_to_create = [
acc for acc in get_accounts_data()
if acc["code"] not in existing_codes
]
logger.info("Creating default accounts") if not accounts_to_create:
accounts_created = 0 logger.info("✅ All default accounts already exist.")
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
except Exception as e: # Create missing ones
logger.error(f"Attempt {attempt + 1} failed: {e}") logger.info(f"🔧 Creating {len(accounts_to_create)} missing accounts...")
success = True
if attempt < max_retries - 1: for acc in accounts_to_create:
logger.info(f"Retrying in {retry_delay} seconds...") if not create_account(entity, coa, acc):
time.sleep(retry_delay * (attempt + 1)) # Exponential backoff logger.warning(f"⚠️ Failed to create account: {acc['code']}")
else: success = False # don't fail task, just log
logger.error(f"All {max_retries} attempts failed for dealer {dealer_id}")
# Schedule a cleanup or notification task if success:
# async_task( logger.info("✅ All missing accounts created successfully.")
# "inventory.tasks.handle_account_creation_failure", else:
# dealer_id=dealer_id, logger.warning("⚠️ Some accounts failed to create — check logs.")
# 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):
""" """
@ -161,7 +155,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,6 +1332,7 @@ 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,45 +2388,36 @@ 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']}")
Create account with proper validation and error handling logger.info(f"COA: {coa}")
"""
try: try:
# existing_account = AccountModel.objects.filter(coa_model=coa,code=account_data["code"]) # Skip if exists
existing_account = entity.get_all_accounts().filter( if coa.get_coa_accounts().filter(code=account_data["code"]).exists():
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"Creating account: {account_data['code']}") logger.info(f"Account does not exist: {account_data['code']},creating...")
account = entity.create_account( account = coa.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"Successfully created account: {account_data['code']}") logger.info(f"Created account: {account}")
if account: if account:
account.role_default = account_data["default"] account.role_default = account_data.get("default", False)
account.save() account.save(update_fields=['role_default'])
logger.info(f"Successfully created account: {account_data['code']}")
return True return True
except IntegrityError: except IntegrityError:
logger.warning(f"Account {account_data['code']} already exists (IntegrityError)") return True # Already created by race condition
return True
except Exception as e: except Exception as e:
logger.error(f"Error creating account {account_data['code']}: {e}") logger.error(f"❌ Error creating {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

@ -2336,28 +2336,28 @@ class DealerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
return context return context
from .forms import VatRateForm from .forms import VatRateForm
@login_required @login_required
def dealer_vat_rate_update(request, slug): def dealer_vat_rate_update(request, slug):
dealer = get_object_or_404(models.Dealer, slug=slug) dealer = get_object_or_404(models.Dealer, slug=slug)
vat_rate_instance, created = models.VatRate.objects.get_or_create(dealer=dealer) vat_rate_instance, created = models.VatRate.objects.get_or_create(dealer=dealer)
if request.method == "POST": if request.method == "POST":
form = VatRateForm(request.POST, instance=vat_rate_instance) form = VatRateForm(request.POST, instance=vat_rate_instance)
if form.is_valid(): if form.is_valid():
form.save() form.save()
messages.success(request, _("VAT rate updated successfully")) messages.success(request, _("VAT rate updated successfully"))
return redirect("dealer_detail", slug=slug) return redirect("dealer_detail", slug=slug)
else: else:
messages.error(request, _("Please enter valid vat rate between 0 and 1.")) messages.error(request, _("Please enter valid vat rate between 0 and 1."))
redirect("dealer_detail", slug=slug) redirect("dealer_detail", slug=slug)
return redirect("dealer_detail", slug=slug) return redirect("dealer_detail", slug=slug)
class DealerUpdateView( class DealerUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
@ -9867,7 +9867,8 @@ 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.")
@ -10764,7 +10765,7 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["entity_slug"] = dealer.entity.slug context["entity_slug"] = dealer.entity.slug
context["vendors"] = vendors context["vendors"] = vendors
context["empty_state_value"] = _("purchase order") context["empty_state_value"] = _("purchase order")
return context return context
@ -11762,4 +11763,10 @@ class CarDealershipSignUpView(CreateView):
def form_valid(self, form): def form_valid(self, form):
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,8 +50,6 @@ 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

@ -0,0 +1,50 @@
{% 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

@ -0,0 +1,50 @@
{% 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... (Your actual integrity hash)" integrity="sha512-X..."
crossorigin="anonymous" crossorigin="anonymous"
referrerpolicy="no-referrer" /> referrerpolicy="no-referrer" />
<style> <style>
@ -21,7 +21,7 @@
--card-selected-shadow: rgba(13, 110, 253, 0.4); --card-selected-shadow: rgba(13, 110, 253, 0.4);
} }
.pricing-card-label { .pricing-card-label {
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
cursor: pointer; cursor: pointer;
@ -144,7 +144,6 @@
} }
.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;
@ -177,9 +176,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>
@ -256,6 +255,7 @@
</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,6 +335,7 @@
</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>
@ -414,6 +415,7 @@
</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>
@ -423,10 +425,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") }}:</strong> <span id="summary_price"></span></p> <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>
</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">0.00</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"></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">
@ -458,6 +460,7 @@
</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"
@ -472,11 +475,10 @@
</div> </div>
</form> </form>
</div> </div>
{% endblock content %} {% endblock content %}
{% 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);
@ -484,22 +486,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
@ -523,7 +525,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()) {
@ -537,13 +539,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);
} }
@ -580,8 +582,9 @@
nextBtn.removeEventListener("click", handleNext); nextBtn.removeEventListener("click", handleNext);
prevBtn.removeEventListener("click", handlePrev); prevBtn.removeEventListener("click", handlePrev);
nextBtn.addEventListener("click", handleNext); // Add new listeners
prevBtn.addEventListener("click", handlePrev); nextBtn.addEventListener("click", handleNext);
prevBtn.addEventListener("click", handlePrev);
function showStep(index) { function showStep(index) {
steps.forEach((step, i) => { steps.forEach((step, i) => {
@ -622,11 +625,7 @@
} }
function populateSummary() { function populateSummary() {
const selectedPlan = document.querySelector('input[name="selected_plan"]:checked'); updatePricingSummary(); // Reuse logic for pricing
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 || '';
@ -648,7 +647,7 @@
return "**** **** **** " + last4; return "**** **** **** " + last4;
} }
// Initialize // Initialize
showStep(currentStep); showStep(currentStep);
} }
@ -669,7 +668,7 @@
function formatCardNumber(e) { function formatCardNumber(e) {
let val = this.value.replace(/\D/g, "").substring(0, 16); let val = this.value.replace(/\D/g, "").substring(0, 16);
this.value = val.replace(/(.{4})/g, "$1 ").trim(); this.value = val.replace(/(.{4})/g, "$1 ").trim();
// Validate as user types // Validate as user types
validateCardNumber(this); validateCardNumber(this);
} }
@ -697,6 +696,20 @@
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");