Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Commit

Permalink
Merge branch 'master' into fix-same-title-to-master
Browse files Browse the repository at this point in the history
  • Loading branch information
e0d authored Sep 29, 2023
2 parents 8dabe82 + f0e196f commit 5653dd9
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 8 deletions.
30 changes: 22 additions & 8 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,39 @@ jobs:
console.log('Will use tag: ' + tagName);
return tagName;
result-encoding: string

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Set up QEMU
uses: docker/setup-qemu-action@v2

- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

- name: Build and push Dev Docker image
uses: docker/build-push-action@v1
uses: docker/build-push-action@v4
with:
push: true
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
target: dev
repository: edxops/ecommerce-dev
tags: ${{ steps.get-tag-name.outputs.result }},${{ github.sha }}
tags: |
edxops/ecommerce-dev:${{ steps.get-tag-name.outputs.result }}
edxops/ecommerce-dev:${{ github.sha }}
platforms: linux/amd64,linux/arm64

# The current priority is to get the devstack off of Ansible based Images. Once that is done, we can come back to this part to get
# suitable images for smaller prod environments.
# - name: Build and push prod Docker image
# uses: docker/build-push-action@v1
# uses: docker/build-push-action@v4
# with:
# push: true
# username: ${{ secrets.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_PASSWORD }}
# target: prod
# repository: edxops/ecommerce-prod
# tags: ${{ steps.get-tag-name.outputs.result }},${{ github.sha }}
# tags: |
# edxops/ecommerce-prod:${{ steps.get-tag-name.outputs.result }}
# edxops/ecommerce-prod:${{ github.sha }}
# platforms: linux/amd64,linux/arm64
File renamed without changes.
3 changes: 3 additions & 0 deletions docs/additional_features/gate_ecommerce.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ Waffle offers the following feature gates.
* - disable_redundant_payment_check_for_mobile
- Switch
- Enable returning an error for duplicate transaction_id for mobile in-app purchases.
* - mail_mobile_team_for_change_in_course
- Switch
- Alert mobile team for a change in a course having mobile seats, so that they can adjust prices on mobile platforms.
* - enable_stripe_payment_processor
- Flag
- Ignore client side payment processor setting and use Stripe. For background, see `frontend-app-payment 0005-stripe-custom-actions <https://github.com/openedx/frontend-app-payment/blob/master/docs/decisions/0005-stripe-custom-actions.rst>`_.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""
Tests for Django management command to un-enroll refunded android users.
"""
from django.core.management import call_command
from mock import patch
from testfixtures import LogCapture

from ecommerce.tests.testcases import TestCase


class TestUnenrollRefundedAndroidUsersCommand(TestCase):

LOGGER_NAME = 'ecommerce.core.management.commands.unenroll_refunded_android_users'

@patch('requests.get')
def test_handle_pass(self, mock_response):
""" Test using mock response from setup, using threshold it will clear"""

mock_response.return_value.status_code = 200

with LogCapture(self.LOGGER_NAME) as log:
call_command('unenroll_refunded_android_users')

log.check(
(
self.LOGGER_NAME,
'INFO',
'Sending request to un-enroll refunded android users'
)
)

@patch('requests.get')
def test_handle_fail(self, mock_response):
""" Test using mock response from setup, using threshold it will clear"""

mock_response.return_value.status_code = 400

with LogCapture(self.LOGGER_NAME) as log:
call_command('unenroll_refunded_android_users')

log.check(
(
self.LOGGER_NAME,
'INFO',
'Sending request to un-enroll refunded android users'
),
(
self.LOGGER_NAME,
'ERROR',
'Failed to refund android users with status code 400'
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""
Django management command to un-enroll refunded android users.
Command is run by Jenkins job daily.
"""
import logging

import requests
from django.core.management.base import BaseCommand
from rest_framework import status

from ecommerce.core.models import SiteConfiguration

logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = 'Management command to un-enroll refunded android users.'

def handle(self, *args, **options):
site = SiteConfiguration.objects.first()
refund_api_url = '{}/api/iap/v1/android/refund/'.format(site.build_ecommerce_url())
logger.info("Sending request to un-enroll refunded android users")
response = requests.get(refund_api_url)

if response.status_code != status.HTTP_200_OK:
logger.error("Failed to refund android users with status code %s", response.status_code)
9 changes: 9 additions & 0 deletions ecommerce/extensions/api/constatnts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# .. toggle_name: mail_mobile_team_for_change_in_course
# .. toggle_type: waffle_switch
# .. toggle_default: False
# .. toggle_description: Alert mobile team for a change in a course having mobile seats.
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2023-07-25
# .. toggle_tickets: LEARNER-9377
# .. toggle_status: supported
MAIL_MOBILE_TEAM_FOR_CHANGE_IN_COURSE = 'mail_mobile_team_for_change_in_course'
13 changes: 13 additions & 0 deletions ecommerce/extensions/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
get_enterprise_customer_uuid_from_voucher
)
from ecommerce.entitlements.utils import create_or_update_course_entitlement
from ecommerce.extensions.api.constatnts import MAIL_MOBILE_TEAM_FOR_CHANGE_IN_COURSE
from ecommerce.extensions.api.utils import send_mail_to_mobile_team_for_change_in_course
from ecommerce.extensions.api.v2.constants import (
ENABLE_HOIST_ORDER_HISTORY,
REFUND_ORDER_EMAIL_CLOSING,
Expand Down Expand Up @@ -820,6 +822,13 @@ def validate_products(self, products):

return products

def _get_seats_offered_on_mobile(self, course):
certificate_type_query = Q(attributes__name='certificate_type', attribute_values__value_text='verified')
mobile_query = Q(stockrecords__partner_sku__contains='mobile')
mobile_seats = course.seat_products.filter(certificate_type_query & mobile_query)

return mobile_seats

def get_partner(self):
"""Validate partner"""
if not self.partner:
Expand Down Expand Up @@ -879,6 +888,10 @@ def save(self): # pylint: disable=arguments-differ
published = (resp_message is None)

if published:
mobile_seats = self._get_seats_offered_on_mobile(course)
if waffle.switch_is_active(MAIL_MOBILE_TEAM_FOR_CHANGE_IN_COURSE) and mobile_seats:
send_mail_to_mobile_team_for_change_in_course(course, mobile_seats)

return created, None, None
raise Exception(resp_message)

Expand Down
62 changes: 62 additions & 0 deletions ecommerce/extensions/api/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import mock
from testfixtures import LogCapture

from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.extensions.api.utils import send_mail_to_mobile_team_for_change_in_course
from ecommerce.extensions.iap.models import IAPProcessorConfiguration
from ecommerce.tests.testcases import TestCase


class UtilTests(TestCase):
def setUp(self):
super(UtilTests, self).setUp()
self.course = CourseFactory(id='test/course/123', name='Test Course 123')
seat = self.course.create_or_update_seat('verified', True, 60)
second_seat = self.course.create_or_update_seat('verified', True, 70)
self.mock_mobile_team_mail = '[email protected]'
self.mock_email_body = {
'subject': 'Course Change Alert for Test Course 123',
'body': 'Course: Test Course 123, Sku: {}, Price: 70.00\n'
'Course: Test Course 123, Sku: {}, Price: 60.00'.format(
second_seat.stockrecords.all()[0].partner_sku,
seat.stockrecords.all()[0].partner_sku
)
}

def test_send_mail_to_mobile_team_with_no_email_specified(self):
logger_name = 'ecommerce.extensions.api.utils'
email_sender = 'ecommerce.extensions.communication.utils.Dispatcher.dispatch_direct_messages'
msg_t = "Couldn't mail mobile team for change in {}. No email was specified for mobile team in configurations"
msg = msg_t.format(self.course.name)
with LogCapture(logger_name) as utils_logger,\
mock.patch(email_sender) as mock_send_email:

send_mail_to_mobile_team_for_change_in_course(self.course, self.course.seat_products.all())
utils_logger.check_present(
(
logger_name,
'INFO',
msg
)
)
assert mock_send_email.call_count == 0

def test_send_mail_to_mobile_team(self):
logger_name = 'ecommerce.extensions.api.utils'
email_sender = 'ecommerce.extensions.communication.utils.Dispatcher.dispatch_direct_messages'
iap_configs = IAPProcessorConfiguration.get_solo()
iap_configs.mobile_team_email = self.mock_mobile_team_mail
iap_configs.save()
with LogCapture(logger_name) as utils_logger,\
mock.patch(email_sender) as mock_send_email:

send_mail_to_mobile_team_for_change_in_course(self.course, self.course.seat_products.all())
utils_logger.check_present(
(
logger_name,
'INFO',
"Sent change in {} email to mobile team.".format(self.course.name)
)
)
assert mock_send_email.call_count == 1
mock_send_email.assert_called_with(self.mock_mobile_team_mail, self.mock_email_body)
36 changes: 36 additions & 0 deletions ecommerce/extensions/api/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import logging

from oscar.core.loading import get_class

from ecommerce.extensions.iap.models import IAPProcessorConfiguration

Dispatcher = get_class('communication.utils', 'Dispatcher')
logger = logging.getLogger(__name__)


def send_mail_to_mobile_team_for_change_in_course(course, seats):
recipient = IAPProcessorConfiguration.get_solo().mobile_team_email
if not recipient:
msg = "Couldn't mail mobile team for change in %s. No email was specified for mobile team in configurations"
logger.info(msg, course.name)
return

def format_seat(seat):
seat_template = "Course: {}, Sku: {}, Price: {}"
stock_record = seat.stockrecords.all()[0]
result = seat_template.format(
course.name,
stock_record.partner_sku,
stock_record.price_excl_tax,
)
return result

formatted_seats = [format_seat(seat) for seat in seats if seat.stockrecords.all()]

messages = {
'subject': 'Course Change Alert for {}'.format(course.name),
'body': "\n".join(formatted_seats)
}

Dispatcher().dispatch_direct_messages(recipient, messages)
logger.info("Sent change in %s email to mobile team.", course.name)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.19 on 2023-08-02 08:00

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('iap', '0005_paymentprocessorresponseextension_meta_data'),
]

operations = [
migrations.AddField(
model_name='iapprocessorconfiguration',
name='mobile_team_email',
field=models.EmailField(default='', max_length=254, verbose_name='mobile team email'),
),
]
6 changes: 6 additions & 0 deletions ecommerce/extensions/iap/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ class IAPProcessorConfiguration(SingletonModel):
)
)

mobile_team_email = models.EmailField(
default='',
verbose_name=_('mobile team email'),
max_length=254
)

class Meta:
verbose_name = "IAP Processor Configuration"

Expand Down

0 comments on commit 5653dd9

Please sign in to comment.