Skip to content

Commit

Permalink
Added discount model (dj-stripe#1751)
Browse files Browse the repository at this point in the history
* Added internal model InvoiceOrLineItem

InvoiceOrLineItem model is similar to the DjstripePaymentMethod model and
acts as a wrapper around the InvoiceItem and LineItem models.

This was done because Discount.invoice_item model field can receive
either of InvoiceItem or LineItem objects.

* Added InvoiceOrLineItemForeignKey custom model field

InvoiceOrLineItemForeignKey is like the PaymentMethodForeignKey to be able to
deal with both InvoiceItem and LineItem models whichever is returned as the
Discounts invoice_item field's value.

* Added Discount Model

* Exposed Discount model on the Admin

* Updated _handle_crud_like_event to allow syncing of discount.* events

This was done because Stripe doesn't allow direct retrieval
of Discount objects.

* Fixed Discount Event Fixtures

* Fixed corner case in Event.customer prop

It could be possible that the json key
may have been expanded using expand_fields
attribute and hence also handled that case.

* Updated Changelog

* Fix Formatting Errors
  • Loading branch information
arnav13081994 authored Feb 8, 2023
1 parent 36d1323 commit 44356fa
Show file tree
Hide file tree
Showing 14 changed files with 509 additions and 15 deletions.
24 changes: 24 additions & 0 deletions djstripe/admin/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,30 @@ def get_queryset(self, request):
)


@admin.register(models.Discount)
class DiscountAdmin(ReadOnlyMixin, StripeModelAdmin):
list_display = (
"customer",
"coupon",
"invoice_item",
"promotion_code",
"subscription",
)
list_filter = ("customer", "start", "end", "promotion_code", "coupon")

def get_actions(self, request):
"""
Returns _resync_instances only for
models with a defined model.stripe_class.retrieve
"""
actions = super().get_actions(request)

# remove "_sync_all_instances" as Discounts cannot be listed
actions.pop("_sync_all_instances", None)

return actions


@admin.register(models.Dispute)
class DisputeAdmin(ReadOnlyMixin, StripeModelAdmin):
list_display = ("reason", "status", "amount", "currency", "is_charge_refundable")
Expand Down
6 changes: 6 additions & 0 deletions djstripe/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,12 @@ class InvoiceStatus(Enum):
void = _("Void")


class InvoiceorLineItemType(Enum):
invoice_item = _("Invoice Item")
line_item = _("Line Item")
unsupported = _("Unsupported")


class IntentUsage(Enum):
on_session = _("On session")
off_session = _("Off session")
Expand Down
11 changes: 8 additions & 3 deletions djstripe/event_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,9 +408,14 @@ def _handle_crud_like_event(
if event.parts[:2] == ["account", "external_account"] and stripe_account:
kwargs["account"] = models.Account._get_or_retrieve(id=stripe_account)

data = target_cls(**kwargs).api_retrieve(
stripe_account=stripe_account, api_key=event.default_api_key
)
# Stripe doesn't allow retrieval of Discount Objects
if target_cls not in (models.Discount,):
data = target_cls(**kwargs).api_retrieve(
stripe_account=stripe_account, api_key=event.default_api_key
)
else:
data = data.get("object")

# create or update the object from the retrieved Stripe Data
obj = target_cls.sync_from_stripe_data(data, api_key=event.default_api_key)

Expand Down
6 changes: 6 additions & 0 deletions djstripe/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ def __init__(self, **kwargs):
super().__init__(**kwargs)


class InvoiceOrLineItemForeignKey(models.ForeignKey):
def __init__(self, **kwargs):
kwargs.setdefault("to", "InvoiceOrLineItem")
super().__init__(**kwargs)


class StripePercentField(FieldDeconstructMixin, models.DecimalField):
"""A field used to define a percent according to djstripe logic."""

Expand Down
32 changes: 32 additions & 0 deletions djstripe/migrations/0017_invoiceorlineitem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 3.2.13 on 2022-07-09 08:04

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models

import djstripe.enums
import djstripe.fields


class Migration(migrations.Migration):
dependencies = [
("djstripe", "0016_alter_payout_destination"),
]

operations = [
migrations.CreateModel(
name="InvoiceOrLineItem",
fields=[
(
"id",
models.CharField(max_length=255, primary_key=True, serialize=False),
),
(
"type",
djstripe.fields.StripeEnumField(
enum=djstripe.enums.InvoiceorLineItemType, max_length=12
),
),
],
),
]
130 changes: 130 additions & 0 deletions djstripe/migrations/0018_discount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Generated by Django 3.2.16 on 2023-01-28 06:04

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models

import djstripe.fields


class Migration(migrations.Migration):
dependencies = [
("djstripe", "0017_invoiceorlineitem"),
]

operations = [
migrations.CreateModel(
name="Discount",
fields=[
("djstripe_created", models.DateTimeField(auto_now_add=True)),
("djstripe_updated", models.DateTimeField(auto_now=True)),
(
"djstripe_id",
models.BigAutoField(
primary_key=True, serialize=False, verbose_name="ID"
),
),
("id", djstripe.fields.StripeIdField(max_length=255, unique=True)),
(
"livemode",
models.BooleanField(
blank=True,
default=None,
help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.",
null=True,
),
),
("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)),
("metadata", djstripe.fields.JSONField(blank=True, null=True)),
(
"description",
models.TextField(
blank=True, help_text="A description of this object.", null=True
),
),
("coupon", djstripe.fields.JSONField(blank=True, null=True)),
("end", djstripe.fields.StripeDateTimeField(blank=True, null=True)),
(
"promotion_code",
models.CharField(
blank=True,
help_text="The promotion code applied to create this discount.",
max_length=255,
),
),
("start", djstripe.fields.StripeDateTimeField(blank=True, null=True)),
(
"checkout_session",
djstripe.fields.StripeForeignKey(
blank=True,
help_text="The Checkout session that this coupon is applied to, if it is applied to a particular session in payment mode. Will not be present for subscription mode.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="djstripe.session",
to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD,
),
),
(
"customer",
djstripe.fields.StripeForeignKey(
blank=True,
help_text="The ID of the customer associated with this discount.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="customer_discounts",
to="djstripe.customer",
to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD,
),
),
(
"djstripe_owner_account",
djstripe.fields.StripeForeignKey(
blank=True,
help_text="The Stripe Account this object belongs to.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="djstripe.account",
to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD,
),
),
(
"invoice",
djstripe.fields.StripeForeignKey(
blank=True,
help_text="The invoice that the discount’s coupon was applied to, if it was applied directly to a particular invoice.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="invoice_discounts",
to="djstripe.invoice",
to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD,
),
),
(
"invoice_item",
djstripe.fields.InvoiceOrLineItemForeignKey(
blank=True,
help_text="The invoice item id (or invoice line item id for invoice line items of type=‘subscription’) that the discount’s coupon was applied to, if it was applied directly to a particular invoice item or invoice line item.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="djstripe.invoiceorlineitem",
),
),
(
"subscription",
djstripe.fields.StripeForeignKey(
blank=True,
help_text="The subscription that this coupon is applied to, if it is applied to a particular subscription.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="subscription_discounts",
to="djstripe.subscription",
to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD,
),
),
],
options={
"get_latest_by": "created",
"abstract": False,
},
),
]
4 changes: 4 additions & 0 deletions djstripe/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from .base import IdempotencyKey, StripeModel
from .billing import (
Coupon,
Discount,
Invoice,
InvoiceItem,
InvoiceOrLineItem,
LineItem,
Plan,
ShippingRate,
Expand Down Expand Up @@ -66,6 +68,7 @@
"CountrySpec",
"Coupon",
"Customer",
"Discount",
"Dispute",
"DjstripePaymentMethod",
"Event",
Expand All @@ -76,6 +79,7 @@
"Invoice",
"InvoiceItem",
"LineItem",
"InvoiceOrLineItem",
"Mandate",
"Order",
"PaymentIntent",
Expand Down
6 changes: 3 additions & 3 deletions djstripe/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ def _stripe_object_field_to_foreign_key(
:type stripe_account: string
:return:
"""
from djstripe.models import DjstripePaymentMethod
from djstripe.models import DjstripePaymentMethod, InvoiceOrLineItem

field_data = None
field_name = field.name
Expand All @@ -451,8 +451,8 @@ def _stripe_object_field_to_foreign_key(
if current_ids is None:
current_ids = set()

if issubclass(field.related_model, StripeModel) or issubclass(
field.related_model, DjstripePaymentMethod
if issubclass(
field.related_model, (StripeModel, DjstripePaymentMethod, InvoiceOrLineItem)
):
if field_name in manipulated_data:
raw_field_data = manipulated_data.get(field_name)
Expand Down
Loading

0 comments on commit 44356fa

Please sign in to comment.