diff --git a/crud.py b/crud.py index d443dd0..6e362b4 100644 --- a/crud.py +++ b/crud.py @@ -283,13 +283,12 @@ async def get_products(user_id: str, stall_id: str) -> List[Product]: 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 FROM nostrmarket.products WHERE user_id = ? AND id IN ({q})", + 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( """ @@ -312,8 +311,10 @@ async def delete_product(user_id: str, product_id: str) -> None: ), ) + ######################################## ORDERS ######################################## + async def create_order(user_id: str, o: Order) -> Order: await db.execute( f""" @@ -325,7 +326,7 @@ async def create_order(user_id: str, o: Order) -> Order: o.id, o.event_id, o.pubkey, - json.dumps(o.contact.dict()), + json.dumps(o.contact.dict() if o.contact else {}), json.dumps([i.dict() for i in o.items]), o.invoice_id, o.total, @@ -336,6 +337,7 @@ async def create_order(user_id: str, o: Order) -> Order: 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 = ?", @@ -345,3 +347,14 @@ async def get_order(user_id: str, order_id: str) -> Optional[Order]: ), ) 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 diff --git a/helpers.py b/helpers.py index 5141009..706d2c6 100644 --- a/helpers.py +++ b/helpers.py @@ -76,6 +76,8 @@ def copy_x(output, x32, y32, data): def order_from_json(s: str) -> Tuple[Optional[Any], Optional[str]]: try: order = json.loads(s) - return (order, None) if "items" in order else (None, s) + return ( + (order, None) if (type(order) is dict) and "items" in order else (None, s) + ) except ValueError: return None, s diff --git a/models.py b/models.py index 2747085..945c1ae 100644 --- a/models.py +++ b/models.py @@ -6,6 +6,8 @@ from typing import List, Optional from pydantic import BaseModel +from lnbits.utils.exchange_rates import fiat_amount_as_satoshis + from .helpers import decrypt_message, get_shared_secret, sign_message_hash from .nostr.event import NostrEvent @@ -234,15 +236,29 @@ class OrderContact(BaseModel): class PartialOrder(BaseModel): - id: Optional[str] + id: str event_id: Optional[str] pubkey: str items: List[OrderItem] contact: Optional[OrderContact] + 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): - id: str invoice_id: str total: float paid: bool = False @@ -253,4 +269,15 @@ class Order(PartialOrder): contact = OrderContact(**json.loads(row["contact_data"])) items = [OrderItem(**z) for z in json.loads(row["order_items"])] order = cls(**dict(row), contact=contact, items=items) - return order \ No newline at end of file + return order + + +class PaymentOption(BaseModel): + type: str + link: str + + +class PaymentRequest(BaseModel): + id: str + message: Optional[str] + payment_options: List[PaymentOption] diff --git a/tasks.py b/tasks.py index 4ee1e4d..b8bc11f 100644 --- a/tasks.py +++ b/tasks.py @@ -94,9 +94,7 @@ async def handle_message(msg: str): assert merchant, f"Merchant not found for public key '{public_key}'" clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) - await handle_nip04_message( - event.pubkey, event.id, clear_text_msg - ) + await handle_nip04_message(event.pubkey, event.id, clear_text_msg) except Exception as ex: logger.warning(ex) @@ -124,13 +122,17 @@ async def handle_nip04_message(from_pubkey: str, event_id: str, msg: str): market_url = url_for(f"/nostrmarket/api/v1/order", external=True) async with httpx.AsyncClient() as client: - await client.post( + resp = await client.post( url=market_url, headers={ "X-Api-Key": wallet.adminkey, }, json=order, ) + resp.raise_for_status() + data = resp.json() + + print("### payment request", data) else: print("### text_msg", text_msg) except Exception as ex: diff --git a/views_api.py b/views_api.py index 2102dc9..56d45fe 100644 --- a/views_api.py +++ b/views_api.py @@ -5,8 +5,8 @@ from typing import List, Optional from fastapi import Depends from fastapi.exceptions import HTTPException from loguru import logger -from lnbits.core import create_invoice +from lnbits.core import create_invoice from lnbits.decorators import ( WalletTypeInfo, get_key_type, @@ -26,6 +26,8 @@ from .crud import ( delete_stall, delete_zone, get_merchant_for_user, + get_order, + get_order_by_event_id, get_product, get_products, get_products_by_ids, @@ -47,6 +49,8 @@ from .models import ( PartialProduct, PartialStall, PartialZone, + PaymentOption, + PaymentRequest, Product, Stall, Zone, @@ -430,36 +434,39 @@ async def api_delete_product( @nostrmarket_ext.post("/api/v1/order") async def api_create_order( data: PartialOrder, wallet: WalletTypeInfo = Depends(require_admin_key) -): +) -> Optional[PaymentRequest]: try: - data.id = data.id or data.event_id + 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 + + products = await get_products_by_ids( + wallet.wallet.user, [p.product_id for p in data.items] + ) + 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}`" - - product_ids = [p.product_id for p in data.items] - products = await get_products_by_ids(wallet.wallet.user, product_ids) - product_prices = {} - for p in products: - product_prices[p.id] = p - - amount: float = 0 # todo - for item in data.items: - amount += item.quantity * product_prices[item.product_id].price - - payment_hash, payment_request = await create_invoice( + payment_hash, invoice = await create_invoice( wallet_id=wallet_id, - amount=round(amount), + amount=round(total_amount), memo=f"Order '{data.id}' for pubkey '{data.pubkey}'", extra={ "tag": "nostrmarket", "order_id": data.id, - } + }, ) - order = Order(**data.dict(), invoice_id=payment_hash, total=100) + order = Order(**data.dict(), invoice_id=payment_hash, total=total_amount) 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(