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, 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, shipping_method=shipping_method, 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()