Merge branch 'improve-ui' into dev-pm
This commit is contained in:
commit
87a4864c49
15 changed files with 2620 additions and 2839 deletions
10
.github/workflows/ci.yml
vendored
Normal file
10
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
uses: lnbits/lnbits/.github/workflows/lint.yml@dev
|
||||||
24
Makefile
24
Makefile
|
|
@ -5,27 +5,27 @@ format: prettier black ruff
|
||||||
check: mypy pyright checkblack checkruff checkprettier
|
check: mypy pyright checkblack checkruff checkprettier
|
||||||
|
|
||||||
prettier:
|
prettier:
|
||||||
poetry run ./node_modules/.bin/prettier --write .
|
uv run ./node_modules/.bin/prettier --write .
|
||||||
pyright:
|
pyright:
|
||||||
poetry run ./node_modules/.bin/pyright
|
uv run ./node_modules/.bin/pyright
|
||||||
|
|
||||||
mypy:
|
mypy:
|
||||||
poetry run mypy .
|
uv run mypy .
|
||||||
|
|
||||||
black:
|
black:
|
||||||
poetry run black .
|
uv run black .
|
||||||
|
|
||||||
ruff:
|
ruff:
|
||||||
poetry run ruff check . --fix
|
uv run ruff check . --fix
|
||||||
|
|
||||||
checkruff:
|
checkruff:
|
||||||
poetry run ruff check .
|
uv run ruff check .
|
||||||
|
|
||||||
checkprettier:
|
checkprettier:
|
||||||
poetry run ./node_modules/.bin/prettier --check .
|
uv run ./node_modules/.bin/prettier --check .
|
||||||
|
|
||||||
checkblack:
|
checkblack:
|
||||||
poetry run black --check .
|
uv run black --check .
|
||||||
|
|
||||||
checkeditorconfig:
|
checkeditorconfig:
|
||||||
editorconfig-checker
|
editorconfig-checker
|
||||||
|
|
@ -33,14 +33,14 @@ checkeditorconfig:
|
||||||
test:
|
test:
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
DEBUG=true \
|
DEBUG=true \
|
||||||
poetry run pytest
|
uv run pytest
|
||||||
install-pre-commit-hook:
|
install-pre-commit-hook:
|
||||||
@echo "Installing pre-commit hook to git"
|
@echo "Installing pre-commit hook to git"
|
||||||
@echo "Uninstall the hook with poetry run pre-commit uninstall"
|
@echo "Uninstall the hook with uv run pre-commit uninstall"
|
||||||
poetry run pre-commit install
|
uv run pre-commit install
|
||||||
|
|
||||||
pre-commit:
|
pre-commit:
|
||||||
poetry run pre-commit run --all-files
|
uv run pre-commit run --all-files
|
||||||
|
|
||||||
|
|
||||||
checkbundle:
|
checkbundle:
|
||||||
|
|
|
||||||
69
crud.py
69
crud.py
|
|
@ -1,5 +1,4 @@
|
||||||
import json
|
import json
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
|
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
|
@ -44,7 +43,7 @@ async def create_merchant(user_id: str, m: PartialMerchant) -> Merchant:
|
||||||
|
|
||||||
async def update_merchant(
|
async def update_merchant(
|
||||||
user_id: str, merchant_id: str, config: MerchantConfig
|
user_id: str, merchant_id: str, config: MerchantConfig
|
||||||
) -> Optional[Merchant]:
|
) -> Merchant | None:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
f"""
|
f"""
|
||||||
UPDATE nostrmarket.merchants SET meta = :meta, time = {db.timestamp_now}
|
UPDATE nostrmarket.merchants SET meta = :meta, time = {db.timestamp_now}
|
||||||
|
|
@ -55,7 +54,7 @@ async def update_merchant(
|
||||||
return await get_merchant(user_id, merchant_id)
|
return await get_merchant(user_id, merchant_id)
|
||||||
|
|
||||||
|
|
||||||
async def touch_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]:
|
async def touch_merchant(user_id: str, merchant_id: str) -> Merchant | None:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
f"""
|
f"""
|
||||||
UPDATE nostrmarket.merchants SET time = {db.timestamp_now}
|
UPDATE nostrmarket.merchants SET time = {db.timestamp_now}
|
||||||
|
|
@ -66,7 +65,7 @@ async def touch_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]:
|
||||||
return await get_merchant(user_id, merchant_id)
|
return await get_merchant(user_id, merchant_id)
|
||||||
|
|
||||||
|
|
||||||
async def get_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]:
|
async def get_merchant(user_id: str, merchant_id: str) -> Merchant | None:
|
||||||
row: dict = await db.fetchone(
|
row: dict = await db.fetchone(
|
||||||
"""SELECT * FROM nostrmarket.merchants WHERE user_id = :user_id AND id = :id""",
|
"""SELECT * FROM nostrmarket.merchants WHERE user_id = :user_id AND id = :id""",
|
||||||
{
|
{
|
||||||
|
|
@ -78,7 +77,7 @@ async def get_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]:
|
||||||
return Merchant.from_row(row) if row else None
|
return Merchant.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_merchant_by_pubkey(public_key: str) -> Optional[Merchant]:
|
async def get_merchant_by_pubkey(public_key: str) -> Merchant | None:
|
||||||
row: dict = await db.fetchone(
|
row: dict = await db.fetchone(
|
||||||
"""SELECT * FROM nostrmarket.merchants WHERE public_key = :public_key""",
|
"""SELECT * FROM nostrmarket.merchants WHERE public_key = :public_key""",
|
||||||
{"public_key": public_key},
|
{"public_key": public_key},
|
||||||
|
|
@ -87,7 +86,7 @@ async def get_merchant_by_pubkey(public_key: str) -> Optional[Merchant]:
|
||||||
return Merchant.from_row(row) if row else None
|
return Merchant.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_merchants_ids_with_pubkeys() -> List[Tuple[str, str]]:
|
async def get_merchants_ids_with_pubkeys() -> list[tuple[str, str]]:
|
||||||
rows: list[dict] = await db.fetchall(
|
rows: list[dict] = await db.fetchall(
|
||||||
"""SELECT id, public_key FROM nostrmarket.merchants""",
|
"""SELECT id, public_key FROM nostrmarket.merchants""",
|
||||||
)
|
)
|
||||||
|
|
@ -95,7 +94,7 @@ async def get_merchants_ids_with_pubkeys() -> List[Tuple[str, str]]:
|
||||||
return [(row["id"], row["public_key"]) for row in rows]
|
return [(row["id"], row["public_key"]) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_merchant_for_user(user_id: str) -> Optional[Merchant]:
|
async def get_merchant_for_user(user_id: str) -> Merchant | None:
|
||||||
row: dict = await db.fetchone(
|
row: dict = await db.fetchone(
|
||||||
"""SELECT * FROM nostrmarket.merchants WHERE user_id = :user_id """,
|
"""SELECT * FROM nostrmarket.merchants WHERE user_id = :user_id """,
|
||||||
{"user_id": user_id},
|
{"user_id": user_id},
|
||||||
|
|
@ -138,7 +137,7 @@ async def create_zone(merchant_id: str, data: Zone) -> Zone:
|
||||||
return zone
|
return zone
|
||||||
|
|
||||||
|
|
||||||
async def update_zone(merchant_id: str, z: Zone) -> Optional[Zone]:
|
async def update_zone(merchant_id: str, z: Zone) -> Zone | None:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE nostrmarket.zones
|
UPDATE nostrmarket.zones
|
||||||
|
|
@ -157,7 +156,7 @@ async def update_zone(merchant_id: str, z: Zone) -> Optional[Zone]:
|
||||||
return await get_zone(merchant_id, z.id)
|
return await get_zone(merchant_id, z.id)
|
||||||
|
|
||||||
|
|
||||||
async def get_zone(merchant_id: str, zone_id: str) -> Optional[Zone]:
|
async def get_zone(merchant_id: str, zone_id: str) -> Zone | None:
|
||||||
row: dict = await db.fetchone(
|
row: dict = await db.fetchone(
|
||||||
"SELECT * FROM nostrmarket.zones WHERE merchant_id = :merchant_id AND id = :id",
|
"SELECT * FROM nostrmarket.zones WHERE merchant_id = :merchant_id AND id = :id",
|
||||||
{
|
{
|
||||||
|
|
@ -168,7 +167,7 @@ async def get_zone(merchant_id: str, zone_id: str) -> Optional[Zone]:
|
||||||
return Zone.from_row(row) if row else None
|
return Zone.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_zones(merchant_id: str) -> List[Zone]:
|
async def get_zones(merchant_id: str) -> list[Zone]:
|
||||||
rows: list[dict] = await db.fetchall(
|
rows: list[dict] = await db.fetchall(
|
||||||
"SELECT * FROM nostrmarket.zones WHERE merchant_id = :merchant_id",
|
"SELECT * FROM nostrmarket.zones WHERE merchant_id = :merchant_id",
|
||||||
{"merchant_id": merchant_id},
|
{"merchant_id": merchant_id},
|
||||||
|
|
@ -235,7 +234,7 @@ async def create_stall(merchant_id: str, data: Stall) -> Stall:
|
||||||
return stall
|
return stall
|
||||||
|
|
||||||
|
|
||||||
async def get_stall(merchant_id: str, stall_id: str) -> Optional[Stall]:
|
async def get_stall(merchant_id: str, stall_id: str) -> Stall | None:
|
||||||
row: dict = await db.fetchone(
|
row: dict = await db.fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM nostrmarket.stalls
|
SELECT * FROM nostrmarket.stalls
|
||||||
|
|
@ -249,7 +248,7 @@ async def get_stall(merchant_id: str, stall_id: str) -> Optional[Stall]:
|
||||||
return Stall.from_row(row) if row else None
|
return Stall.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_stalls(merchant_id: str, pending: Optional[bool] = False) -> List[Stall]:
|
async def get_stalls(merchant_id: str, pending: bool | None = False) -> list[Stall]:
|
||||||
rows: list[dict] = await db.fetchall(
|
rows: list[dict] = await db.fetchall(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM nostrmarket.stalls
|
SELECT * FROM nostrmarket.stalls
|
||||||
|
|
@ -274,7 +273,7 @@ async def get_last_stall_update_time() -> int:
|
||||||
return row["event_created_at"] or 0 if row else 0
|
return row["event_created_at"] or 0 if row else 0
|
||||||
|
|
||||||
|
|
||||||
async def update_stall(merchant_id: str, stall: Stall) -> Optional[Stall]:
|
async def update_stall(merchant_id: str, stall: Stall) -> Stall | None:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE nostrmarket.stalls
|
UPDATE nostrmarket.stalls
|
||||||
|
|
@ -398,9 +397,7 @@ async def update_product(merchant_id: str, product: Product) -> Product:
|
||||||
return updated_product
|
return updated_product
|
||||||
|
|
||||||
|
|
||||||
async def update_product_quantity(
|
async def update_product_quantity(product_id: str, new_quantity: int) -> Product | None:
|
||||||
product_id: str, new_quantity: int
|
|
||||||
) -> Optional[Product]:
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE nostrmarket.products SET quantity = :quantity
|
UPDATE nostrmarket.products SET quantity = :quantity
|
||||||
|
|
@ -415,7 +412,7 @@ async def update_product_quantity(
|
||||||
return Product.from_row(row) if row else None
|
return Product.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_product(merchant_id: str, product_id: str) -> Optional[Product]:
|
async def get_product(merchant_id: str, product_id: str) -> Product | None:
|
||||||
row: dict = await db.fetchone(
|
row: dict = await db.fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM nostrmarket.products
|
SELECT * FROM nostrmarket.products
|
||||||
|
|
@ -431,8 +428,8 @@ async def get_product(merchant_id: str, product_id: str) -> Optional[Product]:
|
||||||
|
|
||||||
|
|
||||||
async def get_products(
|
async def get_products(
|
||||||
merchant_id: str, stall_id: str, pending: Optional[bool] = False
|
merchant_id: str, stall_id: str, pending: bool | None = False
|
||||||
) -> List[Product]:
|
) -> list[Product]:
|
||||||
rows: list[dict] = await db.fetchall(
|
rows: list[dict] = await db.fetchall(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM nostrmarket.products
|
SELECT * FROM nostrmarket.products
|
||||||
|
|
@ -445,8 +442,8 @@ async def get_products(
|
||||||
|
|
||||||
|
|
||||||
async def get_products_by_ids(
|
async def get_products_by_ids(
|
||||||
merchant_id: str, product_ids: List[str]
|
merchant_id: str, product_ids: list[str]
|
||||||
) -> List[Product]:
|
) -> list[Product]:
|
||||||
# todo: revisit
|
# todo: revisit
|
||||||
|
|
||||||
keys = []
|
keys = []
|
||||||
|
|
@ -467,7 +464,7 @@ async def get_products_by_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) -> str | None:
|
||||||
row: dict = await db.fetchone(
|
row: dict = await db.fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT s.wallet as wallet FROM nostrmarket.products p
|
SELECT s.wallet as wallet FROM nostrmarket.products p
|
||||||
|
|
@ -574,7 +571,7 @@ async def create_order(merchant_id: str, o: Order) -> Order:
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
async def get_order(merchant_id: str, order_id: str) -> Optional[Order]:
|
async def get_order(merchant_id: str, order_id: str) -> Order | None:
|
||||||
row: dict = await db.fetchone(
|
row: dict = await db.fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM nostrmarket.orders
|
SELECT * FROM nostrmarket.orders
|
||||||
|
|
@ -588,7 +585,7 @@ async def get_order(merchant_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(merchant_id: str, event_id: str) -> Optional[Order]:
|
async def get_order_by_event_id(merchant_id: str, event_id: str) -> Order | None:
|
||||||
row: dict = await db.fetchone(
|
row: dict = await db.fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM nostrmarket.orders
|
SELECT * FROM nostrmarket.orders
|
||||||
|
|
@ -602,7 +599,7 @@ 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, **kwargs) -> List[Order]:
|
async def get_orders(merchant_id: str, **kwargs) -> list[Order]:
|
||||||
q = " AND ".join(
|
q = " AND ".join(
|
||||||
[
|
[
|
||||||
f"{field[0]} = :{field[0]}"
|
f"{field[0]} = :{field[0]}"
|
||||||
|
|
@ -629,7 +626,7 @@ async def get_orders(merchant_id: str, **kwargs) -> List[Order]:
|
||||||
|
|
||||||
async def get_orders_for_stall(
|
async def get_orders_for_stall(
|
||||||
merchant_id: str, stall_id: str, **kwargs
|
merchant_id: str, stall_id: str, **kwargs
|
||||||
) -> List[Order]:
|
) -> list[Order]:
|
||||||
q = " AND ".join(
|
q = " AND ".join(
|
||||||
[
|
[
|
||||||
f"{field[0]} = :{field[0]}"
|
f"{field[0]} = :{field[0]}"
|
||||||
|
|
@ -654,7 +651,7 @@ async def get_orders_for_stall(
|
||||||
return [Order.from_row(row) for row in rows]
|
return [Order.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def update_order(merchant_id: str, order_id: str, **kwargs) -> Optional[Order]:
|
async def update_order(merchant_id: str, order_id: str, **kwargs) -> Order | None:
|
||||||
q = ", ".join(
|
q = ", ".join(
|
||||||
[
|
[
|
||||||
f"{field[0]} = :{field[0]}"
|
f"{field[0]} = :{field[0]}"
|
||||||
|
|
@ -678,7 +675,7 @@ async def update_order(merchant_id: str, order_id: str, **kwargs) -> Optional[Or
|
||||||
return await get_order(merchant_id, order_id)
|
return await get_order(merchant_id, order_id)
|
||||||
|
|
||||||
|
|
||||||
async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order]:
|
async def update_order_paid_status(order_id: str, paid: bool) -> Order | None:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"UPDATE nostrmarket.orders SET paid = :paid WHERE id = :id",
|
"UPDATE nostrmarket.orders SET paid = :paid WHERE id = :id",
|
||||||
{"paid": paid, "id": order_id},
|
{"paid": paid, "id": order_id},
|
||||||
|
|
@ -692,7 +689,7 @@ async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order]
|
||||||
|
|
||||||
async def update_order_shipped_status(
|
async def update_order_shipped_status(
|
||||||
merchant_id: str, order_id: str, shipped: bool
|
merchant_id: str, order_id: str, shipped: bool
|
||||||
) -> Optional[Order]:
|
) -> Order | None:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE nostrmarket.orders
|
UPDATE nostrmarket.orders
|
||||||
|
|
@ -756,7 +753,7 @@ async def create_direct_message(
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
async def get_direct_message(merchant_id: str, dm_id: str) -> Optional[DirectMessage]:
|
async def get_direct_message(merchant_id: str, dm_id: str) -> DirectMessage | None:
|
||||||
row: dict = await db.fetchone(
|
row: dict = await db.fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM nostrmarket.direct_messages
|
SELECT * FROM nostrmarket.direct_messages
|
||||||
|
|
@ -772,7 +769,7 @@ async def get_direct_message(merchant_id: str, dm_id: str) -> Optional[DirectMes
|
||||||
|
|
||||||
async def get_direct_message_by_event_id(
|
async def get_direct_message_by_event_id(
|
||||||
merchant_id: str, event_id: str
|
merchant_id: str, event_id: str
|
||||||
) -> Optional[DirectMessage]:
|
) -> DirectMessage | None:
|
||||||
row: dict = await db.fetchone(
|
row: dict = await db.fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM nostrmarket.direct_messages
|
SELECT * FROM nostrmarket.direct_messages
|
||||||
|
|
@ -786,7 +783,7 @@ async def get_direct_message_by_event_id(
|
||||||
return DirectMessage.from_row(row) if row else None
|
return DirectMessage.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_direct_messages(merchant_id: str, public_key: str) -> List[DirectMessage]:
|
async def get_direct_messages(merchant_id: str, public_key: str) -> list[DirectMessage]:
|
||||||
rows: list[dict] = await db.fetchall(
|
rows: list[dict] = await db.fetchall(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM nostrmarket.direct_messages
|
SELECT * FROM nostrmarket.direct_messages
|
||||||
|
|
@ -798,7 +795,7 @@ async def get_direct_messages(merchant_id: str, public_key: str) -> List[DirectM
|
||||||
return [DirectMessage.from_row(row) for row in rows]
|
return [DirectMessage.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_orders_from_direct_messages(merchant_id: str) -> List[DirectMessage]:
|
async def get_orders_from_direct_messages(merchant_id: str) -> list[DirectMessage]:
|
||||||
rows: list[dict] = await db.fetchall(
|
rows: list[dict] = await db.fetchall(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM nostrmarket.direct_messages
|
SELECT * FROM nostrmarket.direct_messages
|
||||||
|
|
@ -859,7 +856,7 @@ async def create_customer(merchant_id: str, data: Customer) -> Customer:
|
||||||
return customer
|
return customer
|
||||||
|
|
||||||
|
|
||||||
async def get_customer(merchant_id: str, public_key: str) -> Optional[Customer]:
|
async def get_customer(merchant_id: str, public_key: str) -> Customer | None:
|
||||||
row: dict = await db.fetchone(
|
row: dict = await db.fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM nostrmarket.customers
|
SELECT * FROM nostrmarket.customers
|
||||||
|
|
@ -873,7 +870,7 @@ async def get_customer(merchant_id: str, public_key: str) -> Optional[Customer]:
|
||||||
return Customer.from_row(row) if row else None
|
return Customer.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_customers(merchant_id: str) -> List[Customer]:
|
async def get_customers(merchant_id: str) -> list[Customer]:
|
||||||
rows: list[dict] = await db.fetchall(
|
rows: list[dict] = await db.fetchall(
|
||||||
"SELECT * FROM nostrmarket.customers WHERE merchant_id = :merchant_id",
|
"SELECT * FROM nostrmarket.customers WHERE merchant_id = :merchant_id",
|
||||||
{"merchant_id": merchant_id},
|
{"merchant_id": merchant_id},
|
||||||
|
|
@ -881,7 +878,7 @@ async def get_customers(merchant_id: str) -> List[Customer]:
|
||||||
return [Customer.from_row(row) for row in rows]
|
return [Customer.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_all_unique_customers() -> List[Customer]:
|
async def get_all_unique_customers() -> list[Customer]:
|
||||||
q = """
|
q = """
|
||||||
SELECT public_key, MAX(merchant_id) as merchant_id, MAX(event_created_at)
|
SELECT public_key, MAX(merchant_id) as merchant_id, MAX(event_created_at)
|
||||||
FROM nostrmarket.customers
|
FROM nostrmarket.customers
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import base64
|
import base64
|
||||||
import secrets
|
import secrets
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import secp256k1
|
import secp256k1
|
||||||
from bech32 import bech32_decode, convertbits
|
from bech32 import bech32_decode, convertbits
|
||||||
|
|
@ -33,7 +32,7 @@ def decrypt_message(encoded_message: str, encryption_key) -> str:
|
||||||
return unpadded_data.decode()
|
return unpadded_data.decode()
|
||||||
|
|
||||||
|
|
||||||
def encrypt_message(message: str, encryption_key, iv: Optional[bytes] = None) -> str:
|
def encrypt_message(message: str, encryption_key, iv: bytes | None = None) -> str:
|
||||||
padder = padding.PKCS7(128).padder()
|
padder = padding.PKCS7(128).padder()
|
||||||
padded_data = padder.update(message.encode()) + padder.finalize()
|
padded_data = padder.update(message.encode()) + padder.finalize()
|
||||||
|
|
||||||
|
|
|
||||||
116
models.py
116
models.py
|
|
@ -2,7 +2,7 @@ import json
|
||||||
import time
|
import time
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, List, Optional, Tuple
|
from typing import Any
|
||||||
|
|
||||||
from lnbits.utils.exchange_rates import btc_price, fiat_amount_as_satoshis
|
from lnbits.utils.exchange_rates import btc_price, fiat_amount_as_satoshis
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
@ -32,17 +32,17 @@ class Nostrable:
|
||||||
|
|
||||||
|
|
||||||
class MerchantProfile(BaseModel):
|
class MerchantProfile(BaseModel):
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
about: Optional[str] = None
|
about: str | None = None
|
||||||
picture: Optional[str] = None
|
picture: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class MerchantConfig(MerchantProfile):
|
class MerchantConfig(MerchantProfile):
|
||||||
event_id: Optional[str] = None
|
event_id: str | None = None
|
||||||
sync_from_nostr = False
|
sync_from_nostr: bool = False
|
||||||
# TODO: switched to True for AIO demo; determine if we leave this as True
|
# TODO: switched to True for AIO demo; determine if we leave this as True
|
||||||
active: bool = True
|
active: bool = True
|
||||||
restore_in_progress: Optional[bool] = False
|
restore_in_progress: bool | None = False
|
||||||
|
|
||||||
|
|
||||||
class CreateMerchantRequest(BaseModel):
|
class CreateMerchantRequest(BaseModel):
|
||||||
|
|
@ -57,7 +57,7 @@ class PartialMerchant(BaseModel):
|
||||||
|
|
||||||
class Merchant(PartialMerchant, Nostrable):
|
class Merchant(PartialMerchant, Nostrable):
|
||||||
id: str
|
id: str
|
||||||
time: Optional[int] = 0
|
time: int | None = 0
|
||||||
|
|
||||||
def sign_hash(self, hash_: bytes) -> str:
|
def sign_hash(self, hash_: bytes) -> str:
|
||||||
return sign_message_hash(self.private_key, hash_)
|
return sign_message_hash(self.private_key, hash_)
|
||||||
|
|
@ -127,11 +127,11 @@ class Merchant(PartialMerchant, Nostrable):
|
||||||
|
|
||||||
######################################## ZONES ########################################
|
######################################## ZONES ########################################
|
||||||
class Zone(BaseModel):
|
class Zone(BaseModel):
|
||||||
id: Optional[str] = None
|
id: str | None = None
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
currency: str
|
currency: str
|
||||||
cost: float
|
cost: float
|
||||||
countries: List[str] = []
|
countries: list[str] = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: dict) -> "Zone":
|
def from_row(cls, row: dict) -> "Zone":
|
||||||
|
|
@ -144,22 +144,22 @@ class Zone(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class StallConfig(BaseModel):
|
class StallConfig(BaseModel):
|
||||||
image_url: Optional[str] = None
|
image_url: str | None = None
|
||||||
description: Optional[str] = None
|
description: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class Stall(BaseModel, Nostrable):
|
class Stall(BaseModel, Nostrable):
|
||||||
id: Optional[str] = None
|
id: str | None = None
|
||||||
wallet: str
|
wallet: str
|
||||||
name: str
|
name: str
|
||||||
currency: str = "sat"
|
currency: str = "sat"
|
||||||
shipping_zones: List[Zone] = []
|
shipping_zones: list[Zone] = []
|
||||||
config: StallConfig = StallConfig()
|
config: StallConfig = StallConfig()
|
||||||
pending: bool = False
|
pending: bool = False
|
||||||
|
|
||||||
"""Last published nostr event for this Stall"""
|
"""Last published nostr event for this Stall"""
|
||||||
event_id: Optional[str] = None
|
event_id: str | None = None
|
||||||
event_created_at: Optional[int] = None
|
event_created_at: int | None = None
|
||||||
|
|
||||||
def validate_stall(self):
|
def validate_stall(self):
|
||||||
for z in self.shipping_zones:
|
for z in self.shipping_zones:
|
||||||
|
|
@ -217,19 +217,19 @@ class ProductShippingCost(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class ProductConfig(BaseModel):
|
class ProductConfig(BaseModel):
|
||||||
description: Optional[str] = None
|
description: str | None = None
|
||||||
currency: Optional[str] = None
|
currency: str | None = None
|
||||||
use_autoreply: Optional[bool] = False
|
use_autoreply: bool | None = False
|
||||||
autoreply_message: Optional[str] = None
|
autoreply_message: str | None = None
|
||||||
shipping: List[ProductShippingCost] = []
|
shipping: list[ProductShippingCost] = []
|
||||||
|
|
||||||
|
|
||||||
class Product(BaseModel, Nostrable):
|
class Product(BaseModel, Nostrable):
|
||||||
id: Optional[str] = None
|
id: str | None = None
|
||||||
stall_id: str
|
stall_id: str
|
||||||
name: str
|
name: str
|
||||||
categories: List[str] = []
|
categories: list[str] = []
|
||||||
images: List[str] = []
|
images: list[str] = []
|
||||||
price: float
|
price: float
|
||||||
quantity: int
|
quantity: int
|
||||||
active: bool = True
|
active: bool = True
|
||||||
|
|
@ -237,8 +237,8 @@ class Product(BaseModel, Nostrable):
|
||||||
config: ProductConfig = ProductConfig()
|
config: ProductConfig = ProductConfig()
|
||||||
|
|
||||||
"""Last published nostr event for this Product"""
|
"""Last published nostr event for this Product"""
|
||||||
event_id: Optional[str] = None
|
event_id: str | None = None
|
||||||
event_created_at: Optional[int] = None
|
event_created_at: int | None = None
|
||||||
|
|
||||||
def to_nostr_event(self, pubkey: str) -> NostrEvent:
|
def to_nostr_event(self, pubkey: str) -> NostrEvent:
|
||||||
content = {
|
content = {
|
||||||
|
|
@ -295,7 +295,7 @@ class ProductOverview(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
price: float
|
price: float
|
||||||
product_shipping_cost: Optional[float] = None
|
product_shipping_cost: float | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_product(cls, p: Product) -> "ProductOverview":
|
def from_product(cls, p: Product) -> "ProductOverview":
|
||||||
|
|
@ -312,21 +312,21 @@ class OrderItem(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class OrderContact(BaseModel):
|
class OrderContact(BaseModel):
|
||||||
nostr: Optional[str] = None
|
nostr: str | None = None
|
||||||
phone: Optional[str] = None
|
phone: str | None = None
|
||||||
email: Optional[str] = None
|
email: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class OrderExtra(BaseModel):
|
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: float = 0
|
||||||
shipping_cost_sat: float = 0
|
shipping_cost_sat: float = 0
|
||||||
fail_message: Optional[str] = None
|
fail_message: str | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_products(cls, products: List[Product]):
|
async def from_products(cls, products: list[Product]):
|
||||||
currency = products[0].config.currency if len(products) else "sat"
|
currency = products[0].config.currency if len(products) else "sat"
|
||||||
exchange_rate = (
|
exchange_rate = (
|
||||||
await btc_price(currency) if currency and currency != "sat" else 1
|
await btc_price(currency) if currency and currency != "sat" else 1
|
||||||
|
|
@ -342,19 +342,19 @@ class OrderExtra(BaseModel):
|
||||||
|
|
||||||
class PartialOrder(BaseModel):
|
class PartialOrder(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
event_id: Optional[str] = None
|
event_id: str | None = None
|
||||||
event_created_at: Optional[int] = None
|
event_created_at: int | None = None
|
||||||
public_key: str
|
public_key: str
|
||||||
merchant_public_key: str
|
merchant_public_key: str
|
||||||
shipping_id: str
|
shipping_id: str
|
||||||
items: List[OrderItem]
|
items: list[OrderItem]
|
||||||
contact: Optional[OrderContact] = None
|
contact: OrderContact | None = None
|
||||||
address: Optional[str] = None
|
address: str | None = None
|
||||||
|
|
||||||
def validate_order(self):
|
def validate_order(self):
|
||||||
assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
|
assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
|
||||||
|
|
||||||
def validate_order_items(self, product_list: List[Product]):
|
def validate_order_items(self, product_list: list[Product]):
|
||||||
assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
|
assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
|
||||||
assert (
|
assert (
|
||||||
len(product_list) != 0
|
len(product_list) != 0
|
||||||
|
|
@ -375,8 +375,8 @@ class PartialOrder(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
async def costs_in_sats(
|
async def costs_in_sats(
|
||||||
self, products: List[Product], shipping_id: str, stall_shipping_cost: float
|
self, products: list[Product], shipping_id: str, stall_shipping_cost: float
|
||||||
) -> Tuple[float, float]:
|
) -> tuple[float, float]:
|
||||||
product_prices = {}
|
product_prices = {}
|
||||||
for p in products:
|
for p in products:
|
||||||
product_shipping_cost = next(
|
product_shipping_cost = next(
|
||||||
|
|
@ -405,7 +405,7 @@ class PartialOrder(BaseModel):
|
||||||
return product_cost, stall_shipping_cost
|
return product_cost, stall_shipping_cost
|
||||||
|
|
||||||
def receipt(
|
def receipt(
|
||||||
self, products: List[Product], shipping_id: str, stall_shipping_cost: float
|
self, products: list[Product], shipping_id: str, stall_shipping_cost: float
|
||||||
) -> str:
|
) -> str:
|
||||||
if len(products) == 0:
|
if len(products) == 0:
|
||||||
return "[No Products]"
|
return "[No Products]"
|
||||||
|
|
@ -454,7 +454,7 @@ class Order(PartialOrder):
|
||||||
total: float
|
total: float
|
||||||
paid: bool = False
|
paid: bool = False
|
||||||
shipped: bool = False
|
shipped: bool = False
|
||||||
time: Optional[int] = None
|
time: int | None = None
|
||||||
extra: OrderExtra
|
extra: OrderExtra
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -468,14 +468,14 @@ class Order(PartialOrder):
|
||||||
|
|
||||||
class OrderStatusUpdate(BaseModel):
|
class OrderStatusUpdate(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
message: Optional[str] = None
|
message: str | None = None
|
||||||
paid: Optional[bool] = False
|
paid: bool | None = False
|
||||||
shipped: Optional[bool] = None
|
shipped: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
class OrderReissue(BaseModel):
|
class OrderReissue(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
shipping_id: Optional[str] = None
|
shipping_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class PaymentOption(BaseModel):
|
class PaymentOption(BaseModel):
|
||||||
|
|
@ -485,8 +485,8 @@ class PaymentOption(BaseModel):
|
||||||
|
|
||||||
class PaymentRequest(BaseModel):
|
class PaymentRequest(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
message: Optional[str] = None
|
message: str | None = None
|
||||||
payment_options: List[PaymentOption]
|
payment_options: list[PaymentOption]
|
||||||
|
|
||||||
|
|
||||||
######################################## MESSAGE #######################################
|
######################################## MESSAGE #######################################
|
||||||
|
|
@ -502,16 +502,16 @@ class DirectMessageType(Enum):
|
||||||
|
|
||||||
|
|
||||||
class PartialDirectMessage(BaseModel):
|
class PartialDirectMessage(BaseModel):
|
||||||
event_id: Optional[str] = None
|
event_id: str | None = None
|
||||||
event_created_at: Optional[int] = None
|
event_created_at: int | None = None
|
||||||
message: str
|
message: str
|
||||||
public_key: str
|
public_key: str
|
||||||
type: int = DirectMessageType.PLAIN_TEXT.value
|
type: int = DirectMessageType.PLAIN_TEXT.value
|
||||||
incoming: bool = False
|
incoming: bool = False
|
||||||
time: Optional[int] = None
|
time: int | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_message(cls, msg) -> Tuple[DirectMessageType, Optional[Any]]:
|
def parse_message(cls, msg) -> tuple[DirectMessageType, Any | None]:
|
||||||
try:
|
try:
|
||||||
msg_json = json.loads(msg)
|
msg_json = json.loads(msg)
|
||||||
if "type" in msg_json:
|
if "type" in msg_json:
|
||||||
|
|
@ -534,15 +534,15 @@ class DirectMessage(PartialDirectMessage):
|
||||||
|
|
||||||
|
|
||||||
class CustomerProfile(BaseModel):
|
class CustomerProfile(BaseModel):
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
about: Optional[str] = None
|
about: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class Customer(BaseModel):
|
class Customer(BaseModel):
|
||||||
merchant_id: str
|
merchant_id: str
|
||||||
public_key: str
|
public_key: str
|
||||||
event_created_at: Optional[int] = None
|
event_created_at: int | None = None
|
||||||
profile: Optional[CustomerProfile] = None
|
profile: CustomerProfile | None = None
|
||||||
unread_messages: int = 0
|
unread_messages: int = 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
2616
poetry.lock
generated
2616
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,42 +1,45 @@
|
||||||
[tool.poetry]
|
[project]
|
||||||
name = "lnbits-nostrmarket"
|
name = "nostrmarket"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
requires-python = ">=3.10,<3.13"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||||
|
urls = { Homepage = "https://lnbits.com", Repository = "https://github.com/lnbits/nostrmarket" }
|
||||||
|
dependencies = [ "lnbits>1" ]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry]
|
||||||
python = "^3.10 | ^3.9"
|
package-mode = false
|
||||||
lnbits = {version = "*", allow-prereleases = true}
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[dependency-groups]
|
||||||
black = "^24.3.0"
|
dev = [
|
||||||
pytest-asyncio = "^0.21.0"
|
"black",
|
||||||
pytest = "^7.3.2"
|
"pytest-asyncio",
|
||||||
mypy = "^1.5.1"
|
"pytest",
|
||||||
pre-commit = "^3.2.2"
|
"mypy==1.17.1",
|
||||||
ruff = "^0.3.2"
|
"pre-commit",
|
||||||
|
"ruff",
|
||||||
[build-system]
|
"pytest-md",
|
||||||
requires = ["poetry-core>=1.0.0"]
|
"types-cffi",
|
||||||
build-backend = "poetry.core.masonry.api"
|
]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
exclude = "(nostr/*)"
|
exclude = "(nostr/*)"
|
||||||
|
plugins = ["pydantic.mypy"]
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = [
|
module = [
|
||||||
|
"nostr.*",
|
||||||
"secp256k1.*",
|
"secp256k1.*",
|
||||||
"embit.*",
|
|
||||||
"lnbits.*",
|
|
||||||
"lnurl.*",
|
|
||||||
"loguru.*",
|
|
||||||
"fastapi.*",
|
|
||||||
"pydantic.*",
|
|
||||||
"pyqrcode.*",
|
|
||||||
"shortuuid.*",
|
|
||||||
"httpx.*",
|
|
||||||
]
|
]
|
||||||
ignore_missing_imports = "True"
|
ignore_missing_imports = "True"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.pydantic-mypy]
|
||||||
|
init_forbid_extra = true
|
||||||
|
init_typed = true
|
||||||
|
warn_required_dynamic_aliases = true
|
||||||
|
warn_untyped_fields = true
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
log_cli = false
|
log_cli = false
|
||||||
testpaths = [
|
testpaths = [
|
||||||
|
|
|
||||||
17
services.py
17
services.py
|
|
@ -1,8 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
|
|
||||||
from lnbits.bolt11 import decode
|
from bolt11 import decode
|
||||||
from lnbits.core.crud import get_wallet
|
from lnbits.core.crud import get_wallet
|
||||||
from lnbits.core.services import create_invoice, websocket_updater
|
from lnbits.core.services import create_invoice, websocket_updater
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
@ -60,7 +59,7 @@ from .nostr.event import NostrEvent
|
||||||
|
|
||||||
async def create_new_order(
|
async def create_new_order(
|
||||||
merchant_public_key: str, data: PartialOrder
|
merchant_public_key: str, data: PartialOrder
|
||||||
) -> Optional[PaymentRequest]:
|
) -> PaymentRequest | None:
|
||||||
merchant = await get_merchant_by_pubkey(merchant_public_key)
|
merchant = await get_merchant_by_pubkey(merchant_public_key)
|
||||||
assert merchant, "Cannot find merchant for order!"
|
assert merchant, "Cannot find merchant for order!"
|
||||||
|
|
||||||
|
|
@ -140,7 +139,7 @@ async def update_merchant_to_nostr(
|
||||||
merchant: Merchant, delete_merchant=False
|
merchant: Merchant, delete_merchant=False
|
||||||
) -> Merchant:
|
) -> Merchant:
|
||||||
stalls = await get_stalls(merchant.id)
|
stalls = await get_stalls(merchant.id)
|
||||||
event: Optional[NostrEvent] = None
|
event: NostrEvent | None = None
|
||||||
for stall in stalls:
|
for stall in stalls:
|
||||||
assert stall.id
|
assert stall.id
|
||||||
products = await get_products(merchant.id, stall.id)
|
products = await get_products(merchant.id, stall.id)
|
||||||
|
|
@ -225,7 +224,7 @@ async def notify_client_of_order_status(
|
||||||
|
|
||||||
async def update_products_for_order(
|
async def update_products_for_order(
|
||||||
merchant: Merchant, order: Order
|
merchant: Merchant, order: Order
|
||||||
) -> Tuple[bool, str]:
|
) -> tuple[bool, str]:
|
||||||
product_ids = [i.product_id for i in order.items]
|
product_ids = [i.product_id for i in order.items]
|
||||||
success, products, message = await compute_products_new_quantity(
|
success, products, message = await compute_products_new_quantity(
|
||||||
merchant.id, product_ids, order.items
|
merchant.id, product_ids, order.items
|
||||||
|
|
@ -293,9 +292,9 @@ async def send_dm(
|
||||||
|
|
||||||
|
|
||||||
async def compute_products_new_quantity(
|
async def compute_products_new_quantity(
|
||||||
merchant_id: str, product_ids: List[str], items: List[OrderItem]
|
merchant_id: str, product_ids: list[str], items: list[OrderItem]
|
||||||
) -> Tuple[bool, List[Product], str]:
|
) -> tuple[bool, list[Product], str]:
|
||||||
products: List[Product] = await get_products_by_ids(merchant_id, product_ids)
|
products: list[Product] = await get_products_by_ids(merchant_id, product_ids)
|
||||||
|
|
||||||
for p in products:
|
for p in products:
|
||||||
required_quantity = next(
|
required_quantity = next(
|
||||||
|
|
@ -489,7 +488,7 @@ async def _handle_outgoing_dms(
|
||||||
|
|
||||||
async def _handle_incoming_structured_dm(
|
async def _handle_incoming_structured_dm(
|
||||||
merchant: Merchant, dm: DirectMessage, json_data: dict
|
merchant: Merchant, dm: DirectMessage, json_data: dict
|
||||||
) -> Tuple[DirectMessageType, Optional[str]]:
|
) -> tuple[DirectMessageType, str | None]:
|
||||||
try:
|
try:
|
||||||
if dm.type == DirectMessageType.CUSTOMER_ORDER.value and merchant.config.active:
|
if dm.type == DirectMessageType.CUSTOMER_ORDER.value and merchant.config.active:
|
||||||
json_resp = await _handle_new_order(
|
json_resp = await _handle_new_order(
|
||||||
|
|
|
||||||
|
|
@ -50,13 +50,16 @@ window.app.component('direct-messages', {
|
||||||
methods: {
|
methods: {
|
||||||
sendMessage: async function () {},
|
sendMessage: async function () {},
|
||||||
buildCustomerLabel: function (c) {
|
buildCustomerLabel: function (c) {
|
||||||
let label = `${c.profile.name || 'unknown'} ${c.profile.about || ''}`
|
if (!c) return ''
|
||||||
if (c.unread_messages) {
|
let label = c.profile.name || 'unknown'
|
||||||
label += `[new: ${c.unread_messages}]`
|
if (c.profile.about) {
|
||||||
|
label += ` - ${c.profile.about.substring(0, 30)}`
|
||||||
|
if (c.profile.about.length > 30) label += '...'
|
||||||
}
|
}
|
||||||
label += ` (${c.public_key.slice(0, 16)}...${c.public_key.slice(
|
if (c.unread_messages) {
|
||||||
c.public_key.length - 16
|
label = `[${c.unread_messages} new] ${label}`
|
||||||
)}`
|
}
|
||||||
|
label += ` (${c.public_key.slice(0, 8)}...${c.public_key.slice(-8)})`
|
||||||
return label
|
return label
|
||||||
},
|
},
|
||||||
getDirectMessages: async function (pubkey) {
|
getDirectMessages: async function (pubkey) {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
<div>
|
<div>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="row">
|
<div class="row items-center q-col-gutter-sm">
|
||||||
<div class="col-2">
|
<div class="col-auto">
|
||||||
<h6 class="text-subtitle1 q-my-none">Messages</h6>
|
<h6 class="text-subtitle1 q-my-none">Messages</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-auto">
|
||||||
<q-badge v-if="unreadMessages" color="primary" outline
|
<q-badge v-if="unreadMessages" color="primary" outline
|
||||||
><span v-text="unreadMessages"></span> new</q-badge
|
><span v-text="unreadMessages"></span> new</q-badge
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-auto q-ml-auto">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="activePublicKey"
|
v-if="activePublicKey"
|
||||||
@click="showClientOrders"
|
@click="showClientOrders"
|
||||||
unelevated
|
unelevated
|
||||||
outline
|
outline
|
||||||
class="float-right"
|
size="sm"
|
||||||
>Client Orders</q-btn
|
>Client Orders</q-btn
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,22 +26,40 @@
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="row">
|
<div class="row q-col-gutter-sm items-end">
|
||||||
<div class="col-10">
|
<div class="col" style="min-width: 0">
|
||||||
<q-select
|
<q-select
|
||||||
v-model="activePublicKey"
|
v-model="activePublicKey"
|
||||||
:options="customers.map(c => ({label: buildCustomerLabel(c), value: c.public_key}))"
|
: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()"
|
||||||
|
:display-value="activePublicKey ? buildCustomerLabel(customers.find(c => c.public_key === activePublicKey)) : ''"
|
||||||
|
class="ellipsis"
|
||||||
>
|
>
|
||||||
|
<template v-slot:option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>
|
||||||
|
<span v-text="scope.opt.label.split('(')[0]"></span>
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
class="text-mono"
|
||||||
|
style="word-break: break-all"
|
||||||
|
>
|
||||||
|
<span v-text="scope.opt.value"></span>
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-auto">
|
||||||
<q-btn
|
<q-btn
|
||||||
label="Add"
|
label="ADD"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="float-right q-mt-md"
|
unelevated
|
||||||
@click="showAddPublicKey = true"
|
@click="showAddPublicKey = true"
|
||||||
>
|
>
|
||||||
<q-tooltip> Add a public key to chat with </q-tooltip>
|
<q-tooltip> Add a public key to chat with </q-tooltip>
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,93 @@
|
||||||
<div>
|
<div>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
<div class="row q-mt-md">
|
|
||||||
<div class="col-6 q-pl-xl">Public Key</div>
|
<!-- Header with toggle -->
|
||||||
<div class="col-6">
|
<div class="row items-center justify-between q-mt-md q-px-md">
|
||||||
<q-toggle v-model="showPrivateKey" class="q-pl-xl" color="secodary">
|
<div class="text-subtitle2">Keys</div>
|
||||||
Show Private Key
|
<q-toggle
|
||||||
</q-toggle>
|
v-model="showPrivateKey"
|
||||||
</div>
|
color="primary"
|
||||||
|
label="Show Private Key"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<!-- QR Codes Container -->
|
||||||
<div class="col-6">
|
<div class="row q-col-gutter-md q-pa-md">
|
||||||
<div class="text-center q-mb-lg cursor-pointer">
|
<!-- Public Key QR -->
|
||||||
<q-responsive :ratio="1" class="q-mx-xl" @click="copyText(publicKey)">
|
<div class="col-12" :class="showPrivateKey ? 'col-sm-6' : ''">
|
||||||
|
<q-card flat bordered>
|
||||||
|
<q-card-section class="text-center">
|
||||||
|
<div class="text-subtitle2 q-mb-sm">Public Key</div>
|
||||||
|
<div
|
||||||
|
class="cursor-pointer q-mx-auto"
|
||||||
|
style="max-width: 200px"
|
||||||
|
@click="copyText(publicKey)"
|
||||||
|
>
|
||||||
|
<q-responsive :ratio="1">
|
||||||
<lnbits-qrcode
|
<lnbits-qrcode
|
||||||
:value="publicKey"
|
:value="publicKey"
|
||||||
:options="{width: 250}"
|
:options="{width: 200}"
|
||||||
|
:show-buttons="false"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></lnbits-qrcode>
|
></lnbits-qrcode>
|
||||||
</q-responsive>
|
</q-responsive>
|
||||||
<small><span v-text="publicKey"></span><br />Click to copy</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="q-mt-md text-caption text-mono" style="padding: 0 16px">
|
||||||
|
<span v-text="publicKey.substring(0, 8)"></span>...<span
|
||||||
|
v-text="publicKey.substring(publicKey.length - 8)"
|
||||||
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 cursor-pointer">
|
<q-btn
|
||||||
<div v-if="showPrivateKey">
|
flat
|
||||||
<div class="text-center q-mb-lg">
|
dense
|
||||||
<q-responsive
|
size="sm"
|
||||||
:ratio="1"
|
icon="content_copy"
|
||||||
class="q-mx-xl"
|
label="Click to copy"
|
||||||
|
@click="copyText(publicKey)"
|
||||||
|
class="q-mt-xs"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Private Key QR (conditional) -->
|
||||||
|
<div v-if="showPrivateKey" class="col-12 col-sm-6">
|
||||||
|
<q-card flat bordered>
|
||||||
|
<q-card-section class="text-center">
|
||||||
|
<div class="text-subtitle2 q-mb-sm text-warning">
|
||||||
|
<q-icon name="warning"></q-icon>
|
||||||
|
Private Key (Keep Secret!)
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="cursor-pointer q-mx-auto"
|
||||||
|
style="max-width: 200px"
|
||||||
@click="copyText(privateKey)"
|
@click="copyText(privateKey)"
|
||||||
>
|
>
|
||||||
<lnbits-qrcode :value="privateKey"></lnbits-qrcode>
|
<q-responsive :ratio="1">
|
||||||
|
<lnbits-qrcode
|
||||||
|
:value="privateKey"
|
||||||
|
:options="{width: 200}"
|
||||||
|
:show-buttons="false"
|
||||||
|
class="rounded-borders"
|
||||||
|
></lnbits-qrcode>
|
||||||
</q-responsive>
|
</q-responsive>
|
||||||
<small><span v-text="privateKey"></span><br />Click to copy</small>
|
</div>
|
||||||
</div>
|
<div class="q-mt-md text-caption text-mono" style="padding: 0 16px">
|
||||||
</div>
|
<span v-text="privateKey.substring(0, 8)"></span>...<span
|
||||||
|
v-text="privateKey.substring(privateKey.length - 8)"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
icon="content_copy"
|
||||||
|
label="Click to copy"
|
||||||
|
@click="copyText(privateKey)"
|
||||||
|
class="q-mt-xs"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@
|
||||||
/>
|
/>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
||||||
|
|
||||||
<q-td key="id" :props="props"
|
<q-td key="id" :props="props"
|
||||||
><span v-text="shortLabel(props.row.name)"></span
|
><span v-text="shortLabel(props.row.name)"></span
|
||||||
></q-td>
|
></q-td>
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
<div v-if="merchant && merchant.id">
|
<div v-if="merchant && merchant.id">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="row">
|
<div class="row items-center q-col-gutter-sm">
|
||||||
<div class="col-4">
|
<div class="col-12 col-sm-auto">
|
||||||
<merchant-details
|
<merchant-details
|
||||||
:merchant-id="merchant.id"
|
:merchant-id="merchant.id"
|
||||||
:inkey="g.user.wallets[0].inkey"
|
:inkey="g.user.wallets[0].inkey"
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
@merchant-deleted="handleMerchantDeleted"
|
@merchant-deleted="handleMerchantDeleted"
|
||||||
></merchant-details>
|
></merchant-details>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-12 col-sm-auto q-mx-sm">
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
<q-toggle
|
<q-toggle
|
||||||
@update:model-value="toggleMerchantState()"
|
@update:model-value="toggleMerchantState()"
|
||||||
size="md"
|
size="md"
|
||||||
|
|
@ -24,17 +25,17 @@
|
||||||
v-model="merchant.config.active"
|
v-model="merchant.config.active"
|
||||||
color="primary"
|
color="primary"
|
||||||
unchecked-icon="clear"
|
unchecked-icon="clear"
|
||||||
class="float-left"
|
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
|
class="q-ml-sm"
|
||||||
v-text="merchant.config.active ? 'Accepting Orders': 'Orders Paused'"
|
v-text="merchant.config.active ? 'Accepting Orders': 'Orders Paused'"
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
</div>
|
||||||
|
<div class="col-12 col-sm-auto q-ml-sm-auto">
|
||||||
<shipping-zones
|
<shipping-zones
|
||||||
:inkey="g.user.wallets[0].inkey"
|
:inkey="g.user.wallets[0].inkey"
|
||||||
:adminkey="g.user.wallets[0].adminkey"
|
:adminkey="g.user.wallets[0].adminkey"
|
||||||
class="float-right"
|
|
||||||
></shipping-zones>
|
></shipping-zones>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
31
views_api.py
31
views_api.py
|
|
@ -1,13 +1,12 @@
|
||||||
import json
|
import json
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
|
from lnbits.core.models import WalletTypeInfo
|
||||||
from lnbits.core.crud import get_account, update_account
|
from lnbits.core.crud import get_account, update_account
|
||||||
from lnbits.core.services import websocket_updater
|
from lnbits.core.services import websocket_updater
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
WalletTypeInfo,
|
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
|
|
@ -168,7 +167,7 @@ async def api_create_merchant(
|
||||||
@nostrmarket_ext.get("/api/v1/merchant")
|
@nostrmarket_ext.get("/api/v1/merchant")
|
||||||
async def api_get_merchant(
|
async def api_get_merchant(
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
) -> Optional[Merchant]:
|
) -> Merchant | None:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
|
|
@ -336,7 +335,7 @@ async def api_delete_merchant_on_nostr(
|
||||||
@nostrmarket_ext.get("/api/v1/zone")
|
@nostrmarket_ext.get("/api/v1/zone")
|
||||||
async def api_get_zones(
|
async def api_get_zones(
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
) -> List[Zone]:
|
) -> list[Zone]:
|
||||||
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"
|
||||||
|
|
@ -536,7 +535,7 @@ async def api_get_stall(
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/stall")
|
@nostrmarket_ext.get("/api/v1/stall")
|
||||||
async def api_get_stalls(
|
async def api_get_stalls(
|
||||||
pending: Optional[bool] = False,
|
pending: bool | None = False,
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
|
|
@ -560,7 +559,7 @@ async def api_get_stalls(
|
||||||
@nostrmarket_ext.get("/api/v1/stall/product/{stall_id}")
|
@nostrmarket_ext.get("/api/v1/stall/product/{stall_id}")
|
||||||
async def api_get_stall_products(
|
async def api_get_stall_products(
|
||||||
stall_id: str,
|
stall_id: str,
|
||||||
pending: Optional[bool] = False,
|
pending: bool | None = False,
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
|
|
@ -584,9 +583,9 @@ 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,
|
paid: bool | None = None,
|
||||||
shipped: Optional[bool] = None,
|
shipped: bool | None = None,
|
||||||
pubkey: Optional[str] = None,
|
pubkey: str | None = None,
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
|
|
@ -720,7 +719,7 @@ async def api_update_product(
|
||||||
async def api_get_product(
|
async def api_get_product(
|
||||||
product_id: str,
|
product_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
) -> Optional[Product]:
|
) -> Product | None:
|
||||||
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"
|
||||||
|
|
@ -805,9 +804,9 @@ async def api_get_order(
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/order")
|
@nostrmarket_ext.get("/api/v1/order")
|
||||||
async def api_get_orders(
|
async def api_get_orders(
|
||||||
paid: Optional[bool] = None,
|
paid: bool | None = None,
|
||||||
shipped: Optional[bool] = None,
|
shipped: bool | None = None,
|
||||||
pubkey: Optional[str] = None,
|
pubkey: str | None = None,
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
|
|
@ -893,7 +892,7 @@ async def api_update_order_status(
|
||||||
async def api_restore_order(
|
async def api_restore_order(
|
||||||
event_id: str,
|
event_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> Optional[Order]:
|
) -> Order | None:
|
||||||
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"
|
||||||
|
|
@ -1020,7 +1019,7 @@ async def api_reissue_order_invoice(
|
||||||
@nostrmarket_ext.get("/api/v1/message/{public_key}")
|
@nostrmarket_ext.get("/api/v1/message/{public_key}")
|
||||||
async def api_get_messages(
|
async def api_get_messages(
|
||||||
public_key: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
public_key: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||||
) -> List[DirectMessage]:
|
) -> list[DirectMessage]:
|
||||||
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"
|
||||||
|
|
@ -1076,7 +1075,7 @@ async def api_create_message(
|
||||||
@nostrmarket_ext.get("/api/v1/customer")
|
@nostrmarket_ext.get("/api/v1/customer")
|
||||||
async def api_get_customers(
|
async def api_get_customers(
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
) -> List[Customer]:
|
) -> list[Customer]:
|
||||||
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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue