Files
Odoo-Modules/fusion_poynt/utils.py
gsinghpal 0e1aebe60b feat: add Pending status for delivery/technician tasks
- New 'pending' status allows tasks to be created without a schedule,
  acting as a queue for unscheduled work that gets assigned later
- Pending group appears in the Delivery Map sidebar with amber color
- Other modules can create tasks in pending state for scheduling
- scheduled_date no longer required (null for pending tasks)
- New Pending Tasks menu item under Field Service
- Pending filter added to search view

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 04:21:05 -05:00

252 lines
7.9 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import time
import uuid
from odoo.exceptions import ValidationError
from odoo.addons.fusion_poynt import const
def generate_request_id():
"""Generate a unique request ID for Poynt API idempotency."""
return str(uuid.uuid4())
def build_api_url(endpoint, business_id=None, store_id=None, is_test=False):
"""Build a full Poynt API URL for the given endpoint.
:param str endpoint: The API endpoint path (e.g., 'orders', 'transactions').
:param str business_id: The merchant's business UUID.
:param str store_id: The store UUID (optional, for store-scoped endpoints).
:param bool is_test: Whether to use the test environment.
:return: The full API URL.
:rtype: str
"""
base = const.API_BASE_URL_TEST if is_test else const.API_BASE_URL
if business_id and store_id:
return f"{base}/businesses/{business_id}/stores/{store_id}/{endpoint}"
elif business_id:
return f"{base}/businesses/{business_id}/{endpoint}"
return f"{base}/{endpoint}"
def build_api_headers(access_token, request_id=None):
"""Build the standard HTTP headers for a Poynt API request.
:param str access_token: The OAuth2 bearer token.
:param str request_id: Optional unique request ID for idempotency.
:return: The request headers dict.
:rtype: dict
"""
headers = {
'Content-Type': 'application/json',
'Api-Version': const.API_VERSION,
'Authorization': f'Bearer {access_token}',
}
if request_id:
headers['POYNT-REQUEST-ID'] = request_id
return headers
def create_self_signed_jwt(application_id, private_key_pem):
"""Create a self-signed JWT for Poynt OAuth2 token request.
The JWT is signed with the application's RSA private key and used
as the assertion in the JWT bearer grant type flow.
:param str application_id: The Poynt application ID (urn:aid:...).
:param str private_key_pem: PEM-encoded RSA private key string.
:return: The signed JWT string.
:rtype: str
:raises ValidationError: If JWT creation fails.
"""
try:
import jwt as pyjwt
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.backends import default_backend
except ImportError:
raise ValidationError(
"Required Python packages 'PyJWT' and 'cryptography' are not installed. "
"Install them with: pip install PyJWT cryptography"
)
try:
if isinstance(private_key_pem, bytes):
key_bytes = private_key_pem
else:
key_bytes = private_key_pem.encode('utf-8')
private_key = load_pem_private_key(key_bytes, password=None, backend=default_backend())
now = int(time.time())
payload = {
'iss': application_id,
'sub': application_id,
'aud': 'https://services.poynt.net',
'iat': now,
'exp': now + 300,
'jti': str(uuid.uuid4()),
}
token = pyjwt.encode(payload, private_key, algorithm='RS256')
return token
except Exception as e:
raise ValidationError(
f"Failed to create self-signed JWT for Poynt authentication: {e}"
)
def format_poynt_amount(amount, currency):
"""Convert a major currency amount to Poynt's minor units (cents).
:param float amount: The amount in major currency units.
:param recordset currency: The currency record.
:return: The amount in minor currency units (integer).
:rtype: int
"""
decimals = const.CURRENCY_DECIMALS.get(currency.name, 2)
return int(round(amount * (10 ** decimals)))
def parse_poynt_amount(minor_amount, currency):
"""Convert Poynt's minor currency units back to major units.
:param int minor_amount: The amount in minor currency units.
:param recordset currency: The currency record.
:return: The amount in major currency units.
:rtype: float
"""
decimals = const.CURRENCY_DECIMALS.get(currency.name, 2)
return minor_amount / (10 ** decimals)
def extract_card_details(funding_source):
"""Extract card details from a Poynt funding source object.
:param dict funding_source: The Poynt fundingSource object from a transaction.
:return: Dict with card brand, last4, expiration, and card type.
:rtype: dict
"""
if not funding_source or 'card' not in funding_source:
return {}
card = funding_source['card']
brand_code = const.CARD_BRAND_MAPPING.get(
card.get('type', ''), 'card'
)
return {
'brand': brand_code,
'last4': card.get('numberLast4', ''),
'exp_month': card.get('expirationMonth'),
'exp_year': card.get('expirationYear'),
'card_holder': card.get('cardHolderFullName', ''),
'card_id': card.get('cardId', ''),
'number_first6': card.get('numberFirst6', ''),
}
def get_poynt_status(status_str):
"""Map a Poynt transaction status string to an Odoo transaction state.
:param str status_str: The Poynt transaction status.
:return: The corresponding Odoo payment state.
:rtype: str
"""
for odoo_state, poynt_statuses in const.STATUS_MAPPING.items():
if status_str in poynt_statuses:
return odoo_state
return 'error'
def build_order_payload(reference, amount, currency, items=None, notes=''):
"""Build a Poynt order creation payload.
:param str reference: The Odoo transaction reference.
:param float amount: The order total in major currency units.
:param recordset currency: The currency record.
:param list items: Optional list of order item dicts.
:param str notes: Optional order notes.
:return: The Poynt-formatted order payload.
:rtype: dict
"""
minor_amount = format_poynt_amount(amount, currency)
if not items:
items = [{
'name': reference,
'quantity': 1,
'unitPrice': minor_amount,
'tax': 0,
'status': 'ORDERED',
'unitOfMeasure': 'EACH',
}]
return {
'items': items,
'amounts': {
'subTotal': minor_amount,
'discountTotal': 0,
'feeTotal': 0,
'taxTotal': 0,
'netTotal': minor_amount,
'currency': currency.name,
},
'context': {
'source': 'WEB',
'transactionInstruction': 'ONLINE_AUTH_REQUIRED',
},
'statuses': {
'status': 'OPENED',
},
'notes': notes or reference,
}
def build_transaction_payload(
action, amount, currency, order_id=None, reference='', funding_source=None
):
"""Build a Poynt transaction payload for charge/auth/capture.
:param str action: The transaction action (AUTHORIZE, SALE, CAPTURE, etc.).
:param float amount: The amount in major currency units.
:param recordset currency: The currency record.
:param str order_id: The Poynt order UUID (optional).
:param str reference: The Odoo transaction reference.
:param dict funding_source: The funding source / card data (optional).
:return: The Poynt-formatted transaction payload.
:rtype: dict
"""
minor_amount = format_poynt_amount(amount, currency)
payload = {
'action': action,
'amounts': {
'transactionAmount': minor_amount,
'orderAmount': minor_amount,
'tipAmount': 0,
'cashbackAmount': 0,
'currency': currency.name,
},
'context': {
'source': 'WEB',
'sourceApp': 'odoo.fusion_poynt',
'transactionInstruction': 'ONLINE_AUTH_REQUIRED',
},
'notes': reference,
}
if order_id:
payload['references'] = [{
'id': order_id,
'type': 'POYNT_ORDER',
}]
if funding_source:
payload['fundingSource'] = funding_source
return payload