Skip to content

Commit

Permalink
adding purchase item functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
matusdrobuliak66 committed Dec 11, 2024
1 parent 969f982 commit 138412e
Show file tree
Hide file tree
Showing 14 changed files with 181 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Original file line number Diff line number Diff line change
@@ -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 ###
Original file line number Diff line number Diff line change
@@ -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
"""
Expand All @@ -10,7 +10,7 @@

# revision identifiers, used by Alembic.
revision = "8fa15c4c3977"
down_revision = "4d007819e61a"
down_revision = "5e27063c3ac9"
branch_labels = None
depends_on = None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import enum

import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID

from ._common import (
NUMERIC_KWARGS,
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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__)
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit 138412e

Please sign in to comment.