Skip to content

Commit

Permalink
check brand uniqueness in sync command
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastienReuiller committed Dec 2, 2024
1 parent 47d5d04 commit 0bc0f40
Show file tree
Hide file tree
Showing 3 changed files with 270 additions and 7 deletions.
40 changes: 34 additions & 6 deletions lemarche/siaes/management/commands/sync_with_emplois_inclusion.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import logging
import os
import re

from django.conf import settings
from django.contrib.gis.geos import GEOSGeometry
from django.core.management.base import CommandError
from django.db.models import Q
from django.utils import timezone
from stdnum.fr import siret

Expand All @@ -15,6 +17,9 @@
from lemarche.utils.data import rename_dict_key


logger = logging.getLogger(__name__)


UPDATE_FIELDS = [
# "name", # what happens to the slug if the name is updated?
# "brand", # see UPDATE_FIELDS_IF_EMPTY
Expand Down Expand Up @@ -245,11 +250,21 @@ def c4_create_siae(self, c1_siae, dry_run):
c1_siae["contact_email"] = c1_siae["admin_email"] or c1_siae["email"]
c1_siae["contact_phone"] = c1_siae["phone"]

# create object
# create object if brand is empty or not already used
if not dry_run:
siae = Siae.objects.create(**c1_siae)
self.add_siae_to_contact_list(siae)
self.stdout_info(f"New Siae created / {siae.id} / {siae.name} / {siae.siret}")
if (
"brand" not in c1_siae
or c1_siae["brand"] == ""
or not Siae.objects.filter(Q(name=c1_siae["brand"]) | Q(brand=c1_siae["brand"])).exists()
):
siae = Siae.objects.create(**c1_siae)

self.add_siae_to_contact_list(siae)
self.stdout_info(f"New Siae created / {siae.id} / {siae.name} / {siae.siret}")
else:
logger.error(
f"Brand name is already used by another SIAE: '{c1_siae['brand']}' / name: '{c1_siae['name']}'"
)

def add_siae_to_contact_list(self, siae: Siae):
if siae.kind != "OPCS" and siae.is_active:
Expand All @@ -276,8 +291,21 @@ def c4_update_siae(self, c1_siae, c4_siae, dry_run):

# update fields only if empty
for key in UPDATE_FIELDS_IF_EMPTY:
if key in c1_siae and not c4_siae[key]:
if key in c1_siae and not getattr(c4_siae, key, None):
c1_siae_filtered[key] = c1_siae[key]

Siae.objects.filter(c1_id=c4_siae.c1_id).update(**c1_siae_filtered) # avoid updated_at change
# update siae only if brand is empty or not already used
if (
"brand" not in c1_siae_filtered
or c1_siae_filtered["brand"] == ""
or not Siae.objects.exclude(c1_id=c4_siae.c1_id)
.filter(Q(name=c1_siae_filtered["brand"]) | Q(brand=c1_siae_filtered["brand"]))
.exists()
):
Siae.objects.filter(c1_id=c4_siae.c1_id).update(**c1_siae_filtered) # avoid updated_at change
else:
logger.error(
f"Brand name is already used by another SIAE: '{c1_siae['brand']}' / name: '{c1_siae['name']}'"
)

# self.stdout_info(f"Siae updated / {c4_siae.id} / {c4_siae.siret}")
236 changes: 235 additions & 1 deletion lemarche/siaes/tests/test_commands.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import logging
import os
from unittest.mock import patch

from django.core.management import call_command
from django.test import TransactionTestCase

Expand All @@ -6,7 +10,237 @@
from lemarche.sectors.factories import SectorFactory
from lemarche.siaes import constants as siae_constants
from lemarche.siaes.factories import SiaeFactory
from lemarche.siaes.models import SiaeActivity
from lemarche.siaes.models import Siae, SiaeActivity


class SyncWithEmploisInclusionCommandTest(TransactionTestCase):
def setUp(self):
logging.disable(logging.DEBUG) # setting in tests disables all logging by default

@patch("lemarche.utils.apis.api_emplois_inclusion.get_siae_list")
def test_sync_with_emplois_inclusion_create_new_siae(self, mock_get_siae_list):
# Mock API response for a new SIAE
mock_get_siae_list.return_value = [
{
"id": 123,
"siret": "12345678901234",
"naf": "8899B",
"kind": "EI",
"name": "New SIAE",
"brand": "",
"phone": "",
"email": "",
"website": "",
"description": "",
"address_line_1": "1 rue Test",
"address_line_2": "",
"post_code": "37000",
"city": "Tours",
"department": "37",
"source": "ASP",
"latitude": 0,
"longitude": 0,
"convention_is_active": True,
"convention_asp_id": 0,
"admin_name": "",
"admin_email": "",
}
]

# Verify SIAE doesn't exist
self.assertEqual(Siae.objects.count(), 0)

# Run command
os.environ["API_EMPLOIS_INCLUSION_TOKEN"] = "test"
with self.assertNoLogs("lemarche.siaes.management.commands.sync_with_emplois_inclusion"):
call_command("sync_with_emplois_inclusion")

# Verify SIAE was created
self.assertEqual(Siae.objects.count(), 1)
siae = Siae.objects.first()
self.assertEqual(siae.siret, "12345678901234")
self.assertEqual(siae.name, "New SIAE")
self.assertEqual(siae.brand, "")

@patch("lemarche.utils.apis.api_emplois_inclusion.get_siae_list")
def test_sync_with_emplois_inclusion_update_existing_siae(self, mock_get_siae_list):
# Create existing SIAE
existing_siae = SiaeFactory(c1_id=123, siret="12345678901234", kind=siae_constants.KIND_EI)

return_value = [
{
"id": 123,
"siret": "12345678901234",
"naf": "8899B",
"kind": "EI",
"name": "New SIAE",
"brand": "Updated Name",
"phone": "",
"email": "",
"website": "",
"description": "",
"address_line_1": "2 rue Test",
"address_line_2": "",
"post_code": "69001",
"city": "Lyon",
"department": "69",
"source": "ASP",
"latitude": 0,
"longitude": 0,
"convention_is_active": True,
"convention_asp_id": 0,
"admin_name": "",
"admin_email": "",
}
]

# Mock API response with updated data
mock_get_siae_list.return_value = return_value

# Run command
os.environ["API_EMPLOIS_INCLUSION_TOKEN"] = "test"
with self.assertNoLogs("lemarche.siaes.management.commands.sync_with_emplois_inclusion"):
call_command("sync_with_emplois_inclusion")

# Verify SIAE was updated
self.assertEqual(Siae.objects.count(), 1)
updated_siae = Siae.objects.get(id=existing_siae.id)
self.assertEqual(updated_siae.brand, "Updated Name") # update first time
self.assertEqual(updated_siae.address, "2 rue Test")
self.assertEqual(updated_siae.post_code, "69001")
self.assertEqual(updated_siae.city, "Lyon")
self.assertEqual(updated_siae.department, "69")

# Mock API response with updated data for the same SIAE with different brand name
return_value[0]["brand"] = "Other Name"
mock_get_siae_list.return_value = return_value

# Run command
with self.assertNoLogs("lemarche.siaes.management.commands.sync_with_emplois_inclusion"):
call_command("sync_with_emplois_inclusion")

# Verify SIAE was updated
self.assertEqual(Siae.objects.count(), 1)
updated_siae.refresh_from_db()
self.assertEqual(updated_siae.brand, "Updated Name") # Brand name can only be updated once

@patch("lemarche.utils.apis.api_emplois_inclusion.get_siae_list")
def test_sync_with_emplois_inclusion_with_duplicate_brand_name_on_create(self, mock_get_siae_list):
# Create existing SIAE with the same brand name
SiaeFactory(siret="98765432101233", brand="Duplicate Brand", kind=siae_constants.KIND_EI)

# Mock API response with duplicate brand name
mock_get_siae_list.return_value = [
{
"id": 123,
"siret": "12345678901234",
"naf": "8899B",
"kind": "EI",
"name": "New SIAE",
"brand": "Duplicate Brand",
"phone": "",
"email": "",
"website": "",
"description": "",
"address_line_1": "1 rue Test",
"address_line_2": "",
"post_code": "37000",
"city": "Tours",
"department": "37",
"source": "ASP",
"latitude": 0,
"longitude": 0,
"convention_is_active": True,
"convention_asp_id": 0,
"admin_name": "",
"admin_email": "",
}
]

# Run command (should not raise exception)
os.environ["API_EMPLOIS_INCLUSION_TOKEN"] = "test"
with self.assertLogs("lemarche.siaes.management.commands.sync_with_emplois_inclusion", level="ERROR") as log:
call_command("sync_with_emplois_inclusion")

# Verify warning was logged
self.assertIn("Brand name is already used by another SIAE", log.output[0])

# Verify both SIAEs exist
self.assertEqual(Siae.objects.count(), 1)

@patch("lemarche.utils.apis.api_emplois_inclusion.get_siae_list")
def test_sync_with_emplois_inclusion_with_duplicate_brand_name_on_update(self, mock_get_siae_list):
# Create existing SIAE with the same brand name
SiaeFactory(siret="98765432101233", brand="Duplicate Brand", kind=siae_constants.KIND_EI)
SiaeFactory(siret="98765432101234", c1_id=123, kind=siae_constants.KIND_EI)

self.assertEqual(Siae.objects.count(), 2)

# Mock API response with duplicate brand name
mock_get_siae_list.return_value = [
{
"id": 123,
"siret": "12345678901234",
"naf": "8899B",
"kind": "EI",
"name": "New SIAE",
"brand": "Duplicate Brand",
"phone": "",
"email": "",
"website": "",
"description": "",
"address_line_1": "1 rue Test",
"address_line_2": "",
"post_code": "37000",
"city": "Tours",
"department": "37",
"source": "ASP",
"latitude": 0,
"longitude": 0,
"convention_is_active": True,
"convention_asp_id": 0,
"admin_name": "",
"admin_email": "",
},
{
"id": 124,
"siret": "12345678901235",
"naf": "8899B",
"kind": "EI",
"name": "Other New SIAE",
"brand": "",
"phone": "",
"email": "",
"website": "",
"description": "",
"address_line_1": "1 rue Test",
"address_line_2": "",
"post_code": "37000",
"city": "Tours",
"department": "37",
"source": "ASP",
"latitude": 0,
"longitude": 0,
"convention_is_active": True,
"convention_asp_id": 0,
"admin_name": "",
"admin_email": "",
},
]

# Run command (should not raise exception)
os.environ["API_EMPLOIS_INCLUSION_TOKEN"] = "test"
with self.assertLogs("lemarche.siaes.management.commands.sync_with_emplois_inclusion", level="ERROR") as log:
call_command("sync_with_emplois_inclusion")

# Verify warning was logged
self.assertIn("Brand name is already used by another SIAE: 'Duplicate Brand'", log.output[0])

# Verify both SIAEs exist
self.assertEqual(Siae.objects.count(), 3)
self.assertEqual(Siae.objects.filter(brand="Duplicate Brand").count(), 1)

self.assertEqual(Siae.objects.filter(name="Other New SIAE").count(), 1) # error logged but sync continued


class SiaeActivitiesCreateCommandTest(TransactionTestCase):
Expand Down
1 change: 1 addition & 0 deletions lemarche/utils/apis/api_emplois_inclusion.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
logger = logging.getLogger(__name__)


# Doc : https://emplois.inclusion.beta.gouv.fr/api/v1/redoc/#tag/marche/operation/marche_list
API_ENDPOINT = f"{settings.API_EMPLOIS_INCLUSION_URL}/marche"
API_HEADERS = {"Authorization": f"Token {settings.API_EMPLOIS_INCLUSION_TOKEN}"}

Expand Down

0 comments on commit 0bc0f40

Please sign in to comment.