feat: add customer multiple addresses management
- Add CustomerAddressService with CRUD operations - Add shop API endpoints for address management (GET, POST, PUT, DELETE) - Add set default endpoint for address type - Implement addresses.html with full UI (cards, modals, Alpine.js) - Integrate saved addresses in checkout flow - Address selector dropdowns for shipping/billing - Auto-select default addresses - Save new address checkbox option - Add country_iso field alongside country_name - Add address exceptions (NotFound, LimitExceeded, InvalidType) - Max 10 addresses per customer limit - One default address per type (shipping/billing) - Add unit tests for CustomerAddressService - Add integration tests for shop addresses API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
141
alembic/versions/r6f7a8b9c0d1_add_country_iso_to_addresses.py
Normal file
141
alembic/versions/r6f7a8b9c0d1_add_country_iso_to_addresses.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""Add country_iso to customer_addresses
|
||||
|
||||
Revision ID: r6f7a8b9c0d1
|
||||
Revises: q5e6f7a8b9c0
|
||||
Create Date: 2026-01-02
|
||||
|
||||
Adds country_iso field to customer_addresses table and renames
|
||||
country to country_name for clarity.
|
||||
|
||||
This migration is idempotent - it checks for existing columns before
|
||||
making changes.
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "r6f7a8b9c0d1"
|
||||
down_revision = "q5e6f7a8b9c0"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
# Country name to ISO code mapping for backfill
|
||||
COUNTRY_ISO_MAP = {
|
||||
"Luxembourg": "LU",
|
||||
"Germany": "DE",
|
||||
"France": "FR",
|
||||
"Belgium": "BE",
|
||||
"Netherlands": "NL",
|
||||
"Austria": "AT",
|
||||
"Italy": "IT",
|
||||
"Spain": "ES",
|
||||
"Portugal": "PT",
|
||||
"Poland": "PL",
|
||||
"Czech Republic": "CZ",
|
||||
"Czechia": "CZ",
|
||||
"Slovakia": "SK",
|
||||
"Hungary": "HU",
|
||||
"Romania": "RO",
|
||||
"Bulgaria": "BG",
|
||||
"Greece": "GR",
|
||||
"Croatia": "HR",
|
||||
"Slovenia": "SI",
|
||||
"Estonia": "EE",
|
||||
"Latvia": "LV",
|
||||
"Lithuania": "LT",
|
||||
"Finland": "FI",
|
||||
"Sweden": "SE",
|
||||
"Denmark": "DK",
|
||||
"Ireland": "IE",
|
||||
"Cyprus": "CY",
|
||||
"Malta": "MT",
|
||||
"United Kingdom": "GB",
|
||||
"Switzerland": "CH",
|
||||
"United States": "US",
|
||||
}
|
||||
|
||||
|
||||
def get_column_names(connection, table_name):
|
||||
"""Get list of column names for a table."""
|
||||
result = connection.execute(sa.text(f"PRAGMA table_info({table_name})"))
|
||||
return [row[1] for row in result]
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
connection = op.get_bind()
|
||||
columns = get_column_names(connection, "customer_addresses")
|
||||
|
||||
# Check if we need to do anything (idempotent check)
|
||||
has_country = "country" in columns
|
||||
has_country_name = "country_name" in columns
|
||||
has_country_iso = "country_iso" in columns
|
||||
|
||||
# If already has new columns, nothing to do
|
||||
if has_country_name and has_country_iso:
|
||||
print(" Columns country_name and country_iso already exist, skipping")
|
||||
return
|
||||
|
||||
# If has old 'country' column, rename it and add country_iso
|
||||
if has_country and not has_country_name:
|
||||
with op.batch_alter_table("customer_addresses") as batch_op:
|
||||
batch_op.alter_column(
|
||||
"country",
|
||||
new_column_name="country_name",
|
||||
)
|
||||
|
||||
# Add country_iso if it doesn't exist
|
||||
if not has_country_iso:
|
||||
with op.batch_alter_table("customer_addresses") as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("country_iso", sa.String(5), nullable=True)
|
||||
)
|
||||
|
||||
# Backfill country_iso from country_name
|
||||
for country_name, iso_code in COUNTRY_ISO_MAP.items():
|
||||
connection.execute(
|
||||
sa.text(
|
||||
"UPDATE customer_addresses SET country_iso = :iso "
|
||||
"WHERE country_name = :name"
|
||||
),
|
||||
{"iso": iso_code, "name": country_name},
|
||||
)
|
||||
|
||||
# Set default for any remaining NULL values
|
||||
connection.execute(
|
||||
sa.text(
|
||||
"UPDATE customer_addresses SET country_iso = 'LU' "
|
||||
"WHERE country_iso IS NULL"
|
||||
)
|
||||
)
|
||||
|
||||
# Make country_iso NOT NULL using batch operation
|
||||
with op.batch_alter_table("customer_addresses") as batch_op:
|
||||
batch_op.alter_column(
|
||||
"country_iso",
|
||||
existing_type=sa.String(5),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
connection = op.get_bind()
|
||||
columns = get_column_names(connection, "customer_addresses")
|
||||
|
||||
has_country_name = "country_name" in columns
|
||||
has_country_iso = "country_iso" in columns
|
||||
has_country = "country" in columns
|
||||
|
||||
# Only downgrade if in the new state
|
||||
if has_country_name and not has_country:
|
||||
with op.batch_alter_table("customer_addresses") as batch_op:
|
||||
batch_op.alter_column(
|
||||
"country_name",
|
||||
new_column_name="country",
|
||||
)
|
||||
|
||||
if has_country_iso:
|
||||
with op.batch_alter_table("customer_addresses") as batch_op:
|
||||
batch_op.drop_column("country_iso")
|
||||
Reference in New Issue
Block a user