Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions memberportal/api_billing/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from django.contrib import admin

from .models import PaymentPlanSwitchOperation


@admin.register(PaymentPlanSwitchOperation)
class PaymentPlanSwitchOperationAdmin(admin.ModelAdmin):
list_display = (
"profile",
"current_plan",
"target_plan",
"status",
"attempt_count",
"updated_at",
)
list_filter = ("status",)
search_fields = (
"profile__user__email",
"stripe_subscription_id",
"idempotency_key",
)
readonly_fields = (
"profile",
"current_plan",
"target_plan",
"stripe_subscription_id",
"idempotency_key",
"status",
"attempt_count",
"last_error",
"created_at",
"updated_at",
)

@admin.action(description="Discard selected failed plan switches")
def discard_failed_operations(self, request, queryset):
queryset.filter(status=PaymentPlanSwitchOperation.STATUS_FAILED).delete()

actions = ("discard_failed_operations",)

def has_delete_permission(self, request, obj=None):
return obj is None or obj.status == PaymentPlanSwitchOperation.STATUS_FAILED

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems strange and I have no idea what it is doing. Probably some django thing I am not familiar with.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything in the migrations folder is a Django ORM database migration. After you change the ORM model, you run ./manage.py makemigration and the corresponding database changes are automatically generated.

Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
("api_admin_tools", "0013_paymentplan_description"),
("api_billing", "0001_initial"),
("constance", "0003_drop_pickle"),
("profile", "0033_alter_inductionproviderstate_provider"),
]

operations = [
migrations.CreateModel(
name="PaymentPlanSwitchOperation",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("stripe_subscription_id", models.CharField(max_length=100)),
("idempotency_key", models.CharField(max_length=255, unique=True)),
(
"status",
models.CharField(
choices=[("pending", "Pending")],
default="pending",
max_length=16,
),
),
("attempt_count", models.PositiveIntegerField(default=0)),
("last_error", models.TextField(blank=True, default="")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"current_plan",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="switch_operations_from",
to="api_admin_tools.paymentplan",
),
),
(
"profile",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="payment_plan_switch_operation",
to="profile.profile",
),
),
(
"target_plan",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="switch_operations_to",
to="api_admin_tools.paymentplan",
),
),
],
options={
"indexes": [
models.Index(
fields=["status", "updated_at"],
name="api_billing_status_55ab81_idx",
)
],
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("api_billing", "0002_paymentplanswitchoperation"),
]

operations = [
migrations.AlterField(
model_name="paymentplanswitchoperation",
name="status",
field=models.CharField(
choices=[("pending", "Pending"), ("failed", "Failed")],
default="pending",
max_length=16,
),
),
]
46 changes: 46 additions & 0 deletions memberportal/api_billing/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,49 @@ class ProcessedStripeEvent(models.Model):
class Meta:
verbose_name = "Processed Stripe Event"
verbose_name_plural = "Processed Stripe Events"


class PaymentPlanSwitchOperation(models.Model):
"""Durable state for a Stripe subscription Price replacement.

The row is created before the Stripe mutation. If the request dies after
Stripe succeeds but before Profile is saved, the periodic reconciliation
task can inspect Stripe and finish the local update using this record.
"""

STATUS_PENDING = "pending"
STATUS_FAILED = "failed"
STATUS_CHOICES = (
(STATUS_PENDING, "Pending"),
(STATUS_FAILED, "Failed"),
)

profile = models.OneToOneField(
"profile.Profile",
on_delete=models.CASCADE,
related_name="payment_plan_switch_operation",
)
current_plan = models.ForeignKey(
"api_admin_tools.PaymentPlan",
on_delete=models.PROTECT,
related_name="switch_operations_from",
)
target_plan = models.ForeignKey(
"api_admin_tools.PaymentPlan",
on_delete=models.PROTECT,
related_name="switch_operations_to",
)
stripe_subscription_id = models.CharField(max_length=100)
idempotency_key = models.CharField(max_length=255, unique=True)
status = models.CharField(
max_length=16,
choices=STATUS_CHOICES,
default=STATUS_PENDING,
)
attempt_count = models.PositiveIntegerField(default=0)
last_error = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
indexes = [models.Index(fields=["status", "updated_at"])]
Loading
Loading