EnCoach Odoo 19 custom modules
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
This commit is contained in:
128
encoach_subscription/services/paypal_service.py
Normal file
128
encoach_subscription/services/paypal_service.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
PAYPAL_SANDBOX_BASE = "https://api-m.sandbox.paypal.com"
|
||||
PAYPAL_LIVE_BASE = "https://api-m.paypal.com"
|
||||
|
||||
|
||||
class EncoachPaypalService:
|
||||
|
||||
def __init__(self, env):
|
||||
self.env = env
|
||||
params = env["ir.config_parameter"].sudo()
|
||||
self.client_id = params.get_param("encoach.paypal_client_id", "")
|
||||
self.client_secret = params.get_param("encoach.paypal_client_secret", "")
|
||||
is_live = params.get_param("encoach.paypal_live", "false").lower() == "true"
|
||||
self.base_url = PAYPAL_LIVE_BASE if is_live else PAYPAL_SANDBOX_BASE
|
||||
|
||||
def _get_access_token(self):
|
||||
resp = httpx.post(
|
||||
f"{self.base_url}/v1/oauth2/token",
|
||||
auth=(self.client_id, self.client_secret),
|
||||
data={"grant_type": "client_credentials"},
|
||||
timeout=30,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()["access_token"]
|
||||
|
||||
def _headers(self, token):
|
||||
return {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def create_order(self, user, package, discount_code=None):
|
||||
if not self.client_id or not self.client_secret:
|
||||
return {"error": "PayPal credentials 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)
|
||||
|
||||
currency = (package.currency or "USD").upper()
|
||||
|
||||
try:
|
||||
token = self._get_access_token()
|
||||
order_data = {
|
||||
"intent": "CAPTURE",
|
||||
"purchase_units": [{
|
||||
"amount": {
|
||||
"currency_code": currency,
|
||||
"value": f"{amount:.2f}",
|
||||
},
|
||||
"description": package.name,
|
||||
"custom_id": f"{user.id}:{package.id}",
|
||||
}],
|
||||
"application_context": {
|
||||
"return_url": "https://encoach.ai/payment/success",
|
||||
"cancel_url": "https://encoach.ai/payment/cancel",
|
||||
},
|
||||
}
|
||||
|
||||
resp = httpx.post(
|
||||
f"{self.base_url}/v2/checkout/orders",
|
||||
headers=self._headers(token),
|
||||
json=order_data,
|
||||
timeout=30,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
order = resp.json()
|
||||
|
||||
approval_url = ""
|
||||
for link in order.get("links", []):
|
||||
if link["rel"] == "approve":
|
||||
approval_url = link["href"]
|
||||
break
|
||||
|
||||
return {"orderId": order["id"], "approvalUrl": approval_url}
|
||||
except httpx.HTTPStatusError as e:
|
||||
_logger.error("PayPal API error: %s", e.response.text)
|
||||
return {"error": f"PayPal error: {e.response.status_code}"}
|
||||
except Exception as e:
|
||||
_logger.exception("PayPal create order error")
|
||||
return {"error": str(e)}
|
||||
|
||||
def capture_order(self, user, order_id):
|
||||
if not self.client_id or not self.client_secret:
|
||||
return {"error": "PayPal credentials not configured"}
|
||||
|
||||
try:
|
||||
token = self._get_access_token()
|
||||
resp = httpx.post(
|
||||
f"{self.base_url}/v2/checkout/orders/{order_id}/capture",
|
||||
headers=self._headers(token),
|
||||
timeout=30,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
capture = resp.json()
|
||||
|
||||
if capture.get("status") == "COMPLETED":
|
||||
purchase_unit = capture.get("purchase_units", [{}])[0]
|
||||
custom_id = purchase_unit.get("payments", {}).get("captures", [{}])[0].get("custom_id", "")
|
||||
parts = custom_id.split(":")
|
||||
user_id = int(parts[0]) if len(parts) >= 1 else user.id
|
||||
package_id = int(parts[1]) if len(parts) >= 2 else 0
|
||||
cap_amount = purchase_unit.get("payments", {}).get("captures", [{}])[0]
|
||||
amount = float(cap_amount.get("amount", {}).get("value", 0))
|
||||
currency = cap_amount.get("amount", {}).get("currency_code", "USD")
|
||||
|
||||
from .subscription_helper import extend_subscription
|
||||
extend_subscription(
|
||||
self.env, user_id, package_id, "paypal", order_id, amount, currency,
|
||||
)
|
||||
return {"ok": True}
|
||||
else:
|
||||
return {"error": f"Order status: {capture.get('status')}"}
|
||||
except httpx.HTTPStatusError as e:
|
||||
_logger.error("PayPal capture error: %s", e.response.text)
|
||||
return {"error": f"PayPal error: {e.response.status_code}"}
|
||||
except Exception as e:
|
||||
_logger.exception("PayPal capture error")
|
||||
return {"error": str(e)}
|
||||
Reference in New Issue
Block a user