payment.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import hashlib
  2. import hmac
  3. import json
  4. import logging
  5. import os
  6. from typing import NoReturn
  7. from pathlib import Path
  8. import httpx
  9. from fastapi import HTTPException
  10. from fastapi.encoders import jsonable_encoder
  11. from pydantic import ValidationError
  12. from app.schema import CartSession, NowPaymentsInvoice, ShippingCostEntry, ShippingRate
  13. logger = logging.getLogger(__name__)
  14. def prepare_stripe_data(cart: CartSession) -> list[str, int]:
  15. """Returns list of items ready for Stripe to be processed.
  16. Each item is in the form of {price: <price_id>, quantity: <amount>}
  17. list_items = [
  18. {
  19. # Provide the exact Price ID (for example, pr_1234) of the product you want to sell
  20. 'price': ,
  21. 'quantity': 0
  22. }
  23. ]
  24. """
  25. list_items = []
  26. for item in cart.items.values():
  27. item = {
  28. "price": item.product_id,
  29. "quantity": item.quantity,
  30. }
  31. list_items.append(item)
  32. return list_items
  33. def is_country_in_europe(country: str) -> bool | NoReturn:
  34. """
  35. Check if given country is part of a list of countries in Europe.
  36. """
  37. dir_path = Path(__file__).resolve().parent
  38. europe_db_filepath = Path(f"{dir_path}/europe.json")
  39. try:
  40. with open(europe_db_filepath, encoding='utf-8') as file:
  41. europe_db = json.load(file)
  42. if country in europe_db['countries']:
  43. return True
  44. return False
  45. except FileNotFoundError as error:
  46. logger.error(f"is-country-in-europe => {error}")
  47. def calculate_shipping_cost(country: str, weight: int, currency: str):
  48. """Returns appropriate shipping price based on given Country and product's Weight."""
  49. dir_path = Path(__file__).resolve().parent
  50. shipping_cost_db_filepath = Path(f"{dir_path}/shipping-cost.json")
  51. try:
  52. with open(shipping_cost_db_filepath, encoding='utf-8') as f:
  53. shipping_cost_db = json.load(f)
  54. area = None
  55. if country == "Switzerland":
  56. area = country
  57. elif is_country_in_europe(country):
  58. area = "Europe"
  59. else:
  60. area = "International"
  61. shipping_rate = None
  62. for entry in shipping_cost_db['areas'][area]:
  63. try:
  64. entry = ShippingCostEntry(**entry)
  65. if (
  66. entry.max is None and weight >= entry.min or
  67. entry.min <= weight <= entry.max
  68. ):
  69. shipping_rate = ShippingRate(price=entry.price, area=area)
  70. except ValidationError as e:
  71. logger.error("pydantic validation errors =>")
  72. for e in e.errors():
  73. logger.error(f"error => {e}")
  74. return shipping_rate
  75. except FileNotFoundError as error:
  76. logger.error(f"calculate-shipping-cost => {error}")
  77. def prepare_shipping_rate(country: str, weight: int, currency: str):
  78. """
  79. """
  80. shipping_rate = calculate_shipping_cost(country, weight, currency)
  81. # Stripe needs shipping price set in cents as an integer
  82. shipping_rate.price = int(shipping_rate.price * 100)
  83. return {
  84. "shipping_rate_data": {
  85. "type": "fixed_amount",
  86. "fixed_amount": {"amount": shipping_rate.price, "currency": currency},
  87. "display_name": shipping_rate.area,
  88. }
  89. }
  90. async def generate_payment_link(data: NowPaymentsInvoice) -> tuple[str, str] | NoReturn:
  91. """Generate payment link for NOW Payments crypto payment service.
  92. See <https://documenter.getpostman.com/view/7907941/2s93JusNJt#f5e4e645-dce2-4b06-b2ca-2a29aaa5e845>.
  93. """
  94. environment = os.getenv("ENV")
  95. API_URL = "https://api.nowpayments.io/v1/"
  96. NOW_API_KEY = os.getenv("NOWPAYMENTS_API_KEY")
  97. if environment != "production":
  98. API_URL = "https://api-sandbox.nowpayments.io/v1/"
  99. NOW_API_KEY = os.getenv("NOWPAYMENTS_SANDBOX_API_KEY")
  100. ENDPOINT = "invoice"
  101. headers = {"x-api-key": NOW_API_KEY}
  102. async with httpx.AsyncClient(headers=headers) as client:
  103. resp = await client.post(f"{API_URL}{ENDPOINT}", json=jsonable_encoder(data))
  104. try:
  105. resp.raise_for_status()
  106. if resp.status_code == 200:
  107. data = resp.json()
  108. return data["invoice_url"], data["id"]
  109. except httpx.HTTPStatusError as exc:
  110. logger.error(f"Error {exc.response.status_code}: {exc.response.text}")
  111. raise HTTPException(
  112. status_code=400, detail="NOWPayments link cannot be generated."
  113. )
  114. def ipn_signature_check(
  115. np_secret_key: str, np_x_signature: str, payload: dict[str]
  116. ) -> bool:
  117. """Signature verification function as part of the NOW Paymnents's
  118. Instant Payment Notifications (IPN) webhook workflow.
  119. See <https://documenter.getpostman.com/view/7907941/2s93JusNJt>.
  120. """
  121. sorted_msg = json.dumps(payload, separators=(",", ":"), sort_keys=True)
  122. digest = hmac.new(
  123. str(np_secret_key).encode(), f"{sorted_msg}".encode(), hashlib.sha512
  124. )
  125. signature = digest.hexdigest()
  126. if signature == np_x_signature:
  127. return True
  128. else:
  129. return False