Skip to content

Commit 018115f

Browse files
♻️ refactor(payments): use Notification service for emails ⚠️ 🚨 (#9042)
1 parent 4ed60a0 commit 018115f

23 files changed

Lines changed: 765 additions & 568 deletions

File tree

.env-devel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ PAYMENTS_AUTORECHARGE_DEFAULT_TOP_UP_AMOUNT=100.0
187187
PAYMENTS_AUTORECHARGE_ENABLED=1
188188
PAYMENTS_AUTORECHARGE_MIN_BALANCE_IN_CREDITS=100
189189
PAYMENTS_BCC_EMAIL=none
190-
PAYMENTS_EMAIL={}
191190
PAYMENTS_FAKE_COMPLETION_DELAY_SEC=10
192191
PAYMENTS_FAKE_COMPLETION=0
193192
PAYMENTS_GATEWAY_API_SECRET=adminadmin

packages/models-library/src/models_library/notifications/celery/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from ._email import EmailContact, EmailContent, EmailMessage
1+
from ._email import EmailAttachment, EmailContact, EmailContent, EmailMessage
22

33
__all__: tuple[str, ...] = (
4+
"EmailAttachment",
45
"EmailContact",
56
"EmailContent",
67
"EmailMessage",

packages/models-library/src/models_library/notifications/rpc/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ._email import (
77
Addressing,
88
EmailAddressing,
9+
EmailAttachment,
910
EmailContact,
1011
EmailContent,
1112
EmailMessage,
@@ -28,6 +29,7 @@
2829
__all__: tuple[str, ...] = (
2930
"Addressing",
3031
"EmailAddressing",
32+
"EmailAttachment",
3133
"EmailContact",
3234
"EmailContent",
3335
"EmailMessage",

packages/models-library/src/models_library/notifications/rpc/_email.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ class EmailContact(BaseModel):
1010
email: EmailStr
1111

1212

13+
class EmailAttachment(BaseModel):
14+
content: bytes
15+
filename: str
16+
17+
1318
class EmailContent(BaseModel):
1419
subject: Annotated[str, Field(min_length=1, max_length=998)]
1520
body_html: str | None = None
@@ -19,8 +24,11 @@ class EmailContent(BaseModel):
1924
class EmailAddressing(BaseModel):
2025
from_: Annotated[EmailContact, Field(alias="from")]
2126
to: list[EmailContact]
27+
bcc: EmailContact | None = None
2228
reply_to: EmailContact | None = None
2329

30+
attachments: list[EmailAttachment] | None = None
31+
2432
model_config = ConfigDict(
2533
frozen=True,
2634
validate_by_alias=True,

packages/notifications-library/src/notifications_library/templates/email/paid/body_html.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<p>We are delighted to confirm the successful processing of your payment of <strong>{{ payment.price_dollars }}</strong> <strong><em>USD</em></strong> for the purchase of <strong>{{ payment.osparc_credits }}</strong> <strong><em>credits</em></strong>.
99
The credits have been added to your {{ product.display_name }} account, and you are all set to utilize them.</p>
1010
<p>For more details you can view or download your <a href="{{ payment.invoice_url }}">receipt</a>.</p>
11-
<p>Please don't hesitate to contact us at {{ product.support_email }} if you need further help.</p>
11+
<p>Please don't hesitate to contact us at <a href="mailto:{{ product.support_email }}">{{ product.support_email }}</a> if you need further help.</p>
1212
<p>Best Regards,</p>
1313
<p>The <i>{{ product.display_name }}</i> Team</p>
1414

services/docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1549,7 +1549,6 @@ services:
15491549
PAYMENTS_AUTORECHARGE_ENABLED: ${PAYMENTS_AUTORECHARGE_ENABLED}
15501550
PAYMENTS_AUTORECHARGE_MIN_BALANCE_IN_CREDITS: ${PAYMENTS_AUTORECHARGE_MIN_BALANCE_IN_CREDITS}
15511551
PAYMENTS_BCC_EMAIL: ${PAYMENTS_BCC_EMAIL}
1552-
PAYMENTS_EMAIL: ${PAYMENTS_EMAIL}
15531552
PAYMENTS_GATEWAY_API_SECRET: ${PAYMENTS_GATEWAY_API_SECRET}
15541553
PAYMENTS_GATEWAY_URL: ${PAYMENTS_GATEWAY_URL}
15551554
PAYMENTS_LOGLEVEL: ${PAYMENTS_LOGLEVEL}

services/notifications/src/simcore_service_notifications/api/celery/_email.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from models_library.celery import TaskKey
1010
from models_library.notifications.celery import EmailContact, EmailContent, EmailMessage
1111
from notifications_library._email import (
12+
add_attachments,
1213
compose_email,
1314
create_email_session,
1415
)
@@ -33,22 +34,29 @@ async def send_email_message(
3334
msg = EmailMessage(
3435
from_=EmailContact(**message.from_.model_dump()),
3536
to=EmailContact(**message.to.model_dump()),
37+
bcc=EmailContact(**message.bcc.model_dump()) if message.bcc else None,
3638
reply_to=EmailContact(**message.reply_to.model_dump()) if message.reply_to else None,
3739
content=EmailContent(**message.content.model_dump()),
40+
attachments=message.attachments,
3841
)
3942

4043
with log_context(_logger, logging.INFO, "Send email to %s", msg.to.email):
4144
settings = SMTPSettings.create_from_envs()
4245

4346
async with create_email_session(settings=settings) as smtp:
44-
await smtp.send_message(
45-
compose_email(
46-
from_=_to_address(msg.from_),
47-
to=_to_address(msg.to),
48-
subject=msg.content.subject,
49-
content_text=msg.content.body_text,
50-
content_html=msg.content.body_html,
51-
reply_to=_to_address(msg.reply_to) if msg.reply_to else None,
52-
extra_headers=settings.SMTP_EXTRA_HEADERS,
53-
)
47+
email_msg = compose_email(
48+
from_=_to_address(msg.from_),
49+
to=_to_address(msg.to),
50+
subject=msg.content.subject,
51+
content_text=msg.content.body_text,
52+
content_html=msg.content.body_html,
53+
reply_to=_to_address(msg.reply_to) if msg.reply_to else None,
54+
bcc=[_to_address(msg.bcc)] if msg.bcc else None,
55+
extra_headers=settings.SMTP_EXTRA_HEADERS,
5456
)
57+
if msg.attachments:
58+
add_attachments(
59+
email_msg,
60+
[(a.content, a.filename) for a in msg.attachments],
61+
)
62+
await smtp.send_message(email_msg)

services/notifications/src/simcore_service_notifications/models/template_contexts/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from ._credit_reimbursement import CreditReimbursementTemplateContext
66
from ._empty import EmptyTemplateContext
77
from ._new_2fa_code import New2faCodeTemplateContext
8+
from ._paid import PaidTemplateContext
89
from ._registered import RegisteredTemplateContext
910
from ._reset_password import ResetPasswordTemplateContext
1011
from ._unregister import UnregisterTemplateContext
@@ -17,6 +18,7 @@
1718
"CreditReimbursementTemplateContext",
1819
"EmptyTemplateContext",
1920
"New2faCodeTemplateContext",
21+
"PaidTemplateContext",
2022
"RegisteredTemplateContext",
2123
"ResetPasswordTemplateContext",
2224
"UnregisterTemplateContext",
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Context model for the 'paid' email template."""
2+
3+
from models_library.notifications import Channel
4+
from pydantic import BaseModel, HttpUrl
5+
6+
from ..template import BaseTemplateContext, register_template_context
7+
8+
9+
class User(BaseModel):
10+
first_name: str | None = None
11+
last_name: str | None = None
12+
user_name: str | None = None
13+
email: str | None = None
14+
15+
16+
class Payment(BaseModel):
17+
price_dollars: str
18+
osparc_credits: str
19+
invoice_url: HttpUrl
20+
21+
22+
@register_template_context(channel=Channel.email, template_name="paid")
23+
class PaidTemplateContext(BaseTemplateContext):
24+
user: User
25+
payment: Payment

services/notifications/src/simcore_service_notifications/services/channel_handlers/_email.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,31 @@ class EmailChannelHandler(ChannelHandler):
2222
def prepare_messages(message: EmailMessage) -> list[dict[str, Any]]:
2323
content_dict = message.content.model_dump()
2424
from_dict = message.addressing.from_.model_dump()
25+
bcc_dict = message.addressing.bcc.model_dump() if message.addressing.bcc else None
2526
reply_to_dict = message.addressing.reply_to.model_dump() if message.addressing.reply_to else None
2627

2728
recipients = _interleave_recipients_by_domain(message.addressing.to)
2829

30+
attachments_list = (
31+
[a.model_dump() for a in message.addressing.attachments] if message.addressing.attachments else None
32+
)
33+
2934
payload_base: dict[str, Any] = {
3035
"channel": message.channel,
3136
"from": from_dict,
3237
"content": content_dict,
3338
}
39+
if bcc_dict:
40+
payload_base["bcc"] = bcc_dict
3441
if reply_to_dict:
3542
payload_base["reply_to"] = reply_to_dict
3643

44+
if attachments_list:
45+
payload_base["attachments"] = attachments_list
46+
3747
return [
38-
CeleryEmailMessage.model_validate({**payload_base, "to": recipient.model_dump()}).model_dump(by_alias=True)
48+
CeleryEmailMessage.model_validate({**payload_base, "to": recipient.model_dump()}).model_dump(
49+
by_alias=True, exclude_none=True
50+
)
3951
for recipient in recipients
4052
]

0 commit comments

Comments
 (0)