V1 (#106)
This commit is contained in:
parent
83c94e94db
commit
0fc26d096f
52 changed files with 6684 additions and 3120 deletions
197
models.py
197
models.py
|
|
@ -2,12 +2,10 @@ import json
|
|||
import time
|
||||
from abc import abstractmethod
|
||||
from enum import Enum
|
||||
from sqlite3 import Row
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from lnbits.utils.exchange_rates import btc_price, fiat_amount_as_satoshis
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .helpers import (
|
||||
decrypt_message,
|
||||
|
|
@ -30,17 +28,17 @@ class Nostrable:
|
|||
pass
|
||||
|
||||
|
||||
######################################## MERCHANT ########################################
|
||||
######################################## MERCHANT ######################################
|
||||
|
||||
|
||||
class MerchantProfile(BaseModel):
|
||||
name: Optional[str]
|
||||
about: Optional[str]
|
||||
picture: Optional[str]
|
||||
name: Optional[str] = None
|
||||
about: Optional[str] = None
|
||||
picture: Optional[str] = None
|
||||
|
||||
|
||||
class MerchantConfig(MerchantProfile):
|
||||
event_id: Optional[str]
|
||||
event_id: Optional[str] = None
|
||||
sync_from_nostr = False
|
||||
active: bool = False
|
||||
restore_in_progress: Optional[bool] = False
|
||||
|
|
@ -56,8 +54,8 @@ class Merchant(PartialMerchant, Nostrable):
|
|||
id: str
|
||||
time: Optional[int] = 0
|
||||
|
||||
def sign_hash(self, hash: bytes) -> str:
|
||||
return sign_message_hash(self.private_key, hash)
|
||||
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)
|
||||
|
|
@ -82,8 +80,8 @@ class Merchant(PartialMerchant, Nostrable):
|
|||
return event
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Merchant":
|
||||
merchant = cls(**dict(row))
|
||||
def from_row(cls, row: dict) -> "Merchant":
|
||||
merchant = cls(**row)
|
||||
merchant.config = MerchantConfig(**json.loads(row["meta"]))
|
||||
return merchant
|
||||
|
||||
|
|
@ -123,20 +121,16 @@ class Merchant(PartialMerchant, Nostrable):
|
|||
|
||||
|
||||
######################################## ZONES ########################################
|
||||
class PartialZone(BaseModel):
|
||||
id: Optional[str]
|
||||
name: Optional[str]
|
||||
class Zone(BaseModel):
|
||||
id: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
currency: str
|
||||
cost: float
|
||||
countries: List[str] = []
|
||||
|
||||
|
||||
class Zone(PartialZone):
|
||||
id: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Zone":
|
||||
zone = cls(**dict(row))
|
||||
def from_row(cls, row: dict) -> "Zone":
|
||||
zone = cls(**row)
|
||||
zone.countries = json.loads(row["regions"])
|
||||
return zone
|
||||
|
||||
|
|
@ -145,12 +139,12 @@ class Zone(PartialZone):
|
|||
|
||||
|
||||
class StallConfig(BaseModel):
|
||||
image_url: Optional[str]
|
||||
description: Optional[str]
|
||||
image_url: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class PartialStall(BaseModel):
|
||||
id: Optional[str]
|
||||
class Stall(BaseModel, Nostrable):
|
||||
id: Optional[str] = None
|
||||
wallet: str
|
||||
name: str
|
||||
currency: str = "sat"
|
||||
|
|
@ -159,8 +153,8 @@ class PartialStall(BaseModel):
|
|||
pending: bool = False
|
||||
|
||||
"""Last published nostr event for this Stall"""
|
||||
event_id: Optional[str]
|
||||
event_created_at: Optional[int]
|
||||
event_id: Optional[str] = None
|
||||
event_created_at: Optional[int] = None
|
||||
|
||||
def validate_stall(self):
|
||||
for z in self.shipping_zones:
|
||||
|
|
@ -169,10 +163,6 @@ class PartialStall(BaseModel):
|
|||
f"Sipping zone '{z.name}' has different currency than stall."
|
||||
)
|
||||
|
||||
|
||||
class Stall(PartialStall, Nostrable):
|
||||
id: str
|
||||
|
||||
def to_nostr_event(self, pubkey: str) -> NostrEvent:
|
||||
content = {
|
||||
"id": self.id,
|
||||
|
|
@ -181,6 +171,7 @@ class Stall(PartialStall, Nostrable):
|
|||
"currency": self.currency,
|
||||
"shipping": [dict(z) for z in self.shipping_zones],
|
||||
}
|
||||
assert self.id
|
||||
event = NostrEvent(
|
||||
pubkey=pubkey,
|
||||
created_at=round(time.time()),
|
||||
|
|
@ -197,7 +188,7 @@ class Stall(PartialStall, Nostrable):
|
|||
pubkey=pubkey,
|
||||
created_at=round(time.time()),
|
||||
kind=5,
|
||||
tags=[["e", self.event_id]],
|
||||
tags=[["e", self.event_id or ""]],
|
||||
content=f"Stall '{self.name}' deleted",
|
||||
)
|
||||
delete_event.id = delete_event.event_id
|
||||
|
|
@ -205,14 +196,14 @@ class Stall(PartialStall, Nostrable):
|
|||
return delete_event
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Stall":
|
||||
stall = cls(**dict(row))
|
||||
def from_row(cls, row: dict) -> "Stall":
|
||||
stall = cls(**row)
|
||||
stall.config = StallConfig(**json.loads(row["meta"]))
|
||||
stall.shipping_zones = [Zone(**z) for z in json.loads(row["zones"])]
|
||||
return stall
|
||||
|
||||
|
||||
######################################## PRODUCTS ########################################
|
||||
######################################## PRODUCTS ######################################
|
||||
|
||||
|
||||
class ProductShippingCost(BaseModel):
|
||||
|
|
@ -221,15 +212,15 @@ class ProductShippingCost(BaseModel):
|
|||
|
||||
|
||||
class ProductConfig(BaseModel):
|
||||
description: Optional[str]
|
||||
currency: Optional[str]
|
||||
description: Optional[str] = None
|
||||
currency: Optional[str] = None
|
||||
use_autoreply: Optional[bool] = False
|
||||
autoreply_message: Optional[str]
|
||||
shipping: Optional[List[ProductShippingCost]] = []
|
||||
autoreply_message: Optional[str] = None
|
||||
shipping: List[ProductShippingCost] = []
|
||||
|
||||
|
||||
class PartialProduct(BaseModel):
|
||||
id: Optional[str]
|
||||
class Product(BaseModel, Nostrable):
|
||||
id: Optional[str] = None
|
||||
stall_id: str
|
||||
name: str
|
||||
categories: List[str] = []
|
||||
|
|
@ -241,12 +232,8 @@ class PartialProduct(BaseModel):
|
|||
config: ProductConfig = ProductConfig()
|
||||
|
||||
"""Last published nostr event for this Product"""
|
||||
event_id: Optional[str]
|
||||
event_created_at: Optional[int]
|
||||
|
||||
|
||||
class Product(PartialProduct, Nostrable):
|
||||
id: str
|
||||
event_id: Optional[str] = None
|
||||
event_created_at: Optional[int] = None
|
||||
|
||||
def to_nostr_event(self, pubkey: str) -> NostrEvent:
|
||||
content = {
|
||||
|
|
@ -259,16 +246,17 @@ class Product(PartialProduct, Nostrable):
|
|||
"price": self.price,
|
||||
"quantity": self.quantity,
|
||||
"active": self.active,
|
||||
"shipping": [dict(s) for s in self.config.shipping or []]
|
||||
"shipping": [dict(s) for s in self.config.shipping or []],
|
||||
}
|
||||
categories = [["t", tag] for tag in self.categories]
|
||||
|
||||
assert self.id
|
||||
if self.active:
|
||||
event = NostrEvent(
|
||||
pubkey=pubkey,
|
||||
created_at=round(time.time()),
|
||||
kind=30018,
|
||||
tags=[["d", self.id]] + categories,
|
||||
tags=[["d", self.id], *categories],
|
||||
content=json.dumps(content, separators=(",", ":"), ensure_ascii=False),
|
||||
)
|
||||
event.id = event.event_id
|
||||
|
|
@ -282,7 +270,7 @@ class Product(PartialProduct, Nostrable):
|
|||
pubkey=pubkey,
|
||||
created_at=round(time.time()),
|
||||
kind=5,
|
||||
tags=[["e", self.event_id]],
|
||||
tags=[["e", self.event_id or ""]],
|
||||
content=f"Product '{self.name}' deleted",
|
||||
)
|
||||
delete_event.id = delete_event.event_id
|
||||
|
|
@ -290,8 +278,8 @@ class Product(PartialProduct, Nostrable):
|
|||
return delete_event
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Product":
|
||||
product = cls(**dict(row))
|
||||
def from_row(cls, row: dict) -> "Product":
|
||||
product = cls(**row)
|
||||
product.config = ProductConfig(**json.loads(row["meta"]))
|
||||
product.images = json.loads(row["image_urls"]) if "image_urls" in row else []
|
||||
product.categories = json.loads(row["category_list"])
|
||||
|
|
@ -302,6 +290,12 @@ class ProductOverview(BaseModel):
|
|||
id: str
|
||||
name: str
|
||||
price: float
|
||||
product_shipping_cost: Optional[float] = None
|
||||
|
||||
@classmethod
|
||||
def from_product(cls, p: Product) -> "ProductOverview":
|
||||
assert p.id
|
||||
return ProductOverview(id=p.id, name=p.name, price=p.price)
|
||||
|
||||
|
||||
######################################## ORDERS ########################################
|
||||
|
|
@ -313,9 +307,9 @@ class OrderItem(BaseModel):
|
|||
|
||||
|
||||
class OrderContact(BaseModel):
|
||||
nostr: Optional[str]
|
||||
phone: Optional[str]
|
||||
email: Optional[str]
|
||||
nostr: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
|
||||
|
||||
class OrderExtra(BaseModel):
|
||||
|
|
@ -324,27 +318,33 @@ class OrderExtra(BaseModel):
|
|||
btc_price: str
|
||||
shipping_cost: float = 0
|
||||
shipping_cost_sat: float = 0
|
||||
fail_message: Optional[str]
|
||||
fail_message: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
async def from_products(cls, products: List[Product]):
|
||||
currency = products[0].config.currency if len(products) else "sat"
|
||||
exchange_rate = (
|
||||
(await btc_price(currency)) if currency and currency != "sat" else 1
|
||||
await btc_price(currency) if currency and currency != "sat" else 1
|
||||
)
|
||||
|
||||
products_overview = [ProductOverview.from_product(p) for p in products]
|
||||
return OrderExtra(
|
||||
products=products_overview,
|
||||
currency=currency or "sat",
|
||||
btc_price=str(exchange_rate),
|
||||
)
|
||||
return OrderExtra(products=products, currency=currency, btc_price=exchange_rate)
|
||||
|
||||
|
||||
class PartialOrder(BaseModel):
|
||||
id: str
|
||||
event_id: Optional[str]
|
||||
event_created_at: Optional[int]
|
||||
event_id: Optional[str] = None
|
||||
event_created_at: Optional[int] = None
|
||||
public_key: str
|
||||
merchant_public_key: str
|
||||
shipping_id: str
|
||||
items: List[OrderItem]
|
||||
contact: Optional[OrderContact]
|
||||
address: Optional[str]
|
||||
contact: Optional[OrderContact] = None
|
||||
address: Optional[str] = None
|
||||
|
||||
def validate_order(self):
|
||||
assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
|
||||
|
|
@ -383,10 +383,11 @@ class PartialOrder(BaseModel):
|
|||
}
|
||||
|
||||
product_cost: float = 0 # todo
|
||||
currency = "sat"
|
||||
for item in self.items:
|
||||
assert item.quantity > 0, "Quantity cannot be negative"
|
||||
price = product_prices[item.product_id]["price"]
|
||||
currency = product_prices[item.product_id]["currency"]
|
||||
price = float(str(product_prices[item.product_id]["price"]))
|
||||
currency = str(product_prices[item.product_id]["currency"])
|
||||
if currency != "sat":
|
||||
price = await fiat_amount_as_satoshis(price, currency)
|
||||
product_cost += item.quantity * price
|
||||
|
|
@ -404,30 +405,39 @@ class PartialOrder(BaseModel):
|
|||
if len(products) == 0:
|
||||
return "[No Products]"
|
||||
receipt = ""
|
||||
product_prices = {}
|
||||
product_prices: dict[str, ProductOverview] = {}
|
||||
for p in products:
|
||||
product_shipping_cost = next(
|
||||
(s.cost for s in p.config.shipping if s.id == shipping_id), 0
|
||||
)
|
||||
product_prices[p.id] = {
|
||||
"name": p.name,
|
||||
"price": p.price,
|
||||
"product_shipping_cost": product_shipping_cost
|
||||
}
|
||||
assert p.id
|
||||
product_prices[p.id] = ProductOverview(
|
||||
id=p.id,
|
||||
name=p.name,
|
||||
price=p.price,
|
||||
product_shipping_cost=product_shipping_cost,
|
||||
)
|
||||
|
||||
currency = products[0].config.currency or "sat"
|
||||
products_cost: float = 0 # todo
|
||||
items_receipts = []
|
||||
for item in self.items:
|
||||
prod = product_prices[item.product_id]
|
||||
price = prod["price"] + prod["product_shipping_cost"]
|
||||
price = prod.price + (prod.product_shipping_cost or 0)
|
||||
|
||||
products_cost += item.quantity * price
|
||||
|
||||
items_receipts.append(f"""[{prod["name"]}: {item.quantity} x ({prod["price"]} + {prod["product_shipping_cost"]}) = {item.quantity * price} {currency}] """)
|
||||
items_receipts.append(
|
||||
f"""[{prod.name}: {item.quantity} x ({prod.price}"""
|
||||
f""" + {prod.product_shipping_cost})"""
|
||||
f""" = {item.quantity * price} {currency}] """
|
||||
)
|
||||
|
||||
receipt = "; ".join(items_receipts)
|
||||
receipt += f"[Products cost: {products_cost} {currency}] [Stall shipping cost: {stall_shipping_cost} {currency}]; "
|
||||
receipt += (
|
||||
f"[Products cost: {products_cost} {currency}] "
|
||||
f"[Stall shipping cost: {stall_shipping_cost} {currency}]; "
|
||||
)
|
||||
receipt += f"[Total: {products_cost + stall_shipping_cost} {currency}]"
|
||||
|
||||
return receipt
|
||||
|
|
@ -439,23 +449,23 @@ class Order(PartialOrder):
|
|||
total: float
|
||||
paid: bool = False
|
||||
shipped: bool = False
|
||||
time: Optional[int]
|
||||
time: Optional[int] = None
|
||||
extra: OrderExtra
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Order":
|
||||
def from_row(cls, row: dict) -> "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)
|
||||
order = cls(**row, contact=contact, items=items, extra=extra)
|
||||
return order
|
||||
|
||||
|
||||
class OrderStatusUpdate(BaseModel):
|
||||
id: str
|
||||
message: Optional[str]
|
||||
paid: Optional[bool]
|
||||
shipped: Optional[bool]
|
||||
message: Optional[str] = None
|
||||
paid: Optional[bool] = False
|
||||
shipped: Optional[bool] = None
|
||||
|
||||
|
||||
class OrderReissue(BaseModel):
|
||||
|
|
@ -470,11 +480,11 @@ class PaymentOption(BaseModel):
|
|||
|
||||
class PaymentRequest(BaseModel):
|
||||
id: str
|
||||
message: Optional[str]
|
||||
message: Optional[str] = None
|
||||
payment_options: List[PaymentOption]
|
||||
|
||||
|
||||
######################################## MESSAGE ########################################
|
||||
######################################## MESSAGE #######################################
|
||||
|
||||
|
||||
class DirectMessageType(Enum):
|
||||
|
|
@ -487,13 +497,13 @@ class DirectMessageType(Enum):
|
|||
|
||||
|
||||
class PartialDirectMessage(BaseModel):
|
||||
event_id: Optional[str]
|
||||
event_created_at: Optional[int]
|
||||
event_id: Optional[str] = None
|
||||
event_created_at: Optional[int] = None
|
||||
message: str
|
||||
public_key: str
|
||||
type: int = DirectMessageType.PLAIN_TEXT.value
|
||||
incoming: bool = False
|
||||
time: Optional[int]
|
||||
time: Optional[int] = None
|
||||
|
||||
@classmethod
|
||||
def parse_message(cls, msg) -> Tuple[DirectMessageType, Optional[Any]]:
|
||||
|
|
@ -511,29 +521,28 @@ class DirectMessage(PartialDirectMessage):
|
|||
id: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "DirectMessage":
|
||||
dm = cls(**dict(row))
|
||||
return dm
|
||||
def from_row(cls, row: dict) -> "DirectMessage":
|
||||
return cls(**row)
|
||||
|
||||
|
||||
######################################## CUSTOMERS ########################################
|
||||
######################################## CUSTOMERS #####################################
|
||||
|
||||
|
||||
class CustomerProfile(BaseModel):
|
||||
name: Optional[str]
|
||||
about: Optional[str]
|
||||
name: Optional[str] = None
|
||||
about: Optional[str] = None
|
||||
|
||||
|
||||
class Customer(BaseModel):
|
||||
merchant_id: str
|
||||
public_key: str
|
||||
event_created_at: Optional[int]
|
||||
profile: Optional[CustomerProfile]
|
||||
event_created_at: Optional[int] = None
|
||||
profile: Optional[CustomerProfile] = None
|
||||
unread_messages: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Customer":
|
||||
customer = cls(**dict(row))
|
||||
def from_row(cls, row: dict) -> "Customer":
|
||||
customer = cls(**row)
|
||||
customer.profile = (
|
||||
CustomerProfile(**json.loads(row["meta"])) if "meta" in row else None
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue