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

✨ Notify payment via email ⚠️ #5310

Merged
merged 33 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
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
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"""
matusdrobuliak66 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading