solve bugs

This commit is contained in:
Raykov-MS 2026-07-08 12:50:21 +03:00
parent 5ddf7479a4
commit ccba554b86
4 changed files with 142 additions and 14 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

@ -1739,7 +1739,6 @@ AS $function$
DECLARE DECLARE
v_old RECORD; v_old RECORD;
v_usr RECORD; v_usr RECORD;
v_closed DATE;
BEGIN BEGIN
SELECT vsp.id, vsp.branch_id SELECT vsp.id, vsp.branch_id
INTO v_old INTO v_old
@ -1755,18 +1754,15 @@ BEGIN
END IF; END IF;
UPDATE v3.vsp SET is_deleted = true, updated_by = p_deleted_by WHERE id = p_vsp_id; UPDATE v3.vsp
UPDATE v3.vsp SET closed_at = CURRENT_DATE SET is_deleted = true,
WHERE id = p_vsp_id AND closed_at IS NULL is_active = false,
RETURNING closed_at INTO v_closed; updated_by = p_deleted_by
WHERE id = p_vsp_id;
IF v_closed IS NULL THEN UPDATE v3.vsp
IF EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN SET closed_at = CURRENT_DATE
RAISE EXCEPTION 'vsp #% уже закрыт', p_vsp_id; WHERE id = p_vsp_id
ELSE AND closed_at IS NULL;
RAISE EXCEPTION 'vsp #% не существует', p_vsp_id;
END IF;
END IF;
PERFORM v3.log_event( PERFORM v3.log_event(
'DELETE_VSP', 'VSP', 'DELETE_VSP', 'VSP',

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),
) )