Full backend implementation with custom Odoo modules: - encoach_api: Core API, user management, JWT auth - encoach_exam: Exam generation (reading, writing, listening, speaking) - encoach_evaluate: AI-powered evaluation (writing, speaking) - encoach_training: Training tips and walkthrough - encoach_storage: File storage management - encoach_payment: Stripe, PayPal, Paymob integration - encoach_mail: Email notifications Made-with: Cursor
171 lines
6.5 KiB
Python
171 lines
6.5 KiB
Python
import hashlib
|
|
import hmac
|
|
import json
|
|
import logging
|
|
|
|
import httpx
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
PAYMOB_BASE = "https://accept.paymob.com/api"
|
|
|
|
|
|
class EncoachPaymobService:
|
|
|
|
def __init__(self, env):
|
|
self.env = env
|
|
params = env["ir.config_parameter"].sudo()
|
|
self.api_key = params.get_param("encoach.paymob_api_key", "")
|
|
self.hmac_secret = params.get_param("encoach.paymob_hmac_secret", "")
|
|
|
|
def _authenticate(self):
|
|
resp = httpx.post(
|
|
f"{PAYMOB_BASE}/auth/tokens",
|
|
json={"api_key": self.api_key},
|
|
timeout=30,
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()["token"]
|
|
|
|
def create_intention(self, user, package, discount_code=None):
|
|
if not self.api_key:
|
|
return {"error": "Paymob API key not configured"}
|
|
|
|
amount = package.price
|
|
if discount_code:
|
|
discount = self.env["encoach.discount"].sudo().search([
|
|
("code", "=", discount_code),
|
|
], limit=1)
|
|
if discount and discount.is_valid():
|
|
amount = amount * (1 - discount.percentage / 100.0)
|
|
|
|
amount_cents = int(round(amount * 100))
|
|
currency = (package.currency or "EGP").upper()
|
|
|
|
try:
|
|
auth_token = self._authenticate()
|
|
|
|
order_resp = httpx.post(
|
|
f"{PAYMOB_BASE}/ecommerce/orders",
|
|
json={
|
|
"auth_token": auth_token,
|
|
"delivery_needed": False,
|
|
"amount_cents": amount_cents,
|
|
"currency": currency,
|
|
"merchant_order_id": f"encoach_{user.id}_{package.id}",
|
|
"items": [{
|
|
"name": package.name,
|
|
"amount_cents": amount_cents,
|
|
"quantity": 1,
|
|
}],
|
|
},
|
|
timeout=30,
|
|
)
|
|
order_resp.raise_for_status()
|
|
order = order_resp.json()
|
|
|
|
integration_id = self.env["ir.config_parameter"].sudo().get_param(
|
|
"encoach.paymob_integration_id", ""
|
|
)
|
|
|
|
key_resp = httpx.post(
|
|
f"{PAYMOB_BASE}/acceptance/payment_keys",
|
|
json={
|
|
"auth_token": auth_token,
|
|
"amount_cents": amount_cents,
|
|
"expiration": 3600,
|
|
"order_id": order["id"],
|
|
"billing_data": {
|
|
"email": user.login,
|
|
"first_name": (user.name or "").split()[0] if user.name else "N/A",
|
|
"last_name": (user.name or "").split()[-1] if user.name else "N/A",
|
|
"phone_number": "N/A",
|
|
"apartment": "N/A",
|
|
"floor": "N/A",
|
|
"street": "N/A",
|
|
"building": "N/A",
|
|
"shipping_method": "N/A",
|
|
"postal_code": "N/A",
|
|
"city": "N/A",
|
|
"country": "N/A",
|
|
"state": "N/A",
|
|
},
|
|
"currency": currency,
|
|
"integration_id": int(integration_id) if integration_id else 0,
|
|
"lock_order_when_paid": True,
|
|
},
|
|
timeout=30,
|
|
)
|
|
key_resp.raise_for_status()
|
|
payment_key = key_resp.json()["token"]
|
|
|
|
return {
|
|
"intentionId": str(order["id"]),
|
|
"clientSecret": payment_key,
|
|
}
|
|
except httpx.HTTPStatusError as e:
|
|
_logger.error("Paymob API error: %s", e.response.text)
|
|
return {"error": f"Paymob error: {e.response.status_code}"}
|
|
except Exception as e:
|
|
_logger.exception("Paymob create intention error")
|
|
return {"error": str(e)}
|
|
|
|
def verify_transaction(self, payload, hmac_header):
|
|
if not self.hmac_secret:
|
|
_logger.warning("No Paymob HMAC secret configured")
|
|
return None
|
|
|
|
try:
|
|
data = payload if isinstance(payload, dict) else json.loads(payload)
|
|
obj = data.get("obj", {})
|
|
concat_str = (
|
|
str(obj.get("amount_cents", "")) +
|
|
str(obj.get("created_at", "")) +
|
|
str(obj.get("currency", "")) +
|
|
str(obj.get("error_occured", "")).lower() +
|
|
str(obj.get("has_parent_transaction", "")).lower() +
|
|
str(obj.get("id", "")) +
|
|
str(obj.get("integration_id", "")) +
|
|
str(obj.get("is_3d_secure", "")).lower() +
|
|
str(obj.get("is_auth", "")).lower() +
|
|
str(obj.get("is_capture", "")).lower() +
|
|
str(obj.get("is_refunded", "")).lower() +
|
|
str(obj.get("is_standalone_payment", "")).lower() +
|
|
str(obj.get("is_voided", "")).lower() +
|
|
str(obj.get("order", {}).get("id", "")) +
|
|
str(obj.get("owner", "")) +
|
|
str(obj.get("pending", "")).lower() +
|
|
str(obj.get("source_data", {}).get("pan", "")) +
|
|
str(obj.get("source_data", {}).get("sub_type", "")) +
|
|
str(obj.get("source_data", {}).get("type", "")) +
|
|
str(obj.get("success", "")).lower()
|
|
)
|
|
|
|
computed = hmac.new(
|
|
self.hmac_secret.encode("utf-8"),
|
|
concat_str.encode("utf-8"),
|
|
hashlib.sha512,
|
|
).hexdigest()
|
|
|
|
if not hmac.compare_digest(computed, hmac_header):
|
|
return None
|
|
|
|
if obj.get("success") is True:
|
|
merchant_order_id = str(obj.get("order", {}).get("merchant_order_id", ""))
|
|
parts = merchant_order_id.replace("encoach_", "").split("_")
|
|
user_id = int(parts[0]) if len(parts) >= 1 else 0
|
|
package_id = int(parts[1]) if len(parts) >= 2 else 0
|
|
amount = obj.get("amount_cents", 0) / 100.0
|
|
currency = obj.get("currency", "EGP")
|
|
transaction_id = str(obj.get("id", ""))
|
|
|
|
from .subscription_helper import extend_subscription
|
|
extend_subscription(
|
|
self.env, user_id, package_id, "paymob", transaction_id, amount, currency,
|
|
)
|
|
return {"ok": True}
|
|
return {"error": "Transaction not successful"}
|
|
except Exception:
|
|
_logger.exception("Paymob verify transaction error")
|
|
return None
|