# 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 ```mermaid 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 ```python 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 ```python 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 ```python 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 ```sql 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 ```javascript 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`) ```javascript 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`) ```javascript 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 ```javascript 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 ```javascript 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`) ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```vue
:pubkey="merchant.pubkey" :profiles="profiles" >
``` ### 2. Order Data Enrichment (`CustomerOrders.vue:208-220`) #### Order Enhancement Pipeline ```javascript 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`) ```javascript 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 ```javascript 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 ```javascript 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 ```python 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 ```python @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 ```python @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 ```python @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 ```python 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 ```python @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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```python 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 ```python 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 ```python 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 ```python 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.