commit
d106879a09
17 changed files with 580 additions and 96 deletions
|
|
@ -46,7 +46,7 @@ def nostrmarket_start():
|
||||||
|
|
||||||
async def _wait_for_nostr_events():
|
async def _wait_for_nostr_events():
|
||||||
# wait for this extension to initialize
|
# wait for this extension to initialize
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(15)
|
||||||
await wait_for_nostr_events(nostr_client)
|
await wait_for_nostr_events(nostr_client)
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
|
||||||
116
crud.py
116
crud.py
|
|
@ -5,6 +5,8 @@ from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .models import (
|
from .models import (
|
||||||
|
Customer,
|
||||||
|
CustomerProfile,
|
||||||
DirectMessage,
|
DirectMessage,
|
||||||
Merchant,
|
Merchant,
|
||||||
MerchantConfig,
|
MerchantConfig,
|
||||||
|
|
@ -391,12 +393,13 @@ async def create_order(merchant_id: str, o: Order) -> Order:
|
||||||
address,
|
address,
|
||||||
contact_data,
|
contact_data,
|
||||||
extra_data,
|
extra_data,
|
||||||
order_items,
|
order_items,
|
||||||
|
shipping_id,
|
||||||
stall_id,
|
stall_id,
|
||||||
invoice_id,
|
invoice_id,
|
||||||
total
|
total
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(event_id) DO NOTHING
|
ON CONFLICT(event_id) DO NOTHING
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
|
|
@ -410,6 +413,7 @@ async def create_order(merchant_id: str, o: Order) -> Order:
|
||||||
json.dumps(o.contact.dict() if o.contact else {}),
|
json.dumps(o.contact.dict() if o.contact else {}),
|
||||||
json.dumps(o.extra.dict()),
|
json.dumps(o.extra.dict()),
|
||||||
json.dumps([i.dict() for i in o.items]),
|
json.dumps([i.dict() for i in o.items]),
|
||||||
|
o.shipping_id,
|
||||||
o.stall_id,
|
o.stall_id,
|
||||||
o.invoice_id,
|
o.invoice_id,
|
||||||
o.total,
|
o.total,
|
||||||
|
|
@ -443,33 +447,38 @@ async def get_order_by_event_id(merchant_id: str, event_id: str) -> Optional[Ord
|
||||||
return Order.from_row(row) if row else None
|
return Order.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_orders(merchant_id: str) -> List[Order]:
|
async def get_orders(merchant_id: str, **kwargs) -> List[Order]:
|
||||||
|
q = " AND ".join(
|
||||||
|
[f"{field[0]} = ?" for field in kwargs.items() if field[1] != None]
|
||||||
|
)
|
||||||
|
values = ()
|
||||||
|
if q:
|
||||||
|
q = f"AND {q}"
|
||||||
|
values = (v for v in kwargs.values() if v != None)
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
"SELECT * FROM nostrmarket.orders WHERE merchant_id = ? ORDER BY time DESC",
|
f"SELECT * FROM nostrmarket.orders WHERE merchant_id = ? {q} ORDER BY time DESC",
|
||||||
(merchant_id,),
|
(merchant_id, *values),
|
||||||
)
|
)
|
||||||
return [Order.from_row(row) for row in rows]
|
return [Order.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_orders_for_stall(merchant_id: str, stall_id: str) -> List[Order]:
|
async def get_orders_for_stall(
|
||||||
|
merchant_id: str, stall_id: str, **kwargs
|
||||||
|
) -> List[Order]:
|
||||||
|
q = " AND ".join(
|
||||||
|
[f"{field[0]} = ?" for field in kwargs.items() if field[1] != None]
|
||||||
|
)
|
||||||
|
values = ()
|
||||||
|
if q:
|
||||||
|
q = f"AND {q}"
|
||||||
|
values = (v for v in kwargs.values() if v != None)
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
"SELECT * FROM nostrmarket.orders WHERE merchant_id = ? AND stall_id = ? ORDER BY time DESC",
|
f"SELECT * FROM nostrmarket.orders WHERE merchant_id = ? AND stall_id = ? {q} ORDER BY time DESC",
|
||||||
(
|
(merchant_id, stall_id, *values),
|
||||||
merchant_id,
|
|
||||||
stall_id,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return [Order.from_row(row) for row in rows]
|
return [Order.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_public_keys_for_orders(merchant_id: str) -> List[str]:
|
|
||||||
rows = await db.fetchall(
|
|
||||||
"SELECT DISTINCT public_key FROM nostrmarket.orders WHERE merchant_id = ?",
|
|
||||||
(merchant_id,),
|
|
||||||
)
|
|
||||||
return [row[0] for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_last_order_time(public_key: str) -> int:
|
async def get_last_order_time(public_key: str) -> int:
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"""
|
"""
|
||||||
|
|
@ -596,9 +605,68 @@ async def delete_merchant_direct_messages(merchant_id: str) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_public_keys_for_direct_messages(merchant_id: str) -> List[str]:
|
######################################## CUSTOMERS ########################################
|
||||||
rows = await db.fetchall(
|
|
||||||
"SELECT DISTINCT public_key FROM nostrmarket.direct_messages WHERE merchant_id = ?",
|
|
||||||
(merchant_id),
|
async def create_customer(merchant_id: str, data: Customer) -> Customer:
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
INSERT INTO nostrmarket.customers (merchant_id, public_key, meta)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
merchant_id,
|
||||||
|
data.public_key,
|
||||||
|
json.dumps(data.profile) if data.profile else "{}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
customer = await get_customer(merchant_id, data.public_key)
|
||||||
|
assert customer, "Newly created customer couldn't be retrieved"
|
||||||
|
return customer
|
||||||
|
|
||||||
|
|
||||||
|
async def get_customer(merchant_id: str, public_key: str) -> Optional[Customer]:
|
||||||
|
row = await db.fetchone(
|
||||||
|
"SELECT * FROM nostrmarket.customers WHERE merchant_id = ? AND public_key = ?",
|
||||||
|
(
|
||||||
|
merchant_id,
|
||||||
|
public_key,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return Customer.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_customers(merchant_id: str) -> List[Customer]:
|
||||||
|
rows = await db.fetchall(
|
||||||
|
"SELECT * FROM nostrmarket.customers WHERE merchant_id = ?", (merchant_id,)
|
||||||
|
)
|
||||||
|
return [Customer.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_customers() -> List[Customer]:
|
||||||
|
rows = await db.fetchall("SELECT * FROM nostrmarket.customers")
|
||||||
|
return [Customer.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def update_customer_profile(
|
||||||
|
public_key: str, event_created_at: int, profile: CustomerProfile
|
||||||
|
):
|
||||||
|
await db.execute(
|
||||||
|
f"UPDATE nostrmarket.customers SET event_created_at = ?, meta = ? WHERE public_key = ?",
|
||||||
|
(event_created_at, json.dumps(profile.dict()), public_key),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def increment_customer_unread_messages(public_key: str):
|
||||||
|
await db.execute(
|
||||||
|
f"UPDATE nostrmarket.customers SET unread_messages = unread_messages + 1 WHERE public_key = ?",
|
||||||
|
(public_key,),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_customer_no_unread_messages(public_key: str):
|
||||||
|
await db.execute(
|
||||||
|
f"UPDATE nostrmarket.customers SET unread_messages = 0 WHERE public_key = ?",
|
||||||
|
(public_key,),
|
||||||
)
|
)
|
||||||
return [row[0] for row in rows]
|
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,6 @@ def copy_x(output, x32, y32, data):
|
||||||
def order_from_json(s: str) -> Tuple[Optional[Any], Optional[str]]:
|
def order_from_json(s: str) -> Tuple[Optional[Any], Optional[str]]:
|
||||||
try:
|
try:
|
||||||
order = json.loads(s)
|
order = json.loads(s)
|
||||||
return (
|
return (order, s) if (type(order) is dict) and "items" in order else (None, s)
|
||||||
(order, s) if (type(order) is dict) and "items" in order else (None, s)
|
|
||||||
)
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None, s
|
return None, s
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ async def m001_initial(db):
|
||||||
order_items TEXT NOT NULL,
|
order_items TEXT NOT NULL,
|
||||||
address TEXT,
|
address TEXT,
|
||||||
total REAL NOT NULL,
|
total REAL NOT NULL,
|
||||||
|
shipping_id TEXT NOT NULL,
|
||||||
stall_id TEXT 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,
|
||||||
|
|
@ -125,3 +126,18 @@ async def m001_initial(db):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"CREATE INDEX idx_event_id ON nostrmarket.direct_messages (event_id)"
|
"CREATE INDEX idx_event_id ON nostrmarket.direct_messages (event_id)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Initial customers table.
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE nostrmarket.customers (
|
||||||
|
merchant_id TEXT NOT NULL,
|
||||||
|
public_key TEXT NOT NULL,
|
||||||
|
event_created_at INTEGER,
|
||||||
|
unread_messages INTEGER NOT NULL DEFAULT 1,
|
||||||
|
meta TEXT NOT NULL DEFAULT '{}'
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
|
||||||
40
models.py
40
models.py
|
|
@ -2,7 +2,7 @@ import json
|
||||||
import time
|
import time
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
@ -313,6 +313,8 @@ class OrderExtra(BaseModel):
|
||||||
products: List[ProductOverview]
|
products: List[ProductOverview]
|
||||||
currency: str
|
currency: str
|
||||||
btc_price: str
|
btc_price: str
|
||||||
|
shipping_cost: float = 0
|
||||||
|
shipping_cost_sat: float = 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_products(cls, products: List[Product]):
|
async def from_products(cls, products: List[Product]):
|
||||||
|
|
@ -329,6 +331,7 @@ class PartialOrder(BaseModel):
|
||||||
event_created_at: Optional[int]
|
event_created_at: Optional[int]
|
||||||
public_key: str
|
public_key: str
|
||||||
merchant_public_key: str
|
merchant_public_key: str
|
||||||
|
shipping_id: str
|
||||||
items: List[OrderItem]
|
items: List[OrderItem]
|
||||||
contact: Optional[OrderContact]
|
contact: Optional[OrderContact]
|
||||||
address: Optional[str]
|
address: Optional[str]
|
||||||
|
|
@ -356,20 +359,25 @@ class PartialOrder(BaseModel):
|
||||||
f"Order ({self.id}) has products from different stalls"
|
f"Order ({self.id}) has products from different stalls"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def total_sats(self, products: List[Product]) -> float:
|
async def costs_in_sats(
|
||||||
|
self, products: List[Product], shipping_cost: float
|
||||||
|
) -> Tuple[float, float]:
|
||||||
product_prices = {}
|
product_prices = {}
|
||||||
for p in products:
|
for p in products:
|
||||||
product_prices[p.id] = p
|
product_prices[p.id] = p
|
||||||
|
|
||||||
amount: float = 0 # todo
|
product_cost: float = 0 # todo
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
price = product_prices[item.product_id].price
|
price = product_prices[item.product_id].price
|
||||||
currency = product_prices[item.product_id].config.currency or "sat"
|
currency = product_prices[item.product_id].config.currency or "sat"
|
||||||
if currency != "sat":
|
if currency != "sat":
|
||||||
price = await fiat_amount_as_satoshis(price, currency)
|
price = await fiat_amount_as_satoshis(price, currency)
|
||||||
amount += item.quantity * price
|
product_cost += item.quantity * price
|
||||||
|
|
||||||
return amount
|
if currency != "sat":
|
||||||
|
shipping_cost = await fiat_amount_as_satoshis(shipping_cost, currency)
|
||||||
|
|
||||||
|
return product_cost, shipping_cost
|
||||||
|
|
||||||
|
|
||||||
class Order(PartialOrder):
|
class Order(PartialOrder):
|
||||||
|
|
@ -427,3 +435,25 @@ class DirectMessage(PartialDirectMessage):
|
||||||
def from_row(cls, row: Row) -> "DirectMessage":
|
def from_row(cls, row: Row) -> "DirectMessage":
|
||||||
dm = cls(**dict(row))
|
dm = cls(**dict(row))
|
||||||
return dm
|
return dm
|
||||||
|
|
||||||
|
|
||||||
|
######################################## CUSTOMERS ########################################
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerProfile(BaseModel):
|
||||||
|
name: Optional[str]
|
||||||
|
about: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class Customer(BaseModel):
|
||||||
|
merchant_id: str
|
||||||
|
public_key: str
|
||||||
|
event_created_at: Optional[int]
|
||||||
|
profile: Optional[CustomerProfile]
|
||||||
|
unread_messages: int = 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_row(cls, row: Row) -> "Customer":
|
||||||
|
customer = cls(**dict(row))
|
||||||
|
customer.profile = CustomerProfile(**json.loads(row["meta"]))
|
||||||
|
return customer
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ class NostrClient:
|
||||||
async def subscribe_to_direct_messages(self, public_key: str, since: int):
|
async def subscribe_to_direct_messages(self, public_key: str, since: int):
|
||||||
in_messages_filter = {"kind": 4, "#p": [public_key]}
|
in_messages_filter = {"kind": 4, "#p": [public_key]}
|
||||||
out_messages_filter = {"kind": 4, "authors": [public_key]}
|
out_messages_filter = {"kind": 4, "authors": [public_key]}
|
||||||
if since != 0:
|
if since and since != 0:
|
||||||
in_messages_filter["since"] = since
|
in_messages_filter["since"] = since
|
||||||
out_messages_filter["since"] = since
|
out_messages_filter["since"] = since
|
||||||
|
|
||||||
|
|
@ -92,6 +92,15 @@ class NostrClient:
|
||||||
["REQ", f"product-events:{public_key}", product_filter]
|
["REQ", f"product-events:{public_key}", product_filter]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def subscribe_to_user_profile(self, public_key: str, since: int):
|
||||||
|
profile_filter = {"kind": 0, "authors": [public_key]}
|
||||||
|
if since and since != 0:
|
||||||
|
profile_filter["since"] = since + 1
|
||||||
|
|
||||||
|
await self.send_req_queue.put(
|
||||||
|
["REQ", f"user-profile-events:{public_key}", profile_filter]
|
||||||
|
)
|
||||||
|
|
||||||
async def unsubscribe_from_direct_messages(self, public_key: str):
|
async def unsubscribe_from_direct_messages(self, public_key: str):
|
||||||
await self.send_req_queue.put(["CLOSE", f"direct-messages-in:{public_key}"])
|
await self.send_req_queue.put(["CLOSE", f"direct-messages-in:{public_key}"])
|
||||||
await self.send_req_queue.put(["CLOSE", f"direct-messages-out:{public_key}"])
|
await self.send_req_queue.put(["CLOSE", f"direct-messages-out:{public_key}"])
|
||||||
|
|
|
||||||
82
services.py
82
services.py
|
|
@ -4,11 +4,15 @@ from typing import List, Optional, Tuple
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core import create_invoice, get_wallet
|
from lnbits.core import create_invoice, get_wallet
|
||||||
|
from lnbits.core.services import websocketUpdater
|
||||||
|
|
||||||
from . import nostr_client
|
from . import nostr_client
|
||||||
from .crud import (
|
from .crud import (
|
||||||
|
CustomerProfile,
|
||||||
|
create_customer,
|
||||||
create_direct_message,
|
create_direct_message,
|
||||||
create_order,
|
create_order,
|
||||||
|
get_customer,
|
||||||
get_merchant_by_pubkey,
|
get_merchant_by_pubkey,
|
||||||
get_order,
|
get_order,
|
||||||
get_order_by_event_id,
|
get_order_by_event_id,
|
||||||
|
|
@ -16,6 +20,9 @@ from .crud import (
|
||||||
get_products_by_ids,
|
get_products_by_ids,
|
||||||
get_stalls,
|
get_stalls,
|
||||||
get_wallet_for_product,
|
get_wallet_for_product,
|
||||||
|
get_zone,
|
||||||
|
increment_customer_unread_messages,
|
||||||
|
update_customer_profile,
|
||||||
update_order_paid_status,
|
update_order_paid_status,
|
||||||
update_product,
|
update_product,
|
||||||
update_product_quantity,
|
update_product_quantity,
|
||||||
|
|
@ -23,6 +30,7 @@ from .crud import (
|
||||||
)
|
)
|
||||||
from .helpers import order_from_json
|
from .helpers import order_from_json
|
||||||
from .models import (
|
from .models import (
|
||||||
|
Customer,
|
||||||
Merchant,
|
Merchant,
|
||||||
Nostrable,
|
Nostrable,
|
||||||
Order,
|
Order,
|
||||||
|
|
@ -53,8 +61,12 @@ async def create_new_order(
|
||||||
merchant.id, [p.product_id for p in data.items]
|
merchant.id, [p.product_id for p in data.items]
|
||||||
)
|
)
|
||||||
data.validate_order_items(products)
|
data.validate_order_items(products)
|
||||||
|
shipping_zone = await get_zone(merchant.id, data.shipping_id)
|
||||||
|
assert shipping_zone, f"Shipping zone not found for order '{data.id}'"
|
||||||
|
|
||||||
total_amount = await data.total_sats(products)
|
product_cost_sat, shipping_cost_sat = await data.costs_in_sats(
|
||||||
|
products, shipping_zone.cost
|
||||||
|
)
|
||||||
|
|
||||||
wallet_id = await get_wallet_for_product(data.items[0].product_id)
|
wallet_id = await get_wallet_for_product(data.items[0].product_id)
|
||||||
assert wallet_id, "Missing wallet for order `{data.id}`"
|
assert wallet_id, "Missing wallet for order `{data.id}`"
|
||||||
|
|
@ -68,7 +80,7 @@ async def create_new_order(
|
||||||
|
|
||||||
payment_hash, invoice = await create_invoice(
|
payment_hash, invoice = await create_invoice(
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet_id,
|
||||||
amount=round(total_amount),
|
amount=round(product_cost_sat + shipping_cost_sat),
|
||||||
memo=f"Order '{data.id}' for pubkey '{data.public_key}'",
|
memo=f"Order '{data.id}' for pubkey '{data.public_key}'",
|
||||||
extra={
|
extra={
|
||||||
"tag": "nostrmarket",
|
"tag": "nostrmarket",
|
||||||
|
|
@ -77,14 +89,29 @@ async def create_new_order(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
extra = await OrderExtra.from_products(products)
|
||||||
|
extra.shipping_cost_sat = shipping_cost_sat
|
||||||
|
extra.shipping_cost = shipping_zone.cost
|
||||||
|
|
||||||
order = Order(
|
order = Order(
|
||||||
**data.dict(),
|
**data.dict(),
|
||||||
stall_id=products[0].stall_id,
|
stall_id=products[0].stall_id,
|
||||||
invoice_id=payment_hash,
|
invoice_id=payment_hash,
|
||||||
total=total_amount,
|
total=product_cost_sat + shipping_cost_sat,
|
||||||
extra=await OrderExtra.from_products(products),
|
extra=extra,
|
||||||
)
|
)
|
||||||
await create_order(merchant.id, order)
|
await create_order(merchant.id, order)
|
||||||
|
await websocketUpdater(
|
||||||
|
merchant.id,
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"type": "new-order",
|
||||||
|
"stallId": products[0].stall_id,
|
||||||
|
"customerPubkey": data.public_key,
|
||||||
|
"orderId": order.id,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
return PaymentRequest(
|
return PaymentRequest(
|
||||||
id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)]
|
id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)]
|
||||||
|
|
@ -206,9 +233,11 @@ async def process_nostr_message(msg: str):
|
||||||
type, *rest = json.loads(msg)
|
type, *rest = json.loads(msg)
|
||||||
if type.upper() == "EVENT":
|
if type.upper() == "EVENT":
|
||||||
subscription_id, event = rest
|
subscription_id, event = rest
|
||||||
_, merchant_public_key = subscription_id.split(":")
|
|
||||||
event = NostrEvent(**event)
|
event = NostrEvent(**event)
|
||||||
|
if event.kind == 0:
|
||||||
|
await _handle_customer_profile_update(event)
|
||||||
if event.kind == 4:
|
if event.kind == 4:
|
||||||
|
_, merchant_public_key = subscription_id.split(":")
|
||||||
await _handle_nip04_message(merchant_public_key, event)
|
await _handle_nip04_message(merchant_public_key, event)
|
||||||
return
|
return
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
|
@ -235,7 +264,13 @@ async def _handle_nip04_message(merchant_public_key: str, event: NostrEvent):
|
||||||
async def _handle_incoming_dms(
|
async def _handle_incoming_dms(
|
||||||
event: NostrEvent, merchant: Merchant, clear_text_msg: str
|
event: NostrEvent, merchant: Merchant, clear_text_msg: str
|
||||||
):
|
):
|
||||||
dm_content = await _handle_dirrect_message(
|
customer = await get_customer(merchant.id, event.pubkey)
|
||||||
|
if not customer:
|
||||||
|
await _handle_new_customer(event, merchant)
|
||||||
|
else:
|
||||||
|
await increment_customer_unread_messages(event.pubkey)
|
||||||
|
|
||||||
|
dm_reply = await _handle_dirrect_message(
|
||||||
merchant.id,
|
merchant.id,
|
||||||
merchant.public_key,
|
merchant.public_key,
|
||||||
event.pubkey,
|
event.pubkey,
|
||||||
|
|
@ -243,8 +278,8 @@ async def _handle_incoming_dms(
|
||||||
event.created_at,
|
event.created_at,
|
||||||
clear_text_msg,
|
clear_text_msg,
|
||||||
)
|
)
|
||||||
if dm_content:
|
if dm_reply:
|
||||||
dm_event = merchant.build_dm_event(dm_content, event.pubkey)
|
dm_event = merchant.build_dm_event(dm_reply, event.pubkey)
|
||||||
await nostr_client.publish_nostr_event(dm_event)
|
await nostr_client.publish_nostr_event(dm_event)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -287,6 +322,11 @@ async def _handle_dirrect_message(
|
||||||
order["event_created_at"] = event_created_at
|
order["event_created_at"] = event_created_at
|
||||||
return await _handle_new_order(PartialOrder(**order))
|
return await _handle_new_order(PartialOrder(**order))
|
||||||
|
|
||||||
|
await websocketUpdater(
|
||||||
|
merchant_id,
|
||||||
|
json.dumps({"type": "new-direct-message", "customerPubkey": from_pubkey}),
|
||||||
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
|
|
@ -308,3 +348,29 @@ async def _handle_new_order(order: PartialOrder) -> Optional[str]:
|
||||||
return json.dumps(new_order.dict(), separators=(",", ":"), ensure_ascii=False)
|
return json.dumps(new_order.dict(), separators=(",", ":"), ensure_ascii=False)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_new_customer(event, merchant):
|
||||||
|
await create_customer(
|
||||||
|
merchant.id, Customer(merchant_id=merchant.id, public_key=event.pubkey)
|
||||||
|
)
|
||||||
|
await nostr_client.subscribe_to_user_profile(event.pubkey, 0)
|
||||||
|
await websocketUpdater(
|
||||||
|
merchant.id,
|
||||||
|
json.dumps({"type": "new-customer"}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_customer_profile_update(event: NostrEvent):
|
||||||
|
try:
|
||||||
|
profile = json.loads(event.content)
|
||||||
|
await update_customer_profile(
|
||||||
|
event.pubkey,
|
||||||
|
event.created_at,
|
||||||
|
CustomerProfile(
|
||||||
|
name=profile["name"] if "name" in profile else "",
|
||||||
|
about=profile["about"] if "about" in profile else "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ async function customerStall(path) {
|
||||||
items: Array.from(this.cart.products, p => {
|
items: Array.from(this.cart.products, p => {
|
||||||
return {product_id: p[0], quantity: p[1].quantity}
|
return {product_id: p[0], quantity: p[1].quantity}
|
||||||
}),
|
}),
|
||||||
shipping: orderData.shippingzone
|
shipping_id: orderData.shippingzone
|
||||||
}
|
}
|
||||||
orderObj.id = await hash(
|
orderObj.id = await hash(
|
||||||
[orderData.pubkey, created_at, JSON.stringify(orderObj)].join(':')
|
[orderData.pubkey, created_at, JSON.stringify(orderObj)].join(':')
|
||||||
|
|
@ -269,7 +269,7 @@ async function customerStall(path) {
|
||||||
items: Array.from(this.cart.products, p => {
|
items: Array.from(this.cart.products, p => {
|
||||||
return {product_id: p[0], quantity: p[1].quantity}
|
return {product_id: p[0], quantity: p[1].quantity}
|
||||||
}),
|
}),
|
||||||
shipping: orderData.shippingzone
|
shipping_id: orderData.shippingzone
|
||||||
}
|
}
|
||||||
let created_at = Math.floor(Date.now() / 1000)
|
let created_at = Math.floor(Date.now() / 1000)
|
||||||
orderObj.id = await hash(
|
orderObj.id = await hash(
|
||||||
|
|
@ -375,8 +375,9 @@ async function customerStall(path) {
|
||||||
this.qrCodeDialog.data.message = json.message
|
this.qrCodeDialog.data.message = json.message
|
||||||
return cb()
|
return cb()
|
||||||
}
|
}
|
||||||
let payment_request = json.payment_options.find(o => o.type == 'ln')
|
let payment_request = json.payment_options.find(
|
||||||
.link
|
o => o.type == 'ln'
|
||||||
|
).link
|
||||||
if (!payment_request) return
|
if (!payment_request) return
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.qrCodeDialog.data.payment_request = payment_request
|
this.qrCodeDialog.data.payment_request = payment_request
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,26 @@
|
||||||
<div>
|
<div>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h6 class="text-subtitle1 q-my-none">Messages</h6>
|
<div class="row">
|
||||||
|
<div class="col-2">
|
||||||
|
<h6 class="text-subtitle1 q-my-none">Messages</h6>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<q-badge v-if="unreadMessages" color="green"
|
||||||
|
><span v-text="unreadMessages"></span> new</q-badge
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-btn
|
||||||
|
v-if="activePublicKey"
|
||||||
|
@click="showClientOrders"
|
||||||
|
unelevated
|
||||||
|
outline
|
||||||
|
class="float-right"
|
||||||
|
>Client Orders</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
|
|
@ -9,7 +28,7 @@
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-select
|
<q-select
|
||||||
v-model="activePublicKey"
|
v-model="activePublicKey"
|
||||||
:options="customersPublicKeys.map(k => ({label: `${k.slice(0, 16)}...${k.slice(k.length - 16)}`, value: k}))"
|
:options="customers.map(c => ({label: buildCustomerLabel(c), value: c.public_key}))"
|
||||||
label="Select Customer"
|
label="Select Customer"
|
||||||
emit-value
|
emit-value
|
||||||
@input="selectActiveCustomer()"
|
@input="selectActiveCustomer()"
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,38 @@ async function directMessages(path) {
|
||||||
const template = await loadTemplateAsync(path)
|
const template = await loadTemplateAsync(path)
|
||||||
Vue.component('direct-messages', {
|
Vue.component('direct-messages', {
|
||||||
name: 'direct-messages',
|
name: 'direct-messages',
|
||||||
props: ['active-public-key', 'adminkey', 'inkey'],
|
props: ['active-chat-customer', 'adminkey', 'inkey'],
|
||||||
template,
|
template,
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
activeChatCustomer: async function (n) {
|
||||||
|
this.activePublicKey = n
|
||||||
|
},
|
||||||
activePublicKey: async function (n) {
|
activePublicKey: async function (n) {
|
||||||
await this.getDirectMessages(n)
|
await this.getDirectMessages(n)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
customersPublicKeys: [],
|
customers: [],
|
||||||
|
unreadMessages: 0,
|
||||||
|
activePublicKey: null,
|
||||||
messages: [],
|
messages: [],
|
||||||
newMessage: ''
|
newMessage: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
sendMessage: async function () {},
|
sendMessage: async function () {},
|
||||||
|
buildCustomerLabel: function (c) {
|
||||||
|
let label = `${c.profile.name || 'unknown'} ${c.profile.about || ''}`
|
||||||
|
if (c.unread_messages) {
|
||||||
|
label += `[new: ${c.unread_messages}]`
|
||||||
|
}
|
||||||
|
label += ` (${c.public_key.slice(0, 16)}...${c.public_key.slice(
|
||||||
|
c.public_key.length - 16
|
||||||
|
)}`
|
||||||
|
return label
|
||||||
|
},
|
||||||
getDirectMessages: async function (pubkey) {
|
getDirectMessages: async function (pubkey) {
|
||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
this.messages = []
|
this.messages = []
|
||||||
|
|
@ -31,23 +46,21 @@ async function directMessages(path) {
|
||||||
this.inkey
|
this.inkey
|
||||||
)
|
)
|
||||||
this.messages = data
|
this.messages = data
|
||||||
console.log(
|
|
||||||
'### this.messages',
|
|
||||||
this.messages.map(m => m.message)
|
|
||||||
)
|
|
||||||
this.focusOnChatBox(this.messages.length - 1)
|
this.focusOnChatBox(this.messages.length - 1)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getCustomersPublicKeys: async function () {
|
getCustomers: async function () {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/nostrmarket/api/v1/customers',
|
'/nostrmarket/api/v1/customers',
|
||||||
this.inkey
|
this.inkey
|
||||||
)
|
)
|
||||||
this.customersPublicKeys = data
|
this.customers = data
|
||||||
|
this.unreadMessages = data.filter(c => c.unread_messages).length
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
|
|
@ -70,8 +83,19 @@ async function directMessages(path) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
handleNewMessage: async function (data) {
|
||||||
|
if (data.customerPubkey === this.activePublicKey) {
|
||||||
|
await this.getDirectMessages(this.activePublicKey)
|
||||||
|
} else {
|
||||||
|
await this.getCustomers()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showClientOrders: function () {
|
||||||
|
this.$emit('customer-selected', this.activePublicKey)
|
||||||
|
},
|
||||||
selectActiveCustomer: async function () {
|
selectActiveCustomer: async function () {
|
||||||
await this.getDirectMessages(this.activePublicKey)
|
await this.getDirectMessages(this.activePublicKey)
|
||||||
|
await this.getCustomers()
|
||||||
},
|
},
|
||||||
focusOnChatBox: function (index) {
|
focusOnChatBox: function (index) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -85,7 +109,7 @@ async function directMessages(path) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: async function () {
|
created: async function () {
|
||||||
await this.getCustomersPublicKeys()
|
await this.getCustomers()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,45 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="row q-mb-md">
|
<div class="row q-mb-md">
|
||||||
<div class="col">
|
<div class="col-3 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
v-model="search.publicKey"
|
||||||
|
:options="customers.map(c => ({label: buildCustomerLabel(c), value: c.public_key}))"
|
||||||
|
label="Customer"
|
||||||
|
emit-value
|
||||||
|
class="text-wrap"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
v-model="search.isPaid"
|
||||||
|
:options="ternaryOptions"
|
||||||
|
label="Paid"
|
||||||
|
emit-value
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
v-model="search.isShipped"
|
||||||
|
:options="ternaryOptions"
|
||||||
|
label="Shipped"
|
||||||
|
emit-value
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
color="secondary"
|
|
||||||
outline
|
outline
|
||||||
icon="refresh"
|
icon="search"
|
||||||
@click="getOrders()"
|
@click="getOrders()"
|
||||||
class="float-left"
|
class="float-right"
|
||||||
>Refresh Orders</q-btn
|
>Search Orders</q-btn
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row q-mt-md">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-table
|
<q-table
|
||||||
flat
|
flat
|
||||||
|
|
@ -36,9 +63,18 @@
|
||||||
/>
|
/>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
||||||
<q-td key="id" :props="props"> {{toShortId(props.row.id)}} </q-td>
|
<q-td key="id" :props="props">
|
||||||
<q-td key="total" :props="props"> {{props.row.total}} </q-td>
|
{{toShortId(props.row.id)}}
|
||||||
<!-- todo: currency per order -->
|
<q-badge v-if="props.row.isNew" color="orange">new</q-badge></q-td
|
||||||
|
>
|
||||||
|
<q-td key="total" :props="props">
|
||||||
|
{{satBtc(props.row.total)}}
|
||||||
|
</q-td>
|
||||||
|
<q-td key="fiat" :props="props">
|
||||||
|
<span v-if="props.row.extra.currency !== 'sat'">
|
||||||
|
{{orderTotal(props.row)}} {{props.row.extra.currency}}
|
||||||
|
</span>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
<q-td key="paid" :props="props">
|
<q-td key="paid" :props="props">
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
|
|
@ -78,7 +114,9 @@
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
<div class="col-1">Quantity</div>
|
<div class="col-1">Quantity</div>
|
||||||
<div class="col-1"></div>
|
<div class="col-1"></div>
|
||||||
<div class="col-10">Name</div>
|
<div class="col-4">Name</div>
|
||||||
|
<div class="col-2">Price</div>
|
||||||
|
<div class="col-4"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-1"></div>
|
<div class="col-1"></div>
|
||||||
|
|
@ -92,13 +130,34 @@
|
||||||
>
|
>
|
||||||
<div class="col-1">{{item.quantity}}</div>
|
<div class="col-1">{{item.quantity}}</div>
|
||||||
<div class="col-1">x</div>
|
<div class="col-1">x</div>
|
||||||
<div class="col-10">
|
<div class="col-4">
|
||||||
{{productOverview(props.row, item.product_id)}}
|
{{productName(props.row, item.product_id)}}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
{{productPrice(props.row, item.product_id)}}
|
||||||
|
</div>
|
||||||
|
<div class="col-4"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-1"></div>
|
<div class="col-1"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="props.row.extra.currency !== 'sat'"
|
||||||
|
class="row items-center no-wrap q-mb-md q-mt-md"
|
||||||
|
>
|
||||||
|
<div class="col-3 q-pr-lg">Exchange Rate (1 BTC):</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
disabled
|
||||||
|
:value="formatFiat(props.row.extra.btc_price, props.row.extra.currency)"
|
||||||
|
type="text"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
<div class="row items-center no-wrap q-mb-md q-mt-md">
|
<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-3 q-pr-lg">Order ID:</div>
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,18 @@ 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: ['stall-id', 'adminkey', 'inkey'],
|
props: ['stall-id', 'customer-pubkey-filter', 'adminkey', 'inkey'],
|
||||||
template,
|
template,
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
customerPubkeyFilter: async function (n) {
|
||||||
|
this.search.publicKey = n
|
||||||
|
this.search.isPaid = {label: 'All', id: null}
|
||||||
|
this.search.isShipped = {label: 'All', id: null}
|
||||||
|
await this.getOrders()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
orders: [],
|
orders: [],
|
||||||
|
|
@ -12,6 +21,32 @@ async function orderList(path) {
|
||||||
shippingMessage: '',
|
shippingMessage: '',
|
||||||
showShipDialog: false,
|
showShipDialog: false,
|
||||||
filter: '',
|
filter: '',
|
||||||
|
search: {
|
||||||
|
publicKey: '',
|
||||||
|
isPaid: {
|
||||||
|
label: 'All',
|
||||||
|
id: null
|
||||||
|
},
|
||||||
|
isShipped: {
|
||||||
|
label: 'All',
|
||||||
|
id: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
customers: [],
|
||||||
|
ternaryOptions: [
|
||||||
|
{
|
||||||
|
label: 'All',
|
||||||
|
id: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Yes',
|
||||||
|
id: 'true'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'No',
|
||||||
|
id: 'false'
|
||||||
|
}
|
||||||
|
],
|
||||||
ordersTable: {
|
ordersTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
|
|
@ -23,15 +58,21 @@ async function orderList(path) {
|
||||||
{
|
{
|
||||||
name: 'id',
|
name: 'id',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'ID',
|
label: 'Order ID',
|
||||||
field: 'id'
|
field: 'id'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'total',
|
name: 'total',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'Total',
|
label: 'Total Sats',
|
||||||
field: 'total'
|
field: 'total'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'fiat',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Total Fiat',
|
||||||
|
field: 'fiat'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'paid',
|
name: 'paid',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
|
@ -73,21 +114,51 @@ async function orderList(path) {
|
||||||
'YYYY-MM-DD HH:mm'
|
'YYYY-MM-DD HH:mm'
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
productOverview: function (order, productId) {
|
satBtc(val, showUnit = true) {
|
||||||
|
return satOrBtc(val, showUnit, true)
|
||||||
|
},
|
||||||
|
formatFiat(value, currency) {
|
||||||
|
return Math.trunc(value) + ' ' + currency
|
||||||
|
},
|
||||||
|
productName: function (order, productId) {
|
||||||
product = order.extra.products.find(p => p.id === productId)
|
product = order.extra.products.find(p => p.id === productId)
|
||||||
if (product) {
|
if (product) {
|
||||||
return `${product.name} (${product.price} ${order.extra.currency})`
|
return product.name
|
||||||
}
|
}
|
||||||
return ''
|
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) {
|
||||||
|
return order.items.reduce((t, item) => {
|
||||||
|
product = order.extra.products.find(p => p.id === item.product_id)
|
||||||
|
return t + item.quantity * product.price
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
getOrders: async function () {
|
getOrders: async function () {
|
||||||
try {
|
try {
|
||||||
const ordersPath = this.stallId
|
const ordersPath = this.stallId
|
||||||
? `/stall/order/${this.stallId}`
|
? `stall/order/${this.stallId}`
|
||||||
: '/order'
|
: 'order'
|
||||||
|
|
||||||
|
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(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/nostrmarket/api/v1' + ordersPath,
|
`/nostrmarket/api/v1/${ordersPath}?${query.join('&')}`,
|
||||||
this.inkey
|
this.inkey
|
||||||
)
|
)
|
||||||
this.orders = data.map(s => ({...s, expanded: false}))
|
this.orders = data.map(s => ({...s, expanded: false}))
|
||||||
|
|
@ -95,6 +166,18 @@ async function orderList(path) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getOrder: async function (orderId) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/nostrmarket/api/v1/order/${orderId}`,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
return {...data, expanded: false, isNew: true}
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
updateOrderShipped: async function () {
|
updateOrderShipped: async function () {
|
||||||
this.selectedOrder.shipped = !this.selectedOrder.shipped
|
this.selectedOrder.shipped = !this.selectedOrder.shipped
|
||||||
try {
|
try {
|
||||||
|
|
@ -117,6 +200,16 @@ async function orderList(path) {
|
||||||
}
|
}
|
||||||
this.showShipDialog = false
|
this.showShipDialog = false
|
||||||
},
|
},
|
||||||
|
addOrder: async function (data) {
|
||||||
|
if (
|
||||||
|
!this.search.publicKey ||
|
||||||
|
this.search.publicKey === data.customerPubkey
|
||||||
|
) {
|
||||||
|
const order = await this.getOrder(data.orderId)
|
||||||
|
this.orders.unshift(order)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
showShipOrderDialog: function (order) {
|
showShipOrderDialog: function (order) {
|
||||||
this.selectedOrder = order
|
this.selectedOrder = order
|
||||||
this.shippingMessage = order.shipped
|
this.shippingMessage = order.shipped
|
||||||
|
|
@ -129,10 +222,35 @@ async function orderList(path) {
|
||||||
},
|
},
|
||||||
customerSelected: function (customerPubkey) {
|
customerSelected: function (customerPubkey) {
|
||||||
this.$emit('customer-selected', customerPubkey)
|
this.$emit('customer-selected', customerPubkey)
|
||||||
|
},
|
||||||
|
getCustomers: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/customers',
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
this.customers = data
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildCustomerLabel: function (c) {
|
||||||
|
let label = `${c.profile.name || 'unknown'} ${c.profile.about || ''}`
|
||||||
|
if (c.unread_messages) {
|
||||||
|
label += `[new: ${c.unread_messages}]`
|
||||||
|
}
|
||||||
|
label += ` (${c.public_key.slice(0, 16)}...${c.public_key.slice(
|
||||||
|
c.public_key.length - 16
|
||||||
|
)}`
|
||||||
|
return label
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: async function () {
|
created: async function () {
|
||||||
await this.getOrders()
|
if (this.stallId) {
|
||||||
|
await this.getOrders()
|
||||||
|
}
|
||||||
|
await this.getCustomers()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ const merchant = async () => {
|
||||||
merchant: {},
|
merchant: {},
|
||||||
shippingZones: [],
|
shippingZones: [],
|
||||||
activeChatCustomer: '',
|
activeChatCustomer: '',
|
||||||
|
orderPubkey: null,
|
||||||
showKeys: false,
|
showKeys: false,
|
||||||
importKeyDialog: {
|
importKeyDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
|
|
@ -102,10 +103,43 @@ const merchant = async () => {
|
||||||
},
|
},
|
||||||
customerSelectedForOrder: function (customerPubkey) {
|
customerSelectedForOrder: function (customerPubkey) {
|
||||||
this.activeChatCustomer = customerPubkey
|
this.activeChatCustomer = customerPubkey
|
||||||
|
},
|
||||||
|
filterOrdersForCustomer: function (customerPubkey) {
|
||||||
|
this.orderPubkey = customerPubkey
|
||||||
|
},
|
||||||
|
waitForNotifications: async function () {
|
||||||
|
try {
|
||||||
|
const scheme = location.protocol === 'http:' ? 'ws' : 'wss'
|
||||||
|
const port = location.port ? `:${location.port}` : ''
|
||||||
|
const wsUrl = `${scheme}://${document.domain}${port}/api/v1/ws/${this.merchant.id}`
|
||||||
|
const wsConnection = new WebSocket(wsUrl)
|
||||||
|
wsConnection.onmessage = async e => {
|
||||||
|
const data = JSON.parse(e.data)
|
||||||
|
if (data.type === 'new-order') {
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'positive',
|
||||||
|
message: 'New Order'
|
||||||
|
})
|
||||||
|
await this.$refs.orderListRef.addOrder(data)
|
||||||
|
} else if (data.type === 'new-customer') {
|
||||||
|
} else if (data.type === 'new-direct-message') {
|
||||||
|
await this.$refs.directMessagesRef.handleNewMessage(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Failed to watch for updated',
|
||||||
|
caption: `${error}`
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: async function () {
|
created: async function () {
|
||||||
await this.getMerchant()
|
await this.getMerchant()
|
||||||
|
await this.waitForNotifications()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,16 @@ function isJson(str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function satOrBtc(val, showUnit = true, showSats = false) {
|
||||||
|
const value = showSats
|
||||||
|
? LNbits.utils.formatSat(val)
|
||||||
|
: val == 0
|
||||||
|
? 0.0
|
||||||
|
: (val / 100000000).toFixed(8)
|
||||||
|
if (!showUnit) return value
|
||||||
|
return showSats ? value + ' sat' : value + ' BTC'
|
||||||
|
}
|
||||||
|
|
||||||
function timeFromNow(time) {
|
function timeFromNow(time) {
|
||||||
// Get timestamps
|
// Get timestamps
|
||||||
let unixTime = new Date(time).getTime()
|
let unixTime = new Date(time).getTime()
|
||||||
|
|
|
||||||
5
tasks.py
5
tasks.py
|
|
@ -4,6 +4,7 @@ from lnbits.core.models import Payment
|
||||||
from lnbits.tasks import register_invoice_listener
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
from .crud import (
|
from .crud import (
|
||||||
|
get_all_customers,
|
||||||
get_last_direct_messages_time,
|
get_last_direct_messages_time,
|
||||||
get_last_order_time,
|
get_last_order_time,
|
||||||
get_public_keys_for_merchants,
|
get_public_keys_for_merchants,
|
||||||
|
|
@ -42,6 +43,10 @@ async def wait_for_nostr_events(nostr_client: NostrClient):
|
||||||
|
|
||||||
await nostr_client.subscribe_to_direct_messages(p, since)
|
await nostr_client.subscribe_to_direct_messages(p, since)
|
||||||
|
|
||||||
|
customers = await get_all_customers()
|
||||||
|
for c in customers:
|
||||||
|
await nostr_client.subscribe_to_user_profile(c.public_key, c.event_created_at)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
message = await nostr_client.get_event()
|
message = await nostr_client.get_event()
|
||||||
await process_nostr_message(message)
|
await process_nostr_message(message)
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,21 @@
|
||||||
></stall-list>
|
></stall-list>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
<q-card class="q-mt-lg">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<order-list
|
||||||
|
ref="orderListRef"
|
||||||
|
:adminkey="g.user.wallets[0].adminkey"
|
||||||
|
:inkey="g.user.wallets[0].inkey"
|
||||||
|
:customer-pubkey-filter="orderPubkey"
|
||||||
|
@customer-selected="customerSelectedForOrder"
|
||||||
|
></order-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
<q-card v-else>
|
<q-card v-else>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
|
@ -133,9 +148,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-if="merchant && merchant.id" class="col-12">
|
<div v-if="merchant && merchant.id" class="col-12">
|
||||||
<direct-messages
|
<direct-messages
|
||||||
:active-public-key="activeChatCustomer"
|
ref="directMessagesRef"
|
||||||
:inkey="g.user.wallets[0].inkey"
|
:inkey="g.user.wallets[0].inkey"
|
||||||
:adminkey="g.user.wallets[0].adminkey"
|
:adminkey="g.user.wallets[0].adminkey"
|
||||||
|
:active-chat-customer="activeChatCustomer"
|
||||||
|
@customer-selected="filterOrdersForCustomer"
|
||||||
>
|
>
|
||||||
</direct-messages>
|
</direct-messages>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
44
views_api.py
44
views_api.py
|
|
@ -1,6 +1,6 @@
|
||||||
import json
|
import json
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
|
|
@ -31,6 +31,7 @@ from .crud import (
|
||||||
delete_product,
|
delete_product,
|
||||||
delete_stall,
|
delete_stall,
|
||||||
delete_zone,
|
delete_zone,
|
||||||
|
get_customers,
|
||||||
get_direct_messages,
|
get_direct_messages,
|
||||||
get_merchant_by_pubkey,
|
get_merchant_by_pubkey,
|
||||||
get_merchant_for_user,
|
get_merchant_for_user,
|
||||||
|
|
@ -39,12 +40,11 @@ from .crud import (
|
||||||
get_orders_for_stall,
|
get_orders_for_stall,
|
||||||
get_product,
|
get_product,
|
||||||
get_products,
|
get_products,
|
||||||
get_public_keys_for_direct_messages,
|
|
||||||
get_public_keys_for_orders,
|
|
||||||
get_stall,
|
get_stall,
|
||||||
get_stalls,
|
get_stalls,
|
||||||
get_zone,
|
get_zone,
|
||||||
get_zones,
|
get_zones,
|
||||||
|
update_customer_no_unread_messages,
|
||||||
update_merchant,
|
update_merchant,
|
||||||
update_order_shipped_status,
|
update_order_shipped_status,
|
||||||
update_product,
|
update_product,
|
||||||
|
|
@ -52,6 +52,7 @@ from .crud import (
|
||||||
update_zone,
|
update_zone,
|
||||||
)
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
|
Customer,
|
||||||
DirectMessage,
|
DirectMessage,
|
||||||
Merchant,
|
Merchant,
|
||||||
Order,
|
Order,
|
||||||
|
|
@ -447,12 +448,17 @@ async def api_get_stall_products(
|
||||||
@nostrmarket_ext.get("/api/v1/stall/order/{stall_id}")
|
@nostrmarket_ext.get("/api/v1/stall/order/{stall_id}")
|
||||||
async def api_get_stall_orders(
|
async def api_get_stall_orders(
|
||||||
stall_id: str,
|
stall_id: str,
|
||||||
|
paid: Optional[bool] = None,
|
||||||
|
shipped: Optional[bool] = None,
|
||||||
|
pubkey: Optional[str] = None,
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant, "Merchant cannot be found"
|
assert merchant, "Merchant cannot be found"
|
||||||
orders = await get_orders_for_stall(merchant.id, stall_id)
|
orders = await get_orders_for_stall(
|
||||||
|
merchant.id, stall_id, paid=paid, shipped=shipped, public_key=pubkey
|
||||||
|
)
|
||||||
return orders
|
return orders
|
||||||
except AssertionError as ex:
|
except AssertionError as ex:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -641,10 +647,10 @@ async def api_delete_product(
|
||||||
######################################## ORDERS ########################################
|
######################################## ORDERS ########################################
|
||||||
|
|
||||||
|
|
||||||
nostrmarket_ext.get("/api/v1/order/{order_id}")
|
@nostrmarket_ext.get("/api/v1/order/{order_id}")
|
||||||
|
async def api_get_order(
|
||||||
|
order_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||||
async def api_get_order(order_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
|
):
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant, "Merchant cannot be found"
|
assert merchant, "Merchant cannot be found"
|
||||||
|
|
@ -672,12 +678,19 @@ async def api_get_order(order_id: str, wallet: WalletTypeInfo = Depends(get_key_
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/order")
|
@nostrmarket_ext.get("/api/v1/order")
|
||||||
async def api_get_orders(wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_get_orders(
|
||||||
|
paid: Optional[bool] = None,
|
||||||
|
shipped: Optional[bool] = None,
|
||||||
|
pubkey: Optional[str] = None,
|
||||||
|
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant, "Merchant cannot be found"
|
assert merchant, "Merchant cannot be found"
|
||||||
|
|
||||||
orders = await get_orders(merchant.id)
|
orders = await get_orders(
|
||||||
|
merchant_id=merchant.id, paid=paid, shipped=shipped, public_key=pubkey
|
||||||
|
)
|
||||||
return orders
|
return orders
|
||||||
except AssertionError as ex:
|
except AssertionError as ex:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -738,6 +751,7 @@ async def api_get_messages(
|
||||||
assert merchant, f"Merchant cannot be found"
|
assert merchant, f"Merchant cannot be found"
|
||||||
|
|
||||||
messages = await get_direct_messages(merchant.id, public_key)
|
messages = await get_direct_messages(merchant.id, public_key)
|
||||||
|
await update_customer_no_unread_messages(public_key)
|
||||||
return messages
|
return messages
|
||||||
except AssertionError as ex:
|
except AssertionError as ex:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -785,17 +799,13 @@ async def api_create_message(
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/customers")
|
@nostrmarket_ext.get("/api/v1/customers")
|
||||||
async def api_create_message(
|
async def api_get_customers(
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||||
) -> DirectMessage:
|
) -> List[Customer]:
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant, f"Merchant cannot be found"
|
assert merchant, f"Merchant cannot be found"
|
||||||
|
return await get_customers(merchant.id)
|
||||||
dm_pubkeys = await get_public_keys_for_direct_messages(merchant.id)
|
|
||||||
orders_pubkeys = await get_public_keys_for_orders(merchant.id)
|
|
||||||
|
|
||||||
return list(dict.fromkeys(dm_pubkeys + orders_pubkeys))
|
|
||||||
|
|
||||||
except AssertionError as ex:
|
except AssertionError as ex:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue