Skip to content

Commit d5d6998

Browse files
authored
✨ webserver API: get payment invoice link (#4870)
1 parent 707d185 commit d5d6998

File tree

13 files changed

+182
-76
lines changed

13 files changed

+182
-76
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""new invoice_url column in payments_transaction table
2+
3+
Revision ID: 7777d181dc1f
4+
Revises: 542e6ee8a8ea
5+
Create Date: 2023-10-16 16:19:26.657533+00:00
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "7777d181dc1f"
13+
down_revision = "542e6ee8a8ea"
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", sa.Column("invoice_url", sa.String(), nullable=True)
22+
)
23+
# ### end Alembic commands ###
24+
25+
26+
def downgrade():
27+
# ### commands auto generated by Alembic - please adjust! ###
28+
op.drop_column("payments_transactions", "invoice_url")
29+
# ### 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
@@ -83,6 +83,12 @@ class PaymentTransactionState(str, enum.Enum):
8383
nullable=True,
8484
doc="Extra comment on this payment (optional)",
8585
),
86+
sa.Column(
87+
"invoice_url",
88+
sa.String,
89+
nullable=True,
90+
doc="Link to invoice of this transaction. Available when completed",
91+
),
8692
#
8793
# States
8894
#

packages/postgres-database/src/simcore_postgres_database/utils_payments.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import datetime
2+
import logging
23
from dataclasses import dataclass
34
from decimal import Decimal
4-
from typing import TypeAlias
5+
from typing import Final, TypeAlias
56

67
import sqlalchemy as sa
78
from aiopg.sa.connection import SAConnection
@@ -10,11 +11,16 @@
1011
from . import errors
1112
from .models.payments_transactions import PaymentTransactionState, payments_transactions
1213

13-
PaymentID: TypeAlias = str
14+
_logger = logging.getLogger(__name__)
15+
1416

17+
PaymentID: TypeAlias = str
1518
PaymentTransactionRow: TypeAlias = RowProxy
1619

1720

21+
UNSET: Final[str] = "__UNSET__"
22+
23+
1824
@dataclass
1925
class PaymentFailure:
2026
payment_id: str
@@ -75,6 +81,7 @@ async def update_payment_transaction_state(
7581
payment_id: str,
7682
completion_state: PaymentTransactionState,
7783
state_message: str | None = None,
84+
invoice_url: str | None = UNSET,
7885
) -> PaymentTransactionRow | PaymentNotFound | PaymentAlreadyAcked:
7986
"""ACKs payment by updating state with SUCCESS, ..."""
8087
if completion_state == PaymentTransactionState.PENDING:
@@ -85,6 +92,17 @@ async def update_payment_transaction_state(
8592
if state_message:
8693
optional["state_message"] = state_message
8794

95+
if completion_state == PaymentTransactionState.SUCCESS and invoice_url is None:
96+
_logger.warning(
97+
"Payment %s completed as %s without invoice (%s)",
98+
payment_id,
99+
state_message,
100+
f"{invoice_url=}",
101+
)
102+
103+
if invoice_url != UNSET:
104+
optional["invoice_url"] = invoice_url
105+
88106
async with connection.begin():
89107
row = await (
90108
await connection.execute(

packages/postgres-database/tests/test_models_payments_transactions.py

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,15 @@
33
# pylint: disable=unused-variable
44
# pylint: disable=too-many-arguments
55

6-
import datetime
76
import decimal
87
from collections.abc import Callable
9-
from typing import Any
108

119
import pytest
1210
import sqlalchemy as sa
1311
from aiopg.sa.connection import SAConnection
1412
from aiopg.sa.result import RowProxy
15-
from pytest_simcore.helpers.rawdata_fakers import FAKE
13+
from faker import Faker
14+
from pytest_simcore.helpers.rawdata_fakers import random_payment_transaction, utcnow
1615
from simcore_postgres_database.models.payments_transactions import (
1716
PaymentTransactionState,
1817
payments_transactions,
@@ -27,32 +26,6 @@
2726
)
2827

2928

30-
def utcnow() -> datetime.datetime:
31-
return datetime.datetime.now(tz=datetime.timezone.utc)
32-
33-
34-
def random_payment_transaction(
35-
**overrides,
36-
) -> dict[str, Any]:
37-
"""Generates Metadata + concept/info (excludes state)"""
38-
data = {
39-
"payment_id": FAKE.uuid4(),
40-
"price_dollars": "123456.78",
41-
"osparc_credits": "123456.78",
42-
"product_name": "osparc",
43-
"user_id": FAKE.pyint(),
44-
"user_email": FAKE.email().lower(),
45-
"wallet_id": 1,
46-
"comment": "Free starting credits",
47-
"initiated_at": utcnow(),
48-
}
49-
# state is not added on purpose
50-
assert set(data.keys()).issubset({c.name for c in payments_transactions.columns})
51-
52-
data.update(overrides)
53-
return data
54-
55-
5629
async def test_numerics_precission_and_scale(connection: SAConnection):
5730
# https://docs.sqlalchemy.org/en/20/core/type_basics.html#sqlalchemy.types.Numeric
5831
# precision: This parameter specifies the total number of digits that can be stored, both before and after the decimal point.
@@ -117,6 +90,13 @@ async def test_init_transaction_sets_it_as_pending(
11790
}
11891

11992

93+
@pytest.fixture
94+
def invoice_url(faker: Faker, expected_state: PaymentTransactionState) -> str | None:
95+
if expected_state == PaymentTransactionState.SUCCESS:
96+
return faker.url()
97+
return None
98+
99+
120100
@pytest.mark.parametrize(
121101
"expected_state,expected_message",
122102
[
@@ -137,6 +117,7 @@ async def test_complete_transaction(
137117
payment_id: str,
138118
expected_state: PaymentTransactionState,
139119
expected_message: str | None,
120+
invoice_url: str | None,
140121
):
141122
await init_transaction(payment_id)
142123

@@ -145,6 +126,7 @@ async def test_complete_transaction(
145126
payment_id=payment_id,
146127
completion_state=expected_state,
147128
state_message=expected_message,
129+
invoice_url=invoice_url,
148130
)
149131

150132
assert isinstance(payment_row, PaymentTransactionRow)

packages/pytest-simcore/src/pytest_simcore/helpers/rawdata_fakers.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,30 @@ def random_payment_method(
205205

206206
data.update(overrides)
207207
return data
208+
209+
210+
def random_payment_transaction(
211+
**overrides,
212+
) -> dict[str, Any]:
213+
"""Generates Metadata + concept/info (excludes state)"""
214+
from simcore_postgres_database.models.payments_transactions import (
215+
payments_transactions,
216+
)
217+
218+
# initiated
219+
data = {
220+
"payment_id": FAKE.uuid4(),
221+
"price_dollars": "123456.78",
222+
"osparc_credits": "123456.78",
223+
"product_name": "osparc",
224+
"user_id": FAKE.pyint(),
225+
"user_email": FAKE.email().lower(),
226+
"wallet_id": 1,
227+
"comment": "Free starting credits",
228+
"initiated_at": utcnow(),
229+
}
230+
# state is not added on purpose
231+
assert set(data.keys()).issubset({c.name for c in payments_transactions.columns})
232+
233+
data.update(overrides)
234+
return data

0 commit comments

Comments
 (0)