commit
b4452b2141
15 changed files with 1055 additions and 65 deletions
23
__init__.py
23
__init__.py
|
|
@ -1,4 +1,5 @@
|
|||
import asyncio
|
||||
from asyncio import Queue, Task
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
|
@ -25,15 +26,29 @@ def nostrmarket_renderer():
|
|||
return template_renderer(["lnbits/extensions/nostrmarket/templates"])
|
||||
|
||||
|
||||
scheduled_tasks: List[asyncio.Task] = []
|
||||
recieve_event_queue: Queue = Queue()
|
||||
send_req_queue: Queue = Queue()
|
||||
scheduled_tasks: List[Task] = []
|
||||
|
||||
from .tasks import subscribe_nostrclient_ws, wait_for_paid_invoices
|
||||
|
||||
from .tasks import (
|
||||
subscribe_to_nostr_client,
|
||||
wait_for_nostr_events,
|
||||
wait_for_paid_invoices,
|
||||
)
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
|
||||
|
||||
def nostrmarket_start():
|
||||
async def _subscribe_to_nostr_client():
|
||||
await subscribe_to_nostr_client(recieve_event_queue, send_req_queue)
|
||||
|
||||
async def _wait_for_nostr_events():
|
||||
await wait_for_nostr_events(recieve_event_queue, send_req_queue)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
task1 = loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
||||
task2 = loop.create_task(catch_everything_and_restart(subscribe_nostrclient_ws))
|
||||
scheduled_tasks.append([task1, task2])
|
||||
task2 = loop.create_task(catch_everything_and_restart(_subscribe_to_nostr_client))
|
||||
task3 = loop.create_task(catch_everything_and_restart(_wait_for_nostr_events))
|
||||
scheduled_tasks.append([task1, task2, task3])
|
||||
|
|
|
|||
142
crud.py
142
crud.py
|
|
@ -1,5 +1,4 @@
|
|||
import json
|
||||
import time
|
||||
from typing import List, Optional
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
|
@ -7,6 +6,7 @@ from lnbits.helpers import urlsafe_short_hash
|
|||
from . import db
|
||||
from .models import (
|
||||
Merchant,
|
||||
Order,
|
||||
PartialMerchant,
|
||||
PartialProduct,
|
||||
PartialStall,
|
||||
|
|
@ -45,6 +45,23 @@ async def get_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]:
|
|||
return Merchant.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_merchant_by_pubkey(public_key: str) -> Optional[Merchant]:
|
||||
row = await db.fetchone(
|
||||
"""SELECT * FROM nostrmarket.merchants WHERE public_key = ? """,
|
||||
(public_key,),
|
||||
)
|
||||
|
||||
return Merchant.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_public_keys_for_merchants() -> List[str]:
|
||||
rows = await db.fetchall(
|
||||
"""SELECT public_key FROM nostrmarket.merchants""",
|
||||
)
|
||||
|
||||
return [row[0] for row in rows]
|
||||
|
||||
|
||||
async def get_merchant_for_user(user_id: str) -> Optional[Merchant]:
|
||||
row = await db.fetchone(
|
||||
"""SELECT * FROM nostrmarket.merchants WHERE user_id = ? """,
|
||||
|
|
@ -189,7 +206,7 @@ async def delete_stall(user_id: str, stall_id: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
######################################## STALL ########################################
|
||||
######################################## PRODUCTS ########################################
|
||||
|
||||
|
||||
async def create_product(user_id: str, data: PartialProduct) -> Product:
|
||||
|
|
@ -197,7 +214,7 @@ async def create_product(user_id: str, data: PartialProduct) -> Product:
|
|||
|
||||
await db.execute(
|
||||
f"""
|
||||
INSERT INTO nostrmarket.products (user_id, id, stall_id, name, images, price, quantity, category_list, meta)
|
||||
INSERT INTO nostrmarket.products (user_id, id, stall_id, name, image, price, quantity, category_list, meta)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
|
|
@ -261,6 +278,28 @@ async def get_products(user_id: str, stall_id: str) -> List[Product]:
|
|||
return [Product.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def get_products_by_ids(user_id: str, product_ids: List[str]) -> List[Product]:
|
||||
q = ",".join(["?"] * len(product_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT id, stall_id, name, price, quantity, category_list, meta FROM nostrmarket.products WHERE user_id = ? AND id IN ({q})",
|
||||
(user_id, *product_ids),
|
||||
)
|
||||
return [Product.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def get_wallet_for_product(product_id: str) -> Optional[str]:
|
||||
row = await db.fetchone(
|
||||
"""
|
||||
SELECT s.wallet FROM nostrmarket.products p
|
||||
INNER JOIN nostrmarket.stalls s
|
||||
ON p.stall_id = s.id
|
||||
WHERE p.id=?
|
||||
""",
|
||||
(product_id,),
|
||||
)
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
async def delete_product(user_id: str, product_id: str) -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM nostrmarket.products WHERE user_id =? AND id = ?",
|
||||
|
|
@ -269,3 +308,100 @@ async def delete_product(user_id: str, product_id: str) -> None:
|
|||
product_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
######################################## ORDERS ########################################
|
||||
|
||||
|
||||
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, extra_data, order_items, stall_id, invoice_id, total)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
o.id,
|
||||
o.event_id,
|
||||
o.pubkey,
|
||||
o.address,
|
||||
json.dumps(o.contact.dict() if o.contact else {}),
|
||||
json.dumps(o.extra.dict()),
|
||||
json.dumps([i.dict() for i in o.items]),
|
||||
o.stall_id,
|
||||
o.invoice_id,
|
||||
o.total,
|
||||
),
|
||||
)
|
||||
order = await get_order(user_id, o.id)
|
||||
assert order, "Newly created order couldn't be retrieved"
|
||||
|
||||
return order
|
||||
|
||||
|
||||
async def get_order(user_id: str, order_id: str) -> Optional[Order]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM nostrmarket.orders WHERE user_id =? AND id = ?",
|
||||
(
|
||||
user_id,
|
||||
order_id,
|
||||
),
|
||||
)
|
||||
return Order.from_row(row) if row else None
|
||||
|
||||
|
||||
async def get_order_by_event_id(user_id: str, event_id: str) -> Optional[Order]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM nostrmarket.orders WHERE user_id =? AND event_id =?",
|
||||
(
|
||||
user_id,
|
||||
event_id,
|
||||
),
|
||||
)
|
||||
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 = ? ORDER BY time DESC",
|
||||
(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 = ? ORDER BY time DESC",
|
||||
(
|
||||
user_id,
|
||||
stall_id,
|
||||
),
|
||||
)
|
||||
return [Order.from_row(row) for row in rows]
|
||||
|
||||
|
||||
async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order]:
|
||||
await db.execute(
|
||||
f"UPDATE nostrmarket.orders SET paid = ? WHERE id = ?",
|
||||
(paid, order_id),
|
||||
)
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM nostrmarket.orders WHERE id = ?",
|
||||
(order_id,),
|
||||
)
|
||||
return Order.from_row(row) if row else None
|
||||
|
||||
|
||||
async def update_order_shipped_status(
|
||||
user_id: str, order_id: str, shipped: bool
|
||||
) -> Optional[Order]:
|
||||
await db.execute(
|
||||
f"UPDATE nostrmarket.orders SET shipped = ? WHERE user_id = ? AND id = ?",
|
||||
(shipped, user_id, order_id),
|
||||
)
|
||||
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM nostrmarket.orders WHERE id = ?",
|
||||
(order_id,),
|
||||
)
|
||||
return Order.from_row(row) if row else None
|
||||
|
|
|
|||
16
helpers.py
16
helpers.py
|
|
@ -1,7 +1,7 @@
|
|||
import base64
|
||||
import json
|
||||
import secrets
|
||||
from typing import Optional
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
import secp256k1
|
||||
from cffi import FFI
|
||||
|
|
@ -31,7 +31,7 @@ def decrypt_message(encoded_message: str, encryption_key) -> str:
|
|||
return unpadded_data.decode()
|
||||
|
||||
|
||||
def encrypt_message(message: str, encryption_key, iv: Optional[bytes]) -> str:
|
||||
def encrypt_message(message: str, encryption_key, iv: Optional[bytes] = None) -> str:
|
||||
padder = padding.PKCS7(128).padder()
|
||||
padded_data = padder.update(message.encode()) + padder.finalize()
|
||||
|
||||
|
|
@ -73,9 +73,11 @@ def copy_x(output, x32, y32, data):
|
|||
return 1
|
||||
|
||||
|
||||
def is_json(string: str):
|
||||
def order_from_json(s: str) -> Tuple[Optional[Any], Optional[str]]:
|
||||
try:
|
||||
json.loads(string)
|
||||
except ValueError as e:
|
||||
return False
|
||||
return True
|
||||
order = json.loads(s)
|
||||
return (
|
||||
(order, None) if (type(order) is dict) and "items" in order else (None, s)
|
||||
)
|
||||
except ValueError:
|
||||
return None, s
|
||||
|
|
|
|||
|
|
@ -71,39 +71,28 @@ async def m001_initial(db):
|
|||
"""
|
||||
Initial orders table.
|
||||
"""
|
||||
empty_object = "{}"
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE nostrmarket.orders (
|
||||
user_id TEXT NOT NULL,
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
username TEXT,
|
||||
pubkey TEXT,
|
||||
shipping_zone TEXT NOT NULL,
|
||||
event_id TEXT,
|
||||
pubkey TEXT NOT NULL,
|
||||
contact_data TEXT NOT NULL DEFAULT '{empty_object}',
|
||||
extra_data TEXT NOT NULL DEFAULT '{empty_object}',
|
||||
order_items TEXT NOT NULL,
|
||||
address TEXT,
|
||||
email TEXT,
|
||||
total REAL NOT NULL,
|
||||
stall_id TEXT NOT NULL,
|
||||
invoice_id TEXT NOT NULL,
|
||||
paid BOOLEAN NOT NULL,
|
||||
shipped BOOLEAN NOT NULL,
|
||||
paid BOOLEAN NOT NULL DEFAULT false,
|
||||
shipped BOOLEAN NOT NULL DEFAULT false,
|
||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
"""
|
||||
Initial order details table.
|
||||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE nostrmarket.order_details (
|
||||
id TEXT PRIMARY KEY,
|
||||
order_id TEXT NOT NULL,
|
||||
product_id TEXT NOT NULL,
|
||||
quantity INTEGER NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
"""
|
||||
Initial market table.
|
||||
"""
|
||||
|
|
@ -117,19 +106,6 @@ async def m001_initial(db):
|
|||
"""
|
||||
)
|
||||
|
||||
"""
|
||||
Initial market stalls table.
|
||||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE nostrmarket.market_stalls (
|
||||
id TEXT PRIMARY KEY,
|
||||
market_id TEXT NOT NULL,
|
||||
stall_id TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
"""
|
||||
Initial chat messages table.
|
||||
"""
|
||||
|
|
|
|||
148
models.py
148
models.py
|
|
@ -6,7 +6,14 @@ from typing import List, Optional
|
|||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .helpers import sign_message_hash
|
||||
from lnbits.utils.exchange_rates import btc_price, fiat_amount_as_satoshis
|
||||
|
||||
from .helpers import (
|
||||
decrypt_message,
|
||||
encrypt_message,
|
||||
get_shared_secret,
|
||||
sign_message_hash,
|
||||
)
|
||||
from .nostr.event import NostrEvent
|
||||
|
||||
######################################## NOSTR ########################################
|
||||
|
|
@ -39,6 +46,28 @@ class Merchant(PartialMerchant):
|
|||
def sign_hash(self, hash: bytes) -> str:
|
||||
return sign_message_hash(self.private_key, hash)
|
||||
|
||||
def decrypt_message(self, encrypted_message: str, public_key: str) -> str:
|
||||
encryption_key = get_shared_secret(self.private_key, public_key)
|
||||
return decrypt_message(encrypted_message, encryption_key)
|
||||
|
||||
def encrypt_message(self, clear_text_message: str, public_key: str) -> str:
|
||||
encryption_key = get_shared_secret(self.private_key, public_key)
|
||||
return encrypt_message(clear_text_message, encryption_key)
|
||||
|
||||
def build_dm_event(self, message: str, to_pubkey: str) -> NostrEvent:
|
||||
content = self.encrypt_message(message, to_pubkey)
|
||||
event = NostrEvent(
|
||||
pubkey=self.public_key,
|
||||
created_at=round(time.time()),
|
||||
kind=4,
|
||||
tags=[["p", to_pubkey]],
|
||||
content=content,
|
||||
)
|
||||
event.id = event.event_id
|
||||
event.sig = self.sign_hash(bytes.fromhex(event.id))
|
||||
|
||||
return event
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Merchant":
|
||||
merchant = cls(**dict(row))
|
||||
|
|
@ -213,3 +242,120 @@ class Product(PartialProduct, Nostrable):
|
|||
product.config = ProductConfig(**json.loads(row["meta"]))
|
||||
product.categories = json.loads(row["category_list"])
|
||||
return product
|
||||
|
||||
|
||||
class ProductOverview(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
price: float
|
||||
|
||||
|
||||
######################################## ORDERS ########################################
|
||||
|
||||
|
||||
class OrderItem(BaseModel):
|
||||
product_id: str
|
||||
quantity: int
|
||||
|
||||
|
||||
class OrderContact(BaseModel):
|
||||
nostr: Optional[str]
|
||||
phone: Optional[str]
|
||||
email: Optional[str]
|
||||
|
||||
|
||||
class OrderExtra(BaseModel):
|
||||
products: List[ProductOverview]
|
||||
currency: str
|
||||
btc_price: str
|
||||
|
||||
@classmethod
|
||||
async def from_products(cls, products: List[Product]):
|
||||
currency = products[0].config.currency
|
||||
exchange_rate = (
|
||||
(await btc_price(currency)) if currency and currency != "sat" else 1
|
||||
)
|
||||
return OrderExtra(products=products, currency=currency, btc_price=exchange_rate)
|
||||
|
||||
|
||||
class PartialOrder(BaseModel):
|
||||
id: str
|
||||
event_id: Optional[str]
|
||||
pubkey: str
|
||||
items: List[OrderItem]
|
||||
contact: Optional[OrderContact]
|
||||
address: Optional[str]
|
||||
|
||||
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:
|
||||
product_prices[p.id] = p
|
||||
|
||||
amount: float = 0 # todo
|
||||
for item in self.items:
|
||||
price = product_prices[item.product_id].price
|
||||
currency = product_prices[item.product_id].config.currency or "sat"
|
||||
if currency != "sat":
|
||||
price = await fiat_amount_as_satoshis(price, currency)
|
||||
amount += item.quantity * price
|
||||
|
||||
return amount
|
||||
|
||||
|
||||
class Order(PartialOrder):
|
||||
stall_id: str
|
||||
invoice_id: str
|
||||
total: float
|
||||
paid: bool = False
|
||||
shipped: bool = False
|
||||
time: Optional[int]
|
||||
extra: OrderExtra
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Order":
|
||||
contact = OrderContact(**json.loads(row["contact_data"]))
|
||||
extra = OrderExtra(**json.loads(row["extra_data"]))
|
||||
items = [OrderItem(**z) for z in json.loads(row["order_items"])]
|
||||
order = cls(**dict(row), contact=contact, items=items, extra=extra)
|
||||
return order
|
||||
|
||||
|
||||
class OrderStatusUpdate(BaseModel):
|
||||
id: str
|
||||
message: Optional[str]
|
||||
paid: Optional[bool]
|
||||
shipped: Optional[bool]
|
||||
|
||||
|
||||
class PaymentOption(BaseModel):
|
||||
type: str
|
||||
link: str
|
||||
|
||||
|
||||
class PaymentRequest(BaseModel):
|
||||
id: str
|
||||
message: Optional[str]
|
||||
payment_options: List[PaymentOption]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
from threading import Thread
|
||||
from typing import Callable
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from websocket import WebSocketApp
|
||||
|
||||
from lnbits.app import settings
|
||||
from lnbits.helpers import url_for
|
||||
|
|
@ -10,7 +14,7 @@ from .event import NostrEvent
|
|||
async def publish_nostr_event(e: NostrEvent):
|
||||
url = url_for("/nostrclient/api/v1/publish", external=True)
|
||||
data = dict(e)
|
||||
# print("### published", dict(data))
|
||||
print("### published", dict(data))
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
await client.post(
|
||||
|
|
@ -19,3 +23,44 @@ async def publish_nostr_event(e: NostrEvent):
|
|||
)
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
||||
|
||||
async def connect_to_nostrclient_ws(
|
||||
on_open: Callable, on_message: Callable
|
||||
) -> WebSocketApp:
|
||||
def on_error(_, error):
|
||||
logger.warning(error)
|
||||
|
||||
logger.debug(f"Subscribing to websockets for nostrclient extension")
|
||||
ws = WebSocketApp(
|
||||
f"ws://localhost:{settings.port}/nostrclient/api/v1/filters",
|
||||
on_message=on_message,
|
||||
on_open=on_open,
|
||||
on_error=on_error,
|
||||
)
|
||||
|
||||
wst = Thread(target=ws.run_forever)
|
||||
wst.daemon = True
|
||||
wst.start()
|
||||
|
||||
return ws
|
||||
|
||||
|
||||
# async def handle_event(event, pubkeys):
|
||||
# tags = [t[1] for t in event["tags"] if t[0] == "p"]
|
||||
# to_merchant = None
|
||||
# if tags and len(tags) > 0:
|
||||
# to_merchant = tags[0]
|
||||
|
||||
# if event["pubkey"] in pubkeys or to_merchant in pubkeys:
|
||||
# logger.debug(f"Event sent to {to_merchant}")
|
||||
# pubkey = to_merchant if to_merchant in pubkeys else event["pubkey"]
|
||||
# # Send event to market extension
|
||||
# await send_event_to_market(event=event, pubkey=pubkey)
|
||||
|
||||
|
||||
# async def send_event_to_market(event: dict, pubkey: str):
|
||||
# # Sends event to market extension, for decrypt and handling
|
||||
# market_url = url_for(f"/market/api/v1/nip04/{pubkey}", external=True)
|
||||
# async with httpx.AsyncClient() as client:
|
||||
# await client.post(url=market_url, json=event)
|
||||
|
|
|
|||
205
static/components/order-list/order-list.html
Normal file
205
static/components/order-list/order-list.html
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
<div>
|
||||
<q-table
|
||||
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>
|
||||
<!-- todo: currency per order -->
|
||||
|
||||
<q-td key="paid" :props="props">
|
||||
<q-checkbox
|
||||
v-model="props.row.paid"
|
||||
:label="props.row.paid ? 'Yes' : 'No'"
|
||||
disable
|
||||
readonly
|
||||
size="sm"
|
||||
></q-checkbox>
|
||||
</q-td>
|
||||
<q-td key="shipped" :props="props">
|
||||
<q-checkbox
|
||||
v-model="props.row.shipped"
|
||||
@input="showShipOrderDialog(props.row)"
|
||||
:label="props.row.shipped ? 'Yes' : 'No'"
|
||||
size="sm"
|
||||
></q-checkbox>
|
||||
</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 no-wrap q-mb-md">
|
||||
<div class="col-3 q-pr-lg">Products:</div>
|
||||
<div class="col-8">
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col-1">Quantity</div>
|
||||
<div class="col-1"></div>
|
||||
<div class="col-10">Name</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-1"></div>
|
||||
</div>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col-3 q-pr-lg"></div>
|
||||
<div class="col-8">
|
||||
<div
|
||||
v-for="item in props.row.items"
|
||||
class="row items-center no-wrap q-mb-md"
|
||||
>
|
||||
<div class="col-1">{{item.quantity}}</div>
|
||||
<div class="col-1">x</div>
|
||||
<div class="col-10">
|
||||
{{productOverview(props.row, item.product_id)}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-1"></div>
|
||||
</div>
|
||||
<div class="row items-center no-wrap q-mb-md q-mt-md">
|
||||
<div class="col-3 q-pr-lg">Order ID:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
disabled
|
||||
v-model.trim="props.row.id"
|
||||
type="text"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-3 col-sm-1"></div>
|
||||
</div>
|
||||
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col-3 q-pr-lg">Address:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
disabled
|
||||
v-model.trim="props.row.address"
|
||||
type="text"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-3 col-sm-1"></div>
|
||||
</div>
|
||||
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col-3 q-pr-lg">Customer Public Key:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
disabled
|
||||
v-model.trim="props.row.pubkey"
|
||||
type="text"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-3 col-sm-1"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="props.row.contact.phone"
|
||||
class="row items-center no-wrap q-mb-md"
|
||||
>
|
||||
<div class="col-3 q-pr-lg">Phone:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
disabled
|
||||
v-model.trim="props.row.contact.phone"
|
||||
type="text"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-3 col-sm-1"></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.row.contact.email"
|
||||
class="row items-center no-wrap q-mb-md"
|
||||
>
|
||||
<div class="col-3 q-pr-lg">Email:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
disabled
|
||||
v-model.trim="props.row.contact.email"
|
||||
type="text"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-3 col-sm-1"></div>
|
||||
</div>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col-3 q-pr-lg">Invoice ID:</div>
|
||||
<div class="col-6 col-sm-8 q-pr-lg">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
disabled
|
||||
v-model.trim="props.row.invoice_id"
|
||||
type="text"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-3 col-sm-1"></div>
|
||||
</div>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<q-dialog v-model="showShipDialog" position="top">
|
||||
<q-card v-if="selectedOrder" class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||
<q-form @submit="updateOrderShipped" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="shippingMessage"
|
||||
label="Shipping Message"
|
||||
type="textarea"
|
||||
rows="4"
|
||||
></q-input>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
:label="selectedOrder.shipped? 'Unship Order' : 'Ship Order'"
|
||||
></q-btn>
|
||||
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
137
static/components/order-list/order-list.js
Normal file
137
static/components/order-list/order-list.js
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
async function orderList(path) {
|
||||
const template = await loadTemplateAsync(path)
|
||||
Vue.component('order-list', {
|
||||
name: 'order-list',
|
||||
props: ['stall-id', 'adminkey', 'inkey'],
|
||||
template,
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
orders: [],
|
||||
selectedOrder: null,
|
||||
shippingMessage: '',
|
||||
showShipDialog: false,
|
||||
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: {
|
||||
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'
|
||||
)
|
||||
},
|
||||
productOverview: function (order, productId) {
|
||||
product = order.extra.products.find(p => p.id === productId)
|
||||
if (product) {
|
||||
return `${product.name} (${product.price} ${order.extra.currency})`
|
||||
}
|
||||
return ''
|
||||
},
|
||||
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)
|
||||
}
|
||||
},
|
||||
updateOrderShipped: async function () {
|
||||
console.log('### order', this.selectedOrder)
|
||||
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,
|
||||
shipped: this.selectedOrder.shipped
|
||||
}
|
||||
)
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Order updated!'
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
this.showShipDialog = false
|
||||
},
|
||||
showShipOrderDialog: function (order) {
|
||||
this.selectedOrder = order
|
||||
this.shippingMessage = order.shipped
|
||||
? `The order has been shipped! Order ID: '${order.id}' `
|
||||
: `The order has NOT yet been shipped! Order ID: '${order.id}'`
|
||||
|
||||
// do not change the status yet
|
||||
this.selectedOrder.shipped = !order.shipped
|
||||
this.showShipDialog = true
|
||||
}
|
||||
},
|
||||
created: async function () {
|
||||
await this.getOrders()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -185,7 +185,13 @@
|
|||
</div>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="orders">
|
||||
<div v-if="stall"></div>
|
||||
<div v-if="stall">
|
||||
<order-list
|
||||
:adminkey="adminkey"
|
||||
:inkey="inkey"
|
||||
:stall-id="stallId"
|
||||
></order-list>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
<q-dialog v-model="productDialog.showDialog" position="top">
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
{{props.row.name}}</a
|
||||
>
|
||||
</q-td>
|
||||
|
||||
<q-td key="currency" :props="props"> {{props.row.currency}} </q-td>
|
||||
<q-td key="description" :props="props">
|
||||
{{props.row.config.description}}
|
||||
</q-td>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ async function stallList(path) {
|
|||
label: 'Name',
|
||||
field: 'id'
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
align: 'left',
|
||||
label: 'Currency',
|
||||
field: 'currency'
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
align: 'left',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const merchant = async () => {
|
|||
await shippingZones('static/components/shipping-zones/shipping-zones.html')
|
||||
await stallDetails('static/components/stall-details/stall-details.html')
|
||||
await stallList('static/components/stall-list/stall-list.html')
|
||||
await orderList('static/components/order-list/order-list.html')
|
||||
|
||||
const nostr = window.NostrTools
|
||||
|
||||
|
|
|
|||
156
tasks.py
156
tasks.py
|
|
@ -1,18 +1,31 @@
|
|||
import asyncio
|
||||
import json
|
||||
import threading
|
||||
from asyncio import Queue
|
||||
|
||||
import httpx
|
||||
import websocket
|
||||
from loguru import logger
|
||||
from websocket import WebSocketApp
|
||||
|
||||
from lnbits.core import get_wallet
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.helpers import Optional, url_for
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import (
|
||||
get_merchant_by_pubkey,
|
||||
get_public_keys_for_merchants,
|
||||
get_wallet_for_product,
|
||||
update_order_paid_status,
|
||||
)
|
||||
from .helpers import order_from_json
|
||||
from .models import OrderStatusUpdate, PartialOrder
|
||||
from .nostr.event import NostrEvent
|
||||
from .nostr.nostr_client import connect_to_nostrclient_ws, publish_nostr_event
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
invoice_queue = Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
|
||||
while True:
|
||||
|
|
@ -24,8 +37,141 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
if payment.extra.get("tag") != "nostrmarket":
|
||||
return
|
||||
|
||||
print("### on_invoice_paid")
|
||||
order_id = payment.extra.get("order_id")
|
||||
merchant_pubkey = payment.extra.get("merchant_pubkey")
|
||||
if not order_id or not merchant_pubkey:
|
||||
return None
|
||||
|
||||
await handle_order_paid(order_id, merchant_pubkey)
|
||||
|
||||
|
||||
async def subscribe_nostrclient_ws():
|
||||
async def handle_order_paid(order_id: str, merchant_pubkey: str):
|
||||
try:
|
||||
order = await update_order_paid_status(order_id, True)
|
||||
assert order, f"Paid order cannot be found. Order id: {order_id}"
|
||||
order_status = OrderStatusUpdate(
|
||||
id=order_id, message="Payment received.", paid=True, shipped=order.shipped
|
||||
)
|
||||
|
||||
merchant = await get_merchant_by_pubkey(merchant_pubkey)
|
||||
assert merchant, f"Merchant cannot be found for order {order_id}"
|
||||
dm_content = json.dumps(
|
||||
order_status.dict(), separators=(",", ":"), ensure_ascii=False
|
||||
)
|
||||
|
||||
dm_event = merchant.build_dm_event(dm_content, order.pubkey)
|
||||
await publish_nostr_event(dm_event)
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
||||
|
||||
async def subscribe_to_nostr_client(recieve_event_queue: Queue, send_req_queue: Queue):
|
||||
print("### subscribe_nostrclient_ws")
|
||||
|
||||
def on_open(_):
|
||||
logger.info("Connected to 'nostrclient' websocket")
|
||||
|
||||
def on_message(_, message):
|
||||
print("### on_message", message)
|
||||
recieve_event_queue.put_nowait(message)
|
||||
|
||||
# wait for 'nostrclient' extension to initialize
|
||||
await asyncio.sleep(5)
|
||||
ws: WebSocketApp = None
|
||||
while True:
|
||||
try:
|
||||
req = None
|
||||
if not ws:
|
||||
ws = await connect_to_nostrclient_ws(on_open, on_message)
|
||||
# be sure the connection is open
|
||||
await asyncio.sleep(3)
|
||||
req = await send_req_queue.get()
|
||||
ws.send(json.dumps(req))
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
if req:
|
||||
await send_req_queue.put(req)
|
||||
ws = None # todo close
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
async def wait_for_nostr_events(recieve_event_queue: Queue, send_req_queue: Queue):
|
||||
public_keys = await get_public_keys_for_merchants()
|
||||
for p in public_keys:
|
||||
await send_req_queue.put(
|
||||
["REQ", f"direct-messages:{p}", {"kind": 4, "#p": [p]}]
|
||||
)
|
||||
|
||||
while True:
|
||||
message = await recieve_event_queue.get()
|
||||
await handle_message(message)
|
||||
|
||||
|
||||
async def handle_message(msg: str):
|
||||
try:
|
||||
type, subscription_id, event = json.loads(msg)
|
||||
_, public_key = subscription_id.split(":")
|
||||
if type.upper() == "EVENT":
|
||||
event = NostrEvent(**event)
|
||||
if event.kind == 4:
|
||||
await handle_nip04_message(public_key, event)
|
||||
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
||||
|
||||
async def handle_nip04_message(public_key: str, event: NostrEvent):
|
||||
merchant = await get_merchant_by_pubkey(public_key)
|
||||
assert merchant, f"Merchant not found for public key '{public_key}'"
|
||||
|
||||
clear_text_msg = merchant.decrypt_message(event.content, event.pubkey)
|
||||
dm_content = await handle_dirrect_message(event.pubkey, event.id, clear_text_msg)
|
||||
if dm_content:
|
||||
dm_event = merchant.build_dm_event(dm_content, event.pubkey)
|
||||
await publish_nostr_event(dm_event)
|
||||
|
||||
|
||||
async def handle_dirrect_message(
|
||||
from_pubkey: str, event_id: str, msg: str
|
||||
) -> Optional[str]:
|
||||
order, text_msg = order_from_json(msg)
|
||||
try:
|
||||
if order:
|
||||
order["pubkey"] = from_pubkey
|
||||
order["event_id"] = event_id
|
||||
return await handle_new_order(PartialOrder(**order))
|
||||
else:
|
||||
print("### text_msg", text_msg)
|
||||
return None
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
return None
|
||||
|
||||
|
||||
async def handle_new_order(order: PartialOrder):
|
||||
### todo: check that event_id not parsed already
|
||||
|
||||
order.validate_order()
|
||||
|
||||
first_product_id = order.items[0].product_id
|
||||
wallet_id = await get_wallet_for_product(first_product_id)
|
||||
assert wallet_id, f"Cannot find wallet id for product id: {first_product_id}"
|
||||
|
||||
wallet = await get_wallet(wallet_id)
|
||||
assert wallet, f"Cannot find wallet for product id: {first_product_id}"
|
||||
|
||||
market_url = url_for(f"/nostrmarket/api/v1/order", external=True)
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
url=market_url,
|
||||
headers={
|
||||
"X-Api-Key": wallet.adminkey,
|
||||
},
|
||||
json=order.dict(),
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if data:
|
||||
return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@
|
|||
<script src="{{ url_for('nostrmarket_static', path='components/shipping-zones/shipping-zones.js') }}"></script>
|
||||
<script src="{{ url_for('nostrmarket_static', path='components/stall-details/stall-details.js') }}"></script>
|
||||
<script src="{{ url_for('nostrmarket_static', path='components/stall-list/stall-list.js') }}"></script>
|
||||
<script src="{{ url_for('nostrmarket_static', path='components/order-list/order-list.js') }}"></script>
|
||||
<script src="{{ url_for('nostrmarket_static', path='js/index.js') }}"></script>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
184
views_api.py
184
views_api.py
|
|
@ -6,18 +6,20 @@ from fastapi import Depends
|
|||
from fastapi.exceptions import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core import create_invoice
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
check_admin,
|
||||
get_key_type,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.extensions.nostrmarket.nostr.event import NostrEvent
|
||||
from lnbits.utils.exchange_rates import currencies
|
||||
|
||||
from . import nostrmarket_ext
|
||||
from . import nostrmarket_ext, scheduled_tasks
|
||||
from .crud import (
|
||||
create_merchant,
|
||||
create_order,
|
||||
create_product,
|
||||
create_stall,
|
||||
create_zone,
|
||||
|
|
@ -25,12 +27,19 @@ from .crud import (
|
|||
delete_stall,
|
||||
delete_zone,
|
||||
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,
|
||||
get_stall,
|
||||
get_stalls,
|
||||
get_wallet_for_product,
|
||||
get_zone,
|
||||
get_zones,
|
||||
update_order_shipped_status,
|
||||
update_product,
|
||||
update_stall,
|
||||
update_zone,
|
||||
|
|
@ -38,14 +47,21 @@ from .crud import (
|
|||
from .models import (
|
||||
Merchant,
|
||||
Nostrable,
|
||||
Order,
|
||||
OrderExtra,
|
||||
OrderStatusUpdate,
|
||||
PartialMerchant,
|
||||
PartialOrder,
|
||||
PartialProduct,
|
||||
PartialStall,
|
||||
PartialZone,
|
||||
PaymentOption,
|
||||
PaymentRequest,
|
||||
Product,
|
||||
Stall,
|
||||
Zone,
|
||||
)
|
||||
from .nostr.event import NostrEvent
|
||||
from .nostr.nostr_client import publish_nostr_event
|
||||
|
||||
######################################## MERCHANT ########################################
|
||||
|
|
@ -80,7 +96,7 @@ async def api_get_merchant(
|
|||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create merchant",
|
||||
detail="Cannot get merchant",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -95,13 +111,13 @@ async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[
|
|||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create merchant",
|
||||
detail="Cannot get zone",
|
||||
)
|
||||
|
||||
|
||||
@nostrmarket_ext.post("/api/v1/zone")
|
||||
async def api_create_zone(
|
||||
data: PartialZone, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
data: PartialZone, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
try:
|
||||
zone = await create_zone(wallet.wallet.user, data)
|
||||
|
|
@ -110,7 +126,7 @@ async def api_create_zone(
|
|||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create merchant",
|
||||
detail="Cannot create zone",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -136,7 +152,7 @@ async def api_update_zone(
|
|||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create merchant",
|
||||
detail="Cannot update zone",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -157,7 +173,7 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi
|
|||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create merchant",
|
||||
detail="Cannot delete zone",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -274,6 +290,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)
|
||||
|
|
@ -418,6 +450,132 @@ async def api_delete_product(
|
|||
)
|
||||
|
||||
|
||||
######################################## ORDERS ########################################
|
||||
|
||||
|
||||
@nostrmarket_ext.post("/api/v1/order")
|
||||
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(
|
||||
wallet.wallet.user, data.event_id
|
||||
):
|
||||
return None
|
||||
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, "Cannot find merchant!"
|
||||
|
||||
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)
|
||||
assert wallet_id, "Missing wallet for order `{data.id}`"
|
||||
|
||||
payment_hash, invoice = await create_invoice(
|
||||
wallet_id=wallet_id,
|
||||
amount=round(total_amount),
|
||||
memo=f"Order '{data.id}' for pubkey '{data.pubkey}'",
|
||||
extra={
|
||||
"tag": "nostrmarket",
|
||||
"order_id": data.id,
|
||||
"merchant_pubkey": merchant.public_key,
|
||||
},
|
||||
)
|
||||
|
||||
order = Order(
|
||||
**data.dict(),
|
||||
stall_id=products[0].stall_id,
|
||||
invoice_id=payment_hash,
|
||||
total=total_amount,
|
||||
extra=await OrderExtra.from_products(products),
|
||||
)
|
||||
await create_order(wallet.wallet.user, order)
|
||||
|
||||
return PaymentRequest(
|
||||
id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)]
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot 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",
|
||||
)
|
||||
|
||||
|
||||
@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 != None, "Shipped value is required for order"
|
||||
order = await update_order_shipped_status(
|
||||
wallet.wallet.user, data.id, data.shipped
|
||||
)
|
||||
assert order, "Cannot find updated order"
|
||||
|
||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||
assert merchant, f"Merchant cannot be found for order {data.id}"
|
||||
|
||||
data.paid = order.paid
|
||||
dm_content = json.dumps(data.dict(), separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
dm_event = merchant.build_dm_event(dm_content, order.pubkey)
|
||||
await publish_nostr_event(dm_event)
|
||||
|
||||
return order
|
||||
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot update order",
|
||||
)
|
||||
|
||||
|
||||
######################################## OTHER ########################################
|
||||
|
||||
|
||||
|
|
@ -426,6 +584,16 @@ async def api_list_currencies_available():
|
|||
return list(currencies.keys())
|
||||
|
||||
|
||||
@nostrmarket_ext.delete("/api/v1", status_code=HTTPStatus.OK)
|
||||
async def api_stop(wallet: WalletTypeInfo = Depends(check_admin)):
|
||||
for t in scheduled_tasks:
|
||||
try:
|
||||
t.cancel()
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
||||
return {"success": True}
|
||||
|
||||
######################################## HELPERS ########################################
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue