Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Is922/add autorecharge functionality #5036

Merged
Show file tree
Hide file tree
Changes from 88 commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
db23f5e
listen to wallet events in payment system
matusdrobuliak66 Nov 10, 2023
5fe1d79
Merge branch 'master' into is922/auto-recharge-emit-msg
matusdrobuliak66 Nov 10, 2023
bd38e85
fix test
matusdrobuliak66 Nov 10, 2023
d5ae4c4
Merge branch 'is922/auto-recharge-emit-msg' of github.com:matusdrobul…
matusdrobuliak66 Nov 10, 2023
caefaf1
fix tests
matusdrobuliak66 Nov 10, 2023
caf251d
Merge branch 'master' into is922/auto-recharge-emit-msg
matusdrobuliak66 Nov 10, 2023
ee3d0ff
Merge branch 'master' into is922/auto-recharge-emit-msg
matusdrobuliak66 Nov 12, 2023
1967ea4
review @sanderegg
matusdrobuliak66 Nov 12, 2023
ff86c8c
Merge branch 'master' of github.com:ITISFoundation/osparc-simcore
matusdrobuliak66 Nov 13, 2023
99d879c
a
matusdrobuliak66 Nov 14, 2023
98238ef
Merge branch 'master' of github.com:ITISFoundation/osparc-simcore
matusdrobuliak66 Nov 15, 2023
cb4a3da
draft
pcrespov Nov 8, 2023
1f979f1
fixes app fixture
pcrespov Nov 8, 2023
99071c9
ignores openapi
pcrespov Nov 11, 2023
2075983
deprecated and new
pcrespov Nov 11, 2023
908db56
updates OAS
pcrespov Nov 11, 2023
c8611f4
updates ack for payment method
pcrespov Nov 11, 2023
726777a
cleanup
pcrespov Nov 11, 2023
e85ca64
gateway
pcrespov Nov 11, 2023
0435700
rpc pay with payment-method
pcrespov Nov 11, 2023
32669a9
minor
pcrespov Nov 11, 2023
5451b6b
init and update
pcrespov Nov 11, 2023
91292b2
health
pcrespov Nov 11, 2023
dbcff38
health
pcrespov Nov 11, 2023
a53f454
mypy
pcrespov Nov 11, 2023
9bfb2e1
cleanup health
pcrespov Nov 14, 2023
9ee4a3a
undo liveness
pcrespov Nov 14, 2023
6eb5759
fixtures
pcrespov Nov 14, 2023
f5c8fe5
vscode swagger extension
pcrespov Nov 14, 2023
accd7df
WIP; error handling in gateway
pcrespov Nov 14, 2023
08cb214
updates OAS
pcrespov Nov 14, 2023
85387b6
updates version
pcrespov Nov 14, 2023
7adec3f
services/payments version: 1.3.1 → 1.4.0
pcrespov Nov 14, 2023
6d318e6
version
pcrespov Nov 14, 2023
52cdcc5
external mark
pcrespov Nov 15, 2023
fe05546
extension
pcrespov Nov 15, 2023
6e01880
updates errors
pcrespov Nov 15, 2023
f9c87cd
rename
pcrespov Nov 15, 2023
66846eb
fixes retries
pcrespov Nov 15, 2023
7fc5628
minor fixtures
pcrespov Nov 15, 2023
7a6d31b
fixes test
pcrespov Nov 15, 2023
a0d5d24
pay-w-cc in webserver
pcrespov Nov 15, 2023
2cc2d35
fixes tst
pcrespov Nov 15, 2023
4fbfac0
minor test failures
pcrespov Nov 15, 2023
d4e51a8
adds fake in pay-with-payment-method
pcrespov Nov 15, 2023
c51c4a3
minor
pcrespov Nov 15, 2023
f5fdd0a
rm todo
pcrespov Nov 15, 2023
be1193a
minor test
pcrespov Nov 15, 2023
2f29559
minor
pcrespov Nov 15, 2023
b988268
minor
pcrespov Nov 15, 2023
b2ec59d
changes model of PaymentMethod
pcrespov Nov 15, 2023
87d5a01
minor
pcrespov Nov 15, 2023
280aace
minor
pcrespov Nov 15, 2023
2b74a69
fix fixtue
pcrespov Nov 15, 2023
c9d6a8b
adapt fxiture
pcrespov Nov 15, 2023
d7bb6ff
@sanderegg review: remvoes deprecated
pcrespov Nov 15, 2023
e1b43e7
@sanderegg review: typo
pcrespov Nov 15, 2023
d644d19
fixes bad fixture
pcrespov Nov 16, 2023
ad425ca
updates info on CC
pcrespov Nov 16, 2023
c698c6b
fe
pcrespov Nov 16, 2023
6348a64
Merge branch 'master' of github.com:ITISFoundation/osparc-simcore
matusdrobuliak66 Nov 16, 2023
105af22
daily work
matusdrobuliak66 Nov 12, 2023
83f0566
daily work
matusdrobuliak66 Nov 13, 2023
3acb1db
daily work
matusdrobuliak66 Nov 13, 2023
0e18c88
introducing rpc for product prices
matusdrobuliak66 Nov 14, 2023
8db2fec
daily work
matusdrobuliak66 Nov 15, 2023
e9099ce
daily work
matusdrobuliak66 Nov 15, 2023
f0d1e4e
small modification, before merging Pedro's PR
matusdrobuliak66 Nov 16, 2023
d9adbab
modification
matusdrobuliak66 Nov 16, 2023
f91448a
Merge branch 'master' of github.com:ITISFoundation/osparc-simcore
matusdrobuliak66 Nov 16, 2023
bf129c7
merge master
matusdrobuliak66 Nov 16, 2023
7183323
fixing after conflicts
matusdrobuliak66 Nov 16, 2023
a34ed54
minor cleanup
matusdrobuliak66 Nov 16, 2023
537d58b
refactor process message
matusdrobuliak66 Nov 16, 2023
a86fa7c
adding test
matusdrobuliak66 Nov 16, 2023
9664b1e
adding test
matusdrobuliak66 Nov 16, 2023
58aad5a
Merge branch 'master' into is922/add-autorecharge-functionality
matusdrobuliak66 Nov 16, 2023
3e6aecb
adding mock
matusdrobuliak66 Nov 16, 2023
7e615b3
fix
matusdrobuliak66 Nov 16, 2023
4d495b0
adding PAYMENTS_AUTORECHARGE_ENABLED
matusdrobuliak66 Nov 16, 2023
bfd067a
adding PAYMENTS_AUTORECHARGE_ENABLED
matusdrobuliak66 Nov 16, 2023
6144dae
add var to .env-devel
matusdrobuliak66 Nov 16, 2023
e5b50f8
adding webserver unit tests
matusdrobuliak66 Nov 16, 2023
a5692a9
Merge branch 'master' into is922/add-autorecharge-functionality
matusdrobuliak66 Nov 17, 2023
b10ae8e
@pcrespov review
matusdrobuliak66 Nov 17, 2023
c16d484
Merge branch 'is922/add-autorecharge-functionality' of github.com:mat…
matusdrobuliak66 Nov 17, 2023
be46fcf
@GitHK @sanderegg review
matusdrobuliak66 Nov 17, 2023
9c860de
minor fix
matusdrobuliak66 Nov 17, 2023
abb3dc4
exclude web server payments from codeclimate
matusdrobuliak66 Nov 17, 2023
4a7fb73
no sonar
matusdrobuliak66 Nov 17, 2023
1d46a7d
no sonar
matusdrobuliak66 Nov 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
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}
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
- 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 @@ -17,6 +17,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 @@ -69,7 +70,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 @@ -128,6 +129,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
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
Loading