Skip to content
6 changes: 4 additions & 2 deletions Dockerfile.local
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ RUN pip install --no-cache-dir uv && uv sync --frozen
ENV UVICORN_RELOAD_ENABLED=true
ENV PATH="/opt/remnashop/.venv/bin:$PATH"

RUN chmod +x ./docker-entrypoint.sh
RUN sed -i 's/\r$//' ./docker-entrypoint.sh && \
chmod +x ./docker-entrypoint.sh && \
ls -la ./docker-entrypoint.sh

CMD ["./docker-entrypoint.sh"]
CMD ["/bin/sh", "./docker-entrypoint.sh"]
7 changes: 7 additions & 0 deletions assets/translations/ru/buttons.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,11 @@ btn-gateway =
*[0] 🔴 Выключено
}

.yookassa-request-email = { $request_email ->
[1] 🟢 Запрашивать почту
*[0] 🔴 Запрашивать почту
}

.default-currency-choice = { $enabled ->
[1] 🔘
*[0] ⚪
Expand Down Expand Up @@ -463,6 +468,8 @@ btn-subscription =
.back-plans = ⬅️ Назад к выбору плана
.back-duration = ⬅️ Изменить длительность
.back-payment-method = ⬅️ Изменить способ оплаты
.back-yookassa-email = ⬅️ Назад к вводу почты
.skip-email = ⏭️ Пропустить
.connect = 🚀 Подключиться

.duration = { $period } | { $final_amount ->
Expand Down
5 changes: 5 additions & 0 deletions assets/translations/ru/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,11 @@ msg-subscription-payment-method =

{ msg-subscription-details }

msg-subscription-yookassa-email =
<b>✉️ Укажите почту для чека</b>

{ msg-subscription-details }

msg-subscription-confirm =
<b>🛒 Подтверждение { $purchase_type ->
[RENEW] продления
Expand Down
1 change: 1 addition & 0 deletions assets/translations/ru/notifications.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ ntf-subscription =
.gateways-unavailable = ❌ <i>В данный момент нет доступных платежных систем.</i>
.renew-plan-unavailable = ❌ <i>Текущий план устарел и недоступен для продления.</i>
.payment-creation-failed = ❌ <i>Ошибка при создании платежа. Попробуйте позже.</i>
.email-invalid = ❌ <i>Введена некорректная почта</i>

ntf-broadcast =
.message = { $content }
Expand Down
11 changes: 11 additions & 0 deletions src/application/dto/payment_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ class YooKassaGatewaySettingsDto(GatewaySettingsDto):
api_key: Optional[SecretStr] = None
customer: Optional[str] = None
vat_code: Optional[int] = None
request_email: bool = False

@property
def as_list(self) -> list[dict[str, Any]]:
return [
{"field": f.name, "value": getattr(self, f.name)}
for f in fields(self)
if f.name
not in {"type", "created_at", "updated_at", "request_email"}
and not f.name.startswith("_")
]


@dataclass(kw_only=True)
Expand Down
2 changes: 2 additions & 0 deletions src/application/use_cases/gateways/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .commands.configuration import (
MovePaymentGatewayUp,
TogglePaymentGatewayActive,
ToggleYooKassaRequestEmail,
UpdatePaymentGatewaySettings,
)
from .commands.payment import (
Expand All @@ -19,6 +20,7 @@
GetPaymentGatewayInstance,
MovePaymentGatewayUp,
TogglePaymentGatewayActive,
ToggleYooKassaRequestEmail,
UpdatePaymentGatewaySettings,
CreateDefaultPaymentGateway,
CreatePayment,
Expand Down
29 changes: 29 additions & 0 deletions src/application/use_cases/gateways/commands/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from src.application.common.policy import Permission
from src.application.common.uow import UnitOfWork
from src.application.dto import UserDto
from src.application.dto.payment_gateway import YooKassaGatewaySettingsDto
from src.core.exceptions import GatewayNotConfiguredError


Expand Down Expand Up @@ -82,6 +83,34 @@ async def _execute(self, actor: UserDto, gateway_id: int) -> None:
)


class ToggleYooKassaRequestEmail(Interactor[int, None]):
required_permission = Permission.REMNASHOP_GATEWAYS

def __init__(self, uow: UnitOfWork, gateway_dao: PaymentGatewayDao) -> None:
self.uow = uow
self.gateway_dao = gateway_dao

async def _execute(self, actor: UserDto, gateway_id: int) -> None:
async with self.uow:
gateway = await self.gateway_dao.get_by_id(gateway_id)

if not gateway:
raise ValueError(f"Payment gateway with id '{gateway_id}' not found")

if not isinstance(gateway.settings, YooKassaGatewaySettingsDto):
raise ValueError(f"Gateway '{gateway_id}' is not YooKassa")

gateway.settings.request_email = not gateway.settings.request_email

await self.gateway_dao.update(gateway)
await self.uow.commit()

logger.info(
f"{actor.log} Toggled YooKassa request_email for gateway '{gateway_id}' "
f"to '{gateway.settings.request_email}'"
)


@dataclass(frozen=True)
class UpdatePaymentGatewaySettingsDto:
gateway_id: int
Expand Down
3 changes: 3 additions & 0 deletions src/application/use_cases/gateways/commands/payment.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import uuid
from dataclasses import dataclass
from typing import Optional
from uuid import UUID

from loguru import logger
Expand Down Expand Up @@ -111,6 +112,7 @@ class CreatePaymentDto:
pricing: PriceDetailsDto
purchase_type: PurchaseType
gateway_type: PaymentGatewayType
receipt_email: Optional[str] = None


class CreatePayment(Interactor[CreatePaymentDto, PaymentResultDto]):
Expand Down Expand Up @@ -166,6 +168,7 @@ async def _execute(self, actor: UserDto, data: CreatePaymentDto) -> PaymentResul
payment: PaymentResultDto = await gateway_instance.handle_create_payment(
amount=data.pricing.final_amount,
details=details,
receipt_email=data.receipt_email,
)

transaction.payment_id = payment.id
Expand Down
12 changes: 12 additions & 0 deletions src/core/utils/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import re

_EMAIL_PATTERN = re.compile(
r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+"
)


def is_valid_email(value: str) -> bool:
s = value.strip()
if not s or len(s) > 254:
return False
return bool(_EMAIL_PATTERN.fullmatch(s))
7 changes: 6 additions & 1 deletion src/infrastructure/payment_gateways/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ def __init__(self, gateway: PaymentGatewayDto, bot: Bot, config: AppConfig) -> N
logger.debug(f"{self.__class__.__name__} Initialized")

@abstractmethod
async def handle_create_payment(self, amount: Decimal, details: str) -> PaymentResultDto: ...
async def handle_create_payment(
self,
amount: Decimal,
details: str,
receipt_email: Optional[str] = None,
) -> PaymentResultDto: ...

@abstractmethod
async def handle_webhook(self, request: Request) -> tuple[UUID, TransactionStatus]: ...
Expand Down
6 changes: 4 additions & 2 deletions src/infrastructure/payment_gateways/cryptomus.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import uuid
from decimal import Decimal
from hmac import compare_digest
from typing import Any
from typing import Any, Optional
from uuid import UUID

import orjson
Expand Down Expand Up @@ -49,7 +49,9 @@ def __init__(self, gateway: PaymentGatewayDto, bot: Bot, config: AppConfig) -> N
headers={"merchant": self.data.settings.merchant_id}, # type: ignore[dict-item]
)

async def handle_create_payment(self, amount: Decimal, details: str) -> PaymentResultDto:
async def handle_create_payment(
self, amount: Decimal, details: str, receipt_email: Optional[str] = None
) -> PaymentResultDto:
payload = await self._create_payment_payload(str(amount), str(uuid.uuid4()))
headers = {"sign": self._generate_signature(json.dumps(payload))}
logger.debug(f"Creating payment payload: {payload}")
Expand Down
6 changes: 4 additions & 2 deletions src/infrastructure/payment_gateways/cryptopay.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import hmac
import uuid
from decimal import Decimal
from typing import Any, Final
from typing import Any, Final, Optional
from uuid import UUID

import orjson
Expand Down Expand Up @@ -39,7 +39,9 @@ def __init__(self, gateway: PaymentGatewayDto, bot: Bot, config: AppConfig) -> N
headers={"Crypto-Pay-API-Token": self.data.settings.api_key.get_secret_value()}, # type: ignore[union-attr]
)

async def handle_create_payment(self, amount: Decimal, details: str) -> PaymentResultDto:
async def handle_create_payment(
self, amount: Decimal, details: str, receipt_email: Optional[str] = None
) -> PaymentResultDto:
payload = await self._create_payment_payload(str(amount), details)
logger.debug(f"Creating payment payload: {payload}")

Expand Down
6 changes: 4 additions & 2 deletions src/infrastructure/payment_gateways/freekassa.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import time
import uuid
from decimal import Decimal
from typing import Any, Final
from typing import Any, Final, Optional
from uuid import UUID

import orjson
Expand Down Expand Up @@ -45,7 +45,9 @@ def __init__(self, gateway: PaymentGatewayDto, bot: Bot, config: AppConfig) -> N

self._client = self._make_client(base_url=self.API_BASE)

async def handle_create_payment(self, amount: Decimal, details: str) -> PaymentResultDto:
async def handle_create_payment(
self, amount: Decimal, details: str, receipt_email: Optional[str] = None
) -> PaymentResultDto:
order_id = str(uuid.uuid4())
payload = await self._create_payment_payload(str(amount), order_id)
logger.debug(f"Creating payment payload: {payload}")
Expand Down
6 changes: 4 additions & 2 deletions src/infrastructure/payment_gateways/mulen_pay.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import uuid
from decimal import Decimal
from hmac import compare_digest
from typing import Any, Final
from typing import Any, Final, Optional
from uuid import UUID

import orjson
Expand Down Expand Up @@ -44,7 +44,9 @@ def __init__(self, gateway: PaymentGatewayDto, bot: Bot, config: AppConfig) -> N
},
)

async def handle_create_payment(self, amount: Decimal, details: str) -> PaymentResultDto:
async def handle_create_payment(
self, amount: Decimal, details: str, receipt_email: Optional[str] = None
) -> PaymentResultDto:
order_uuid = str(uuid.uuid4())
payload = self._create_payment_payload(amount, details, order_uuid)
logger.debug(f"Creating payment payload: {payload}")
Expand Down
6 changes: 4 additions & 2 deletions src/infrastructure/payment_gateways/pay_master.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import uuid
from decimal import Decimal
from typing import Any, Final
from typing import Any, Final, Optional
from uuid import UUID

import orjson
Expand Down Expand Up @@ -40,7 +40,9 @@ def __init__(self, gateway: PaymentGatewayDto, bot: Bot, config: AppConfig) -> N
},
)

async def handle_create_payment(self, amount: Decimal, details: str) -> PaymentResultDto:
async def handle_create_payment(
self, amount: Decimal, details: str, receipt_email: Optional[str] = None
) -> PaymentResultDto:
payload = await self._create_payment_payload(str(amount), details)
headers = {"Idempotency-Key": str(uuid.uuid4())}
logger.debug(f"Creating payment payload: {payload}")
Expand Down
6 changes: 4 additions & 2 deletions src/infrastructure/payment_gateways/platega.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import hmac
from decimal import Decimal
from typing import Any, Final
from typing import Any, Final, Optional
from uuid import UUID

import orjson
Expand Down Expand Up @@ -42,7 +42,9 @@ def __init__(self, gateway: PaymentGatewayDto, bot: Bot, config: AppConfig) -> N
},
)

async def handle_create_payment(self, amount: Decimal, details: str) -> PaymentResultDto:
async def handle_create_payment(
self, amount: Decimal, details: str, receipt_email: Optional[str] = None
) -> PaymentResultDto:
payload = await self._create_payment_payload(amount, details)
logger.debug(f"Creating payment payload: {payload}")

Expand Down
6 changes: 4 additions & 2 deletions src/infrastructure/payment_gateways/robokassa.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import uuid
from decimal import Decimal
from hmac import compare_digest
from typing import Any, Final
from typing import Any, Final, Optional
from urllib.parse import parse_qs, urlencode
from uuid import UUID

Expand Down Expand Up @@ -34,7 +34,9 @@ def __init__(self, gateway: PaymentGatewayDto, bot: Bot, config: AppConfig) -> N
f"got {type(self.data.settings).__name__}"
)

async def handle_create_payment(self, amount: Decimal, details: str) -> PaymentResultDto:
async def handle_create_payment(
self, amount: Decimal, details: str, receipt_email: Optional[str] = None
) -> PaymentResultDto:
order_id = uuid.uuid4()
inv_id = 0
out_sum = self._format_amount(amount)
Expand Down
5 changes: 4 additions & 1 deletion src/infrastructure/payment_gateways/telegram_stars.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import uuid
from decimal import Decimal
from typing import Optional
from uuid import UUID

from aiogram.types import LabeledPrice
Expand All @@ -14,7 +15,9 @@

# https://core.telegram.org/api/stars/
class TelegramStarsGateway(BasePaymentGateway):
async def handle_create_payment(self, amount: Decimal, details: str) -> PaymentResultDto:
async def handle_create_payment(
self, amount: Decimal, details: str, receipt_email: Optional[str] = None
) -> PaymentResultDto:
prices = [LabeledPrice(label=self.data.currency, amount=int(amount))]
payment_id = uuid.uuid4()

Expand Down
6 changes: 4 additions & 2 deletions src/infrastructure/payment_gateways/url_pay.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import hmac
import uuid
from decimal import Decimal
from typing import Any, Final
from typing import Any, Final, Optional
from uuid import UUID

import orjson
Expand Down Expand Up @@ -44,7 +44,9 @@ def __init__(self, gateway: PaymentGatewayDto, bot: Bot, config: AppConfig) -> N
},
)

async def handle_create_payment(self, amount: Decimal, details: str) -> PaymentResultDto:
async def handle_create_payment(
self, amount: Decimal, details: str, receipt_email: Optional[str] = None
) -> PaymentResultDto:
order_uuid = str(uuid.uuid4())
payload = self._create_payment_payload(str(amount), details, order_uuid)
logger.debug(f"Creating payment payload: {payload}")
Expand Down
6 changes: 4 additions & 2 deletions src/infrastructure/payment_gateways/wata.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from base64 import b64decode
from datetime import datetime, timezone
from decimal import Decimal
from typing import Any, Final
from typing import Any, Final, Optional
from uuid import UUID

import orjson
Expand Down Expand Up @@ -50,7 +50,9 @@ def __init__(self, gateway: PaymentGatewayDto, bot: Bot, config: AppConfig) -> N
},
)

async def handle_create_payment(self, amount: Decimal, details: str) -> PaymentResultDto:
async def handle_create_payment(
self, amount: Decimal, details: str, receipt_email: Optional[str] = None
) -> PaymentResultDto:
order_id = str(uuid.uuid4())
payload = await self._create_payment_payload(amount, details, order_id)
logger.debug(f"Creating payment payload: {payload}")
Expand Down
Loading