From 8bb3732ac3d1136d7430b889ab4ae9cc4f1a3928 Mon Sep 17 00:00:00 2001 From: cp-at-mit Date: Tue, 8 Oct 2024 08:27:55 -0400 Subject: [PATCH] 5511 add apis and view for order history and receipts (#156) * initial hold, api view url * Fix API * hold * add ol django mail * email task, template, api * hold * Fix bug * merge * Emails being sent * Extend base email * Works with mailjet * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ruff * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ruff * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update lock file * Update open api * Remove print * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * PR comments * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add back param * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ruff * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix * fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * revert * try * fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- app.json | 8 + cart/templates/checkout_interstitial.html | 22 +- fixtures/common.py | 9 + openapi.yaml | 219 ++++++++++++++++++ payments/api.py | 15 +- payments/hooks/post_sale.py | 13 +- payments/mail_api.py | 31 +++ payments/messages.py | 6 + payments/models.py | 4 + payments/serializers/v0/__init__.py | 20 ++ payments/tasks.py | 16 +- .../templates/mail/order_payment/body.html | 9 + .../templates/mail/order_payment/subject.txt | 1 + payments/views/v0/__init__.py | 20 +- payments/views/v0/urls.py | 3 + poetry.lock | 84 ++++++- pyproject.toml | 1 + unified_ecommerce/settings.py | 31 ++- 18 files changed, 476 insertions(+), 36 deletions(-) create mode 100644 payments/mail_api.py create mode 100644 payments/messages.py create mode 100644 payments/templates/mail/order_payment/body.html create mode 100644 payments/templates/mail/order_payment/subject.txt diff --git a/app.json b/app.json index 26c27d77..563e70fe 100644 --- a/app.json +++ b/app.json @@ -26,6 +26,10 @@ "description": "", "required": false }, + "SITE_NAME": { + "description": "Name of the site.", + "required": false + }, "AWS_ACCESS_KEY_ID": { "description": "AWS Access Key for S3 storage.", "required": false @@ -357,6 +361,10 @@ "description": "Shared secret for JWT auth tokens", "required": true }, + "MITOL_MAIL_REPLY_TO_ADDRESS": { + "description": "E-mail to use for reply-to address of emails", + "required": false + }, "MITOPEN_SECURE_SSL_REDIRECT": { "description": "Application-level SSL redirect setting.", "value": "True" diff --git a/cart/templates/checkout_interstitial.html b/cart/templates/checkout_interstitial.html index 21d51deb..797288eb 100644 --- a/cart/templates/checkout_interstitial.html +++ b/cart/templates/checkout_interstitial.html @@ -14,18 +14,18 @@

Redirecting to the payment processor...

{% if debug_mode %} - {% for key, value in form.items %} -
-
- {% endfor %} - -
- -
+ {% for key, value in form.items %} +
+
+ {% endfor %} + +
+ +
{% else %} - {% for key, value in form.items %} - - {% endfor %} + {% for key, value in form.items %} + + {% endfor %} {% endif %}
diff --git a/fixtures/common.py b/fixtures/common.py index 1dadaaeb..fd4a4572 100644 --- a/fixtures/common.py +++ b/fixtures/common.py @@ -9,6 +9,7 @@ import pytest import responses from pytest_mock import PytestMockWarning +from rest_framework.test import APIClient from urllib3.exceptions import InsecureRequestWarning @@ -83,3 +84,11 @@ def mocked_responses(): """Mock responses fixture""" with responses.RequestsMock() as rsps: yield rsps + + +@pytest.fixture() +def admin_drf_client(admin_user): + """DRF API test client with admin user""" + client = APIClient() + client.force_authenticate(user=admin_user) + return client diff --git a/openapi.yaml b/openapi.yaml index 8633aed6..4da622c8 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -239,6 +239,53 @@ paths: responses: '200': description: No response body + /api/v0/payments/orders/history/: + get: + operationId: api_v0_payments_orders_history_list + parameters: + - name: limit + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - name: offset + required: false + in: query + description: The initial index from which to return the results. + schema: + type: integer + tags: + - api + security: + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedOrderHistoryList' + description: '' + /api/v0/payments/orders/history/{id}/: + get: + operationId: api_v0_payments_orders_history_retrieve + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - api + security: + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OrderHistory' + description: '' /integrated_system/: get: operationId: integrated_system_list @@ -615,6 +662,137 @@ components: required: - id - name + Line: + type: object + description: Serializes a line item for an order. + properties: + id: + type: integer + readOnly: true + quantity: + type: integer + maximum: 2147483647 + minimum: 0 + item_description: + type: string + readOnly: true + unit_price: + type: string + format: decimal + pattern: ^-?\d{0,7}(?:\.\d{0,2})?$ + total_price: + type: string + format: decimal + pattern: ^-?\d{0,7}(?:\.\d{0,2})?$ + product: + $ref: '#/components/schemas/Product' + required: + - id + - item_description + - product + - quantity + - total_price + - unit_price + Nested: + type: object + properties: + id: + type: integer + readOnly: true + password: + type: string + maxLength: 128 + last_login: + type: string + format: date-time + nullable: true + is_superuser: + type: boolean + title: Superuser status + description: Designates that this user has all permissions without explicitly + assigning them. + username: + type: string + description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ + only. + pattern: ^[\w.@+-]+$ + maxLength: 150 + first_name: + type: string + maxLength: 150 + last_name: + type: string + maxLength: 150 + email: + type: string + format: email + title: Email address + maxLength: 254 + is_staff: + type: boolean + title: Staff status + description: Designates whether the user can log into this admin site. + is_active: + type: boolean + title: Active + description: Designates whether this user should be treated as active. Unselect + this instead of deleting accounts. + date_joined: + type: string + format: date-time + groups: + type: array + items: + type: integer + description: The groups this user belongs to. A user will get all permissions + granted to each of their groups. + user_permissions: + type: array + items: + type: integer + description: Specific permissions for this user. + required: + - id + - password + - username + OrderHistory: + type: object + properties: + id: + type: integer + readOnly: true + state: + $ref: '#/components/schemas/StateEnum' + reference_number: + type: string + maxLength: 255 + purchaser: + allOf: + - $ref: '#/components/schemas/Nested' + readOnly: true + total_price_paid: + type: string + format: decimal + pattern: ^-?\d{0,15}(?:\.\d{0,5})?$ + lines: + type: array + items: + $ref: '#/components/schemas/Line' + created_on: + type: string + format: date-time + readOnly: true + updated_on: + type: string + format: date-time + readOnly: true + required: + - created_on + - id + - lines + - purchaser + - total_price_paid + - updated_on PaginatedBasketItemList: type: object required: @@ -684,6 +862,29 @@ components: type: array items: $ref: '#/components/schemas/IntegratedSystem' + PaginatedOrderHistoryList: + type: object + required: + - count + - results + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?offset=400&limit=100 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?offset=200&limit=100 + results: + type: array + items: + $ref: '#/components/schemas/OrderHistory' PaginatedProductList: type: object required: @@ -824,6 +1025,24 @@ components: - sku - system - updated_on + StateEnum: + enum: + - pending + - fulfilled + - canceled + - refunded + - declined + - errored + - review + type: string + description: |- + * `pending` - Pending + * `fulfilled` - Fulfilled + * `canceled` - Canceled + * `refunded` - Refunded + * `declined` - Declined + * `errored` - Errored + * `review` - Review securitySchemes: cookieAuth: type: apiKey diff --git a/payments/api.py b/payments/api.py index bef2f2b0..d32c5d1b 100644 --- a/payments/api.py +++ b/payments/api.py @@ -105,11 +105,13 @@ def generate_checkout_payload(request): def fulfill_completed_order( - order, payment_data, basket=None, source=POST_SALE_SOURCE_BACKOFFICE + order, + payment_data, + basket=None, + source=POST_SALE_SOURCE_BACKOFFICE, # noqa: ARG001 ): """Fulfill the order.""" - order.fulfill(payment_data, source) - order.save() + order.fulfill(payment_data) if basket and basket.compare_to_order(order): basket.delete() @@ -179,14 +181,12 @@ def process_cybersource_payment_response( msg = f"Transaction declined: {processor_response.message}" log.debug(msg) order.decline() - order.save() return_message = order.state elif processor_response.state == ProcessorResponse.STATE_ERROR: # Error - something went wrong with the request msg = f"Error happened submitting the transaction: {processor_response.message}" log.debug(msg) - order.error() - order.save() + order.errored() return_message = order.state elif processor_response.state in [ ProcessorResponse.STATE_CANCELLED, @@ -200,7 +200,6 @@ def process_cybersource_payment_response( msg = f"Transaction cancelled/reviewed: {processor_response.message}" log.debug(msg) order.cancel() - order.save() return_message = order.state elif ( @@ -230,7 +229,6 @@ def process_cybersource_payment_response( ) log.error(msg) order.cancel() - order.save() return_message = order.state return return_message @@ -373,7 +371,6 @@ def check_and_process_pending_orders_for_resolution(refnos=None): ).get() order.fulfill(payload) - order.save() fulfilled_count += 1 msg = f"Fulfilled order {order.reference_number}." diff --git a/payments/hooks/post_sale.py b/payments/hooks/post_sale.py index b2678bb6..7093100a 100644 --- a/payments/hooks/post_sale.py +++ b/payments/hooks/post_sale.py @@ -1,7 +1,5 @@ """Post-sale hook implementations for payments.""" -import logging - import pluggy hookimpl = pluggy.HookimplMarker("unified_ecommerce") @@ -11,12 +9,15 @@ class PostSaleSendEmails: """Send email when the order is fulfilled.""" @hookimpl - def post_sale(self, order_id, source): + def post_sale(self, order_id, source): # noqa: ARG002 """Send email when the order is fulfilled.""" - log = logging.getLogger(__name__) + from payments.tasks import successful_order_payment_email_task - msg = "Sending email for order %s with source %s" - log.info(msg, order_id, source) + successful_order_payment_email_task.delay( + order_id, + "Successful Order Payment", + "Your payment has been successfully processed.", + ) class IntegratedSystemWebhooks: diff --git a/payments/mail_api.py b/payments/mail_api.py new file mode 100644 index 00000000..88e52122 --- /dev/null +++ b/payments/mail_api.py @@ -0,0 +1,31 @@ +import logging +from email.utils import formataddr + +from django.contrib.auth import get_user_model +from mitol.mail.api import get_message_sender + +from payments.messages import SuccessfulOrderPaymentMessage + +log = logging.getLogger(__name__) +User = get_user_model() + + +def send_successful_order_payment_email(order, email_subject, email_body): + try: + with get_message_sender(SuccessfulOrderPaymentMessage) as sender: + sender.build_and_send_message( + order.purchaser.email, + { + "subject": email_subject, + "first_name": order.purchaser.first_name, + "message": email_body, + }, + ) + log.info("Sent successful order payment email to %s", order.purchaser.email) + except: # noqa: E722 + log.exception("Error sending successful order payment email") + + +def format_recipient(user: User) -> str: + """Format the user as a recipient""" + return formataddr((user.name, user.email)) diff --git a/payments/messages.py b/payments/messages.py new file mode 100644 index 00000000..1f4999c6 --- /dev/null +++ b/payments/messages.py @@ -0,0 +1,6 @@ +from mitol.mail.messages import TemplatedMessage + + +class SuccessfulOrderPaymentMessage(TemplatedMessage): + template_name = "mail/order_payment" + name = "Successful Order Payment" diff --git a/payments/models.py b/payments/models.py index 538fa26e..919825cb 100644 --- a/payments/models.py +++ b/payments/models.py @@ -181,12 +181,16 @@ def fulfill(self, payment_data, source=POST_SALE_SOURCE_REDIRECT): # trigger post-sale events self.handle_post_sale(source=source) + self.state = Order.STATE.FULFILLED + self.save() + # send the receipt emails self.send_ecommerce_order_receipt() except Exception as e: # pylint: disable=broad-except log.exception( "Error occurred fulfilling order %s", self.reference_number, exc_info=e ) + self.errored() def cancel(self): diff --git a/payments/serializers/v0/__init__.py b/payments/serializers/v0/__init__.py index 82ebbb04..ad084f2b 100644 --- a/payments/serializers/v0/__init__.py +++ b/payments/serializers/v0/__init__.py @@ -69,6 +69,7 @@ def perform_create(self, validated_data): Returns: BasketItem: The created BasketItem instance. """ + basket = Basket.objects.get(user=validated_data["user"]) # Product queryset returns active Products by default product = Product.objects.get(id=validated_data["product"]) @@ -217,3 +218,22 @@ class Meta: """Meta options for WebhookBaseSerializer""" dataclass = WebhookBase + model = Line + + +class OrderHistorySerializer(serializers.ModelSerializer): + lines = LineSerializer(many=True) + + class Meta: + fields = [ + "id", + "state", + "reference_number", + "purchaser", + "total_price_paid", + "lines", + "created_on", + "updated_on", + ] + model = Order + depth = 1 diff --git a/payments/tasks.py b/payments/tasks.py index a282dad2..7ffaaa8c 100644 --- a/payments/tasks.py +++ b/payments/tasks.py @@ -1,19 +1,28 @@ -"""Tasks for the payments app.""" - import logging import requests from django.conf import settings from payments.constants import PAYMENT_HOOK_ACTION_POST_SALE -from payments.models import Order +from payments.mail_api import send_successful_order_payment_email from payments.serializers.v0 import WebhookBase, WebhookBaseSerializer, WebhookOrder from system_meta.models import IntegratedSystem from unified_ecommerce.celery import app +"""Tasks for the payments app.""" + + log = logging.getLogger(__name__) +@app.task +def successful_order_payment_email_task(order_id, email_subject, email_body): + from payments.models import Order + + order = Order.objects.get(id=order_id) + send_successful_order_payment_email(order, email_subject, email_body) + + @app.task() def send_post_sale_webhook(system_id, order_id, source, attempt_count=0): """ @@ -21,6 +30,7 @@ def send_post_sale_webhook(system_id, order_id, source, attempt_count=0): This is split out so we can queue the webhook requests individually. """ + from payments.models import Order order = Order.objects.get(pk=order_id) system = IntegratedSystem.objects.get(pk=system_id) diff --git a/payments/templates/mail/order_payment/body.html b/payments/templates/mail/order_payment/body.html new file mode 100644 index 00000000..c0844272 --- /dev/null +++ b/payments/templates/mail/order_payment/body.html @@ -0,0 +1,9 @@ +{% extends "mail/email_base.html" %} +{% block content %} +
+
+

Dear {{ first_name }},

+

{{ message }}

+

Thank you

+
+{% endblock %} diff --git a/payments/templates/mail/order_payment/subject.txt b/payments/templates/mail/order_payment/subject.txt new file mode 100644 index 00000000..ac892a39 --- /dev/null +++ b/payments/templates/mail/order_payment/subject.txt @@ -0,0 +1 @@ +{{subject}} diff --git a/payments/views/v0/__init__.py b/payments/views/v0/__init__.py index 40524bba..96d36718 100644 --- a/payments/views/v0/__init__.py +++ b/payments/views/v0/__init__.py @@ -16,13 +16,18 @@ from rest_framework.views import APIView from rest_framework.viewsets import ( GenericViewSet, + ReadOnlyModelViewSet, ViewSet, ) from rest_framework_extensions.mixins import NestedViewSetMixin from payments import api from payments.models import Basket, BasketItem, Order -from payments.serializers.v0 import BasketItemSerializer, BasketSerializer +from payments.serializers.v0 import ( + BasketItemSerializer, + BasketSerializer, + OrderHistorySerializer, +) from system_meta.models import IntegratedSystem, Product from unified_ecommerce.constants import POST_SALE_SOURCE_BACKOFFICE @@ -232,3 +237,16 @@ def post(self, request): ) return Response(status=status.HTTP_200_OK) + + +class OrderHistoryViewSet(ReadOnlyModelViewSet): + serializer_class = OrderHistorySerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return ( + Order.objects.filter(purchaser=self.request.user) + .filter(state__in=[Order.STATE.FULFILLED, Order.STATE.REFUNDED]) + .order_by("-created_on") + .all() + ) diff --git a/payments/views/v0/urls.py b/payments/views/v0/urls.py index b7acd2c3..0929c960 100644 --- a/payments/views/v0/urls.py +++ b/payments/views/v0/urls.py @@ -7,6 +7,7 @@ BasketItemViewSet, BasketViewSet, CheckoutApiViewSet, + OrderHistoryViewSet, clear_basket, create_basket_from_product, ) @@ -22,6 +23,8 @@ parents_query_lookups=["basket"], ) +router.register(r"orders/history", OrderHistoryViewSet, basename="orderhistory_api") + router.register(r"checkout", CheckoutApiViewSet, basename="checkout") urlpatterns = [ diff --git a/poetry.lock b/poetry.lock index 06267839..75b35504 100644 --- a/poetry.lock +++ b/poetry.lock @@ -724,6 +724,17 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "cssselect" +version = "1.2.0" +description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cssselect-1.2.0-py2.py3-none-any.whl", hash = "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e"}, + {file = "cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc"}, +] + [[package]] name = "cssselect2" version = "0.7.0" @@ -743,6 +754,24 @@ webencodings = "*" doc = ["sphinx", "sphinx_rtd_theme"] test = ["flake8", "isort", "pytest"] +[[package]] +name = "cssutils" +version = "2.11.1" +description = "A CSS Cascading Style Sheets library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cssutils-2.11.1-py3-none-any.whl", hash = "sha256:a67bfdfdff4f3867fab43698ec4897c1a828eca5973f4073321b3bccaf1199b1"}, + {file = "cssutils-2.11.1.tar.gz", hash = "sha256:0563a76513b6af6eebbe788c3bf3d01c920e46b3f90c8416738c5cfc773ff8e2"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["cssselect", "importlib-resources", "jaraco.test (>=5.1)", "lxml", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + [[package]] name = "curtsies" version = "0.4.2" @@ -2155,6 +2184,26 @@ requests = ">=2.20.0" setuptools = "*" typing-extensions = "*" +[[package]] +name = "mitol-django-mail" +version = "2023.12.19" +description = "MIT Open Learning django app extensions for mail" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mitol-django-mail-2023.12.19.tar.gz", hash = "sha256:824f6ff0a7fb7b996962e608c61e5d02d06c174e466d00896eb43fdac864148f"}, + {file = "mitol_django_mail-2023.12.19-py3-none-any.whl", hash = "sha256:a32853bfe7da39d4c34651d7e5bddc547678c1b5b3a54e56e492879276eab371"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.6.0" +django = ">=3.0" +django-anymail = ">=6.0" +html5lib = ">=1.1" +mitol-django-common = "*" +premailer = ">=3.7.0" +toolz = ">=0.10.0" + [[package]] name = "mitol-django-payment-gateway" version = "2023.12.19" @@ -2172,6 +2221,17 @@ django = ">=3.0" mitol-django-common = "*" setuptools = "*" +[[package]] +name = "more-itertools" +version = "10.5.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, + {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, +] + [[package]] name = "moto" version = "4.2.14" @@ -2537,6 +2597,28 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "premailer" +version = "3.10.0" +description = "Turns CSS blocks into style attributes" +optional = false +python-versions = "*" +files = [ + {file = "premailer-3.10.0-py2.py3-none-any.whl", hash = "sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a"}, + {file = "premailer-3.10.0.tar.gz", hash = "sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2"}, +] + +[package.dependencies] +cachetools = "*" +cssselect = "*" +cssutils = "*" +lxml = "*" +requests = "*" + +[package.extras] +dev = ["black", "flake8", "therapist", "tox", "twine", "wheel"] +test = ["mock", "nose"] + [[package]] name = "prettytable" version = "3.11.0" @@ -4115,4 +4197,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11.0" -content-hash = "084508218f325c3596e886c2d426b3df8f6652b7c03c2326d7f3bc793a27d720" +content-hash = "0d63e792c8442e009630abd38f7d284864c303cf0d5e9335f41a8b44670fbb2c" diff --git a/pyproject.toml b/pyproject.toml index 46426d4e..b8cd1a00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ python-slugify = "^8.0.1" django-oauth-toolkit = "^2.3.0" requests-oauthlib = "^1.3.1" oauthlib = "^3.2.2" +mitol-django-mail = "^2023.12.19" djangorestframework-dataclasses = "^1.3.1" [tool.poetry.group.dev.dependencies] diff --git a/unified_ecommerce/settings.py b/unified_ecommerce/settings.py index c2aab537..071a5e6a 100644 --- a/unified_ecommerce/settings.py +++ b/unified_ecommerce/settings.py @@ -15,6 +15,7 @@ import logging import os import platform +from pathlib import Path from urllib.parse import urljoin import dj_database_url @@ -51,7 +52,7 @@ SECRET_KEY = get_string("SECRET_KEY", "terribly_unsafe_default_secret_key") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = get_bool("DEBUG", False) # noqa: FBT003 +DEBUG = get_bool(name="DEBUG", default=False) ALLOWED_HOSTS = ["*"] @@ -89,6 +90,7 @@ "safedelete", "reversion", "oauth2_provider", + "mitol.mail.apps.MailApp", # Application modules "unified_ecommerce", "system_meta", @@ -133,7 +135,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], + "DIRS": [Path(BASE_DIR) / "templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -147,6 +149,7 @@ }, }, ] +MITOL_MAIL_MESSAGE_CLASSES = ["payments.messages.SuccessfulOrderPaymentMessage"] WSGI_APPLICATION = "unified_ecommerce.wsgi.application" @@ -218,7 +221,7 @@ # Configure e-mail settings EMAIL_BACKEND = get_string( - "MITOL_UE_EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend" + "MITOL_UE_EMAIL_BACKEND", "anymail.backends.mailgun.EmailBackend" ) EMAIL_HOST = get_string("MITOL_UE_EMAIL_HOST", "localhost") EMAIL_PORT = get_int("MITOL_UE_EMAIL_PORT", 25) @@ -234,11 +237,19 @@ MAILGUN_FROM_EMAIL = get_string("MITOL_UE_FROM_EMAIL", "no-reply@example.com") MAILGUN_BCC_TO_EMAIL = get_string("MITOL_UE_BCC_EMAIL", None) +# mitol-django-mail +MITOL_MAIL_FROM_EMAIL = MAILGUN_FROM_EMAIL +MITOL_MAIL_RECIPIENT_OVERRIDE = MAILGUN_RECIPIENT_OVERRIDE +MITOL_MAIL_FORMAT_RECIPIENT_FUNC = "payments.mail_api.format_recipient" +MITOL_MAIL_ENABLE_EMAIL_DEBUGGER = get_bool( # NOTE: this will override the legacy mail debugger defined in this project # noqa: E501 + name="MITOL_MAIL_ENABLE_EMAIL_DEBUGGER", + default=DEBUG, +) + ANYMAIL = { "MAILGUN_API_KEY": MAILGUN_KEY, "MAILGUN_SENDER_DOMAIN": MAILGUN_SENDER_DOMAIN, } - # e-mail configurable admins ADMIN_EMAIL = get_string("MITOL_UE_ADMIN_EMAIL", "") ADMINS = (("Admins", ADMIN_EMAIL),) if ADMIN_EMAIL != "" else () @@ -384,6 +395,16 @@ 60 * 60 * 24 * 7, # 7 days ) +MITOL_MAIL_REPLY_TO_ADDRESS = get_string( + name="MITOL_MAIL_REPLY_TO_ADDRESS", + default="webmaster@localhost.com", +) + +SITE_NAME = get_string( + name="SITE_NAME", + default="Unified Ecommerce", +) + JWT_AUTH = { "JWT_SECRET_KEY": MITOL_UE_JWT_SECRET, "JWT_VERIFY": True, @@ -445,7 +466,7 @@ "MITOL_UE_REFERENCE_NUMBER_PREFIX", "mitol-" ) MITOL_UE_PAYMENT_INTERSTITIAL_DEBUG = get_bool( - "MITOL_UE_PAYMENT_INTERSTITIAL_DEBUG", DEBUG + name="MITOL_UE_PAYMENT_INTERSTITIAL_DEBUG", default=DEBUG ) MITOL_UE_WEBHOOK_RETRY_COOLDOWN = get_int("MITOL_UE_WEBHOOK_RETRY_COOLDOWN", 60) MITOL_UE_WEBHOOK_RETRY_MAX = get_int("MITOL_UE_WEBHOOK_RETRY_MAX", 4)