Files
shoppy/shop/utils.py
T
2025-01-12 22:40:46 +01:00

276 lines
7.6 KiB
Python

import base64
import hashlib
import hmac
import json
from decimal import Decimal
import pyDes
from django.contrib.auth import get_user_model
from django.db import transaction
from django.utils import timezone
from django.utils.text import gettext_lazy as _
from shop.exceptions import RedsysPaymentException, RedsysValidationException
from shop.models import (
Cart,
CustomerAddress,
Order,
OrderLine,
Payment,
Product,
ProductBatch,
ProductPrice,
ShippingMethod,
)
from shop.settings import ERROR_CODES
User = get_user_model()
def create_order_line_for_product(product: Product, quantity: Decimal, order: Order):
price = product.prices.last()
base_total = round(price.price * quantity, 2)
tax_value = price.tax.value / Decimal("100")
taxes = round(base_total * tax_value, 2)
return OrderLine.objects.create(
order=order,
product=product,
quantity=quantity,
price=price.price,
base_total=base_total,
tax_value=tax_value,
taxes=taxes,
total=base_total + taxes,
)
def create_order(
customer: User,
billing_address: str,
billing_city: str,
billing_state: str,
billing_country: str,
billing_zip: str,
shipping_address: str = "",
shipping_city: str = "",
shipping_state: str = "",
shipping_country: str = "",
shipping_zip: str = "",
) -> Order:
order = Order.objects.create(
user=customer,
billing_address=billing_address,
billing_city=billing_city,
billing_state=billing_state,
billing_country=billing_country,
billing_zip=billing_zip,
shipping_address=shipping_address,
shipping_city=shipping_city,
shipping_state=shipping_state,
shipping_country=shipping_country,
shipping_zip=shipping_zip,
)
return order
def delete_product_batch(batch: ProductBatch):
product = batch.product
product.stock = max(product.stock - batch.quantity, 0)
product.save()
batch.delete()
def create_order_from_cart(
cart: Cart,
shipping_method: ShippingMethod,
billing_address_full_name="",
billing_address_address="",
billing_address_town="",
billing_address_state="",
billing_address_country="",
billing_address_zip="",
shipping_address_full_name="",
shipping_address_address="",
shipping_address_town="",
shipping_address_state="",
shipping_address_country="",
shipping_address_zip="",
shipping_address_phone="",
email="",
):
order = Order.objects.create(
billing_address=f"{billing_address_full_name} {billing_address_address}",
billing_city=billing_address_town,
billing_state=billing_address_state,
billing_country=billing_address_country,
billing_zip=billing_address_zip,
contact_phone=shipping_address_phone,
shipping_address=f"{shipping_address_address} {shipping_address_full_name}",
shipping_city=shipping_address_town,
shipping_state=shipping_address_state,
shipping_country=shipping_address_country,
shipping_zip=shipping_address_zip,
user=cart.user,
email=email,
)
for item in cart.items.all():
product = item.product
price: ProductPrice = product.price
base_total = price.price * item.quantity
tax_value = price.tax.value
taxes = base_total * tax_value / Decimal(100)
total = base_total + taxes
OrderLine.objects.create(
order=order,
product=product,
quantity=item.quantity,
price=price.price,
base_total=base_total,
tax_value=tax_value,
taxes=taxes,
total=total,
)
order.calculate_total_from_lines()
add_shipping_order_line(order, shipping_method)
return order
def add_shipping_order_line(order, shipping_method):
shipping_price: ProductPrice = shipping_method.shipping_product.price
shipping_base_total = shipping_price.price
shipping_tax_value = shipping_price.tax.value
shipping_taxes = shipping_base_total * (shipping_tax_value / Decimal(100))
shipping_total = shipping_base_total + shipping_taxes
OrderLine.objects.create(
order=order,
product=shipping_method.shipping_product,
quantity=1,
price=shipping_price.price,
base_total=shipping_base_total,
tax_value=shipping_tax_value,
taxes=shipping_taxes,
total=shipping_total,
)
order.calculate_total_from_lines()
def compute_signature(salt, payload, key):
"""
:param salt: order number (Ds_Order or Ds_Merchant_Order)
:param payload: Ds_MerchantParameters
:param key: shared secret (aka key) from the Redsys Administration Module
:return:
"""
b64_key = base64.b64decode(key)
des3 = pyDes.triple_des(
b64_key, mode=pyDes.CBC, IV="\0" * 8, pad="\0", padmode=pyDes.PAD_NORMAL
)
pepper = des3.encrypt(str(salt))
payload_hash = hmac.new(pepper, payload.encode(), hashlib.sha256).digest()
return base64.b64encode(payload_hash)
def validate_payment_for_order(request, order: Order) -> Decimal:
"""
example_response_data = {
'Ds_MerchantCode': '999008881',
'Ds_Terminal': '001',
'Ds_Order': 'a082bd1cda0b488786f23a62e89107c0',
'Ds_Amount': '13532',
'Ds_Currency': '978',
'Ds_Date': '20/03/2024',
'Ds_Hour': '12:21',
'Ds_SecurePayment': '1',
'Ds_Card_Number': '454881******1156',
'Ds_Card_Country': '724',
'Ds_Response': '0000',
'Ds_MerchantData': '',
'Ds_TransactionType': '0',
'Ds_ConsumerLanguage': '1',
'Ds_AuthorisationCode': '182670',
'Ds_Card_Brand': '1',
'Ds_ProcessedPayMethod': '80',
'Ds_ECI': '05',
'Ds_Response_Description': 'OPERACION AUTORIZADA'
}
Raises AssertionError
"""
data = request.POST
merchant_parameters = data.get("Ds_MerchantParameters")
if not merchant_parameters:
raise RedsysValidationException(
_("No se ha recibido ningún valor para Ds_MerchantParameters")
)
merchant_params = decode_b64_string(merchant_parameters)
result = json.loads(merchant_params)
order_code = result.get("Ds_Order")
assert order_code == order.code
status_code = result.get("Ds_Response")
if int(status_code) > 100:
reason = ERROR_CODES.get(status_code, _("Error no tipificado"))
raise RedsysPaymentException(_(f"No se ha realizado el pago. Motivo: {reason}"))
amount = Decimal(result.get("Ds_Amount")) / 100
return amount
def decode_b64_string(value: str):
return base64.b64decode(value).decode()
def decode_b64_dict(value: str):
return json.loads(decode_b64_string(value))
def validate_expiry_date(expiry_date: str):
if len(expiry_date) != 4:
return False
month, year = int(expiry_date[2:4]), int(expiry_date[:2])
if month < 1 or month > 12:
return False
current_year = int(str(timezone.now().year)[2:])
current_month = timezone.now().month
if year < current_year:
return False
if year == current_year and month > current_month:
return False
return True
def pay_order(order: Order, amount_paid: Decimal):
with transaction.atomic() as tx:
payment = Payment.objects.create(
amount=amount_paid,
order=order,
user=order.user,
method=Payment.MethodChoices.REDSYS,
)
if amount_paid >= order.total:
order.status = Order.Statuses.STATUS_PAID
order.save()