diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index 5c9eacd..484e6dd 100644 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -25,7 +25,7 @@ SessionDep = Annotated[AsyncSession, Depends(get_session)] _UNAUTHORIZED = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Требуется аутентификация", + detail="Потрібна автентифікація", headers={"WWW-Authenticate": "Bearer"}, ) @@ -76,7 +76,7 @@ def require_roles(*roles: UserRole) -> Callable[..., Coroutine[Any, Any, User]]: if user.role not in allowed: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Недостаточно прав для этого действия", + detail="Недостатньо прав для цієї дії", ) return user diff --git a/backend/app/api/v1/auth.py b/backend/app/api/v1/auth.py index f489acd..e346d3f 100644 --- a/backend/app/api/v1/auth.py +++ b/backend/app/api/v1/auth.py @@ -70,7 +70,7 @@ async def change_password( ) -> Response: if not verify_password(payload.current_password, user.password_hash): raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail="Текущий пароль указан неверно" + status_code=status.HTTP_400_BAD_REQUEST, detail="Поточний пароль вказано неправильно" ) user.password_hash = hash_password(payload.new_password) diff --git a/backend/app/api/v1/cash_registers.py b/backend/app/api/v1/cash_registers.py index 3b5b180..edbe0e5 100644 --- a/backend/app/api/v1/cash_registers.py +++ b/backend/app/api/v1/cash_registers.py @@ -44,7 +44,7 @@ async def _ensure_np_key_unique( if same: raise HTTPException( status_code=status.HTTP_409_CONFLICT, - detail=f"Этот ключ Новой Почты уже привязан к кассе «{register.name}»", + detail=f"Цей ключ Нової Пошти вже прив'язаний до каси «{register.name}»", ) @@ -101,7 +101,7 @@ async def update_cash_register( ) -> CashRegisterOut: register = await session.get(CashRegister, register_id) if register is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Касса не найдена") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Касу не знайдено") changes = payload.model_dump(exclude_unset=True) if "license_key" in changes: @@ -139,7 +139,7 @@ async def check_cash_register( """Пробный вход кассира в Checkbox — проверка ключа лицензии и PIN.""" register = await session.get(CashRegister, register_id) if register is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Касса не найдена") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Касу не знайдено") try: await checkbox.sign_in(credentials(register)) except (CheckboxError, crypto.DecryptionError) as exc: diff --git a/backend/app/api/v1/orders.py b/backend/app/api/v1/orders.py index caa97c8..33db176 100644 --- a/backend/app/api/v1/orders.py +++ b/backend/app/api/v1/orders.py @@ -50,7 +50,7 @@ async def update_order( except orders_service.OrderEditError as exc: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc if result is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Заказ не найден") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Замовлення не знайдено") order, changed = result if changed: @@ -75,7 +75,7 @@ async def delete_order( ) -> None: order = await orders_service.delete_order(session, order_id) if order is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Заказ не найден") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Замовлення не знайдено") await audit.record( session, diff --git a/backend/app/api/v1/receipts.py b/backend/app/api/v1/receipts.py index 2b44e0c..713dcf6 100644 --- a/backend/app/api/v1/receipts.py +++ b/backend/app/api/v1/receipts.py @@ -60,7 +60,7 @@ async def list_receipts(session: SessionDep, order_id: str | None = None) -> lis async def get_receipt(receipt_id: uuid.UUID, session: SessionDep) -> ReceiptOut: receipt = await session.get(Receipt, receipt_id) if receipt is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Чек не найден") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Чек не знайдено") return ReceiptOut.from_receipt(receipt) @@ -83,5 +83,5 @@ async def cancel_receipt( status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Checkbox: {exc}" ) from exc if receipt is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Чек не найден") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Чек не знайдено") return ReceiptOut.from_receipt(receipt) diff --git a/backend/app/api/v1/users.py b/backend/app/api/v1/users.py index 6e3e4a1..676c930 100644 --- a/backend/app/api/v1/users.py +++ b/backend/app/api/v1/users.py @@ -71,7 +71,7 @@ async def create_user( await session.rollback() raise HTTPException( status_code=status.HTTP_409_CONFLICT, - detail="Пользователь с таким email уже существует", + detail="Користувач із таким email уже існує", ) from exc return UserOut.model_validate(user) @@ -87,7 +87,7 @@ async def update_user( ) -> UserOut: user = await session.get(User, user_id) if user is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Користувача не знайдено") changes = payload.model_dump(exclude_unset=True) @@ -97,12 +97,12 @@ async def update_user( if changes.get("is_active") is False: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Нельзя отключить собственную учётную запись", + detail="Не можна вимкнути власний обліковий запис", ) if "role" in changes and changes["role"] != UserRole.ADMIN: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Нельзя снять с себя роль администратора", + detail="Не можна зняти із себе роль адміністратора", ) if (new_password := changes.pop("password", None)) is not None: diff --git a/backend/app/cli.py b/backend/app/cli.py index 6b2f222..65d42dd 100644 --- a/backend/app/cli.py +++ b/backend/app/cli.py @@ -25,7 +25,7 @@ async def bootstrap() -> int: Идемпотентна: повторный запуск ничего не меняет и пароль не сбрасывает. """ if not settings.first_admin_password: - print("FIRST_ADMIN_PASSWORD не задан в .env", file=sys.stderr) + print("FIRST_ADMIN_PASSWORD не задано в .env", file=sys.stderr) return 1 email = settings.first_admin_email.strip().lower() @@ -33,7 +33,7 @@ async def bootstrap() -> int: async with session_scope() as session: existing = await session.scalar(select(User).where(User.email == email)) if existing is not None: - print(f"Пользователь {email} уже существует — ничего не изменено.") + print(f"Користувач {email} уже існує — нічого не змінено.") return 0 session.add( @@ -45,8 +45,8 @@ async def bootstrap() -> int: ) ) - print(f"Администратор {email} создан.") - print("Смените пароль после первого входа и уберите FIRST_ADMIN_PASSWORD из .env.") + print(f"Адміністратора {email} створено.") + print("Змініть пароль після першого входу та приберіть FIRST_ADMIN_PASSWORD з .env.") return 0 @@ -59,7 +59,7 @@ def gen_keys() -> int: def main() -> int: - parser = argparse.ArgumentParser(prog="app.cli", description="Служебные команды lux_fiscal") + parser = argparse.ArgumentParser(prog="app.cli", description="Службові команди lux_fiscal") parser.add_argument("command", choices=["bootstrap", "gen-keys"]) args = parser.parse_args() diff --git a/backend/app/core/config.py b/backend/app/core/config.py index a5bf2d9..2a0c2bd 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -53,7 +53,7 @@ class Settings(BaseSettings): # --- Первый администратор (только для команды bootstrap) --- first_admin_email: str = "admin@example.com" first_admin_password: str = "" - first_admin_name: str = "Администратор" + first_admin_name: str = "Адміністратор" # --- CRM --- crm_base_url: str = "https://optstore.exocrm.com/api/1.1/" diff --git a/backend/app/core/crypto.py b/backend/app/core/crypto.py index 2eb418d..b71b00f 100644 --- a/backend/app/core/crypto.py +++ b/backend/app/core/crypto.py @@ -26,7 +26,7 @@ def _fernet() -> Fernet: return Fernet(settings.encryption_key.encode()) except (ValueError, TypeError) as exc: raise RuntimeError( - "ENCRYPTION_KEY некорректен. Сгенерируйте валидный ключ: " + "ENCRYPTION_KEY некоректний. Згенеруйте валідний ключ: " "python -c \"from cryptography.fernet import Fernet; " 'print(Fernet.generate_key().decode())"' ) from exc @@ -41,8 +41,8 @@ def decrypt(value: str) -> str: return _fernet().decrypt(value.encode()).decode() except InvalidToken as exc: raise DecryptionError( - "Не удалось расшифровать значение. Вероятная причина — ENCRYPTION_KEY " - "изменился с момента сохранения. Секрет нужно ввести заново." + "Не вдалося розшифрувати значення. Імовірна причина — ENCRYPTION_KEY " + "змінився з моменту збереження. Секрет потрібно ввести заново." ) from exc diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 075e6af..10630d9 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -67,13 +67,13 @@ def decode_access_token(token: str) -> dict[str, Any]: try: payload = jwt.decode(token, settings.secret_key, algorithms=[ALGORITHM]) except jwt.ExpiredSignatureError as exc: - raise TokenError("Срок действия токена истёк") from exc + raise TokenError("Термін дії токена минув") from exc except jwt.InvalidTokenError as exc: - raise TokenError("Некорректный токен") from exc + raise TokenError("Некоректний токен") from exc # Без этой проверки refresh-токен можно было бы предъявить как access. if payload.get("type") != "access": - raise TokenError("Ожидался access-токен") + raise TokenError("Очікувався access-токен") return payload diff --git a/backend/app/main.py b/backend/app/main.py index ca94427..0a1498d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -78,7 +78,7 @@ def create_app() -> FastAPI: app = FastAPI( title=settings.project_name, version="0.1.0", - description="Фискализация заказов через Checkbox", + description="Фіскалізація замовлень через Checkbox", lifespan=lifespan, # В проде интерактивная документация закрыта: схема API — лишняя # подсказка для того, кто ищет незащищённый эндпоинт. diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index e57a056..3164515 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -22,7 +22,7 @@ class TokenPair(BaseModel): access_token: str refresh_token: str token_type: str = "bearer" - expires_in: int = Field(description="Время жизни access-токена в секундах") + expires_in: int = Field(description="Час життя access-токена в секундах") class UserOut(BaseModel): diff --git a/backend/app/schemas/receipts.py b/backend/app/schemas/receipts.py index 074e2ff..1f8bd85 100644 --- a/backend/app/schemas/receipts.py +++ b/backend/app/schemas/receipts.py @@ -74,7 +74,7 @@ def _masked(encrypted: str) -> str: try: return crypto.mask(crypto.decrypt(encrypted)) except crypto.DecryptionError: - return "не расшифровывается — введите заново" + return "не розшифровується — введіть заново" class CashRegisterOut(BaseModel): diff --git a/backend/app/services/auth.py b/backend/app/services/auth.py index 2e8cd10..15ce5f3 100644 --- a/backend/app/services/auth.py +++ b/backend/app/services/auth.py @@ -53,7 +53,7 @@ async def authenticate( request=request, ) await session.commit() - raise AuthError("Неверный email или пароль") + raise AuthError("Невірний email або пароль") if not user.is_active: await audit.record( @@ -64,7 +64,7 @@ async def authenticate( request=request, ) await session.commit() - raise AuthError("Учётная запись отключена") + raise AuthError("Обліковий запис вимкнено") # Параметры argon2 со временем ужесточаются — обновляем хеш на живом пароле. if password_needs_rehash(user.password_hash): @@ -117,7 +117,7 @@ async def rotate_refresh_token( stored = await session.scalar(select(RefreshToken).where(RefreshToken.token_hash == token_hash)) if stored is None: - raise AuthError("Некорректный refresh-токен") + raise AuthError("Некоректний refresh-токен") if stored.revoked_at is not None: await revoke_all_for_user(session, stored.user_id) @@ -131,14 +131,14 @@ async def rotate_refresh_token( ) await session.commit() log.warning("refresh_token_reuse_detected", user_id=str(stored.user_id)) - raise AuthError("Сессия отозвана, требуется повторный вход") + raise AuthError("Сесію відкликано, потрібен повторний вхід") if stored.expires_at <= datetime.now(UTC): - raise AuthError("Срок действия refresh-токена истёк") + raise AuthError("Термін дії refresh-токена минув") user = await session.get(User, stored.user_id) if user is None or not user.is_active: - raise AuthError("Учётная запись недоступна") + raise AuthError("Обліковий запис недоступний") pair = await issue_token_pair(session, user, request=request, replaces=stored) await audit.record( diff --git a/backend/app/services/checkbox/client.py b/backend/app/services/checkbox/client.py index 509b008..9cd96c1 100644 --- a/backend/app/services/checkbox/client.py +++ b/backend/app/services/checkbox/client.py @@ -63,7 +63,7 @@ def get_checkbox_client() -> CheckboxClient: """Один экземпляр на процесс: внутри — кэш токенов кассиров.""" if settings.checkbox_use_stub: if settings.is_production: - raise RuntimeError("CHECKBOX_USE_STUB=true запрещён в production") + raise RuntimeError("CHECKBOX_USE_STUB=true заборонено в production") from app.services.checkbox.stub_client import StubCheckboxClient return StubCheckboxClient(auto_complete_after=2) diff --git a/backend/app/services/checkbox/http_client.py b/backend/app/services/checkbox/http_client.py index 229d8e2..856a470 100644 --- a/backend/app/services/checkbox/http_client.py +++ b/backend/app/services/checkbox/http_client.py @@ -57,7 +57,7 @@ def _error_message(response: httpx.Response) -> str: # {"code": "third_party.*", "message": "Internal Server Error"} — # без кода кассир видит только бесполезное «Internal Server Error». if code := data.get("code"): - message = f"{message or 'Ошибка'} ({code})" + message = f"{message or 'Помилка'} ({code})" if isinstance(detail, list) and detail: parts = [ f"{'.'.join(str(p) for p in item.get('loc', []))}: {item.get('msg')}" @@ -130,7 +130,7 @@ class HttpCheckboxClient: method, path, headers=headers, json=json, params=params ) except httpx.HTTPError as exc: - raise CheckboxUnavailableError(f"Checkbox недоступен: {exc!r}") from exc + raise CheckboxUnavailableError(f"Checkbox недоступний: {exc!r}") from exc if error := _transient_error(response): raise error return response @@ -155,7 +155,7 @@ class HttpCheckboxClient: json={"pin_code": creds.pin_code}, ) if response.status_code != 200: - raise CheckboxError(f"Вход кассира не удался: {_error_message(response)}") + raise CheckboxError(f"Вхід касира не вдався: {_error_message(response)}") token = response.json()["access_token"] self._tokens[creds.cash_register_id] = token return token diff --git a/backend/app/services/checkbox/stub_client.py b/backend/app/services/checkbox/stub_client.py index 71d66fa..29ce6f3 100644 --- a/backend/app/services/checkbox/stub_client.py +++ b/backend/app/services/checkbox/stub_client.py @@ -32,12 +32,12 @@ class StubCheckboxClient: async def sign_in(self, creds: CheckboxCredentials) -> None: if not creds.license_key or not creds.pin_code: - raise CheckboxError("Вход кассира не удался: пустой ключ или PIN") + raise CheckboxError("Вхід касира не вдався: порожній ключ або PIN") async def create_ettn(self, creds: CheckboxCredentials, body: dict[str, Any]) -> EttnOut: waybill = body["receipt_body"]["payments"][0]["ettn"] if await self.find_ettn(creds, waybill) is not None: - raise CheckboxError(f"ЕТТН {waybill} уже привязана к чеку") + raise CheckboxError(f"ЕТТН {waybill} вже прив'язана до чека") ettn = EttnOut( id=str(uuid.uuid4()), status=EttnStatus.CREATED, @@ -50,7 +50,7 @@ class StubCheckboxClient: async def get_ettn(self, creds: CheckboxCredentials, ettn_id: str) -> EttnOut: if ettn_id not in self.orders: - raise CheckboxError(f"ЕТТН-чек {ettn_id} не найден") + raise CheckboxError(f"ЕТТН-чек {ettn_id} не знайдено") self._polls[ettn_id] = self._polls.get(ettn_id, 0) + 1 if ( self.auto_complete_after is not None diff --git a/backend/app/services/crm/exo_client.py b/backend/app/services/crm/exo_client.py index ca7ccb3..afcea79 100644 --- a/backend/app/services/crm/exo_client.py +++ b/backend/app/services/crm/exo_client.py @@ -39,10 +39,10 @@ class ExoCrmClient: data = response.json() except ValueError as exc: # PHP-notice'ы CRM перед JSON — признак неверных параметров. - raise CrmError(f"CRM вернула не JSON: {response.text[:300]}") from exc + raise CrmError(f"CRM повернула не JSON: {response.text[:300]}") from exc if not isinstance(data, dict): - raise CrmError(f"Неожиданный ответ CRM: {str(data)[:300]}") + raise CrmError(f"Неочікувана відповідь CRM: {str(data)[:300]}") return data async def get_orders(self, *, status: str) -> list[OrderOut]: @@ -57,7 +57,7 @@ class ExoCrmClient: }, ) if data.get("status") != "OK": - raise CrmError(f"CRM вернула ошибку: {_errors_text(data) or 'неизвестная ошибка'}") + raise CrmError(f"CRM повернула помилку: {_errors_text(data) or 'невідома помилка'}") return [OrderOut.model_validate(order) for order in data.get("result") or []] async def set_status(self, *, order_id: str, status: str) -> None: @@ -73,6 +73,6 @@ class ExoCrmClient: if isinstance(result, dict) and result.get("Status") == "Success": return details = _errors_text(data) or ( - str(result) if result is not None else "нет ответа по заказу" + str(result) if result is not None else "немає відповіді щодо замовлення" ) - raise CrmError(f"CRM не сменила статус заказа {order_id} на {status}: {details}") + raise CrmError(f"CRM не змінила статус замовлення {order_id} на {status}: {details}") diff --git a/backend/app/services/crm/stub_client.py b/backend/app/services/crm/stub_client.py index 04b4d88..f6c0a68 100644 --- a/backend/app/services/crm/stub_client.py +++ b/backend/app/services/crm/stub_client.py @@ -12,7 +12,7 @@ _FIXTURE_ORDERS: list[dict] = [ "RecipientPhone": "+380501112233", "RecipientEmail": None, "Waybill_Number": "20450123456789", - "Notes": "Тестовий заказ", + "Notes": "Тестове замовлення", "Total": { "Cost": "0.00", "Quantity": "2", diff --git a/backend/app/services/nova_poshta/np_client.py b/backend/app/services/nova_poshta/np_client.py index d6e57d7..57e3526 100644 --- a/backend/app/services/nova_poshta/np_client.py +++ b/backend/app/services/nova_poshta/np_client.py @@ -21,7 +21,7 @@ class NpTrackingClient: return [] if len(waybill_numbers) > _MAX_DOCUMENTS_PER_REQUEST: raise NovaPoshtaError( - f"Слишком много ТТН за один запрос: {len(waybill_numbers)} " + f"Забагато ТТН за один запит: {len(waybill_numbers)} " f"(максимум {_MAX_DOCUMENTS_PER_REQUEST})" ) @@ -42,6 +42,6 @@ class NpTrackingClient: if not data.get("success"): errors = data.get("errors") or [] message = "; ".join(str(error) for error in errors) - raise NovaPoshtaError(f"NP вернул ошибку: {message or 'неизвестная ошибка'}") + raise NovaPoshtaError(f"NP повернув помилку: {message or 'невідома помилка'}") return [TrackingStatusOut.model_validate(item) for item in data.get("data", [])] diff --git a/backend/app/services/orders.py b/backend/app/services/orders.py index 3f6c636..9188775 100644 --- a/backend/app/services/orders.py +++ b/backend/app/services/orders.py @@ -216,7 +216,7 @@ async def sync_np_statuses(session: AsyncSession, np: NovaPoshtaClient) -> None: """ accounts = await _np_accounts(session) if not accounts: - log.warning("np_no_api_keys", hint="Укажите ключ API Новой Почты у кассы") + log.warning("np_no_api_keys", hint="Вкажіть ключ API Нової Пошти в касі") return orders = await session.scalars( @@ -327,7 +327,7 @@ def _build_goods(data: OrderUpdateIn) -> tuple[list[dict[str, Any]], int]: gross = int((price * good.quantity).to_integral_value(ROUND_HALF_UP)) net = gross - discount if net < 0: - raise OrderEditError(f"Товар «{good.name}»: скидка больше суммы строки") + raise OrderEditError(f"Товар «{good.name}»: знижка більша за суму рядка") goods.append( { "id": good.id or f"new-{uuid.uuid4().hex[:8]}", @@ -357,13 +357,13 @@ async def update_order( if order is None or order.is_deleted: return None if order.receipt_created_at is not None: - raise OrderEditError("По заказу уже создан чек — редактирование недоступно") + raise OrderEditError("За замовленням уже створено чек — редагування недоступне") goods, goods_total = _build_goods(data) total = _to_kopecks(str(data.total_amount)) if total > goods_total: raise OrderEditError( - f"Сумма заказа {total / 100:.2f} ₴ больше суммы товаров {goods_total / 100:.2f} ₴" + f"Сума замовлення {total / 100:.2f} ₴ більша за суму товарів {goods_total / 100:.2f} ₴" ) changed = [ diff --git a/backend/app/services/receipts.py b/backend/app/services/receipts.py index c968212..effa5a2 100644 --- a/backend/app/services/receipts.py +++ b/backend/app/services/receipts.py @@ -133,7 +133,7 @@ def build_goods(order: Order, tax_codes: list[Any]) -> tuple[list[dict[str, Any] price = to_kopecks(good["price"]) quantity = to_thousandths(good["quantity"]) if quantity <= 0: - raise ReceiptValidationError(f"Товар «{good['name']}»: количество должно быть больше 0") + raise ReceiptValidationError(f"Товар «{good['name']}»: кількість має бути більшою за 0") gross = _line_sum(price, quantity) # `amount` — сумма строки после скидки CRM; скидку выводим из неё, # а не из discount_amount/percent, чтобы не расходиться с итогом CRM. @@ -141,7 +141,7 @@ def build_goods(order: Order, tax_codes: list[Any]) -> tuple[list[dict[str, Any] discount = gross - net if discount < 0: raise ReceiptValidationError( - f"Товар «{good['name']}»: сумма строки больше цены × количество" + f"Товар «{good['name']}»: сума рядка більша за ціну × кількість" ) payload: dict[str, Any] = { @@ -168,7 +168,7 @@ def build_ettn_body( order_discount = goods_total - amounts.total_kopecks if order_discount < 0: raise ReceiptValidationError( - "Сумма товаров меньше суммы заказа — проверьте заказ в CRM" + "Сума товарів менша за суму замовлення — перевірте замовлення в CRM" ) # Предоплата и скидка заказа — одной обычной скидкой «Знижка», как в чеках из # портала Checkbox. Тип `PRE_PAYMENT` для ЕТТН не годится: Checkbox отвечает @@ -185,7 +185,7 @@ def build_ettn_body( receipt_total = goods_total - order_discount - amounts.prepayment_kopecks if receipt_total != amounts.cod_kopecks: raise ReceiptValidationError( - f"Сумма чека {receipt_total / 100:.2f} ₴ ≠ наложка {amounts.cod_kopecks / 100:.2f} ₴" + f"Сума чека {receipt_total / 100:.2f} ₴ ≠ післяплата {amounts.cod_kopecks / 100:.2f} ₴" ) receipt_body: dict[str, Any] = { @@ -222,30 +222,30 @@ def resolve_amounts(order: Order, prepayment_kopecks: int | None) -> ReceiptAmou `prepayment_kopecks=None` — взять разницу между суммой заказа и наложкой. """ if order.is_deleted: - raise ReceiptValidationError("Заказ удалён") + raise ReceiptValidationError("Замовлення видалено") if not order.waybill_number: - raise ReceiptValidationError("У заказа нет ТТН") + raise ReceiptValidationError("У замовлення немає ТТН") if not order.np_cod_amount_kopecks: - raise ReceiptValidationError("По ТТН нет суммы контроля оплаты (наложки)") + raise ReceiptValidationError("За ТТН немає суми контролю оплати (післяплати)") if order.np_status_code in NP_FINAL_STATUS_CODES: raise ReceiptValidationError( - f"Посылка уже не в пути ({order.np_status or order.np_status_code}) — " - "ЕТТН-чек создать нельзя" + f"Посилка вже не в дорозі ({order.np_status or order.np_status_code}) — " + "ЕТТН-чек створити неможливо" ) if not order.goods: - raise ReceiptValidationError("В заказе нет товаров") + raise ReceiptValidationError("У замовленні немає товарів") total = order.total_amount_kopecks cod = order.np_cod_amount_kopecks prepayment = total - cod if prepayment_kopecks is None else prepayment_kopecks if prepayment < 0: raise ReceiptValidationError( - f"Наложка {cod / 100:.2f} ₴ больше суммы заказа {total / 100:.2f} ₴" + f"Післяплата {cod / 100:.2f} ₴ більша за суму замовлення {total / 100:.2f} ₴" ) if total - prepayment != cod: raise ReceiptValidationError( - f"Сумма заказа {total / 100:.2f} ₴ − предоплата {prepayment / 100:.2f} ₴ " - f"≠ наложка {cod / 100:.2f} ₴" + f"Сума замовлення {total / 100:.2f} ₴ − передоплата {prepayment / 100:.2f} ₴ " + f"≠ післяплата {cod / 100:.2f} ₴" ) return ReceiptAmounts(total_kopecks=total, prepayment_kopecks=prepayment, cod_kopecks=cod) @@ -306,7 +306,7 @@ async def request_receipts( registers = await _active_registers(session) if not registers: for order_id, _ in items: - result.errors[order_id] = "Не настроена касса Checkbox" + result.errors[order_id] = "Не налаштовано касу Checkbox" return result order_ids = [order_id for order_id, _ in items] @@ -319,16 +319,16 @@ async def request_receipts( for order_id, prepayment in items: order = orders.get(order_id) if order is None: - result.errors[order_id] = "Заказ не найден" + result.errors[order_id] = "Замовлення не знайдено" continue if order_id in busy: - result.errors[order_id] = "По заказу уже есть чек" + result.errors[order_id] = "За замовленням уже є чек" continue register = registers.get(order.cash_register_id) if order.cash_register_id else None if register is None: result.errors[order_id] = ( - "Касса не определена: ТТН не найдена ни одним ключом Новой Почты " - "активных касс" + "Касу не визначено: ТТН не знайдено жодним ключем Нової Пошти " + "активних кас" ) continue try: @@ -485,7 +485,7 @@ async def cancel_receipt( if receipt is None: return None if receipt.status not in (ReceiptStatus.CREATED, ReceiptStatus.RECEIPT_ERROR): - raise ReceiptStateError(f"Чек в статусе «{receipt.status}» отменить нельзя") + raise ReceiptStateError(f"Чек у статусі «{receipt.status}» скасувати неможливо") if receipt.checkbox_ettn_id: register = await session.get(CashRegister, receipt.cash_register_id) diff --git a/backend/tests/test_nova_poshta_client.py b/backend/tests/test_nova_poshta_client.py index c132bc3..c9b33a4 100644 --- a/backend/tests/test_nova_poshta_client.py +++ b/backend/tests/test_nova_poshta_client.py @@ -110,7 +110,7 @@ class TestNpTrackingClientGetStatuses: async def test_rejects_too_many_documents(self) -> None: client = NpTrackingClient() - with pytest.raises(NovaPoshtaError, match="Слишком много"): + with pytest.raises(NovaPoshtaError, match="Забагато"): await client.get_statuses( api_key=API_KEY, waybill_numbers=[str(i) for i in range(101)] ) diff --git a/backend/tests/test_orders_router.py b/backend/tests/test_orders_router.py index b2b14cb..87ea009 100644 --- a/backend/tests/test_orders_router.py +++ b/backend/tests/test_orders_router.py @@ -88,7 +88,7 @@ def _patch_orders_service(monkeypatch: pytest.MonkeyPatch) -> None: session: object, order_id: str, data: object ) -> tuple[Order, list[str]] | None: if order_id == "locked": - raise orders_router.orders_service.OrderEditError("По заказу уже создан чек") + raise orders_router.orders_service.OrderEditError("За замовленням уже створено чек") if order_id != "1": return None order = _order(order_id) diff --git a/backend/tests/test_orders_service.py b/backend/tests/test_orders_service.py index 391de36..6109804 100644 --- a/backend/tests/test_orders_service.py +++ b/backend/tests/test_orders_service.py @@ -140,16 +140,16 @@ class TestUpdateOrder: assert order.goods == [stored] def test_total_above_goods_sum_is_rejected(self) -> None: - with pytest.raises(OrderEditError, match="больше суммы товаров"): + with pytest.raises(OrderEditError, match="більша за суму товарів"): _update(_order(), _payload(total_amount="1200.00")) def test_discount_above_line_sum_is_rejected(self) -> None: good = {"name": "Товар", "price": "10.00", "quantity": "1", "discount_amount": "11"} - with pytest.raises(OrderEditError, match="скидка больше"): + with pytest.raises(OrderEditError, match="знижка більша"): _update(_order(), _payload(goods=[good], total_amount="0")) def test_order_with_receipt_is_locked(self) -> None: - with pytest.raises(OrderEditError, match="уже создан чек"): + with pytest.raises(OrderEditError, match="уже створено чек"): _update(_order(receipt_created_at=datetime.now(UTC)), _payload()) def test_missing_or_deleted_order_returns_none(self) -> None: diff --git a/backend/tests/test_receipts_router.py b/backend/tests/test_receipts_router.py index 88a4dd6..67d5cfa 100644 --- a/backend/tests/test_receipts_router.py +++ b/backend/tests/test_receipts_router.py @@ -45,7 +45,9 @@ def queue(monkeypatch: pytest.MonkeyPatch) -> FakeQueue: ) async def fake_request(session: Any, items: Any, *, user: Any, request: Any) -> Any: - return receipts_service.RequestResult(created=[receipt], errors={"2": "У заказа нет ТТН"}) + return receipts_service.RequestResult( + created=[receipt], errors={"2": "У замовлення немає ТТН"} + ) async def fake_commit(self: AsyncSession) -> None: return None @@ -74,5 +76,5 @@ def test_cashier_creates_and_enqueues(queue: FakeQueue) -> None: assert response.status_code == 202 body = response.json() assert [r["order_id"] for r in body["created"]] == ["1"] - assert body["errors"] == {"2": "У заказа нет ТТН"} + assert body["errors"] == {"2": "У замовлення немає ТТН"} assert queue.jobs == [("create_ettn_receipt", (body["created"][0]["id"],))] diff --git a/backend/tests/test_receipts_service.py b/backend/tests/test_receipts_service.py index 1746696..bc8de44 100644 --- a/backend/tests/test_receipts_service.py +++ b/backend/tests/test_receipts_service.py @@ -77,17 +77,17 @@ class TestResolveAmounts: assert amounts.cod_kopecks == 100000 def test_explicit_prepayment_must_match_cod(self) -> None: - with pytest.raises(svc.ReceiptValidationError, match="≠ наложка"): + with pytest.raises(svc.ReceiptValidationError, match="≠ післяплата"): svc.resolve_amounts(_order(np_cod_amount_kopecks=100000), 10000) @pytest.mark.parametrize( ("overrides", "message"), [ - ({"waybill_number": None}, "нет ТТН"), - ({"np_cod_amount_kopecks": None}, "контроля оплаты"), - ({"np_status_code": "9"}, "не в пути"), - ({"np_cod_amount_kopecks": 130000}, "больше суммы заказа"), - ({"is_deleted": True}, "удалён"), + ({"waybill_number": None}, "немає ТТН"), + ({"np_cod_amount_kopecks": None}, "контролю оплати"), + ({"np_status_code": "9"}, "не в дорозі"), + ({"np_cod_amount_kopecks": 130000}, "більша за суму замовлення"), + ({"is_deleted": True}, "видалено"), ], ) def test_rejects(self, overrides: dict[str, Any], message: str) -> None: @@ -226,7 +226,7 @@ class TestRequestReceipts: ) assert result.created == [] - assert "Касса не определена" in result.errors[order.id] + assert "Касу не визначено" in result.errors[order.id] class FakeSession: @@ -289,7 +289,7 @@ class TestCreateEttn: await svc.create_ettn_for_receipt(FakeSession(order, register, receipt), client, receipt.id) assert receipt.status == ReceiptStatus.FAILED - assert "уже привязана" in (receipt.error or "") + assert "вже прив'язана" in (receipt.error or "") assert order.receipt_created_at is None async def test_unavailable_keeps_pending_then_reconciles(self) -> None: diff --git a/frontend/index.html b/frontend/index.html index 8bc8b22..cd9a1f5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,10 +1,10 @@ - + - lux_fiscal + Assistant System
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 63d80ef..3fc4a8a 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -27,10 +27,10 @@ async function readErrorMessage(response: Response): Promise { // Тело не JSON или пустое — используем сообщение по статусу ниже. } - if (response.status === 401) return 'Требуется вход в систему' - if (response.status === 403) return 'Недостаточно прав для этого действия' - if (response.status >= 500) return 'Сервер временно недоступен, попробуйте позже' - return `Ошибка запроса (${response.status})` + if (response.status === 401) return 'Потрібно увійти в систему' + if (response.status === 403) return 'Недостатньо прав для цієї дії' + if (response.status >= 500) return 'Сервер тимчасово недоступний, спробуйте пізніше' + return `Помилка запиту (${response.status})` } // Параллельные 401 не должны порождать несколько запросов на refresh — @@ -42,7 +42,7 @@ async function refreshAccessToken(): Promise { const refreshToken = getRefreshToken() if (!refreshToken) { - throw new ApiError(401, 'Сессия истекла, войдите снова') + throw new ApiError(401, 'Сесія закінчилася, увійдіть знову') } refreshPromise = (async () => { diff --git a/frontend/src/features/auth/AuthContext.tsx b/frontend/src/features/auth/AuthContext.tsx index 1145285..6ce81df 100644 --- a/frontend/src/features/auth/AuthContext.tsx +++ b/frontend/src/features/auth/AuthContext.tsx @@ -65,7 +65,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { setUser(result.user) setStatus('authenticated') } catch (err) { - const message = err instanceof ApiError ? err.message : 'Не удалось подключиться к серверу' + const message = err instanceof ApiError ? err.message : 'Не вдалося підключитися до сервера' setError(message) throw err } diff --git a/frontend/src/features/auth/LoginPage.tsx b/frontend/src/features/auth/LoginPage.tsx index c6bd175..f74ff12 100644 --- a/frontend/src/features/auth/LoginPage.tsx +++ b/frontend/src/features/auth/LoginPage.tsx @@ -37,7 +37,7 @@ export function LoginPage() { try { await login(email, password) } catch (err) { - const message = err instanceof ApiError ? err.message : 'Не удалось подключиться к серверу' + const message = err instanceof ApiError ? err.message : 'Не вдалося підключитися до сервера' setFormError(message) } finally { setSubmitting(false) @@ -102,7 +102,7 @@ export function LoginPage() { type="button" className="login-toggle" onClick={() => setShowPassword((v) => !v)} - aria-label={showPassword ? 'Скрыть пароль' : 'Показать пароль'} + aria-label={showPassword ? 'Приховати пароль' : 'Показати пароль'} aria-pressed={showPassword} >