Skip to content

Commit bfc01d2

Browse files
committed
feat(customer): Generate a unique short ID for invoice order number
Set the base26 encoding of the new short id as an infix for the invoice order number.
1 parent a89f7da commit bfc01d2

File tree

8 files changed

+229
-27
lines changed

8 files changed

+229
-27
lines changed

server/migrations/functions.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# PostgreSQL functions that are applied, that needs to be both migrated and recreated
2+
# in our tests.
3+
4+
SEQUENCE_CREATE_CUSTOMER_SHORT_ID = "CREATE SEQUENCE customer_short_id_seq START 1;"
5+
6+
# ID generation algorithm based on https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c
7+
FUNCTION_GENERATE_CUSTOMER_SHORT_ID = """
8+
CREATE OR REPLACE FUNCTION generate_customer_short_id(creation_timestamp TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp())
9+
RETURNS bigint AS $$
10+
DECLARE
11+
our_epoch bigint := 1672531200000; -- 2023-01-01 in milliseconds
12+
seq_id bigint;
13+
now_millis bigint;
14+
result bigint;
15+
BEGIN
16+
-- Get sequence number modulo 1024 (10 bits)
17+
SELECT nextval('customer_short_id_seq') % 1024 INTO seq_id;
18+
19+
-- Use provided timestamp (defaults to clock_timestamp())
20+
SELECT FLOOR(EXTRACT(EPOCH FROM creation_timestamp) * 1000) INTO now_millis;
21+
22+
-- 42 bits timestamp (milliseconds) | 10 bits sequence = 52 bits total
23+
-- Capacity: 1,024 IDs per millisecond (over 1 million per second)
24+
-- Combine: (timestamp - epoch) << 10 | sequence
25+
result := (now_millis - our_epoch) << 10;
26+
result := result | seq_id;
27+
28+
RETURN result;
29+
END;
30+
$$ LANGUAGE plpgsql;
31+
"""
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""add short_id to customers
2+
3+
Revision ID: aee75623c4bb
4+
Revises: 8c9ef8060e95
5+
Create Date: 2025-10-24 11:39:54.946342
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
12+
# Polar Custom Imports
13+
from migrations.functions import (
14+
FUNCTION_GENERATE_CUSTOMER_SHORT_ID,
15+
SEQUENCE_CREATE_CUSTOMER_SHORT_ID,
16+
)
17+
18+
# revision identifiers, used by Alembic.
19+
revision = "aee75623c4bb"
20+
down_revision = "8c9ef8060e95"
21+
branch_labels: tuple[str] | None = None
22+
depends_on: tuple[str] | None = None
23+
24+
25+
def upgrade() -> None:
26+
op.execute(SEQUENCE_CREATE_CUSTOMER_SHORT_ID)
27+
28+
# Create Instagram-style ID generator
29+
# Combines timestamp (milliseconds since epoch) + sequence number
30+
op.execute(FUNCTION_GENERATE_CUSTOMER_SHORT_ID)
31+
32+
op.add_column(
33+
"customers",
34+
sa.Column("short_id", sa.BigInteger(), nullable=True),
35+
)
36+
37+
op.execute(
38+
"""
39+
UPDATE customers
40+
SET short_id = generate_customer_short_id(created_at);
41+
"""
42+
)
43+
44+
op.alter_column("customers", "short_id", nullable=False)
45+
46+
op.execute(
47+
"""
48+
ALTER TABLE customers
49+
ALTER COLUMN short_id SET DEFAULT generate_customer_short_id();
50+
"""
51+
)
52+
53+
op.create_unique_constraint(
54+
op.f("customers_organization_id_short_id_key"),
55+
"customers",
56+
["organization_id", "short_id"],
57+
)
58+
op.create_index(
59+
op.f("ix_customers_short_id"), "customers", ["short_id"], unique=False
60+
)
61+
# ### end Alembic commands ###
62+
63+
64+
def downgrade() -> None:
65+
# ### commands auto generated by Alembic - please adjust! ###
66+
op.drop_index(op.f("ix_customers_short_id"), table_name="customers")
67+
op.drop_constraint(
68+
op.f("customers_organization_id_short_id_key"), "customers", type_="unique"
69+
)
70+
op.drop_column("customers", "short_id")
71+
op.execute("DROP FUNCTION IF EXISTS generate_customer_short_id();")
72+
op.execute("DROP SEQUENCE IF EXISTS customer_short_id_seq;")
73+
# ### end Alembic commands ###

server/polar/models/customer.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import dataclasses
2+
import string
23
import time
34
from collections.abc import Sequence
45
from datetime import datetime
56
from enum import StrEnum
67
from typing import TYPE_CHECKING, Any
78
from uuid import UUID
89

10+
import sqlalchemy as sa
911
from sqlalchemy import (
1012
TIMESTAMP,
1113
Boolean,
@@ -37,6 +39,21 @@
3739
from .subscription import Subscription
3840

3941

42+
def short_id_to_base26(short_id: int) -> str:
43+
"""Convert a numeric short_id to an 8-character base-26 string (A-Z)."""
44+
chars = string.ascii_uppercase
45+
result = ""
46+
num = short_id
47+
48+
# Convert to base-26
49+
while num > 0:
50+
result = chars[num % 26] + result
51+
num = num // 26
52+
53+
# Pad with 'A' to ensure 8 characters
54+
return result.rjust(8, "A")
55+
56+
4057
class CustomerOAuthPlatform(StrEnum):
4158
github = "github"
4259
discord = "discord"
@@ -92,9 +109,16 @@ class Customer(MetadataMixin, RecordModel):
92109
postgresql_nulls_not_distinct=True,
93110
),
94111
UniqueConstraint("organization_id", "external_id"),
112+
UniqueConstraint("organization_id", "short_id"),
95113
)
96114

97115
external_id: Mapped[str | None] = mapped_column(String, nullable=True, default=None)
116+
short_id: Mapped[int] = mapped_column(
117+
sa.BigInteger,
118+
nullable=False,
119+
index=True,
120+
server_default=sa.text("generate_customer_short_id()"),
121+
)
98122
email: Mapped[str] = mapped_column(String(320), nullable=False)
99123
email_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
100124
stripe_customer_id: Mapped[str | None] = mapped_column(
@@ -217,6 +241,11 @@ def remove_oauth_account(
217241
def oauth_accounts(self) -> dict[str, Any]:
218242
return self._oauth_accounts
219243

244+
@property
245+
def short_id_str(self) -> str:
246+
"""Get the base-26 string representation of the short_id."""
247+
return short_id_to_base26(self.short_id)
248+
220249
@property
221250
def legacy_user_id(self) -> UUID:
222251
return self._legacy_user_id or self.id

server/polar/organization/service.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,10 +370,9 @@ async def get_next_invoice_number(
370370
) -> str:
371371
match organization.invoice_numbering:
372372
case InvoiceNumbering.customer:
373-
customer_suffix = str(customer.id).split("-")[0].upper()
374-
invoice_number = f"{organization.customer_invoice_prefix}-{customer_suffix}-{customer.invoice_next_number:04d}"
373+
invoice_number = f"{organization.customer_invoice_prefix}-{customer.short_id_str}-{customer.invoice_next_number:04d}"
375374
customer_repository = CustomerRepository.from_session(session)
376-
await customer_repository.update(
375+
customer = await customer_repository.update(
377376
customer,
378377
update_dict={
379378
"invoice_next_number": customer.invoice_next_number + 1
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import pytest
2+
from sqlalchemy import text
3+
from sqlalchemy.ext.asyncio import AsyncSession
4+
5+
from polar.models.customer import short_id_to_base26
6+
7+
8+
@pytest.mark.asyncio
9+
async def test_generate_customer_short_id_function(session: AsyncSession) -> None:
10+
"""Test that the PostgreSQL generate_customer_short_id() function works correctly."""
11+
12+
result1 = await session.execute(text("SELECT generate_customer_short_id()"))
13+
short_id1 = result1.scalar()
14+
assert short_id1 is not None
15+
16+
result2 = await session.execute(text("SELECT generate_customer_short_id()"))
17+
short_id2 = result2.scalar()
18+
assert short_id2 is not None
19+
20+
result3 = await session.execute(text("SELECT generate_customer_short_id()"))
21+
short_id3 = result3.scalar()
22+
assert short_id3 is not None
23+
24+
assert short_id1 != short_id2
25+
assert short_id2 != short_id3
26+
assert short_id1 != short_id3
27+
28+
assert short_id1 > 0
29+
assert short_id2 > 0
30+
assert short_id3 > 0
31+
32+
assert short_id2 > short_id1
33+
assert short_id3 > short_id2
34+
35+
36+
@pytest.mark.asyncio
37+
async def test_customer_short_id_to_base26(session: AsyncSession) -> None:
38+
"""Test that short_id converts to base-26 correctly."""
39+
40+
# Test some known conversions
41+
assert short_id_to_base26(0) == "AAAAAAAA"
42+
assert short_id_to_base26(1) == "AAAAAAAB"
43+
assert short_id_to_base26(25) == "AAAAAAAZ"
44+
assert short_id_to_base26(26) == "AAAAAABA"
45+
46+
# Test with a realistic generated ID
47+
result = await session.execute(text("SELECT generate_customer_short_id()"))
48+
short_id = result.scalar()
49+
assert short_id is not None
50+
51+
base26 = short_id_to_base26(short_id)
52+
53+
assert len(base26) == 8
54+
assert base26.isupper()
55+
assert all(c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" for c in base26)

server/tests/fixtures/database.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
from sqlalchemy.sql import text
99
from sqlalchemy_utils import create_database, database_exists, drop_database
1010

11+
from migrations.functions import (
12+
FUNCTION_GENERATE_CUSTOMER_SHORT_ID,
13+
SEQUENCE_CREATE_CUSTOMER_SHORT_ID,
14+
)
1115
from polar.config import settings
1216
from polar.kit.db.postgres import create_async_engine
1317
from polar.models import Model
@@ -45,6 +49,8 @@ async def initialize_test_database(worker_id: str) -> AsyncIterator[None]:
4549
async with engine.begin() as conn:
4650
await conn.execute(text("CREATE EXTENSION IF NOT EXISTS citext"))
4751
await conn.execute(text('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'))
52+
await conn.execute(text(SEQUENCE_CREATE_CUSTOMER_SHORT_ID))
53+
await conn.execute(text(FUNCTION_GENERATE_CUSTOMER_SHORT_ID))
4854
await conn.run_sync(Model.metadata.create_all)
4955
await engine.dispose()
5056

server/tests/order/test_service.py

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3893,7 +3893,6 @@ async def test_different_customers_different_invoice_numbers(
38933893
}
38943894
await save_fixture(organization)
38953895

3896-
# Create two different customers
38973896
customer_1 = await create_customer(
38983897
save_fixture,
38993898
organization=organization,
@@ -3909,7 +3908,6 @@ async def test_different_customers_different_invoice_numbers(
39093908
stripe_customer_id="STRIPE_CUSTOMER_2",
39103909
)
39113910

3912-
# Create checkout and order for customer 1
39133911
checkout_1 = await create_checkout(
39143912
save_fixture,
39153913
products=[product_one_time],
@@ -3918,7 +3916,6 @@ async def test_different_customers_different_invoice_numbers(
39183916
)
39193917
order_1 = await order_service.create_from_checkout_one_time(session, checkout_1)
39203918

3921-
# Create checkout and order for customer 2
39223919
checkout_2 = await create_checkout(
39233920
save_fixture,
39243921
products=[product_one_time],
@@ -3927,36 +3924,29 @@ async def test_different_customers_different_invoice_numbers(
39273924
)
39283925
order_2 = await order_service.create_from_checkout_one_time(session, checkout_2)
39293926

3930-
# Refresh to get the invoice numbers
39313927
await session.refresh(order_1)
39323928
await session.refresh(order_2)
39333929

3934-
# Both customers should have different invoice number sequences
3935-
# They should both start with the organization prefix but have customer-specific numbering
39363930
assert order_1.invoice_number is not None
39373931
assert order_2.invoice_number is not None
39383932
assert order_1.invoice_number != order_2.invoice_number
39393933

3940-
# Verify they both start with organization prefix
39413934
assert order_1.invoice_number.startswith(organization.customer_invoice_prefix)
39423935
assert order_2.invoice_number.startswith(organization.customer_invoice_prefix)
39433936

3944-
# Each customer should start at 0001
39453937
assert order_1.invoice_number.endswith("-0001")
39463938
assert order_2.invoice_number.endswith("-0001")
39473939

3948-
# Verify invoice numbers contain customer-specific suffix
3949-
customer_1_suffix = str(customer_1.id).split("-")[0].upper()
3950-
customer_2_suffix = str(customer_2.id).split("-")[0].upper()
3951-
assert customer_1_suffix in order_1.invoice_number
3952-
assert customer_2_suffix in order_2.invoice_number
3940+
await session.refresh(customer_1)
3941+
await session.refresh(customer_2)
3942+
assert customer_1.short_id_str in order_1.invoice_number
3943+
assert customer_2.short_id_str in order_2.invoice_number
39533944

3954-
# Verify format: PREFIX-CUSTOMER_SUFFIX-0001
39553945
assert (
39563946
order_1.invoice_number
3957-
== f"{organization.customer_invoice_prefix}-{customer_1_suffix}-0001"
3947+
== f"{organization.customer_invoice_prefix}-{customer_1.short_id_str}-0001"
39583948
)
39593949
assert (
39603950
order_2.invoice_number
3961-
== f"{organization.customer_invoice_prefix}-{customer_2_suffix}-0001"
3951+
== f"{organization.customer_invoice_prefix}-{customer_2.short_id_str}-0001"
39623952
)

0 commit comments

Comments
 (0)