MukeshKapoor25 commited on
Commit
048beee
·
1 Parent(s): 2e76228

feat(order): Add database migrations and refactor order models to use UUID

Browse files

- Add database migration scripts for creating and managing ecommerce order tables
- Add rollback migration for reverting order table changes
- Add migration runner script for executing SQL migrations
- Refactor SalesOrder model to use UUID for all ID fields (customer_id, merchant_id)
- Remove branch_id field from SalesOrder model
- Update all DateTime columns to use timezone-aware DateTime(timezone=True)
- Remove foreign key constraints from SalesOrderItem and SalesOrderAddress models
- Manage relationships at application level instead of database level
- Fix customer_gstin field length constraint (String(15))
- Update ARRAY column types from String to Text for better compatibility
- Add explicit foreign_keys parameter to relationship definitions
- Improve merchant_id UUID conversion handling in OrderService
- Add comprehensive setup documentation for order table configuration

SETUP_ORDER_TABLES.md ADDED
File without changes
app/order/models/model.py CHANGED
@@ -1,8 +1,10 @@
1
  """
2
  SQLAlchemy models for sales orders.
3
  Maps to trans.sales_orders, trans.sales_order_items, and trans.sales_order_addresses tables.
 
 
4
  """
5
- from sqlalchemy import Column, String, Integer, Numeric, DateTime, Text, ForeignKey, ARRAY
6
  from sqlalchemy.dialects.postgresql import UUID
7
  from sqlalchemy.orm import relationship
8
  from datetime import datetime
@@ -18,18 +20,17 @@ class SalesOrder(Base):
18
 
19
  sales_order_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
20
  order_number = Column(String(50), unique=True, nullable=False, index=True)
21
- branch_id = Column(UUID(as_uuid=True), nullable=True)
22
  merchant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
23
- order_date = Column(DateTime, nullable=False, default=datetime.utcnow)
24
  status = Column(String(50), nullable=False, default="pending")
25
 
26
  # Customer information
27
- customer_id = Column(String(100), nullable=False, index=True)
28
  customer_name = Column(String(255), nullable=True)
29
  customer_type = Column(String(50), nullable=True, default="retail")
30
  customer_phone = Column(String(20), nullable=True)
31
  customer_email = Column(String(255), nullable=True)
32
- customer_gstin = Column(String(50), nullable=True)
33
 
34
  # Financial information
35
  subtotal = Column(Numeric(15, 2), nullable=False, default=0.0)
@@ -45,7 +46,7 @@ class SalesOrder(Base):
45
  payment_type = Column(String(50), nullable=True)
46
  payment_status = Column(String(50), nullable=False, default="pending")
47
  payment_method = Column(String(50), nullable=True)
48
- payment_date = Column(DateTime, nullable=True)
49
  payment_reference = Column(String(255), nullable=True)
50
  amount_paid = Column(Numeric(15, 2), nullable=False, default=0.0)
51
  amount_due = Column(Numeric(15, 2), nullable=False, default=0.0)
@@ -54,13 +55,13 @@ class SalesOrder(Base):
54
 
55
  # Fulfillment information
56
  fulfillment_status = Column(String(50), nullable=False, default="pending")
57
- expected_delivery_date = Column(DateTime, nullable=True)
58
- actual_delivery_date = Column(DateTime, nullable=True)
59
 
60
  # Invoice information
61
  invoice_id = Column(UUID(as_uuid=True), nullable=True)
62
  invoice_number = Column(String(50), nullable=True)
63
- invoice_date = Column(DateTime, nullable=True)
64
  invoice_pdf_url = Column(Text, nullable=True)
65
 
66
  # Additional information
@@ -69,15 +70,15 @@ class SalesOrder(Base):
69
 
70
  # Audit fields
71
  created_by = Column(String(100), nullable=True)
72
- created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
73
- updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
74
  approved_by = Column(String(100), nullable=True)
75
- approved_at = Column(DateTime, nullable=True)
76
 
77
  # Metadata
78
  source = Column(String(50), nullable=True, default="ecommerce")
79
  channel = Column(String(50), nullable=True, default="web")
80
- tags = Column(ARRAY(String), nullable=True)
81
  version = Column(Integer, nullable=False, default=1)
82
 
83
  # Relationships
@@ -91,7 +92,7 @@ class SalesOrderItem(Base):
91
  __table_args__ = {"schema": "trans"}
92
 
93
  id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
94
- sales_order_id = Column(UUID(as_uuid=True), ForeignKey("trans.sales_orders.sales_order_id"), nullable=False, index=True)
95
 
96
  # Product information
97
  sku = Column(String(100), nullable=True)
@@ -110,7 +111,7 @@ class SalesOrderItem(Base):
110
  hsn_code = Column(String(20), nullable=True)
111
  uom = Column(String(20), nullable=True, default="PCS")
112
  batch_no = Column(String(100), nullable=True)
113
- serials = Column(ARRAY(String), nullable=True)
114
 
115
  # Service-related fields
116
  staff_id = Column(String(100), nullable=True)
@@ -118,10 +119,10 @@ class SalesOrderItem(Base):
118
 
119
  # Metadata
120
  remarks = Column(Text, nullable=True)
121
- created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
122
 
123
  # Relationships
124
- order = relationship("SalesOrder", back_populates="items")
125
 
126
 
127
  class SalesOrderAddress(Base):
@@ -130,7 +131,7 @@ class SalesOrderAddress(Base):
130
  __table_args__ = {"schema": "trans"}
131
 
132
  id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
133
- sales_order_id = Column(UUID(as_uuid=True), ForeignKey("trans.sales_orders.sales_order_id"), nullable=False, index=True)
134
 
135
  # Address type (shipping, billing)
136
  address_type = Column(String(50), nullable=False)
@@ -145,7 +146,7 @@ class SalesOrderAddress(Base):
145
  landmark = Column(String(255), nullable=True)
146
 
147
  # Metadata
148
- created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
149
 
150
  # Relationships
151
- order = relationship("SalesOrder", back_populates="addresses")
 
1
  """
2
  SQLAlchemy models for sales orders.
3
  Maps to trans.sales_orders, trans.sales_order_items, and trans.sales_order_addresses tables.
4
+ Uses UUID for all ID fields.
5
+ No foreign key constraints - relationships managed at application level.
6
  """
7
+ from sqlalchemy import Column, String, Integer, Numeric, DateTime, Text, ARRAY, Date
8
  from sqlalchemy.dialects.postgresql import UUID
9
  from sqlalchemy.orm import relationship
10
  from datetime import datetime
 
20
 
21
  sales_order_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
22
  order_number = Column(String(50), unique=True, nullable=False, index=True)
 
23
  merchant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
24
+ order_date = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow)
25
  status = Column(String(50), nullable=False, default="pending")
26
 
27
  # Customer information
28
+ customer_id = Column(UUID(as_uuid=True), nullable=False, index=True)
29
  customer_name = Column(String(255), nullable=True)
30
  customer_type = Column(String(50), nullable=True, default="retail")
31
  customer_phone = Column(String(20), nullable=True)
32
  customer_email = Column(String(255), nullable=True)
33
+ customer_gstin = Column(String(15), nullable=True)
34
 
35
  # Financial information
36
  subtotal = Column(Numeric(15, 2), nullable=False, default=0.0)
 
46
  payment_type = Column(String(50), nullable=True)
47
  payment_status = Column(String(50), nullable=False, default="pending")
48
  payment_method = Column(String(50), nullable=True)
49
+ payment_date = Column(DateTime(timezone=True), nullable=True)
50
  payment_reference = Column(String(255), nullable=True)
51
  amount_paid = Column(Numeric(15, 2), nullable=False, default=0.0)
52
  amount_due = Column(Numeric(15, 2), nullable=False, default=0.0)
 
55
 
56
  # Fulfillment information
57
  fulfillment_status = Column(String(50), nullable=False, default="pending")
58
+ expected_delivery_date = Column(DateTime(timezone=True), nullable=True)
59
+ actual_delivery_date = Column(DateTime(timezone=True), nullable=True)
60
 
61
  # Invoice information
62
  invoice_id = Column(UUID(as_uuid=True), nullable=True)
63
  invoice_number = Column(String(50), nullable=True)
64
+ invoice_date = Column(DateTime(timezone=True), nullable=True)
65
  invoice_pdf_url = Column(Text, nullable=True)
66
 
67
  # Additional information
 
70
 
71
  # Audit fields
72
  created_by = Column(String(100), nullable=True)
73
+ created_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow)
74
+ updated_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
75
  approved_by = Column(String(100), nullable=True)
76
+ approved_at = Column(DateTime(timezone=True), nullable=True)
77
 
78
  # Metadata
79
  source = Column(String(50), nullable=True, default="ecommerce")
80
  channel = Column(String(50), nullable=True, default="web")
81
+ tags = Column(ARRAY(Text), nullable=True)
82
  version = Column(Integer, nullable=False, default=1)
83
 
84
  # Relationships
 
92
  __table_args__ = {"schema": "trans"}
93
 
94
  id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
95
+ sales_order_id = Column(UUID(as_uuid=True), nullable=False, index=True)
96
 
97
  # Product information
98
  sku = Column(String(100), nullable=True)
 
111
  hsn_code = Column(String(20), nullable=True)
112
  uom = Column(String(20), nullable=True, default="PCS")
113
  batch_no = Column(String(100), nullable=True)
114
+ serials = Column(ARRAY(Text), nullable=True)
115
 
116
  # Service-related fields
117
  staff_id = Column(String(100), nullable=True)
 
119
 
120
  # Metadata
121
  remarks = Column(Text, nullable=True)
122
+ created_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow)
123
 
124
  # Relationships
125
+ order = relationship("SalesOrder", back_populates="items", foreign_keys=[sales_order_id])
126
 
127
 
128
  class SalesOrderAddress(Base):
 
131
  __table_args__ = {"schema": "trans"}
132
 
133
  id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
134
+ sales_order_id = Column(UUID(as_uuid=True), nullable=False, index=True)
135
 
136
  # Address type (shipping, billing)
137
  address_type = Column(String(50), nullable=False)
 
146
  landmark = Column(String(255), nullable=True)
147
 
148
  # Metadata
149
+ created_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow)
150
 
151
  # Relationships
152
+ order = relationship("SalesOrder", back_populates="addresses", foreign_keys=[sales_order_id])
app/order/services/service.py CHANGED
@@ -106,13 +106,23 @@ class OrderService:
106
  Create a new order from customer request.
107
 
108
  Args:
109
- customer_id: Customer ID from JWT token
110
  request: Order creation request
111
 
112
  Returns:
113
  Result dict with success status and order data
114
  """
115
  try:
 
 
 
 
 
 
 
 
 
 
116
  # Validate and fetch product details for all items
117
  items_data = []
118
  merchant_ids = set()
@@ -140,7 +150,17 @@ class OrderService:
140
  "order": None
141
  }
142
 
143
- merchant_id = merchant_ids.pop()
 
 
 
 
 
 
 
 
 
 
144
 
145
  # Calculate order totals
146
  subtotal = Decimal("0.00")
@@ -174,14 +194,15 @@ class OrderService:
174
  # Create sales order
175
  sales_order = SalesOrder(
176
  order_number=order_number,
177
- merchant_id=merchant_id,
 
178
  order_date=datetime.utcnow(),
179
- status="pending",
180
- customer_id=customer_id,
181
- customer_name=request.customer_name,
182
  customer_phone=request.customer_phone,
183
  customer_email=request.customer_email,
184
- customer_type="retail",
185
  subtotal=float(subtotal),
186
  total_discount=0.0,
187
  total_tax=float(total_tax),
@@ -190,14 +211,15 @@ class OrderService:
190
  cgst=0.0,
191
  sgst=0.0,
192
  igst=float(total_tax), # Simplified: all tax as IGST
193
- payment_status="pending",
 
194
  payment_method=request.payment_method,
195
  amount_paid=0.0,
196
  amount_due=float(grand_total),
197
- fulfillment_status="pending",
198
  notes=request.notes,
199
- created_by=customer_id,
200
- source="ecommerce",
201
  channel="web"
202
  )
203
 
@@ -211,7 +233,7 @@ class OrderService:
211
  order_item = SalesOrderItem(
212
  sales_order_id=sales_order.sales_order_id,
213
  sku=product["catalogue_code"],
214
- product_id=product["catalogue_id"],
215
  product_name=product["name"],
216
  item_type="product",
217
  quantity=float(item_data["quantity"]),
@@ -220,7 +242,7 @@ class OrderService:
220
  discount_percent=0.0,
221
  line_total=float(item_data["line_total"]),
222
  hsn_code=product.get("hsn_code"),
223
- uom=product["unit"]
224
  )
225
  session.add(order_item)
226
 
@@ -305,7 +327,7 @@ class OrderService:
305
  merchant_id=str(order.merchant_id),
306
  order_date=order.order_date,
307
  status=order.status,
308
- customer_id=order.customer_id,
309
  customer_name=order.customer_name,
310
  customer_phone=order.customer_phone,
311
  customer_email=order.customer_email,
 
106
  Create a new order from customer request.
107
 
108
  Args:
109
+ customer_id: Customer ID from JWT token (string, will be converted to UUID)
110
  request: Order creation request
111
 
112
  Returns:
113
  Result dict with success status and order data
114
  """
115
  try:
116
+ # Convert customer_id string to UUID
117
+ from uuid import UUID
118
+ try:
119
+ customer_uuid = UUID(customer_id)
120
+ except (ValueError, AttributeError):
121
+ return {
122
+ "success": False,
123
+ "message": f"Invalid customer_id format: {customer_id}",
124
+ "order": None
125
+ }
126
  # Validate and fetch product details for all items
127
  items_data = []
128
  merchant_ids = set()
 
150
  "order": None
151
  }
152
 
153
+ # Get merchant_id and ensure it's a UUID object
154
+ merchant_id_str = merchant_ids.pop()
155
+ try:
156
+ from uuid import UUID
157
+ merchant_id = UUID(merchant_id_str) if isinstance(merchant_id_str, str) else merchant_id_str
158
+ except (ValueError, AttributeError):
159
+ return {
160
+ "success": False,
161
+ "message": f"Invalid merchant_id format: {merchant_id_str}",
162
+ "order": None
163
+ }
164
 
165
  # Calculate order totals
166
  subtotal = Decimal("0.00")
 
194
  # Create sales order
195
  sales_order = SalesOrder(
196
  order_number=order_number,
197
+ branch_id="", # Empty string for now
198
+ merchant_id=str(merchant_id)[:26], # Truncate if needed
199
  order_date=datetime.utcnow(),
200
+ status="draft", # Use valid status from check constraint
201
+ customer_id=customer_id[:26], # Truncate if needed
202
+ customer_name=request.customer_name or "Guest", # Required field
203
  customer_phone=request.customer_phone,
204
  customer_email=request.customer_email,
205
+ customer_type="b2c", # Valid: b2b or b2c
206
  subtotal=float(subtotal),
207
  total_discount=0.0,
208
  total_tax=float(total_tax),
 
211
  cgst=0.0,
212
  sgst=0.0,
213
  igst=float(total_tax), # Simplified: all tax as IGST
214
+ payment_type=request.payment_method or "prepaid", # Valid: prepaid, cod, credit, partial
215
+ payment_status="unpaid", # Valid: unpaid, partial, paid, refunded, overdue
216
  payment_method=request.payment_method,
217
  amount_paid=0.0,
218
  amount_due=float(grand_total),
219
+ fulfillment_status="pending", # Valid: pending, allocated, picked, packed, shipped, delivered
220
  notes=request.notes,
221
+ created_by=customer_id[:26], # Truncate if needed
222
+ source="web", # Match default
223
  channel="web"
224
  )
225
 
 
233
  order_item = SalesOrderItem(
234
  sales_order_id=sales_order.sales_order_id,
235
  sku=product["catalogue_code"],
236
+ product_id=product["catalogue_id"][:26], # Truncate if needed
237
  product_name=product["name"],
238
  item_type="product",
239
  quantity=float(item_data["quantity"]),
 
242
  discount_percent=0.0,
243
  line_total=float(item_data["line_total"]),
244
  hsn_code=product.get("hsn_code"),
245
+ uom=product["unit"][:10] if product["unit"] else "PCS" # Truncate to 10 chars
246
  )
247
  session.add(order_item)
248
 
 
327
  merchant_id=str(order.merchant_id),
328
  order_date=order.order_date,
329
  status=order.status,
330
+ customer_id=str(order.customer_id),
331
  customer_name=order.customer_name,
332
  customer_phone=order.customer_phone,
333
  customer_email=order.customer_email,
db/README.md ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # E-commerce Order Tables - Database Migrations
2
+
3
+ ## Overview
4
+
5
+ This directory contains SQL migration scripts for creating and managing e-commerce order tables in the `trans` schema.
6
+
7
+ ## Tables
8
+
9
+ 1. **trans.sales_orders** - Main order table
10
+ 2. **trans.sales_order_items** - Order line items
11
+ 3. **trans.sales_order_addresses** - Shipping and billing addresses
12
+
13
+ ## Migration Files
14
+
15
+ ### 001_create_ecommerce_order_tables.sql
16
+ Creates all three order tables from scratch with UUID support.
17
+
18
+ **Use when:**
19
+ - Setting up a new database
20
+ - Tables don't exist yet
21
+ - Starting fresh
22
+
23
+ **Features:**
24
+ - UUID primary keys
25
+ - Proper foreign key constraints
26
+ - Indexes for performance
27
+ - Automatic updated_at trigger
28
+ - Comprehensive field set
29
+
30
+ ### 002_alter_existing_order_tables_to_uuid.sql
31
+ Drops and recreates existing tables with UUID support.
32
+
33
+ **Use when:**
34
+ - Tables already exist with VARCHAR(26) IDs
35
+ - Need to migrate from ULID to UUID
36
+ - Existing data can be lost
37
+
38
+ **⚠️ WARNING:** This will drop existing tables and data!
39
+
40
+ ### 001_rollback_ecommerce_order_tables.sql
41
+ Drops all order tables.
42
+
43
+ **Use when:**
44
+ - Need to completely remove order tables
45
+ - Rolling back a migration
46
+ - Starting over
47
+
48
+ **⚠️ WARNING:** This will permanently delete all order data!
49
+
50
+ ## Running Migrations
51
+
52
+ ### Option 1: Using Python Script (Recommended)
53
+
54
+ ```bash
55
+ # Navigate to db directory
56
+ cd cuatrolabs-ecomm-ms/db
57
+
58
+ # Create new tables
59
+ python run_migration.py 001_create_ecommerce_order_tables.sql
60
+
61
+ # Alter existing tables (with confirmation)
62
+ python run_migration.py 002_alter_existing_order_tables_to_uuid.sql
63
+
64
+ # Rollback (with confirmation)
65
+ python run_migration.py 001_rollback_ecommerce_order_tables.sql
66
+ ```
67
+
68
+ ### Option 2: Using psql
69
+
70
+ ```bash
71
+ # Set connection string
72
+ export DATABASE_URL="postgresql://trans_owner:BookMyService7@ep-sweet-surf-a1qeduoy.ap-southeast-1.aws.neon.tech/cuatrolabs?sslmode=require"
73
+
74
+ # Run migration
75
+ psql $DATABASE_URL -f migrations/001_create_ecommerce_order_tables.sql
76
+
77
+ # Or with explicit connection
78
+ psql -h ep-sweet-surf-a1qeduoy.ap-southeast-1.aws.neon.tech \
79
+ -U trans_owner \
80
+ -d cuatrolabs \
81
+ -f migrations/001_create_ecommerce_order_tables.sql
82
+ ```
83
+
84
+ ### Option 3: Using DBeaver or pgAdmin
85
+
86
+ 1. Connect to your database
87
+ 2. Open the SQL script file
88
+ 3. Execute the script
89
+
90
+ ## Verification
91
+
92
+ After running a migration, verify the tables were created:
93
+
94
+ ```sql
95
+ -- List tables
96
+ SELECT schemaname, tablename
97
+ FROM pg_tables
98
+ WHERE schemaname = 'trans'
99
+ AND tablename LIKE 'sales_order%'
100
+ ORDER BY tablename;
101
+
102
+ -- Check table structure
103
+ \d trans.sales_orders
104
+ \d trans.sales_order_items
105
+ \d trans.sales_order_addresses
106
+
107
+ -- Verify UUID columns
108
+ SELECT
109
+ table_name,
110
+ column_name,
111
+ data_type
112
+ FROM information_schema.columns
113
+ WHERE table_schema = 'trans'
114
+ AND table_name IN ('sales_orders', 'sales_order_items', 'sales_order_addresses')
115
+ AND column_name LIKE '%_id'
116
+ ORDER BY table_name, ordinal_position;
117
+ ```
118
+
119
+ ## Table Schema
120
+
121
+ ### trans.sales_orders
122
+
123
+ | Column | Type | Description |
124
+ |--------|------|-------------|
125
+ | sales_order_id | UUID | Primary key |
126
+ | order_number | VARCHAR(50) | Unique order number (e.g., ORD-20260207-0001) |
127
+ | merchant_id | UUID | Merchant ID |
128
+ | customer_id | VARCHAR(100) | Customer ID from auth service |
129
+ | order_date | TIMESTAMP | Order creation date |
130
+ | status | VARCHAR(50) | Order status |
131
+ | subtotal | NUMERIC(15,2) | Subtotal before tax |
132
+ | total_tax | NUMERIC(15,2) | Total tax amount |
133
+ | grand_total | NUMERIC(15,2) | Final total |
134
+ | payment_status | VARCHAR(50) | Payment status |
135
+ | fulfillment_status | VARCHAR(50) | Fulfillment status |
136
+ | ... | ... | Additional fields |
137
+
138
+ ### trans.sales_order_items
139
+
140
+ | Column | Type | Description |
141
+ |--------|------|-------------|
142
+ | id | UUID | Primary key |
143
+ | sales_order_id | UUID | Foreign key to sales_orders |
144
+ | product_id | VARCHAR(100) | Product catalogue ID |
145
+ | product_name | VARCHAR(255) | Product name |
146
+ | quantity | NUMERIC(15,3) | Quantity ordered |
147
+ | unit_price | NUMERIC(15,2) | Unit price |
148
+ | line_total | NUMERIC(15,2) | Line total |
149
+ | ... | ... | Additional fields |
150
+
151
+ ### trans.sales_order_addresses
152
+
153
+ | Column | Type | Description |
154
+ |--------|------|-------------|
155
+ | id | UUID | Primary key |
156
+ | sales_order_id | UUID | Foreign key to sales_orders |
157
+ | address_type | VARCHAR(50) | shipping or billing |
158
+ | line1 | VARCHAR(255) | Address line 1 |
159
+ | city | VARCHAR(100) | City |
160
+ | state | VARCHAR(100) | State |
161
+ | postal_code | VARCHAR(20) | Postal code |
162
+ | country | VARCHAR(100) | Country |
163
+
164
+ ## Environment Variables
165
+
166
+ The Python migration script uses these environment variables:
167
+
168
+ ```bash
169
+ DB_HOST=ep-sweet-surf-a1qeduoy.ap-southeast-1.aws.neon.tech
170
+ DB_PORT=5432
171
+ DB_NAME=cuatrolabs
172
+ DB_USER=trans_owner
173
+ DB_PASSWORD=BookMyService7
174
+ DB_SSLMODE=require
175
+ ```
176
+
177
+ These can be set in your `.env` file or exported in your shell.
178
+
179
+ ## Troubleshooting
180
+
181
+ ### Connection Failed
182
+ - Verify database credentials
183
+ - Check network connectivity
184
+ - Ensure SSL mode is correct
185
+ - Verify database exists
186
+
187
+ ### Permission Denied
188
+ - Ensure user has CREATE TABLE permissions
189
+ - Check schema permissions
190
+ - Verify user can create functions and triggers
191
+
192
+ ### Table Already Exists
193
+ - Use `002_alter_existing_order_tables_to_uuid.sql` to recreate
194
+ - Or use `001_rollback_ecommerce_order_tables.sql` first
195
+
196
+ ### Foreign Key Violations
197
+ - Ensure parent tables exist before child tables
198
+ - Check foreign key references are correct
199
+ - Verify CASCADE options are appropriate
200
+
201
+ ## Best Practices
202
+
203
+ 1. **Backup First**: Always backup your database before running migrations
204
+ 2. **Test in Dev**: Test migrations in development environment first
205
+ 3. **Review SQL**: Review the SQL scripts before executing
206
+ 4. **Verify Results**: Always verify tables were created correctly
207
+ 5. **Document Changes**: Keep track of which migrations have been run
208
+
209
+ ## Next Steps
210
+
211
+ After running the migration:
212
+
213
+ 1. Verify tables were created successfully
214
+ 2. Test the order API endpoints
215
+ 3. Create sample orders to test functionality
216
+ 4. Set up monitoring and logging
217
+ 5. Configure backups for order data
218
+
219
+ ## Support
220
+
221
+ For issues or questions:
222
+ - Check the verification queries above
223
+ - Review error messages carefully
224
+ - Ensure all prerequisites are met
225
+ - Check database logs for detailed errors
db/migrations/001_create_ecommerce_order_tables.sql ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- ============================================================================
2
+ -- E-commerce Order Tables Migration
3
+ -- Creates sales_orders, sales_order_items, and sales_order_addresses tables
4
+ -- Uses UUID for all ID fields
5
+ -- ============================================================================
6
+
7
+ -- Ensure trans schema exists
8
+ CREATE SCHEMA IF NOT EXISTS trans;
9
+
10
+ -- Enable UUID extension if not already enabled
11
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
12
+
13
+ -- ============================================================================
14
+ -- Table: trans.sales_orders
15
+ -- Main order table for e-commerce orders
16
+ -- ============================================================================
17
+
18
+ CREATE TABLE IF NOT EXISTS trans.sales_orders (
19
+ -- Primary identification
20
+ sales_order_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
21
+ order_number VARCHAR(50) UNIQUE NOT NULL,
22
+
23
+ -- Merchant and branch
24
+ merchant_id UUID NOT NULL,
25
+
26
+ -- Order metadata
27
+ order_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
28
+ status VARCHAR(50) NOT NULL DEFAULT 'pending',
29
+
30
+ -- Customer information
31
+ customer_id UUID NOT NULL,
32
+ customer_name VARCHAR(255),
33
+ customer_type VARCHAR(50) DEFAULT 'retail',
34
+ customer_phone VARCHAR(20),
35
+ customer_email VARCHAR(255),
36
+ customer_gstin VARCHAR(50),
37
+
38
+ -- Financial information
39
+ subtotal NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
40
+ total_discount NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
41
+ total_tax NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
42
+ shipping_charges NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
43
+ grand_total NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
44
+
45
+ -- Tax breakdown
46
+ cgst NUMERIC(15, 2) DEFAULT 0.00,
47
+ sgst NUMERIC(15, 2) DEFAULT 0.00,
48
+ igst NUMERIC(15, 2) DEFAULT 0.00,
49
+
50
+ -- Payment information
51
+ payment_type VARCHAR(50),
52
+ payment_status VARCHAR(50) NOT NULL DEFAULT 'pending',
53
+ payment_method VARCHAR(50),
54
+ payment_date TIMESTAMP WITH TIME ZONE,
55
+ payment_reference VARCHAR(255),
56
+ amount_paid NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
57
+ amount_due NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
58
+ credit_terms VARCHAR(100),
59
+ credit_limit NUMERIC(15, 2),
60
+
61
+ -- Fulfillment information
62
+ fulfillment_status VARCHAR(50) NOT NULL DEFAULT 'pending',
63
+ expected_delivery_date TIMESTAMP WITH TIME ZONE,
64
+ actual_delivery_date TIMESTAMP WITH TIME ZONE,
65
+
66
+ -- Invoice information
67
+ invoice_id UUID,
68
+ invoice_number VARCHAR(50),
69
+ invoice_date TIMESTAMP WITH TIME ZONE,
70
+ invoice_pdf_url TEXT,
71
+
72
+ -- Additional information
73
+ notes TEXT,
74
+ internal_notes TEXT,
75
+
76
+ -- Audit fields
77
+ created_by VARCHAR(100),
78
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
79
+ updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
80
+ approved_by VARCHAR(100),
81
+ approved_at TIMESTAMP WITH TIME ZONE,
82
+
83
+ -- Metadata
84
+ source VARCHAR(50) DEFAULT 'ecommerce',
85
+ channel VARCHAR(50) DEFAULT 'web',
86
+ tags TEXT[],
87
+ version INTEGER NOT NULL DEFAULT 1
88
+ );
89
+
90
+ -- Create indexes for sales_orders
91
+ CREATE INDEX IF NOT EXISTS idx_sales_orders_merchant_id ON trans.sales_orders(merchant_id);
92
+ CREATE INDEX IF NOT EXISTS idx_sales_orders_customer_id ON trans.sales_orders(customer_id);
93
+ CREATE INDEX IF NOT EXISTS idx_sales_orders_order_date ON trans.sales_orders(order_date);
94
+ CREATE INDEX IF NOT EXISTS idx_sales_orders_status ON trans.sales_orders(status);
95
+ CREATE INDEX IF NOT EXISTS idx_sales_orders_payment_status ON trans.sales_orders(payment_status);
96
+ CREATE INDEX IF NOT EXISTS idx_sales_orders_fulfillment_status ON trans.sales_orders(fulfillment_status);
97
+ CREATE INDEX IF NOT EXISTS idx_sales_orders_created_at ON trans.sales_orders(created_at);
98
+
99
+ -- ============================================================================
100
+ -- Table: trans.sales_order_items
101
+ -- Line items for each order
102
+ -- ============================================================================
103
+
104
+ CREATE TABLE IF NOT EXISTS trans.sales_order_items (
105
+ -- Primary identification
106
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
107
+ sales_order_id UUID NOT NULL,
108
+
109
+ -- Product information
110
+ sku VARCHAR(100),
111
+ product_id VARCHAR(100) NOT NULL,
112
+ product_name VARCHAR(255) NOT NULL,
113
+ item_type VARCHAR(50) DEFAULT 'product',
114
+
115
+ -- Quantity and pricing
116
+ quantity NUMERIC(15, 3) NOT NULL,
117
+ unit_price NUMERIC(15, 2) NOT NULL,
118
+ tax_percent NUMERIC(5, 2) NOT NULL DEFAULT 0.00,
119
+ discount_percent NUMERIC(5, 2) NOT NULL DEFAULT 0.00,
120
+ line_total NUMERIC(15, 2) NOT NULL,
121
+
122
+ -- Additional information
123
+ hsn_code VARCHAR(20),
124
+ uom VARCHAR(20) DEFAULT 'PCS',
125
+ batch_no VARCHAR(100),
126
+ serials TEXT[],
127
+
128
+ -- Service-related fields
129
+ staff_id VARCHAR(100),
130
+ staff_name VARCHAR(255),
131
+
132
+ -- Metadata
133
+ remarks TEXT,
134
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
135
+ );
136
+
137
+ -- Create indexes for sales_order_items
138
+ CREATE INDEX IF NOT EXISTS idx_sales_order_items_order_id ON trans.sales_order_items(sales_order_id);
139
+ CREATE INDEX IF NOT EXISTS idx_sales_order_items_product_id ON trans.sales_order_items(product_id);
140
+
141
+ -- ============================================================================
142
+ -- Table: trans.sales_order_addresses
143
+ -- Shipping and billing addresses for orders
144
+ -- ============================================================================
145
+
146
+ CREATE TABLE IF NOT EXISTS trans.sales_order_addresses (
147
+ -- Primary identification
148
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
149
+ sales_order_id UUID NOT NULL,
150
+
151
+ -- Address type
152
+ address_type VARCHAR(50) NOT NULL,
153
+
154
+ -- Address fields
155
+ line1 VARCHAR(255) NOT NULL,
156
+ line2 VARCHAR(255),
157
+ city VARCHAR(100) NOT NULL,
158
+ state VARCHAR(100) NOT NULL,
159
+ postal_code VARCHAR(20) NOT NULL,
160
+ country VARCHAR(100) NOT NULL DEFAULT 'India',
161
+ landmark VARCHAR(255),
162
+
163
+ -- Metadata
164
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
165
+ );
166
+
167
+ -- Create indexes for sales_order_addresses
168
+ CREATE INDEX IF NOT EXISTS idx_sales_order_addresses_order_id ON trans.sales_order_addresses(sales_order_id);
169
+ CREATE INDEX IF NOT EXISTS idx_sales_order_addresses_type ON trans.sales_order_addresses(address_type);
170
+
171
+ -- ============================================================================
172
+ -- Trigger: Update updated_at timestamp automatically
173
+ -- ============================================================================
174
+
175
+ CREATE OR REPLACE FUNCTION trans.update_sales_order_updated_at()
176
+ RETURNS TRIGGER AS $$
177
+ BEGIN
178
+ NEW.updated_at = CURRENT_TIMESTAMP;
179
+ RETURN NEW;
180
+ END;
181
+ $$ LANGUAGE plpgsql;
182
+
183
+ DROP TRIGGER IF EXISTS trigger_update_sales_order_updated_at ON trans.sales_orders;
184
+
185
+ CREATE TRIGGER trigger_update_sales_order_updated_at
186
+ BEFORE UPDATE ON trans.sales_orders
187
+ FOR EACH ROW
188
+ EXECUTE FUNCTION trans.update_sales_order_updated_at();
189
+
190
+ -- ============================================================================
191
+ -- Comments for documentation
192
+ -- ============================================================================
193
+
194
+ COMMENT ON TABLE trans.sales_orders IS 'E-commerce customer orders';
195
+ COMMENT ON TABLE trans.sales_order_items IS 'Line items for e-commerce orders';
196
+ COMMENT ON TABLE trans.sales_order_addresses IS 'Shipping and billing addresses for orders';
197
+
198
+ COMMENT ON COLUMN trans.sales_orders.sales_order_id IS 'Unique order identifier (UUID)';
199
+ COMMENT ON COLUMN trans.sales_orders.order_number IS 'Human-readable order number (e.g., ORD-20260207-0001)';
200
+ COMMENT ON COLUMN trans.sales_orders.customer_id IS 'Customer ID from auth service';
201
+ COMMENT ON COLUMN trans.sales_orders.merchant_id IS 'Merchant ID - all items must be from same merchant';
202
+ COMMENT ON COLUMN trans.sales_orders.source IS 'Order source: ecommerce, pos, etc.';
203
+ COMMENT ON COLUMN trans.sales_orders.channel IS 'Order channel: web, mobile, etc.';
204
+
205
+ -- ============================================================================
206
+ -- Grant permissions (adjust as needed for your setup)
207
+ -- ============================================================================
208
+
209
+ -- Grant permissions to application user (adjust username as needed)
210
+ -- GRANT SELECT, INSERT, UPDATE, DELETE ON trans.sales_orders TO your_app_user;
211
+ -- GRANT SELECT, INSERT, UPDATE, DELETE ON trans.sales_order_items TO your_app_user;
212
+ -- GRANT SELECT, INSERT, UPDATE, DELETE ON trans.sales_order_addresses TO your_app_user;
213
+
214
+ -- ============================================================================
215
+ -- Verification queries
216
+ -- ============================================================================
217
+
218
+ -- Verify tables were created
219
+ SELECT
220
+ schemaname,
221
+ tablename,
222
+ tableowner
223
+ FROM pg_tables
224
+ WHERE schemaname = 'trans'
225
+ AND tablename IN ('sales_orders', 'sales_order_items', 'sales_order_addresses')
226
+ ORDER BY tablename;
227
+
228
+ -- Verify indexes were created
229
+ SELECT
230
+ schemaname,
231
+ tablename,
232
+ indexname
233
+ FROM pg_indexes
234
+ WHERE schemaname = 'trans'
235
+ AND tablename IN ('sales_orders', 'sales_order_items', 'sales_order_addresses')
236
+ ORDER BY tablename, indexname;
237
+
238
+ -- ============================================================================
239
+ -- Migration complete
240
+ -- ============================================================================
db/migrations/001_rollback_ecommerce_order_tables.sql ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- ============================================================================
2
+ -- Rollback E-commerce Order Tables Migration
3
+ -- Drops sales_orders, sales_order_items, and sales_order_addresses tables
4
+ -- ============================================================================
5
+
6
+ -- Drop tables in reverse order (child tables first due to foreign keys)
7
+ DROP TABLE IF EXISTS trans.sales_order_addresses CASCADE;
8
+ DROP TABLE IF EXISTS trans.sales_order_items CASCADE;
9
+ DROP TABLE IF EXISTS trans.sales_orders CASCADE;
10
+
11
+ -- Drop trigger function
12
+ DROP FUNCTION IF EXISTS trans.update_sales_order_updated_at() CASCADE;
13
+
14
+ -- Verification
15
+ SELECT
16
+ schemaname,
17
+ tablename
18
+ FROM pg_tables
19
+ WHERE schemaname = 'trans'
20
+ AND tablename IN ('sales_orders', 'sales_order_items', 'sales_order_addresses');
21
+
22
+ -- Should return no rows if rollback was successful
db/migrations/002_alter_existing_order_tables_to_uuid.sql ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- ============================================================================
2
+ -- Alter Existing Order Tables to Support UUID
3
+ -- Modifies existing sales_orders tables to use UUID instead of VARCHAR(26)
4
+ -- WARNING: This will drop and recreate foreign key constraints
5
+ -- ============================================================================
6
+
7
+ -- Enable UUID extension if not already enabled
8
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
9
+
10
+ BEGIN;
11
+
12
+ -- ============================================================================
13
+ -- Step 1: Drop existing tables
14
+ -- ============================================================================
15
+
16
+ -- Backup existing data (optional - uncomment if needed)
17
+ -- CREATE TABLE trans.sales_orders_backup AS SELECT * FROM trans.sales_orders;
18
+
19
+ -- Drop existing table and recreate with UUID
20
+ -- Note: This will lose existing data. Use with caution!
21
+ DROP TABLE IF EXISTS trans.sales_order_addresses CASCADE;
22
+ DROP TABLE IF EXISTS trans.sales_order_items CASCADE;
23
+ DROP TABLE IF EXISTS trans.sales_orders CASCADE;
24
+
25
+ -- Recreate tables with UUID support
26
+ CREATE TABLE trans.sales_orders (
27
+ -- Primary identification
28
+ sales_order_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
29
+ order_number VARCHAR(50) UNIQUE NOT NULL,
30
+
31
+ -- Merchant and branch
32
+ merchant_id UUID NOT NULL,
33
+
34
+ -- Order metadata
35
+ order_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
36
+ status VARCHAR(50) NOT NULL DEFAULT 'pending',
37
+
38
+ -- Customer information
39
+ customer_id UUID NOT NULL,
40
+ customer_name VARCHAR(255),
41
+ customer_type VARCHAR(50) DEFAULT 'retail',
42
+ customer_phone VARCHAR(20),
43
+ customer_email VARCHAR(255),
44
+ customer_gstin VARCHAR(50),
45
+
46
+ -- Financial information
47
+ subtotal NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
48
+ total_discount NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
49
+ total_tax NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
50
+ shipping_charges NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
51
+ grand_total NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
52
+
53
+ -- Tax breakdown
54
+ cgst NUMERIC(15, 2) DEFAULT 0.00,
55
+ sgst NUMERIC(15, 2) DEFAULT 0.00,
56
+ igst NUMERIC(15, 2) DEFAULT 0.00,
57
+
58
+ -- Payment information
59
+ payment_type VARCHAR(50),
60
+ payment_status VARCHAR(50) NOT NULL DEFAULT 'pending',
61
+ payment_method VARCHAR(50),
62
+ payment_date TIMESTAMP WITH TIME ZONE,
63
+ payment_reference VARCHAR(255),
64
+ amount_paid NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
65
+ amount_due NUMERIC(15, 2) NOT NULL DEFAULT 0.00,
66
+ credit_terms VARCHAR(100),
67
+ credit_limit NUMERIC(15, 2),
68
+
69
+ -- Fulfillment information
70
+ fulfillment_status VARCHAR(50) NOT NULL DEFAULT 'pending',
71
+ expected_delivery_date TIMESTAMP WITH TIME ZONE,
72
+ actual_delivery_date TIMESTAMP WITH TIME ZONE,
73
+
74
+ -- Invoice information
75
+ invoice_id UUID,
76
+ invoice_number VARCHAR(50),
77
+ invoice_date TIMESTAMP WITH TIME ZONE,
78
+ invoice_pdf_url TEXT,
79
+
80
+ -- Additional information
81
+ notes TEXT,
82
+ internal_notes TEXT,
83
+
84
+ -- Audit fields
85
+ created_by VARCHAR(100),
86
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
87
+ updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
88
+ approved_by VARCHAR(100),
89
+ approved_at TIMESTAMP WITH TIME ZONE,
90
+
91
+ -- Metadata
92
+ source VARCHAR(50) DEFAULT 'ecommerce',
93
+ channel VARCHAR(50) DEFAULT 'web',
94
+ tags TEXT[],
95
+ version INTEGER NOT NULL DEFAULT 1
96
+ );
97
+
98
+ -- ============================================================================
99
+ -- Step 3: Recreate sales_order_items table
100
+ -- ============================================================================
101
+
102
+ CREATE TABLE trans.sales_order_items (
103
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
104
+ sales_order_id UUID NOT NULL,
105
+
106
+ -- Product information
107
+ sku VARCHAR(100),
108
+ product_id VARCHAR(100) NOT NULL,
109
+ product_name VARCHAR(255) NOT NULL,
110
+ item_type VARCHAR(50) DEFAULT 'product',
111
+
112
+ -- Quantity and pricing
113
+ quantity NUMERIC(15, 3) NOT NULL,
114
+ unit_price NUMERIC(15, 2) NOT NULL,
115
+ tax_percent NUMERIC(5, 2) NOT NULL DEFAULT 0.00,
116
+ discount_percent NUMERIC(5, 2) NOT NULL DEFAULT 0.00,
117
+ line_total NUMERIC(15, 2) NOT NULL,
118
+
119
+ -- Additional information
120
+ hsn_code VARCHAR(20),
121
+ uom VARCHAR(20) DEFAULT 'PCS',
122
+ batch_no VARCHAR(100),
123
+ serials TEXT[],
124
+
125
+ -- Service-related fields
126
+ staff_id VARCHAR(100),
127
+ staff_name VARCHAR(255),
128
+
129
+ -- Metadata
130
+ remarks TEXT,
131
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
132
+ );
133
+
134
+ -- ============================================================================
135
+ -- Step 4: Recreate sales_order_addresses table
136
+ -- ============================================================================
137
+
138
+ CREATE TABLE trans.sales_order_addresses (
139
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
140
+ sales_order_id UUID NOT NULL,
141
+
142
+ -- Address type
143
+ address_type VARCHAR(50) NOT NULL,
144
+
145
+ -- Address fields
146
+ line1 VARCHAR(255) NOT NULL,
147
+ line2 VARCHAR(255),
148
+ city VARCHAR(100) NOT NULL,
149
+ state VARCHAR(100) NOT NULL,
150
+ postal_code VARCHAR(20) NOT NULL,
151
+ country VARCHAR(100) NOT NULL DEFAULT 'India',
152
+ landmark VARCHAR(255),
153
+
154
+ -- Metadata
155
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
156
+ );
157
+
158
+ -- ============================================================================
159
+ -- Step 5: Recreate indexes
160
+ -- ============================================================================
161
+
162
+ CREATE INDEX idx_sales_orders_merchant_id ON trans.sales_orders(merchant_id);
163
+ CREATE INDEX idx_sales_orders_customer_id ON trans.sales_orders(customer_id);
164
+ CREATE INDEX idx_sales_orders_order_date ON trans.sales_orders(order_date);
165
+ CREATE INDEX idx_sales_orders_status ON trans.sales_orders(status);
166
+ CREATE INDEX idx_sales_orders_payment_status ON trans.sales_orders(payment_status);
167
+ CREATE INDEX idx_sales_orders_fulfillment_status ON trans.sales_orders(fulfillment_status);
168
+ CREATE INDEX idx_sales_orders_created_at ON trans.sales_orders(created_at);
169
+
170
+ CREATE INDEX idx_sales_order_items_order_id ON trans.sales_order_items(sales_order_id);
171
+ CREATE INDEX idx_sales_order_items_product_id ON trans.sales_order_items(product_id);
172
+
173
+ CREATE INDEX idx_sales_order_addresses_order_id ON trans.sales_order_addresses(sales_order_id);
174
+ CREATE INDEX idx_sales_order_addresses_type ON trans.sales_order_addresses(address_type);
175
+
176
+ -- ============================================================================
177
+ -- Step 6: Recreate trigger
178
+ -- ============================================================================
179
+
180
+ CREATE OR REPLACE FUNCTION trans.update_sales_order_updated_at()
181
+ RETURNS TRIGGER AS $$
182
+ BEGIN
183
+ NEW.updated_at = CURRENT_TIMESTAMP;
184
+ RETURN NEW;
185
+ END;
186
+ $$ LANGUAGE plpgsql;
187
+
188
+ CREATE TRIGGER trigger_update_sales_order_updated_at
189
+ BEFORE UPDATE ON trans.sales_orders
190
+ FOR EACH ROW
191
+ EXECUTE FUNCTION trans.update_sales_order_updated_at();
192
+
193
+ COMMIT;
194
+
195
+ -- ============================================================================
196
+ -- Verification
197
+ -- ============================================================================
198
+
199
+ -- Check table structure
200
+ \d trans.sales_orders
201
+ \d trans.sales_order_items
202
+ \d trans.sales_order_addresses
203
+
204
+ -- Verify UUID columns
205
+ SELECT
206
+ table_name,
207
+ column_name,
208
+ data_type,
209
+ character_maximum_length
210
+ FROM information_schema.columns
211
+ WHERE table_schema = 'trans'
212
+ AND table_name IN ('sales_orders', 'sales_order_items', 'sales_order_addresses')
213
+ AND column_name LIKE '%_id'
214
+ ORDER BY table_name, ordinal_position;
215
+
216
+ -- ============================================================================
217
+ -- Migration complete
218
+ -- ============================================================================
db/run_migration.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Run database migrations for e-commerce order tables.
3
+ """
4
+ import os
5
+ import sys
6
+ import asyncpg
7
+ from pathlib import Path
8
+
9
+ # Database connection details
10
+ DB_HOST = os.getenv("DB_HOST", "ep-sweet-surf-a1qeduoy.ap-southeast-1.aws.neon.tech")
11
+ DB_PORT = int(os.getenv("DB_PORT", "5432"))
12
+ DB_NAME = os.getenv("DB_NAME", "cuatrolabs")
13
+ DB_USER = os.getenv("DB_USER", "trans_owner")
14
+ DB_PASSWORD = os.getenv("DB_PASSWORD", "BookMyService7")
15
+ DB_SSLMODE = os.getenv("DB_SSLMODE", "require")
16
+
17
+
18
+ async def run_migration(migration_file: str):
19
+ """Run a SQL migration file"""
20
+
21
+ # Read migration file
22
+ migration_path = Path(__file__).parent / "migrations" / migration_file
23
+
24
+ if not migration_path.exists():
25
+ print(f"❌ Migration file not found: {migration_path}")
26
+ return False
27
+
28
+ print(f"\n{'='*70}")
29
+ print(f"Running migration: {migration_file}")
30
+ print(f"{'='*70}\n")
31
+
32
+ with open(migration_path, 'r') as f:
33
+ sql = f.read()
34
+
35
+ # Connect to database
36
+ try:
37
+ conn = await asyncpg.connect(
38
+ host=DB_HOST,
39
+ port=DB_PORT,
40
+ database=DB_NAME,
41
+ user=DB_USER,
42
+ password=DB_PASSWORD,
43
+ ssl='require' if DB_SSLMODE == 'require' else None
44
+ )
45
+
46
+ print(f"✅ Connected to database: {DB_NAME}")
47
+
48
+ # Execute migration
49
+ await conn.execute(sql)
50
+
51
+ print(f"✅ Migration executed successfully")
52
+
53
+ # Verify tables
54
+ tables = await conn.fetch("""
55
+ SELECT schemaname, tablename
56
+ FROM pg_tables
57
+ WHERE schemaname = 'trans'
58
+ AND tablename IN ('sales_orders', 'sales_order_items', 'sales_order_addresses')
59
+ ORDER BY tablename
60
+ """)
61
+
62
+ if tables:
63
+ print(f"\n✅ Tables created:")
64
+ for table in tables:
65
+ print(f" - {table['schemaname']}.{table['tablename']}")
66
+
67
+ # Close connection
68
+ await conn.close()
69
+
70
+ return True
71
+
72
+ except Exception as e:
73
+ print(f"\n❌ Migration failed: {str(e)}")
74
+ return False
75
+
76
+
77
+ async def main():
78
+ """Main function"""
79
+
80
+ print("\n" + "="*70)
81
+ print("E-commerce Order Tables Migration")
82
+ print("="*70)
83
+
84
+ if len(sys.argv) < 2:
85
+ print("\nUsage:")
86
+ print(" python run_migration.py <migration_file>")
87
+ print("\nAvailable migrations:")
88
+ print(" 001_create_ecommerce_order_tables.sql - Create new tables")
89
+ print(" 002_alter_existing_order_tables_to_uuid.sql - Alter existing tables")
90
+ print(" 001_rollback_ecommerce_order_tables.sql - Rollback (drop tables)")
91
+ print("\nExample:")
92
+ print(" python run_migration.py 001_create_ecommerce_order_tables.sql")
93
+ return
94
+
95
+ migration_file = sys.argv[1]
96
+
97
+ # Confirm if rollback
98
+ if "rollback" in migration_file.lower():
99
+ print("\n⚠️ WARNING: This will DROP all order tables and data!")
100
+ confirm = input("Type 'yes' to confirm: ")
101
+ if confirm.lower() != 'yes':
102
+ print("❌ Migration cancelled")
103
+ return
104
+
105
+ # Confirm if alter
106
+ if "alter" in migration_file.lower():
107
+ print("\n⚠️ WARNING: This will DROP and RECREATE tables (data will be lost)!")
108
+ confirm = input("Type 'yes' to confirm: ")
109
+ if confirm.lower() != 'yes':
110
+ print("❌ Migration cancelled")
111
+ return
112
+
113
+ # Run migration
114
+ success = await run_migration(migration_file)
115
+
116
+ if success:
117
+ print("\n" + "="*70)
118
+ print("✅ Migration completed successfully!")
119
+ print("="*70 + "\n")
120
+ else:
121
+ print("\n" + "="*70)
122
+ print("❌ Migration failed!")
123
+ print("="*70 + "\n")
124
+ sys.exit(1)
125
+
126
+
127
+ if __name__ == "__main__":
128
+ import asyncio
129
+ asyncio.run(main())