web-app/docs/02-modules/market-module/order-management.md
padreug cdf099e45f Create comprehensive Obsidian-style documentation structure
- Reorganize all markdown documentation into structured docs/ folder
- Create 7 main documentation categories (00-overview through 06-deployment)
- Add comprehensive index files for each category with cross-linking
- Implement Obsidian-compatible [[link]] syntax throughout
- Move legacy/deprecated documentation to archive folder
- Establish documentation standards and maintenance guidelines
- Provide complete coverage of modular architecture, services, and deployment
- Enable better navigation and discoverability for developers and contributors

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 14:31:27 +02:00

30 KiB

Order Management & Fulfillment Workflows

This document provides comprehensive coverage of the complete order lifecycle, from initial placement through payment processing to final fulfillment and shipping management. It includes detailed analysis of both merchant and customer interfaces, database operations, and automated fulfillment processes.

Overview: Order Lifecycle Management

The marketplace implements a comprehensive order management system with dual interfaces for merchants and customers, supporting complete order tracking from placement to fulfillment with automated inventory management and payment processing.

Order States and Transitions

graph LR
    A[Order Created] --> B[Invoice Generated]
    B --> C[Payment Request Sent]
    C --> D{Payment Status}
    D -->|Paid| E[Payment Confirmed]
    D -->|Unpaid| F[Invoice Expired/Reissued]
    F --> C
    E --> G[Inventory Updated]
    G --> H{Fulfillment}
    H -->|Ready| I[Shipped Status]
    H -->|Issue| J[Error/Refund]
    I --> K[Order Complete]

Core Order Data Models

1. Order Schema (models.py:400-485)

Order Structure Hierarchy

class OrderItem(BaseModel):
    product_id: str                    # Product identifier from order
    quantity: int                      # Quantity ordered

class OrderContact(BaseModel):
    nostr: Optional[str] = None        # Customer's nostr pubkey
    phone: Optional[str] = None        # Customer phone number
    email: Optional[str] = None        # Customer email address

class OrderExtra(BaseModel):
    products: List[ProductOverview]    # Snapshot of products at time of order
    currency: str                      # Pricing currency (USD, EUR, sat, etc.)
    btc_price: str                     # Exchange rate at time of order
    shipping_cost: float = 0           # Shipping cost in currency
    shipping_cost_sat: float = 0       # Shipping cost in satoshis
    fail_message: Optional[str] = None # Error message if order failed

Complete Order Model

class Order(PartialOrder):
    stall_id: str                      # Associated stall identifier
    invoice_id: str                    # Lightning invoice payment hash
    total: float                       # Total amount in satoshis
    paid: bool = False                 # Payment status
    shipped: bool = False              # Shipping/fulfillment status
    time: Optional[int] = None         # Completion timestamp
    extra: OrderExtra                  # Additional order metadata

2. Order Status Models (models.py:467-485)

Status Update Structure

class OrderStatusUpdate(BaseModel):
    id: str                            # Order identifier
    message: Optional[str] = None      # Status update message
    paid: Optional[bool] = False       # Payment status
    shipped: Optional[bool] = None     # Shipping status

class OrderReissue(BaseModel):
    id: str                            # Order identifier to reissue
    shipping_id: Optional[str] = None  # Updated shipping zone

class PaymentRequest(BaseModel):
    id: str                            # Order identifier
    message: Optional[str] = None      # Response message
    payment_options: List[PaymentOption] # Available payment methods

3. Database Schema (migrations.py:110-130)

Order Table Structure

CREATE TABLE nostrmarket.orders (
    merchant_id TEXT NOT NULL,         -- Merchant who owns this order
    id TEXT PRIMARY KEY,               -- Unique order identifier (UUID)
    event_id TEXT,                     -- Nostr event ID for order placement
    event_created_at INTEGER NOT NULL, -- Unix timestamp of order creation
    public_key TEXT NOT NULL,          -- Customer's public key
    merchant_public_key TEXT NOT NULL, -- Merchant's public key
    contact_data TEXT NOT NULL DEFAULT '{}', -- JSON contact information
    extra_data TEXT NOT NULL DEFAULT '{}',   -- JSON extra metadata
    order_items TEXT NOT NULL,         -- JSON array of ordered items
    address TEXT,                      -- Shipping address (deprecated)
    total REAL NOT NULL,               -- Total amount in satoshis
    shipping_id TEXT NOT NULL,         -- Shipping zone identifier
    stall_id TEXT NOT NULL,            -- Associated stall identifier
    invoice_id TEXT NOT NULL,          -- Lightning invoice payment hash
    paid BOOLEAN NOT NULL DEFAULT false,    -- Payment confirmation
    shipped BOOLEAN NOT NULL DEFAULT false, -- Fulfillment status
    time INTEGER                       -- Completion timestamp
);

Merchant Order Management Interface

1. Order List Component (order-list.js)

Component Structure and Properties

window.app.component('order-list', {
  name: 'order-list',
  props: ['stall-id', 'customer-pubkey-filter', 'adminkey', 'inkey'],
  template: '#order-list',
  delimiters: ['${', '}'],

Advanced Search and Filtering (order-list.js:15-49)

data: function () {
  return {
    orders: [],
    selectedOrder: null,
    search: {
      publicKey: null,               // Filter by customer public key
      isPaid: {label: 'All', id: null},    // Payment status filter
      isShipped: {label: 'All', id: null}, // Shipping status filter
    },
    ternaryOptions: [
      {label: 'All', id: null},      // Show all orders
      {label: 'Yes', id: 'true'},    // Filter for paid/shipped = true
      {label: 'No', id: 'false'}     // Filter for paid/shipped = false
    ]
  }
}

Dynamic Order Fetching (order-list.js:156-181)

getOrders: async function () {
  try {
    // Support both stall-specific and merchant-wide queries
    const ordersPath = this.stallId
      ? `stall/order/${this.stallId}`    // Orders for specific stall
      : 'order'                          // All orders for merchant

    // Build query parameters for filtering
    const query = []
    if (this.search.publicKey) {
      query.push(`pubkey=${this.search.publicKey}`)
    }
    if (this.search.isPaid.id) {
      query.push(`paid=${this.search.isPaid.id}`)
    }
    if (this.search.isShipped.id) {
      query.push(`shipped=${this.search.isShipped.id}`)
    }
    
    const {data} = await LNbits.api.request(
      'GET',
      `/nostrmarket/api/v1/${ordersPath}?${query.join('&')}`,
      this.inkey
    )
    this.orders = data.map(s => ({...s, expanded: false}))
  } catch (error) {
    LNbits.utils.notifyApiError(error)
  }
}

2. Order Display and Calculations (order-list.js:119-155)

Product Information Retrieval

productName: function (order, productId) {
  product = order.extra.products.find(p => p.id === productId)
  if (product) {
    return product.name
  }
  return ''
},

productPrice: function (order, productId) {
  product = order.extra.products.find(p => p.id === productId)
  if (product) {
    return `${product.price} ${order.extra.currency}`
  }
  return ''
},

orderTotal: function (order) {
  // Calculate total from individual product costs + shipping
  const productCost = order.items.reduce((t, item) => {
    product = order.extra.products.find(p => p.id === item.product_id)
    return t + item.quantity * product.price
  }, 0)
  return productCost + order.extra.shipping_cost
}

3. Shipping Status Management (order-list.js:259-280)

Shipping Status Updates

updateOrderShipped: async function () {
  this.selectedOrder.shipped = !this.selectedOrder.shipped
  try {
    await LNbits.api.request(
      'PATCH',
      `/nostrmarket/api/v1/order/${this.selectedOrder.id}`,
      this.adminkey,
      {
        id: this.selectedOrder.id,
        message: this.shippingMessage,        // Custom message to customer
        shipped: this.selectedOrder.shipped   // New shipping status
      }
    )
    this.$q.notify({
      type: 'positive',
      message: 'Order updated!'
    })
  } catch (error) {
    LNbits.utils.notifyApiError(error)
  }
  this.showShipDialog = false
}

Shipping Dialog Interface (order-list.js:356-365)

showShipOrderDialog: function (order) {
  this.selectedOrder = order
  this.shippingMessage = order.shipped
    ? 'The order has been shipped!'
    : 'The order has NOT yet been shipped!'

  // Toggle status (will be confirmed on dialog submit)
  this.selectedOrder.shipped = !order.shipped
  this.showShipDialog = true
}

4. Order Recovery and Restoration (order-list.js:194-233)

Individual Order Restoration

restoreOrder: async function (eventId) {
  try {
    this.search.restoring = true
    const {data} = await LNbits.api.request(
      'PUT',
      `/nostrmarket/api/v1/order/restore/${eventId}`,    // Restore from DM event
      this.adminkey
    )
    await this.getOrders()                               // Refresh order list
    this.$q.notify({
      type: 'positive',
      message: 'Order restored!'
    })
    return data
  } catch (error) {
    LNbits.utils.notifyApiError(error)
  } finally {
    this.search.restoring = false
  }
}

Bulk Order Restoration

restoreOrders: async function () {
  try {
    this.search.restoring = true
    await LNbits.api.request(
      'PUT',
      `/nostrmarket/api/v1/orders/restore`,              // Restore all from DMs
      this.adminkey
    )
    await this.getOrders()
    this.$q.notify({
      type: 'positive',
      message: 'Orders restored!'
    })
  } catch (error) {
    LNbits.utils.notifyApiError(error)
  }
}

5. Invoice Management (order-list.js:234-258)

Invoice Reissuance

reissueOrderInvoice: async function (order) {
  try {
    const {data} = await LNbits.api.request(
      'PUT',
      `/nostrmarket/api/v1/order/reissue`,
      this.adminkey,
      {
        id: order.id,
        shipping_id: order.shipping_id              // Optional shipping zone update
      }
    )
    this.$q.notify({
      type: 'positive',
      message: 'Order invoice reissued!'
    })
    
    // Update order in local state
    data.expanded = order.expanded
    const i = this.orders.map(o => o.id).indexOf(order.id)
    if (i !== -1) {
      this.orders[i] = {...this.orders[i], ...data}
    }
  } catch (error) {
    LNbits.utils.notifyApiError(error)
  }
}

Customer Order Interface

1. Customer Orders Component (CustomerOrders.vue)

Order Display Structure

<div v-for="merchant in merchantOrders" :key="merchant.id">
  <q-card bordered class="q-mb-md">
    <q-item>
      <user-profile                               <!-- Merchant identity -->
        :pubkey="merchant.pubkey"
        :profiles="profiles"
      ></user-profile>
    </q-item>
    
    <q-list>
      <div v-for="order in merchant.orders" :key="order.id">
        <q-expansion-item dense expand-separator>
          <template v-slot:header>
            <q-item-section>
              <q-item-label>
                <strong><span v-text="order.stallName"></span></strong>
                <q-badge                          <!-- Total amount -->
                  v-if="order.invoice?.human_readable_part?.amount"
                  color="orange"
                >
                  <span v-text="formatCurrency(order.invoice.human_readable_part.amount / 1000, 'sat')"></span>
                </q-badge>
              </q-item-label>
            </q-item-section>
            
            <q-item-section side>
              <q-badge :color="order.paid ? 'green' : 'grey'">    <!-- Payment status -->
                <span v-text="order.paid ? 'Paid' : 'Not Paid'"></span>
              </q-badge>
              <q-badge :color="order.shipped ? 'green' : 'grey'"> <!-- Shipping status -->
                <span v-text="order.shipped ? 'Shipped' : 'Not Shipped'"></span>
              </q-badge>
            </q-item-section>
          </template>
        </q-expansion-item>
      </div>
    </q-list>
  </q-card>
</div>

2. Order Data Enrichment (CustomerOrders.vue:208-220)

Order Enhancement Pipeline

enrichOrder: function (order) {
  const stall = this.stallForOrder(order);
  return {
    ...order,
    stallName: stall?.name || "Stall",                   // Stall name for display
    shippingZone: stall?.shipping?.find(                 // Shipping zone details
      (s) => s.id === order.shipping_id
    ) || { id: order.shipping_id, name: order.shipping_id },
    invoice: this.invoiceForOrder(order),                // Parsed Lightning invoice
    products: this.getProductsForOrder(order),           // Product details with quantities
  };
}

Stall Association (CustomerOrders.vue:221-233)

stallForOrder: function (order) {
  try {
    const productId = order.items && order.items[0]?.product_id;
    if (!productId) return;
    
    const product = this.products.find((p) => p.id === productId);
    if (!product) return;
    
    const stall = this.stalls.find((s) => s.id === product.stall_id);
    if (!stall) return;
    
    return stall;
  } catch (error) {
    console.log(error);
  }
}

3. Invoice Processing (CustomerOrders.vue:234-244)

Lightning Invoice Decoding

invoiceForOrder: function (order) {
  try {
    const lnPaymentOption = order?.payment_options?.find(
      (p) => p.type === "ln"                             // Find Lightning payment option
    );
    if (!lnPaymentOption?.link) return;
    
    return decode(lnPaymentOption.link);                 // Decode BOLT11 invoice
  } catch (error) {
    console.warn(error);
  }
}

4. Product Aggregation (CustomerOrders.vue:246-259)

Order Item Processing

getProductsForOrder: function (order) {
  if (!order?.items?.length) return [];

  return order.items.map((i) => {
    const product = this.products.find((p) => p.id === i.product_id) || {
      id: i.product_id,
      name: i.product_id,                                // Fallback if product not found
    };
    return {
      ...product,
      orderedQuantity: i.quantity,                       // Add ordered quantity to product
    };
  });
}

Backend Order Operations

1. Order Creation (services.py:84-133)

Order Build Pipeline

async def build_order_with_payment(merchant_id, merchant_public_key, data):
    # 1. Validate products and calculate costs
    products = await get_products_by_ids(merchant_id, [p.product_id for p in data.items])
    data.validate_order_items(products)                  # Ensure products exist and have stock
    
    shipping_zone = await get_zone(merchant_id, data.shipping_id)
    product_cost_sat, shipping_cost_sat = await data.costs_in_sats(
        products, shipping_zone.id, shipping_zone.cost
    )
    
    # 2. Check inventory availability
    success, _, message = await compute_products_new_quantity(
        merchant_id, [i.product_id for i in data.items], data.items
    )
    if not success:
        raise ValueError(message)                         # Insufficient inventory
    
    # 3. Create Lightning invoice via LNbits
    payment = await create_invoice(
        wallet_id=wallet_id,
        amount=round(product_cost_sat + shipping_cost_sat),
        memo=f"Order '{data.id}' for pubkey '{data.public_key}'",
        extra={
            "tag": "nostrmarket",                         # Tags invoice as marketplace
            "order_id": data.id,
            "merchant_pubkey": merchant_public_key,
        },
    )
    
    # 4. Create order record
    order = Order(
        **data.dict(),
        stall_id=products[0].stall_id,
        invoice_id=payment.payment_hash,
        total=product_cost_sat + shipping_cost_sat,
        extra=extra,
    )
    
    return order, payment.bolt11, receipt

2. Order Retrieval API (views_api.py:540-577)

Multi-filter Order Queries

@nostrmarket_ext.get("/api/v1/stall/order/{stall_id}")
async def api_get_orders_for_stall(
    stall_id: str,
    paid: Optional[bool] = None,                         # Filter by payment status
    shipped: Optional[bool] = None,                      # Filter by shipping status
    pubkey: Optional[str] = None,                        # Filter by customer pubkey
    wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> List[Order]:
    try:
        merchant = await get_merchant_for_user(wallet.wallet.user)
        assert merchant, "Merchant cannot be found"
        
        orders = await get_orders_for_stall(
            merchant.id, stall_id, paid=paid, shipped=shipped, public_key=pubkey
        )
        return orders
    except AssertionError as ex:
        raise HTTPException(
            status_code=HTTPStatus.NOT_FOUND, detail=str(ex)
        ) from ex

3. Order Status Updates (views_api.py:625-641)

Shipping Status API

@nostrmarket_ext.patch("/api/v1/order/{order_id}")
async def api_update_order_status(
    data: OrderStatusUpdate,
    wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Order:
    try:
        assert data.shipped is not None, "Shipped value is required for order"
        merchant = await get_merchant_for_user(wallet.wallet.user)
        assert merchant, "Merchant cannot be found for order {data.id}"

        # Update shipping status in database
        order = await update_order_shipped_status(merchant.id, data.id, data.shipped)
        assert order, "Cannot find updated order"

        # Send status update to customer via DM
        data.paid = order.paid                           # Include current payment status
        dm_content = json.dumps(
            {"type": DirectMessageType.ORDER_PAID_OR_SHIPPED.value, **data.dict()},
            separators=(",", ":"),
            ensure_ascii=False,
        )
        await reply_to_structured_dm(
            merchant, order.public_key, DirectMessageType.ORDER_PAID_OR_SHIPPED.value, dm_content
        )

        return order
    except AssertionError as ex:
        raise HTTPException(
            status_code=HTTPStatus.NOT_FOUND, detail=str(ex)
        ) from ex

4. Invoice Reissuance (views_api.py:710-740)

Payment Request Regeneration

@nostrmarket_ext.put("/api/v1/order/reissue")
async def api_reissue_order_invoice(
    reissue_data: OrderReissue,
    wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Order:
    try:
        merchant = await get_merchant_for_user(wallet.wallet.user)
        assert merchant, "Merchant cannot be found"

        # Get existing order
        data = await get_order(merchant.id, reissue_data.id)
        assert data, "Order cannot be found"

        # Update shipping zone if provided
        if reissue_data.shipping_id:
            data.shipping_id = reissue_data.shipping_id

        # Generate new payment request
        payment_req, order = await build_order_with_payment(
            merchant.id, merchant.public_key, data
        )

        # Update order with new invoice details
        order_update = {
            "total": payment_req.total,
            "invoice_id": order.invoice_id,                # New payment hash
            "extra_data": json.dumps(order.extra.dict()),
        }

        await update_order(
            merchant.id,
            order.id,
            **order_update,
        )
        
        # Send new payment request to customer
        payment_req = PaymentRequest(
            id=order.id,
            message="Updated payment request",
            payment_options=[PaymentOption(type="ln", link=order.bolt11)],
        )
        
        dm_content = json.dumps(
            {"type": DirectMessageType.PAYMENT_REQUEST.value, **payment_req.dict()},
        )
        await reply_to_structured_dm(
            merchant, order.public_key, DirectMessageType.PAYMENT_REQUEST.value, dm_content
        )

        return await get_order(merchant.id, reissue_data.id)
    except Exception as ex:
        logger.warning(ex)
        raise HTTPException(
            status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
            detail="Cannot reissue order invoice",
        ) from ex

Order Restoration System

1. Order Recovery from Direct Messages (services.py:645-690)

DM-based Order Restoration

async def create_or_update_order_from_dm(
    merchant_id: str, merchant_pubkey: str, dm: DirectMessage
):
    type_, json_data = PartialDirectMessage.parse_message(dm.message)
    if not json_data or "id" not in json_data:
        return

    if type_ == DirectMessageType.CUSTOMER_ORDER:
        # Restore customer order from DM
        order, _ = await extract_customer_order_from_dm(
            merchant_id, merchant_pubkey, dm, json_data
        )
        new_order = await create_order(merchant_id, order)
        
        # Handle stall association updates
        if new_order.stall_id == "None" and order.stall_id != "None":
            await update_order(
                merchant_id,
                order.id,
                **{
                    "stall_id": order.stall_id,
                    "extra_data": json.dumps(order.extra.dict()),
                },
            )
        return

    if type_ == DirectMessageType.PAYMENT_REQUEST:
        # Update order with payment request details
        payment_request = PaymentRequest(**json_data)
        pr = payment_request.payment_options[0].link
        invoice = decode(pr)
        total = invoice.amount_msat / 1000 if invoice.amount_msat else 0
        await update_order(
            merchant_id,
            payment_request.id,
            **{"total": total, "invoice_id": invoice.payment_hash},
        )
        return

    if type_ == DirectMessageType.ORDER_PAID_OR_SHIPPED:
        # Update order status from status messages
        order_update = OrderStatusUpdate(**json_data)
        if order_update.paid:
            await update_order_paid_status(order_update.id, True)
        if order_update.shipped:
            await update_order_shipped_status(merchant_id, order_update.id, True)

2. Bulk Restoration API (views_api.py:580-595)

Complete Order Recovery

@nostrmarket_ext.put("/api/v1/orders/restore")
async def api_restore_orders_from_dms(
    wallet: WalletTypeInfo = Depends(require_admin_key),
):
    try:
        merchant = await get_merchant_for_user(wallet.wallet.user)
        assert merchant, "Merchant cannot be found"

        # Get all order-related direct messages
        dms = await get_orders_from_direct_messages(merchant.id)
        for dm in dms:
            try:
                # Attempt to restore/update each order from DM history
                await create_or_update_order_from_dm(
                    merchant.id, merchant.public_key, dm
                )
            except Exception as e:
                logger.debug(
                    f"Failed to restore order from event '{dm.event_id}': '{e!s}'."
                )
                continue

        return {"status": "Orders restoration completed!"}
    except AssertionError as ex:
        raise HTTPException(
            status_code=HTTPStatus.NOT_FOUND, detail=str(ex)
        ) from ex

Real-time Order Updates

1. WebSocket Order Notifications (order-list.js:281-296)

Live Order Addition

addOrder: async function (data) {
  if (
    !this.search.publicKey ||
    this.search.publicKey === data.customerPubkey       // Filter matches current view
  ) {
    const orderData = JSON.parse(data.dm.message)
    const i = this.orders.map(o => o.id).indexOf(orderData.id)
    if (i === -1) {                                      // Prevent duplicates
      const order = await this.getOrder(orderData.id)   // Fetch complete order data
      this.orders.unshift(order)                         // Add to top of list
    }
  }
}

2. Payment Status Updates (order-list.js:391-396)

Real-time Payment Confirmation

orderPaid: function (orderId) {
  const order = this.orders.find(o => o.id === orderId)
  if (order) {
    order.paid = true                                    // Update payment status immediately
  }
}

Advanced Order Management Features

1. Order Selection and Deep Linking (order-list.js:294-315)

Order Detail Navigation

orderSelected: async function (orderId, eventId) {
  const order = await this.getOrder(orderId)
  if (!order) {
    // Order missing - offer restoration from DM
    LNbits.utils
      .confirmDialog(
        'Order could not be found. Do you want to restore it from this direct message?'
      )
      .onOk(async () => {
        const restoredOrder = await this.restoreOrder(eventId)
        if (restoredOrder) {
          restoredOrder.expanded = true
          restoredOrder.isNew = false
          this.orders = [restoredOrder]                  // Show only restored order
        }
      })
    return
  }
  
  // Show order details
  order.expanded = true
  order.isNew = false
  this.orders = [order]                                  // Focus on single order
}

2. Customer Association and Filtering

Customer Management Integration

computed: {
  customerOptions: function () {
    const options = this.customers.map(c => ({
      label: this.buildCustomerLabel(c),                 // Include unread message counts
      value: c.public_key
    }))
    options.unshift({label: 'All', value: null, id: null}) // All customers option
    return options
  }
}

3. Shipping Zone Integration (order-list.js:348-355)

Dynamic Shipping Options

getStallZones: function (stallId) {
  const stall = this.stalls.find(s => s.id === stallId)
  if (!stall) return []

  return this.zoneOptions.filter(z =>
    stall.shipping_zones.find(s => s.id === z.id)       // Only zones supported by stall
  )
}

Database Operations

1. Order CRUD Operations (crud.py)

Order Creation

async def create_order(merchant_id: str, o: Order) -> Order:
    await db.execute(
        """
        INSERT INTO nostrmarket.orders (
            merchant_id, id, event_id, event_created_at, public_key,
            merchant_public_key, contact_data, extra_data, order_items,
            address, total, shipping_id, stall_id, invoice_id, paid, shipped
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """,
        (
            merchant_id, o.id, o.event_id, o.event_created_at, o.public_key,
            o.merchant_public_key, json.dumps(o.contact.dict()),
            json.dumps(o.extra.dict()), json.dumps([i.dict() for i in o.items]),
            o.address, o.total, o.shipping_id, o.stall_id, o.invoice_id,
            o.paid, o.shipped,
        ),
    )
    return o

Flexible Order Queries

async def get_orders(merchant_id: str, **kwargs) -> List[Order]:
    # Build dynamic WHERE clause from keyword arguments
    q = " AND ".join(
        [
            f"{field[0]} = :{field[0]}"
            for field in kwargs.items()
            if field[1] is not None
        ]
    )
    
    rows: list[dict] = await db.fetchall(
        f"SELECT * FROM nostrmarket.orders WHERE merchant_id = :merchant_id "
        f"{' AND ' + q if q else ''} ORDER BY event_created_at DESC",
        {"merchant_id": merchant_id, **kwargs},
    )
    return [Order.from_row(row) for row in rows]

2. Status Update Operations

Payment Status Updates

async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order]:
    await db.execute(
        "UPDATE nostrmarket.orders SET paid = :paid  WHERE id = :id",
        {"paid": paid, "id": order_id},
    )
    row: dict = await db.fetchone(
        "SELECT * FROM nostrmarket.orders WHERE id = :id", {"id": order_id}
    )
    return Order.from_row(row) if row else None

Shipping Status Updates

async def update_order_shipped_status(
    merchant_id: str, order_id: str, shipped: bool
) -> Optional[Order]:
    await db.execute(
        """
            UPDATE nostrmarket.orders
            SET shipped = :shipped
            WHERE merchant_id = :merchant_id AND id = :id
        """,
        {"shipped": shipped, "merchant_id": merchant_id, "id": order_id},
    )
    row: dict = await db.fetchone(
        "SELECT * FROM nostrmarket.orders WHERE merchant_id = :merchant_id AND id = :id",
        {"merchant_id": merchant_id, "id": order_id},
    )
    return Order.from_row(row) if row else None

Error Handling and Edge Cases

1. Order Restoration Failures

  • Missing Products: Orders reference products that no longer exist
  • Invalid Stall Association: Product moved between stalls after order creation
  • Corrupted DM Data: JSON parsing errors in message restoration
  • Payment Hash Conflicts: Duplicate invoice IDs from reissuance

2. Payment Processing Issues

  • Invoice Expiration: Lightning invoices expire after timeout
  • Partial Payments: Underpayment or overpayment scenarios
  • Payment Verification: Webhook delays or failures
  • Double Payment: Multiple payments for same order

3. Inventory Synchronization

  • Race Conditions: Multiple orders for limited stock
  • Negative Inventory: Orders processed despite insufficient stock
  • Product Updates: Price or availability changes after order placement
  • Stall Deactivation: Orders for disabled stalls or products

Integration Points

1. Payment System Integration

  • LNbits Invoice Creation: Automatic Lightning invoice generation
  • Payment Monitoring: Real-time payment confirmation via webhooks
  • Refund Processing: Automated refunds for failed orders
  • Multi-currency Support: Fiat pricing with BTC conversion

2. Inventory Management Integration

  • Stock Validation: Pre-order inventory checking
  • Automatic Deduction: Post-payment inventory updates
  • Backorder Handling: Out-of-stock order management
  • Restock Notifications: Customer alerts for inventory replenishment

3. Communication System Integration

  • Status Updates: Automated customer notifications
  • Order Confirmations: Receipt and tracking information
  • Shipping Notifications: Fulfillment status updates
  • Support Integration: Customer service ticket creation

This comprehensive order management system provides complete lifecycle tracking from initial order placement through final fulfillment, with robust error handling, real-time updates, and flexible merchant tools for efficient order processing and customer communication.