Skip to content

Commit

Permalink
✨ Notify payment via email ⚠️ (#5310)
Browse files Browse the repository at this point in the history
  • Loading branch information
pcrespov authored Feb 9, 2024
1 parent a284681 commit c80cedd
Show file tree
Hide file tree
Showing 26 changed files with 982 additions and 182 deletions.
4 changes: 2 additions & 2 deletions .env-devel
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,9 @@ PAYMENTS_ACCESS_TOKEN_EXPIRE_MINUTES=30
PAYMENTS_ACCESS_TOKEN_SECRET_KEY=2c0411810565e063309be1457009fb39ce023946f6a354e6935107b57676
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
PAYMENTS_AUTORECHARGE_MIN_BALANCE_IN_CREDITS=100
PAYMENTS_EMAIL={}
PAYMENTS_FAKE_COMPLETION_DELAY_SEC=10
PAYMENTS_FAKE_COMPLETION=0
PAYMENTS_GATEWAY_API_SECRET=adminadmin
Expand All @@ -96,7 +97,6 @@ PAYMENTS_PORT=8000
PAYMENTS_SWAGGER_API_DOC_ENABLED=1
PAYMENTS_USERNAME=admin


POSTGRES_DB=simcoredb
POSTGRES_ENDPOINT=postgres:5432
POSTGRES_HOST=postgres
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ class Vendor(TypedDict, total=False):
E.g. company name, address, copyright, etc.
"""

name: str
name: str # e.g. IT'IS Foundation
copyright: str
url: str
license_url: str # Which are the license terms? (if applies)
invitation_url: str # How to request a trial invitation? (if applies)
has_landing_page: bool # Landing page enabled
address: str # e.g. Zeughausstrasse 43, 8004 Zurich, Switzerland


class IssueTracker(TypedDict, total=True):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from simcore_postgres_database.models.payments_transactions import (
PaymentTransactionState,
)
from simcore_postgres_database.models.products import products
from simcore_postgres_database.models.products import Vendor, products
from simcore_postgres_database.models.projects import projects
from simcore_postgres_database.models.users import users
from simcore_postgres_database.webserver_models import GroupType, UserStatus
Expand Down Expand Up @@ -170,23 +170,25 @@ def random_product(
- registration_email_template
"""

fake_vendor = {
"name": fake.company(),
"copyright": fake.company_suffix(),
"url": fake.url(),
"license_url": fake.url(),
"invitation_url": fake.url(),
"has_landing_page": fake.boolean(),
}
name = overrides.get("name")
suffix = fake.unique.word() if name is None else name

data = {
"name": fake.unique.first_name(),
"display_name": fake.company(),
"short_name": fake.user_name()[:10],
"name": f"prd_{suffix}",
"display_name": suffix.capitalize(),
"short_name": suffix[:4],
"host_regex": r"[a-zA-Z0-9]+\.com",
"support_email": fake.email(),
"twilio_messaging_sid": fake.random_element(elements=(None, fake.uuid4()[:34])),
"vendor": fake.random_element([None, fake_vendor]),
"vendor": Vendor(
name=fake.company(),
copyright=fake.company_suffix(),
url=fake.url(),
license_url=fake.url(),
invitation_url=fake.url(),
has_landing_page=fake.boolean(),
address=fake.address().replace("\n", ". "),
),
"registration_email_template": registration_email_template,
"created": fake.date_time_this_decade(),
"modified": fake.date_time_this_decade(),
Expand Down
4 changes: 4 additions & 0 deletions packages/settings-library/src/settings_library/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,7 @@ def enabled_tls_required_authentication(cls, values):
msg = "when using SMTP_PROTOCOL other than UNENCRYPTED username and password are required"
raise ValueError(msg)
return values

@property
def has_credentials(self) -> bool:
return self.SMTP_USERNAME is not None and self.SMTP_PASSWORD is not None
54 changes: 30 additions & 24 deletions services/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -354,30 +354,36 @@ services:
networks:
- default
environment:
- 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}
- PAYMENTS_GATEWAY_API_SECRET=${PAYMENTS_GATEWAY_API_SECRET}
- PAYMENTS_GATEWAY_URL=${PAYMENTS_GATEWAY_URL}
- PAYMENTS_LOGLEVEL=${PAYMENTS_LOGLEVEL}
- 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}
- RABBIT_SECURE=${RABBIT_SECURE}
- RABBIT_USER=${RABBIT_USER}
- RESOURCE_USAGE_TRACKER_HOST=${RESOURCE_USAGE_TRACKER_HOST}
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_ENABLED: ${PAYMENTS_AUTORECHARGE_ENABLED}
PAYMENTS_AUTORECHARGE_MIN_BALANCE_IN_CREDITS: ${PAYMENTS_AUTORECHARGE_MIN_BALANCE_IN_CREDITS}
PAYMENTS_GATEWAY_API_SECRET: ${PAYMENTS_GATEWAY_API_SECRET}
PAYMENTS_GATEWAY_URL: ${PAYMENTS_GATEWAY_URL}
PAYMENTS_LOGLEVEL: ${PAYMENTS_LOGLEVEL}
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}
RABBIT_SECURE: ${RABBIT_SECURE}
RABBIT_USER: ${RABBIT_USER}
RESOURCE_USAGE_TRACKER_HOST: ${RESOURCE_USAGE_TRACKER_HOST}
PAYMENTS_EMAIL: ${PAYMENTS_EMAIL}
SMTP_HOST: ${SMTP_HOST}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_PORT: ${SMTP_PORT}
SMTP_PROTOCOL: ${SMTP_PROTOCOL}
SMTP_USERNAME: ${SMTP_USERNAME}

resource-usage-tracker:
image: ${DOCKER_REGISTRY:-itisfoundation}/resource-usage-tracker:${DOCKER_IMAGE_TAG:-latest}
Expand Down
11 changes: 11 additions & 0 deletions services/payments/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,14 @@ test-dev-unit-external: ## runs test-dev against external service defined in $(e
test-ci-unit-external: ## runs test-ci against external service defined in $(external) envfile
# Running tests using external environ '$(external)'
$(MAKE) test-ci-unit pytest-parameters="--external-envfile=$(external) -m can_run_against_external"


test-repo-config: ## runs validation against `repo.config` files. e.g. `make test-repo-config SEARCH_ROOT=/path/to/ospar-config/deployments`
@if [ -z "$(SEARCH_ROOT)" ]; then \
echo "Error: SEARCH_ROOT is not set. Please set SEARCH_ROOT to the directory with repo.config files"; \
exit 1; \
fi
@for file in $$(find $(SEARCH_ROOT) -type f -name 'repo.config'); do \
echo "Validating settings for $$file"; \
pytest --external-envfile="$$file" --pdb tests/unit/test_core_settings.py; \
done
4 changes: 3 additions & 1 deletion services/payments/requirements/_base.in
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
--requirement ../../../packages/service-library/requirements/_fastapi.in


aiosmtplib # notifier
cryptography
fastapi
httpx
Jinja2 # notifier
packaging
python-jose
python-multipart
python-socketio
python-socketio # notifier
typer[all]
uvicorn[standard]
26 changes: 25 additions & 1 deletion services/payments/requirements/_base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ aiormq==6.7.7
# via aio-pika
aiosignal==1.3.1
# via aiohttp
aiosmtplib==3.0.1
# via -r requirements/_base.in
alembic==1.12.1
# via -r requirements/../../../packages/postgres-database/requirements/_base.in
anyio==4.0.0
Expand Down Expand Up @@ -123,6 +125,7 @@ fastapi==0.99.1
# -c requirements/../../../requirements/constraints.txt
# -r requirements/../../../packages/service-library/requirements/_fastapi.in
# -r requirements/_base.in
# prometheus-fastapi-instrumentator
frozenlist==1.4.0
# via
# aiohttp
Expand Down Expand Up @@ -158,6 +161,19 @@ idna==3.4
# email-validator
# httpx
# yarl
jinja2==3.1.3
# via
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/./../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/./../../../packages/settings-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/./../../../requirements/constraints.txt
# -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../requirements/constraints.txt
# -r requirements/_base.in
jsonschema==4.19.2
# via
# -r requirements/../../../packages/models-library/requirements/_base.in
Expand All @@ -181,7 +197,9 @@ mako==1.2.4
markdown-it-py==3.0.0
# via rich
markupsafe==2.1.3
# via mako
# via
# jinja2
# mako
mdurl==0.1.2
# via markdown-it-py
multidict==6.0.4
Expand All @@ -197,6 +215,12 @@ packaging==23.2
# via -r requirements/_base.in
pamqp==3.2.1
# via aiormq
prometheus-client==0.19.0
# via
# -r requirements/../../../packages/service-library/requirements/_fastapi.in
# prometheus-fastapi-instrumentator
prometheus-fastapi-instrumentator==6.1.0
# via -r requirements/../../../packages/service-library/requirements/_fastapi.in
psycopg2-binary==2.9.9
# via sqlalchemy
pyasn1==0.5.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
PaymentMethodID,
)
from ...services import payments, payments_methods
from ...services.notifier import Notifier
from ...services.notifier import NotifierService
from ...services.resource_usage_tracker import ResourceUsageTrackerApi
from ._dependencies import (
create_repository,
Expand All @@ -46,7 +46,7 @@ async def acknowledge_payment(
PaymentsMethodsRepo, Depends(create_repository(PaymentsMethodsRepo))
],
rut_api: Annotated[ResourceUsageTrackerApi, Depends(get_rut_api)],
notifier: Annotated[Notifier, Depends(get_from_app_state(Notifier))],
notifier: Annotated[NotifierService, Depends(get_from_app_state(NotifierService))],
background_tasks: BackgroundTasks,
):
"""completes (ie. ack) request initated by `/init` on the payments-gateway API"""
Expand Down Expand Up @@ -96,7 +96,7 @@ async def acknowledge_payment_method(
repo: Annotated[
PaymentsMethodsRepo, Depends(create_repository(PaymentsMethodsRepo))
],
notifier: Annotated[Notifier, Depends(get_from_app_state(Notifier))],
notifier: Annotated[NotifierService, Depends(get_from_app_state(NotifierService))],
background_tasks: BackgroundTasks,
):
"""completes (ie. ack) request initated by `/payments-methods:init` on the payments-gateway API"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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
from settings_library.email import SMTPSettings
from settings_library.postgres import PostgresSettings
from settings_library.rabbit import RabbitSettings
from settings_library.resource_usage_tracker import ResourceUsageTrackerSettings
Expand Down Expand Up @@ -114,3 +115,8 @@ class ApplicationSettings(_BaseApplicationSettings):
)

PAYMENTS_PROMETHEUS_INSTRUMENTATION_ENABLED: bool = True

PAYMENTS_EMAIL: SMTPSettings | None = Field(
auto_default_from_env=True,
description="optional email (see notifier_email service)",
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import sqlalchemy as sa
from models_library.api_schemas_webserver.wallets import PaymentID
from models_library.users import GroupID, UserID
from simcore_postgres_database.models.payments_transactions import payments_transactions
from simcore_postgres_database.models.products import products
from simcore_postgres_database.models.users import users

from .base import BaseRepository
Expand All @@ -10,13 +13,53 @@ class PaymentsUsersRepo(BaseRepository):
# Currently linked to `users` but expected to be linked to `payments_users`
# when databases are separated. The latter will be a subset copy of the former.
#
async def get_primary_group_id(self, user_id: UserID) -> GroupID:

async def _get(self, query):
async with self.db_engine.begin() as conn:
result = await conn.execute(
sa.select(users.c.primary_gid).where(users.c.id == user_id)
)
row = result.first()
if row is None:
msg = f"{user_id=} not found"
raise ValueError(msg)
result = await conn.execute(query)
return result.first()

async def get_primary_group_id(self, user_id: UserID) -> GroupID:
if row := await self._get(
sa.select(
users.c.primary_gid,
).where(users.c.id == user_id)
):
return GroupID(row.primary_gid)

msg = f"{user_id=} not found"
raise ValueError(msg)

async def get_notification_data(self, user_id: UserID, payment_id: PaymentID):
"""Retrives data that will be injected in a notification for the user on this payment"""
if row := await self._get(
sa.select(
payments_transactions.c.payment_id,
users.c.first_name,
users.c.last_name,
users.c.email,
products.c.name.label("product_name"),
products.c.display_name,
products.c.vendor,
products.c.support_email,
)
.select_from(
sa.join(
sa.join(
payments_transactions,
users,
payments_transactions.c.user_id == users.c.id,
),
products,
payments_transactions.c.product_name == products.c.name,
)
)
.where(
(payments_transactions.c.payment_id == payment_id)
& (payments_transactions.c.user_id == user_id)
)
):
return row

msg = f"{payment_id=} for {user_id=} was not found"
raise ValueError(msg)
Loading

0 comments on commit c80cedd

Please sign in to comment.