diff --git a/crud.py b/crud.py
index c74512f..be080b7 100644
--- a/crud.py
+++ b/crud.py
@@ -318,8 +318,8 @@ async def delete_product(user_id: str, product_id: str) -> None:
async def create_order(user_id: str, o: Order) -> Order:
await db.execute(
f"""
- INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, order_items, invoice_id, total)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, order_items, stall_id, invoice_id, total)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
user_id,
@@ -329,6 +329,7 @@ async def create_order(user_id: str, o: Order) -> Order:
o.address,
json.dumps(o.contact.dict() if o.contact else {}),
json.dumps([i.dict() for i in o.items]),
+ o.stall_id,
o.invoice_id,
o.total,
),
@@ -359,3 +360,22 @@ async def get_order_by_event_id(user_id: str, event_id: str) -> Optional[Order]:
),
)
return Order.from_row(row) if row else None
+
+
+async def get_orders(user_id: str) -> List[Order]:
+ rows = await db.fetchall(
+ "SELECT * FROM nostrmarket.orders WHERE user_id = ?",
+ (user_id,),
+ )
+ return [Order.from_row(row) for row in rows]
+
+
+async def get_orders_for_stall(user_id: str, stall_id: str) -> List[Order]:
+ rows = await db.fetchall(
+ "SELECT * FROM nostrmarket.orders WHERE user_id = ? AND stall_id = ?",
+ (
+ user_id,
+ stall_id,
+ ),
+ )
+ return [Order.from_row(row) for row in rows]
diff --git a/migrations.py b/migrations.py
index 4ed088b..7c50264 100644
--- a/migrations.py
+++ b/migrations.py
@@ -78,11 +78,12 @@ async def m001_initial(db):
user_id TEXT NOT NULL,
id TEXT PRIMARY KEY,
event_id TEXT,
- pubkey EXT NOT NULL,
+ pubkey TEXT NOT NULL,
contact_data TEXT NOT NULL DEFAULT '{empty_object}',
order_items TEXT NOT NULL,
address TEXT,
total REAL NOT NULL,
+ stall_id TEXT NOT NULL,
invoice_id TEXT NOT NULL,
paid BOOLEAN NOT NULL DEFAULT false,
shipped BOOLEAN NOT NULL DEFAULT false,
diff --git a/models.py b/models.py
index 0cfee8f..a8415e1 100644
--- a/models.py
+++ b/models.py
@@ -269,6 +269,26 @@ class PartialOrder(BaseModel):
def validate_order(self):
assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
+ def validate_order_items(self, product_list: List[Product]):
+ assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
+ assert (
+ len(product_list) != 0
+ ), f"No products found for order. Order: '{self.id}'"
+
+ product_ids = [p.id for p in product_list]
+ for item in self.items:
+ if item.product_id not in product_ids:
+ raise ValueError(
+ f"Order ({self.id}) item product does not exist: {item.product_id}"
+ )
+
+ stall_id = product_list[0].stall_id
+ for p in product_list:
+ if p.stall_id != stall_id:
+ raise ValueError(
+ f"Order ({self.id}) has products from different stalls"
+ )
+
async def total_sats(self, products: List[Product]) -> float:
product_prices = {}
for p in products:
@@ -286,10 +306,12 @@ class PartialOrder(BaseModel):
class Order(PartialOrder):
+ stall_id: str
invoice_id: str
total: float
paid: bool = False
shipped: bool = False
+ time: int
@classmethod
def from_row(cls, row: Row) -> "Order":
diff --git a/static/components/order-list/order-list.html b/static/components/order-list/order-list.html
index 147c21f..99c556a 100644
--- a/static/components/order-list/order-list.html
+++ b/static/components/order-list/order-list.html
@@ -1,3 +1,44 @@
- xx1
-
\ No newline at end of file
+
+
+
+
+
+
+
+ {{toShortId(props.row.id)}}
+ {{props.row.total}}
+
+ {{props.row.paid}}
+ {{props.row.shipped}}
+
+
+ {{toShortId(props.row.pubkey)}}
+
+ {{formatDate(props.row.time)}}
+
+
+
+
+
+
+
+
+
diff --git a/static/components/order-list/order-list.js b/static/components/order-list/order-list.js
index 15b1528..9d5e1f7 100644
--- a/static/components/order-list/order-list.js
+++ b/static/components/order-list/order-list.js
@@ -1,18 +1,95 @@
async function orderList(path) {
- const template = await loadTemplateAsync(path)
- Vue.component('order-list', {
- name: 'order-list',
- props: ['adminkey', 'inkey'],
- template,
-
- data: function () {
- return {
+ const template = await loadTemplateAsync(path)
+ Vue.component('order-list', {
+ name: 'order-list',
+ props: ['stall-id', 'adminkey', 'inkey'],
+ template,
+
+ data: function () {
+ return {
+ orders: [],
+
+ filter: '',
+ ordersTable: {
+ columns: [
+ {
+ name: '',
+ align: 'left',
+ label: '',
+ field: ''
+ },
+ {
+ name: 'id',
+ align: 'left',
+ label: 'ID',
+ field: 'id'
+ },
+ {
+ name: 'total',
+ align: 'left',
+ label: 'Total',
+ field: 'total'
+ },
+ {
+ name: 'paid',
+ align: 'left',
+ label: 'Paid',
+ field: 'paid'
+ },
+ {
+ name: 'shipped',
+ align: 'left',
+ label: 'Shipped',
+ field: 'shipped'
+ },
+ {
+ name: 'pubkey',
+ align: 'left',
+ label: 'Customer',
+ field: 'pubkey'
+ },
+ {
+ name: 'time',
+ align: 'left',
+ label: 'Date',
+ field: 'time'
+ }
+ ],
+ pagination: {
+ rowsPerPage: 10
+ }
}
- },
- methods: {
- },
- created: async function () {
}
- })
- }
-
\ No newline at end of file
+ },
+ methods: {
+ toShortId: function (value) {
+ return value.substring(0, 5) + '...' + value.substring(value.length - 5)
+ },
+ formatDate: function (value) {
+ return Quasar.utils.date.formatDate(
+ new Date(value * 1000),
+ 'YYYY-MM-DD HH:mm'
+ )
+ },
+ getOrders: async function () {
+ try {
+ const ordersPath = this.stallId
+ ? `/stall/order/${this.stallId}`
+ : '/order'
+ const {data} = await LNbits.api.request(
+ 'GET',
+ '/nostrmarket/api/v1' + ordersPath,
+ this.inkey
+ )
+ this.orders = data.map(s => ({...s, expanded: false}))
+ console.log('### this.orders', this.orders)
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ }
+ },
+ created: async function () {
+ await this.getOrders()
+ }
+ })
+}
diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html
index 9ffe49a..4239f68 100644
--- a/static/components/stall-details/stall-details.html
+++ b/static/components/stall-details/stall-details.html
@@ -187,10 +187,10 @@
+ :adminkey="adminkey"
+ :inkey="inkey"
+ :stall-id="stallId"
+ >
diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html
index 86ae34e..bc6236e 100644
--- a/static/components/stall-list/stall-list.html
+++ b/static/components/stall-list/stall-list.html
@@ -50,7 +50,7 @@
{{props.row.name}}
-
+ {{props.row.currency}}
{{props.row.config.description}}
diff --git a/static/components/stall-list/stall-list.js b/static/components/stall-list/stall-list.js
index 5fd8ffd..d41c062 100644
--- a/static/components/stall-list/stall-list.js
+++ b/static/components/stall-list/stall-list.js
@@ -35,6 +35,12 @@ async function stallList(path) {
label: 'Name',
field: 'id'
},
+ {
+ name: 'currency',
+ align: 'left',
+ label: 'Currency',
+ field: 'currency'
+ },
{
name: 'description',
align: 'left',
diff --git a/tasks.py b/tasks.py
index 9a78344..f4b9477 100644
--- a/tasks.py
+++ b/tasks.py
@@ -127,9 +127,6 @@ async def handle_new_order(order: PartialOrder):
### check that event_id not parsed already
order.validate_order()
- assert (
- len(order.items) != 0
- ), f"Order has no items. Order: '{order.id}' ({order.event_id})"
first_product_id = order.items[0].product_id
wallet_id = await get_wallet_for_product(first_product_id)
diff --git a/views_api.py b/views_api.py
index bca744d..e21193d 100644
--- a/views_api.py
+++ b/views_api.py
@@ -1,3 +1,4 @@
+import json
from http import HTTPStatus
from typing import List, Optional
@@ -27,6 +28,8 @@ from .crud import (
get_merchant_for_user,
get_order,
get_order_by_event_id,
+ get_orders,
+ get_orders_for_stall,
get_product,
get_products,
get_products_by_ids,
@@ -283,6 +286,22 @@ async def api_get_stall_products(
)
+@nostrmarket_ext.get("/api/v1/stall/order/{stall_id}")
+async def api_get_stall_orders(
+ stall_id: str,
+ wallet: WalletTypeInfo = Depends(require_invoice_key),
+):
+ try:
+ orders = await get_orders_for_stall(wallet.wallet.user, stall_id)
+ return orders
+ except Exception as ex:
+ logger.warning(ex)
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail="Cannot get stall products",
+ )
+
+
@nostrmarket_ext.delete("/api/v1/stall/{stall_id}")
async def api_delete_stall(
stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
@@ -435,6 +454,7 @@ async def api_create_order(
data: PartialOrder, wallet: WalletTypeInfo = Depends(require_admin_key)
) -> Optional[PaymentRequest]:
try:
+ # print("### new order: ", json.dumps(data.dict()))
if await get_order(wallet.wallet.user, data.id):
return None
if data.event_id and await get_order_by_event_id(
@@ -445,6 +465,8 @@ async def api_create_order(
products = await get_products_by_ids(
wallet.wallet.user, [p.product_id for p in data.items]
)
+ data.validate_order_items(products)
+
total_amount = await data.total_sats(products)
wallet_id = await get_wallet_for_product(data.items[0].product_id)
@@ -460,7 +482,12 @@ async def api_create_order(
},
)
- order = Order(**data.dict(), invoice_id=payment_hash, total=total_amount)
+ order = Order(
+ **data.dict(),
+ stall_id=products[0].stall_id,
+ invoice_id=payment_hash,
+ total=total_amount,
+ )
await create_order(wallet.wallet.user, order)
return PaymentRequest(
@@ -474,6 +501,41 @@ async def api_create_order(
)
+nostrmarket_ext.get("/api/v1/order/{order_id}")
+
+
+async def api_get_order(order_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
+ try:
+ order = await get_order(wallet.wallet.user, order_id)
+ if not order:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Order does not exist.",
+ )
+ return order
+ except HTTPException as ex:
+ raise ex
+ except Exception as ex:
+ logger.warning(ex)
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail="Cannot get order",
+ )
+
+
+@nostrmarket_ext.get("/api/v1/order")
+async def api_get_orders(wallet: WalletTypeInfo = Depends(get_key_type)):
+ try:
+ orders = await get_orders(wallet.wallet.user)
+ return orders
+ except Exception as ex:
+ logger.warning(ex)
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail="Cannot get orders",
+ )
+
+
######################################## OTHER ########################################