Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""add_new_organization_status_values

Revision ID: 2af63e66a18e
Revises: 2c7d8141f6b4
Create Date: 2025-10-31 16:50:46.076228

"""

import sqlalchemy as sa
from alembic import op

# Polar Custom Imports

# revision identifiers, used by Alembic.
revision = "2af63e66a18e"
down_revision = "2c7d8141f6b4"
branch_labels: tuple[str] | None = None
depends_on: tuple[str] | None = None


def upgrade() -> None:
# Add new enum values to organizationstatus
op.execute("ALTER TYPE organizationstatus ADD VALUE IF NOT EXISTS 'ready'")
op.execute("ALTER TYPE organizationstatus ADD VALUE IF NOT EXISTS 'first_review'")
op.execute("ALTER TYPE organizationstatus ADD VALUE IF NOT EXISTS 'ongoing_review'")
op.execute("ALTER TYPE organizationstatus ADD VALUE IF NOT EXISTS 'blocked'")

# Backfill: Migrate organizations with blocked_at set to BLOCKED status
op.execute(
"""
UPDATE organizations
SET status = 'blocked'
WHERE blocked_at IS NOT NULL
"""
)

# Backfill: Migrate UNDER_REVIEW to FIRST_REVIEW
# We assume all existing UNDER_REVIEW cases are first-time reviews
op.execute(
"""
UPDATE organizations
SET status = 'first_review'
WHERE status = 'under_review'
"""
)


def downgrade() -> None:
# Revert FIRST_REVIEW back to UNDER_REVIEW
op.execute(
"""
UPDATE organizations
SET status = 'under_review'
WHERE status = 'first_review'
"""
)

# Revert BLOCKED status back to using blocked_at
# Organizations with BLOCKED status should have blocked_at set during upgrade
op.execute(
"""
UPDATE organizations
SET status = 'active'
WHERE status = 'blocked' AND blocked_at IS NOT NULL
"""
)

# Note: PostgreSQL doesn't support removing enum values easily
# The new enum values (ready, first_review, ongoing_review, blocked) will remain
# but won't be used after downgrade
28 changes: 22 additions & 6 deletions server/polar/backoffice/components/_status_badge.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,30 @@ def status_badge(
"""
# Map status to badge variant with flat design
status_config = {
OrganizationStatus.CREATED: {
"class": "badge-ghost border border-base-300",
"aria": "created status",
},
OrganizationStatus.ONBOARDING_STARTED: {
"class": "badge-ghost border border-base-300",
"aria": "onboarding started status",
},
OrganizationStatus.READY: {
"class": "badge-ghost border border-base-300",
"aria": "ready status",
},
OrganizationStatus.FIRST_REVIEW: {
"class": "badge-ghost border border-base-300",
"aria": "first review status",
},
OrganizationStatus.ACTIVE: {
"class": "badge-ghost border border-base-300",
"aria": "active status",
},
OrganizationStatus.ONGOING_REVIEW: {
"class": "badge-ghost border border-base-300",
"aria": "ongoing review status",
},
OrganizationStatus.UNDER_REVIEW: {
"class": "badge-ghost border border-base-300",
"aria": "under review status",
Expand All @@ -49,13 +69,9 @@ def status_badge(
"class": "badge-ghost border border-base-300",
"aria": "denied status",
},
OrganizationStatus.CREATED: {
"class": "badge-ghost border border-base-300",
"aria": "created status",
},
OrganizationStatus.ONBOARDING_STARTED: {
OrganizationStatus.BLOCKED: {
"class": "badge-ghost border border-base-300",
"aria": "onboarding started status",
"aria": "blocked status",
},
}

Expand Down
10 changes: 4 additions & 6 deletions server/polar/backoffice/organizations_v2/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,8 +676,8 @@ async def unblock_approve_dialog(
raw_threshold = data.get("threshold", "250")
threshold = int(float(str(raw_threshold)) * 100)

# Unblock the organization (set blocked_at to None)
organization.blocked_at = None
# Unblock the organization
await organization_service.unblock_organization(session, organization)

# Approve the organization
await organization_service.confirm_organization_reviewed(
Expand Down Expand Up @@ -761,10 +761,8 @@ async def block_dialog(
raise HTTPException(status_code=404, detail="Organization not found")

if request.method == "POST":
# Block the organization (set blocked_at to current time)
from datetime import UTC, datetime

organization.blocked_at = datetime.now(UTC)
# Block the organization using service method
await organization_service.block_organization(session, organization)
await session.commit()

return HXRedirectResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,11 @@ def right_sidebar(self, request: Request) -> Generator[None]:
):
text("Deny")

elif self.org.status == OrganizationStatus.UNDER_REVIEW:
elif self.org.status in (
OrganizationStatus.UNDER_REVIEW,
OrganizationStatus.FIRST_REVIEW,
OrganizationStatus.ONGOING_REVIEW,
):
# Quick approve with default threshold
max_threshold = (self.org.next_review_threshold or 25000000) * 2
max_threshold_display = f"${max_threshold / 100:,.0f}"
Expand Down
44 changes: 39 additions & 5 deletions server/polar/backoffice/organizations_v2/views/list_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,15 @@ def is_needs_attention(self, org: Organization) -> bool:
days_in_status = self.calculate_days_in_status(org)

# Under review for more than 3 days
if org.status == OrganizationStatus.UNDER_REVIEW and days_in_status > 3:
if (
org.status
in (
OrganizationStatus.UNDER_REVIEW,
OrganizationStatus.FIRST_REVIEW,
OrganizationStatus.ONGOING_REVIEW,
)
and days_in_status > 3
):
return True

# Has pending appeal
Expand Down Expand Up @@ -265,10 +273,16 @@ def render(
count=sum(status_counts.values()),
),
Tab(
label="Under Review",
url="/backoffice/organizations-v2?status=under_review",
active=status_filter == OrganizationStatus.UNDER_REVIEW,
count=status_counts.get(OrganizationStatus.UNDER_REVIEW, 0),
label="Ready",
url="/backoffice/organizations-v2?status=ready",
active=status_filter == OrganizationStatus.READY,
count=status_counts.get(OrganizationStatus.READY, 0),
),
Tab(
label="First Review",
url="/backoffice/organizations-v2?status=first_review",
active=status_filter == OrganizationStatus.FIRST_REVIEW,
count=status_counts.get(OrganizationStatus.FIRST_REVIEW, 0),
badge_variant="warning",
),
Tab(
Expand All @@ -278,13 +292,33 @@ def render(
count=status_counts.get(OrganizationStatus.ACTIVE, 0),
badge_variant="success",
),
Tab(
label="Ongoing Review",
url="/backoffice/organizations-v2?status=ongoing_review",
active=status_filter == OrganizationStatus.ONGOING_REVIEW,
count=status_counts.get(OrganizationStatus.ONGOING_REVIEW, 0),
),
Tab(
label="Under Review (Legacy)",
url="/backoffice/organizations-v2?status=under_review",
active=status_filter == OrganizationStatus.UNDER_REVIEW,
count=status_counts.get(OrganizationStatus.UNDER_REVIEW, 0),
badge_variant="warning",
),
Tab(
label="Denied",
url="/backoffice/organizations-v2?status=denied",
active=status_filter == OrganizationStatus.DENIED,
count=status_counts.get(OrganizationStatus.DENIED, 0),
badge_variant="error",
),
Tab(
label="Blocked",
url="/backoffice/organizations-v2?status=blocked",
active=status_filter == OrganizationStatus.BLOCKED,
count=status_counts.get(OrganizationStatus.BLOCKED, 0),
badge_variant="error",
),
]

with tab_nav(tabs):
Expand Down
35 changes: 26 additions & 9 deletions server/polar/models/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,25 @@ class OrganizationCustomerEmailSettings(TypedDict):
class OrganizationStatus(StrEnum):
CREATED = "created"
ONBOARDING_STARTED = "onboarding_started"
UNDER_REVIEW = "under_review"
DENIED = "denied"
READY = "ready"
UNDER_REVIEW = "under_review" # Deprecated: use FIRST_REVIEW or ONGOING_REVIEW
FIRST_REVIEW = "first_review"
ACTIVE = "active"
ONGOING_REVIEW = "ongoing_review"
DENIED = "denied"
BLOCKED = "blocked"

def get_display_name(self) -> str:
return {
OrganizationStatus.CREATED: "Created",
OrganizationStatus.ONBOARDING_STARTED: "Onboarding Started",
OrganizationStatus.READY: "Ready",
OrganizationStatus.UNDER_REVIEW: "Under Review",
OrganizationStatus.DENIED: "Denied",
OrganizationStatus.FIRST_REVIEW: "First Review",
OrganizationStatus.ACTIVE: "Active",
OrganizationStatus.ONGOING_REVIEW: "Ongoing Review",
OrganizationStatus.DENIED: "Denied",
OrganizationStatus.BLOCKED: "Blocked",
}[self]


Expand Down Expand Up @@ -234,12 +242,18 @@ def account(cls) -> Mapped[Account | None]:

@hybrid_property
def can_authenticate(self) -> bool:
return self.deleted_at is None and self.blocked_at is None
# Check both status and blocked_at during migration period
return self.deleted_at is None and not self.is_blocked()

@can_authenticate.inplace.expression
@classmethod
def _can_authenticate_expression(cls) -> ColumnElement[bool]:
return and_(cls.deleted_at.is_(None), cls.blocked_at.is_(None))
# During expand phase, check both blocked_at and status
return and_(
cls.deleted_at.is_(None),
cls.blocked_at.is_(None),
cls.status != OrganizationStatus.BLOCKED,
)

@hybrid_property
def storefront_enabled(self) -> bool:
Expand Down Expand Up @@ -309,12 +323,15 @@ def review(cls) -> Mapped["OrganizationReview | None"]:
)

def is_blocked(self) -> bool:
if self.blocked_at is not None:
return True
return False
# Check both status and blocked_at during migration period
return self.status == OrganizationStatus.BLOCKED or self.blocked_at is not None

def is_under_review(self) -> bool:
return self.status == OrganizationStatus.UNDER_REVIEW
return self.status in (
OrganizationStatus.UNDER_REVIEW,
OrganizationStatus.FIRST_REVIEW,
OrganizationStatus.ONGOING_REVIEW,
)

def is_active(self) -> bool:
return self.status == OrganizationStatus.ACTIVE
Expand Down
Loading