AURORA-1174-73 #29

Merged
Raykov-MS merged 3 commits from AURORA-1174-73 into test 2026-07-09 10:30:22 +03:00
3 changed files with 133 additions and 1 deletions

View File

@ -0,0 +1,63 @@
from alembic import op
revision = "0004"
down_revision = "0003"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"""
CREATE OR REPLACE FUNCTION v3.del_vsp(p_vsp_id integer, p_deleted_by integer)
RETURNS integer
LANGUAGE plpgsql
AS $function$
DECLARE
v_old RECORD;
v_usr RECORD;
BEGIN
SELECT vsp.id, vsp.branch_id
INTO v_old
FROM v3.vsp vsp WHERE vsp.id = p_vsp_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'vsp #% не существует', p_vsp_id;
END IF;
SELECT usr.id
INTO v_usr
FROM v3.app_user usr WHERE usr.id = p_deleted_by;
IF NOT FOUND THEN
RAISE EXCEPTION 'user #% не существует', p_deleted_by;
END IF;
UPDATE v3.vsp
SET is_deleted = true,
is_active = false,
updated_by = p_deleted_by
WHERE id = p_vsp_id;
UPDATE v3.vsp
SET closed_at = CURRENT_DATE
WHERE id = p_vsp_id
AND closed_at IS NULL;
PERFORM v3.log_event(
'DELETE_VSP', 'VSP',
jsonb_build_object(
'vsp_id', p_vsp_id,
'entity_id', p_vsp_id,
'branch_id', v_old.branch_id,
'updated_by', p_deleted_by
),
null, null, v_old.branch_id
);
RETURN p_vsp_id;
END;
$function$;
"""
)
def downgrade() -> None:
pass

View File

@ -11,6 +11,7 @@ from src.repository.budget_form_repository import BudgetFormRepository
from src.core.errors import AccessDeniedException, ValidationException from src.core.errors import AccessDeniedException, ValidationException
from src.db.models import AppUser, UserRoleEnum, Vsp from src.db.models import AppUser, UserRoleEnum, Vsp
from src.domain.schemas import VSPCreate, VSPUpdate from src.domain.schemas import VSPCreate, VSPUpdate
from src.repository.org_unit_repository import OrgUnitRepository
from src.repository.user_repository import UserRepository from src.repository.user_repository import UserRepository
from src.repository.vsp_repository import VSPRepository from src.repository.vsp_repository import VSPRepository
@ -31,6 +32,7 @@ class VSPService:
self.db = db self.db = db
self.vsp_repo = VSPRepository(db) self.vsp_repo = VSPRepository(db)
self.form_repo = BudgetFormRepository(db) self.form_repo = BudgetFormRepository(db)
self.org_unit_repo = OrgUnitRepository(db)
self.user_repo = UserRepository(db) self.user_repo = UserRepository(db)
async def get(self, vsp_id: int, user: AppUser, load_org_unit: bool = False) -> Optional[Vsp]: async def get(self, vsp_id: int, user: AppUser, load_org_unit: bool = False) -> Optional[Vsp]:
@ -128,6 +130,7 @@ class VSPService:
async def create(self, payload: VSPCreate, user: AppUser, load_org_unit: bool = False) -> Vsp: async def create(self, payload: VSPCreate, user: AppUser, load_org_unit: bool = False) -> Vsp:
if not self._can_modify(user): if not self._can_modify(user):
raise AccessDeniedException() raise AccessDeniedException()
await self._ensure_active_branch(payload.branch_id)
try: try:
created = await self.vsp_repo.create(payload=payload, created_by=user.id, load_org_unit=load_org_unit) created = await self.vsp_repo.create(payload=payload, created_by=user.id, load_org_unit=load_org_unit)
if created.closed_at is not None and created.closed_at <= date.today(): if created.closed_at is not None and created.closed_at <= date.today():
@ -245,6 +248,9 @@ class VSPService:
if not data: if not data:
return vsp return vsp
if "branch_id" in data:
await self._ensure_active_branch(data["branch_id"])
if user.role_id != UserRoleEnum.ADMIN: if user.role_id != UserRoleEnum.ADMIN:
ssp_ids = await self._get_scoped_ssp_ids(user=user) ssp_ids = await self._get_scoped_ssp_ids(user=user)
if not ssp_ids or vsp.branch_id not in set(ssp_ids): if not ssp_ids or vsp.branch_id not in set(ssp_ids):
@ -291,3 +297,10 @@ class VSPService:
return return
await self.vsp_repo.deactivate_by_close_date(as_of=today) await self.vsp_repo.deactivate_by_close_date(as_of=today)
_last_info_close_sync_run = today _last_info_close_sync_run = today
async def _ensure_active_branch(self, branch_id: int | None) -> None:
if branch_id is None:
return
org_unit = await self.org_unit_repo.get(org_unit_id=branch_id)
if org_unit is None or not org_unit.is_active:
raise ValidationException(description="Указан неактивный или отсутствующий ССП/РФ")

View File

@ -150,6 +150,36 @@ def test_vsp_create_forbidden_for_executor(client, isp_tokens, auth_headers):
assert "message" in payload assert "message" in payload
def test_vsp_create_with_inactive_ssp_fails(client, admin_tokens, auth_headers):
created_org = client.post(
"/api/v1/org-unit/",
json={"title": f"Inactive SSP {uuid.uuid4().hex[:6]}", "is_ssp": True},
headers=auth_headers(admin_tokens),
)
assert created_org.status_code == 200
org_unit_id = created_org.json()["result"]["id"]
deactivate = client.patch(
f"/api/v1/org-unit/{org_unit_id}",
json={"is_active": False},
headers=auth_headers(admin_tokens),
)
assert deactivate.status_code == 200
response = client.post(
"/api/v1/dict/info",
json={
"registration_number": _unique_reg_number(),
"address": "Should Fail",
"ssp_id": org_unit_id,
},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 400
payload = response.json()
assert "message" in payload
def test_vsp_update_smoke(client, admin_tokens, auth_headers): def test_vsp_update_smoke(client, admin_tokens, auth_headers):
reg_number = _unique_reg_number() reg_number = _unique_reg_number()
created = client.post( created = client.post(
@ -195,7 +225,33 @@ def test_vsp_delete_smoke(client, admin_tokens, auth_headers):
json={ json={
"registration_number": reg_number, "registration_number": reg_number,
"address": "To Be Deleted", "address": "To Be Deleted",
"ssp_id": 1, "ssp_id": 2,
},
headers=auth_headers(admin_tokens),
)
assert created.status_code == 201
vsp_id = created.json()["result"]["id"]
response = client.delete(
f"/api/v1/dict/info/{vsp_id}",
headers=auth_headers(admin_tokens),
)
assert response.status_code == 200
payload = response.json()
assert payload["success"] is True
assert payload["message"] == "Запись INFO удалена"
def test_vsp_delete_with_close_date_smoke(client, admin_tokens, auth_headers):
reg_number = _unique_reg_number()
created = client.post(
"/api/v1/dict/info",
json={
"registration_number": reg_number,
"address": "To Be Deleted with close_date",
"ssp_id": 2,
"open_date": "2026-01-01",
"close_date": "2030-01-01",
}, },
headers=auth_headers(admin_tokens), headers=auth_headers(admin_tokens),
) )