pricelist internal plans comparison

This commit is contained in:
Tobias Brunner 2025-06-20 14:20:28 +02:00
parent 4b515702e3
commit 15869ca542
No known key found for this signature in database
2 changed files with 260 additions and 15 deletions

View file

@ -51,6 +51,41 @@
.servala-row { .servala-row {
border-bottom: 2px solid #007bff; border-bottom: 2px solid #007bff;
border-left: 4px solid #007bff;
background-color: rgba(13, 110, 253, 0.03);
}
.internal-comparison-row {
background-color: rgba(25, 135, 84, 0.08) !important;
border-left: 4px solid #198754;
border-top: 1px solid #198754;
}
.external-comparison-row {
background-color: rgba(108, 117, 125, 0.08) !important;
border-left: 4px solid #6c757d;
border-top: 1px solid #6c757d;
}
/* Group comparison rows visually */
.comparison-group {
border-bottom: 2px solid #dee2e6;
margin-bottom: 8px;
}
/* Add visual connection between main row and comparisons */
.has-comparisons {
position: relative;
}
.has-comparisons::after {
content: '';
position: absolute;
left: 0;
bottom: -1px;
width: 100%;
height: 2px;
background: linear-gradient(to right, #007bff, transparent);
} }
/* Price calculation breakdown styling */ /* Price calculation breakdown styling */
@ -171,6 +206,10 @@
<small class="text-muted"> <small class="text-muted">
This transparent pricing model ensures you understand exactly what you're paying for. This transparent pricing model ensures you understand exactly what you're paying for.
The table below breaks down each component for every service variant we offer. The table below breaks down each component for every service variant we offer.
<br><br>
<strong>Price Comparisons:</strong> When enabled, you'll see:
<br><span class="badge bg-secondary">External Providers</span> - Competitor prices from AWS, Google Cloud, etc.
<br><span class="badge bg-success">Other Servala Providers</span> - Same service specs on different cloud providers within our network
</small> </small>
</p> </p>
</div> </div>
@ -246,7 +285,7 @@
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" name="price_comparison" value="true" id="price_comparison" {% if show_price_comparison %}checked{% endif %}> <input class="form-check-input" type="checkbox" name="price_comparison" value="true" id="price_comparison" {% if show_price_comparison %}checked{% endif %}>
<label class="form-check-label" for="price_comparison"> <label class="form-check-label" for="price_comparison">
Show external price comparisons Show price comparisons (external providers + internal cloud providers)
</label> </label>
</div> </div>
</div> </div>
@ -268,7 +307,7 @@
{% if filter_service_level %}<span class="badge me-1">Service Level: {{ filter_service_level }}</span>{% endif %} {% if filter_service_level %}<span class="badge me-1">Service Level: {{ filter_service_level }}</span>{% endif %}
{% if show_discount_details %}<span class="badge bg-secondary me-1">Discount Details</span>{% endif %} {% if show_discount_details %}<span class="badge bg-secondary me-1">Discount Details</span>{% endif %}
{% if show_addon_details %}<span class="badge bg-info me-1">Addon Details</span>{% endif %} {% if show_addon_details %}<span class="badge bg-info me-1">Addon Details</span>{% endif %}
{% if show_price_comparison %}<span class="badge bg-warning me-1">Price Comparison</span>{% endif %} {% if show_price_comparison %}<span class="badge bg-warning me-1">Price Comparisons</span>{% endif %}
</div> </div>
{% endif %} {% endif %}
@ -430,7 +469,7 @@
<th rowspan="2">Discount Details</th> <th rowspan="2">Discount Details</th>
{% endif %} {% endif %}
{% if show_price_comparison %} {% if show_price_comparison %}
<th rowspan="2">External Comparisons</th> <th rowspan="2">Price Comparisons</th>
{% endif %} {% endif %}
<th rowspan="2" class="final-price-header">Final Price</th> <th rowspan="2" class="final-price-header">Final Price</th>
</tr> </tr>
@ -444,7 +483,7 @@
</thead> </thead>
<tbody> <tbody>
{% for row in pricing_data %} {% for row in pricing_data %}
<tr class="servala-row"> <tr class="servala-row {% if show_price_comparison and row.external_comparisons or row.internal_comparisons %}has-comparisons{% endif %}">
<td>{{ row.compute_plan }}</td> <td>{{ row.compute_plan }}</td>
<td>{{ row.cloud_provider }}</td> <td>{{ row.cloud_provider }}</td>
<td>{{ row.vcpus }}</td> <td>{{ row.vcpus }}</td>
@ -566,16 +605,39 @@
{% endif %} {% endif %}
{% if show_price_comparison %} {% if show_price_comparison %}
<td> <td>
<span class="badge">-</span> {% if row.external_comparisons or row.internal_comparisons %}
<div class="text-center">
{% if row.external_comparisons %}
<div class="mb-1">
<span class="badge bg-secondary">{{ row.external_comparisons|length }} External</span>
</div>
{% endif %}
{% if row.internal_comparisons %}
<div class="mb-1">
<span class="badge bg-success">{{ row.internal_comparisons|length }} Internal</span>
</div>
{% endif %}
<small class="text-muted">See rows below</small>
</div>
{% else %}
<span class="text-muted">No comparisons</span>
{% endif %}
</td> </td>
{% endif %} {% endif %}
<td class="final-price-cell fw-bold">{{ row.final_price|floatformat:2 }}</td> <td class="final-price-cell fw-bold">{{ row.final_price|floatformat:2 }}</td>
</tr> </tr>
{% if show_price_comparison and row.external_comparisons %} {% if show_price_comparison and row.external_comparisons %}
{% for comparison in row.external_comparisons %} {% for comparison in row.external_comparisons %}
<tr class="table-light comparison-row"> <tr class="comparison-row external-comparison-row">
<td class="text-muted">{{ comparison.plan_name }}</td> <td class="text-muted">
<td class="text-muted">{{ comparison.provider }}</td> <div class="d-flex align-items-center">
<i class="bi bi-arrow-return-right me-2 text-secondary"></i>
{{ comparison.plan_name }}
</div>
</td>
<td class="text-muted">
<span class="badge bg-secondary">{{ comparison.provider }}</span>
</td>
<td class="text-muted"> <td class="text-muted">
{% if comparison.vcpus %}{{ comparison.vcpus }}{% else %}-{% endif %} {% if comparison.vcpus %}{{ comparison.vcpus }}{% else %}-{% endif %}
</td> </td>
@ -585,11 +647,11 @@
<td class="text-muted">{{ row.term }}</td> <td class="text-muted">{{ row.term }}</td>
<td class="text-muted">{{ comparison.currency }}</td> <td class="text-muted">{{ comparison.currency }}</td>
<!-- Price breakdown columns for external comparisons --> <!-- Price breakdown columns for external comparisons -->
<td class="text-muted">-</td> <td class="text-muted text-center">-</td>
<td class="text-muted">-</td> <td class="text-muted text-center">-</td>
<td class="text-muted">-</td> <td class="text-muted text-center">-</td>
<td class="text-muted">-</td> <td class="text-muted text-center">-</td>
<td class="text-muted">-</td> <td class="text-muted text-center">-</td>
{% if show_addon_details %} {% if show_addon_details %}
<td class="text-muted">-</td> <td class="text-muted">-</td>
{% endif %} {% endif %}
@ -599,7 +661,11 @@
{% endif %} {% endif %}
<td> <td>
<small> <small>
<span class="badge bg-secondary">{% if comparison.source %}<span class="text-muted"><a href="{{ comparison.source }}" target="_blank">{{ comparison.provider }}</a></span>{% else %}{{ comparison.provider }}{% endif %}</span><br> {% if comparison.source %}
<a href="{{ comparison.source }}" target="_blank" class="text-decoration-none">
<i class="bi bi-link-45deg"></i> External Source
</a><br>
{% endif %}
{% if comparison.description %} {% if comparison.description %}
<span class="text-muted">{{ comparison.description }}</span><br> <span class="text-muted">{{ comparison.description }}</span><br>
{% endif %} {% endif %}
@ -610,7 +676,7 @@
<span class="text-muted">Replicas: {{ comparison.replicas }}</span><br> <span class="text-muted">Replicas: {{ comparison.replicas }}</span><br>
{% endif %} {% endif %}
{% if comparison.ratio %} {% if comparison.ratio %}
<span class="text-muted">Price ratio: {{ comparison.ratio|floatformat:2 }}x</span><br> <span class="text-muted">Ratio: {{ comparison.ratio|floatformat:2 }}x</span>
{% endif %} {% endif %}
</small> </small>
</td> </td>
@ -625,6 +691,62 @@
</tr> </tr>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if show_price_comparison and row.internal_comparisons %}
{% for comparison in row.internal_comparisons %}
<tr class="comparison-row internal-comparison-row">
<td class="text-muted">
<div class="d-flex align-items-center">
<i class="bi bi-arrow-return-right me-2 text-success"></i>
{{ comparison.plan_name }}
</div>
</td>
<td class="text-muted">
<span class="badge bg-success">{{ comparison.provider }}</span>
</td>
<td class="text-muted">{{ comparison.vcpus }}</td>
<td class="text-muted">{{ comparison.ram }}</td>
<td class="text-muted">{{ row.term }}</td>
<td class="text-muted">{{ comparison.currency }}</td>
<!-- Price breakdown columns for internal comparisons -->
<td class="text-center text-success">{{ comparison.compute_plan_price|floatformat:2 }}</td>
<td class="text-muted text-center"><small>Same</small></td>
<td class="text-muted text-center"><small>Same</small></td>
<td class="text-muted text-center"><small>Same</small></td>
<td class="text-center text-success">{{ comparison.service_price|floatformat:2 }}</td>
{% if show_addon_details %}
<td class="text-muted"><small>Same as above</small></td>
{% endif %}
{% if show_discount_details %}
<td class="text-muted"><small>Same</small></td>
<td class="text-muted"><small>Same</small></td>
{% endif %}
<td>
<small>
<span class="text-success"><i class="bi bi-check-circle"></i> Servala Network</span><br>
<span class="text-muted">{{ comparison.description }}</span><br>
<span class="text-muted">Group: {{ comparison.group_name }}</span><br>
<span class="text-muted">Ratio: {{ comparison.ratio|floatformat:2 }}x</span>
</small>
</td>
<td class="fw-bold">
{{ comparison.amount|floatformat:2 }} {{ comparison.currency }}
{% if comparison.difference > 0 %}
<span class="badge bg-danger ms-1">+{{ comparison.difference|floatformat:2 }}</span>
{% elif comparison.difference < 0 %}
<span class="badge bg-success ms-1">{{ comparison.difference|floatformat:2 }}</span>
{% elif comparison.difference == 0 %}
<span class="badge bg-info ms-1">Same</span>
{% endif %}
</td>
</tr>
{% endfor %}
{% endif %}
<!-- Add visual separator after comparison rows -->
{% if show_price_comparison and row.external_comparisons or row.internal_comparisons %}
<tr class="comparison-group">
<td colspan="{% if show_addon_details and show_discount_details %}14{% elif show_addon_details or show_discount_details %}13{% else %}12{% endif %}"></td>
</tr>
{% endif %}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View file

@ -38,6 +38,90 @@ def get_external_price_comparisons(plan, appcat_price, currency, service_level):
return [] return []
def get_internal_cloud_provider_comparisons(
plan, appcat_price, currency, service_level
):
"""Get internal comparisons with other cloud provider plans from the database"""
try:
# Get similar compute plans from other cloud providers with same specs
similar_plans = (
ComputePlan.objects.filter(
active=True,
vcpus=plan.vcpus,
ram=plan.ram,
)
.exclude(cloud_provider=plan.cloud_provider) # Exclude same cloud provider
.select_related("cloud_provider")
.prefetch_related("prices")
)
internal_comparisons = []
for similar_plan in similar_plans:
# Get pricing components for comparison plan
compare_plan_price = similar_plan.get_price(currency)
compare_base_fee = appcat_price.get_base_fee(currency)
compare_unit_rate = appcat_price.get_unit_rate(currency, service_level)
# Skip if any pricing component is missing
if any(
price is None
for price in [compare_plan_price, compare_base_fee, compare_unit_rate]
):
continue
# Calculate units based on variable unit type
if appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.RAM:
units = int(similar_plan.ram)
elif appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.CPU:
units = int(similar_plan.vcpus)
else:
continue
# Calculate replica enforcement based on service level
if service_level == VSHNAppCatPrice.ServiceLevel.GUARANTEED:
replica_enforce = appcat_price.ha_replica_min
else:
replica_enforce = 1
total_units = units * replica_enforce
# Calculate final price using the same logic as the main plan
price_calculation = appcat_price.calculate_final_price(
currency_code=currency,
service_level=service_level,
number_of_units=total_units,
addon_ids=None, # Include only mandatory addons
)
if price_calculation is None:
continue
service_price_with_addons = price_calculation["total_price"]
compare_final_price = compare_plan_price + service_price_with_addons
internal_comparisons.append(
{
"plan_name": similar_plan.name,
"provider": similar_plan.cloud_provider.name,
"compute_plan_price": compare_plan_price,
"service_price": service_price_with_addons,
"final_price": compare_final_price,
"currency": currency,
"vcpus": similar_plan.vcpus,
"ram": similar_plan.ram,
"group_name": (
similar_plan.group.name if similar_plan.group else "No Group"
),
"is_internal": True, # Flag to distinguish from external comparisons
}
)
return internal_comparisons
except Exception:
return []
@staff_member_required @staff_member_required
def pricelist(request): def pricelist(request):
"""Generate comprehensive price list grouped by compute plan groups and service levels""" """Generate comprehensive price list grouped by compute plan groups and service levels"""
@ -287,7 +371,9 @@ def pricelist(request):
# Get external price comparisons if enabled # Get external price comparisons if enabled
external_comparisons = [] external_comparisons = []
internal_comparisons = []
if show_price_comparison: if show_price_comparison:
# Get external price comparisons
external_prices = get_external_price_comparisons( external_prices = get_external_price_comparisons(
plan, appcat_price, currency, service_level plan, appcat_price, currency, service_level
) )
@ -313,6 +399,42 @@ def pricelist(request):
"ratio": ratio, "ratio": ratio,
"source": ext_price.source, "source": ext_price.source,
"date_retrieved": ext_price.date_retrieved, "date_retrieved": ext_price.date_retrieved,
"is_internal": False,
}
)
# Get internal cloud provider comparisons
internal_price_comparisons = (
get_internal_cloud_provider_comparisons(
plan, appcat_price, currency, service_level
)
)
for int_price in internal_price_comparisons:
# Calculate price difference
difference = int_price["final_price"] - final_price
ratio = (
int_price["final_price"] / final_price
if final_price > 0
else 0
)
internal_comparisons.append(
{
"plan_name": int_price["plan_name"],
"provider": int_price["provider"],
"description": f"Same specs with {int_price['provider']}",
"amount": int_price["final_price"],
"currency": int_price["currency"],
"vcpus": int_price["vcpus"],
"ram": int_price["ram"],
"group_name": int_price["group_name"],
"compute_plan_price": int_price[
"compute_plan_price"
],
"service_price": int_price["service_price"],
"difference": difference,
"ratio": ratio,
"is_internal": True,
} }
) )
@ -374,6 +496,7 @@ def pricelist(request):
and appcat_price.discount_model.active and appcat_price.discount_model.active
), ),
"external_comparisons": external_comparisons, "external_comparisons": external_comparisons,
"internal_comparisons": internal_comparisons,
"mandatory_addons": mandatory_addons, "mandatory_addons": mandatory_addons,
"optional_addons": optional_addons, "optional_addons": optional_addons,
} }