Skip to content

Commit

Permalink
✨ webserver API for wallet/*/payments 🗃️ ⚠️ (#4683)
Browse files Browse the repository at this point in the history
  • Loading branch information
pcrespov authored Sep 5, 2023
1 parent 3e94757 commit c634fb4
Show file tree
Hide file tree
Showing 41 changed files with 1,818 additions and 234 deletions.
6 changes: 4 additions & 2 deletions .env-devel
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,16 @@ INVITATIONS_USERNAME=admin

LOG_FORMAT_LOCAL_DEV_ENABLED=1

PAYMENTS_HOST=invitations
PAYMENTS_LOGLEVEL=INFO
PAYMENT_FAKE_COMPLETION=0 # NOTE: this can be 1 ONLY if WEBSERVER_DEV_FEATURES_ENABLED=1
PAYMENTS_GATEWAY_URL=http://fake-payment-gateway.com
PAYMENTS_HOST=payments
PAYMENTS_LOGLEVEL=INFO
PAYMENTS_PASSWORD=adminadmin
PAYMENTS_PORT=8000
PAYMENTS_SECRET_KEY='REPLACE_ME_with_result__Fernet_generate_key='
PAYMENTS_USERNAME=admin


POSTGRES_DB=simcoredb
POSTGRES_ENDPOINT=postgres:5432
POSTGRES_HOST=postgres
Expand Down
23 changes: 23 additions & 0 deletions api/specs/web-server/_wallets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
from fastapi import APIRouter, status
from models_library.api_schemas_webserver.wallets import (
CreateWalletBodyParams,
CreateWalletPayment,
PaymentTransaction,
PutWalletBodyParams,
WalletGet,
WalletGetWithAvailableCredits,
WalletPaymentCreated,
)
from models_library.generics import Envelope
from models_library.rest_pagination import Page, PageQueryParameters
from models_library.users import GroupID
from models_library.wallets import WalletID
from simcore_service_webserver._meta import API_VTAG
Expand Down Expand Up @@ -56,6 +60,25 @@ async def update_wallet(wallet_id: WalletID, body: PutWalletBodyParams):
...


### Wallets payments


@router.post(
"/wallets/{wallet_id}/payments",
response_model=Envelope[WalletPaymentCreated],
)
async def create_payment(wallet_id: WalletID, body: CreateWalletPayment):
"""Creates payment to wallet `wallet_id`"""


@router.get(
"/wallets/-/payments",
response_model=Page[PaymentTransaction],
)
async def list_all_payments(params: PageQueryParameters):
"""Lists all user payments to his/her wallets (only the ones he/she created)"""


### Wallets groups


Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from datetime import datetime
from decimal import Decimal
from typing import TypeAlias

from models_library.utils.pydantic_tools_extension import FieldNotRequired
from pydantic import Field, HttpUrl

from ..basic_types import IDStr
from ..users import GroupID
from ..utils.pydantic_tools_extension import FieldNotRequired
from ..wallets import WalletID, WalletStatus
from ._base import OutputSchema
from ._base import InputSchema, OutputSchema


class WalletGet(OutputSchema):
Expand Down Expand Up @@ -37,3 +44,34 @@ class PutWalletBodyParams(OutputSchema):
description: str | None
thumbnail: str | None
status: WalletStatus


#
# Payments to top-up credits in wallets
#

PaymentID: TypeAlias = IDStr


class CreateWalletPayment(InputSchema):
price_dollars: Decimal
osparc_credits: Decimal
comment: str = FieldNotRequired(max_length=100)


class WalletPaymentCreated(OutputSchema):
payment_id: PaymentID
payment_form_url: HttpUrl = Field(
..., description="Link to external site that holds the payment submission form"
)


class PaymentTransaction(OutputSchema):
payment_id: PaymentID
price_dollars: Decimal
wallet_id: WalletID
osparc_credits: Decimal
comment: str = FieldNotRequired()
created_at: datetime
completed_at: datetime | None
invoice_url: HttpUrl = FieldNotRequired()
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""new payments_transactions table
Revision ID: fc6ea424f586
Revises: 763666c698fb
Create Date: 2023-09-04 14:13:28.201570+00:00
"""
from typing import Final

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "fc6ea424f586"
down_revision = "763666c698fb"
branch_labels = None
depends_on = None

# auto-update modified
# TRIGGERS ------------------------
_TABLE_NAME: Final[str] = "payments_transactions"
_TRIGGER_NAME: Final[str] = "trigger_auto_update" # NOTE: scoped on table
_PROCEDURE_NAME: Final[
str
] = f"{_TABLE_NAME}_auto_update_modified()" # NOTE: scoped on database
modified_timestamp_trigger = sa.DDL(
f"""
DROP TRIGGER IF EXISTS {_TRIGGER_NAME} on {_TABLE_NAME};
CREATE TRIGGER {_TRIGGER_NAME}
BEFORE INSERT OR UPDATE ON {_TABLE_NAME}
FOR EACH ROW EXECUTE PROCEDURE {_PROCEDURE_NAME};
"""
)

# PROCEDURES ------------------------
update_modified_timestamp_procedure = sa.DDL(
f"""
CREATE OR REPLACE FUNCTION {_PROCEDURE_NAME}
RETURNS TRIGGER AS $$
BEGIN
NEW.modified := current_timestamp;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
"""
)


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"payments_transactions",
sa.Column("payment_id", sa.String(), nullable=False),
sa.Column("price_dollars", sa.Numeric(scale=2), nullable=False),
sa.Column("osparc_credits", sa.Numeric(scale=2), nullable=False),
sa.Column("product_name", sa.String(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=False),
sa.Column("user_email", sa.String(), nullable=False),
sa.Column("wallet_id", sa.BigInteger(), nullable=False),
sa.Column("comment", sa.Text(), nullable=True),
sa.Column("initiated_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("success", sa.Boolean(), nullable=True),
sa.Column("errors", sa.Text(), nullable=True),
sa.Column(
"created",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"modified",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.PrimaryKeyConstraint("payment_id"),
)
op.create_index(
op.f("ix_payments_transactions_user_id"),
"payments_transactions",
["user_id"],
unique=False,
)
op.create_index(
op.f("ix_payments_transactions_wallet_id"),
"payments_transactions",
["wallet_id"],
unique=False,
)
# ### end Alembic commands ###

# custom
op.execute(update_modified_timestamp_procedure)
op.execute(modified_timestamp_trigger)


def downgrade():

# custom
op.execute(f"DROP TRIGGER IF EXISTS {_TRIGGER_NAME} on {_TABLE_NAME};")
op.execute(f"DROP FUNCTION {_PROCEDURE_NAME};")

# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
op.f("ix_payments_transactions_wallet_id"), table_name="payments_transactions"
)
op.drop_index(
op.f("ix_payments_transactions_user_id"), table_name="payments_transactions"
)
op.drop_table("payments_transactions")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sqlalchemy as sa


def column_created_datetime(timezone: bool = True) -> sa.Column:
def column_created_datetime(*, timezone: bool = True) -> sa.Column:
return sa.Column(
"created",
sa.DateTime(timezone=timezone),
Expand All @@ -13,7 +13,7 @@ def column_created_datetime(timezone: bool = True) -> sa.Column:
)


def column_modified_datetime(timezone: bool = True) -> sa.Column:
def column_modified_datetime(*, timezone: bool = True) -> sa.Column:
return sa.Column(
"modified",
sa.DateTime(timezone=timezone),
Expand All @@ -24,6 +24,9 @@ def column_modified_datetime(timezone: bool = True) -> sa.Column:
)


_TRIGGER_NAME: Final[str] = "auto_update_modified_timestamp"


def register_modified_datetime_auto_update_trigger(table: sa.Table) -> None:
"""registers a trigger/procedure couple in order to ensure auto
update of the modified timestamp column when a row is modified.
Expand All @@ -36,24 +39,22 @@ def register_modified_datetime_auto_update_trigger(table: sa.Table) -> None:
table -- the table to add the auto-trigger to
"""

TRIGGER_NAME: Final[str] = "auto_update_modified_timestamp" # NOTE: scoped on table
PROCEDURE_NAME: Final[
str
] = f"{table.name}_auto_update_modified_timestamp()" # NOTE: scoped on database
# NOTE: scoped on database
procedure_name: Final[str] = f"{table.name}_auto_update_modified_timestamp()"

# TRIGGER
modified_timestamp_trigger = sa.DDL(
f"""
DROP TRIGGER IF EXISTS {TRIGGER_NAME} on {table.name};
CREATE TRIGGER {TRIGGER_NAME}
DROP TRIGGER IF EXISTS {_TRIGGER_NAME} on {table.name};
CREATE TRIGGER {_TRIGGER_NAME}
BEFORE INSERT OR UPDATE ON {table.name}
FOR EACH ROW EXECUTE PROCEDURE {PROCEDURE_NAME};
FOR EACH ROW EXECUTE PROCEDURE {procedure_name};
"""
)
# PROCEDURE
update_modified_timestamp_procedure = sa.DDL(
f"""
CREATE OR REPLACE FUNCTION {PROCEDURE_NAME}
CREATE OR REPLACE FUNCTION {procedure_name}
RETURNS TRIGGER AS $$
BEGIN
NEW.modified := current_timestamp;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import sqlalchemy as sa

from ._common import (
column_created_datetime,
column_modified_datetime,
register_modified_datetime_auto_update_trigger,
)
from .base import metadata

payments_transactions = sa.Table(
"payments_transactions",
metadata,
sa.Column(
"payment_id",
sa.String,
nullable=False,
primary_key=True,
doc="Identifer of the payment provided by payment gateway",
),
sa.Column(
"price_dollars",
sa.Numeric(scale=2),
nullable=False,
doc="Total amount of the transaction (in dollars). E.g. 1234.12 $",
),
#
# Concept/Info
#
sa.Column(
"osparc_credits",
sa.Numeric(scale=2),
nullable=False,
doc="Amount of credits that will be added to the wallet_id "
"once the transaction completes successfuly."
"E.g. 1234.12 credits",
),
sa.Column(
"product_name",
sa.String,
nullable=False,
doc="Product name from which the transaction took place",
),
sa.Column(
"user_id",
sa.BigInteger,
nullable=False,
doc="User unique identifier",
index=True,
),
sa.Column(
"user_email",
sa.String,
nullable=False,
doc="User email at the time of the transaction",
),
sa.Column(
"wallet_id",
sa.BigInteger,
nullable=False,
doc="Wallet identifier owned by the user",
index=True,
),
sa.Column(
"comment",
sa.Text,
nullable=True,
doc="Extra comment on this payment (optional)",
),
#
# States
#
sa.Column(
"initiated_at",
sa.DateTime(timezone=True),
nullable=False,
doc="Timestamps when transaction initated (successful respose to /init)",
),
sa.Column(
"completed_at",
sa.DateTime(timezone=True),
nullable=True,
doc="Timestamps when transaction completed (payment acked)",
),
sa.Column(
"success",
sa.Boolean,
nullable=True,
doc="Transation still incomplete (=null) or "
"completed successfuly (=true) "
"completed with failures (=false).",
),
sa.Column(
"errors",
sa.Text,
nullable=True,
doc="Stores error messages in case of transaction failure",
),
# timestamps for this row
column_created_datetime(timezone=True),
column_modified_datetime(timezone=True),
)


register_modified_datetime_auto_update_trigger(payments_transactions)
Loading

0 comments on commit c634fb4

Please sign in to comment.