- 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>
921 lines
No EOL
30 KiB
Markdown
921 lines
No EOL
30 KiB
Markdown
# 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
|
|
<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
|
|
```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. |