feat: return payment-request for order

This commit is contained in:
Vlad Stan 2023-03-06 16:31:34 +02:00
parent d0471744e0
commit bee52340a2
5 changed files with 80 additions and 29 deletions

19
crud.py
View file

@ -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]: async def get_products_by_ids(user_id: str, product_ids: List[str]) -> List[Product]:
q = ",".join(["?"] * len(product_ids)) q = ",".join(["?"] * len(product_ids))
rows = await db.fetchall( 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), (user_id, *product_ids),
) )
return [Product.from_row(row) for row in rows] return [Product.from_row(row) for row in rows]
async def get_wallet_for_product(product_id: str) -> Optional[str]: async def get_wallet_for_product(product_id: str) -> Optional[str]:
row = await db.fetchone( row = await db.fetchone(
""" """
@ -312,8 +311,10 @@ async def delete_product(user_id: str, product_id: str) -> None:
), ),
) )
######################################## ORDERS ######################################## ######################################## ORDERS ########################################
async def create_order(user_id: str, o: Order) -> Order: async def create_order(user_id: str, o: Order) -> Order:
await db.execute( await db.execute(
f""" f"""
@ -325,7 +326,7 @@ async def create_order(user_id: str, o: Order) -> Order:
o.id, o.id,
o.event_id, o.event_id,
o.pubkey, 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]), json.dumps([i.dict() for i in o.items]),
o.invoice_id, o.invoice_id,
o.total, o.total,
@ -336,6 +337,7 @@ async def create_order(user_id: str, o: Order) -> Order:
return order return order
async def get_order(user_id: str, order_id: str) -> Optional[Order]: async def get_order(user_id: str, order_id: str) -> Optional[Order]:
row = await db.fetchone( row = await db.fetchone(
"SELECT * FROM nostrmarket.orders WHERE user_id =? AND id = ?", "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 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

View file

@ -76,6 +76,8 @@ 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 (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: except ValueError:
return None, s return None, s

View file

@ -6,6 +6,8 @@ from typing import List, Optional
from pydantic import BaseModel 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 .helpers import decrypt_message, get_shared_secret, sign_message_hash
from .nostr.event import NostrEvent from .nostr.event import NostrEvent
@ -234,15 +236,29 @@ class OrderContact(BaseModel):
class PartialOrder(BaseModel): class PartialOrder(BaseModel):
id: Optional[str] id: str
event_id: Optional[str] event_id: Optional[str]
pubkey: str pubkey: str
items: List[OrderItem] items: List[OrderItem]
contact: Optional[OrderContact] 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): class Order(PartialOrder):
id: str
invoice_id: str invoice_id: str
total: float total: float
paid: bool = False paid: bool = False
@ -253,4 +269,15 @@ class Order(PartialOrder):
contact = OrderContact(**json.loads(row["contact_data"])) contact = OrderContact(**json.loads(row["contact_data"]))
items = [OrderItem(**z) for z in json.loads(row["order_items"])] items = [OrderItem(**z) for z in json.loads(row["order_items"])]
order = cls(**dict(row), contact=contact, items=items) order = cls(**dict(row), contact=contact, items=items)
return order return order
class PaymentOption(BaseModel):
type: str
link: str
class PaymentRequest(BaseModel):
id: str
message: Optional[str]
payment_options: List[PaymentOption]

View file

@ -94,9 +94,7 @@ async def handle_message(msg: str):
assert merchant, f"Merchant not found for public key '{public_key}'" assert merchant, f"Merchant not found for public key '{public_key}'"
clear_text_msg = merchant.decrypt_message(event.content, event.pubkey) clear_text_msg = merchant.decrypt_message(event.content, event.pubkey)
await handle_nip04_message( await handle_nip04_message(event.pubkey, event.id, clear_text_msg)
event.pubkey, event.id, clear_text_msg
)
except Exception as ex: except Exception as ex:
logger.warning(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) market_url = url_for(f"/nostrmarket/api/v1/order", external=True)
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
await client.post( resp = await client.post(
url=market_url, url=market_url,
headers={ headers={
"X-Api-Key": wallet.adminkey, "X-Api-Key": wallet.adminkey,
}, },
json=order, json=order,
) )
resp.raise_for_status()
data = resp.json()
print("### payment request", data)
else: else:
print("### text_msg", text_msg) print("### text_msg", text_msg)
except Exception as ex: except Exception as ex:

View file

@ -5,8 +5,8 @@ from typing import List, Optional
from fastapi import Depends from fastapi import Depends
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from loguru import logger from loguru import logger
from lnbits.core import create_invoice
from lnbits.core import create_invoice
from lnbits.decorators import ( from lnbits.decorators import (
WalletTypeInfo, WalletTypeInfo,
get_key_type, get_key_type,
@ -26,6 +26,8 @@ from .crud import (
delete_stall, delete_stall,
delete_zone, delete_zone,
get_merchant_for_user, get_merchant_for_user,
get_order,
get_order_by_event_id,
get_product, get_product,
get_products, get_products,
get_products_by_ids, get_products_by_ids,
@ -47,6 +49,8 @@ from .models import (
PartialProduct, PartialProduct,
PartialStall, PartialStall,
PartialZone, PartialZone,
PaymentOption,
PaymentRequest,
Product, Product,
Stall, Stall,
Zone, Zone,
@ -430,36 +434,39 @@ async def api_delete_product(
@nostrmarket_ext.post("/api/v1/order") @nostrmarket_ext.post("/api/v1/order")
async def api_create_order( async def api_create_order(
data: PartialOrder, wallet: WalletTypeInfo = Depends(require_admin_key) data: PartialOrder, wallet: WalletTypeInfo = Depends(require_admin_key)
): ) -> Optional[PaymentRequest]:
try: 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) 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}`"
product_ids = [p.product_id for p in data.items]
products = await get_products_by_ids(wallet.wallet.user, product_ids)
product_prices = {} payment_hash, invoice = await create_invoice(
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(
wallet_id=wallet_id, wallet_id=wallet_id,
amount=round(amount), amount=round(total_amount),
memo=f"Order '{data.id}' for pubkey '{data.pubkey}'", memo=f"Order '{data.id}' for pubkey '{data.pubkey}'",
extra={ extra={
"tag": "nostrmarket", "tag": "nostrmarket",
"order_id": data.id, "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) await create_order(wallet.wallet.user, order)
return PaymentRequest(
id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)]
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(