Skip to content

Commit

Permalink
Payment Gateway: Add tax support (#172)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkachel authored Nov 12, 2024
1 parent 449a58c commit de18dd0
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 13 deletions.
43 changes: 43 additions & 0 deletions src/payment_gateway/changelog.d/20241107_212223_jkachel_add_tax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Removed
- A bullet item for the Removed category.
-->

### Added

- Adds support for tax collection.
- Bumps CyberSource REST Client package to at least 0.0.54.
- Adds a helper for quantizing decimals for currency amounts.

<!--
### Changed
- A bullet item for the Changed category.
-->
<!--
### Deprecated
- A bullet item for the Deprecated category.
-->
<!--
### Fixed
- A bullet item for the Fixed category.
-->
<!--
### Security
- A bullet item for the Security category.
-->
19 changes: 15 additions & 4 deletions src/payment_gateway/mitol/payment_gateway/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
InvalidTransactionException,
RefundDuplicateException,
)
from mitol.payment_gateway.payment_utils import clean_request_data, strip_nones
from mitol.payment_gateway.payment_utils import (
clean_request_data,
quantize_decimal,
strip_nones,
)


@dataclass
Expand Down Expand Up @@ -362,6 +366,10 @@ class CyberSourcePaymentGateway(
def _generate_line_items(self, cart):
"""
Generates CyberSource-formatted line items based on what's in the cart.
The unit price being stored should be the unit price after any discounts
have been applied. The tax amount should be the _total_ for the line.
Args:
cart: List of CartItems
Expand All @@ -370,9 +378,11 @@ def _generate_line_items(self, cart):
""" # noqa: D401
lines = {}
cart_total = 0
tax_total = 0

for i, line in enumerate(cart):
cart_total += line.quantity * line.unitprice
tax_total += line.taxable

lines[f"item_{i}_code"] = str(line.code)
lines[f"item_{i}_name"] = str(line.name)[:254]
Expand All @@ -381,7 +391,7 @@ def _generate_line_items(self, cart):
lines[f"item_{i}_tax_amount"] = str(line.taxable)
lines[f"item_{i}_unit_price"] = str(line.unitprice)

return (lines, cart_total)
return (lines, cart_total, tax_total)

def _generate_cybersource_sa_signature(self, payload):
"""
Expand Down Expand Up @@ -438,7 +448,7 @@ def prepare_checkout(
stored anywhere.
""" # noqa: D401

(line_items, total) = self._generate_line_items(order.items)
(line_items, total, tax_total) = self._generate_line_items(order.items)

formatted_merchant_fields = {}

Expand All @@ -455,7 +465,8 @@ def prepare_checkout(

payload = {
"access_key": settings.MITOL_PAYMENT_GATEWAY_CYBERSOURCE_ACCESS_KEY,
"amount": str(total),
"amount": str(quantize_decimal(total + tax_total)),
"tax_amount": str(quantize_decimal(tax_total)),
"consumer_id": consumer_id,
"currency": "USD",
"locale": "en-us",
Expand Down
7 changes: 7 additions & 0 deletions src/payment_gateway/mitol/payment_gateway/payment_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Utilities for the Payment Gateway"""

from decimal import Decimal


# To delete None values in Input Request Json body
def clean_request_data(request_data):
Expand All @@ -21,3 +23,8 @@ def strip_nones(datasource):
retval[key] = datasource[key]

return retval


def quantize_decimal(value, precision=2):
"""Quantize a decimal value to the specified precision"""
return Decimal(value).quantize(Decimal("0.{}".format("0" * precision)))
2 changes: 1 addition & 1 deletion src/payment_gateway/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "mitol-django-payment-gateway"
version = "2023.12.19"
description = "Django application to handle payment processing"
dependencies = [
"cybersource-rest-client-python>=0.0.36",
"cybersource-rest-client-python>=0.0.59",
"django-stubs>=1.13.1",
"django>=3.0",
"mitol-django-common"
Expand Down
21 changes: 18 additions & 3 deletions tests/mitol/payment_gateway/api/test_cybersource.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from collections import namedtuple
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from typing import Dict

import pytest
Expand Down Expand Up @@ -81,10 +82,11 @@ def generate_test_cybersource_payload(order, cartitems, transaction_uuid):
cancel_url = "https://duckduckgo.com"

test_line_items = {}
test_total = 0
test_total = tax_total = 0

for idx, line in enumerate(cartitems):
test_total += line.quantity * line.unitprice
tax_total += line.taxable

test_line_items[f"item_{idx}_code"] = str(line.code)
test_line_items[f"item_{idx}_name"] = str(line.name)[:254]
Expand All @@ -97,7 +99,8 @@ def generate_test_cybersource_payload(order, cartitems, transaction_uuid):

test_payload = {
"access_key": settings.MITOL_PAYMENT_GATEWAY_CYBERSOURCE_ACCESS_KEY,
"amount": str(test_total),
"amount": str(Decimal(test_total + tax_total).quantize(Decimal("0.01"))),
"tax_amount": str(Decimal(tax_total).quantize(Decimal("0.01"))),
"consumer_id": consumer_id,
"currency": "USD",
"locale": "en-us",
Expand Down Expand Up @@ -140,7 +143,14 @@ def test_invalid_payload_generation(order, cartitems):
assert isinstance(checkout_data, TypeError)


def test_cybersource_payload_generation(order, cartitems):
@pytest.mark.parametrize(
("with_tax"),
[
(True),
(False),
],
)
def test_cybersource_payload_generation(order, cartitems, with_tax):
"""
Starts a payment through the payment gateway, and then checks to make sure
there's stuff in the payload that it generates. The transaction is not sent
Expand All @@ -151,6 +161,11 @@ def test_cybersource_payload_generation(order, cartitems):
cancel_url = "https://duckduckgo.com"
order.items = cartitems

# By default, the cart items will have tax.
if not with_tax:
for idx in range(len(order.items)):
order.items[idx].taxable = 0

checkout_data = PaymentGateway.start_payment(
MITOL_PAYMENT_GATEWAY_CYBERSOURCE,
order,
Expand Down
22 changes: 21 additions & 1 deletion tests/mitol/payment_gateway/utils/test_payment_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
"""Tests for payment_gateway application utils""" # noqa: INP001

from decimal import Decimal

import pytest
from mitol.payment_gateway.payment_utils import clean_request_data, strip_nones
from mitol.payment_gateway.payment_utils import (
clean_request_data,
quantize_decimal,
strip_nones,
)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -44,3 +50,17 @@ def test_strip_nones():
test_ds2 = strip_nones(ds2)

assert test_ds2 == ds2


def test_quantize_decimal():
"""
Tests quantize_decimal to make sure that the decimal is quantized to
the correct precision.
"""

test_decimal = 1.23456789
test_precision = 2

quantized_decimal = quantize_decimal(test_decimal, test_precision)

assert quantized_decimal == Decimal("1.23")
9 changes: 7 additions & 2 deletions tests/testapp/factories.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Test factories"""

import string
from decimal import Decimal

import faker
from factory import Factory, SubFactory, fuzzy
from factory import Factory, LazyAttribute, SubFactory, fuzzy
from factory.django import DjangoModelFactory
from mitol.common.factories import UserFactory
from mitol.digitalcredentials.factories import (
Expand Down Expand Up @@ -46,8 +47,12 @@ class Meta:
code = fuzzy.FuzzyText(length=6)
quantity = fuzzy.FuzzyInteger(1, 5, 1)
name = FAKE.sentence(nb_words=3)
taxable = 0
unitprice = fuzzy.FuzzyDecimal(1, 300, precision=2)
taxable = LazyAttribute(
lambda o: Decimal(o.unitprice * Decimal(FAKE.random_number(2) * 0.01)).quantize(
Decimal("0.01")
)
)


class OrderFactory(Factory):
Expand Down
4 changes: 2 additions & 2 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit de18dd0

Please sign in to comment.