Uplink 3.0: Custom Store Migration Plan¶
Goal: Replace PrestaShop with a custom Django-based store (Connected Things) integrated directly into Uplink
Approach: Phased migration starting with customer logins and custom pricing, then full store replacement
Prerequisites: Uplink 2.0 upgrade complete (Django 5.x, Python 3.12, containerized, production-ready)
Table of Contents¶
- Current State Analysis
- Target Architecture
- Migration Strategy
- Phase 1: Customer Portal (Custom Pricing & Login)
- Phase 2: Product Catalog & Browse
- Phase 3: Shopping Cart & Checkout
- Phase 4: Payment Integration
- Phase 5: PrestaShop Data Migration
- Phase 6: PrestaShop Deprecation
- Technical Considerations
- Risk Mitigation
- Timeline & Resources
Current State Analysis¶
PrestaShop Integration¶
What PrestaShop Currently Does: - Public-facing e-commerce store (Connected Things) - Product catalog display - Shopping cart & checkout - Customer accounts (email/password login) - Order processing - Payment gateway integration - Stock level synchronization from Uplink - Order import into Uplink for fulfillment
Current Architecture:
┌──────────────────────────────────────────────────────────────┐
│ CURRENT ARCHITECTURE │
└──────────────────────────────────────────────────────────────┘
Customer PrestaShop Uplink
│ │ │
│ Browse Products │ │
├───────────────────────>│ │
│ │ │
│ Add to Cart │ │
├───────────────────────>│ │
│ │ │
│ Checkout & Pay │ │
├───────────────────────>│ │
│ │ │
│ │ Sync Stock Levels │
│ │<──────────────────────────│
│ │ (Huey periodic task) │
│ │ │
│ │ Import Orders │
│ │───────────────────────────>│
│ │ (Management command) │
│ │ │
│ Order Confirmation │ │
│<───────────────────────│ │
│ │ │
│ │ │
│ [Fulfillment] │
│ │ Pick & Ship │
│ │<──────────────────────────│
│ │ │
│ Shipping Notification│ │
│<───────────────────────┼───────────────────────────│
Uplink's PrestaShop Integration Code¶
Key Files:
- prestashop/api.py - API client for PrestaShop REST API
- prestashop/models.py - PrestashopProduct, PrestashopCombination, PrestashopCustomerContactMap
- prestashop/operations.py - Product/stock sync operations
- prestashop/tasks.py - Periodic sync tasks (Huey)
- prestashop/management/commands/prestashop_import_orders.py - Order import
- prestashop/management/commands/prestashop_import_products.py - Product import
Data Flow: 1. Stock Sync (Uplink → PrestaShop): - Huey periodic task runs every minute - Checks for stock changes in Uplink - Updates PrestaShop via REST API - Syncs for both UK and EU stores
- Order Import (PrestaShop → Uplink):
- Management command runs periodically
- Fetches orders with specific statuses
- Creates Order records in Uplink
- Maps PrestaShop customer IDs to Uplink Contacts
-
Imports order items, addresses, shipping info
-
Product Sync (Uplink → PrestaShop):
- Triggered when products updated in Uplink
- Sends product data, pricing, descriptions
- Updates both simple products and combinations (variants)
Pain Points with PrestaShop¶
Technical Debt: - Maintaining two separate systems (PrestaShop + Uplink) - Complex synchronization logic prone to errors - Stock discrepancies between systems - Duplicate customer data - No single source of truth for orders - Limited customization without PrestaShop module development - PHP codebase separate from Python stack
Business Limitations: - No customer-specific pricing - Cannot show negotiated prices before login - No customer portal - Customers can't see order history integrated with Uplink - No account management - Can't manage custom pricing tiers - No quote-to-order workflow - No RFQ system for B2B customers - Generic e-commerce - Not tailored for IoT hardware sales - Limited bulk ordering - No CSV upload or quick reorder
Operational Issues: - Order import delays (periodic sync, not real-time) - Stock sync issues cause overselling - Customer service checks two systems - Complex refund/return workflows - Difficult to add custom fields (LoRaWAN config, device provisioning)
Target Architecture¶
Vision: Unified Uplink System¶
┌──────────────────────────────────────────────────────────────┐
│ TARGET ARCHITECTURE │
└──────────────────────────────────────────────────────────────┘
Customer Uplink (Connected Things)
│ │
│ Login (Django Auth) │
├─────────────────────────────>│
│ │
│ Browse Products │
│ (with custom pricing) │
├─────────────────────────────>│
│ │
│ Add to Cart │
├─────────────────────────────>│
│ │
│ Checkout & Pay │
├─────────────────────────────>│
│ (integrated payment) │
│ │
│ Order Created │
│ (real-time, no sync) │
│<─────────────────────────────│
│ │
│ View Order History │
├─────────────────────────────>│
│ (integrated with Uplink) │
│ │
│ Track Shipment │
│ (real-time status) │
├─────────────────────────────>│
│ │
│ Download Invoices │
├─────────────────────────────>│
Key Features¶
Customer Portal (Phase 1): - Django-based authentication (email/password, social auth optional) - Customer-specific pricing (price lists per customer) - Order history view - Invoice downloads - Account management
Product Catalog (Phase 2): - Product browsing with search/filters - Category navigation - Product detail pages - Stock availability (real-time) - Custom product attributes (LoRaWAN band, battery type, etc.) - Product bundling
Shopping Cart (Phase 3): - Session-based cart for anonymous users - Persistent cart for logged-in users - Quantity updates - Remove items - Apply discounts/coupons - Shipping calculator
Checkout & Payment (Phase 4): - Guest checkout option - Saved addresses for customers - Multiple payment methods (Stripe, PayPal, bank transfer) - Order confirmation emails - Real-time order creation (no sync delay)
Advanced Features (Future): - Quote requests (RFQ) - Bulk ordering (CSV upload) - Custom device configuration during checkout - Subscription/recurring orders - Customer groups with tiered pricing
Migration Strategy¶
Principles¶
- Incremental Migration - Build alongside PrestaShop, switch gradually
- No Big Bang - Phase 1 can coexist with PrestaShop
- Data Preservation - Import all historical orders and customers
- Zero Downtime - DNS switch when ready, rollback possible
- Customer Communication - Advance notice, migration guides
Phased Approach¶
Phase 1: Customer Portal (3-4 months) - Build customer login system - Implement customer-specific pricing - Create order history view - PrestaShop still active for public purchases - Favorite customers get early access
Phase 2: Product Catalog (2-3 months) - Build product listing pages - Category navigation - Search & filters - Product detail pages - PrestaShop still handles checkout
Phase 3: Shopping Cart & Checkout (2-3 months) - Shopping cart functionality - Checkout flow - Address management - Still no payment, orders created as "pending payment"
Phase 4: Payment Integration (1-2 months) - Integrate Stripe/PayPal - Complete checkout flow - Soft launch with beta customers
Phase 5: Data Migration (1 month) - Import all PrestaShop customers - Import historical orders - Import product reviews (if any) - Parallel running: Uplink store + PrestaShop
Phase 6: PrestaShop Deprecation (1 month) - Redirect PrestaShop to Uplink - Monitor for issues - Keep PrestaShop read-only for 6 months - Eventually decommission
Total Timeline: 10-14 months
Phase 1: Customer Portal (Custom Pricing & Login)¶
Goal: Enable favorite customers to log in, see their custom prices, and view order history
Duration: 3-4 months
Prerequisites: Uplink 2.0 complete
1.1 User Authentication¶
1.1.1 Extend Django User Model¶
Create customer profile model:
# customers/models.py
from django.contrib.auth.models import User
from django.db import models
from contacts.models import Contact
class CustomerProfile(models.Model):
"""
Extends Django User for e-commerce customers
Links to existing Contact model for business customers
"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='customer_profile')
contact = models.ForeignKey(Contact, on_delete=models.SET_NULL, null=True, blank=True)
# Customer tier for pricing
price_list = models.ForeignKey('PriceList', on_delete=models.PROTECT, null=True, blank=True)
# Account status
is_verified = models.BooleanField(default=False)
is_approved = models.BooleanField(default=False) # Admin approval for B2B
# Preferences
preferred_currency = models.CharField(max_length=3, default='GBP')
preferred_shipping_address = models.ForeignKey('Address', on_delete=models.SET_NULL, null=True, blank=True)
# Marketing
newsletter_subscribed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.user.email} - {self.contact or 'Individual'}"
1.1.2 Registration & Login Views¶
# customers/views.py
from django.contrib.auth import login, authenticate
from django.contrib.auth.forms import AuthenticationForm
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .forms import CustomerRegistrationForm
def register(request):
"""Customer registration"""
if request.method == 'POST':
form = CustomerRegistrationForm(request.POST)
if form.is_valid():
user = form.save(commit=False)
user.is_active = True # Or False if email verification required
user.save()
# Create customer profile
CustomerProfile.objects.create(
user=user,
newsletter_subscribed=form.cleaned_data.get('newsletter', False)
)
# Send verification email (optional)
send_verification_email(user)
# Auto-login after registration
login(request, user)
return redirect('customer_dashboard')
else:
form = CustomerRegistrationForm()
return render(request, 'customers/register.html', {'form': form})
def customer_login(request):
"""Customer login"""
if request.method == 'POST':
form = AuthenticationForm(request, data=request.POST)
if form.is_valid():
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password')
user = authenticate(username=username, password=password)
if user is not None:
login(request, user)
return redirect('customer_dashboard')
else:
form = AuthenticationForm()
return render(request, 'customers/login.html', {'form': form})
@login_required
def dashboard(request):
"""Customer dashboard"""
profile = request.user.customer_profile
recent_orders = Order.objects.filter(contact=profile.contact).order_by('-ordered_at')[:5]
context = {
'profile': profile,
'recent_orders': recent_orders,
}
return render(request, 'customers/dashboard.html', context)
1.2 Customer-Specific Pricing¶
1.2.1 Price List Model¶
# catalogue/models.py (extend existing)
class PriceList(models.Model):
"""
Price lists for customer tiers
Examples: Retail, Wholesale, VIP, Distributor
"""
name = models.CharField(max_length=255)
code = models.CharField(max_length=50, unique=True)
description = models.TextField(blank=True)
# Discount type
discount_type = models.CharField(
max_length=20,
choices=[
('PERCENTAGE', 'Percentage Discount'),
('FIXED', 'Fixed Amount Discount'),
('CUSTOM', 'Custom Prices Per Product'),
],
default='PERCENTAGE'
)
# Global discount (if applicable)
discount_percentage = models.DecimalField(
max_digits=5, decimal_places=2, null=True, blank=True,
help_text="e.g., 10.00 for 10% off"
)
discount_amount = MoneyField(
max_digits=10, default_currency='GBP', null=True, blank=True
)
# Priority (lower number = higher priority)
priority = models.IntegerField(default=0)
is_active = models.BooleanField(default=True)
valid_from = models.DateField(null=True, blank=True)
valid_until = models.DateField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.name} ({self.code})"
class Meta:
ordering = ['priority', 'name']
class PriceListItem(models.Model):
"""
Custom prices for specific products in a price list
"""
price_list = models.ForeignKey(PriceList, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey('Product', on_delete=models.CASCADE)
# Custom price for this product
price_excluding_vat = MoneyField(max_digits=10, default_currency='GBP')
price_including_vat = MoneyField(max_digits=10, default_currency='GBP')
# Minimum order quantity (optional)
min_quantity = models.IntegerField(default=1)
is_active = models.BooleanField(default=True)
class Meta:
unique_together = ['price_list', 'product']
def __str__(self):
return f"{self.price_list.code} - {self.product.sku}"
1.2.2 Pricing Logic¶
# catalogue/utils.py
def get_customer_price(product, user=None, quantity=1):
"""
Get the price for a product based on customer's price list
Args:
product: Product instance
user: User instance (optional, for logged-in customers)
quantity: Order quantity (for tiered pricing)
Returns:
dict: {
'price_excl_vat': Decimal,
'price_incl_vat': Decimal,
'original_price': Decimal,
'discount_applied': bool,
'price_list_name': str or None
}
"""
# Default to standard retail price
original_price_excl = product.price_excluding_vat
original_price_incl = product.price_including_vat
if not user or not user.is_authenticated:
# Anonymous user - use retail price
return {
'price_excl_vat': original_price_excl,
'price_incl_vat': original_price_incl,
'original_price': original_price_excl,
'discount_applied': False,
'price_list_name': None,
}
# Get customer's price list
try:
profile = user.customer_profile
price_list = profile.price_list
except (CustomerProfile.DoesNotExist, AttributeError):
price_list = None
if not price_list or not price_list.is_active:
# No price list or inactive - use retail price
return {
'price_excl_vat': original_price_excl,
'price_incl_vat': original_price_incl,
'original_price': original_price_excl,
'discount_applied': False,
'price_list_name': None,
}
# Check for custom price for this product
custom_price = PriceListItem.objects.filter(
price_list=price_list,
product=product,
is_active=True,
min_quantity__lte=quantity
).order_by('-min_quantity').first()
if custom_price:
return {
'price_excl_vat': custom_price.price_excluding_vat,
'price_incl_vat': custom_price.price_including_vat,
'original_price': original_price_excl,
'discount_applied': True,
'price_list_name': price_list.name,
}
# Apply global discount if applicable
if price_list.discount_type == 'PERCENTAGE' and price_list.discount_percentage:
discount_multiplier = (100 - price_list.discount_percentage) / 100
discounted_price_excl = original_price_excl * discount_multiplier
discounted_price_incl = original_price_incl * discount_multiplier
return {
'price_excl_vat': discounted_price_excl,
'price_incl_vat': discounted_price_incl,
'original_price': original_price_excl,
'discount_applied': True,
'price_list_name': price_list.name,
}
# No custom pricing or discount
return {
'price_excl_vat': original_price_excl,
'price_incl_vat': original_price_incl,
'original_price': original_price_excl,
'discount_applied': False,
'price_list_name': price_list.name,
}
1.3 Order History View¶
# customers/views.py
@login_required
def order_history(request):
"""Display customer's order history"""
profile = request.user.customer_profile
# Get orders for this customer
orders = Order.objects.none()
if profile.contact:
orders = Order.objects.filter(contact=profile.contact)
# Filtering
status_filter = request.GET.get('status')
if status_filter:
orders = orders.filter(status=status_filter)
# Pagination
from django.core.paginator import Paginator
paginator = Paginator(orders.order_by('-ordered_at'), 20)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'orders': page_obj,
'status_choices': OrderStatus.choices,
}
return render(request, 'customers/order_history.html', context)
@login_required
def order_detail(request, order_id):
"""Display single order details"""
profile = request.user.customer_profile
# Security: Only show orders for this customer
order = get_object_or_404(
Order,
id=order_id,
contact=profile.contact
)
context = {
'order': order,
}
return render(request, 'customers/order_detail.html', context)
@login_required
def download_invoice(request, invoice_id):
"""Download invoice as PDF"""
profile = request.user.customer_profile
invoice = get_object_or_404(
Invoice,
id=invoice_id,
order__contact=profile.contact
)
# Generate PDF (using reportlab or weasyprint)
from django.http import HttpResponse
from .utils import generate_invoice_pdf
pdf = generate_invoice_pdf(invoice)
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="invoice_{invoice.reference}.pdf"'
return response
1.4 Admin Interface¶
# customers/admin.py
from django.contrib import admin
from .models import CustomerProfile, PriceList, PriceListItem
@admin.register(CustomerProfile)
class CustomerProfileAdmin(admin.ModelAdmin):
list_display = ('user', 'contact', 'price_list', 'is_approved', 'created_at')
list_filter = ('is_approved', 'is_verified', 'price_list')
search_fields = ('user__email', 'user__first_name', 'user__last_name', 'contact__name')
raw_id_fields = ('contact',)
fieldsets = (
('User', {
'fields': ('user', 'contact')
}),
('Pricing', {
'fields': ('price_list', 'preferred_currency')
}),
('Status', {
'fields': ('is_verified', 'is_approved')
}),
('Preferences', {
'fields': ('newsletter_subscribed', 'preferred_shipping_address')
}),
)
@admin.register(PriceList)
class PriceListAdmin(admin.ModelAdmin):
list_display = ('name', 'code', 'discount_type', 'is_active', 'priority')
list_filter = ('discount_type', 'is_active')
search_fields = ('name', 'code')
@admin.register(PriceListItem)
class PriceListItemAdmin(admin.ModelAdmin):
list_display = ('price_list', 'product', 'price_excluding_vat', 'min_quantity', 'is_active')
list_filter = ('price_list', 'is_active')
search_fields = ('product__sku', 'product__name')
raw_id_fields = ('product',)
1.5 Templates¶
Base Template (templates/customers/base.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Account - Connected Things{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-100">
<nav class="bg-white shadow-lg">
<div class="max-w-7xl mx-auto px-4">
<div class="flex justify-between items-center py-4">
<div class="text-2xl font-bold">Connected Things</div>
<div class="space-x-4">
{% if user.is_authenticated %}
<a href="{% url 'customer_dashboard' %}" class="text-gray-700 hover:text-gray-900">Dashboard</a>
<a href="{% url 'order_history' %}" class="text-gray-700 hover:text-gray-900">Orders</a>
<a href="{% url 'logout' %}" class="text-gray-700 hover:text-gray-900">Logout</a>
{% else %}
<a href="{% url 'customer_login' %}" class="text-gray-700 hover:text-gray-900">Login</a>
<a href="{% url 'register' %}" class="bg-blue-600 text-white px-4 py-2 rounded">Register</a>
{% endif %}
</div>
</div>
</div>
</nav>
<main class="max-w-7xl mx-auto px-4 py-8">
{% if messages %}
{% for message in messages %}
<div class="bg-{{ message.tags }}-100 border border-{{ message.tags }}-400 text-{{ message.tags }}-700 px-4 py-3 rounded mb-4">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% block content %}{% endblock %}
</main>
</body>
</html>
Dashboard (templates/customers/dashboard.html):
{% extends 'customers/base.html' %}
{% block content %}
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Account Info -->
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Account Information</h2>
<p><strong>Email:</strong> {{ user.email }}</p>
<p><strong>Name:</strong> {{ user.get_full_name }}</p>
{% if profile.contact %}
<p><strong>Company:</strong> {{ profile.contact.name }}</p>
{% endif %}
{% if profile.price_list %}
<p class="mt-4 text-green-600">
<strong>Price Tier:</strong> {{ profile.price_list.name }}
</p>
{% endif %}
</div>
<!-- Recent Orders -->
<div class="bg-white p-6 rounded-lg shadow md:col-span-2">
<h2 class="text-xl font-bold mb-4">Recent Orders</h2>
{% if recent_orders %}
<table class="w-full">
<thead>
<tr class="border-b">
<th class="text-left py-2">Order #</th>
<th class="text-left py-2">Date</th>
<th class="text-left py-2">Status</th>
<th class="text-right py-2">Total</th>
</tr>
</thead>
<tbody>
{% for order in recent_orders %}
<tr class="border-b">
<td class="py-2">
<a href="{% url 'order_detail' order.id %}" class="text-blue-600 hover:underline">
{{ order.reference }}
</a>
</td>
<td>{{ order.ordered_at|date:"M d, Y" }}</td>
<td>
<span class="px-2 py-1 bg-gray-200 rounded text-sm">
{{ order.get_status_display }}
</span>
</td>
<td class="text-right">{{ order.total_including_vat }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="mt-4">
<a href="{% url 'order_history' %}" class="text-blue-600 hover:underline">
View all orders →
</a>
</div>
{% else %}
<p class="text-gray-500">No orders yet.</p>
{% endif %}
</div>
</div>
{% endblock %}
1.6 URL Configuration¶
# customers/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('register/', views.register, name='register'),
path('login/', views.customer_login, name='customer_login'),
path('dashboard/', views.dashboard, name='customer_dashboard'),
path('orders/', views.order_history, name='order_history'),
path('orders/<int:order_id>/', views.order_detail, name='order_detail'),
path('invoices/<int:invoice_id>/download/', views.download_invoice, name='download_invoice'),
]
1.7 Testing Phase 1¶
Test Cases: - [ ] User can register with email/password - [ ] User receives verification email (if enabled) - [ ] User can log in and access dashboard - [ ] Dashboard shows correct customer information - [ ] Order history displays correct orders - [ ] Custom pricing shows for assigned customers - [ ] Retail pricing shows for customers without price list - [ ] Order detail page shows full order information - [ ] Invoice download generates correct PDF - [ ] Admin can assign price lists to customers - [ ] Admin can create custom prices per product
1.8 Migration from PrestaShop Customers (Phase 1)¶
# customers/management/commands/migrate_prestashop_customers.py
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from prestashop.models import PrestashopCustomerContactMap
from customers.models import CustomerProfile
class Command(BaseCommand):
help = 'Migrate PrestaShop customers to Uplink customer accounts'
def handle(self, *args, **options):
mappings = PrestashopCustomerContactMap.objects.all()
created_count = 0
updated_count = 0
for mapping in mappings:
# Check if user already exists
email = mapping.contact.primary_email
if not email:
self.stdout.write(f"Skipping {mapping.contact} - no email")
continue
user, created = User.objects.get_or_create(
username=email,
defaults={
'email': email,
'first_name': mapping.contact.first_name or '',
'last_name': mapping.contact.last_name or '',
}
)
if created:
# Set unusable password - they'll need to reset
user.set_unusable_password()
user.save()
created_count += 1
else:
updated_count += 1
# Create or update customer profile
profile, _ = CustomerProfile.objects.get_or_create(
user=user,
defaults={
'contact': mapping.contact,
'is_approved': True, # Pre-approve existing customers
}
)
if not profile.contact:
profile.contact = mapping.contact
profile.save()
self.stdout.write(
self.style.SUCCESS(
f'Created {created_count} users, updated {updated_count} users'
)
)
Completion Checklist - Phase 1¶
- [ ] Customer authentication system implemented
- [ ] CustomerProfile model created and migrated
- [ ] Price list models created
- [ ] Customer-specific pricing logic working
- [ ] Dashboard showing customer info and recent orders
- [ ] Order history view implemented
- [ ] Invoice download working
- [ ] Admin can manage customers and price lists
- [ ] PrestaShop customers migrated
- [ ] Email notifications for registration/password reset
- [ ] Testing complete with 10 beta customers
- [ ] Documentation for customer onboarding
Phase 2: Product Catalog & Browse¶
Goal: Build public-facing product catalog that matches/improves on PrestaShop
Duration: 2-3 months
Prerequisites: Phase 1 complete
2.1 Product Listing Page¶
Features: - Grid/list view toggle - Pagination - Category filtering - Search functionality - Sort by price, name, date added - Stock status indicators - "Quick view" modal
2.2 Product Detail Page¶
Features: - Product images (gallery) - Full description - Technical specifications - Stock availability - Customer-specific pricing (if logged in) - Related products - "Add to cart" button (Phase 3) - Product attributes (LoRaWAN band, battery type, etc.)
2.3 Category Pages¶
Features: - Hierarchical categories - Category description - Breadcrumb navigation - Subcategory display
2.4 Search & Filtering¶
Features: - Full-text search (Postgres full-text search or Elasticsearch) - Filter by category, manufacturer, price range - Filter by product attributes - Faceted search
2.5 Key Considerations¶
SEO:
- Clean URLs (/products/lorawan-sensor/ not /product?id=123)
- Meta tags, Open Graph tags
- Structured data (JSON-LD for products)
- XML sitemap
- Canonical URLs
Performance: - Database query optimization (select_related, prefetch_related) - Redis caching for product listings - CDN for product images - Lazy loading images
Accessibility: - WCAG 2.1 AA compliance - Keyboard navigation - Screen reader support - Alt text for images
Phase 3: Shopping Cart & Checkout¶
Goal: Complete e-commerce functionality without payment
Duration: 2-3 months
Prerequisites: Phase 2 complete
3.1 Shopping Cart¶
Features: - Session-based cart for anonymous users - Persistent cart for logged-in users - Add/remove items - Update quantities - Display customer-specific pricing - Subtotal calculation - Estimated shipping cost - Apply discount codes - Save cart for later
3.2 Checkout Flow¶
Steps: 1. Cart review 2. Shipping address 3. Billing address (if different) 4. Shipping method selection 5. Order review 6. Payment (Phase 4)
3.3 Address Management¶
Features: - Save multiple addresses - Default shipping/billing addresses - Address validation - Country/region selection
3.4 Order Creation¶
Process: - Create Order in Uplink (no PrestaShop sync!) - Generate unique order reference - Create OrderItems - Stock reservation - Send confirmation email - Redirect to order detail page
Phase 4: Payment Integration¶
Goal: Accept online payments
Duration: 1-2 months
Prerequisites: Phase 3 complete
4.1 Payment Gateways¶
Options: - Stripe (recommended: developer-friendly, UK-based) - PayPal (ubiquitous, trusted) - Bank Transfer (manual, for large B2B orders) - Credit Terms (for approved customers)
4.2 Stripe Integration¶
# payments/stripe_integration.py
import stripe
from django.conf import settings
stripe.api_key = settings.STRIPE_SECRET_KEY
def create_payment_intent(order):
"""Create Stripe PaymentIntent for an order"""
intent = stripe.PaymentIntent.create(
amount=int(order.total_including_vat.amount * 100), # Amount in pence
currency=order.currency.lower(),
metadata={
'order_id': order.id,
'order_reference': order.reference,
}
)
return intent
4.3 Payment Flow¶
- Customer clicks "Place Order"
- Create PaymentIntent via Stripe API
- Display Stripe checkout form
- Customer enters card details
- Stripe processes payment
- Webhook confirms payment
- Update order status to "Paid"
- Send confirmation email
- Trigger fulfillment workflow
4.4 Security Considerations¶
- PCI DSS compliance (Stripe handles card data)
- HTTPS only
- CSRF protection
- Webhook signature verification
- Fraud detection (Stripe Radar)
Phase 5: PrestaShop Data Migration¶
Goal: Import all historical data from PrestaShop
Duration: 1 month
Prerequisites: Phases 1-4 complete and tested
5.1 Data to Migrate¶
Customers: - ✅ Already migrated in Phase 1 - Additional: order counts, lifetime value
Orders: - Import all orders from PrestaShop - Preserve order dates, references - Link to migrated customers - Import order items, prices, shipping - Import payment status - Import invoices
Products: - ✅ Already synced (products exist in Uplink) - Import product descriptions from PrestaShop - Import product images - Import category mappings
Product Reviews (if any): - Import customer reviews - Import ratings
5.2 Migration Script¶
# management/commands/migrate_prestashop_all.py
from django.core.management.base import BaseCommand
from prestashop.api import PrestashopApi, EuPrestashopApi
from orders.models import Order
from customers.models import CustomerProfile
class Command(BaseCommand):
help = 'Full migration of PrestaShop data'
def handle(self, *args, **options):
# 1. Migrate customers (if not done)
self.migrate_customers()
# 2. Migrate orders
self.migrate_orders()
# 3. Migrate product images
self.migrate_product_images()
# 4. Verify data integrity
self.verify_migration()
def migrate_orders(self):
# Import ALL orders, not just pending ones
# Use existing prestashop_import_orders logic but expanded
pass
5.3 Testing Migration¶
- Compare order counts
- Verify customer data accuracy
- Check order totals match
- Confirm all images imported
- Validate category mappings
Phase 6: PrestaShop Deprecation¶
Goal: Shut down PrestaShop, redirect to Uplink
Duration: 1 month
Prerequisites: Phase 5 complete, parallel running successful
6.1 Parallel Running¶
Setup:
- Uplink store at store.uplink.sensational.systems
- PrestaShop at connectedthings.co.uk (existing)
- Run both for 2 weeks
- Monitor Uplink for issues
- Compare order volumes
6.2 DNS Cutover¶
Steps:
1. Announce maintenance window to customers
2. Set PrestaShop to read-only mode
3. Update DNS: connectedthings.co.uk → Uplink server
4. Update SSL certificates
5. Redirect all PrestaShop URLs to Uplink equivalents
6. Monitor error logs
6.3 URL Redirects¶
# Legacy PrestaShop URL redirects
# /product.php?id_product=123 → /products/lorawan-sensor/
from django.urls import path, re_path
from django.views.generic import RedirectView
urlpatterns = [
# Legacy product URLs
re_path(r'^product\.php$', legacy_product_redirect),
re_path(r'^category\.php$', legacy_category_redirect),
re_path(r'^order-history$', RedirectView.as_view(url='/customers/orders/', permanent=True)),
]
6.4 PrestaShop Decommissioning¶
Timeline: - Week 1-2: Parallel running, monitor Uplink - Week 3: DNS cutover - Week 4: Fix any issues - Month 2-6: Keep PrestaShop database read-only (backup) - Month 7: Archive PrestaShop database, decommission server
Technical Considerations¶
Database Schema Changes¶
New Tables:
- customers_customerprofile - Customer accounts
- customers_pricelist - Price list tiers
- customers_pricelistitem - Custom prices per product
- customers_address - Saved addresses
- store_cart - Shopping carts
- store_cartitem - Cart items
- payments_payment - Payment records
- payments_paymentmethod - Payment methods
Modified Tables:
- orders_order - Add customer FK (already has contact)
- catalogue_product - Add store-specific fields (descriptions, images)
API Considerations¶
RESTful API (Django REST Framework): - Cart endpoints (add, remove, update) - Product catalog API - Order creation API - Payment processing API
Why? - Future mobile app - Third-party integrations - Headless commerce option
Frontend Framework¶
Options:
1. Django Templates + HTMX (Recommended for Phase 1) - Pros: Simple, server-side rendering, fast development - Cons: Less interactive
2. Vue.js SPA - Pros: Rich interactivity, modern UX - Cons: More complex, SEO challenges
Performance & Scalability¶
Caching Strategy: - Redis for session storage - Cache product listings (invalidate on update) - Cache customer prices (invalidate on price list change) - CDN for static assets
Database Optimization: - Index frequently queried fields - Use database views for complex queries - Connection pooling - Read replicas for reporting
Load Testing: - Simulate Black Friday traffic - Target: 100 concurrent users, <2s page load
Security¶
Authentication: - Django's built-in auth - Rate limiting on login attempts - 2FA for admin accounts (optional for customers) - Social auth (Google, Facebook) optional
Data Protection: - GDPR compliance (right to be forgotten) - Encrypt sensitive data at rest - Audit logs for admin actions - Regular security scans (OWASP ZAP)
Monitoring & Analytics¶
Application Monitoring: - Sentry for error tracking - Prometheus + Grafana for metrics - Uptime monitoring
Business Analytics: - Google Analytics 4 - Conversion tracking - Sales dashboards - Customer insights
Email System¶
Transactional Emails: - Registration confirmation - Password reset - Order confirmation - Shipping notification - Invoice emails
Marketing Emails: - Newsletter - Abandoned cart recovery - Product recommendations - Promotional campaigns
Provider: Mailgun, SendGrid, or Amazon SES
Risk Mitigation¶
Technical Risks¶
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Data loss during migration | Low | High | Full backups, dry-run migrations, validation scripts |
| Performance issues under load | Medium | High | Load testing, caching, optimization before launch |
| Payment gateway integration bugs | Medium | High | Sandbox testing, gradual rollout, manual payment fallback |
| SEO ranking loss | Medium | Medium | Preserve URLs, 301 redirects, submit new sitemap |
| Security vulnerability | Low | High | Security audit, penetration testing, bug bounty |
| Downtime during cutover | Low | Medium | Maintenance window, rollback plan, status page |
Business Risks¶
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Customer confusion | Medium | Medium | Clear communication, migration guide, support availability |
| Lost sales during transition | Low | Medium | Parallel running, choose low-traffic time |
| Feature gaps vs PrestaShop | Medium | Medium | Feature comparison checklist, prioritize must-haves |
| Developer capacity | Medium | High | Realistic timeline, phased approach, outsource if needed |
| Budget overruns | Medium | Medium | Fixed-price phases, MVP approach, avoid scope creep |
Timeline & Resources¶
Detailed Timeline¶
| Phase | Duration | Start | End | Key Deliverables |
|---|---|---|---|---|
| Phase 1: Customer Portal | 3-4 months | Month 1 | Month 4 | Login, pricing, order history |
| Phase 2: Product Catalog | 2-3 months | Month 5 | Month 7 | Product pages, search, categories |
| Phase 3: Cart & Checkout | 2-3 months | Month 8 | Month 10 | Shopping cart, checkout flow |
| Phase 4: Payment | 1-2 months | Month 11 | Month 12 | Stripe integration |
| Phase 5: Migration | 1 month | Month 13 | Month 13 | Full data import |
| Phase 6: Deprecation | 1 month | Month 14 | Month 14 | DNS cutover, monitoring |
Total Timeline: 14 months (1 year 2 months)
Team Requirements¶
Developer Roles: - 1x Full-stack Django developer (Backend + Frontend) - 1x Frontend developer (Templates, CSS, JavaScript) - Optional if full-stack covers - 0.5x DevOps (Deployment, monitoring) - Part-time - 0.5x QA tester - Part-time
Other Roles: - Project manager (could be developer wearing two hats) - UX/UI designer (contract for design phase) - Content writer (product descriptions) - Customer support (migration assistance)
Budget Estimate¶
Development: - Senior Django developer: £60-80k/year (pro-rated for timeline) - Frontend developer: £50-70k/year (if separate) - DevOps: £10-15k (part-time) - Total development: £70-95k
Infrastructure: - Cloud hosting: £100-200/month - CDN: £50-100/month - Monitoring tools: £50/month - Email service: £50/month - Total infrastructure/year: £3-5k
Third-Party Services: - Stripe fees: 1.5% + 20p per transaction (variable) - SSL certificates: Free (Let's Encrypt) - Design work: £5-10k (one-time)
Contingency: 20% of budget
Total Estimated Budget: £80-110k
Success Criteria¶
Technical: - [ ] 99.9% uptime - [ ] Page load time <2 seconds - [ ] Zero critical bugs at launch - [ ] All PrestaShop features replicated or improved - [ ] Mobile-responsive design - [ ] Accessibility compliance (WCAG 2.1 AA)
Business: - [ ] 90% customer retention during migration - [ ] Order processing time reduced by 50% (no sync delay) - [ ] Customer satisfaction score >4.5/5 - [ ] Conversion rate equal to or better than PrestaShop - [ ] Operational costs reduced (no PrestaShop license/maintenance)
Appendix: PrestaShop Feature Comparison¶
Must-Have Features¶
| Feature | PrestaShop | Uplink Store | Priority |
|---|---|---|---|
| Product catalog | ✅ | Phase 2 | High |
| Shopping cart | ✅ | Phase 3 | High |
| Checkout | ✅ | Phase 3 | High |
| Payment processing | ✅ | Phase 4 | High |
| Customer accounts | ✅ | Phase 1 | High |
| Order history | ✅ | Phase 1 | High |
| Custom pricing | ❌ | Phase 1 | High |
| Search | ✅ | Phase 2 | High |
| Categories | ✅ | Phase 2 | Medium |
| Product filtering | ✅ | Phase 2 | Medium |
| Email notifications | ✅ | All phases | High |
| Invoice generation | ✅ | Phase 1 | High |
| Multi-currency | ✅ | Future | Low |
| Multi-language | ✅ | Future | Low |
| Discount codes | ✅ | Phase 3 | Medium |
| Product reviews | ✅ | Future | Low |
| Wish lists | ✅ | Future | Low |
| Stock management | ✅ | ✅ (existing) | High |
Nice-to-Have Features (Future)¶
- Live chat support
- Product comparison
- Recently viewed products
- Abandoned cart recovery
- Gift certificates
- Product bundles
- Subscription products
- B2B quote requests
- CSV bulk ordering
- API for third-party integrations
Next Steps¶
Immediate Actions:
1. Review this plan with the team
2. Prioritize features - What's must-have vs nice-to-have?
3. Set realistic timeline - Do we have 14 months? Budget?
4. Choose Phase 1 beta customers - 10 favorite customers for early access
5. Create designs - Wireframes for customer portal
6. Set up development environment - Separate customers app
7. Define success metrics - How do we measure success?
Questions to Answer: - Do we have developer capacity or need to hire/outsource? - What's the budget for this project? - Can we start Phase 1 while Uplink 2.0 is still in progress? - Which payment gateways are required? (Stripe, PayPal, both?) - Do we need multi-currency support from day 1? - Should we soft-launch with B2B customers only, or also B2C?
Decision Points: - Frontend framework choice (Django templates vs Vue.js vs Next.js) - Mobile app scope (future consideration) - Multi-language support (now or later?) - Social login (Google, Facebook, Apple)
Conclusion¶
Migrating from PrestaShop to a custom Uplink store is a major undertaking but offers significant benefits:
Benefits: - ✅ Single source of truth - No sync issues - ✅ Customer-specific pricing - Competitive advantage - ✅ Integrated customer portal - Better customer experience - ✅ Real-time order processing - No sync delays - ✅ Full customization - Tailor for IoT hardware sales - ✅ Lower operational costs - No PrestaShop license/maintenance - ✅ Better developer experience - Python stack consistency
Challenges: - ⚠️ Large scope - 14-month project - ⚠️ Resource intensive - Requires dedicated developer(s) - ⚠️ Migration complexity - Data migration, testing, cutover - ⚠️ Customer impact - Need clear communication
Recommendation: - Start with Phase 1 (Customer Portal) as a Uplink 2.5 release - Phase 1 is low-risk and provides immediate value - Can run alongside PrestaShop indefinitely - Proves the concept before committing to full migration - Allows favorite customers to benefit from custom pricing immediately