diff --git a/hub/services/admin/pricing.py b/hub/services/admin/pricing.py
index ac34e12..973679a 100644
--- a/hub/services/admin/pricing.py
+++ b/hub/services/admin/pricing.py
@@ -22,6 +22,7 @@ from ..models import (
VSHNAppCatUnitRate,
ProgressiveDiscountModel,
DiscountTier,
+ ExternalPricePlans,
)
@@ -308,3 +309,33 @@ class StoragePlanAdmin(ImportExportModelAdmin):
return format_html("
".join([f"{p.amount} {p.currency}" for p in prices]))
display_prices.short_description = "Prices (Amount Currency)"
+
+
+@admin.register(ExternalPricePlans)
+class ExternalPricePlansAdmin(admin.ModelAdmin):
+ """Admin configuration for ExternalPricePlans model"""
+
+ list_display = (
+ "plan_name",
+ "cloud_provider",
+ "service",
+ "currency",
+ "amount",
+ "display_compare_to_count",
+ "date_retrieved",
+ )
+ list_filter = ("cloud_provider", "service", "currency", "term")
+ search_fields = ("plan_name", "cloud_provider__name", "service__name")
+ ordering = ("cloud_provider", "service", "plan_name")
+
+ # Configure many-to-many field display
+ filter_horizontal = ("compare_to",)
+
+ def display_compare_to_count(self, obj):
+ """Display count of compute plans this external price compares to"""
+ count = obj.compare_to.count()
+ if count == 0:
+ return "No comparisons"
+ return f"{count} plan{'s' if count != 1 else ''}"
+
+ display_compare_to_count.short_description = "Compare To"
diff --git a/hub/services/migrations/0031_externalpriceplans.py b/hub/services/migrations/0031_externalpriceplans.py
new file mode 100644
index 0000000..12f24d4
--- /dev/null
+++ b/hub/services/migrations/0031_externalpriceplans.py
@@ -0,0 +1,127 @@
+# Generated by Django 5.2 on 2025-05-27 14:52
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("services", "0030_serviceoffering_msp"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="ExternalPricePlans",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("plan_name", models.CharField()),
+ (
+ "description",
+ models.CharField(blank=True, max_length=200, null=True),
+ ),
+ ("source", models.URLField(blank=True, null=True)),
+ ("date_retrieved", models.DateField(blank=True, null=True)),
+ (
+ "currency",
+ models.CharField(
+ choices=[
+ ("CHF", "Swiss Franc"),
+ ("EUR", "Euro"),
+ ("USD", "US Dollar"),
+ ],
+ default="CHF",
+ max_length=3,
+ ),
+ ),
+ (
+ "term",
+ models.CharField(
+ choices=[
+ ("MTH", "per Month (30d)"),
+ ("DAY", "per Day"),
+ ("HR", "per Hour"),
+ ("MIN", "per Minute"),
+ ],
+ default="MTH",
+ max_length=3,
+ ),
+ ),
+ (
+ "amount",
+ models.DecimalField(
+ decimal_places=4,
+ help_text="Price per unit in the specified currency, excl. VAT",
+ max_digits=10,
+ ),
+ ),
+ (
+ "vcpus",
+ models.FloatField(
+ blank=True, help_text="Number of included vCPUs", null=True
+ ),
+ ),
+ (
+ "ram",
+ models.FloatField(
+ blank=True, help_text="Amount of GiB RAM included", null=True
+ ),
+ ),
+ (
+ "storage",
+ models.FloatField(
+ blank=True, help_text="Amount of GiB included", null=True
+ ),
+ ),
+ ("competitor_sla", models.CharField(blank=True, null=True)),
+ ("replicas", models.IntegerField(blank=True, null=True)),
+ (
+ "cloud_provider",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="external_price",
+ to="services.cloudprovider",
+ ),
+ ),
+ (
+ "compare_to",
+ models.ManyToManyField(
+ blank=True,
+ null=True,
+ related_name="external_prices",
+ to="services.computeplan",
+ ),
+ ),
+ (
+ "service",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="external_price",
+ to="services.service",
+ ),
+ ),
+ (
+ "vshn_appcat_price",
+ models.ForeignKey(
+ blank=True,
+ help_text="Specific VSHN AppCat price configuration to compare against",
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="external_comparisons",
+ to="services.vshnappcatprice",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "External Price",
+ },
+ ),
+ ]
diff --git a/hub/services/migrations/0032_externalpriceplans_service_level.py b/hub/services/migrations/0032_externalpriceplans_service_level.py
new file mode 100644
index 0000000..b230add
--- /dev/null
+++ b/hub/services/migrations/0032_externalpriceplans_service_level.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.2 on 2025-05-27 15:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("services", "0031_externalpriceplans"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="externalpriceplans",
+ name="service_level",
+ field=models.CharField(
+ blank=True,
+ choices=[("BE", "Best Effort"), ("GA", "Guaranteed Availability")],
+ help_text="Service level equivalent for comparison",
+ max_length=2,
+ null=True,
+ ),
+ ),
+ ]
diff --git a/hub/services/models/pricing.py b/hub/services/models/pricing.py
index 6816fe8..4079d16 100644
--- a/hub/services/models/pricing.py
+++ b/hub/services/models/pricing.py
@@ -382,3 +382,72 @@ class VSHNAppCatUnitRate(models.Model):
def __str__(self):
return f"{self.vshn_appcat_price_config.service.name} - {self.get_service_level_display()} Unit Rate - {self.amount} {self.currency}"
+
+
+class ExternalPricePlans(models.Model):
+ plan_name = models.CharField()
+ description = models.CharField(max_length=200, blank=True, null=True)
+ source = models.URLField(blank=True, null=True)
+ date_retrieved = models.DateField(blank=True, null=True)
+
+ ## Relations
+ cloud_provider = models.ForeignKey(
+ CloudProvider, on_delete=models.CASCADE, related_name="external_price"
+ )
+ service = models.ForeignKey(
+ Service, on_delete=models.CASCADE, related_name="external_price"
+ )
+ compare_to = models.ManyToManyField(
+ ComputePlan, related_name="external_prices", blank=True, null=True
+ )
+ vshn_appcat_price = models.ForeignKey(
+ VSHNAppCatPrice,
+ on_delete=models.CASCADE,
+ related_name="external_comparisons",
+ blank=True,
+ null=True,
+ help_text="Specific VSHN AppCat price configuration to compare against",
+ )
+ service_level = models.CharField(
+ max_length=2,
+ choices=VSHNAppCatPrice.ServiceLevel.choices,
+ blank=True,
+ null=True,
+ help_text="Service level equivalent for comparison",
+ )
+
+ ## Money
+ currency = models.CharField(
+ max_length=3,
+ default=Currency.CHF,
+ choices=Currency.choices,
+ )
+ term = models.CharField(
+ max_length=3,
+ default=Term.MTH,
+ choices=Term.choices,
+ )
+ amount = models.DecimalField(
+ max_digits=10,
+ decimal_places=4,
+ help_text="Price per unit in the specified currency, excl. VAT",
+ )
+
+ ## Offering
+ vcpus = models.FloatField(
+ help_text="Number of included vCPUs", blank=True, null=True
+ )
+ ram = models.FloatField(
+ help_text="Amount of GiB RAM included", blank=True, null=True
+ )
+ storage = models.FloatField(
+ help_text="Amount of GiB included", blank=True, null=True
+ )
+ competitor_sla = models.CharField(blank=True, null=True)
+ replicas = models.IntegerField(blank=True, null=True)
+
+ class Meta:
+ verbose_name = "External Price"
+
+ def __str__(self):
+ return f"{self.cloud_provider.name} - {self.service.name} - {self.plan_name}"
diff --git a/hub/services/templates/services/pricelist.html b/hub/services/templates/services/pricelist.html
index 2fc25f6..e5b5ef7 100644
--- a/hub/services/templates/services/pricelist.html
+++ b/hub/services/templates/services/pricelist.html
@@ -71,6 +71,12 @@
Show discount details
+