Skip to content

Commit 88492cd

Browse files
🎨 Send invoice attachment 🗃️ (#5688)
1 parent daa896c commit 88492cd

File tree

17 files changed

+168
-45
lines changed

17 files changed

+168
-45
lines changed

packages/notifications-library/src/notifications_library/_email.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from contextlib import asynccontextmanager
55
from email.headerregistry import Address
66
from email.message import EmailMessage
7-
from pathlib import Path
87
from typing import cast
98

109
from aiosmtplib import SMTP
@@ -38,22 +37,27 @@ def compose_email(
3837
return msg
3938

4039

41-
def _guess_file_type(file_path: Path) -> tuple[str, str]:
42-
assert file_path.is_file()
43-
mimetype, _encoding = mimetypes.guess_type(file_path)
40+
def _guess_file_type(file_name: str) -> tuple[str, str]:
41+
"""
42+
Guess the MIME type based on the file name extension.
43+
"""
44+
mimetype, _encoding = mimetypes.guess_type(file_name)
4445
if mimetype:
4546
maintype, subtype = mimetype.split("/", maxsplit=1)
4647
else:
4748
maintype, subtype = "application", "octet-stream"
4849
return maintype, subtype
4950

5051

51-
def add_attachments(msg: EmailMessage, file_paths: list[Path]):
52-
for attachment_path in file_paths:
53-
maintype, subtype = _guess_file_type(attachment_path)
52+
def add_attachments(msg: EmailMessage, attachments: list[tuple[bytes, str]]):
53+
for file_data, file_name in attachments:
54+
# Use the filename to guess the file type
55+
maintype, subtype = _guess_file_type(file_name)
56+
57+
# Add the attachment
5458
msg.add_attachment(
55-
attachment_path.read_bytes(),
56-
filename=attachment_path.name,
59+
file_data,
60+
filename=file_name,
5761
maintype=maintype,
5862
subtype=subtype,
5963
)

packages/notifications-library/src/notifications_library/payments.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class PaymentData:
2626
price_dollars: str
2727
osparc_credits: str
2828
invoice_url: str
29+
invoice_pdf_url: str
2930

3031

3132
async def notify_payment_completed(

packages/notifications-library/tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,5 @@ def payment_data(successful_transaction: dict[str, Any]) -> PaymentData:
154154
price_dollars=successful_transaction["price_dollars"],
155155
osparc_credits=successful_transaction["osparc_credits"],
156156
invoice_url=successful_transaction["invoice_url"],
157+
invoice_pdf_url=successful_transaction["invoice_pdf_url"],
157158
)

packages/notifications-library/tests/email/test_email_events.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,16 +139,17 @@ def event_extra_data( # noqa: PLR0911
139139

140140

141141
@pytest.fixture
142-
def event_attachments(event_name: str, faker: Faker, tmp_path: Path) -> list[Path]:
143-
paths = []
142+
def event_attachments(event_name: str, faker: Faker) -> list[tuple[bytes, str]]:
143+
attachments = []
144144
match event_name:
145145
case "on_payed":
146-
paths.append(tmp_path / "test-payed-invoice.pdf")
146+
# Create a fake PDF-like byte content and its filename
147+
file_name = "test-payed-invoice.pdf"
148+
# Simulate generating PDF data.
149+
fake_pdf_content = faker.text().encode("utf-8")
150+
attachments.append((fake_pdf_content, file_name))
147151

148-
# fill with fake data
149-
for p in paths:
150-
p.write_text(faker.text())
151-
return paths
152+
return attachments
152153

153154

154155
@pytest.mark.parametrize(
@@ -173,7 +174,7 @@ async def test_email_event(
173174
product_name: ProductName,
174175
event_name: str,
175176
event_extra_data: dict[str, Any],
176-
event_attachments: list[Path],
177+
event_attachments: list[tuple[bytes, str]],
177178
tmp_path: Path,
178179
):
179180
assert user_data.email == user_email
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""add invoice pdf column
2+
3+
Revision ID: 95d0932aaa83
4+
Revises: 4a0f4efe8c86
5+
Create Date: 2024-04-17 11:36:46.653089+00:00
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "95d0932aaa83"
13+
down_revision = "4a0f4efe8c86"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.add_column(
21+
"payments_transactions",
22+
sa.Column("invoice_pdf_url", sa.String(), nullable=True),
23+
)
24+
# ### end Alembic commands ###
25+
26+
27+
def downgrade():
28+
# ### commands auto generated by Alembic - please adjust! ###
29+
op.drop_column("payments_transactions", "invoice_pdf_url")
30+
# ### end Alembic commands ###

packages/postgres-database/src/simcore_postgres_database/models/payments_transactions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ def is_acknowledged(self) -> bool:
101101
nullable=True,
102102
doc="Invoice ID of invoice of this transaction. Available when completed",
103103
),
104+
sa.Column(
105+
"invoice_pdf_url",
106+
sa.String,
107+
nullable=True,
108+
doc="Link to invoice PDF. Available when completed",
109+
),
104110
#
105111
# States
106112
#

packages/pytest-simcore/src/pytest_simcore/faker_payments_data.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ def invoice_url(faker: Faker) -> HttpUrl:
4747
return parse_obj_as(HttpUrl, faker.image_url())
4848

4949

50+
@pytest.fixture
51+
def invoice_pdf_url(faker: Faker) -> HttpUrl:
52+
return parse_obj_as(HttpUrl, faker.image_url())
53+
54+
5055
@pytest.fixture
5156
def stripe_invoice_id(faker: Faker) -> StripeInvoiceID:
5257
return parse_obj_as(StripeInvoiceID, f"in_{faker.word()}")
@@ -60,6 +65,7 @@ def successful_transaction(
6065
user_id: UserID,
6166
product_name: ProductName,
6267
invoice_url: HttpUrl,
68+
invoice_pdf_url: HttpUrl,
6369
stripe_invoice_id: StripeInvoiceID,
6470
) -> dict[str, Any]:
6571

@@ -77,5 +83,6 @@ def successful_transaction(
7783
wallet_id=wallet_id,
7884
comment=f"fake fixture in {__name__}.successful_transaction",
7985
invoice_url=invoice_url,
86+
invoice_pdf_url=invoice_pdf_url,
8087
stripe_invoice_id=stripe_invoice_id,
8188
)

services/payments/src/simcore_service_payments/db/payments_transactions_repo.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ async def update_ack_payment_transaction(
6868
state_message: str | None,
6969
invoice_url: HttpUrl | None,
7070
stripe_invoice_id: StripeInvoiceID | None,
71+
invoice_pdf_url: HttpUrl | None,
7172
) -> PaymentsTransactionsDB:
7273
"""
7374
- ACKs payment by updating state with SUCCESS, CANCEL, etc
@@ -115,6 +116,7 @@ async def update_ack_payment_transaction(
115116
state=completion_state,
116117
invoice_url=invoice_url,
117118
stripe_invoice_id=stripe_invoice_id,
119+
invoice_pdf_url=invoice_pdf_url,
118120
**optional,
119121
)
120122
.where(payments_transactions.c.payment_id == f"{payment_id}")

services/payments/src/simcore_service_payments/models/db.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"comment": "This is a test comment.",
2626
"invoice_url": None,
2727
"stripe_invoice_id": None,
28+
"invoice_pdf_url": None,
2829
"initiated_at": "2023-09-27T10:00:00",
2930
"state": PaymentTransactionState.PENDING,
3031
}
@@ -41,6 +42,7 @@ class PaymentsTransactionsDB(BaseModel):
4142
comment: str | None
4243
invoice_url: HttpUrl | None
4344
stripe_invoice_id: StripeInvoiceID | None
45+
invoice_pdf_url: HttpUrl | None
4446
initiated_at: datetime.datetime
4547
completed_at: datetime.datetime | None
4648
state: PaymentTransactionState
@@ -55,6 +57,8 @@ class Config:
5557
{
5658
**_EXAMPLE_AFTER_INIT,
5759
"invoice_url": "https://my-fake-pdf-link.com",
60+
"stripe_invoice_id": "12345",
61+
"invoice_pdf_url": "https://my-fake-pdf-link.com",
5862
"completed_at": "2023-09-27T10:00:10",
5963
"state": "SUCCESS",
6064
"state_message": "Payment completed successfully",

services/payments/src/simcore_service_payments/services/notifier.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22
import logging
33

44
from fastapi import FastAPI
5-
from models_library.api_schemas_webserver.wallets import (
6-
PaymentMethodTransaction,
7-
PaymentTransaction,
8-
)
5+
from models_library.api_schemas_webserver.wallets import PaymentMethodTransaction
96
from models_library.users import UserID
107
from servicelib.fastapi.app_state import SingletonInAppStateMixin
118
from servicelib.utils import fire_and_forget_task
129

1310
from ..core.settings import ApplicationSettings
1411
from ..db.payment_users_repo import PaymentsUsersRepo
12+
from ..models.db import PaymentsTransactionsDB
1513
from .notifier_abc import NotificationProvider
1614
from .notifier_email import EmailProvider
1715
from .notifier_ws import WebSocketProvider
@@ -37,7 +35,7 @@ def _run_in_background(self, coro, suffix):
3735
async def notify_payment_completed(
3836
self,
3937
user_id: UserID,
40-
payment: PaymentTransaction,
38+
payment: PaymentsTransactionsDB,
4139
*,
4240
exclude: set | None = None,
4341
):

0 commit comments

Comments
 (0)