Skip to content

Commit

Permalink
feat: Add discount payment types (#1390)
Browse files Browse the repository at this point in the history
* feat: Add payment_type field for discounts

* backfill migration

* replace for_flexible_pricing & update choices

* fmt

* lint & fmt

* feat: Add staff as a payment type

* feat(generate_discount_code): Add support for payment types

* fmt

* isort

* remove usage of for_flexible_pricing from generate_discount codes

* feat(generate_legacy_enrollment_codes): sets payment_type to customer-support for legacy discount command

* feat: update config_tiers command to set payment_type

* isort & ran black

* Updated data migration

* feat(backfill_payment_types: Add more backfill)

* remove for_flexible_pricing field in favour of payment_type

* update generate_discount_code docs

* add new payment_type `legacy`
  • Loading branch information
asadali145 authored Feb 14, 2023
1 parent 2938128 commit 82d222e
Show file tree
Hide file tree
Showing 25 changed files with 288 additions and 83 deletions.
12 changes: 7 additions & 5 deletions courses/management/commands/create_verified_enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
from courses.api import create_run_enrollments
from courses.models import CourseRun, PaidCourseRun
from ecommerce.api import fulfill_completed_order
from ecommerce.constants import ZERO_PAYMENT_DATA
from ecommerce.constants import PAYMENT_TYPE_FINANCIAL_ASSISTANCE, ZERO_PAYMENT_DATA
from ecommerce.discounts import DiscountType
from ecommerce.models import PendingOrder, Product, Discount
from ecommerce.models import Discount, PendingOrder, Product
from openedx.constants import EDX_ENROLLMENT_VERIFIED_MODE
from users.api import fetch_user

Expand Down Expand Up @@ -101,9 +101,11 @@ def handle(self, *args, **options):
)
)

discount = Discount.objects.filter(
for_flexible_pricing=False, discount_code=options["code"]
).first()
discount = (
Discount.objects.filter(discount_code=options["code"])
.exclude(payment_type=PAYMENT_TYPE_FINANCIAL_ASSISTANCE)
.first()
)
if not discount:
raise CommandError(
"That enrollment code {} does not exist".format(options["code"])
Expand Down
11 changes: 9 additions & 2 deletions ecommerce/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,15 @@ class BasketItemAdmin(VersionAdmin):
class DiscountAdmin(admin.ModelAdmin):
model = Discount
search_fields = ["discount_type", "redemption_type", "discount_code"]
list_display = ["id", "discount_code", "discount_type", "amount", "redemption_type"]
list_filter = ["discount_type", "redemption_type", "for_flexible_pricing"]
list_display = [
"id",
"discount_code",
"discount_type",
"amount",
"redemption_type",
"payment_type",
]
list_filter = ["discount_type", "redemption_type", "payment_type"]


@admin.register(DiscountProduct)
Expand Down
9 changes: 7 additions & 2 deletions ecommerce/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

from courses.api import deactivate_run_enrollment
from courses.constants import ENROLL_CHANGE_STATUS_REFUNDED
from ecommerce.constants import REFUND_SUCCESS_STATES, ZERO_PAYMENT_DATA
from ecommerce.constants import (
PAYMENT_TYPE_FINANCIAL_ASSISTANCE,
REFUND_SUCCESS_STATES,
ZERO_PAYMENT_DATA,
)
from ecommerce.models import (
Basket,
BasketDiscount,
Expand Down Expand Up @@ -189,7 +193,8 @@ def apply_user_discounts(request):
discount = None

BasketDiscount.objects.filter(
redeemed_basket=basket, redeemed_discount__for_flexible_pricing=True
redeemed_basket=basket,
redeemed_discount__payment_type=PAYMENT_TYPE_FINANCIAL_ASSISTANCE,
).delete()
if BasketDiscount.objects.filter(redeemed_basket=basket).count() > 0:
return
Expand Down
18 changes: 18 additions & 0 deletions ecommerce/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@

REDEMPTION_TYPES = list(zip(ALL_REDEMPTION_TYPES, ALL_REDEMPTION_TYPES))

PAYMENT_TYPE_MARKETING = "marketing"
PAYMENT_TYPE_SALES = "sales"
PAYMENT_TYPE_FINANCIAL_ASSISTANCE = "financial-assistance"
PAYMENT_TYPE_CUSTOMER_SUPPORT = "customer-support"
PAYMENT_TYPE_STAFF = "staff"
PAYMENT_TYPE_LEGACY = "legacy"

ALL_PAYMENT_TYPES = [
PAYMENT_TYPE_MARKETING,
PAYMENT_TYPE_SALES,
PAYMENT_TYPE_FINANCIAL_ASSISTANCE,
PAYMENT_TYPE_CUSTOMER_SUPPORT,
PAYMENT_TYPE_STAFF,
PAYMENT_TYPE_LEGACY,
]

PAYMENT_TYPES = list(zip(ALL_PAYMENT_TYPES, ALL_PAYMENT_TYPES))

TRANSACTION_TYPE_REFUND = "refund"
TRANSACTION_TYPE_PAYMENT = "payment"

Expand Down
2 changes: 1 addition & 1 deletion ecommerce/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class DiscountFactory(DjangoModelFactory):
redemption_type = ALL_REDEMPTION_TYPES[
random.randrange(0, len(ALL_REDEMPTION_TYPES), 1)
]
for_flexible_pricing = False
payment_type = None

class Meta:
model = models.Discount
Expand Down
37 changes: 27 additions & 10 deletions ecommerce/management/commands/generate_discount_code.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
"""
Generates discount (sometimes called enrollment) codes with some parameters.
This is meant to be an easy way to make these in bulk - for one-offs, you can
use this or go through Django Admin (or, eventually, the Staff Dashboard).
Generates discount (sometimes called enrollment) codes with some parameters.
This is meant to be an easy way to make these in bulk - for one-offs, you can
use this or go through Django Admin (or, eventually, the Staff Dashboard).
Codes can be created in one of two ways: you can specify the codes themselves on
the command line, or you can specify --count and --prefix and have it generate
the codes.
the codes.
* Codes (literally, the code that the learner will enter) are just listed
on the command line. Any number of these can be specified, but you must
provide at least one if you're explicitly specifying the code. All codes
on the command line. Any number of these can be specified, but you must
provide at least one if you're explicitly specifying the code. All codes
will share the same options (type, amount, expiration date, etc.)
* If you use --count and --prefix, the command will generate the specified
amount of codes using the given prefix and a UUID. Discount code length is
limited to 50 so your prefix must be 13 characters or less. Include any
punctuation that you need in the prefix - the command will not, for
example, add a dash between the prefix and the UUID.
example, add a dash between the prefix and the UUID.
The default is to generate a dollars off discount code in the specified amount,
without an expiration date, and with unlimited redemptions. Use the --expires
option to specify an expiration date, or use --one-time to make the code a
one-time discount. You can also set the discount type with --discount-type. The
type should be one of the normal types (dollars-off, percent-off, or
type should be one of the normal types (dollars-off, percent-off, or
fixed-price). If the type is set to percent-off, the command will make sure your
amount is 100% or less.
amount is 100% or less. You can set the discount payment type using --payment-type.
The payment type should be one of (`marketing`, `sales`, `financial-assistance`,
`customer-support`, or `staff`).
"""

Expand All @@ -34,6 +36,7 @@

from ecommerce.constants import (
ALL_DISCOUNT_TYPES,
ALL_PAYMENT_TYPES,
DISCOUNT_TYPE_PERCENT_OFF,
REDEMPTION_TYPE_ONE_TIME,
REDEMPTION_TYPE_ONE_TIME_PER_USER,
Expand Down Expand Up @@ -76,6 +79,13 @@ def add_arguments(self, parser) -> None:
default="percent-off",
)

parser.add_argument(
"--payment-type",
type=str,
help="Sets the payment type (marketing, sales, financial-assistance, customer-support, staff)",
required=True,
)

parser.add_argument(
"--amount",
type=str,
Expand Down Expand Up @@ -115,6 +125,7 @@ def handle(self, *args, **kwargs): # pylint: disable=unused-argument
codes_to_generate = []
discount_type = kwargs["discount_type"]
redemption_type = REDEMPTION_TYPE_UNLIMITED
payment_type = kwargs["payment_type"]
amount = Decimal(kwargs["amount"])

if kwargs["discount_type"] not in ALL_DISCOUNT_TYPES:
Expand All @@ -125,6 +136,12 @@ def handle(self, *args, **kwargs): # pylint: disable=unused-argument
)
exit(-1)

if payment_type not in ALL_PAYMENT_TYPES:
self.stderr.write(
self.style.ERROR(f"Payment type {payment_type} is not valid.")
)
exit(-1)

if kwargs["discount_type"] == DISCOUNT_TYPE_PERCENT_OFF and amount > 100:
self.stderr.write(
self.style.ERROR(
Expand Down Expand Up @@ -183,11 +200,11 @@ def handle(self, *args, **kwargs): # pylint: disable=unused-argument
discount = Discount.objects.create(
discount_type=discount_type,
redemption_type=redemption_type,
payment_type=payment_type,
expiration_date=expiration_date,
activation_date=activation_date,
discount_code=code_to_generate,
amount=amount,
for_flexible_pricing=False,
)

generated_codes.append(discount)
Expand Down
32 changes: 18 additions & 14 deletions ecommerce/management/commands/generate_legacy_enrollment_codes.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
"""
Generates enrollment (discount) codes for legacy users.
Generates enrollment (discount) codes for legacy users.
This is for learners that have paid for a course but aren't enrolled in a
course run with a verified enrollment. The list is generated outside this
This is for learners that have paid for a course but aren't enrolled in a
course run with a verified enrollment. The list is generated outside this
command and is passed to it as a CSV file. The format of the file is:
```
learner email,course readable id or course readable id or micromasters course ID
```
and this command will generate single-use per-user discounts for each learner
and course combination (if a product exists for it). The course must be
available for enrollment and must also have a product associated with it.
and course combination (if a product exists for it). The course must be
available for enrollment and must also have a product associated with it.
"""
from django.core.management import BaseCommand
from django.db import transaction
from django.db.models import Q

import csv
import uuid

import dateutil
import pytz
from django.core.management import BaseCommand
from django.db import transaction
from django.db.models import Q

from courses.models import CourseRun, Course
from courses.models import Course, CourseRun
from ecommerce.constants import (
DISCOUNT_TYPE_PERCENT_OFF,
PAYMENT_TYPE_CUSTOMER_SUPPORT,
REDEMPTION_TYPE_ONE_TIME,
)
from ecommerce.models import Discount, DiscountProduct, UserDiscount
from ecommerce.constants import REDEMPTION_TYPE_ONE_TIME, DISCOUNT_TYPE_PERCENT_OFF
from users.models import User
from micromasters_import.models import CourseId
from main.settings import TIME_ZONE
from micromasters_import.models import CourseId
from users.models import User


class Command(BaseCommand):
Expand Down Expand Up @@ -159,7 +163,7 @@ def handle(self, *args, **kwargs): # pylint: disable=unused-argument
discount_type=DISCOUNT_TYPE_PERCENT_OFF,
redemption_type=REDEMPTION_TYPE_ONE_TIME,
discount_code=code,
for_flexible_pricing=False,
payment_type=PAYMENT_TYPE_CUSTOMER_SUPPORT,
expiration_date=expiration_date,
)

Expand Down
29 changes: 29 additions & 0 deletions ecommerce/migrations/0031_discount_payment_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 3.2.15 on 2023-02-09 08:49

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("ecommerce", "0030_alter_discount_for_flexible_pricing"),
]

operations = [
migrations.AddField(
model_name="discount",
name="payment_type",
field=models.CharField(
choices=[
("marketing", "marketing"),
("sales", "sales"),
("financial-assistance", "financial-assistance"),
("customer-support", "customer-support"),
("staff", "staff"),
("legacy", "legacy"),
],
max_length=30,
null=True,
),
),
]
59 changes: 59 additions & 0 deletions ecommerce/migrations/0032_backfill_payment_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Generated by Django 3.2.15 on 2023-02-09 08:49

from django.db import migrations
from django.db.models import Q

from ecommerce.constants import (
PAYMENT_TYPE_CUSTOMER_SUPPORT,
PAYMENT_TYPE_FINANCIAL_ASSISTANCE,
PAYMENT_TYPE_LEGACY,
PAYMENT_TYPE_MARKETING,
PAYMENT_TYPE_STAFF,
)


def backfill_payment_types(apps, schema_editor):
discount = apps.get_model("ecommerce", "Discount")

# financial-assistance have `for_flexible_pricing=True`
discount.objects.filter(for_flexible_pricing=True).update(
payment_type=PAYMENT_TYPE_FINANCIAL_ASSISTANCE
)

# customer-support discount codes start with `CS-`
discount.objects.filter(discount_code__startswith="CS-").update(
payment_type=PAYMENT_TYPE_CUSTOMER_SUPPORT
)

# staff discount codes start with `JPAL-`
discount.objects.filter(discount_code__startswith="JPAL-").update(
payment_type=PAYMENT_TYPE_STAFF
)

# legacy discounts start with `MM-prepaid-`
discount.objects.filter(discount_code__startswith="MM-prepaid-").update(
payment_type=PAYMENT_TYPE_LEGACY
)

# marketing discount codes have multiple start codes
discount.objects.filter(
Q(discount_code__startswith="MITALUM15-")
| Q(discount_code__startswith="14750x-")
| Q(discount_code__startswith="14740x-")
| Q(discount_code__startswith="1473x-")
| Q(discount_code__startswith="14310x-")
| Q(discount_code__startswith="JPAL102x-")
| Q(discount_code__startswith="CYB2022-")
| Q(discount_code="CYBER2022")
).update(payment_type=PAYMENT_TYPE_MARKETING)


class Migration(migrations.Migration):

dependencies = [
("ecommerce", "0031_discount_payment_type"),
]

operations = [
migrations.RunPython(backfill_payment_types, migrations.RunPython.noop),
]
17 changes: 17 additions & 0 deletions ecommerce/migrations/0033_remove_discount_for_flexible_pricing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 3.2.15 on 2023-02-09 08:52

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("ecommerce", "0032_backfill_payment_types"),
]

operations = [
migrations.RemoveField(
model_name="discount",
name="for_flexible_pricing",
),
]
3 changes: 2 additions & 1 deletion ecommerce/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
DISCOUNT_TYPE_FIXED_PRICE,
DISCOUNT_TYPE_PERCENT_OFF,
DISCOUNT_TYPES,
PAYMENT_TYPES,
REDEMPTION_TYPE_ONE_TIME,
REDEMPTION_TYPE_ONE_TIME_PER_USER,
REDEMPTION_TYPE_UNLIMITED,
Expand Down Expand Up @@ -239,9 +240,9 @@ class Discount(TimestampedModel):
automatic = models.BooleanField(default=False)
discount_type = models.CharField(choices=DISCOUNT_TYPES, max_length=30)
redemption_type = models.CharField(choices=REDEMPTION_TYPES, max_length=30)
payment_type = models.CharField(null=True, choices=PAYMENT_TYPES, max_length=30)
max_redemptions = models.PositiveIntegerField(null=True, default=0)
discount_code = models.CharField(max_length=50)
for_flexible_pricing = models.BooleanField(null=False, default=False)
activation_date = models.DateTimeField(
null=True,
blank=True,
Expand Down
2 changes: 1 addition & 1 deletion ecommerce/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ class Meta:
"redemption_type",
"max_redemptions",
"discount_code",
"for_flexible_pricing",
"payment_type",
"is_redeemed",
"activation_date",
"expiration_date",
Expand Down
Loading

0 comments on commit 82d222e

Please sign in to comment.