Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ALEJ4NDRO2025/urban-store/llms.txt

Use this file to discover all available pages before exploring further.

Every purchase in Urban Store follows a well-defined order lifecycle managed entirely in MongoDB. Orders are created with a 5-minute payment window; unpaid orders expire automatically and their reserved stock is restored. Paid orders move through a fulfilment pipeline, and any cancellation — whether by an admin or by expiry — triggers an automatic stock restoration to keep inventory accurate.

Order States

          ┌──────────┐
  create  │  pending │ ──── expires in 5 min (TTL) ────┐
  ───────►│          │                                  │
          └────┬─────┘                                  ▼
               │ Stripe confirms                 ┌─────────────┐
               ▼                                 │  cancelled  │
          ┌──────────┐                           └─────────────┘
          │   paid   │ ──────────────────────────────── ▲
          └────┬─────┘         admin cancels            │
               │ admin sets                             │
               ▼                                        │
     ┌──────────────────┐                               │
     │ pending_shipment │ ────── admin cancels ─────────┘
     └────────┬─────────┘
              │ admin sets

         ┌──────────┐
         │ shipped  │
         └──────────┘

pending

Order has been created and is awaiting payment. Stock has already been decremented. The order expires 5 minutes after creation if payment is not completed.

paid

Stripe has confirmed the payment. paid_at is set to the current timestamp. The order is ready for fulfilment.

pending_shipment

Payment confirmed but the order has not yet been handed to a carrier. Set by an admin to indicate the order is being prepared for dispatch.

shipped

The order has been delivered to the carrier. Set by an admin once a tracking number is assigned.

cancelled

The order has been cancelled. Can be reached from pending, paid, or pending_shipment. Stock is restored on cancellation (see below).

Stock Management

On Order Creation

When POST /api/orders/ is called, the order document is saved first, and then stock is decremented immediately afterward. For each OrderItem, the backend builds the variant key "{size}|{color}" and checks stock_by_variant:
variant_key = f"{item.size}|{item.color}"

if product.stock_by_variant and variant_key in product.stock_by_variant:
    # Decrement per-variant stock
    current = product.stock_by_variant[variant_key]
    product.stock_by_variant[variant_key] = max(0, current - item.quantity)
    product.save()
else:
    # Fallback: decrement general stock
    new_stock = max(0, product.stock - item.quantity)
    product.update(stock=new_stock)
Stock is never allowed to go below 0.

On Cancellation — Stock Restoration

When an admin cancels an order via PATCH /api/orders/<order_id>/status/, the _restore_stock() method is called if the order was not already in pending or cancelled state (those states are treated as having no pending stock deduction to undo on the admin path; expired pending orders are handled separately by the expiry mechanism). The same variant-key logic is applied in reverse:
variant_key = f"{item.size}|{item.color}"

if product.stock_by_variant and variant_key in product.stock_by_variant:
    product.stock_by_variant[variant_key] += item.quantity
    product.save()
else:
    # Fallback: restore general stock
    if hasattr(product, 'update'):
        product.update(stock=product.stock + item.quantity)
    else:
        product.stock += item.quantity
        product.save()

Passive Expiry — TTL-Based Cancellation

Urban Store uses a passive expiry model. There is no scheduled background task; instead, check_and_cancel_expired_orders() is invoked at the start of every order query (list, detail, admin list):
def check_and_cancel_expired_orders():
    now = timezone.now()
    expired_orders = Order.objects.filter(status='pending', expires_at__lt=now)

    for order in expired_orders:
        for item in order.items:
            product = Product.objects(slug=item.product_slug).first()
            if product:
                variant_key = f"{item.size}|{item.color}"
                if product.stock_by_variant and variant_key in product.stock_by_variant:
                    product.stock_by_variant[variant_key] += item.quantity
                    product.save()
                else:
                    # Fallback: restore general stock
                    if hasattr(product, 'update'):
                        product.update(stock=product.stock + item.quantity)
                    else:
                        product.stock += item.quantity
                        product.save()
        order.status = 'cancelled'
        order.updated_at = now
        order.save()
MongoDB also has a TTL index on expires_at (expireAfterSeconds: 0), which can eventually purge the document entirely, but the passive check ensures cancelled status is reflected immediately on next access.

Order Data Model

The Order document is stored in the orders MongoDB collection.

Order Fields

FieldTypeDescription
user_idStringFieldEmail of the owning user (from JWT)
order_numberStringField(unique=True)Human-readable order ID
itemsListField(EmbeddedDocumentField(OrderItem))Line items
subtotalFloatFieldSum of all item subtotals
taxFloatField(default=0)Tax amount
shippingFloatField(default=0)Shipping cost
totalFloatFieldGrand total
statusStringField(default='pending')Current lifecycle state
payment_methodStringFieldPayment method identifier
shipping_addressEmbeddedDocumentField(ShippingAddress)Delivery address
notesStringField(default='')Optional customer notes
created_atDateTimeFieldCreation timestamp (UTC)
updated_atDateTimeFieldLast update timestamp (UTC)
paid_atDateTimeFieldSet when status becomes paid
payment_intent_idStringFieldStripe PaymentIntent ID
expires_atDateTimeFieldExpiry time for pending orders (5 min after creation)

OrderItem Fields

FieldTypeDescription
product_slugStringField(required=True)Product reference
product_nameStringFieldDisplay name at time of purchase
quantityIntField(required=True)Units ordered
sizeStringFieldSelected size
colorStringFieldSelected color
price_paidFloatField(required=True)Unit price at time of purchase
subtotalFloatField(required=True)price_paid × quantity

ShippingAddress Fields

FieldTypeDescription
emailStringField(required=True)Contact email
nameStringField(required=True)Recipient full name
phoneStringField(required=True)Contact phone number
addressStringField(required=True)Street address
cityStringField(required=True)City
departmentStringField(required=True)Department / state / province
countryStringField(default='Colombia')Country (defaults to Colombia)

Order Number Format

Order numbers are generated by Order.generate_order_number():
timestamp = datetime.utcnow().strftime('%Y%m%d%H%M%S')
random_suffix = random.randint(100, 999)
return f"ORD-{timestamp}-{random_suffix}"
Example: ORD-20260115143022-847 This gives a human-readable, roughly chronologically sortable identifier with very low collision probability.

Confirmation Email

A styled HTML confirmation email is sent via Django’s send_mail immediately after a successful order creation. The email includes:
  • Order number
  • Itemized product list (name, size, color, quantity, subtotal)
  • Subtotal, shipping, taxes, and grand total
  • Full shipping address
The email is sent synchronously during the request. If the mail backend raises an exception (because fail_silently=False), the order creation request will fail. Ensure EMAIL_BACKEND and DEFAULT_FROM_EMAIL are correctly configured in Django settings.

API Endpoints

MethodEndpointAccessDescription
POST/api/orders/Authenticated userCreate a new order
GET/api/orders/Authenticated userList own orders
GET/api/orders/<order_id>/Authenticated user or adminGet order detail
GET/api/orders/all/Admin onlyList all orders
PATCH/api/orders/<order_id>/status/Admin onlyUpdate order status

Build docs developers (and LLMs) love