Skip to content

Commit

Permalink
Merge branch 'master' into is4657/payment-liveness-probes
Browse files Browse the repository at this point in the history
  • Loading branch information
pcrespov committed Nov 17, 2023
2 parents 2ac11c3 + 8f207be commit f171caa
Show file tree
Hide file tree
Showing 31 changed files with 1,102 additions and 108 deletions.
1 change: 1 addition & 0 deletions .env-devel
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ PAYMENTS_ACCESS_TOKEN_SECRET_KEY=2c0411810565e063309be1457009fb39ce023946f6a354e
PAYMENTS_AUTORECHARGE_DEFAULT_MONTHLY_LIMIT=10000
PAYMENTS_AUTORECHARGE_DEFAULT_TOP_UP_AMOUNT=100.0
PAYMENTS_AUTORECHARGE_MIN_BALANCE_IN_CREDITS=100
PAYMENTS_AUTORECHARGE_ENABLED=1
PAYMENTS_FAKE_COMPLETION_DELAY_SEC=10
PAYMENTS_FAKE_COMPLETION=0
PAYMENTS_GATEWAY_API_SECRET=replace-with-api-secret
Expand Down
17 changes: 16 additions & 1 deletion packages/models-library/src/models_library/products.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
from typing import TypeAlias
from decimal import Decimal
from typing import Any, ClassVar, TypeAlias

from pydantic import BaseModel, Field

ProductName: TypeAlias = str


class CreditResultGet(BaseModel):
product_name: ProductName
credit_amount: Decimal = Field(..., description="")

class Config:
schema_extra: ClassVar[dict[str, Any]] = {
"examples": [
{"product_name": "s4l", "credit_amount": Decimal(15.5)},
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Any, Literal, TypeAlias

import arrow
from models_library.products import ProductName
from pydantic import BaseModel, Field
from pydantic.types import NonNegativeFloat

Expand Down Expand Up @@ -264,6 +265,7 @@ class WalletCreditsMessage(RabbitMessageBase):
)
wallet_id: WalletID
credits: Decimal
product_name: ProductName

def routing_key(self) -> str | None:
return f"{self.wallet_id}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ def init_transaction(connection: SAConnection):
async def _init(payment_id: str):
# get payment_id from payment-gateway
values = random_payment_transaction(payment_id=payment_id)

# remove states
values.pop("state")
values.pop("completed_at")
# init successful: set timestamp
values["initiated_at"] = utcnow()

Expand Down Expand Up @@ -178,6 +180,9 @@ async def _go(expected_total=5):
payment_ids = []
for _ in range(expected_total):
values = random_payment_transaction(user_id=user_id)
# remove states
values.pop("state")
values.pop("completed_at")
payment_id = await insert_init_payment_transaction(connection, **values)
assert payment_id
payment_ids.append(payment_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
from faker import Faker
from simcore_postgres_database.models.api_keys import api_keys
from simcore_postgres_database.models.comp_pipeline import StateType
from simcore_postgres_database.models.payments_methods import InitPromptAckFlowState
from simcore_postgres_database.models.payments_transactions import (
PaymentTransactionState,
)
from simcore_postgres_database.models.products import products
from simcore_postgres_database.models.projects import projects
from simcore_postgres_database.models.users import users
Expand Down Expand Up @@ -199,6 +203,8 @@ def random_payment_method(
"user_id": FAKE.pyint(),
"wallet_id": FAKE.pyint(),
"initiated_at": utcnow(),
"state": InitPromptAckFlowState.PENDING,
"completed_at": None,
}
# state is not added on purpose
assert set(data.keys()).issubset({c.name for c in payments_methods.columns})
Expand Down Expand Up @@ -226,6 +232,8 @@ def random_payment_transaction(
"wallet_id": 1,
"comment": "Free starting credits",
"initiated_at": utcnow(),
"state": PaymentTransactionState.PENDING,
"completed_at": None,
}
# state is not added on purpose
assert set(data.keys()).issubset({c.name for c in payments_transactions.columns})
Expand All @@ -234,6 +242,27 @@ def random_payment_transaction(
return data


def random_payment_autorecharge(
primary_payment_method_id: str = FAKE.uuid4(),
**overrides,
) -> dict[str, Any]:
from simcore_postgres_database.models.payments_autorecharge import (
payments_autorecharge,
)

data = {
"wallet_id": FAKE.pyint(),
"enabled": True,
"primary_payment_method_id": primary_payment_method_id,
"top_up_amount_in_usd": 100,
"monthly_limit_in_usd": 1000,
}
assert set(data.keys()).issubset({c.name for c in payments_autorecharge.columns})

data.update(overrides)
return data


def random_api_key(product_name: str, user_id: int, **overrides) -> dict[str, Any]:
data = {
"display_name": FAKE.word(),
Expand Down
4 changes: 4 additions & 0 deletions services/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,10 @@ services:
- LOG_FORMAT_LOCAL_DEV_ENABLED=${LOG_FORMAT_LOCAL_DEV_ENABLED}
- PAYMENTS_ACCESS_TOKEN_EXPIRE_MINUTES=${PAYMENTS_ACCESS_TOKEN_EXPIRE_MINUTES}
- PAYMENTS_ACCESS_TOKEN_SECRET_KEY=${PAYMENTS_ACCESS_TOKEN_SECRET_KEY}
- PAYMENTS_AUTORECHARGE_DEFAULT_MONTHLY_LIMIT=${PAYMENTS_AUTORECHARGE_DEFAULT_MONTHLY_LIMIT}
- PAYMENTS_AUTORECHARGE_DEFAULT_TOP_UP_AMOUNT=${PAYMENTS_AUTORECHARGE_DEFAULT_TOP_UP_AMOUNT}
- PAYMENTS_AUTORECHARGE_MIN_BALANCE_IN_CREDITS=${PAYMENTS_AUTORECHARGE_MIN_BALANCE_IN_CREDITS}
- PAYMENTS_AUTORECHARGE_ENABLED=${PAYMENTS_AUTORECHARGE_ENABLED}
- PAYMENTS_GATEWAY_API_SECRET=${PAYMENTS_GATEWAY_API_SECRET}
- PAYMENTS_GATEWAY_URL=${PAYMENTS_GATEWAY_URL}
- PAYMENTS_LOGLEVEL=${PAYMENTS_LOGLEVEL}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from ...db.payments_transactions_repo import PaymentsTransactionsRepo
from ...services import payments, payments_methods
from ...services.payments_gateway import PaymentsGatewayApi
from ...services.resource_usage_tracker import ResourceUsageTrackerApi

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -84,7 +85,7 @@ async def list_payment_methods(
user_id: UserID,
wallet_id: WalletID,
) -> list[PaymentMethodGet]:
return await payments_methods.list_payments_methods(
return await payments_methods.list_payment_methods(
gateway=PaymentsGatewayApi.get_from_app_state(app),
repo=PaymentsMethodsRepo(db_engine=app.state.engine),
user_id=user_id,
Expand Down Expand Up @@ -151,6 +152,7 @@ async def pay_with_payment_method( # noqa: PLR0913 # pylint: disable=too-many-a
):
return await payments.pay_with_payment_method(
gateway=PaymentsGatewayApi.get_from_app_state(app),
rut=ResourceUsageTrackerApi.get_from_app_state(app),
repo_transactions=PaymentsTransactionsRepo(db_engine=app.state.engine),
repo_methods=PaymentsMethodsRepo(db_engine=app.state.engine),
payment_method_id=payment_method_id,
Expand Down
4 changes: 4 additions & 0 deletions services/payments/src/simcore_service_payments/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ class PaymentMethodAlreadyAckedError(PaymentsMethodsError):

class PaymentMethodUniqueViolationError(PaymentsMethodsError):
msg_template = "Payment method '{payment_method_id}' aready exists"


class InvalidPaymentMethodError(PaymentsMethodsError):
msg_template = "Invalid payment method '{payment_method_id}'"
20 changes: 20 additions & 0 deletions services/payments/src/simcore_service_payments/core/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from functools import cached_property
from typing import cast

from models_library.basic_types import NonNegativeDecimal
from pydantic import Field, HttpUrl, PositiveFloat, SecretStr, parse_obj_as, validator
from settings_library.application import BaseApplicationSettings
from settings_library.basic_types import LogLevel, VersionTag
Expand Down Expand Up @@ -77,6 +78,25 @@ class ApplicationSettings(_BaseApplicationSettings):
)
PAYMENTS_ACCESS_TOKEN_EXPIRE_MINUTES: PositiveFloat = Field(default=30)

PAYMENTS_AUTORECHARGE_MIN_BALANCE_IN_CREDITS: NonNegativeDecimal = Field(
default=100,
description="Minimum balance in credits to top-up for auto-recharge",
)

PAYMENTS_AUTORECHARGE_DEFAULT_TOP_UP_AMOUNT: NonNegativeDecimal = Field(
default=100,
description="Default value in USD on the amount to top-up for auto-recharge (`top_up_amount_in_usd`)",
)

PAYMENTS_AUTORECHARGE_DEFAULT_MONTHLY_LIMIT: NonNegativeDecimal | None = Field(
default=10000,
description="Default value in USD for the montly limit for auto-recharge (`monthly_limit_in_usd`)",
)
PAYMENTS_AUTORECHARGE_ENABLED: bool = Field(
default=False,
description="Based on this variable is the auto recharge functionality in Payment service enabled",
)

PAYMENTS_RABBITMQ: RabbitSettings = Field(
auto_default_from_env=True, description="settings for service/rabbitmq"
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from typing import TypeAlias

from models_library.api_schemas_webserver.wallets import PaymentMethodID
from models_library.basic_types import NonNegativeDecimal
from models_library.users import UserID
from models_library.wallets import WalletID
from pydantic import BaseModel, PositiveInt
from simcore_postgres_database.utils_payments_autorecharge import AutoRechargeStmts

from ..core.errors import InvalidPaymentMethodError
from .base import BaseRepository

AutoRechargeID: TypeAlias = PositiveInt


class PaymentsAutorechargeDB(BaseModel):
wallet_id: WalletID
enabled: bool
primary_payment_method_id: PaymentMethodID
top_up_amount_in_usd: NonNegativeDecimal
monthly_limit_in_usd: NonNegativeDecimal | None

class Config:
orm_mode = True


class AutoRechargeRepo(BaseRepository):
async def get_wallet_autorecharge(
self,
wallet_id: WalletID,
) -> PaymentsAutorechargeDB | None:
"""Annotates init-payment transaction
Raises:
PaymentAlreadyExistsError
"""

async with self.db_engine.begin() as conn:
stmt = AutoRechargeStmts.get_wallet_autorecharge(wallet_id)
result = await conn.execute(stmt)
row = result.first()
return PaymentsAutorechargeDB.from_orm(row) if row else None

async def replace_wallet_autorecharge(
self,
user_id: UserID,
wallet_id: WalletID,
new: PaymentsAutorechargeDB,
) -> PaymentsAutorechargeDB:
"""
Raises:
InvalidPaymentMethodError: if `new` includes some invalid 'primary_payment_method_id'
"""
async with self.db_engine.begin() as conn:
stmt = AutoRechargeStmts.is_valid_payment_method(
user_id=user_id,
wallet_id=new.wallet_id,
payment_method_id=new.primary_payment_method_id,
)

if await conn.scalar(stmt) != new.primary_payment_method_id:
raise InvalidPaymentMethodError(
payment_method_id=new.primary_payment_method_id
)

stmt = AutoRechargeStmts.upsert_wallet_autorecharge(
wallet_id=wallet_id,
enabled=new.enabled,
primary_payment_method_id=new.primary_payment_method_id,
top_up_amount_in_usd=new.top_up_amount_in_usd,
monthly_limit_in_usd=new.monthly_limit_in_usd,
)
result = await conn.execute(stmt)
row = result.first()
assert row # nosec
return PaymentsAutorechargeDB.from_orm(row)
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ async def insert_payment_method(
completion_state: InitPromptAckFlowState,
state_message: str | None,
) -> PaymentsMethodsDB:

await self.insert_init_payment_method(
payment_method_id,
user_id=user_id,
Expand Down Expand Up @@ -135,6 +134,23 @@ async def list_user_payment_methods(
rows = result.fetchall() or []
return parse_obj_as(list[PaymentsMethodsDB], rows)

async def get_payment_method_by_id(
self,
payment_method_id: PaymentMethodID,
) -> PaymentsMethodsDB:
async with self.db_engine.begin() as conn:
result = await conn.execute(
payments_methods.select().where(
(payments_methods.c.payment_method_id == payment_method_id)
& (payments_methods.c.state == InitPromptAckFlowState.SUCCESS)
)
)
row = result.first()
if row is None:
raise PaymentMethodNotFoundError(payment_method_id=payment_method_id)

return PaymentsMethodsDB.from_orm(row)

async def get_payment_method(
self,
payment_method_id: PaymentMethodID,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import datetime
from datetime import datetime, timezone
from decimal import Decimal

import sqlalchemy as sa
Expand Down Expand Up @@ -33,7 +33,7 @@ async def insert_init_payment_transaction(
user_email: str,
wallet_id: WalletID,
comment: str | None,
initiated_at: datetime.datetime,
initiated_at: datetime,
) -> PaymentID:
"""Annotates init-payment transaction
Expand Down Expand Up @@ -177,3 +177,43 @@ async def get_payment_transaction(
)
row = result.fetchone()
return PaymentsTransactionsDB.from_orm(row) if row else None

async def sum_current_month_dollars(self, *, wallet_id: WalletID) -> Decimal:
_current_timestamp = datetime.now(tz=timezone.utc)
_current_month_start_timestamp = _current_timestamp.replace(
day=1, hour=0, minute=0, second=0, microsecond=0
)

async with self.db_engine.begin() as conn:
sum_stmt = sa.select(
sa.func.sum(payments_transactions.c.price_dollars)
).where(
(payments_transactions.c.wallet_id == wallet_id)
& (
payments_transactions.c.state.in_(
[
PaymentTransactionState.SUCCESS,
]
)
)
& (
payments_transactions.c.completed_at
>= _current_month_start_timestamp
)
)
result = await conn.execute(sum_stmt)
row = result.first()
return Decimal(0) if row is None or row[0] is None else Decimal(row[0])

async def get_last_payment_transaction_for_wallet(
self, *, wallet_id: WalletID
) -> PaymentsTransactionsDB | None:
async with self.db_engine.begin() as connection:
result = await connection.execute(
payments_transactions.select()
.where(payments_transactions.c.wallet_id == wallet_id)
.order_by(payments_transactions.c.initiated_at.desc())
.limit(1)
)
row = result.fetchone()
return PaymentsTransactionsDB.from_orm(row) if row else None
Loading

0 comments on commit f171caa

Please sign in to comment.