diff --git a/src/servala/api/views.py b/src/servala/api/views.py index 0aa73a2..5d67754 100644 --- a/src/servala/api/views.py +++ b/src/servala/api/views.py @@ -108,16 +108,19 @@ class OSBServiceInstanceView(OSBBasicAuthPermission, View): if service in organization.limit_osb_services.all(): return JsonResponse({"message": "Service already enabled"}, status=200) except Organization.DoesNotExist: - odoo_data = { - "company_name": organization_display_name, - "invoice_email": user.email, - } - with transaction.atomic(): - try: - billing_entity = BillingEntity.create_from_data( - name=f"{organization_display_name} (Exoscale)", - odoo_data=odoo_data, - ) + try: + with transaction.atomic(): + if exoscale_origin.billing_entity: + billing_entity = exoscale_origin.billing_entity + else: + odoo_data = { + "company_name": organization_display_name, + "invoice_email": user.email, + } + billing_entity = BillingEntity.create_from_data( + name=f"{organization_display_name} (Exoscale)", + odoo_data=odoo_data, + ) organization = Organization( name=organization_display_name, billing_entity=billing_entity, @@ -126,8 +129,8 @@ class OSBServiceInstanceView(OSBBasicAuthPermission, View): ) organization = Organization.create_organization(organization, user) self._send_invitation_email(request, organization, user) - except Exception: - return JsonResponse({"error": "Internal server error"}, status=500) + except Exception: + return JsonResponse({"error": "Internal server error"}, status=500) organization.limit_osb_services.add(service) self._send_service_welcome_email( diff --git a/src/servala/core/migrations/0010_organizationorigin_billing_entity.py b/src/servala/core/migrations/0010_organizationorigin_billing_entity.py new file mode 100644 index 0000000..d61a75f --- /dev/null +++ b/src/servala/core/migrations/0010_organizationorigin_billing_entity.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.7 on 2025-10-17 00:22 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0009_organization_limit_cloudproviders_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="organizationorigin", + name="billing_entity", + field=models.ForeignKey( + help_text="If set, this billing entity will be used on new organizations with this origin.", + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="origins", + to="core.billingentity", + verbose_name="Billing entity", + ), + ), + ] diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 84453b9..7b06a41 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -355,6 +355,16 @@ class OrganizationOrigin(ServalaModelMixin, models.Model): name = models.CharField(max_length=100, verbose_name=_("Name")) description = models.TextField(blank=True, verbose_name=_("Description")) + billing_entity = models.ForeignKey( + to="BillingEntity", + on_delete=models.PROTECT, + related_name="origins", + verbose_name=_("Billing entity"), + help_text=_( + "If set, this billing entity will be used on new organizations with this origin." + ), + null=True, + ) class Meta: verbose_name = _("Organization origin") diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 32499ca..09db220 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -3,6 +3,7 @@ import base64 import pytest from servala.core.models import ( + BillingEntity, Organization, OrganizationMembership, OrganizationOrigin, @@ -21,6 +22,11 @@ def origin(): return OrganizationOrigin.objects.create(name="TESTORIGIN") +@pytest.fixture +def billing_entity(): + return BillingEntity.objects.create(name="Test Entity") + + @pytest.fixture def organization(origin): return Organization.objects.create(name="Test Org", origin=origin) diff --git a/src/tests/test_api_exoscale.py b/src/tests/test_api_exoscale.py index 725eddf..b6fa4dc 100644 --- a/src/tests/test_api_exoscale.py +++ b/src/tests/test_api_exoscale.py @@ -99,6 +99,36 @@ def test_successful_onboarding_new_organization( assert "redis/offering/" in welcome_email.body +@pytest.mark.django_db +def test_new_organization_inherits_origin( + osb_client, + test_service, + test_service_offering, + valid_osb_payload, + exoscale_origin, + instance_id, + billing_entity, +): + valid_osb_payload["service_id"] = test_service.osb_service_id + valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id + exoscale_origin.billing_entity = billing_entity + exoscale_origin.save() + + response = osb_client.put( + f"/api/osb/v2/service_instances/{instance_id}", + data=json.dumps(valid_osb_payload), + content_type="application/json", + ) + + assert response.status_code == 201 + response_data = json.loads(response.content) + assert response_data["message"] == "Successfully enabled service" + + org = Organization.objects.get(osb_guid="test-org-guid-123") + assert org.name == "Test Organization Display" + assert org.billing_entity == exoscale_origin.billing_entity + + @pytest.mark.django_db def test_duplicate_organization_returns_existing( osb_client,