Skip to content

Design Review System

This document describes the hybrid design review workflow for custom designs.

Overview

The design review system uses a hybrid approach: - Auto-approve: Designs containing only text and clipart are automatically finalized - Manual review: Designs with user-uploaded images require staff review for print quality

This balances customer experience (fast turnaround for simple designs) with quality control (uploaded images need resolution/quality checks).

Implementation status (verified 2026-07-02)

The review flow is now driven by explicit actions rather than automatic upload-detection:

  • Customer requests review: POST /api/designs/{id}/request_approval/ moves a draft or rejected design to pending_review (CustomDesignViewSet.request_approval, backend/apps/designs/views.py).
  • Admin decides: POST /api/admin/designs/{id}/approve/ (pending_reviewapproved, unblocks checkout) or POST /api/admin/designs/{id}/reject/ (pending_reviewrejected, requires a reason, stored in review_note). Both stamp reviewed_at / reviewed_by and write an audit log (backend/apps/designs/admin_views.py).
  • Finalize (PATCH /api/designs/{id}/finalize/) is a separate "lock for production" action that sets finalized unconditionally.
  • The upload-detection helper CustomDesign.has_user_uploaded_images() still exists on the model but is not wired into an automatic pending-review trigger — moving to pending_review is an explicit customer/admin action.
  • Checkout is blocked while the cart contains a pending_review (or rejected) design (OrderService._validate_design_status in backend/apps/orders/services.py).

Design Status Workflow

stateDiagram-v2
    [*] --> draft
    draft --> pending_review: request_approval
    rejected --> pending_review: request_approval
    pending_review --> approved: admin approve
    pending_review --> rejected: admin reject (reason)
    approved --> finalized: finalize (lock)
    draft --> finalized: finalize (lock)
    finalized --> in_production: order fulfilment
Status Description When Used
draft Design is being edited Default status, active editing
pending_review Awaiting staff review Customer requested approval (request_approval)
approved Passed staff review Admin approve — unblocks checkout
rejected Rejected by staff Admin reject with a reason (shown to customer)
finalized Locked for production finalize action
in_production Being manufactured Order is being fulfilled

Email Notifications

Design Pending Review

Trigger: Design status changes to pending_review

Template: design_pending_review.html / .txt

Subject: Je ontwerp voor {product_name} wordt beoordeeld

Content: - Explains uploaded images are being checked for print quality - Sets expectation: review within 24 hours - Preview thumbnail of the design - Link to view/edit the design

Design Approved

Trigger: Design status changes to approved or finalized

Template: design_approved.html / .txt

Subject: Je ontwerp voor {product_name} is goedgekeurd!

Content: - Confirms design is approved and ready for production - Preview thumbnail - Link to designer or cart

Design Rejected

Trigger: Design status changes to rejected

Template: design_rejected.html / .txt

Content: - Explains the design was not approved, including the rejection reason - Link back to the designer so the customer can adjust and re-request approval

Status-change emails are dispatched from backend/apps/designs/signals.py (via transaction.on_commit) using the model's FieldTracker.

Email Recipient Logic

The system uses a fallback pattern for email recipients:

def get_recipient_email(design):
    # 1. Prefer user's account email
    if design.user:
        return design.user.email

    # 2. Fallback to order email for guest checkout
    order_item = design.orderitem_set.select_related('order').first()
    if order_item and order_item.order:
        return order_item.order.email

    return None

This ensures guests who checkout without an account still receive design status emails.

Detecting User Uploads

The has_user_uploaded_images() method checks if a design contains user-uploaded images:

def has_user_uploaded_images(self):
    for view in ["front", "back", "left", "right"]:
        view_json = getattr(self, f"design_json_{view}", {})
        for obj in view_json.get("objects", []):
            if obj.get("type") in ("image", "Image"):
                src = obj.get("src", "")
                # User uploads in /uploads/, clipart in /clipart/
                if "/uploads/" in src or src.startswith("data:"):
                    return True
    return False

Detection logic: - User uploads are stored in /uploads/ directory - Clipart is stored in /clipart/ directory - Base64 data URLs (data:) are treated as user uploads

Frontend Components

ImageTab Info Banner

Location: frontend/components/ProductDesigner/components/tabs/ImageTab.tsx

A blue info banner warns users that uploaded images require review:

Let op: Geuploade afbeeldingen worden door ons team gecontroleerd op printkwaliteit. Je ontvangt binnen 24 uur een e-mail zodra je ontwerp is goedgekeurd.

Order Progress Stepper

Location: frontend/components/orders/OrderProgressStepper.tsx

Shows order fulfillment progress:

Betaald → In behandeling → Wordt bedrukt → Verzonden → Afgeleverd

Also displays: - Design review notice when status is pending_review - Waiting for payment notice when status is pending

Design Status Badges

On the order detail page, each order item shows a design status badge: - 🔍 Ontwerp wordt beoordeeld - pending_review - ✓ Ontwerp goedgekeurd - finalized

Admin Workflow

The primary review UI is the frontend admin (/admin), backed by the /api/admin/designs/{id}/approve/ and /reject/ actions:

  1. Open the admin designs list and filter by status pending_review
  2. Review the design preview images and uploaded-image quality/resolution
  3. Approve (pending_reviewapproved) — unblocks checkout, or Reject (pending_reviewrejected) with a reason shown to the customer
  4. A status email is sent automatically (see the signals below)

The Django admin can still be used for ad-hoc status changes (status is editable there), but approve/reject with audit + review metadata is the intended path.

API Endpoints

Method & path Actor Effect
POST /api/designs/{id}/request_approval/ Customer draft/rejectedpending_review
POST /api/admin/designs/{id}/approve/ Staff pending_reviewapproved (unblocks checkout)
POST /api/admin/designs/{id}/reject/ Staff pending_reviewrejected; body {"reason": "..."} required
PATCH /api/designs/{id}/finalize/ Customer/system finalized (locks the design)

The generic PATCH /api/designs/{id}/ still exists; status is a writable serializer field, so it can also be set directly (e.g. from the Django admin).

Database

The current STATUS_CHOICES on CustomDesign (backend/apps/designs/models.py):

STATUS_CHOICES = [
    ("draft", "Draft"),
    ("pending_review", "Pending Review"),
    ("approved", "Approved"),
    ("rejected", "Rejected"),
    ("finalized", "Finalized"),
    ("in_production", "In Production"),
]

The model also carries review metadata (review_note, reviewed_at, reviewed_by) populated by the admin approve/reject actions.

Testing

Backend Tests

cd backend
pytest apps/designs/tests.py

Frontend Tests

cd frontend
npm test -- --testPathPatterns="OrderDetailPage"

Manual E2E Test

  1. Upload image in designer → see info banner
  2. Move the design to pending_review (customer request_approval, Django admin, or API PATCH) → checkout with this design in the cart is blocked with a notice
  3. Check pending-review email received (user email, or order email for guests)
  4. Admin: approve the design (pending_reviewapproved)
  5. Check approval email received; checkout is possible again
  6. (Alternatively) Admin reject with a reason → rejected email is sent and checkout stays blocked until the customer re-requests and it's approved
  7. Order detail page shows progress stepper

Configuration

No additional configuration is required. The feature uses existing environment variables:

Variable Purpose
FRONTEND_URL Base URL for designer links in emails
Email settings Existing Anymail/SMTP configuration