feat: show orders per stall
This commit is contained in:
parent
b446629707
commit
40c25ad085
10 changed files with 255 additions and 29 deletions
24
crud.py
24
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:
|
async def create_order(user_id: str, o: Order) -> Order:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, order_items, invoice_id, total)
|
INSERT INTO nostrmarket.orders (user_id, id, event_id, pubkey, address, contact_data, order_items, stall_id, invoice_id, total)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
user_id,
|
user_id,
|
||||||
|
|
@ -329,6 +329,7 @@ async def create_order(user_id: str, o: Order) -> Order:
|
||||||
o.address,
|
o.address,
|
||||||
json.dumps(o.contact.dict() if o.contact else {}),
|
json.dumps(o.contact.dict() if o.contact else {}),
|
||||||
json.dumps([i.dict() for i in o.items]),
|
json.dumps([i.dict() for i in o.items]),
|
||||||
|
o.stall_id,
|
||||||
o.invoice_id,
|
o.invoice_id,
|
||||||
o.total,
|
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
|
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]
|
||||||
|
|
|
||||||
|
|
@ -78,11 +78,12 @@ async def m001_initial(db):
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
event_id TEXT,
|
event_id TEXT,
|
||||||
pubkey EXT NOT NULL,
|
pubkey TEXT NOT NULL,
|
||||||
contact_data TEXT NOT NULL DEFAULT '{empty_object}',
|
contact_data TEXT NOT NULL DEFAULT '{empty_object}',
|
||||||
order_items TEXT NOT NULL,
|
order_items TEXT NOT NULL,
|
||||||
address TEXT,
|
address TEXT,
|
||||||
total REAL NOT NULL,
|
total REAL NOT NULL,
|
||||||
|
stall_id TEXT NOT NULL,
|
||||||
invoice_id TEXT NOT NULL,
|
invoice_id TEXT NOT NULL,
|
||||||
paid BOOLEAN NOT NULL DEFAULT false,
|
paid BOOLEAN NOT NULL DEFAULT false,
|
||||||
shipped BOOLEAN NOT NULL DEFAULT false,
|
shipped BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
|
||||||
22
models.py
22
models.py
|
|
@ -269,6 +269,26 @@ class PartialOrder(BaseModel):
|
||||||
def validate_order(self):
|
def validate_order(self):
|
||||||
assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
|
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:
|
async def total_sats(self, products: List[Product]) -> float:
|
||||||
product_prices = {}
|
product_prices = {}
|
||||||
for p in products:
|
for p in products:
|
||||||
|
|
@ -286,10 +306,12 @@ class PartialOrder(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Order(PartialOrder):
|
class Order(PartialOrder):
|
||||||
|
stall_id: str
|
||||||
invoice_id: str
|
invoice_id: str
|
||||||
total: float
|
total: float
|
||||||
paid: bool = False
|
paid: bool = False
|
||||||
shipped: bool = False
|
shipped: bool = False
|
||||||
|
time: int
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Order":
|
def from_row(cls, row: Row) -> "Order":
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,44 @@
|
||||||
<div>
|
<div>
|
||||||
xx1
|
<q-table
|
||||||
</div>
|
flat
|
||||||
|
dense
|
||||||
|
:data="orders"
|
||||||
|
row-key="id"
|
||||||
|
:columns="ordersTable.columns"
|
||||||
|
:pagination.sync="ordersTable.pagination"
|
||||||
|
:filter="filter"
|
||||||
|
>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
@click="props.row.expanded= !props.row.expanded"
|
||||||
|
:icon="props.row.expanded? 'remove' : 'add'"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="id" :props="props"> {{toShortId(props.row.id)}} </q-td>
|
||||||
|
<q-td key="total" :props="props"> {{props.row.total}} </q-td>
|
||||||
|
|
||||||
|
<q-td key="paid" :props="props"> {{props.row.paid}} </q-td>
|
||||||
|
<q-td key="shipped" :props="props"> {{props.row.shipped}} </q-td>
|
||||||
|
|
||||||
|
<q-td key="pubkey" :props="props">
|
||||||
|
{{toShortId(props.row.pubkey)}}
|
||||||
|
</q-td>
|
||||||
|
<q-td key="time" :props="props"> {{formatDate(props.row.time)}} </q-td>
|
||||||
|
</q-tr>
|
||||||
|
<q-tr v-if="props.row.expanded" :props="props">
|
||||||
|
<q-td colspan="100%">
|
||||||
|
<div class="row items-center q-mb-lg">
|
||||||
|
<div class="col-12"></div>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,95 @@
|
||||||
async function orderList(path) {
|
async function orderList(path) {
|
||||||
const template = await loadTemplateAsync(path)
|
const template = await loadTemplateAsync(path)
|
||||||
Vue.component('order-list', {
|
Vue.component('order-list', {
|
||||||
name: 'order-list',
|
name: 'order-list',
|
||||||
props: ['adminkey', 'inkey'],
|
props: ['stall-id', 'adminkey', 'inkey'],
|
||||||
template,
|
template,
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
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 () {
|
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
}
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -187,10 +187,10 @@
|
||||||
<q-tab-panel name="orders">
|
<q-tab-panel name="orders">
|
||||||
<div v-if="stall">
|
<div v-if="stall">
|
||||||
<order-list
|
<order-list
|
||||||
:adminkey="adminkey"
|
:adminkey="adminkey"
|
||||||
:inkey="inkey"
|
:inkey="inkey"
|
||||||
:wallet-options="walletOptions"
|
:stall-id="stallId"
|
||||||
></order-list>
|
></order-list>
|
||||||
</div>
|
</div>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
{{props.row.name}}</a
|
{{props.row.name}}</a
|
||||||
>
|
>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
<q-td key="currency" :props="props"> {{props.row.currency}} </q-td>
|
||||||
<q-td key="description" :props="props">
|
<q-td key="description" :props="props">
|
||||||
{{props.row.config.description}}
|
{{props.row.config.description}}
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,12 @@ async function stallList(path) {
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
field: 'id'
|
field: 'id'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'currency',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Currency',
|
||||||
|
field: 'currency'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'description',
|
name: 'description',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
|
|
||||||
3
tasks.py
3
tasks.py
|
|
@ -127,9 +127,6 @@ async def handle_new_order(order: PartialOrder):
|
||||||
### check that event_id not parsed already
|
### check that event_id not parsed already
|
||||||
|
|
||||||
order.validate_order()
|
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
|
first_product_id = order.items[0].product_id
|
||||||
wallet_id = await get_wallet_for_product(first_product_id)
|
wallet_id = await get_wallet_for_product(first_product_id)
|
||||||
|
|
|
||||||
64
views_api.py
64
views_api.py
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
@ -27,6 +28,8 @@ from .crud import (
|
||||||
get_merchant_for_user,
|
get_merchant_for_user,
|
||||||
get_order,
|
get_order,
|
||||||
get_order_by_event_id,
|
get_order_by_event_id,
|
||||||
|
get_orders,
|
||||||
|
get_orders_for_stall,
|
||||||
get_product,
|
get_product,
|
||||||
get_products,
|
get_products,
|
||||||
get_products_by_ids,
|
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}")
|
@nostrmarket_ext.delete("/api/v1/stall/{stall_id}")
|
||||||
async def api_delete_stall(
|
async def api_delete_stall(
|
||||||
stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
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)
|
data: PartialOrder, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
) -> Optional[PaymentRequest]:
|
) -> Optional[PaymentRequest]:
|
||||||
try:
|
try:
|
||||||
|
# print("### new order: ", json.dumps(data.dict()))
|
||||||
if await get_order(wallet.wallet.user, data.id):
|
if await get_order(wallet.wallet.user, data.id):
|
||||||
return None
|
return None
|
||||||
if data.event_id and await get_order_by_event_id(
|
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(
|
products = await get_products_by_ids(
|
||||||
wallet.wallet.user, [p.product_id for p in data.items]
|
wallet.wallet.user, [p.product_id for p in data.items]
|
||||||
)
|
)
|
||||||
|
data.validate_order_items(products)
|
||||||
|
|
||||||
total_amount = await data.total_sats(products)
|
total_amount = await data.total_sats(products)
|
||||||
|
|
||||||
wallet_id = await get_wallet_for_product(data.items[0].product_id)
|
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)
|
await create_order(wallet.wallet.user, order)
|
||||||
|
|
||||||
return PaymentRequest(
|
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 ########################################
|
######################################## OTHER ########################################
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue