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

payments service: implementation of apis and db repos for one-time-payment workflow ⚠️ #4743

Merged
merged 97 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 89 commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
8e8efb8
adds pg settings
pcrespov Sep 12, 2023
bc7528f
adds rut
pcrespov Sep 12, 2023
1420522
exposes API models in RUT
pcrespov Sep 12, 2023
0900aea
drafted RUT sdk
pcrespov Sep 12, 2023
4486f7f
minor
pcrespov Sep 26, 2023
51c176e
minor on pyament-gateway interface
pcrespov Sep 27, 2023
d20de66
adds jsonref
pcrespov Sep 27, 2023
a212334
minor
pcrespov Sep 27, 2023
e84b47f
rut interface and tests
pcrespov Sep 27, 2023
9f5b205
doc and placeholder
pcrespov Sep 27, 2023
bf185fd
init db
pcrespov Sep 27, 2023
e7d0878
tests db
pcrespov Sep 27, 2023
b071b74
repo
pcrespov Sep 27, 2023
a284bf0
minor
pcrespov Sep 27, 2023
a838038
fixes import
pcrespov Sep 27, 2023
6649a8f
fixing tests
pcrespov Sep 28, 2023
1354477
fixes tests fixtures
pcrespov Sep 28, 2023
454911d
common baseclientapi
pcrespov Sep 28, 2023
97c7595
cleanup
pcrespov Sep 28, 2023
5d7c518
is-devel flag
pcrespov Oct 2, 2023
93117d0
minor
pcrespov Oct 2, 2023
38443bf
udpates errors
pcrespov Oct 2, 2023
60c663b
auth
pcrespov Oct 2, 2023
0ceb141
testing against real dev server
pcrespov Oct 2, 2023
aed9c24
secret envs
pcrespov Oct 2, 2023
165f58a
cleanup
pcrespov Oct 2, 2023
7aa8603
adds cancel and undo test real
pcrespov Oct 3, 2023
bc26dc9
fixes tests
pcrespov Oct 4, 2023
c2b74cb
fix
pcrespov Oct 4, 2023
e6e11a4
extends CLI
pcrespov Oct 4, 2023
b33b4b3
mixin
pcrespov Oct 4, 2023
df56f09
tests http-client
pcrespov Oct 4, 2023
a6e08f9
adapts PaymentsGatewayApi
pcrespov Oct 4, 2023
786057a
updated ResourceUsageTrackerApi
pcrespov Oct 4, 2023
35113d6
rename
pcrespov Oct 4, 2023
e305498
minor
pcrespov Oct 4, 2023
f68f827
cleanup
pcrespov Oct 4, 2023
ffbb40b
changes both oas
pcrespov Oct 4, 2023
bd67570
services/payments version: 1.1.0 → 1.2.0
pcrespov Oct 4, 2023
73ab1b3
minor
pcrespov Oct 4, 2023
ad0597c
minor fix
pcrespov Oct 4, 2023
718d80e
Merge branch 'master' into is4657/payments-service-part2
pcrespov Oct 9, 2023
5a97db8
Merge branch 'master' into is4657/payments-service-part2
pcrespov Oct 10, 2023
c1085c4
Merge branch 'master' into is4657/payments-service-part2
pcrespov Oct 11, 2023
f111c7c
Merge branch 'master' into is4657/payments-service-part2
pcrespov Oct 11, 2023
b82cc11
fake email server
pcrespov Oct 11, 2023
272f591
minor test fixes
pcrespov Oct 11, 2023
421ecc3
implements rpc create_payment to init a payment
pcrespov Oct 11, 2023
93c3f14
drafts implementation ACK
pcrespov Oct 11, 2023
a5085be
changed setup postgres
pcrespov Oct 11, 2023
df37265
db dependency
pcrespov Oct 11, 2023
19715f4
gets repo
pcrespov Oct 11, 2023
811c0e0
adapts disable fixtures
pcrespov Oct 11, 2023
7b528cc
Merge branch 'master' into is4657/payments-service-part2
pcrespov Oct 11, 2023
1e680ab
codeclimate
pcrespov Oct 11, 2023
3d0ba1f
Merge branch 'master' into is4657/payments-service-part2
pcrespov Oct 12, 2023
ae6a103
Merge branch 'master' into is4657/payments-service-part2
pcrespov Oct 13, 2023
6c465c0
Merge branch 'master' into is4657/payments-service-part2
pcrespov Oct 13, 2023
d3bb4cf
undo
pcrespov Oct 13, 2023
16b4325
invoice in db
pcrespov Oct 13, 2023
833e6cf
fix oas
pcrespov Oct 13, 2023
0731491
fixes log
pcrespov Oct 13, 2023
cea0d7f
pg fixtures
pcrespov Oct 13, 2023
4d29162
adds pg fixture
pcrespov Oct 13, 2023
ff6e8e4
first db impl
pcrespov Oct 13, 2023
d5c0344
fixes tests_rpc
pcrespov Oct 14, 2023
b16b902
mv fakes to pytest_simcore
pcrespov Oct 14, 2023
2911cb5
setting up tests
pcrespov Oct 14, 2023
56d03ac
drafts db-rpoe
pcrespov Oct 14, 2023
55a98a5
new colun
pcrespov Oct 14, 2023
a1d37e8
update transction includes invoice_url
pcrespov Oct 14, 2023
cf589bd
basic db test passes
pcrespov Oct 14, 2023
ad018b4
fixes test model
pcrespov Oct 14, 2023
8405449
new mock_patch
pcrespov Oct 14, 2023
046f5d0
handling api errors
pcrespov Oct 14, 2023
da5fa6b
errors from code
pcrespov Oct 14, 2023
ce73411
fixes errors
pcrespov Oct 14, 2023
2994bb8
error handlers
pcrespov Oct 14, 2023
75ccea4
handle brad exceptions
pcrespov Oct 14, 2023
80dbfbc
rename exceptions
pcrespov Oct 14, 2023
b7e86f3
mypy
pcrespov Oct 14, 2023
e4d61be
fix
pcrespov Oct 14, 2023
932703f
Merge branch 'master' into is4657/payments-service-part2
pcrespov Oct 16, 2023
fdb5db3
Merge branch 'master' into is4657/payments-service-part2
pcrespov Oct 16, 2023
64e4fcb
rm old migration script
pcrespov Oct 16, 2023
34cd5dc
minor
pcrespov Oct 16, 2023
cb4d62d
cleanup
pcrespov Oct 16, 2023
23e5137
minor
pcrespov Oct 16, 2023
38a4f77
minor to avoid too many tests
pcrespov Oct 16, 2023
574f00d
@sanderegg review: comments
pcrespov Oct 17, 2023
4925b79
@sanderegg review: avoid acroynyms in logs
pcrespov Oct 17, 2023
d3aafff
@sanderegg review: comments
pcrespov Oct 17, 2023
7938889
@sanderegg review: comments
pcrespov Oct 17, 2023
f52292d
@GitHK review: comments
pcrespov Oct 17, 2023
6a89d14
@sanderegg review: rename fixtures
pcrespov Oct 17, 2023
c664810
Merge branch 'master' into is4657/payments-service-part2
odeimaiz Oct 17, 2023
50b2c45
Merge branch 'master' into is4657/payments-service-part2
pcrespov Oct 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
5 changes: 5 additions & 0 deletions services/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ services:
- PAYMENTS_PASSWORD=${PAYMENTS_PASSWORD}
- PAYMENTS_SWAGGER_API_DOC_ENABLED=${PAYMENTS_SWAGGER_API_DOC_ENABLED}
- PAYMENTS_USERNAME=${PAYMENTS_USERNAME}
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_HOST=${POSTGRES_HOST}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_PORT=${POSTGRES_PORT}
- POSTGRES_USER=${POSTGRES_USER}
- RABBIT_HOST=${RABBIT_HOST}
- RABBIT_PASSWORD=${RABBIT_PASSWORD}
- RABBIT_PORT=${RABBIT_PORT}
Expand Down
2 changes: 1 addition & 1 deletion services/payments/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.0
1.2.0
114 changes: 95 additions & 19 deletions services/payments/doc/payments.drawio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 10 additions & 2 deletions services/payments/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"info": {
"title": "simcore-service-payments web API",
"description": " Service that manages creation and validation of registration payments",
"version": "1.1.0"
"version": "1.2.0"
},
"paths": {
"/": {
Expand Down Expand Up @@ -218,6 +218,13 @@
"type": "string",
"title": "Message"
},
"invoice_url": {
"type": "string",
"maxLength": 2083,
"minLength": 1,
"format": "uri",
"title": "Invoice Url"
},
"saved": {
"allOf": [
{
Expand All @@ -230,7 +237,8 @@
},
"type": "object",
"required": [
"success"
"success",
"invoice_url"
],
"title": "AckPayment"
},
Expand Down
4 changes: 3 additions & 1 deletion services/payments/scripts/fake_payment_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ def cancel_payment(
return router


def auth_session(x_init_api_secret: Annotated[str | None, Header()] = None):
# NOTE: keep `X_Init_Api_Secret` with capital letters (even if headers are case-insensitive) to
sanderegg marked this conversation as resolved.
Show resolved Hide resolved
# to agree with the specs provided by our partners
def auth_session(X_Init_Api_Secret: Annotated[str | None, Header()] = None):
return 1


Expand Down
2 changes: 1 addition & 1 deletion services/payments/setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.1.0
current_version = 1.2.0
commit = True
message = services/payments version: {current_version} → {new_version}
tag = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@
from typing import Annotated

from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from servicelib.logging_utils import log_context
from simcore_postgres_database.models.payments_transactions import (
PaymentTransactionState,
)

from ...db.payments_transactions_repo import (
PaymentNotFoundError,
PaymentsTransactionsRepo,
)
from ...models.auth import SessionData
from ...models.db import PaymentsTransactionsDB
from ...models.schemas.acknowledgements import (
AckPayment,
AckPaymentMethod,
PaymentID,
PaymentMethodID,
)
from ._dependencies import get_current_session
from ...services.resource_usage_tracker import ResourceUsageTrackerApi
from ._dependencies import get_current_session, get_repository, get_rut_api

_logger = logging.getLogger(__name__)

Expand All @@ -19,34 +29,82 @@


async def on_payment_completed(
payment_id: PaymentID, ack: AckPayment, session: SessionData
transaction: PaymentsTransactionsDB, rut_api: ResourceUsageTrackerApi
):
assert transaction.completed_at is not None # nosec
assert transaction.initiated_at < transaction.completed_at # nosec

_logger.debug("TODO next PR Notify front-end of payment -> sio ")
pcrespov marked this conversation as resolved.
Show resolved Hide resolved

with log_context(
_logger,
logging.INFO,
"Top-up %s credits for %s in RUT",
f"{transaction.osparc_credits}",
f"{transaction.payment_id=}",
):
credit_transaction_id = await rut_api.create_credit_transaction(
product_name=transaction.product_name,
wallet_id=transaction.wallet_id,
wallet_name="id={transaction.wallet_id}",
user_id=transaction.user_id,
user_email=transaction.user_email,
osparc_credits=transaction.osparc_credits,
payment_transaction_id=transaction.payment_id,
created_at=transaction.completed_at,
)

_logger.debug(
"payment completed: %s",
f"{payment_id=}, {ack.success=}, {ack.message=}, {session.username=}",
"RUT response to %s was %s",
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
f"{transaction.payment_id=}",
f"{credit_transaction_id=}",
)
_logger.debug("Notify front-end -> sio ")
_logger.debug("Authorize inc/dec credits -> RUT")
_logger.debug("Annotate RUT response")


@router.post("/payments/{payment_id}:ack")
async def acknowledge_payment(
payment_id: PaymentID,
ack: AckPayment,
session: Annotated[SessionData, Depends(get_current_session)],
_session: Annotated[SessionData, Depends(get_current_session)],
repo: Annotated[
PaymentsTransactionsRepo, Depends(get_repository(PaymentsTransactionsRepo))
],
rut_api: Annotated[ResourceUsageTrackerApi, Depends(get_rut_api)],
background_tasks: BackgroundTasks,
):
"""completes (ie. ack) request initated by `/init` on the payments-gateway API"""
_logger.debug(
"User %s is acknoledging payment with %s as %s", session, f"{payment_id=}", ack
)
_logger.debug("Validate and complete transaction -> DB")
_logger.debug(
"When annotated in db, respond and start a background task with the rest"
)
background_tasks.add_task(on_payment_completed, payment_id, ack, session)
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED)

with log_context(
_logger,
logging.INFO,
"Annotate ACK transaction %s in db",
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
f"{payment_id=}",
):
try:
transaction = await repo.update_ack_payment_transaction(
payment_id=payment_id,
completion_state=(
PaymentTransactionState.SUCCESS
if ack.success
else PaymentTransactionState.FAILED
),
state_message=ack.message,
invoice_url=ack.invoice_url,
)
except PaymentNotFoundError as err:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"{err}"
) from err

if ack.saved:
_logger.debug("TODO Annotate CREATE payment method")
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
)

if transaction.state == PaymentTransactionState.SUCCESS:
assert payment_id == transaction.payment_id # nosec
background_tasks.add_task(on_payment_completed, transaction, rut_api)


@router.post("/payments-methods/{payment_method_id}:ack")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import logging
from typing import Annotated
from collections.abc import AsyncGenerator, Callable
from typing import Annotated, cast

from fastapi import Depends, Request
from fastapi.security import OAuth2PasswordBearer
from servicelib.fastapi.dependencies import get_app, get_reverse_url_mapper
from sqlalchemy.ext.asyncio import AsyncEngine

from ..._meta import API_VTAG
from ...core.settings import ApplicationSettings
from ...db.payments_transactions_repo import BaseRepository
from ...models.auth import SessionData
from ...services.auth import get_session_data
from ...services.resource_usage_tracker import ResourceUsageTrackerApi

_logger = logging.getLogger(__name__)

Expand All @@ -27,11 +31,32 @@ def get_settings(request: Request) -> ApplicationSettings:
assert get_reverse_url_mapper # nosec
assert get_app # nosec


#
# auth dependencies
# services dependencies
#


def get_rut_api(request: Request) -> ResourceUsageTrackerApi:
return cast(
ResourceUsageTrackerApi, ResourceUsageTrackerApi.get_from_app_state(request.app)
)


def get_db_engine(request: Request) -> AsyncEngine:
engine: AsyncEngine = request.app.state.engine
assert engine # nosec
return engine


def get_repository(repo_type: type[BaseRepository]) -> Callable:
async def _get_repo(
engine: Annotated[AsyncEngine, Depends(get_db_engine)],
) -> AsyncGenerator[BaseRepository, None]:
yield repo_type(db_engine=engine)

return _get_repo


# Implements `password` flow defined in OAuth2
_oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"/{API_VTAG}/token")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import logging

from fastapi import HTTPException, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse

from ...models.schemas.errors import DefaultApiError

_logger = logging.getLogger(__name__)


# NOTE: https://www.starlette.io/exceptions/
# Handled exceptions **do not represent error cases** !
# - They are coerced into appropriate HTTP responses, which are then sent through the standard middleware stack.
# - By default the HTTPException class is used to manage any handled exceptions.


async def http_exception_as_json_response(
request: Request, exc: HTTPException
) -> JSONResponse:
assert request # nosec
error = DefaultApiError.from_status_code(exc.status_code)

error_detail = error.detail or ""
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
if exc.detail not in error_detail:
# starlette.exceptions.HTTPException default to similar detail
error.detail = exc.detail

return JSONResponse(
jsonable_encoder(error, exclude_none=True), status_code=exc.status_code
)


async def handle_errors_as_500(request: Request, exc: Exception) -> JSONResponse:
assert request # nosec
assert isinstance(exc, Exception) # nosec

error = DefaultApiError.from_status_code(status.HTTP_500_INTERNAL_SERVER_ERROR)
_logger.exception("Unhandled exeption responded as %s", error)
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
return JSONResponse(
jsonable_encoder(error, exclude_none=True),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from fastapi import APIRouter, FastAPI
from fastapi import APIRouter, FastAPI, HTTPException

from ..._meta import API_VTAG
from . import _acknowledgements, _auth, _health, _meta
from ._exceptions import handle_errors_as_500, http_exception_as_json_response


def setup_rest_api_routes(app: FastAPI):
def setup_rest_api(app: FastAPI):
app.include_router(_health.router)

api_router = APIRouter(prefix=f"/{API_VTAG}")
api_router.include_router(_auth.router, tags=["auth"])
api_router.include_router(_meta.router, tags=["meta"])
api_router.include_router(_acknowledgements.router, tags=["acks"])
app.include_router(api_router)

app.add_exception_handler(Exception, handle_errors_as_500)
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
app.add_exception_handler(HTTPException, http_exception_as_json_response)
Loading