Skip to content

Commit e880d71

Browse files
authored
🐛 Improved Error Handling for Missing Billing Details (#6418)
1 parent 2990728 commit e880d71

File tree

7 files changed

+69
-5
lines changed

7 files changed

+69
-5
lines changed

packages/models-library/src/models_library/users.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class UserBillingDetails(BaseModel):
2222
address: str | None
2323
city: str | None
2424
state: str | None = Field(description="State, province, canton, ...")
25-
country: str
25+
country: str # Required for taxes
2626
postal_code: str | None
2727
phone: str | None
2828

services/web/server/src/simcore_service_webserver/payments/_onetime_api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ async def init_creation_of_wallet_payment(
279279
Raises:
280280
UserNotFoundError
281281
WalletAccessForbiddenError
282+
BillingDetailsNotFoundError
282283
"""
283284

284285
# wallet: check permissions
@@ -293,6 +294,7 @@ async def init_creation_of_wallet_payment(
293294
# user info
294295
user = await get_user_display_and_id_names(app, user_id=user_id)
295296
user_invoice_address = await get_user_invoice_address(app, user_id=user_id)
297+
296298
# stripe info
297299
product_stripe_info = await get_product_stripe_info(app, product_name=product_name)
298300

services/web/server/src/simcore_service_webserver/users/_db.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from ..db.models import user_to_groups
2323
from ..db.plugin import get_database_engine
24+
from .exceptions import BillingDetailsNotFoundError
2425
from .schemas import Permission
2526

2627
_ALL = None
@@ -203,9 +204,12 @@ async def new_user_details(
203204
async def get_user_billing_details(
204205
engine: Engine, user_id: UserID
205206
) -> UserBillingDetails:
207+
"""
208+
Raises:
209+
BillingDetailsNotFoundError
210+
"""
206211
async with engine.acquire() as conn:
207212
user_billing_details = await UsersRepo.get_billing_details(conn, user_id)
208213
if not user_billing_details:
209-
msg = f"Missing biling details for user {user_id}"
210-
raise ValueError(msg)
214+
raise BillingDetailsNotFoundError(user_id=user_id)
211215
return UserBillingDetails.from_orm(user_billing_details)

services/web/server/src/simcore_service_webserver/users/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,8 @@ class AlreadyPreRegisteredError(UsersBaseError):
4545
msg_template = (
4646
"Found {num_found} matches for '{email}'. Cannot pre-register existing user"
4747
)
48+
49+
50+
class BillingDetailsNotFoundError(UsersBaseError):
51+
# NOTE: this is for internal log and should not be transmitted to the final user
52+
msg_template = "Billing details are missing for user_id={user_id}. TIP: Check whether this user is pre-registered"

services/web/server/src/simcore_service_webserver/wallets/_constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@
33
MSG_PRICE_NOT_DEFINED_ERROR: Final[
44
str
55
] = "No payments are accepted until this product has a price"
6+
7+
MSG_BILLING_DETAILS_NOT_DEFINED_ERROR: Final[str] = (
8+
"Payments cannot be processed: Required billing details (e.g. country for tax) are missing from your account."
9+
"Please contact support to resolve this configuration issue."
10+
)

services/web/server/src/simcore_service_webserver/wallets/_handlers.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
parse_request_path_parameters_as,
1919
)
2020
from servicelib.aiohttp.typing_extension import Handler
21+
from servicelib.error_codes import create_error_code
22+
from servicelib.logging_utils import LogExtra, get_log_record_extra
2123
from servicelib.request_keys import RQT_USERID_KEY
2224

2325
from .._constants import RQ_PRODUCT_KEY
@@ -36,10 +38,16 @@
3638
)
3739
from ..products.errors import BelowMinimumPaymentError, ProductPriceNotDefinedError
3840
from ..security.decorators import permission_required
39-
from ..users.exceptions import UserDefaultWalletNotFoundError
41+
from ..users.exceptions import (
42+
BillingDetailsNotFoundError,
43+
UserDefaultWalletNotFoundError,
44+
)
4045
from ..utils_aiohttp import envelope_json_response
4146
from . import _api
42-
from ._constants import MSG_PRICE_NOT_DEFINED_ERROR
47+
from ._constants import (
48+
MSG_BILLING_DETAILS_NOT_DEFINED_ERROR,
49+
MSG_PRICE_NOT_DEFINED_ERROR,
50+
)
4351
from .errors import WalletAccessForbiddenError, WalletNotFoundError
4452

4553
_logger = logging.getLogger(__name__)
@@ -80,6 +88,20 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
8088
except ProductPriceNotDefinedError as exc:
8189
raise web.HTTPConflict(reason=MSG_PRICE_NOT_DEFINED_ERROR) from exc
8290

91+
except BillingDetailsNotFoundError as exc:
92+
error_code = create_error_code(exc)
93+
log_extra: LogExtra = {}
94+
if user_id := getattr(exc, "user_id", None):
95+
log_extra = get_log_record_extra(user_id=user_id) or {}
96+
97+
log_msg = f"{exc} [{error_code}]"
98+
_logger.exception(
99+
log_msg,
100+
extra={"error_code": error_code, **log_extra},
101+
)
102+
user_msg = f"{MSG_BILLING_DETAILS_NOT_DEFINED_ERROR} ({error_code})"
103+
raise web.HTTPServiceUnavailable(reason=user_msg) from exc
104+
83105
return wrapper
84106

85107

services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
PaymentsSettings,
3434
get_plugin_settings,
3535
)
36+
from simcore_service_webserver.wallets._constants import (
37+
MSG_BILLING_DETAILS_NOT_DEFINED_ERROR,
38+
)
3639

3740
OpenApiDict: TypeAlias = dict[str, Any]
3841

@@ -312,6 +315,29 @@ async def test_complete_payment_errors(
312315
send_message.assert_called_once()
313316

314317

318+
async def test_billing_info_missing_error(
319+
latest_osparc_price: Decimal,
320+
client: TestClient,
321+
logged_user_wallet: WalletGet,
322+
):
323+
# NOTE: setup_user_pre_registration_details_db is not setup to emulate missing pre-registration
324+
325+
assert client.app
326+
wallet = logged_user_wallet
327+
328+
# Pay
329+
response = await client.post(
330+
f"/v0/wallets/{wallet.wallet_id}/payments", json={"priceDollars": 25}
331+
)
332+
data, error = await assert_status(response, status.HTTP_503_SERVICE_UNAVAILABLE)
333+
334+
assert not data
335+
assert MSG_BILLING_DETAILS_NOT_DEFINED_ERROR in error["message"]
336+
337+
assert response.reason
338+
assert MSG_BILLING_DETAILS_NOT_DEFINED_ERROR in response.reason
339+
340+
315341
async def test_payment_not_found(
316342
latest_osparc_price: Decimal,
317343
client: TestClient,

0 commit comments

Comments
 (0)