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,45 @@
"""add_global_invoice_sequence

Revision ID: 1c01dfc86203
Revises: 902dd5c6c2b7
Create Date: 2025-10-21 15:18:00.469806

"""

import sqlalchemy as sa
from alembic import op

# Polar Custom Imports

# revision identifiers, used by Alembic.
revision = "1c01dfc86203"
down_revision = "902dd5c6c2b7"
branch_labels: tuple[str] | None = None
depends_on: tuple[str] | None = None


def upgrade() -> None:
# Buffer to add to max existing invoice number
# This ensures clean separation between old per-org and new global numbering
BUFFER_SIZE = 10000

# Create the global invoice number sequence
op.execute("CREATE SEQUENCE invoice_number_seq START WITH 1")

# Initialize sequence to max existing invoice number + buffer
# Extract numeric part from "PREFIX-0001" format and find the maximum
op.execute(
f"""
SELECT setval('invoice_number_seq',
COALESCE(
(SELECT MAX(CAST(SPLIT_PART(invoice_number, '-', 2) AS INTEGER))
Copy link
Contributor

Choose a reason for hiding this comment

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

Alternatively we could just do SELECT COUNT(*) FROM orders and start there?

FROM orders),
0
) + {BUFFER_SIZE}
)
"""
)


def downgrade() -> None:
op.execute("DROP SEQUENCE IF EXISTS invoice_number_seq")
11 changes: 8 additions & 3 deletions server/polar/organization/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import structlog
from pydantic import BaseModel, Field
from sqlalchemy import text
from sqlalchemy import update as sqlalchemy_update
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import joinedload
Expand Down Expand Up @@ -362,16 +363,20 @@ async def get_next_invoice_number(
session: AsyncSession,
organization: Organization,
) -> str:
invoice_number = f"{organization.customer_invoice_prefix}-{organization.customer_invoice_next_number:04d}"
result = await session.execute(text("SELECT nextval('invoice_number_seq')"))
number = result.scalar_one()

# Backward compatability
repository = OrganizationRepository.from_session(session)
organization = await repository.update(
await repository.update(
organization,
update_dict={
"customer_invoice_next_number": organization.customer_invoice_next_number
+ 1
},
)
return invoice_number

return f"{organization.customer_invoice_prefix}-{number:04d}"

async def _after_update(
self,
Expand Down
4 changes: 4 additions & 0 deletions server/tests/fixtures/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ async def initialize_test_database(worker_id: str) -> AsyncIterator[None]:
await conn.execute(text("CREATE EXTENSION IF NOT EXISTS citext"))
await conn.execute(text('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'))
await conn.run_sync(Model.metadata.create_all)
# Create global invoice number sequence (from migration)
await conn.execute(
text("CREATE SEQUENCE IF NOT EXISTS invoice_number_seq START WITH 1")
)
await engine.dispose()

yield
Expand Down
24 changes: 22 additions & 2 deletions server/tests/organization/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,35 @@ async def test_valid_with_none_subscription_settings(
async def test_get_next_invoice_number(
session: AsyncSession,
organization: Organization,
organization_second: Organization,
) -> None:
assert organization.customer_invoice_next_number == 1
assert organization_second.customer_invoice_next_number == 1

next_invoice_number = await organization_service.get_next_invoice_number(
# Get invoice from first organization
invoice_org1 = await organization_service.get_next_invoice_number(
session, organization
)

assert next_invoice_number == f"{organization.customer_invoice_prefix}-0001"
# Get invoice from second organization
invoice_org2 = await organization_service.get_next_invoice_number(
session, organization_second
)

# Extract numbers from format "PREFIX-0001"
number_1 = int(invoice_org1.split("-")[1])
number_2 = int(invoice_org2.split("-")[1])

# Verify sequential global numbering across organizations
assert number_2 == number_1 + 1

# Verify each uses their own prefix
assert invoice_org1.startswith(organization.customer_invoice_prefix)
assert invoice_org2.startswith(organization_second.customer_invoice_prefix)

# Verify per-org counter still increments (behind the scenes)
assert organization.customer_invoice_next_number == 2
assert organization_second.customer_invoice_next_number == 2


@pytest.mark.asyncio
Expand Down
Loading