Skip to content

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

  1. Current State Analysis
  2. Target Architecture
  3. Migration Strategy
  4. Phase 1: Customer Portal (Custom Pricing & Login)
  5. Phase 2: Product Catalog & Browse
  6. Phase 3: Shopping Cart & Checkout
  7. Phase 4: Payment Integration
  8. Phase 5: PrestaShop Data Migration
  9. Phase 6: PrestaShop Deprecation
  10. Technical Considerations
  11. Risk Mitigation
  12. 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│                           │
     │<───────────────────────┼───────────────────────────│

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

  1. Order Import (PrestaShop → Uplink):
  2. Management command runs periodically
  3. Fetches orders with specific statuses
  4. Creates Order records in Uplink
  5. Maps PrestaShop customer IDs to Uplink Contacts
  6. Imports order items, addresses, shipping info

  7. Product Sync (Uplink → PrestaShop):

  8. Triggered when products updated in Uplink
  9. Sends product data, pricing, descriptions
  10. 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

┌──────────────────────────────────────────────────────────────┐
│                     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

  1. Incremental Migration - Build alongside PrestaShop, switch gradually
  2. No Big Bang - Phase 1 can coexist with PrestaShop
  3. Data Preservation - Import all historical orders and customers
  4. Zero Downtime - DNS switch when ready, rollback possible
  5. 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

  1. Customer clicks "Place Order"
  2. Create PaymentIntent via Stripe API
  3. Display Stripe checkout form
  4. Customer enters card details
  5. Stripe processes payment
  6. Webhook confirms payment
  7. Update order status to "Paid"
  8. Send confirmation email
  9. 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