From 138412e32736abbc1cbf67188daa30a9ffd5abc9 Mon Sep 17 00:00:00 2001 From: matusdrobuliak66 Date: Wed, 11 Dec 2024 06:12:52 +0100 Subject: [PATCH] adding purchase item functionality --- .../src/models_library/resource_tracker.py | 1 + ...source_tracker_licensed_items_purchases.py | 5 +- ...f_add_cols_to_licensed_items_purchases_.py | 38 ++++++++++ ...7_add_cols_to_licensed_items_purchases_.py | 4 +- .../resource_tracker_credit_transactions.py | 10 ++- .../api/rpc/_licensed_items_purchases.py | 2 +- .../models/credit_transactions.py | 4 ++ .../services/credit_transactions.py | 1 + .../services/licensed_items_purchases.py | 72 ++++++++++++++----- .../modules/db/credit_transactions_db.py | 1 + .../process_message_running_service.py | 1 + .../test_api_licensed_items_purchases.py | 5 +- .../licenses/_licensed_items_api.py | 58 ++++++++++++++- .../licenses/_models.py | 3 + 14 files changed, 181 insertions(+), 24 deletions(-) create mode 100644 packages/postgres-database/src/simcore_postgres_database/migration/versions/77ac824a77ff_add_cols_to_licensed_items_purchases_.py diff --git a/packages/models-library/src/models_library/resource_tracker.py b/packages/models-library/src/models_library/resource_tracker.py index c94755817b3..20e35b7e614 100644 --- a/packages/models-library/src/models_library/resource_tracker.py +++ b/packages/models-library/src/models_library/resource_tracker.py @@ -48,6 +48,7 @@ class CreditTransactionStatus(StrAutoEnum): class CreditClassification(StrAutoEnum): ADD_WALLET_TOP_UP = auto() # user top up credits DEDUCT_SERVICE_RUN = auto() # computational/dynamic service run costs) + DEDUCT_LICENSE_PURCHASE = auto() class PricingPlanClassification(StrAutoEnum): diff --git a/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py b/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py index d1ab2d88dc8..8cddc1d98aa 100644 --- a/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py +++ b/packages/models-library/src/models_library/resource_tracker_licensed_items_purchases.py @@ -7,7 +7,7 @@ from .licensed_items import LicensedItemID from .products import ProductName -from .resource_tracker import PricingUnitCostId +from .resource_tracker import PricingPlanId, PricingUnitCostId, PricingUnitId from .users import UserID from .wallets import WalletID @@ -19,12 +19,15 @@ class LicensedItemsPurchasesCreate(BaseModel): licensed_item_id: LicensedItemID wallet_id: WalletID wallet_name: str + pricing_plan_id: PricingPlanId + pricing_unit_id: PricingUnitId pricing_unit_cost_id: PricingUnitCostId pricing_unit_cost: Decimal start_at: datetime expire_at: datetime num_of_seats: int purchased_by_user: UserID + user_email: str purchased_at: datetime model_config = ConfigDict(from_attributes=True) diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/77ac824a77ff_add_cols_to_licensed_items_purchases_.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/77ac824a77ff_add_cols_to_licensed_items_purchases_.py new file mode 100644 index 00000000000..d829ece7e7a --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/77ac824a77ff_add_cols_to_licensed_items_purchases_.py @@ -0,0 +1,38 @@ +"""add cols to licensed_items_purchases table 3 + +Revision ID: 77ac824a77ff +Revises: d68b8128c23b +Create Date: 2024-12-10 16:42:14.041313+00:00 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "77ac824a77ff" +down_revision = "d68b8128c23b" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "resource_tracker_credit_transactions", + sa.Column( + "licensed_item_purchase_id", postgresql.UUID(as_uuid=True), nullable=True + ), + ) + # ### end Alembic commands ### + op.execute( + sa.DDL( + "ALTER TYPE credittransactionclassification ADD VALUE 'DEDUCT_LICENSE_PURCHASE'" + ) + ) + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("resource_tracker_credit_transactions", "licensed_item_purchase_id") + # ### end Alembic commands ### diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py index ee47dcb5d4a..6f425116490 100644 --- a/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/8fa15c4c3977_add_cols_to_licensed_items_purchases_.py @@ -1,7 +1,7 @@ """add cols to licensed_items_purchases table Revision ID: 8fa15c4c3977 -Revises: 4d007819e61a +Revises: 5e27063c3ac9 Create Date: 2024-12-10 06:42:23.319239+00:00 """ @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. revision = "8fa15c4c3977" -down_revision = "4d007819e61a" +down_revision = "5e27063c3ac9" branch_labels = None depends_on = None diff --git a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_credit_transactions.py b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_credit_transactions.py index d1501a42431..ca4cc470b5f 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_credit_transactions.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/resource_tracker_credit_transactions.py @@ -4,6 +4,7 @@ import enum import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID from ._common import ( NUMERIC_KWARGS, @@ -26,6 +27,7 @@ class CreditTransactionClassification(str, enum.Enum): DEDUCT_SERVICE_RUN = ( "DEDUCT_SERVICE_RUN" # computational/dynamic service run costs) ) + DEDUCT_LICENSE_PURCHASE = "DEDUCT_LICENSE_PURCHASE" resource_tracker_credit_transactions = sa.Table( @@ -117,7 +119,13 @@ class CreditTransactionClassification(str, enum.Enum): "payment_transaction_id", sa.String, nullable=True, - doc="Service run id connected with this transaction", + doc="Payment transaction id connected with this transaction", + ), + sa.Column( + "licensed_item_purchase_id", + UUID(as_uuid=True), + nullable=True, + doc="Licensed item purchase id connected with this transaction", ), column_created_datetime(timezone=True), sa.Column( diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py index 1245ab3f6b4..e8f71dfb97d 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/api/rpc/_licensed_items_purchases.py @@ -57,5 +57,5 @@ async def create_licensed_item_purchase( app: FastAPI, *, data: LicensedItemsPurchasesCreate ) -> LicensedItemPurchaseGet: return await licensed_items_purchases.create_licensed_item_purchase( - db_engine=app.state.engine, data=data + rabbitmq_client=app.state.rabbitmq_client, db_engine=app.state.engine, data=data ) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/credit_transactions.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/credit_transactions.py index 4cdf74b6429..b9fd942fee0 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/credit_transactions.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/models/credit_transactions.py @@ -11,6 +11,9 @@ PricingUnitId, ServiceRunId, ) +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemPurchaseID, +) from models_library.users import UserID from models_library.wallets import WalletID from pydantic import BaseModel, ConfigDict @@ -32,6 +35,7 @@ class CreditTransactionCreate(BaseModel): payment_transaction_id: str | None created_at: datetime last_heartbeat_at: datetime + licensed_item_purchase_id: LicensedItemPurchaseID | None class CreditTransactionCreditsUpdate(BaseModel): diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/credit_transactions.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/credit_transactions.py index c58eb76be8a..fa314ee2550 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/credit_transactions.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/credit_transactions.py @@ -43,6 +43,7 @@ async def create_credit_transaction( transaction_classification=CreditClassification.ADD_WALLET_TOP_UP, service_run_id=None, payment_transaction_id=credit_transaction_create_body.payment_transaction_id, + licensed_item_purchase_id=None, created_at=credit_transaction_create_body.created_at, last_heartbeat_at=credit_transaction_create_body.created_at, ) diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/licensed_items_purchases.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/licensed_items_purchases.py index 3e106559b9e..f88316e095b 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/licensed_items_purchases.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/licensed_items_purchases.py @@ -6,20 +6,28 @@ LicensedItemsPurchasesPage, ) from models_library.products import ProductName +from models_library.resource_tracker import ( + CreditClassification, + CreditTransactionStatus, +) from models_library.resource_tracker_licensed_items_purchases import ( LicensedItemPurchaseID, LicensedItemsPurchasesCreate, ) from models_library.rest_ordering import OrderBy from models_library.wallets import WalletID +from simcore_postgres_database.utils_repos import transaction_context from sqlalchemy.ext.asyncio import AsyncEngine from ..api.rest.dependencies import get_resource_tracker_db_engine +from ..models.credit_transactions import CreditTransactionCreate from ..models.licensed_items_purchases import ( CreateLicensedItemsPurchasesDB, LicensedItemsPurchasesDB, ) -from .modules.db import licensed_items_purchases_db +from .modules.db import credit_transactions_db, licensed_items_purchases_db +from .modules.rabbitmq import RabbitMQClient, get_rabbitmq_client +from .utils import make_negative, sum_credit_transactions_and_publish_to_rabbitmq async def list_licensed_items_purchases( @@ -94,27 +102,59 @@ async def get_licensed_item_purchase( async def create_licensed_item_purchase( + rabbitmq_client: Annotated[RabbitMQClient, Depends(get_rabbitmq_client)], db_engine: Annotated[AsyncEngine, Depends(get_resource_tracker_db_engine)], *, data: LicensedItemsPurchasesCreate, ) -> LicensedItemPurchaseGet: - _create_db_data = CreateLicensedItemsPurchasesDB( - product_name=data.product_name, - licensed_item_id=data.licensed_item_id, - wallet_id=data.wallet_id, - wallet_name=data.wallet_name, - pricing_unit_cost_id=data.pricing_unit_cost_id, - pricing_unit_cost=data.pricing_unit_cost, - start_at=data.start_at, - expire_at=data.expire_at, - num_of_seats=data.num_of_seats, - purchased_by_user=data.purchased_by_user, - purchased_at=data.purchased_at, - ) + async with transaction_context(db_engine) as conn: + item_purchase_create = CreateLicensedItemsPurchasesDB( + product_name=data.product_name, + licensed_item_id=data.licensed_item_id, + wallet_id=data.wallet_id, + wallet_name=data.wallet_name, + pricing_unit_cost_id=data.pricing_unit_cost_id, + pricing_unit_cost=data.pricing_unit_cost, + start_at=data.start_at, + expire_at=data.expire_at, + num_of_seats=data.num_of_seats, + purchased_by_user=data.purchased_by_user, + purchased_at=data.purchased_at, + ) - licensed_item_purchase_db: LicensedItemsPurchasesDB = ( - await licensed_items_purchases_db.create(db_engine, data=_create_db_data) + licensed_item_purchase_db: LicensedItemsPurchasesDB = ( + await licensed_items_purchases_db.create( + db_engine, connection=conn, data=item_purchase_create + ) + ) + + # Deduct credits from credit_transactions table + transaction_create = CreditTransactionCreate( + product_name=data.product_name, + wallet_id=data.wallet_id, + wallet_name=data.wallet_name, + pricing_plan_id=data.pricing_plan_id, + pricing_unit_id=data.pricing_unit_id, + pricing_unit_cost_id=data.pricing_unit_cost_id, + user_id=data.purchased_by_user, + user_email=data.user_email, + osparc_credits=make_negative(data.pricing_unit_cost), + transaction_status=CreditTransactionStatus.BILLED, + transaction_classification=CreditClassification.DEDUCT_LICENSE_PURCHASE, + service_run_id=None, + payment_transaction_id=None, + licensed_item_purchase_id=licensed_item_purchase_db.licensed_item_purchase_id, + created_at=data.start_at, + last_heartbeat_at=data.start_at, + ) + await credit_transactions_db.create_credit_transaction( + db_engine, connection=conn, data=transaction_create + ) + + # Publish wallet total credits to RabbitMQ + await sum_credit_transactions_and_publish_to_rabbitmq( + db_engine, rabbitmq_client, data.product_name, data.wallet_id ) return LicensedItemPurchaseGet( diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/credit_transactions_db.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/credit_transactions_db.py index 76a8e9f1dfe..254a36a9732 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/credit_transactions_db.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/modules/db/credit_transactions_db.py @@ -48,6 +48,7 @@ async def create_credit_transaction( transaction_classification=data.transaction_classification, service_run_id=data.service_run_id, payment_transaction_id=data.payment_transaction_id, + licensed_item_purchase_id=data.licensed_item_purchase_id, created=data.created_at, last_heartbeat_at=data.last_heartbeat_at, modified=sa.func.now(), diff --git a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/process_message_running_service.py b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/process_message_running_service.py index 8300ede8283..e9234f65435 100644 --- a/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/process_message_running_service.py +++ b/services/resource-usage-tracker/src/simcore_service_resource_usage_tracker/services/process_message_running_service.py @@ -143,6 +143,7 @@ async def _process_start_event( transaction_classification=CreditClassification.DEDUCT_SERVICE_RUN, service_run_id=service_run_id, payment_transaction_id=None, + licensed_item_purchase_id=None, created_at=msg.created_at, last_heartbeat_at=msg.created_at, ) diff --git a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py index aad656d1728..e5920728d3c 100644 --- a/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py +++ b/services/resource-usage-tracker/tests/unit/with_dbs/test_api_licensed_items_purchases.py @@ -45,19 +45,22 @@ async def test_rpc_licensed_items_purchases_workflow( licensed_item_id="beb16d18-d57d-44aa-a638-9727fa4a72ef", wallet_id=1, wallet_name="My Wallet", + pricing_plan_id=1, + pricing_unit_id=1, pricing_unit_cost_id=1, pricing_unit_cost=Decimal(10), start_at=datetime.now(tz=UTC), expire_at=datetime.now(tz=UTC), num_of_seats=1, purchased_by_user=1, + user_email="test@test.com", purchased_at=datetime.now(tz=UTC), ) created_item = await licensed_items_purchases.create_licensed_item_purchase( rpc_client, data=_create_data ) - assert isinstance(result, LicensedItemPurchaseGet) # nosec + assert isinstance(created_item, LicensedItemPurchaseGet) # nosec result = await licensed_items_purchases.get_licensed_item_purchase( rpc_client, diff --git a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py index bb024b0423b..a9ee7f1ef0b 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_licensed_items_api.py @@ -1,6 +1,7 @@ # pylint: disable=unused-argument import logging +from datetime import UTC, datetime, timedelta from aiohttp import web from models_library.api_schemas_webserver.licensed_items import ( @@ -9,11 +10,21 @@ ) from models_library.licensed_items import LicensedItemID from models_library.products import ProductName +from models_library.resource_tracker_licensed_items_purchases import ( + LicensedItemsPurchasesCreate, +) from models_library.rest_ordering import OrderBy from models_library.users import UserID from pydantic import NonNegativeInt +from servicelib.rabbitmq.rpc_interfaces.resource_usage_tracker import ( + licensed_items_purchases, +) -from . import _licensed_items_db +from ..rabbitmq import get_rabbitmq_rpc_client +from ..resource_usage.api import get_pricing_plan_unit +from ..users.api import get_user +from ..wallets.api import get_wallet_with_available_credits_by_user_and_wallet +from . import _licensed_items_api, _licensed_items_db from ._models import LicensedItemsBodyParams _logger = logging.getLogger(__name__) @@ -74,4 +85,47 @@ async def purchase_licensed_item( licensed_item_id: LicensedItemID, body_params: LicensedItemsBodyParams, ) -> None: - raise NotImplementedError + # Check user wallet permissions + wallet = await get_wallet_with_available_credits_by_user_and_wallet( + app, user_id=user_id, wallet_id=body_params.wallet_id, product_name=product_name + ) + + licensed_item = await _licensed_items_api.get_licensed_item( + app, licensed_item_id=licensed_item_id, product_name=product_name + ) + + if licensed_item.pricing_plan_id != body_params.pricing_plan_id: + raise ValueError("You are lying!") + + pricing_unit = await get_pricing_plan_unit( + app, + product_name=product_name, + pricing_plan_id=body_params.pricing_plan_id, + pricing_unit_id=body_params.pricing_unit_id, + ) + + # Check whether wallet has enough credits + if wallet.available_credits - pricing_unit.current_cost_per_unit < 0: + raise ValueError("Not enough credits!") + + user = await get_user(app, user_id=user_id) + + _data = LicensedItemsPurchasesCreate( + product_name=product_name, + licensed_item_id=licensed_item_id, + wallet_id=wallet.wallet_id, + wallet_name=wallet.name, + pricing_plan_id=body_params.pricing_plan_id, + pricing_unit_id=body_params.pricing_unit_id, + pricing_unit_cost_id=pricing_unit.current_cost_per_unit_id, + pricing_unit_cost=pricing_unit.current_cost_per_unit, + start_at=datetime.now(tz=UTC), + expire_at=datetime.now(tz=UTC) + + timedelta(days=30), # <-- Temporary agreement with OM for proof of concept + num_of_seats=body_params.num_of_seats, + purchased_by_user=user_id, + user_email=user["email"], + purchased_at=datetime.now(tz=UTC), + ) + rpc_client = get_rabbitmq_rpc_client(app) + await licensed_items_purchases.create_licensed_item_purchase(rpc_client, data=_data) diff --git a/services/web/server/src/simcore_service_webserver/licenses/_models.py b/services/web/server/src/simcore_service_webserver/licenses/_models.py index 2d8514e28e9..d5c2ac0947e 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_models.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_models.py @@ -2,6 +2,7 @@ from models_library.basic_types import IDStr from models_library.licensed_items import LicensedItemID +from models_library.resource_tracker import PricingPlanId, PricingUnitId from models_library.resource_tracker_licensed_items_purchases import ( LicensedItemPurchaseID, ) @@ -52,6 +53,8 @@ class LicensedItemsListQueryParams( class LicensedItemsBodyParams(BaseModel): wallet_id: WalletID + pricing_plan_id: PricingPlanId + pricing_unit_id: PricingUnitId num_of_seats: int model_config = ConfigDict(extra="forbid")