mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-21 14:04:25 +01:00
remove market (#1516)
This commit is contained in:
parent
1eb3df60fc
commit
b2c580268e
22 changed files with 0 additions and 5890 deletions
|
@ -1,283 +0,0 @@
|
||||||
## Nostr Diagon Alley protocol (for resilient marketplaces)
|
|
||||||
|
|
||||||
`authur: Ben Arc`
|
|
||||||
|
|
||||||
#### Original protocol https://github.com/lnbits/Diagon-Alley
|
|
||||||
|
|
||||||
> The concepts around resilience in Diagon Alley helped influence the creation of the NOSTR protocol, now we get to build Diagon Alley on NOSTR!
|
|
||||||
|
|
||||||
In Diagon Alley, `merchant` and `customer` communicate via NOSTR relays, so loss of money, product information, and reputation become far less likely if attacked.
|
|
||||||
|
|
||||||
A `merchant` and `customer` both have a NOSTR key-pair that are used to sign notes and subscribe to events.
|
|
||||||
|
|
||||||
#### For further information about NOSTR, see https://github.com/nostr-protocol/nostr
|
|
||||||
|
|
||||||
|
|
||||||
## Terms
|
|
||||||
|
|
||||||
* `merchant` - seller of products with NOSTR key-pair
|
|
||||||
* `customer` - buyer of products with NOSTR key-pair
|
|
||||||
* `product` - item for sale by the `merchant`
|
|
||||||
* `stall` - list of products controlled by `merchant` (a `merchant` can have multiple stalls)
|
|
||||||
* `marketplace` - clientside software for searching `stalls` and purchasing `products`
|
|
||||||
|
|
||||||
## Diagon Alley Clients
|
|
||||||
|
|
||||||
### Merchant admin
|
|
||||||
|
|
||||||
Where the `merchant` creates, updates and deletes `stalls` and `products`, as well as where they manage sales, payments and communication with `customers`.
|
|
||||||
|
|
||||||
The `merchant` admin software can be purely clientside, but for `convenience` and uptime, implementations will likely have a server listening for NOSTR events.
|
|
||||||
|
|
||||||
### Marketplace
|
|
||||||
|
|
||||||
`Marketplace` software should be entirely clientside, either as a stand-alone app, or as a purely frontend webpage. A `customer` subscribes to different merchant NOSTR public keys, and those `merchants` `stalls` and `products` become listed and searchable. The marketplace client is like any other ecommerce site, with basket and checkout. `Marketplaces` may also wish to include a `customer` support area for direct message communication with `merchants`.
|
|
||||||
|
|
||||||
## `Merchant` publishing/updating products (event)
|
|
||||||
|
|
||||||
NIP-01 https://github.com/nostr-protocol/nips/blob/master/01.md uses the basic NOSTR event type.
|
|
||||||
|
|
||||||
The `merchant` event that publishes and updates product lists
|
|
||||||
|
|
||||||
The below json goes in `content` of NIP-01.
|
|
||||||
|
|
||||||
Data from newer events should replace data from older events.
|
|
||||||
|
|
||||||
`action` types (used to indicate changes):
|
|
||||||
* `update` element has changed
|
|
||||||
* `delete` element should be deleted
|
|
||||||
* `suspend` element is suspended
|
|
||||||
* `unsuspend` element is unsuspended
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"name": <String, name of merchant>,
|
|
||||||
"description": <String, description of merchant>,
|
|
||||||
"currency": <Str, currency used>,
|
|
||||||
"action": <String, optional action>,
|
|
||||||
"shipping": [
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from stall ID>,
|
|
||||||
"zones": <String, CSV of countries/zones>,
|
|
||||||
"price": <int, cost>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from stall ID>,
|
|
||||||
"zones": <String, CSV of countries/zones>,
|
|
||||||
"price": <int, cost>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from stall ID>,
|
|
||||||
"zones": <String, CSV of countries/zones>,
|
|
||||||
"price": <int, cost>,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stalls": [
|
|
||||||
{
|
|
||||||
"id": <UUID derived from merchant public-key>,
|
|
||||||
"name": <String, stall name>,
|
|
||||||
"description": <String, stall description>,
|
|
||||||
"categories": <String, CSV of voluntary categories>,
|
|
||||||
"shipping": <String, CSV of shipping ids>,
|
|
||||||
"action": <String, optional action>,
|
|
||||||
"products": [
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from stall ID>,
|
|
||||||
"name": <String, name of product>,
|
|
||||||
"description": <String, product description>,
|
|
||||||
"categories": <String, CSV of voluntary categories>,
|
|
||||||
"amount": <Int, number of units>,
|
|
||||||
"price": <Int, cost per unit>,
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from product ID>,
|
|
||||||
"name": <String, image name>,
|
|
||||||
"link": <String, URL or BASE64>
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"action": <String, optional action>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from stall ID>,
|
|
||||||
"name": <String, name of product>,
|
|
||||||
"description": <String, product description>,
|
|
||||||
"categories": <String, CSV of voluntary categories>,
|
|
||||||
"amount": <Int, number of units>,
|
|
||||||
"price": <Int, cost per unit>,
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from product ID>,
|
|
||||||
"name": <String, image name>,
|
|
||||||
"link": <String, URL or BASE64>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from product ID>,
|
|
||||||
"name": <String, image name>,
|
|
||||||
"link": <String, URL or BASE64>
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"action": <String, optional action>,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": <UUID derived from merchant public_key>,
|
|
||||||
"name": <String, stall name>,
|
|
||||||
"description": <String, stall description>,
|
|
||||||
"categories": <String, CSV of voluntary categories>,
|
|
||||||
"shipping": <String, CSV of shipping ids>,
|
|
||||||
"action": <String, optional action>,
|
|
||||||
"products": [
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from stall ID>,
|
|
||||||
"name": <String, name of product>,
|
|
||||||
"categories": <String, CSV of voluntary categories>,
|
|
||||||
"amount": <Int, number of units>,
|
|
||||||
"price": <Int, cost per unit>,
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from product ID>,
|
|
||||||
"name": <String, image name>,
|
|
||||||
"link": <String, URL or BASE64>
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"action": <String, optional action>,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
As all elements are optional, an `update` `action` to a `product` `image`, may look as simple as:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"stalls": [
|
|
||||||
{
|
|
||||||
"id": <UUID derived from merchant public-key>,
|
|
||||||
"products": [
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from stall ID>,
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from product ID>,
|
|
||||||
"name": <String, image name>,
|
|
||||||
"link": <String, URL or BASE64>
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"action": <String, optional action>,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Checkout events
|
|
||||||
|
|
||||||
NIP-04 https://github.com/nostr-protocol/nips/blob/master/04.md, all checkout events are encrypted
|
|
||||||
|
|
||||||
The below json goes in `content` of NIP-04.
|
|
||||||
|
|
||||||
### Step 1: `customer` order (event)
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from sum of product ids + timestamp>,
|
|
||||||
"name": <String, name of customer>,
|
|
||||||
"description": <String, description of customer>,
|
|
||||||
"address": <String, postal address>,
|
|
||||||
"message": <String, special request>,
|
|
||||||
"contact": [
|
|
||||||
"nostr": <String, NOSTR public key>,
|
|
||||||
"phone": <String, phone number>,
|
|
||||||
"email": <String, email address>
|
|
||||||
],
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": <String, product ID>,
|
|
||||||
"quantity": <String, stall name>,
|
|
||||||
"message": <String, special request>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": <String, product ID>,
|
|
||||||
"quantity": <String, stall name>,
|
|
||||||
"message": <String, special request>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": <String, product ID>,
|
|
||||||
"quantity": <String, stall name>,
|
|
||||||
"message": <String, special request>
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Merchant should verify the sum of product ids + timestamp.
|
|
||||||
|
|
||||||
### Step 2: `merchant` request payment (event)
|
|
||||||
|
|
||||||
Sent back from the merchant for payment. Any payment option is valid that the merchant can check.
|
|
||||||
|
|
||||||
The below json goes in `content` of NIP-04.
|
|
||||||
|
|
||||||
`payment_options`/`type` include:
|
|
||||||
* `url` URL to a payment page, stripe, paypal, btcpayserver, etc
|
|
||||||
* `btc` onchain bitcoin address
|
|
||||||
* `ln` bitcoin lightning invoice
|
|
||||||
* `lnurl` bitcoin lnurl-pay
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from sum of product ids + timestamp>,
|
|
||||||
"message": <String, message to customer>,
|
|
||||||
"payment_options": [
|
|
||||||
{
|
|
||||||
"type": <String, option type>,
|
|
||||||
"link": <String, url, btc address, ln invoice, etc>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": <String, option type>,
|
|
||||||
"link": <String, url, btc address, ln invoice, etc>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": <String, option type>,
|
|
||||||
"link": <String, url, btc address, ln invoice, etc>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: `merchant` verify payment/shipped (event)
|
|
||||||
|
|
||||||
Once payment has been received and processed.
|
|
||||||
|
|
||||||
The below json goes in `content` of NIP-04.
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"id": <String, UUID derived from sum of product ids + timestamp>,
|
|
||||||
"message": <String, message to customer>,
|
|
||||||
"paid": <Bool, true/false has received payment>,
|
|
||||||
"shipped": <Bool, true/false has been shipped>,
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Customer support events
|
|
||||||
|
|
||||||
Customer support is handle over whatever communication method was specified. If communicationg via nostr, NIP-04 is used https://github.com/nostr-protocol/nips/blob/master/04.md.
|
|
||||||
|
|
||||||
## Additional
|
|
||||||
|
|
||||||
Standard data models can be found here <a href="models.json">here</a>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
|
||||||
from starlette.staticfiles import StaticFiles
|
|
||||||
|
|
||||||
from lnbits.db import Database
|
|
||||||
from lnbits.helpers import template_renderer
|
|
||||||
from lnbits.tasks import catch_everything_and_restart
|
|
||||||
|
|
||||||
db = Database("ext_market")
|
|
||||||
|
|
||||||
market_ext: APIRouter = APIRouter(prefix="/market", tags=["market"])
|
|
||||||
|
|
||||||
market_static_files = [
|
|
||||||
{
|
|
||||||
"path": "/market/static",
|
|
||||||
"app": StaticFiles(directory="lnbits/extensions/market/static"),
|
|
||||||
"name": "market_static",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# if 'nostradmin' not in LNBITS_ADMIN_EXTENSIONS:
|
|
||||||
# @market_ext.get("/", response_class=HTMLResponse)
|
|
||||||
# async def index(request: Request):
|
|
||||||
# return template_renderer().TemplateResponse(
|
|
||||||
# "error.html", {"request": request, "err": "Ask system admin to enable NostrAdmin!"}
|
|
||||||
# )
|
|
||||||
# else:
|
|
||||||
|
|
||||||
|
|
||||||
def market_renderer():
|
|
||||||
return template_renderer(["lnbits/extensions/market/templates"])
|
|
||||||
# return template_renderer(["lnbits/extensions/market/templates"])
|
|
||||||
|
|
||||||
|
|
||||||
from .tasks import wait_for_paid_invoices
|
|
||||||
from .views import * # noqa: F401,F403
|
|
||||||
from .views_api import * # noqa: F401,F403
|
|
||||||
|
|
||||||
|
|
||||||
def market_start():
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Marketplace",
|
|
||||||
"short_description": "Webshop/market on LNbits",
|
|
||||||
"tile": "/market/static/images/bitcoin-shop.png",
|
|
||||||
"contributors": ["benarc", "talvasconcelos"]
|
|
||||||
}
|
|
|
@ -1,488 +0,0 @@
|
||||||
from typing import List, Optional, Union
|
|
||||||
|
|
||||||
# from lnbits.db import open_ext_db
|
|
||||||
from lnbits.db import SQLITE
|
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
|
||||||
|
|
||||||
from . import db
|
|
||||||
from .models import (
|
|
||||||
ChatMessage,
|
|
||||||
CreateChatMessage,
|
|
||||||
CreateMarket,
|
|
||||||
Market,
|
|
||||||
MarketSettings,
|
|
||||||
OrderDetail,
|
|
||||||
Orders,
|
|
||||||
Products,
|
|
||||||
Stalls,
|
|
||||||
Zones,
|
|
||||||
createOrder,
|
|
||||||
createOrderDetails,
|
|
||||||
createProduct,
|
|
||||||
createStalls,
|
|
||||||
createZones,
|
|
||||||
)
|
|
||||||
|
|
||||||
###Products
|
|
||||||
|
|
||||||
|
|
||||||
async def create_market_product(data: createProduct) -> Products:
|
|
||||||
product_id = urlsafe_short_hash()
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO market.products (id, stall, product, categories, description, image, price, quantity)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
product_id,
|
|
||||||
data.stall,
|
|
||||||
data.product,
|
|
||||||
data.categories,
|
|
||||||
data.description,
|
|
||||||
data.image,
|
|
||||||
data.price,
|
|
||||||
data.quantity,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
product = await get_market_product(product_id)
|
|
||||||
assert product, "Newly created product couldn't be retrieved"
|
|
||||||
return product
|
|
||||||
|
|
||||||
|
|
||||||
async def update_market_product(product_id: str, **kwargs) -> Optional[Products]:
|
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
f"UPDATE market.products SET {q} WHERE id = ?",
|
|
||||||
(*kwargs.values(), product_id),
|
|
||||||
)
|
|
||||||
row = await db.fetchone("SELECT * FROM market.products WHERE id = ?", (product_id,))
|
|
||||||
|
|
||||||
return Products(**row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_product(product_id: str) -> Optional[Products]:
|
|
||||||
row = await db.fetchone("SELECT * FROM market.products WHERE id = ?", (product_id,))
|
|
||||||
return Products(**row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_products(stall_ids: Union[str, List[str]]) -> List[Products]:
|
|
||||||
if isinstance(stall_ids, str):
|
|
||||||
stall_ids = [stall_ids]
|
|
||||||
|
|
||||||
# with open_ext_db("market") as db:
|
|
||||||
q = ",".join(["?"] * len(stall_ids))
|
|
||||||
rows = await db.fetchall(
|
|
||||||
f"""
|
|
||||||
SELECT * FROM market.products WHERE stall IN ({q})
|
|
||||||
""",
|
|
||||||
(*stall_ids,),
|
|
||||||
)
|
|
||||||
return [Products(**row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_market_product(product_id: str) -> None:
|
|
||||||
await db.execute("DELETE FROM market.products WHERE id = ?", (product_id,))
|
|
||||||
|
|
||||||
|
|
||||||
###zones
|
|
||||||
|
|
||||||
|
|
||||||
async def create_market_zone(user, data: createZones) -> Zones:
|
|
||||||
zone_id = urlsafe_short_hash()
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO market.zones (
|
|
||||||
id,
|
|
||||||
"user",
|
|
||||||
cost,
|
|
||||||
countries
|
|
||||||
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(zone_id, user, data.cost, data.countries.lower()),
|
|
||||||
)
|
|
||||||
|
|
||||||
zone = await get_market_zone(zone_id)
|
|
||||||
assert zone, "Newly created zone couldn't be retrieved"
|
|
||||||
return zone
|
|
||||||
|
|
||||||
|
|
||||||
async def update_market_zone(zone_id: str, **kwargs) -> Optional[Zones]:
|
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
|
||||||
await db.execute(
|
|
||||||
f"UPDATE market.zones SET {q} WHERE id = ?",
|
|
||||||
(*kwargs.values(), zone_id),
|
|
||||||
)
|
|
||||||
row = await db.fetchone("SELECT * FROM market.zones WHERE id = ?", (zone_id,))
|
|
||||||
return Zones(**row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_zone(zone_id: str) -> Optional[Zones]:
|
|
||||||
row = await db.fetchone("SELECT * FROM market.zones WHERE id = ?", (zone_id,))
|
|
||||||
return Zones(**row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_zones(user: str) -> List[Zones]:
|
|
||||||
rows = await db.fetchall('SELECT * FROM market.zones WHERE "user" = ?', (user,))
|
|
||||||
return [Zones(**row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_market_zone(zone_id: str) -> None:
|
|
||||||
await db.execute("DELETE FROM market.zones WHERE id = ?", (zone_id,))
|
|
||||||
|
|
||||||
|
|
||||||
###Stalls
|
|
||||||
|
|
||||||
|
|
||||||
async def create_market_stall(data: createStalls) -> Stalls:
|
|
||||||
stall_id = urlsafe_short_hash()
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO market.stalls (
|
|
||||||
id,
|
|
||||||
wallet,
|
|
||||||
name,
|
|
||||||
currency,
|
|
||||||
publickey,
|
|
||||||
relays,
|
|
||||||
shippingzones
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
stall_id,
|
|
||||||
data.wallet,
|
|
||||||
data.name,
|
|
||||||
data.currency,
|
|
||||||
data.publickey,
|
|
||||||
data.relays,
|
|
||||||
data.shippingzones,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
stall = await get_market_stall(stall_id)
|
|
||||||
assert stall, "Newly created stall couldn't be retrieved"
|
|
||||||
return stall
|
|
||||||
|
|
||||||
|
|
||||||
async def update_market_stall(stall_id: str, **kwargs) -> Optional[Stalls]:
|
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
|
||||||
await db.execute(
|
|
||||||
f"UPDATE market.stalls SET {q} WHERE id = ?",
|
|
||||||
(*kwargs.values(), stall_id),
|
|
||||||
)
|
|
||||||
row = await db.fetchone("SELECT * FROM market.stalls WHERE id = ?", (stall_id,))
|
|
||||||
return Stalls(**row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_stall(stall_id: str) -> Optional[Stalls]:
|
|
||||||
row = await db.fetchone("SELECT * FROM market.stalls WHERE id = ?", (stall_id,))
|
|
||||||
return Stalls(**row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_stalls(wallet_ids: Union[str, List[str]]) -> List[Stalls]:
|
|
||||||
q = ",".join(["?"] * len(wallet_ids))
|
|
||||||
rows = await db.fetchall(
|
|
||||||
f"SELECT * FROM market.stalls WHERE wallet IN ({q})", (*wallet_ids,)
|
|
||||||
)
|
|
||||||
return [Stalls(**row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_stalls_by_ids(stall_ids: Union[str, List[str]]) -> List[Stalls]:
|
|
||||||
q = ",".join(["?"] * len(stall_ids))
|
|
||||||
rows = await db.fetchall(
|
|
||||||
f"SELECT * FROM market.stalls WHERE id IN ({q})", (*stall_ids,)
|
|
||||||
)
|
|
||||||
return [Stalls(**row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_market_stall(stall_id: str) -> None:
|
|
||||||
await db.execute("DELETE FROM market.stalls WHERE id = ?", (stall_id,))
|
|
||||||
|
|
||||||
|
|
||||||
###Orders
|
|
||||||
|
|
||||||
|
|
||||||
async def create_market_order(data: createOrder, invoiceid: str):
|
|
||||||
returning = "" if db.type == SQLITE else "RETURNING ID"
|
|
||||||
method = db.execute if db.type == SQLITE else db.fetchone
|
|
||||||
|
|
||||||
result = await (method)(
|
|
||||||
f"""
|
|
||||||
INSERT INTO market.orders (wallet, shippingzone, address, email, total, invoiceid, paid, shipped)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
{returning}
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
data.wallet,
|
|
||||||
data.shippingzone,
|
|
||||||
data.address,
|
|
||||||
data.email,
|
|
||||||
data.total,
|
|
||||||
invoiceid,
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if db.type == SQLITE:
|
|
||||||
return result._result_proxy.lastrowid
|
|
||||||
else:
|
|
||||||
return result[0]
|
|
||||||
|
|
||||||
|
|
||||||
async def create_market_order_details(order_id: str, data: List[createOrderDetails]):
|
|
||||||
for item in data:
|
|
||||||
item_id = urlsafe_short_hash()
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO market.order_details (id, order_id, product_id, quantity)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
item_id,
|
|
||||||
order_id,
|
|
||||||
item.product_id,
|
|
||||||
item.quantity,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
order_details = await get_market_order_details(order_id)
|
|
||||||
return order_details
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_order_details(order_id: str) -> List[OrderDetail]:
|
|
||||||
rows = await db.fetchall(
|
|
||||||
"SELECT * FROM market.order_details WHERE order_id = ?", (order_id,)
|
|
||||||
)
|
|
||||||
|
|
||||||
return [OrderDetail(**row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_order(order_id: str) -> Optional[Orders]:
|
|
||||||
row = await db.fetchone("SELECT * FROM market.orders WHERE id = ?", (order_id,))
|
|
||||||
return Orders(**row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_order_invoiceid(invoice_id: str) -> Optional[Orders]:
|
|
||||||
row = await db.fetchone(
|
|
||||||
"SELECT * FROM market.orders WHERE invoiceid = ?", (invoice_id,)
|
|
||||||
)
|
|
||||||
return Orders(**row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def set_market_order_paid(payment_hash: str):
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
UPDATE market.orders
|
|
||||||
SET paid = true
|
|
||||||
WHERE invoiceid = ?
|
|
||||||
""",
|
|
||||||
(payment_hash,),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def set_market_order_pubkey(payment_hash: str, pubkey: str):
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
UPDATE market.orders
|
|
||||||
SET pubkey = ?
|
|
||||||
WHERE invoiceid = ?
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
pubkey,
|
|
||||||
payment_hash,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_market_product_stock(products):
|
|
||||||
|
|
||||||
q = "\n".join(
|
|
||||||
[f"""WHEN id='{p.product_id}' THEN quantity - {p.quantity}""" for p in products]
|
|
||||||
)
|
|
||||||
v = ",".join(["?"] * len(products))
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
UPDATE market.products
|
|
||||||
SET quantity=(CASE
|
|
||||||
{q}
|
|
||||||
END)
|
|
||||||
WHERE id IN ({v});
|
|
||||||
""",
|
|
||||||
(*[p.product_id for p in products],),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]:
|
|
||||||
if isinstance(wallet_ids, str):
|
|
||||||
wallet_ids = [wallet_ids]
|
|
||||||
|
|
||||||
q = ",".join(["?"] * len(wallet_ids))
|
|
||||||
rows = await db.fetchall(
|
|
||||||
f"SELECT * FROM market.orders WHERE wallet IN ({q})", (*wallet_ids,)
|
|
||||||
)
|
|
||||||
#
|
|
||||||
return [Orders(**row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_market_order(order_id: str) -> None:
|
|
||||||
await db.execute("DELETE FROM market.orders WHERE id = ?", (order_id,))
|
|
||||||
|
|
||||||
|
|
||||||
### Market/Marketplace
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_markets(user: str) -> List[Market]:
|
|
||||||
rows = await db.fetchall("SELECT * FROM market.markets WHERE usr = ?", (user,))
|
|
||||||
return [Market(**row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_market(market_id: str) -> Optional[Market]:
|
|
||||||
row = await db.fetchone("SELECT * FROM market.markets WHERE id = ?", (market_id,))
|
|
||||||
return Market(**row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_market_stalls(market_id: str):
|
|
||||||
rows = await db.fetchall(
|
|
||||||
"SELECT * FROM market.market_stalls WHERE marketid = ?", (market_id,)
|
|
||||||
)
|
|
||||||
|
|
||||||
ids = [row["stallid"] for row in rows]
|
|
||||||
|
|
||||||
return await get_market_stalls_by_ids(ids)
|
|
||||||
|
|
||||||
|
|
||||||
async def create_market_market(data: CreateMarket):
|
|
||||||
market_id = urlsafe_short_hash()
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO market.markets (id, usr, name)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
market_id,
|
|
||||||
data.usr,
|
|
||||||
data.name,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
market = await get_market_market(market_id)
|
|
||||||
assert market, "Newly created market couldn't be retrieved"
|
|
||||||
return market
|
|
||||||
|
|
||||||
|
|
||||||
async def create_market_market_stalls(market_id: str, data: List[str]):
|
|
||||||
for stallid in data:
|
|
||||||
id = urlsafe_short_hash()
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO market.market_stalls (id, marketid, stallid)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
id,
|
|
||||||
market_id,
|
|
||||||
stallid,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
market_stalls = await get_market_market_stalls(market_id)
|
|
||||||
return market_stalls
|
|
||||||
|
|
||||||
|
|
||||||
async def update_market_market(market_id: str, name: str):
|
|
||||||
await db.execute(
|
|
||||||
"UPDATE market.markets SET name = ? WHERE id = ?",
|
|
||||||
(name, market_id),
|
|
||||||
)
|
|
||||||
await db.execute(
|
|
||||||
"DELETE FROM market.market_stalls WHERE marketid = ?",
|
|
||||||
(market_id,),
|
|
||||||
)
|
|
||||||
|
|
||||||
market = await get_market_market(market_id)
|
|
||||||
return market
|
|
||||||
|
|
||||||
|
|
||||||
### CHAT / MESSAGES
|
|
||||||
|
|
||||||
|
|
||||||
async def create_chat_message(data: CreateChatMessage):
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO market.messages (msg, pubkey, id_conversation)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
data.msg,
|
|
||||||
data.pubkey,
|
|
||||||
data.room_name,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_latest_chat_messages(room_name: str):
|
|
||||||
rows = await db.fetchall(
|
|
||||||
"SELECT * FROM market.messages WHERE id_conversation = ? ORDER BY timestamp DESC LIMIT 20",
|
|
||||||
(room_name,),
|
|
||||||
)
|
|
||||||
|
|
||||||
return [ChatMessage(**row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_chat_messages(room_name: str):
|
|
||||||
rows = await db.fetchall(
|
|
||||||
"SELECT * FROM market.messages WHERE id_conversation = ? ORDER BY timestamp DESC",
|
|
||||||
(room_name,),
|
|
||||||
)
|
|
||||||
|
|
||||||
return [ChatMessage(**row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_chat_by_merchant(ids: List[str]) -> List[ChatMessage]:
|
|
||||||
|
|
||||||
q = ",".join(["?"] * len(ids))
|
|
||||||
rows = await db.fetchall(
|
|
||||||
f"SELECT * FROM market.messages WHERE id_conversation IN ({q})",
|
|
||||||
(*ids,),
|
|
||||||
)
|
|
||||||
return [ChatMessage(**row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_settings(user) -> Optional[MarketSettings]:
|
|
||||||
row = await db.fetchone(
|
|
||||||
"""SELECT * FROM market.settings WHERE "user" = ?""", (user,)
|
|
||||||
)
|
|
||||||
|
|
||||||
return MarketSettings(**row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def create_market_settings(user: str, data):
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO market.settings ("user", currency, fiat_base_multiplier)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
user,
|
|
||||||
data.currency,
|
|
||||||
data.fiat_base_multiplier,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def set_market_settings(user: str, data):
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
UPDATE market.settings
|
|
||||||
SET currency = ?, fiat_base_multiplier = ?
|
|
||||||
WHERE "user" = ?;
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
data.currency,
|
|
||||||
data.fiat_base_multiplier,
|
|
||||||
user,
|
|
||||||
),
|
|
||||||
)
|
|
|
@ -1,156 +0,0 @@
|
||||||
async def m001_initial(db):
|
|
||||||
"""
|
|
||||||
Initial Market settings table.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE market.settings (
|
|
||||||
"user" TEXT PRIMARY KEY,
|
|
||||||
currency TEXT DEFAULT 'sat',
|
|
||||||
fiat_base_multiplier INTEGER DEFAULT 1
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Initial stalls table.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE market.stalls (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
wallet TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
currency TEXT,
|
|
||||||
publickey TEXT,
|
|
||||||
relays TEXT,
|
|
||||||
shippingzones TEXT NOT NULL,
|
|
||||||
rating INTEGER DEFAULT 0
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Initial products table.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE market.products (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
stall TEXT NOT NULL REFERENCES {db.references_schema}stalls (id) ON DELETE CASCADE,
|
|
||||||
product TEXT NOT NULL,
|
|
||||||
categories TEXT,
|
|
||||||
description TEXT,
|
|
||||||
image TEXT,
|
|
||||||
price INTEGER NOT NULL,
|
|
||||||
quantity INTEGER NOT NULL,
|
|
||||||
rating INTEGER DEFAULT 0
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Initial zones table.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE market.zones (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
"user" TEXT NOT NULL,
|
|
||||||
cost TEXT NOT NULL,
|
|
||||||
countries TEXT NOT NULL
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Initial orders table.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE market.orders (
|
|
||||||
id {db.serial_primary_key},
|
|
||||||
wallet TEXT NOT NULL,
|
|
||||||
username TEXT,
|
|
||||||
pubkey TEXT,
|
|
||||||
shippingzone TEXT NOT NULL,
|
|
||||||
address TEXT NOT NULL,
|
|
||||||
email TEXT NOT NULL,
|
|
||||||
total INTEGER NOT NULL,
|
|
||||||
invoiceid TEXT NOT NULL,
|
|
||||||
paid BOOLEAN NOT NULL,
|
|
||||||
shipped BOOLEAN NOT NULL,
|
|
||||||
time TIMESTAMP NOT NULL DEFAULT """
|
|
||||||
+ db.timestamp_now
|
|
||||||
+ """
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Initial order details table.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE market.order_details (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
order_id INTEGER NOT NULL REFERENCES {db.references_schema}orders (id) ON DELETE CASCADE,
|
|
||||||
product_id TEXT NOT NULL REFERENCES {db.references_schema}products (id) ON DELETE CASCADE,
|
|
||||||
quantity INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Initial market table.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE market.markets (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
usr TEXT NOT NULL,
|
|
||||||
name TEXT
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Initial market stalls table.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE market.market_stalls (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
marketid TEXT NOT NULL REFERENCES {db.references_schema}markets (id) ON DELETE CASCADE,
|
|
||||||
stallid TEXT NOT NULL REFERENCES {db.references_schema}stalls (id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Initial chat messages table.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE market.messages (
|
|
||||||
id {db.serial_primary_key},
|
|
||||||
msg TEXT NOT NULL,
|
|
||||||
pubkey TEXT NOT NULL,
|
|
||||||
id_conversation TEXT NOT NULL,
|
|
||||||
timestamp TIMESTAMP NOT NULL DEFAULT """
|
|
||||||
+ db.timestamp_now
|
|
||||||
+ """
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
if db.type != "SQLITE":
|
|
||||||
"""
|
|
||||||
Create indexes for message fetching
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
"CREATE INDEX idx_messages_timestamp ON market.messages (timestamp DESC)"
|
|
||||||
)
|
|
||||||
await db.execute(
|
|
||||||
"CREATE INDEX idx_messages_conversations ON market.messages (id_conversation)"
|
|
||||||
)
|
|
|
@ -1,227 +0,0 @@
|
||||||
{
|
|
||||||
"shipping_zones": [
|
|
||||||
"Free (digital)",
|
|
||||||
"Worldwide",
|
|
||||||
"Europe",
|
|
||||||
"Australia",
|
|
||||||
"Austria",
|
|
||||||
"Belgium",
|
|
||||||
"Brazil",
|
|
||||||
"Canada",
|
|
||||||
"Denmark",
|
|
||||||
"Finland",
|
|
||||||
"France",
|
|
||||||
"Germany",
|
|
||||||
"Greece",
|
|
||||||
"Hong Kong",
|
|
||||||
"Hungary",
|
|
||||||
"Ireland",
|
|
||||||
"Indonesia",
|
|
||||||
"Israel",
|
|
||||||
"Italy",
|
|
||||||
"Japan",
|
|
||||||
"Kazakhstan",
|
|
||||||
"Korea",
|
|
||||||
"Luxembourg",
|
|
||||||
"Malaysia",
|
|
||||||
"Mexico",
|
|
||||||
"Netherlands",
|
|
||||||
"New Zealand",
|
|
||||||
"Norway",
|
|
||||||
"Poland",
|
|
||||||
"Portugal",
|
|
||||||
"Russia",
|
|
||||||
"Saudi Arabia",
|
|
||||||
"Singapore",
|
|
||||||
"Spain",
|
|
||||||
"Sweden",
|
|
||||||
"Switzerland",
|
|
||||||
"Thailand",
|
|
||||||
"Turkey",
|
|
||||||
"Ukraine",
|
|
||||||
"United Kingdom**",
|
|
||||||
"United States***",
|
|
||||||
"Vietnam",
|
|
||||||
"China"
|
|
||||||
],
|
|
||||||
"categories": [
|
|
||||||
"Fashion (clothing and accessories)",
|
|
||||||
"Health (and beauty)",
|
|
||||||
"Toys (and baby equipment)",
|
|
||||||
"Media (Books and CDs)",
|
|
||||||
"Groceries (Food and Drink)",
|
|
||||||
"Technology (Phones and Computers)",
|
|
||||||
"Home (furniture and accessories)",
|
|
||||||
"Gifts (flowers, cards, etc)",
|
|
||||||
"Adult",
|
|
||||||
"Other"
|
|
||||||
],
|
|
||||||
"currency": {
|
|
||||||
"BTC": "Bitcoin",
|
|
||||||
"SAT": "Bitcoin satoshis",
|
|
||||||
"AED": "United Arab Emirates Dirham",
|
|
||||||
"AFN": "Afghan Afghani",
|
|
||||||
"ALL": "Albanian Lek",
|
|
||||||
"AMD": "Armenian Dram",
|
|
||||||
"ANG": "Netherlands Antillean Gulden",
|
|
||||||
"AOA": "Angolan Kwanza",
|
|
||||||
"ARS": "Argentine Peso",
|
|
||||||
"AUD": "Australian Dollar",
|
|
||||||
"AWG": "Aruban Florin",
|
|
||||||
"AZN": "Azerbaijani Manat",
|
|
||||||
"BAM": "Bosnia and Herzegovina Convertible Mark",
|
|
||||||
"BBD": "Barbadian Dollar",
|
|
||||||
"BDT": "Bangladeshi Taka",
|
|
||||||
"BGN": "Bulgarian Lev",
|
|
||||||
"BHD": "Bahraini Dinar",
|
|
||||||
"BIF": "Burundian Franc",
|
|
||||||
"BMD": "Bermudian Dollar",
|
|
||||||
"BND": "Brunei Dollar",
|
|
||||||
"BOB": "Bolivian Boliviano",
|
|
||||||
"BRL": "Brazilian Real",
|
|
||||||
"BSD": "Bahamian Dollar",
|
|
||||||
"BTN": "Bhutanese Ngultrum",
|
|
||||||
"BWP": "Botswana Pula",
|
|
||||||
"BYN": "Belarusian Ruble",
|
|
||||||
"BYR": "Belarusian Ruble",
|
|
||||||
"BZD": "Belize Dollar",
|
|
||||||
"CAD": "Canadian Dollar",
|
|
||||||
"CDF": "Congolese Franc",
|
|
||||||
"CHF": "Swiss Franc",
|
|
||||||
"CLF": "Unidad de Fomento",
|
|
||||||
"CLP": "Chilean Peso",
|
|
||||||
"CNH": "Chinese Renminbi Yuan Offshore",
|
|
||||||
"CNY": "Chinese Renminbi Yuan",
|
|
||||||
"COP": "Colombian Peso",
|
|
||||||
"CRC": "Costa Rican Colón",
|
|
||||||
"CUC": "Cuban Convertible Peso",
|
|
||||||
"CVE": "Cape Verdean Escudo",
|
|
||||||
"CZK": "Czech Koruna",
|
|
||||||
"DJF": "Djiboutian Franc",
|
|
||||||
"DKK": "Danish Krone",
|
|
||||||
"DOP": "Dominican Peso",
|
|
||||||
"DZD": "Algerian Dinar",
|
|
||||||
"EGP": "Egyptian Pound",
|
|
||||||
"ERN": "Eritrean Nakfa",
|
|
||||||
"ETB": "Ethiopian Birr",
|
|
||||||
"EUR": "Euro",
|
|
||||||
"FJD": "Fijian Dollar",
|
|
||||||
"FKP": "Falkland Pound",
|
|
||||||
"GBP": "British Pound",
|
|
||||||
"GEL": "Georgian Lari",
|
|
||||||
"GGP": "Guernsey Pound",
|
|
||||||
"GHS": "Ghanaian Cedi",
|
|
||||||
"GIP": "Gibraltar Pound",
|
|
||||||
"GMD": "Gambian Dalasi",
|
|
||||||
"GNF": "Guinean Franc",
|
|
||||||
"GTQ": "Guatemalan Quetzal",
|
|
||||||
"GYD": "Guyanese Dollar",
|
|
||||||
"HKD": "Hong Kong Dollar",
|
|
||||||
"HNL": "Honduran Lempira",
|
|
||||||
"HRK": "Croatian Kuna",
|
|
||||||
"HTG": "Haitian Gourde",
|
|
||||||
"HUF": "Hungarian Forint",
|
|
||||||
"IDR": "Indonesian Rupiah",
|
|
||||||
"ILS": "Israeli New Sheqel",
|
|
||||||
"IMP": "Isle of Man Pound",
|
|
||||||
"INR": "Indian Rupee",
|
|
||||||
"IQD": "Iraqi Dinar",
|
|
||||||
"ISK": "Icelandic Króna",
|
|
||||||
"JEP": "Jersey Pound",
|
|
||||||
"JMD": "Jamaican Dollar",
|
|
||||||
"JOD": "Jordanian Dinar",
|
|
||||||
"JPY": "Japanese Yen",
|
|
||||||
"KES": "Kenyan Shilling",
|
|
||||||
"KGS": "Kyrgyzstani Som",
|
|
||||||
"KHR": "Cambodian Riel",
|
|
||||||
"KMF": "Comorian Franc",
|
|
||||||
"KRW": "South Korean Won",
|
|
||||||
"KWD": "Kuwaiti Dinar",
|
|
||||||
"KYD": "Cayman Islands Dollar",
|
|
||||||
"KZT": "Kazakhstani Tenge",
|
|
||||||
"LAK": "Lao Kip",
|
|
||||||
"LBP": "Lebanese Pound",
|
|
||||||
"LKR": "Sri Lankan Rupee",
|
|
||||||
"LRD": "Liberian Dollar",
|
|
||||||
"LSL": "Lesotho Loti",
|
|
||||||
"LYD": "Libyan Dinar",
|
|
||||||
"MAD": "Moroccan Dirham",
|
|
||||||
"MDL": "Moldovan Leu",
|
|
||||||
"MGA": "Malagasy Ariary",
|
|
||||||
"MKD": "Macedonian Denar",
|
|
||||||
"MMK": "Myanmar Kyat",
|
|
||||||
"MNT": "Mongolian Tögrög",
|
|
||||||
"MOP": "Macanese Pataca",
|
|
||||||
"MRO": "Mauritanian Ouguiya",
|
|
||||||
"MUR": "Mauritian Rupee",
|
|
||||||
"MVR": "Maldivian Rufiyaa",
|
|
||||||
"MWK": "Malawian Kwacha",
|
|
||||||
"MXN": "Mexican Peso",
|
|
||||||
"MYR": "Malaysian Ringgit",
|
|
||||||
"MZN": "Mozambican Metical",
|
|
||||||
"NAD": "Namibian Dollar",
|
|
||||||
"NGN": "Nigerian Naira",
|
|
||||||
"NIO": "Nicaraguan Córdoba",
|
|
||||||
"NOK": "Norwegian Krone",
|
|
||||||
"NPR": "Nepalese Rupee",
|
|
||||||
"NZD": "New Zealand Dollar",
|
|
||||||
"OMR": "Omani Rial",
|
|
||||||
"PAB": "Panamanian Balboa",
|
|
||||||
"PEN": "Peruvian Sol",
|
|
||||||
"PGK": "Papua New Guinean Kina",
|
|
||||||
"PHP": "Philippine Peso",
|
|
||||||
"PKR": "Pakistani Rupee",
|
|
||||||
"PLN": "Polish Złoty",
|
|
||||||
"PYG": "Paraguayan Guaraní",
|
|
||||||
"QAR": "Qatari Riyal",
|
|
||||||
"RON": "Romanian Leu",
|
|
||||||
"RSD": "Serbian Dinar",
|
|
||||||
"RUB": "Russian Ruble",
|
|
||||||
"RWF": "Rwandan Franc",
|
|
||||||
"SAR": "Saudi Riyal",
|
|
||||||
"SBD": "Solomon Islands Dollar",
|
|
||||||
"SCR": "Seychellois Rupee",
|
|
||||||
"SEK": "Swedish Krona",
|
|
||||||
"SGD": "Singapore Dollar",
|
|
||||||
"SHP": "Saint Helenian Pound",
|
|
||||||
"SLL": "Sierra Leonean Leone",
|
|
||||||
"SOS": "Somali Shilling",
|
|
||||||
"SRD": "Surinamese Dollar",
|
|
||||||
"SSP": "South Sudanese Pound",
|
|
||||||
"STD": "São Tomé and Príncipe Dobra",
|
|
||||||
"SVC": "Salvadoran Colón",
|
|
||||||
"SZL": "Swazi Lilangeni",
|
|
||||||
"THB": "Thai Baht",
|
|
||||||
"TJS": "Tajikistani Somoni",
|
|
||||||
"TMT": "Turkmenistani Manat",
|
|
||||||
"TND": "Tunisian Dinar",
|
|
||||||
"TOP": "Tongan Paʻanga",
|
|
||||||
"TRY": "Turkish Lira",
|
|
||||||
"TTD": "Trinidad and Tobago Dollar",
|
|
||||||
"TWD": "New Taiwan Dollar",
|
|
||||||
"TZS": "Tanzanian Shilling",
|
|
||||||
"UAH": "Ukrainian Hryvnia",
|
|
||||||
"UGX": "Ugandan Shilling",
|
|
||||||
"USD": "US Dollar",
|
|
||||||
"UYU": "Uruguayan Peso",
|
|
||||||
"UZS": "Uzbekistan Som",
|
|
||||||
"VEF": "Venezuelan Bolívar",
|
|
||||||
"VES": "Venezuelan Bolívar Soberano",
|
|
||||||
"VND": "Vietnamese Đồng",
|
|
||||||
"VUV": "Vanuatu Vatu",
|
|
||||||
"WST": "Samoan Tala",
|
|
||||||
"XAF": "Central African Cfa Franc",
|
|
||||||
"XAG": "Silver (Troy Ounce)",
|
|
||||||
"XAU": "Gold (Troy Ounce)",
|
|
||||||
"XCD": "East Caribbean Dollar",
|
|
||||||
"XDR": "Special Drawing Rights",
|
|
||||||
"XOF": "West African Cfa Franc",
|
|
||||||
"XPD": "Palladium",
|
|
||||||
"XPF": "Cfp Franc",
|
|
||||||
"XPT": "Platinum",
|
|
||||||
"YER": "Yemeni Rial",
|
|
||||||
"ZAR": "South African Rand",
|
|
||||||
"ZMW": "Zambian Kwacha",
|
|
||||||
"ZWL": "Zimbabwean Dollar"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from fastapi.param_functions import Query
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class MarketSettings(BaseModel):
|
|
||||||
user: str
|
|
||||||
currency: str
|
|
||||||
fiat_base_multiplier: int
|
|
||||||
|
|
||||||
|
|
||||||
class SetSettings(BaseModel):
|
|
||||||
currency: str
|
|
||||||
fiat_base_multiplier: int = Query(100, ge=1)
|
|
||||||
|
|
||||||
|
|
||||||
class Stalls(BaseModel):
|
|
||||||
id: str
|
|
||||||
wallet: str
|
|
||||||
name: str
|
|
||||||
currency: str
|
|
||||||
publickey: Optional[str]
|
|
||||||
relays: Optional[str]
|
|
||||||
shippingzones: str
|
|
||||||
|
|
||||||
|
|
||||||
class createStalls(BaseModel):
|
|
||||||
wallet: str = Query(...)
|
|
||||||
name: str = Query(...)
|
|
||||||
currency: str = Query("sat")
|
|
||||||
publickey: str = Query(None)
|
|
||||||
relays: str = Query(None)
|
|
||||||
shippingzones: str = Query(...)
|
|
||||||
|
|
||||||
|
|
||||||
class createProduct(BaseModel):
|
|
||||||
stall: str = Query(...)
|
|
||||||
product: str = Query(...)
|
|
||||||
categories: str = Query(None)
|
|
||||||
description: str = Query(None)
|
|
||||||
image: str = Query(None)
|
|
||||||
price: float = Query(0, ge=0)
|
|
||||||
quantity: int = Query(0, ge=0)
|
|
||||||
|
|
||||||
|
|
||||||
class Products(BaseModel):
|
|
||||||
id: str
|
|
||||||
stall: str
|
|
||||||
product: str
|
|
||||||
categories: Optional[str]
|
|
||||||
description: Optional[str]
|
|
||||||
image: Optional[str]
|
|
||||||
price: float
|
|
||||||
quantity: int
|
|
||||||
|
|
||||||
|
|
||||||
class createZones(BaseModel):
|
|
||||||
cost: float = Query(0, ge=0)
|
|
||||||
countries: str = Query(...)
|
|
||||||
|
|
||||||
|
|
||||||
class Zones(BaseModel):
|
|
||||||
id: str
|
|
||||||
user: str
|
|
||||||
cost: float
|
|
||||||
countries: str
|
|
||||||
|
|
||||||
|
|
||||||
class OrderDetail(BaseModel):
|
|
||||||
id: str
|
|
||||||
order_id: str
|
|
||||||
product_id: str
|
|
||||||
quantity: int
|
|
||||||
|
|
||||||
|
|
||||||
class createOrderDetails(BaseModel):
|
|
||||||
product_id: str = Query(...)
|
|
||||||
quantity: int = Query(..., ge=1)
|
|
||||||
|
|
||||||
|
|
||||||
class createOrder(BaseModel):
|
|
||||||
wallet: str = Query(...)
|
|
||||||
username: str = Query(None)
|
|
||||||
pubkey: str = Query(None)
|
|
||||||
shippingzone: str = Query(...)
|
|
||||||
address: str = Query(...)
|
|
||||||
email: str = Query(...)
|
|
||||||
total: int = Query(...)
|
|
||||||
products: List[createOrderDetails]
|
|
||||||
|
|
||||||
|
|
||||||
class Orders(BaseModel):
|
|
||||||
id: str
|
|
||||||
wallet: str
|
|
||||||
username: Optional[str]
|
|
||||||
pubkey: Optional[str]
|
|
||||||
shippingzone: str
|
|
||||||
address: str
|
|
||||||
email: str
|
|
||||||
total: int
|
|
||||||
invoiceid: str
|
|
||||||
paid: bool
|
|
||||||
shipped: bool
|
|
||||||
time: int
|
|
||||||
|
|
||||||
|
|
||||||
class CreateMarket(BaseModel):
|
|
||||||
usr: str = Query(...)
|
|
||||||
name: str = Query(None)
|
|
||||||
stalls: List[str] = Query(...)
|
|
||||||
|
|
||||||
|
|
||||||
class Market(BaseModel):
|
|
||||||
id: str
|
|
||||||
usr: str
|
|
||||||
name: Optional[str]
|
|
||||||
|
|
||||||
|
|
||||||
class CreateMarketStalls(BaseModel):
|
|
||||||
stallid: str
|
|
||||||
|
|
||||||
|
|
||||||
class ChatMessage(BaseModel):
|
|
||||||
id: str
|
|
||||||
msg: str
|
|
||||||
pubkey: str
|
|
||||||
id_conversation: str
|
|
||||||
timestamp: int
|
|
||||||
|
|
||||||
|
|
||||||
class CreateChatMessage(BaseModel):
|
|
||||||
msg: str = Query(..., min_length=1)
|
|
||||||
pubkey: str = Query(...)
|
|
||||||
room_name: str = Query(...)
|
|
|
@ -1,91 +0,0 @@
|
||||||
## adapted from https://github.com/Sentymental/chat-fastapi-websocket
|
|
||||||
"""
|
|
||||||
Create a class Notifier that will handle messages
|
|
||||||
and delivery to the specific person
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from collections import defaultdict
|
|
||||||
from typing import AsyncGenerator
|
|
||||||
|
|
||||||
from fastapi import WebSocket
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from .crud import create_chat_message
|
|
||||||
from .models import CreateChatMessage
|
|
||||||
|
|
||||||
|
|
||||||
class Notifier:
|
|
||||||
"""
|
|
||||||
Manages chatrooms, sessions and members.
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
- get_notification_generator(self): async generator with notification messages
|
|
||||||
- get_members(self, room_name: str): get members in room
|
|
||||||
- push(message: str, room_name: str): push message
|
|
||||||
- connect(websocket: WebSocket, room_name: str): connect to room
|
|
||||||
- remove(websocket: WebSocket, room_name: str): remove
|
|
||||||
- _notify(message: str, room_name: str): notifier
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# Create sessions as a dict:
|
|
||||||
self.sessions: dict = defaultdict(dict)
|
|
||||||
|
|
||||||
# Create notification generator:
|
|
||||||
self.generator = self.get_notification_generator()
|
|
||||||
|
|
||||||
async def get_notification_generator(self) -> AsyncGenerator:
|
|
||||||
"""Notification Generator"""
|
|
||||||
|
|
||||||
while True:
|
|
||||||
message = yield
|
|
||||||
msg = message["message"]
|
|
||||||
room_name = message["room_name"]
|
|
||||||
await self._notify(msg, room_name)
|
|
||||||
|
|
||||||
def get_members(self, room_name: str):
|
|
||||||
"""Get all members in a room"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.info(f"Looking for members in room: {room_name}")
|
|
||||||
return self.sessions[room_name]
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception(f"There is no member in room: {room_name}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def push(self, message: str, room_name: str):
|
|
||||||
"""Push a message"""
|
|
||||||
message_body = {"message": message, "room_name": room_name}
|
|
||||||
await self.generator.asend(message_body)
|
|
||||||
|
|
||||||
async def connect(self, websocket: WebSocket, room_name: str):
|
|
||||||
"""Connect to room"""
|
|
||||||
|
|
||||||
await websocket.accept()
|
|
||||||
if self.sessions[room_name] == {} or len(self.sessions[room_name]) == 0:
|
|
||||||
self.sessions[room_name] = []
|
|
||||||
|
|
||||||
self.sessions[room_name].append(websocket)
|
|
||||||
print(f"Connections ...: {self.sessions[room_name]}")
|
|
||||||
|
|
||||||
def remove(self, websocket: WebSocket, room_name: str):
|
|
||||||
"""Remove websocket from room"""
|
|
||||||
|
|
||||||
self.sessions[room_name].remove(websocket)
|
|
||||||
print(f"Connection removed...\nOpen connections...: {self.sessions[room_name]}")
|
|
||||||
|
|
||||||
async def _notify(self, message: str, room_name: str):
|
|
||||||
"""Notifier"""
|
|
||||||
d = json.loads(message)
|
|
||||||
d["room_name"] = room_name
|
|
||||||
db_msg = CreateChatMessage.parse_obj(d)
|
|
||||||
await create_chat_message(data=db_msg)
|
|
||||||
|
|
||||||
remaining_sessions = []
|
|
||||||
while len(self.sessions[room_name]) > 0:
|
|
||||||
websocket = self.sessions[room_name].pop()
|
|
||||||
await websocket.send_text(message)
|
|
||||||
remaining_sessions.append(websocket)
|
|
||||||
self.sessions[room_name] = remaining_sessions
|
|
Binary file not shown.
Before Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.8 KiB |
|
@ -1,39 +0,0 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.core.models import Payment
|
|
||||||
from lnbits.tasks import register_invoice_listener
|
|
||||||
|
|
||||||
from .crud import (
|
|
||||||
get_market_order_details,
|
|
||||||
get_market_order_invoiceid,
|
|
||||||
set_market_order_paid,
|
|
||||||
update_market_product_stock,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices():
|
|
||||||
invoice_queue = asyncio.Queue()
|
|
||||||
register_invoice_listener(invoice_queue)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
payment = await invoice_queue.get()
|
|
||||||
await on_invoice_paid(payment)
|
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
|
||||||
if payment.extra.get("tag") != "market":
|
|
||||||
return
|
|
||||||
|
|
||||||
order = await get_market_order_invoiceid(payment.payment_hash)
|
|
||||||
if not order:
|
|
||||||
logger.error("this should never happen", payment)
|
|
||||||
return
|
|
||||||
|
|
||||||
# set order as paid
|
|
||||||
await set_market_order_paid(payment.payment_hash)
|
|
||||||
|
|
||||||
# deduct items sold from stock
|
|
||||||
details = await get_market_order_details(order.id)
|
|
||||||
await update_market_product_stock(details)
|
|
|
@ -1,128 +0,0 @@
|
||||||
<q-expansion-item
|
|
||||||
group="extras"
|
|
||||||
icon="swap_vertical_circle"
|
|
||||||
label="Setup guide"
|
|
||||||
:content-inset-level="0.5"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<h5 class="text-subtitle1 q-my-none">
|
|
||||||
LNbits Market (Nostr support coming soon)
|
|
||||||
</h5>
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<li>Create Shipping Zones you're willing to ship to</li>
|
|
||||||
<li>Create a Stall to list yiur products on</li>
|
|
||||||
<li>Create products to put on the Stall</li>
|
|
||||||
<li>Take orders</li>
|
|
||||||
<li>Includes chat support!</li>
|
|
||||||
</ol>
|
|
||||||
The first LNbits market idea 'Diagon Alley' helped create Nostr, and soon
|
|
||||||
this market extension will have the option to work on Nostr 'Diagon Alley'
|
|
||||||
mode, by the merchant, market, and buyer all having keys, and data being
|
|
||||||
routed through Nostr relays.
|
|
||||||
<br />
|
|
||||||
<small>
|
|
||||||
Created by,
|
|
||||||
<a href="https://github.com/talvasconcelos">Tal Vasconcelos</a>,
|
|
||||||
<a href="https://github.com/benarc">Ben Arc</a></small
|
|
||||||
>
|
|
||||||
<!-- </p> -->
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-expansion-item
|
|
||||||
group="extras"
|
|
||||||
icon="swap_vertical_circle"
|
|
||||||
label="API info"
|
|
||||||
:content-inset-level="0.5"
|
|
||||||
>
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Get prodcuts, categorised by wallet"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-light-blue">GET</span>
|
|
||||||
/market/api/v1/stall/products/<relay_id></code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 201 CREATED (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>Product JSON list</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X GET {{ request.url_root
|
|
||||||
}}api/v1/stall/products/<relay_id></code
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Get invoice for product"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-light-green">POST</span>
|
|
||||||
/market/api/v1/stall/order/<relay_id></code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
|
||||||
<code
|
|
||||||
>{"id": <string>, "address": <string>, "shippingzone":
|
|
||||||
<integer>, "email": <string>, "quantity":
|
|
||||||
<integer>}</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 201 CREATED (application/json)
|
|
||||||
</h5>
|
|
||||||
<code
|
|
||||||
>{"checking_id": <string>,"payment_request":
|
|
||||||
<string>}</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X POST {{ request.url_root
|
|
||||||
}}api/v1/stall/order/<relay_id> -d '{"id": <product_id&>,
|
|
||||||
"email": <customer_email>, "address": <customer_address>,
|
|
||||||
"quantity": 2, "shippingzone": 1}' -H "Content-type: application/json"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Check a product has been shipped"
|
|
||||||
class="q-mb-md"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-light-blue">GET</span>
|
|
||||||
/market/api/v1/stall/checkshipped/<checking_id></code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
|
||||||
Returns 200 OK (application/json)
|
|
||||||
</h5>
|
|
||||||
<code>{"shipped": <boolean>}</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
|
||||||
<code
|
|
||||||
>curl -X GET {{ request.url_root
|
|
||||||
}}api/v1/stall/checkshipped/<checking_id> -H "Content-type:
|
|
||||||
application/json"</code
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-expansion-item>
|
|
|
@ -1,58 +0,0 @@
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<h6 class="text-subtitle1 q-my-none">Messages</h6>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<q-separator></q-separator>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-select
|
|
||||||
v-model="customerKey"
|
|
||||||
:options="Object.keys(messages).map(k => ({label: `${k.slice(0, 25)}...`, value: k}))"
|
|
||||||
label="Customers"
|
|
||||||
@input="chatRoom(customerKey)"
|
|
||||||
emit-value
|
|
||||||
></q-select>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="chat-container" ref="chatCard">
|
|
||||||
<div class="chat-box">
|
|
||||||
<!-- <p v-if="Object.keys(messages).length === 0">No messages yet</p> -->
|
|
||||||
<div class="chat-messages">
|
|
||||||
<q-chat-message
|
|
||||||
:key="index"
|
|
||||||
v-for="(message, index) in orderMessages"
|
|
||||||
:name="message.pubkey == keys.pubkey ? 'me' : 'customer'"
|
|
||||||
:text="[message.msg]"
|
|
||||||
:sent="message.pubkey == keys.pubkey ? true : false"
|
|
||||||
:bg-color="message.pubkey == keys.pubkey ? 'white' : 'light-green-2'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-card-section>
|
|
||||||
<q-form @submit="sendMessage" class="full-width chat-input">
|
|
||||||
<q-input
|
|
||||||
ref="newMessage"
|
|
||||||
v-model="newMessage"
|
|
||||||
placeholder="Message"
|
|
||||||
class="full-width"
|
|
||||||
dense
|
|
||||||
outlined
|
|
||||||
@click="checkWebSocket"
|
|
||||||
>
|
|
||||||
<template>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
type="submit"
|
|
||||||
icon="send"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</q-form>
|
|
||||||
</q-card-section>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
|
@ -1,405 +0,0 @@
|
||||||
<!-- PRODUCT DIALOG -->
|
|
||||||
<q-dialog v-model="productDialog.show" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
|
||||||
<q-form @submit="sendProductFormData" class="q-gutter-md">
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
v-model="productDialog.data.stall"
|
|
||||||
:options="stalls.map(s => ({label: s.name, value: s.id}))"
|
|
||||||
label="Stall"
|
|
||||||
>
|
|
||||||
</q-select>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="productDialog.data.product"
|
|
||||||
label="Product"
|
|
||||||
></q-input>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="productDialog.data.description"
|
|
||||||
label="Description"
|
|
||||||
></q-input>
|
|
||||||
<!-- <div class="row"> -->
|
|
||||||
<!-- <div class="col-5">
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
multiple
|
|
||||||
v-model.trim="productDialog.data.categories"
|
|
||||||
:options="categories"
|
|
||||||
label="Categories"
|
|
||||||
class="q-pr-sm"
|
|
||||||
></q-select>
|
|
||||||
</div> -->
|
|
||||||
<!-- <div class="col-7"> -->
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
multiple
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
v-model.trim="productDialog.data.categories"
|
|
||||||
use-input
|
|
||||||
use-chips
|
|
||||||
multiple
|
|
||||||
hide-dropdown-icon
|
|
||||||
input-debounce="0"
|
|
||||||
new-value-mode="add-unique"
|
|
||||||
label="Categories"
|
|
||||||
placeholder="crafts,robots,etc"
|
|
||||||
hint="Hit Enter to add"
|
|
||||||
></q-select>
|
|
||||||
<!-- </div> -->
|
|
||||||
<!-- </div> -->
|
|
||||||
<q-input
|
|
||||||
v-if="productDialog.url"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="productDialog.data.image"
|
|
||||||
type="url"
|
|
||||||
label="Image URL"
|
|
||||||
></q-input>
|
|
||||||
<q-file
|
|
||||||
v-else
|
|
||||||
class="q-pr-md"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
capture="environment"
|
|
||||||
accept="image/jpeg, image/png"
|
|
||||||
:max-file-size="3*1024**2"
|
|
||||||
label="Small image (optional)"
|
|
||||||
clearable
|
|
||||||
@input="imageAdded"
|
|
||||||
@clear="imageCleared"
|
|
||||||
>
|
|
||||||
<template v-if="productDialog.data.image" v-slot:before>
|
|
||||||
<img style="height: 1em" :src="productDialog.data.image" />
|
|
||||||
</template>
|
|
||||||
<template v-if="productDialog.data.image" v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
name="cancel"
|
|
||||||
@click.stop.prevent="imageCleared"
|
|
||||||
class="cursor-pointer"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</q-file>
|
|
||||||
<q-toggle
|
|
||||||
:label="`${productDialog.url ? 'Insert image URL' : 'Upload image file'}`"
|
|
||||||
v-model="productDialog.url"
|
|
||||||
></q-toggle>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.number="productDialog.data.price"
|
|
||||||
type="number"
|
|
||||||
:label="'Price (' + currencies.unit + ') *'"
|
|
||||||
:mask="currencies.unit != 'sat' ? '#.##' : '#'"
|
|
||||||
fill-mask="0"
|
|
||||||
reverse-fill-mask
|
|
||||||
:step="currencies.unit != 'sat' ? '0.01' : '1'"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.number="productDialog.data.quantity"
|
|
||||||
type="number"
|
|
||||||
label="Quantity"
|
|
||||||
></q-input>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
v-if="productDialog.data.id"
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
type="submit"
|
|
||||||
>Update Product</q-btn
|
|
||||||
>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
v-else
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
:disable="productDialog.data.price == null
|
|
||||||
|| productDialog.data.product == null
|
|
||||||
|| productDialog.data.description == null
|
|
||||||
|| productDialog.data.quantity == null"
|
|
||||||
type="submit"
|
|
||||||
>Create Product</q-btn
|
|
||||||
>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
@click="resetDialog('productDialog')"
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<!-- ZONE DIALOG -->
|
|
||||||
<q-dialog v-model="zoneDialog.show" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
|
||||||
<q-form @submit="sendZoneFormData" class="q-gutter-md">
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
multiple
|
|
||||||
:options="shippingZoneOptions"
|
|
||||||
label="Countries"
|
|
||||||
v-model.trim="zoneDialog.data.countries"
|
|
||||||
></q-select>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
:label="'Amount (' + currencies.unit + ') *'"
|
|
||||||
:mask="currencies.unit != 'sat' ? '#.##' : '#'"
|
|
||||||
fill-mask="0"
|
|
||||||
reverse-fill-mask
|
|
||||||
:step="currencies.unit != 'sat' ? '0.01' : '1'"
|
|
||||||
type="number"
|
|
||||||
v-model.trim="zoneDialog.data.cost"
|
|
||||||
></q-input>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
v-if="zoneDialog.data.id"
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
type="submit"
|
|
||||||
>Update Shipping Zone</q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
v-else
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
:disable="zoneDialog.data.countries == null
|
|
||||||
|| zoneDialog.data.cost == null"
|
|
||||||
type="submit"
|
|
||||||
>Create Shipping Zone</q-btn
|
|
||||||
>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
@click="resetDialog('zoneDialog')"
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<!-- MARKETPLACE/market DIALOG -->
|
|
||||||
<q-dialog v-model="marketDialog.show" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
|
||||||
<q-form @submit="sendMarketplaceFormData" class="q-gutter-md">
|
|
||||||
<q-toggle
|
|
||||||
label="Activate marketplace"
|
|
||||||
color="primary"
|
|
||||||
v-model="marketDialog.data.activate"
|
|
||||||
></q-toggle>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
multiple
|
|
||||||
emit-value
|
|
||||||
:options="stalls.map(s => ({label: s.name, value: s.id}))"
|
|
||||||
label="Stalls"
|
|
||||||
v-model="marketDialog.data.stalls"
|
|
||||||
map-options
|
|
||||||
></q-select>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="marketDialog.data.name"
|
|
||||||
label="Name"
|
|
||||||
></q-input>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
v-if="marketDialog.data.id"
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
type="submit"
|
|
||||||
>Update Marketplace</q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
v-else
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
:disable="marketDialog.data.activate == null
|
|
||||||
|| marketDialog.data.stalls == null"
|
|
||||||
type="submit"
|
|
||||||
>Launch Marketplace</q-btn
|
|
||||||
>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
@click="resetDialog('marketDialog')"
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<!-- STALL/STORE DIALOG -->
|
|
||||||
<q-dialog v-model="stallDialog.show" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
|
||||||
<q-form @submit="sendStallFormData" class="q-gutter-md">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="stallDialog.data.name"
|
|
||||||
label="Name"
|
|
||||||
></q-input>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
v-model="stallDialog.data.wallet"
|
|
||||||
:options="g.user.walletOptions"
|
|
||||||
label="Wallet *"
|
|
||||||
>
|
|
||||||
</q-select>
|
|
||||||
<q-input
|
|
||||||
v-if="diagonAlley"
|
|
||||||
v-if="keys"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="stallDialog.data.publickey"
|
|
||||||
label="Public Key"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
v-if="diagonAlley"
|
|
||||||
v-if="keys"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="stallDialog.data.privatekey"
|
|
||||||
label="Private Key"
|
|
||||||
></q-input>
|
|
||||||
<!-- NOSTR -->
|
|
||||||
<div v-if="diagonAlley" class="row">
|
|
||||||
<div class="col-5">
|
|
||||||
<q-btn unelevated @click="generateKeys" color="primary"
|
|
||||||
>Generate keys</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="col-5">
|
|
||||||
<q-btn unelevated @click="restoreKeys" color="primary"
|
|
||||||
>Restore keys</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-select
|
|
||||||
:options="zoneOptions"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
multiple
|
|
||||||
v-model.trim="stallDialog.data.shippingzones"
|
|
||||||
label="Shipping Zones"
|
|
||||||
></q-select>
|
|
||||||
<q-select
|
|
||||||
v-if="diagonAlley"
|
|
||||||
:options="relayOptions"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
multiple
|
|
||||||
v-model.trim="stallDialog.data.relays"
|
|
||||||
label="Relays"
|
|
||||||
></q-select>
|
|
||||||
<q-input
|
|
||||||
v-if="diagonAlley"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="stallDialog.data.crelays"
|
|
||||||
label="Custom relays (seperate by comma)"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
v-if="diagonAlley"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="stallDialog.data.nostrMarkets"
|
|
||||||
label="Nostr market public keys (seperate by comma)"
|
|
||||||
></q-input>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
v-if="stallDialog.data.id"
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
type="submit"
|
|
||||||
>Update Stall</q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
v-else
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
:disable="stallDialog.data.wallet == null
|
|
||||||
|| stallDialog.data.shippingzones == null"
|
|
||||||
type="submit"
|
|
||||||
>Create Stall</q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
@click="resetDialog('stallDialog')"
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<!-- ONBOARDING DIALOG -->
|
|
||||||
<q-dialog v-model="onboarding.show">
|
|
||||||
<q-card class="q-pa-lg">
|
|
||||||
<h6 class="q-my-md text-primary">How to use Market</h6>
|
|
||||||
<q-stepper v-model="step" color="primary" vertical animated>
|
|
||||||
<q-step
|
|
||||||
:name="1"
|
|
||||||
title="Create a Shipping Zone"
|
|
||||||
icon="settings"
|
|
||||||
:done="step > 1"
|
|
||||||
>
|
|
||||||
Create Shipping Zones you're willing to ship to. You can define
|
|
||||||
different values for different zones.
|
|
||||||
<q-stepper-navigation>
|
|
||||||
<q-btn @click="step = step + 1" color="primary" label="Next" />
|
|
||||||
</q-stepper-navigation>
|
|
||||||
</q-step>
|
|
||||||
<q-step
|
|
||||||
:name="2"
|
|
||||||
title="Create a Stall"
|
|
||||||
icon="create_new_folder"
|
|
||||||
:done="step > 2"
|
|
||||||
>
|
|
||||||
Create a Stall and provide private and public keys to use for
|
|
||||||
communication. If you don't have one, LNbits will create a key pair for
|
|
||||||
you. It will be saved and can be used on other stalls.
|
|
||||||
<q-stepper-navigation>
|
|
||||||
<q-btn @click="step = step + 1" color="primary" label="Next" />
|
|
||||||
</q-stepper-navigation>
|
|
||||||
</q-step>
|
|
||||||
|
|
||||||
<q-step :name="3" title="Create Products" icon="assignment">
|
|
||||||
Create your products, add a small description and an image. Choose to
|
|
||||||
what stall, if you have more than one, it belongs to
|
|
||||||
<q-stepper-navigation>
|
|
||||||
<q-btn @click="onboarding.finish" color="primary" label="Finish" />
|
|
||||||
</q-stepper-navigation>
|
|
||||||
<div>
|
|
||||||
<q-checkbox v-model="onboarding.showAgain" label="Show this again?" />
|
|
||||||
</div>
|
|
||||||
</q-step>
|
|
||||||
</q-stepper>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
|
@ -1,443 +0,0 @@
|
||||||
<q-card>
|
|
||||||
<!-- ORDERS TABLE -->
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col">
|
|
||||||
<h5 class="text-subtitle1 q-my-none">Orders</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<q-btn flat color="grey" @click="exportOrdersCSV">Export to CSV</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-table
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:data="orders"
|
|
||||||
row-key="id"
|
|
||||||
:columns="ordersTable.columns"
|
|
||||||
:pagination.sync="ordersTable.pagination"
|
|
||||||
>
|
|
||||||
{% raw %}
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
{{ col.label }}
|
|
||||||
</q-th>
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
size="sm"
|
|
||||||
color="accent"
|
|
||||||
round
|
|
||||||
dense
|
|
||||||
@click="props.expand = !props.expand"
|
|
||||||
:icon="props.expand ? 'remove' : 'add'"
|
|
||||||
/>
|
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
size="sm"
|
|
||||||
color="green"
|
|
||||||
dense
|
|
||||||
icon="chat"
|
|
||||||
@click="chatRoom(props.row.invoiceid)"
|
|
||||||
>
|
|
||||||
<q-badge
|
|
||||||
v-if="props.row.unread"
|
|
||||||
color="red"
|
|
||||||
rounded
|
|
||||||
floating
|
|
||||||
style="padding: 6px; border-radius: 6px"
|
|
||||||
/>
|
|
||||||
</q-btn>
|
|
||||||
</q-td>
|
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
{{ col.value }}
|
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="shipOrder(props.row.id)"
|
|
||||||
icon="add_marketping_cart"
|
|
||||||
color="green"
|
|
||||||
>
|
|
||||||
<q-tooltip> Product shipped? </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="deleteOrder(props.row.id)"
|
|
||||||
icon="cancel"
|
|
||||||
color="pink"
|
|
||||||
></q-btn>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
<q-tr v-show="props.expand" :props="props">
|
|
||||||
<q-td colspan="100%">
|
|
||||||
<template>
|
|
||||||
<div class="q-pa-md">
|
|
||||||
<q-list>
|
|
||||||
<q-item-label header>Order Details</q-item-label>
|
|
||||||
|
|
||||||
<q-item v-for="col in props.row.details" :key="col.id">
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Products</q-item-label>
|
|
||||||
<q-item-label caption
|
|
||||||
>{{ products.length && (_.findWhere(products, {id:
|
|
||||||
col.product_id})).product }}</q-item-label
|
|
||||||
>
|
|
||||||
<q-item-label caption
|
|
||||||
>Quantity: {{ col.quantity }}</q-item-label
|
|
||||||
>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Shipping to</q-item-label>
|
|
||||||
<q-item-label caption
|
|
||||||
>{{ props.row.address }}</q-item-label
|
|
||||||
>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>User info</q-item-label>
|
|
||||||
<q-item-label caption v-if="props.row.username"
|
|
||||||
>{{ props.row.username }}</q-item-label
|
|
||||||
>
|
|
||||||
<q-item-label caption>{{ props.row.email }}</q-item-label>
|
|
||||||
<q-item-label caption v-if="props.row.pubkey"
|
|
||||||
>{{ props.row.pubkey }}</q-item-label
|
|
||||||
>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Total</q-item-label>
|
|
||||||
<q-item-label>{{ props.row.total }}</q-item-label>
|
|
||||||
<!-- <q-icon name="star" color="yellow" /> -->
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
{% endraw %}
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<q-card>
|
|
||||||
<!-- PRODUCTS TABLE -->
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col">
|
|
||||||
<h5 class="text-subtitle1 q-my-none">
|
|
||||||
Products
|
|
||||||
<span v-if="stalls.length > 0" class="q-px-sm">
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
color="primary"
|
|
||||||
icon="add"
|
|
||||||
size="sm"
|
|
||||||
@click="productDialog.show = true"
|
|
||||||
/>
|
|
||||||
<q-tooltip> Add a product </q-tooltip>
|
|
||||||
</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<q-btn flat color="grey" @click="exportProductsCSV"
|
|
||||||
>Export to CSV</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-table
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:data="products"
|
|
||||||
row-key="id"
|
|
||||||
:columns="productsTable.columns"
|
|
||||||
:pagination.sync="productsTable.pagination"
|
|
||||||
>
|
|
||||||
{% raw %}
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
{{ col.label }}
|
|
||||||
</q-th>
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
disabled
|
|
||||||
unelevated
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
icon="add_marketping_cart"
|
|
||||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
|
||||||
type="a"
|
|
||||||
:href="props.row.wallet"
|
|
||||||
target="_blank"
|
|
||||||
></q-btn>
|
|
||||||
<q-tooltip
|
|
||||||
>Disabled: link to pass to stall relays when using
|
|
||||||
nostr</q-tooltip
|
|
||||||
>
|
|
||||||
</q-td>
|
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
{{ col.value }}
|
|
||||||
</q-td>
|
|
||||||
<q-td class="text-center" auto-width>
|
|
||||||
<img
|
|
||||||
v-if="props.row.image"
|
|
||||||
:src="props.row.image"
|
|
||||||
style="height: 1.5em"
|
|
||||||
/>
|
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="openProductUpdateDialog(props.row.id)"
|
|
||||||
icon="edit"
|
|
||||||
color="light-blue"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="deleteProduct(props.row.id)"
|
|
||||||
icon="cancel"
|
|
||||||
color="pink"
|
|
||||||
></q-btn>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
{% endraw %}
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<q-card>
|
|
||||||
<!-- STALLS TABLE -->
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col">
|
|
||||||
<h5 class="text-subtitle1 q-my-none">Market Stalls</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<q-btn flat color="grey" @click="exportStallsCSV">Export to CSV</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-table
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:data="stalls"
|
|
||||||
row-key="id"
|
|
||||||
:columns="stallTable.columns"
|
|
||||||
:pagination.sync="stallTable.pagination"
|
|
||||||
>
|
|
||||||
{% raw %}
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
{{ col.label }}
|
|
||||||
</q-th>
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
icon="storefront"
|
|
||||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
|
||||||
type="a"
|
|
||||||
:href="'/market/stalls/' + props.row.id"
|
|
||||||
target="_blank"
|
|
||||||
></q-btn>
|
|
||||||
<q-tooltip> Stall simple UI marketping cart </q-tooltip>
|
|
||||||
</q-td>
|
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
{{ col.value }}
|
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="openStallUpdateDialog(props.row.id)"
|
|
||||||
icon="edit"
|
|
||||||
color="light-blue"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="deleteStall(props.row.id)"
|
|
||||||
icon="cancel"
|
|
||||||
color="pink"
|
|
||||||
></q-btn>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
{% endraw %}
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<q-card v-if="markets.length">
|
|
||||||
<!-- MARKETPLACES TABLE -->
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col">
|
|
||||||
<h5 class="text-subtitle1 q-my-none">Marketplaces</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<q-btn flat color="grey" @click="exportStallsCSV">Export to CSV</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-table
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:data="markets"
|
|
||||||
row-key="id"
|
|
||||||
:columns="marketTable.columns"
|
|
||||||
:pagination.sync="marketTable.pagination"
|
|
||||||
>
|
|
||||||
{% raw %}
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
{{ col.label }}
|
|
||||||
</q-th>
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
icon="storefront"
|
|
||||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
|
||||||
type="a"
|
|
||||||
:href="'/market/market/' + props.row.id"
|
|
||||||
target="_blank"
|
|
||||||
></q-btn>
|
|
||||||
<q-tooltip> Link to pass to stall relay </q-tooltip>
|
|
||||||
</q-td>
|
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
{{ col.name == 'stalls' ? stallName(col.value) : col.value }}
|
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="openMarketUpdateDialog(props.row.id)"
|
|
||||||
icon="edit"
|
|
||||||
color="light-blue"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="deleteMarket(props.row.id)"
|
|
||||||
icon="cancel"
|
|
||||||
color="pink"
|
|
||||||
></q-btn>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
{% endraw %}
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<q-card>
|
|
||||||
<!-- ZONES TABLE -->
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col">
|
|
||||||
<h5 class="text-subtitle1 q-my-none">Shipping Zones</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<q-btn flat color="grey" @click="exportZonesCSV">Export to CSV</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-table
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:data="zones"
|
|
||||||
row-key="id"
|
|
||||||
:columns="zonesTable.columns"
|
|
||||||
:pagination.sync="zonesTable.pagination"
|
|
||||||
>
|
|
||||||
{% raw %}
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
{{ col.label }}
|
|
||||||
</q-th>
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
{{ col.value }}
|
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="openZoneUpdateDialog(props.row.id)"
|
|
||||||
icon="edit"
|
|
||||||
color="light-blue"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="deleteZone(props.row.id)"
|
|
||||||
icon="cancel"
|
|
||||||
color="pink"
|
|
||||||
></q-btn>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
{% endraw %}
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,175 +0,0 @@
|
||||||
{% extends "public.html" %} {% block page %}
|
|
||||||
<div class="row q-mb-md">
|
|
||||||
<div class="col-12 q-gutter-y-md">
|
|
||||||
<q-toolbar class="row">
|
|
||||||
<div class="col">
|
|
||||||
<q-toolbar-title> Market: {{ market.name }} </q-toolbar-title>
|
|
||||||
</div>
|
|
||||||
<div class="col q-mx-md">
|
|
||||||
<q-input
|
|
||||||
class="float-left full-width q-ml-md"
|
|
||||||
standout
|
|
||||||
square
|
|
||||||
dense
|
|
||||||
outlined
|
|
||||||
clearable
|
|
||||||
v-model.trim="searchText"
|
|
||||||
label="Search for products"
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon v-if="!searchText" name="search" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</q-toolbar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div
|
|
||||||
class="col-xs-12 col-sm-6 col-md-4 col-lg-3"
|
|
||||||
v-for="item in filterProducts"
|
|
||||||
:key="item.id"
|
|
||||||
>
|
|
||||||
<q-card class="card--product">
|
|
||||||
{% raw %}
|
|
||||||
<q-img
|
|
||||||
:src="item.image ? item.image : '/market/static/images/placeholder.png'"
|
|
||||||
alt="Product Image"
|
|
||||||
loading="lazy"
|
|
||||||
spinner-color="white"
|
|
||||||
fit="contain"
|
|
||||||
height="300px"
|
|
||||||
></q-img>
|
|
||||||
|
|
||||||
<q-card-section class="q-pb-xs q-pt-md">
|
|
||||||
<div class="row no-wrap items-center">
|
|
||||||
<div class="col text-subtitle2 ellipsis-2-lines">
|
|
||||||
{{ item.product }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- <q-rating v-model="stars" color="orange" :max="5" readonly size="17px"></q-rating> -->
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section class="q-py-sm">
|
|
||||||
<div>
|
|
||||||
<div class="text-caption text-weight-bolder">
|
|
||||||
{{ item.stallName }}
|
|
||||||
</div>
|
|
||||||
<span v-if="item.currency == 'sat'">
|
|
||||||
<span class="text-h6">{{ item.price }} sats</span
|
|
||||||
><span class="q-ml-sm text-grey-6"
|
|
||||||
>BTC {{ (item.price / 1e8).toFixed(8) }}</span
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
<span class="text-h6"
|
|
||||||
>{{ getAmountFormated(item.price, item.currency) }}</span
|
|
||||||
>
|
|
||||||
<span v-if="exchangeRates" class="q-ml-sm text-grey-6"
|
|
||||||
>({{ getValueInSats(item.price, item.currency) }} sats)</span
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="q-ml-md text-caption text-green-8 text-weight-bolder q-mt-md"
|
|
||||||
>{{item.quantity}} left</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div v-if="item.categories" class="text-subtitle1">
|
|
||||||
<q-chip v-for="(cat, i) in item.categories.split(',')" :key="i" dense
|
|
||||||
>{{cat}}</q-chip
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="text-caption text-grey ellipsis-2-lines"
|
|
||||||
style="min-height: 40px"
|
|
||||||
>
|
|
||||||
<p v-if="item.description">{{ item.description }}</p>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-separator></q-separator>
|
|
||||||
|
|
||||||
<q-card-actions>
|
|
||||||
<span>Stall: {{ item.stallName }}</span>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
class="text-weight-bold text-capitalize q-ml-auto"
|
|
||||||
dense
|
|
||||||
color="primary"
|
|
||||||
type="a"
|
|
||||||
:href="'/market/stalls/' + item.stall"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Visit Stall
|
|
||||||
</q-btn>
|
|
||||||
</q-card-actions>
|
|
||||||
{% endraw %}
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
<script>
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
stalls: null,
|
|
||||||
products: [],
|
|
||||||
searchText: null,
|
|
||||||
exchangeRates: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
filterProducts() {
|
|
||||||
if (!this.searchText || this.searchText.length < 2) return this.products
|
|
||||||
return this.products.filter(p => {
|
|
||||||
return (
|
|
||||||
p.product.includes(this.searchText) ||
|
|
||||||
p.description.includes(this.searchText) ||
|
|
||||||
p.categories.includes(this.searchText)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async getRates() {
|
|
||||||
let noFiat = this.stalls.map(s => s.currency).every(c => c == 'sat')
|
|
||||||
if (noFiat) return
|
|
||||||
try {
|
|
||||||
let rates = await axios.get('https://api.opennode.co/v1/rates')
|
|
||||||
this.exchangeRates = rates.data.data
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getValueInSats(amount, unit = 'USD') {
|
|
||||||
if (!this.exchangeRates) return 0
|
|
||||||
return Math.ceil(
|
|
||||||
(amount / this.exchangeRates[`BTC${unit}`][unit]) * 1e8
|
|
||||||
)
|
|
||||||
},
|
|
||||||
getAmountFormated(amount, unit = 'USD') {
|
|
||||||
return LNbits.utils.formatCurrency(amount, unit)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async created() {
|
|
||||||
this.stalls = JSON.parse('{{ stalls | tojson }}')
|
|
||||||
let products = JSON.parse('{{ products | tojson }}')
|
|
||||||
|
|
||||||
this.products = products.map(obj => {
|
|
||||||
let stall = this.stalls.find(s => s.id == obj.stall)
|
|
||||||
obj.currency = stall.currency
|
|
||||||
if (obj.currency != 'sat') {
|
|
||||||
obj.price = parseFloat((obj.price / 100).toFixed(2))
|
|
||||||
}
|
|
||||||
obj.stallName = stall.name
|
|
||||||
return obj
|
|
||||||
})
|
|
||||||
await this.getRates()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
|
@ -1,564 +0,0 @@
|
||||||
{% extends "public.html" %} {% block page %}
|
|
||||||
<div class="row q-col-gutter-md flex">
|
|
||||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<div class="chat-container q-pa-md">
|
|
||||||
<div class="chat-box">
|
|
||||||
<!-- <p v-if="Object.keys(messages).length === 0">No messages yet</p> -->
|
|
||||||
<div class="chat-messages">
|
|
||||||
<q-chat-message
|
|
||||||
:key="index"
|
|
||||||
v-for="(message, index) in messages"
|
|
||||||
:name="message.pubkey == user.keys.publickey ? 'me' : 'merchant'"
|
|
||||||
:text="[message.msg]"
|
|
||||||
:sent="message.pubkey == user.keys.publickey ? true : false"
|
|
||||||
:bg-color="message.pubkey == user.keys.publickey ? 'white' : 'light-green-2'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-form @submit="sendMessage" class="full-width chat-input">
|
|
||||||
<q-input
|
|
||||||
ref="newMessage"
|
|
||||||
v-model="newMessage"
|
|
||||||
placeholder="Message"
|
|
||||||
class="full-width"
|
|
||||||
dense
|
|
||||||
outlined
|
|
||||||
@click="checkWebSocket"
|
|
||||||
>
|
|
||||||
<template>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
type="submit"
|
|
||||||
icon="send"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</q-form>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-5 col-lg-6 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
{% raw %}
|
|
||||||
<h6 class="text-subtitle1 q-my-none">{{ stall.name }}</h6>
|
|
||||||
<p @click="copyText(stall.publickey)" style="width: max-content">
|
|
||||||
Public Key: {{ sliceKey(stall.publickey) }}
|
|
||||||
<q-tooltip>Click to copy</q-tooltip>
|
|
||||||
</p>
|
|
||||||
{% endraw %}
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section v-if="user">
|
|
||||||
<q-form @submit="" class="q-gutter-md">
|
|
||||||
<!-- <q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
v-model="model"
|
|
||||||
:options="mockMerch"
|
|
||||||
label="Merchant"
|
|
||||||
hint="Select a merchant you've opened an order to"
|
|
||||||
></q-select>
|
|
||||||
<br /> -->
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
v-model="selectedOrder"
|
|
||||||
:options="Object.keys(user.orders).map(o => ({label: `${o.slice(0, 25)}...`, value: o}))"
|
|
||||||
label="Order"
|
|
||||||
hint="Select an order from this merchant"
|
|
||||||
@input="val => { changeOrder() }"
|
|
||||||
emit-value
|
|
||||||
></q-select>
|
|
||||||
</q-form>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-list>
|
|
||||||
{% raw %}
|
|
||||||
<q-item clickable :key="p.id" v-for="p in products">
|
|
||||||
<q-item-section side>
|
|
||||||
<span>{{p.quantity}} x </span>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar color="primary">
|
|
||||||
<img size="sm" :src="p.image" />
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ p.name }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section side>
|
|
||||||
<span v-if="stall.currency != 'sat'"
|
|
||||||
>{{ getAmountFormated(p.price) }}</span
|
|
||||||
>
|
|
||||||
<span v-else> {{p.price}} sats</span>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
{% endraw %}
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-list>
|
|
||||||
<q-expansion-item group="extras" icon="vpn_key" label="Keys"
|
|
||||||
><p>
|
|
||||||
Bellow are the keys needed to contact the merchant. They are
|
|
||||||
stored in the browser!
|
|
||||||
</p>
|
|
||||||
<div v-if="user?.keys" class="row q-col-gutter-md">
|
|
||||||
<div
|
|
||||||
class="col-12 col-sm-6"
|
|
||||||
v-for="type in ['publickey', 'privatekey']"
|
|
||||||
v-bind:key="type"
|
|
||||||
>
|
|
||||||
<div class="text-center q-mb-lg">
|
|
||||||
{% raw %}
|
|
||||||
<q-responsive
|
|
||||||
:ratio="1"
|
|
||||||
class="q-mx-auto"
|
|
||||||
style="max-width: 250px"
|
|
||||||
>
|
|
||||||
<qrcode
|
|
||||||
:value="user.keys[type]"
|
|
||||||
:options="{width: 500}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
<q-tooltip>{{ user.keys[type] }}</q-tooltip>
|
|
||||||
</q-responsive>
|
|
||||||
<p>
|
|
||||||
{{ type == 'publickey' ? 'Public Key' : 'Private Key' }}
|
|
||||||
</p>
|
|
||||||
{% endraw %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn outline color="grey" @click="downloadKeys"
|
|
||||||
>Backup keys
|
|
||||||
<q-tooltip>Download your keys</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
class="q-mx-sm"
|
|
||||||
@click="keysDialog.show = true"
|
|
||||||
:disabled="this.user.keys"
|
|
||||||
>Restore keys
|
|
||||||
<q-tooltip>Restore keys</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
@click="deleteData"
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
>Delete data
|
|
||||||
<q-tooltip>Delete all data from browser</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-list>
|
|
||||||
<q-expansion-item icon="qr_code" label="Export page">
|
|
||||||
<p>Export, or send, this page to another device</p>
|
|
||||||
<div class="text-center q-mb-lg">
|
|
||||||
<q-responsive
|
|
||||||
:ratio="1"
|
|
||||||
class="q-my-xl q-mx-auto"
|
|
||||||
style="max-width: 250px"
|
|
||||||
@click="copyText(exportURL)"
|
|
||||||
>
|
|
||||||
<qrcode
|
|
||||||
:value="exportURL"
|
|
||||||
:options="{width: 500}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
<q-tooltip>Click to copy</q-tooltip>
|
|
||||||
</q-responsive>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
@click="copyText(exportURL)"
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
>Copy URL
|
|
||||||
<q-tooltip
|
|
||||||
>Export, or send, this page to another device</q-tooltip
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<!-- RESTORE KEYS DIALOG -->
|
|
||||||
<q-dialog
|
|
||||||
v-if="diagonalley"
|
|
||||||
v-model="keysDialog.show"
|
|
||||||
position="top"
|
|
||||||
@hide="clearRestoreKeyDialog"
|
|
||||||
>
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> </q-card>
|
|
||||||
<q-card class="q-pa-lg lnbits__dialog-card">
|
|
||||||
<q-form @submit="restoreKeys" class="q-gutter-md">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="keysDialog.data.publickey"
|
|
||||||
label="Public Key"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="keysDialog.data.privatekey"
|
|
||||||
label="Private Key *optional"
|
|
||||||
></q-input>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
:disable="keysDialog.data.publickey == null"
|
|
||||||
type="submit"
|
|
||||||
label="Submit"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
@click="clearRestoreKeyDialog"
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
label="Cancel"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<!-- ONBOARDING DIALOG -->
|
|
||||||
<q-dialog v-model="lnbitsBookmark.show">
|
|
||||||
<q-card class="q-pa-lg">
|
|
||||||
<h6 class="q-my-md text-primary">Bookmark this page</h6>
|
|
||||||
<p>
|
|
||||||
Don't forget to bookmark this page to be able to check on your order!
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
You can backup your keys, and export the page to another device also.
|
|
||||||
</p>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
@click="lnbitsBookmark.finish"
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
>Close</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const mapChatMsg = msg => {
|
|
||||||
let obj = {}
|
|
||||||
obj.timestamp = {
|
|
||||||
msg: msg,
|
|
||||||
pubkey: pubkey
|
|
||||||
}
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapProductsItems = obj => {
|
|
||||||
obj.price = (obj.price / 100).toFixed(2)
|
|
||||||
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
const nostr = window.NostrTools
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
lnbitsBookmark: {
|
|
||||||
show: true,
|
|
||||||
finish: () => {
|
|
||||||
this.$q.localStorage.set('lnbits.marketbookmark', false)
|
|
||||||
this.lnbitsBookmark.show = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
newMessage: '',
|
|
||||||
showMessages: false,
|
|
||||||
messages: {},
|
|
||||||
stall: null,
|
|
||||||
selectedOrder: null,
|
|
||||||
diagonalley: false,
|
|
||||||
products: [],
|
|
||||||
orders: [],
|
|
||||||
user: {
|
|
||||||
keys: {},
|
|
||||||
orders: {}
|
|
||||||
},
|
|
||||||
keysDialog: {
|
|
||||||
show: false,
|
|
||||||
data: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
exportURL() {
|
|
||||||
return (
|
|
||||||
'{{request.url}}' +
|
|
||||||
`&keys=${this.user.keys.publickey},${this.user.keys.privatekey}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getAmountFormated(amount) {
|
|
||||||
return LNbits.utils.formatCurrency(amount, this.stall.currency)
|
|
||||||
},
|
|
||||||
clearMessage() {
|
|
||||||
this.newMessage = ''
|
|
||||||
this.$refs.newMessage.focus()
|
|
||||||
},
|
|
||||||
clearRestoreKeyDialog() {
|
|
||||||
this.keysDialog = {
|
|
||||||
show: false,
|
|
||||||
data: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sendMessage() {
|
|
||||||
let message = {
|
|
||||||
msg: this.newMessage,
|
|
||||||
pubkey: this.user.keys.publickey
|
|
||||||
}
|
|
||||||
this.ws.send(JSON.stringify(message))
|
|
||||||
|
|
||||||
this.clearMessage()
|
|
||||||
},
|
|
||||||
sliceKey(key) {
|
|
||||||
if (!key) return ''
|
|
||||||
return `${key.slice(0, 4)}...${key.slice(-4)}`
|
|
||||||
},
|
|
||||||
downloadKeys() {
|
|
||||||
const file = new File(
|
|
||||||
[JSON.stringify(this.user.keys)],
|
|
||||||
'backup_keys.json',
|
|
||||||
{
|
|
||||||
type: 'text/json'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const link = document.createElement('a')
|
|
||||||
const url = URL.createObjectURL(file)
|
|
||||||
|
|
||||||
link.href = url
|
|
||||||
link.download = file.name
|
|
||||||
link.click()
|
|
||||||
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
},
|
|
||||||
restoreKeys() {
|
|
||||||
this.user.keys = this.keysDialog.data
|
|
||||||
let data = this.$q.localStorage.getItem(`lnbits.market.data`)
|
|
||||||
this.$q.localStorage.set(`lnbits.market.data`, {
|
|
||||||
...data,
|
|
||||||
keys: this.user.keys
|
|
||||||
})
|
|
||||||
|
|
||||||
this.clearRestoreKeyDialog()
|
|
||||||
},
|
|
||||||
deleteData() {
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog('Are you sure you want to delete your stored data?')
|
|
||||||
.onOk(() => {
|
|
||||||
this.$q.localStorage.remove('lnbits.market.data')
|
|
||||||
this.user = null
|
|
||||||
})
|
|
||||||
},
|
|
||||||
generateKeys() {
|
|
||||||
//check if the keys are set
|
|
||||||
if ('publickey' in this.user.keys && 'privatekey' in this.user.keys)
|
|
||||||
return
|
|
||||||
|
|
||||||
const privkey = nostr.generatePrivateKey()
|
|
||||||
const pubkey = nostr.getPublicKey(privkey)
|
|
||||||
|
|
||||||
this.user.keys = {
|
|
||||||
privatekey: privkey,
|
|
||||||
publickey: pubkey
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async getMessages(room_name, all = false) {
|
|
||||||
await LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
`/market/api/v1/chat/messages/${room_name}${
|
|
||||||
all ? '?all_messages=true' : ''
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
.then(response => {
|
|
||||||
if (response.data) {
|
|
||||||
response.data.reverse().map(m => {
|
|
||||||
this.$set(this.messages, m.timestamp * 1000, {
|
|
||||||
msg: m.msg,
|
|
||||||
pubkey: m.pubkey
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async changeOrder() {
|
|
||||||
this.products = this.user.orders[this.selectedOrder]
|
|
||||||
this.messages = {}
|
|
||||||
await this.getMessages(this.selectedOrder)
|
|
||||||
this.startChat(this.selectedOrder)
|
|
||||||
},
|
|
||||||
checkWebSocket() {
|
|
||||||
if (!this.ws) return
|
|
||||||
if (this.ws.readyState === WebSocket.CLOSED) {
|
|
||||||
console.log('WebSocket CLOSED: Reopening')
|
|
||||||
this.ws = new WebSocket(
|
|
||||||
ws_scheme + location.host + '/market/ws/' + this.selectedOrder
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
startChat(room_name) {
|
|
||||||
if (this.ws) {
|
|
||||||
this.ws.close()
|
|
||||||
}
|
|
||||||
if (location.protocol == 'https:') {
|
|
||||||
ws_scheme = 'wss://'
|
|
||||||
} else {
|
|
||||||
ws_scheme = 'ws://'
|
|
||||||
}
|
|
||||||
ws = new WebSocket(
|
|
||||||
ws_scheme + location.host + '/market/ws/' + room_name
|
|
||||||
)
|
|
||||||
|
|
||||||
ws.onmessage = event => {
|
|
||||||
let event_data = JSON.parse(event.data)
|
|
||||||
|
|
||||||
this.$set(this.messages, Date.now(), event_data)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ws = ws
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async created() {
|
|
||||||
let showBookmark = this.$q.localStorage.getItem('lnbits.marketbookmark')
|
|
||||||
this.lnbitsBookmark.show = showBookmark === true || showBookmark == null
|
|
||||||
|
|
||||||
let order_details = JSON.parse('{{ order | tojson }}')
|
|
||||||
let products = JSON.parse('{{ products | tojson }}')
|
|
||||||
let order_id = '{{ order_id }}'
|
|
||||||
let hasKeys = Boolean(
|
|
||||||
JSON.parse('{{ publickey | tojson }}') &&
|
|
||||||
JSON.parse('{{ privatekey | tojson }}')
|
|
||||||
)
|
|
||||||
|
|
||||||
if (hasKeys) {
|
|
||||||
this.user.keys = {
|
|
||||||
privatekey: '{{ privatekey }}',
|
|
||||||
publickey: '{{ publickey }}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stall = JSON.parse('{{ stall | tojson }}')
|
|
||||||
this.products = order_details.map(o => {
|
|
||||||
let product = products.find(p => p.id == o.product_id)
|
|
||||||
return {
|
|
||||||
quantity: o.quantity,
|
|
||||||
name: product.product,
|
|
||||||
image: product.image,
|
|
||||||
price: product.price
|
|
||||||
}
|
|
||||||
})
|
|
||||||
console.log(this.stall)
|
|
||||||
if (this.stall.currency != 'sat') {
|
|
||||||
this.products = this.products.map(mapProductsItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = this.$q.localStorage.getItem(`lnbits.market.data`) || false
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
this.user = data
|
|
||||||
if (!this.user.orders[`${order_id}`]) {
|
|
||||||
this.$set(this.user.orders, order_id, this.products)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// generate keys
|
|
||||||
this.generateKeys()
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/market/api/v1/order/pubkey/${order_id}/${this.user.keys.publickey}`
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
// populate user data
|
|
||||||
this.user.orders = {
|
|
||||||
[`${order_id}`]: this.products
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedOrder = order_id
|
|
||||||
|
|
||||||
await this.getMessages(order_id)
|
|
||||||
|
|
||||||
this.$q.localStorage.set(`lnbits.market.data`, this.user)
|
|
||||||
this.startChat(order_id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.q-field__native span {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-container {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 1fr auto;
|
|
||||||
/*height: calc(100vh - 200px);*/
|
|
||||||
height: 70vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-box {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
padding: 1rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin-left: auto;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-messages {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-other {
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: end;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
|
@ -1,14 +0,0 @@
|
||||||
{% extends "public.html" %} {% block page %}
|
|
||||||
<h1>Product page</h1>
|
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
<script>
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data: function () {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
|
@ -1,531 +0,0 @@
|
||||||
{% extends "public.html" %} {% block page %}
|
|
||||||
<div class="row q-mb-md">
|
|
||||||
<div class="col-12 q-gutter-y-md">
|
|
||||||
<q-toolbar class="row">
|
|
||||||
<div class="col">
|
|
||||||
<q-toolbar-title> Stall: {{ stall.name }} </q-toolbar-title>
|
|
||||||
</div>
|
|
||||||
<div class="col q-mx-md">
|
|
||||||
<q-input
|
|
||||||
class="float-left full-width q-ml-md"
|
|
||||||
standout
|
|
||||||
square
|
|
||||||
dense
|
|
||||||
outlined
|
|
||||||
clearable
|
|
||||||
v-model.trim="searchText"
|
|
||||||
label="Search for products"
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon v-if="!searchText" name="search" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
<q-btn dense round flat icon="shopping_cart">
|
|
||||||
{% raw %}
|
|
||||||
<q-badge v-if="cart.size" color="red" class="text-bold" floating>
|
|
||||||
{{ cart.size }}
|
|
||||||
</q-badge>
|
|
||||||
{% endraw %}
|
|
||||||
<q-menu v-if="cart.size">
|
|
||||||
<q-list style="min-width: 100px">
|
|
||||||
{% raw %}
|
|
||||||
<q-item :key="p.id" v-for="p in cartMenu">
|
|
||||||
<q-item-section side>
|
|
||||||
<span>{{p.quantity}} x </span>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar color="primary">
|
|
||||||
<img
|
|
||||||
size="sm"
|
|
||||||
:src="products.find(f => f.id == p.id).image"
|
|
||||||
/>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ p.name }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section side>
|
|
||||||
<span>
|
|
||||||
{{unit != 'sat' ? getAmountFormated(p.price) : p.price +
|
|
||||||
'sats'}}
|
|
||||||
<q-btn
|
|
||||||
class="q-ml-md"
|
|
||||||
round
|
|
||||||
color="red"
|
|
||||||
size="xs"
|
|
||||||
icon="close"
|
|
||||||
@click="removeFromCart(p)"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
{% endraw %}
|
|
||||||
<q-separator />
|
|
||||||
</q-list>
|
|
||||||
<div class="row q-pa-md q-gutter-md">
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
icon-right="checkout"
|
|
||||||
label="Checkout"
|
|
||||||
@click="checkoutDialog.show = true"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
class="q-ml-lg"
|
|
||||||
flat
|
|
||||||
color="primary"
|
|
||||||
label="Reset"
|
|
||||||
@click="resetCart"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</q-menu>
|
|
||||||
</q-btn>
|
|
||||||
</q-toolbar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div
|
|
||||||
class="col-xs-12 col-sm-6 col-md-4 col-lg-3"
|
|
||||||
v-for="item in filterProducts"
|
|
||||||
:key="item.id"
|
|
||||||
>
|
|
||||||
<q-card class="card--product">
|
|
||||||
{% raw %}
|
|
||||||
<q-img
|
|
||||||
:src="item.image ? item.image : '/market/static/images/placeholder.png'"
|
|
||||||
alt="Product Image"
|
|
||||||
loading="lazy"
|
|
||||||
spinner-color="white"
|
|
||||||
fit="contain"
|
|
||||||
height="300px"
|
|
||||||
></q-img>
|
|
||||||
|
|
||||||
<q-card-section class="q-pb-xs q-pt-md">
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
:disabled="item.quantity < 1"
|
|
||||||
color="primary"
|
|
||||||
icon="shopping_cart"
|
|
||||||
size="lg"
|
|
||||||
style="
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
"
|
|
||||||
@click="addToCart(item)"
|
|
||||||
><q-tooltip> Add to cart </q-tooltip></q-btn
|
|
||||||
>
|
|
||||||
|
|
||||||
<div class="row no-wrap items-center">
|
|
||||||
<div class="col text-subtitle2 ellipsis-2-lines">
|
|
||||||
{{ item.product }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- <q-rating v-model="stars" color="orange" :max="5" readonly size="17px"></q-rating> -->
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section class="q-py-sm">
|
|
||||||
<div>
|
|
||||||
<span v-if="unit == 'sat'">
|
|
||||||
<span class="text-h6">{{ item.price }} sats</span
|
|
||||||
><span class="q-ml-sm text-grey-6"
|
|
||||||
>BTC {{ (item.price / 1e8).toFixed(8) }}</span
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
<span class="text-h6">{{ getAmountFormated(item.price) }}</span>
|
|
||||||
<span v-if="exchangeRate" class="q-ml-sm text-grey-6"
|
|
||||||
>({{ getValueInSats(item.price) }} sats)</span
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="q-ml-md text-caption text-green-8 text-weight-bolder q-mt-md"
|
|
||||||
>{{item.quantity}} left</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div v-if="item.categories" class="text-subtitle1">
|
|
||||||
<q-chip v-for="(cat, i) in item.categories.split(',')" :key="i" dense
|
|
||||||
>{{cat}}</q-chip
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="text-caption text-grey ellipsis-2-lines"
|
|
||||||
style="min-height: 40px"
|
|
||||||
>
|
|
||||||
<p v-if="item.description">{{ item.description }}</p>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- <q-separator></q-separator>
|
|
||||||
|
|
||||||
<q-card-actions>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
class="text-weight-bold text-capitalize"
|
|
||||||
dense
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
View details
|
|
||||||
</q-btn>
|
|
||||||
</q-card-actions> -->
|
|
||||||
{% endraw %}
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<!-- CHECKOUT DIALOG -->
|
|
||||||
<q-dialog v-model="checkoutDialog.show" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<q-form @submit="placeOrder" class="q-gutter-md">
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="checkoutDialog.data.username"
|
|
||||||
label="Name *optional"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
v-if="diagonalley"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="checkoutDialog.data.pubkey"
|
|
||||||
label="Public key *optional"
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon @click="getPubkey" name="settings_backup_restore" />
|
|
||||||
<q-tooltip>Click to restore saved public key</q-tooltip>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="checkoutDialog.data.address"
|
|
||||||
label="Address"
|
|
||||||
></q-input>
|
|
||||||
<!-- <q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="checkoutDialog.data.address_2"
|
|
||||||
label="Address (line 2)"
|
|
||||||
></q-input> -->
|
|
||||||
<q-input
|
|
||||||
v-model="checkoutDialog.data.email"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
type="email"
|
|
||||||
label="Email"
|
|
||||||
></q-input>
|
|
||||||
<p>Select the shipping zone:</p>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-option-group
|
|
||||||
:options="stall.zones"
|
|
||||||
type="radio"
|
|
||||||
emit-value
|
|
||||||
v-model="checkoutDialog.data.shippingzone"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
{% raw %} Total: {{ unit != 'sat' ? getAmountFormated(finalCost) :
|
|
||||||
finalCost + 'sats' }}
|
|
||||||
<span v-if="unit != 'sat'" class="q-ml-sm text-grey-6"
|
|
||||||
>({{ getValueInSats(finalCost) }} sats)</span
|
|
||||||
>
|
|
||||||
{% endraw %}
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
:disable="checkoutDialog.data.address == null
|
|
||||||
|| checkoutDialog.data.email == null
|
|
||||||
|| checkoutDialog.data.shippingzone == null"
|
|
||||||
type="submit"
|
|
||||||
>Checkout</q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
@click="checkoutDialog = {show: false, data: {pubkey: ''}}"
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<!-- INVOICE DIALOG -->
|
|
||||||
<q-dialog
|
|
||||||
v-model="qrCodeDialog.show"
|
|
||||||
position="top"
|
|
||||||
@hide="closeQrCodeDialog"
|
|
||||||
>
|
|
||||||
<q-card
|
|
||||||
v-if="!qrCodeDialog.data.payment_request"
|
|
||||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
|
||||||
>
|
|
||||||
</q-card>
|
|
||||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
||||||
<div class="text-center q-mb-lg">
|
|
||||||
<a :href="'lightning:' + qrCodeDialog.data.payment_request">
|
|
||||||
<q-responsive :ratio="1" class="q-mx-xl">
|
|
||||||
<qrcode
|
|
||||||
:value="qrCodeDialog.data.payment_request"
|
|
||||||
:options="{width: 340}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
@click="copyText(qrCodeDialog.data.payment_request)"
|
|
||||||
>Copy invoice</q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="closeQrCodeDialog"
|
|
||||||
v-close-popup
|
|
||||||
flat
|
|
||||||
color="grey"
|
|
||||||
class="q-ml-auto"
|
|
||||||
>Close</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
<script>
|
|
||||||
const mapProductsItems = obj => {
|
|
||||||
obj.price = parseFloat((obj.price / 100).toFixed(2))
|
|
||||||
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
stall: null,
|
|
||||||
products: [],
|
|
||||||
searchText: null,
|
|
||||||
diagonalley: false,
|
|
||||||
unit: 'sat',
|
|
||||||
exchangeRate: 0,
|
|
||||||
cart: {
|
|
||||||
total: 0,
|
|
||||||
size: 0,
|
|
||||||
products: new Map()
|
|
||||||
},
|
|
||||||
cartMenu: [],
|
|
||||||
checkoutDialog: {
|
|
||||||
show: false,
|
|
||||||
data: {
|
|
||||||
pubkey: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
qrCodeDialog: {
|
|
||||||
data: {
|
|
||||||
payment_request: null
|
|
||||||
},
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
filterProducts() {
|
|
||||||
if (!this.searchText || this.searchText.length < 2) return this.products
|
|
||||||
return this.products.filter(p => {
|
|
||||||
return (
|
|
||||||
p.product.includes(this.searchText) ||
|
|
||||||
p.description.includes(this.searchText) ||
|
|
||||||
p.categories.includes(this.searchText)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
finalCost() {
|
|
||||||
if (!this.checkoutDialog.data.shippingzone) return this.cart.total
|
|
||||||
|
|
||||||
let zoneCost = this.stall.zones.find(
|
|
||||||
z => z.value == this.checkoutDialog.data.shippingzone
|
|
||||||
)
|
|
||||||
return +this.cart.total + zoneCost.cost
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
closeQrCodeDialog() {
|
|
||||||
this.qrCodeDialog.dismissMsg()
|
|
||||||
this.qrCodeDialog.show = false
|
|
||||||
},
|
|
||||||
resetCart() {
|
|
||||||
this.cart = {
|
|
||||||
total: 0,
|
|
||||||
size: 0,
|
|
||||||
products: new Map()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getAmountFormated(amount) {
|
|
||||||
return LNbits.utils.formatCurrency(amount, this.unit)
|
|
||||||
},
|
|
||||||
async getRates() {
|
|
||||||
if (this.unit == 'sat') return
|
|
||||||
try {
|
|
||||||
let rate = (
|
|
||||||
await LNbits.api.request('POST', '/api/v1/conversion', null, {
|
|
||||||
amount: 1e8,
|
|
||||||
to: this.unit
|
|
||||||
})
|
|
||||||
).data
|
|
||||||
this.exchangeRate = rate[this.unit]
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getValueInSats(amount) {
|
|
||||||
if (!this.exchangeRate) return 0
|
|
||||||
return Math.ceil((amount / this.exchangeRate) * 1e8)
|
|
||||||
},
|
|
||||||
addToCart(item) {
|
|
||||||
let prod = this.cart.products
|
|
||||||
if (prod.has(item.id)) {
|
|
||||||
let qty = prod.get(item.id).quantity
|
|
||||||
prod.set(item.id, {
|
|
||||||
...prod.get(item.id),
|
|
||||||
quantity: qty + 1
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
prod.set(item.id, {
|
|
||||||
name: item.product,
|
|
||||||
quantity: 1,
|
|
||||||
price: item.price
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: `${item.product} added to cart`,
|
|
||||||
icon: 'thumb_up'
|
|
||||||
})
|
|
||||||
this.cart.products = prod
|
|
||||||
this.updateCart(+item.price)
|
|
||||||
},
|
|
||||||
removeFromCart(item) {
|
|
||||||
this.cart.products.delete(item.id)
|
|
||||||
this.updateCart(+item.price, true)
|
|
||||||
},
|
|
||||||
updateCart(price, del = false) {
|
|
||||||
console.log(this.cart, this.cartMenu)
|
|
||||||
if (del) {
|
|
||||||
this.cart.total -= price
|
|
||||||
this.cart.size--
|
|
||||||
} else {
|
|
||||||
this.cart.total += price
|
|
||||||
this.cart.size++
|
|
||||||
}
|
|
||||||
this.cartMenu = Array.from(this.cart.products, item => {
|
|
||||||
return {id: item[0], ...item[1]}
|
|
||||||
})
|
|
||||||
console.log(this.cart, this.cartMenu)
|
|
||||||
},
|
|
||||||
getPubkey() {
|
|
||||||
let data = this.$q.localStorage.getItem(`lnbits.market.data`)
|
|
||||||
if (data && data.keys.publickey) {
|
|
||||||
this.checkoutDialog.data.pubkey = data.keys.publickey
|
|
||||||
} else {
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'warning',
|
|
||||||
message: 'No public key stored!',
|
|
||||||
icon: 'settings_backup_restore'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
placeOrder() {
|
|
||||||
let dialog = this.checkoutDialog.data
|
|
||||||
let data = {
|
|
||||||
...this.checkoutDialog.data,
|
|
||||||
wallet: this.stall.wallet,
|
|
||||||
total:
|
|
||||||
this.unit != 'sat'
|
|
||||||
? this.getValueInSats(this.finalCost)
|
|
||||||
: this.finalCost, // maybe this is better made in Python to allow API ordering?!
|
|
||||||
products: Array.from(this.cart.products, p => {
|
|
||||||
return {product_id: p[0], quantity: p[1].quantity}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
LNbits.api
|
|
||||||
.request('POST', '/market/api/v1/orders', null, data)
|
|
||||||
.then(res => {
|
|
||||||
this.checkoutDialog = {show: false, data: {}}
|
|
||||||
|
|
||||||
return res.data
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
this.qrCodeDialog.data = data
|
|
||||||
this.qrCodeDialog.show = true
|
|
||||||
|
|
||||||
this.qrCodeDialog.dismissMsg = this.$q.notify({
|
|
||||||
timeout: 0,
|
|
||||||
message: 'Waiting for payment...'
|
|
||||||
})
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
this.qrCodeDialog.paymentChecker = setInterval(() => {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
`/market/api/v1/orders/payments/${this.qrCodeDialog.data.payment_hash}`
|
|
||||||
)
|
|
||||||
.then(res => {
|
|
||||||
if (res.data.paid) {
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
multiLine: true,
|
|
||||||
message:
|
|
||||||
"Sats received, thanks! You'l be redirected to the order page...",
|
|
||||||
icon: 'thumb_up',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: 'See Order',
|
|
||||||
handler: () => {
|
|
||||||
window.location.href = `/market/order/?merch=${this.stall.id}&invoice_id=${this.qrCodeDialog.data.payment_hash}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
clearInterval(this.qrCodeDialog.paymentChecker)
|
|
||||||
this.resetCart()
|
|
||||||
this.closeQrCodeDialog()
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = `/market/order/?merch=${this.stall.id}&invoice_id=${this.qrCodeDialog.data.payment_hash}`
|
|
||||||
}, 5000)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
}, 3000)
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async created() {
|
|
||||||
this.stall = JSON.parse('{{ stall | tojson }}')
|
|
||||||
this.products = JSON.parse('{{ products | tojson }}')
|
|
||||||
this.unit = this.stall.currency
|
|
||||||
if (this.unit != 'sat') {
|
|
||||||
this.products = this.products.map(mapProductsItems)
|
|
||||||
}
|
|
||||||
await this.getRates()
|
|
||||||
setInterval(this.getRates, 300000)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
|
@ -1,155 +0,0 @@
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from fastapi import Depends, Query, Request, WebSocket, WebSocketDisconnect
|
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from loguru import logger
|
|
||||||
from starlette.exceptions import HTTPException
|
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
|
|
||||||
from lnbits.core.models import User
|
|
||||||
from lnbits.decorators import check_user_exists
|
|
||||||
|
|
||||||
from . import market_ext, market_renderer
|
|
||||||
from .crud import (
|
|
||||||
create_market_settings,
|
|
||||||
get_market_market,
|
|
||||||
get_market_market_stalls,
|
|
||||||
get_market_order_details,
|
|
||||||
get_market_order_invoiceid,
|
|
||||||
get_market_products,
|
|
||||||
get_market_settings,
|
|
||||||
get_market_stall,
|
|
||||||
get_market_zone,
|
|
||||||
)
|
|
||||||
from .models import SetSettings
|
|
||||||
from .notifier import Notifier
|
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/", response_class=HTMLResponse)
|
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
|
||||||
settings = await get_market_settings(user=user.id)
|
|
||||||
|
|
||||||
if not settings:
|
|
||||||
await create_market_settings(
|
|
||||||
user=user.id, data=SetSettings(currency="sat", fiat_base_multiplier=1)
|
|
||||||
)
|
|
||||||
settings = await get_market_settings(user.id)
|
|
||||||
assert settings
|
|
||||||
return market_renderer().TemplateResponse(
|
|
||||||
"market/index.html",
|
|
||||||
{"request": request, "user": user.dict(), "currency": settings.currency},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/stalls/{stall_id}", response_class=HTMLResponse)
|
|
||||||
async def stall(request: Request, stall_id):
|
|
||||||
stall = await get_market_stall(stall_id)
|
|
||||||
products = await get_market_products(stall_id)
|
|
||||||
|
|
||||||
if not stall:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Stall does not exist."
|
|
||||||
)
|
|
||||||
|
|
||||||
zones = []
|
|
||||||
for id in stall.shippingzones.split(","):
|
|
||||||
zone = await get_market_zone(id)
|
|
||||||
assert zone
|
|
||||||
z = zone.dict()
|
|
||||||
zones.append({"label": z["countries"], "cost": z["cost"], "value": z["id"]})
|
|
||||||
|
|
||||||
_stall = stall.dict()
|
|
||||||
|
|
||||||
_stall["zones"] = zones
|
|
||||||
|
|
||||||
return market_renderer().TemplateResponse(
|
|
||||||
"market/stall.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"stall": _stall,
|
|
||||||
"products": [product.dict() for product in products],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/market/{market_id}", response_class=HTMLResponse)
|
|
||||||
async def market(request: Request, market_id):
|
|
||||||
market = await get_market_market(market_id)
|
|
||||||
|
|
||||||
if not market:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Marketplace doesn't exist."
|
|
||||||
)
|
|
||||||
|
|
||||||
stalls = await get_market_market_stalls(market_id)
|
|
||||||
stalls_ids = [stall.id for stall in stalls]
|
|
||||||
products = [product.dict() for product in await get_market_products(stalls_ids)]
|
|
||||||
|
|
||||||
return market_renderer().TemplateResponse(
|
|
||||||
"market/market.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"market": market,
|
|
||||||
"stalls": [stall.dict() for stall in stalls],
|
|
||||||
"products": products,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/order", response_class=HTMLResponse)
|
|
||||||
async def order_chat(
|
|
||||||
request: Request,
|
|
||||||
merch: str = Query(...),
|
|
||||||
invoice_id: str = Query(...),
|
|
||||||
keys: str = Query(None),
|
|
||||||
):
|
|
||||||
stall = await get_market_stall(merch)
|
|
||||||
assert stall
|
|
||||||
order = await get_market_order_invoiceid(invoice_id)
|
|
||||||
assert order
|
|
||||||
_order = await get_market_order_details(order.id)
|
|
||||||
products = await get_market_products(stall.id)
|
|
||||||
assert products
|
|
||||||
|
|
||||||
return market_renderer().TemplateResponse(
|
|
||||||
"market/order.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"stall": {
|
|
||||||
"id": stall.id,
|
|
||||||
"name": stall.name,
|
|
||||||
"publickey": stall.publickey,
|
|
||||||
"wallet": stall.wallet,
|
|
||||||
"currency": stall.currency,
|
|
||||||
},
|
|
||||||
"publickey": keys.split(",")[0] if keys else None,
|
|
||||||
"privatekey": keys.split(",")[1] if keys else None,
|
|
||||||
"order_id": order.invoiceid,
|
|
||||||
"order": [details.dict() for details in _order],
|
|
||||||
"products": [product.dict() for product in products],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
##################WEBSOCKET ROUTES########################
|
|
||||||
|
|
||||||
# Initialize Notifier:
|
|
||||||
notifier = Notifier()
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.websocket("/ws/{room_name}")
|
|
||||||
async def websocket_endpoint(websocket: WebSocket, room_name: str):
|
|
||||||
await notifier.connect(websocket, room_name)
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
data = await websocket.receive_text()
|
|
||||||
room_members = notifier.get_members(room_name) or []
|
|
||||||
if websocket not in room_members:
|
|
||||||
logger.warning("Sender not in room member: Reconnecting...")
|
|
||||||
await notifier.connect(websocket, room_name)
|
|
||||||
await notifier._notify(data, room_name)
|
|
||||||
|
|
||||||
except WebSocketDisconnect:
|
|
||||||
notifier.remove(websocket, room_name)
|
|
|
@ -1,527 +0,0 @@
|
||||||
from http import HTTPStatus
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi import Depends, Query
|
|
||||||
from loguru import logger
|
|
||||||
from starlette.exceptions import HTTPException
|
|
||||||
|
|
||||||
from lnbits.core.crud import get_user
|
|
||||||
from lnbits.core.services import create_invoice
|
|
||||||
from lnbits.core.views.api import api_payment
|
|
||||||
from lnbits.decorators import (
|
|
||||||
WalletTypeInfo,
|
|
||||||
get_key_type,
|
|
||||||
require_admin_key,
|
|
||||||
require_invoice_key,
|
|
||||||
)
|
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
|
||||||
from lnbits.utils.exchange_rates import currencies
|
|
||||||
|
|
||||||
from . import db, market_ext
|
|
||||||
from .crud import (
|
|
||||||
create_market_market,
|
|
||||||
create_market_market_stalls,
|
|
||||||
create_market_order,
|
|
||||||
create_market_order_details,
|
|
||||||
create_market_product,
|
|
||||||
create_market_settings,
|
|
||||||
create_market_stall,
|
|
||||||
create_market_zone,
|
|
||||||
delete_market_order,
|
|
||||||
delete_market_product,
|
|
||||||
delete_market_stall,
|
|
||||||
delete_market_zone,
|
|
||||||
get_market_chat_by_merchant,
|
|
||||||
get_market_chat_messages,
|
|
||||||
get_market_latest_chat_messages,
|
|
||||||
get_market_market,
|
|
||||||
get_market_market_stalls,
|
|
||||||
get_market_markets,
|
|
||||||
get_market_order,
|
|
||||||
get_market_order_details,
|
|
||||||
get_market_order_invoiceid,
|
|
||||||
get_market_orders,
|
|
||||||
get_market_product,
|
|
||||||
get_market_products,
|
|
||||||
get_market_settings,
|
|
||||||
get_market_stall,
|
|
||||||
get_market_stalls,
|
|
||||||
get_market_zone,
|
|
||||||
get_market_zones,
|
|
||||||
set_market_order_pubkey,
|
|
||||||
set_market_settings,
|
|
||||||
update_market_market,
|
|
||||||
update_market_product,
|
|
||||||
update_market_stall,
|
|
||||||
update_market_zone,
|
|
||||||
)
|
|
||||||
from .models import (
|
|
||||||
CreateMarket,
|
|
||||||
SetSettings,
|
|
||||||
createOrder,
|
|
||||||
createProduct,
|
|
||||||
createStalls,
|
|
||||||
createZones,
|
|
||||||
)
|
|
||||||
|
|
||||||
# from lnbits.db import open_ext_db
|
|
||||||
|
|
||||||
|
|
||||||
### Products
|
|
||||||
@market_ext.get("/api/v1/products")
|
|
||||||
async def api_market_products(
|
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
|
||||||
all_stalls: bool = Query(False),
|
|
||||||
):
|
|
||||||
wallet_ids = [wallet.wallet.id]
|
|
||||||
|
|
||||||
if all_stalls:
|
|
||||||
user = await get_user(wallet.wallet.user)
|
|
||||||
wallet_ids = user.wallet_ids if user else []
|
|
||||||
|
|
||||||
stalls = [stall.id for stall in await get_market_stalls(wallet_ids)]
|
|
||||||
|
|
||||||
if not stalls:
|
|
||||||
return
|
|
||||||
|
|
||||||
return [product.dict() for product in await get_market_products(stalls)]
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.post("/api/v1/products")
|
|
||||||
@market_ext.put("/api/v1/products/{product_id}")
|
|
||||||
async def api_market_product_create(
|
|
||||||
data: createProduct,
|
|
||||||
product_id=None,
|
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
|
||||||
):
|
|
||||||
# For fiat currencies,
|
|
||||||
# we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents.
|
|
||||||
settings = await get_market_settings(user=wallet.wallet.user)
|
|
||||||
assert settings
|
|
||||||
|
|
||||||
stall = await get_market_stall(stall_id=data.stall)
|
|
||||||
assert stall
|
|
||||||
|
|
||||||
if stall.currency != "sat":
|
|
||||||
data.price *= settings.fiat_base_multiplier
|
|
||||||
|
|
||||||
if data.image:
|
|
||||||
image_is_url = data.image.startswith("https://") or data.image.startswith(
|
|
||||||
"http://"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not image_is_url:
|
|
||||||
|
|
||||||
def size(b64string):
|
|
||||||
return int((len(b64string) * 3) / 4 - b64string.count("=", -2))
|
|
||||||
|
|
||||||
image_size = size(data.image) / 1024
|
|
||||||
if image_size > 100:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail=f"Image size is too big, {int(image_size)}Kb. Max: 100kb, Compress the image at https://tinypng.com, or use an URL.",
|
|
||||||
)
|
|
||||||
|
|
||||||
if product_id:
|
|
||||||
product = await get_market_product(product_id)
|
|
||||||
if not product:
|
|
||||||
return {"message": "Product does not exist."}
|
|
||||||
|
|
||||||
# stall = await get_market_stall(stall_id=product.stall)
|
|
||||||
if stall.wallet != wallet.wallet.id:
|
|
||||||
return {"message": "Not your product."}
|
|
||||||
|
|
||||||
product = await update_market_product(product_id, **data.dict())
|
|
||||||
else:
|
|
||||||
product = await create_market_product(data=data)
|
|
||||||
assert product
|
|
||||||
return product.dict()
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.delete("/api/v1/products/{product_id}")
|
|
||||||
async def api_market_products_delete(
|
|
||||||
product_id, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
):
|
|
||||||
product = await get_market_product(product_id)
|
|
||||||
|
|
||||||
if not product:
|
|
||||||
return {"message": "Product does not exist."}
|
|
||||||
|
|
||||||
stall = await get_market_stall(product.stall)
|
|
||||||
assert stall
|
|
||||||
|
|
||||||
if stall.wallet != wallet.wallet.id:
|
|
||||||
return {"message": "Not your Market."}
|
|
||||||
|
|
||||||
await delete_market_product(product_id)
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
# # # Shippingzones
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/api/v1/zones")
|
|
||||||
async def api_market_zones(wallet: WalletTypeInfo = Depends(get_key_type)):
|
|
||||||
|
|
||||||
return await get_market_zones(wallet.wallet.user)
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.post("/api/v1/zones")
|
|
||||||
async def api_market_zone_create(
|
|
||||||
data: createZones, wallet: WalletTypeInfo = Depends(get_key_type)
|
|
||||||
):
|
|
||||||
zone = await create_market_zone(user=wallet.wallet.user, data=data)
|
|
||||||
return zone.dict()
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.post("/api/v1/zones/{zone_id}")
|
|
||||||
async def api_market_zone_update(
|
|
||||||
data: createZones,
|
|
||||||
zone_id: str,
|
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
):
|
|
||||||
zone = await get_market_zone(zone_id)
|
|
||||||
if not zone:
|
|
||||||
return {"message": "Zone does not exist."}
|
|
||||||
if zone.user != wallet.wallet.user:
|
|
||||||
return {"message": "Not your record."}
|
|
||||||
zone = await update_market_zone(zone_id, **data.dict())
|
|
||||||
return zone
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.delete("/api/v1/zones/{zone_id}")
|
|
||||||
async def api_market_zone_delete(
|
|
||||||
zone_id, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
):
|
|
||||||
zone = await get_market_zone(zone_id)
|
|
||||||
|
|
||||||
if not zone:
|
|
||||||
return {"message": "zone does not exist."}
|
|
||||||
|
|
||||||
if zone.user != wallet.wallet.user:
|
|
||||||
return {"message": "Not your zone."}
|
|
||||||
|
|
||||||
await delete_market_zone(zone_id)
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
# # # Stalls
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/api/v1/stalls")
|
|
||||||
async def api_market_stalls(
|
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
|
||||||
):
|
|
||||||
wallet_ids = [wallet.wallet.id]
|
|
||||||
|
|
||||||
if all_wallets:
|
|
||||||
user = await get_user(wallet.wallet.user)
|
|
||||||
wallet_ids = user.wallet_ids if user else []
|
|
||||||
|
|
||||||
return [stall.dict() for stall in await get_market_stalls(wallet_ids)]
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.post("/api/v1/stalls")
|
|
||||||
@market_ext.put("/api/v1/stalls/{stall_id}")
|
|
||||||
async def api_market_stall_create(
|
|
||||||
data: createStalls,
|
|
||||||
stall_id: Optional[str] = None,
|
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
|
||||||
):
|
|
||||||
|
|
||||||
if stall_id:
|
|
||||||
stall = await get_market_stall(stall_id)
|
|
||||||
if not stall:
|
|
||||||
return {"message": "Withdraw stall does not exist."}
|
|
||||||
|
|
||||||
if stall.wallet != wallet.wallet.id:
|
|
||||||
return {"message": "Not your withdraw stall."}
|
|
||||||
|
|
||||||
stall = await update_market_stall(stall_id, **data.dict())
|
|
||||||
else:
|
|
||||||
stall = await create_market_stall(data=data)
|
|
||||||
assert stall
|
|
||||||
return stall.dict()
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.delete("/api/v1/stalls/{stall_id}")
|
|
||||||
async def api_market_stall_delete(
|
|
||||||
stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
):
|
|
||||||
stall = await get_market_stall(stall_id)
|
|
||||||
|
|
||||||
if not stall:
|
|
||||||
return {"message": "Stall does not exist."}
|
|
||||||
|
|
||||||
if stall.wallet != wallet.wallet.id:
|
|
||||||
return {"message": "Not your Stall."}
|
|
||||||
|
|
||||||
await delete_market_stall(stall_id)
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
###Orders
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/api/v1/orders")
|
|
||||||
async def api_market_orders(
|
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
|
||||||
):
|
|
||||||
wallet_ids = [wallet.wallet.id]
|
|
||||||
if all_wallets:
|
|
||||||
user = await get_user(wallet.wallet.user)
|
|
||||||
wallet_ids = user.wallet_ids if user else []
|
|
||||||
|
|
||||||
orders = await get_market_orders(wallet_ids)
|
|
||||||
if not orders:
|
|
||||||
return
|
|
||||||
orders_with_details = []
|
|
||||||
for order in orders:
|
|
||||||
_order = order.dict()
|
|
||||||
_order["details"] = await get_market_order_details(_order["id"])
|
|
||||||
orders_with_details.append(_order)
|
|
||||||
try:
|
|
||||||
return orders_with_details # [order for order in orders]
|
|
||||||
# return [order.dict() for order in await get_market_orders(wallet_ids)]
|
|
||||||
except:
|
|
||||||
return {"message": "We could not retrieve the orders."}
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/api/v1/orders/{order_id}")
|
|
||||||
async def api_market_order_by_id(order_id: str):
|
|
||||||
order = await get_market_order(order_id)
|
|
||||||
assert order
|
|
||||||
_order = order.dict()
|
|
||||||
_order["details"] = await get_market_order_details(order_id)
|
|
||||||
|
|
||||||
return _order
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.post("/api/v1/orders")
|
|
||||||
async def api_market_order_create(data: createOrder):
|
|
||||||
ref = urlsafe_short_hash()
|
|
||||||
|
|
||||||
payment_hash, payment_request = await create_invoice(
|
|
||||||
wallet_id=data.wallet,
|
|
||||||
amount=data.total,
|
|
||||||
memo="New order on Market",
|
|
||||||
extra={
|
|
||||||
"tag": "market",
|
|
||||||
"reference": ref,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
order_id = await create_market_order(invoiceid=payment_hash, data=data)
|
|
||||||
logger.debug(f"ORDER ID {order_id}")
|
|
||||||
logger.debug(f"PRODUCTS {data.products}")
|
|
||||||
await create_market_order_details(order_id=order_id, data=data.products)
|
|
||||||
return {
|
|
||||||
"payment_hash": payment_hash,
|
|
||||||
"payment_request": payment_request,
|
|
||||||
"order_reference": ref,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/api/v1/orders/payments/{payment_hash}")
|
|
||||||
async def api_market_check_payment(payment_hash: str):
|
|
||||||
order = await get_market_order_invoiceid(payment_hash)
|
|
||||||
if not order:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Order does not exist."
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
status = await api_payment(payment_hash)
|
|
||||||
|
|
||||||
except Exception as exc:
|
|
||||||
logger.error(exc)
|
|
||||||
return {"paid": False}
|
|
||||||
return status
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.delete("/api/v1/orders/{order_id}")
|
|
||||||
async def api_market_order_delete(
|
|
||||||
order_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
):
|
|
||||||
order = await get_market_order(order_id)
|
|
||||||
|
|
||||||
if not order:
|
|
||||||
return {"message": "Order does not exist."}
|
|
||||||
|
|
||||||
if order.wallet != wallet.wallet.id:
|
|
||||||
return {"message": "Not your Order."}
|
|
||||||
|
|
||||||
await delete_market_order(order_id)
|
|
||||||
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
# @market_ext.get("/api/v1/orders/paid/{order_id}")
|
|
||||||
# async def api_market_order_paid(
|
|
||||||
# order_id, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
# ):
|
|
||||||
# await db.execute(
|
|
||||||
# "UPDATE market.orders SET paid = ? WHERE id = ?",
|
|
||||||
# (
|
|
||||||
# True,
|
|
||||||
# order_id,
|
|
||||||
# ),
|
|
||||||
# )
|
|
||||||
# return "", HTTPStatus.OK
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/api/v1/order/pubkey/{payment_hash}/{pubkey}")
|
|
||||||
async def api_market_order_pubkey(payment_hash: str, pubkey: str):
|
|
||||||
await set_market_order_pubkey(payment_hash, pubkey)
|
|
||||||
return "", HTTPStatus.OK
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/api/v1/orders/shipped/{order_id}")
|
|
||||||
async def api_market_order_shipped(
|
|
||||||
order_id, shipped: bool = Query(...), wallet: WalletTypeInfo = Depends(get_key_type)
|
|
||||||
):
|
|
||||||
await db.execute(
|
|
||||||
"UPDATE market.orders SET shipped = ? WHERE id = ?",
|
|
||||||
(
|
|
||||||
shipped,
|
|
||||||
order_id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
order = await db.fetchone("SELECT * FROM market.orders WHERE id = ?", (order_id,))
|
|
||||||
|
|
||||||
return order
|
|
||||||
|
|
||||||
|
|
||||||
###List products based on stall id
|
|
||||||
|
|
||||||
|
|
||||||
# @market_ext.get("/api/v1/stall/products/{stall_id}")
|
|
||||||
# async def api_market_stall_products(
|
|
||||||
# stall_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
|
||||||
# ):
|
|
||||||
|
|
||||||
# rows = await db.fetchone("SELECT * FROM market.stalls WHERE id = ?", (stall_id,))
|
|
||||||
# if not rows:
|
|
||||||
# return {"message": "Stall does not exist."}
|
|
||||||
|
|
||||||
# products = db.fetchone("SELECT * FROM market.products WHERE wallet = ?", (rows[1],))
|
|
||||||
# if not products:
|
|
||||||
# return {"message": "No products"}
|
|
||||||
|
|
||||||
# return [products.dict() for products in await get_market_products(rows[1])]
|
|
||||||
|
|
||||||
|
|
||||||
###Check a product has been shipped
|
|
||||||
|
|
||||||
|
|
||||||
# @market_ext.get("/api/v1/stall/checkshipped/{checking_id}")
|
|
||||||
# async def api_market_stall_checkshipped(
|
|
||||||
# checking_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
|
||||||
# ):
|
|
||||||
# rows = await db.fetchone(
|
|
||||||
# "SELECT * FROM market.orders WHERE invoiceid = ?", (checking_id,)
|
|
||||||
# )
|
|
||||||
# return {"shipped": rows["shipped"]}
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# MARKETS
|
|
||||||
##
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/api/v1/markets")
|
|
||||||
async def api_market_markets(wallet: WalletTypeInfo = Depends(get_key_type)):
|
|
||||||
# await get_market_market_stalls(market_id="FzpWnMyHQMcRppiGVua4eY")
|
|
||||||
try:
|
|
||||||
return [
|
|
||||||
market.dict() for market in await get_market_markets(wallet.wallet.user)
|
|
||||||
]
|
|
||||||
except:
|
|
||||||
return {"message": "We could not retrieve the markets."}
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/api/v1/markets/{market_id}/stalls")
|
|
||||||
async def api_market_market_stalls(market_id: str):
|
|
||||||
stall_ids = await get_market_market_stalls(market_id)
|
|
||||||
return stall_ids
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.post("/api/v1/markets")
|
|
||||||
@market_ext.put("/api/v1/markets/{market_id}")
|
|
||||||
async def api_market_market_create(
|
|
||||||
data: CreateMarket,
|
|
||||||
market_id: Optional[str] = None,
|
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
|
||||||
):
|
|
||||||
if market_id:
|
|
||||||
market = await get_market_market(market_id)
|
|
||||||
if not market:
|
|
||||||
return {"message": "Market does not exist."}
|
|
||||||
|
|
||||||
if market.usr != wallet.wallet.user:
|
|
||||||
return {"message": "Not your market."}
|
|
||||||
|
|
||||||
market = await update_market_market(market_id, data.name)
|
|
||||||
else:
|
|
||||||
market = await create_market_market(data=data)
|
|
||||||
|
|
||||||
assert market
|
|
||||||
await create_market_market_stalls(market_id=market.id, data=data.stalls)
|
|
||||||
|
|
||||||
return market.dict()
|
|
||||||
|
|
||||||
|
|
||||||
## MESSAGES/CHAT
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/api/v1/chat/messages/merchant")
|
|
||||||
async def api_get_merchant_messages(
|
|
||||||
orders: str = Query(...), wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
):
|
|
||||||
return [msg.dict() for msg in await get_market_chat_by_merchant(orders.split(","))]
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/api/v1/chat/messages/{room_name}")
|
|
||||||
async def api_get_latest_chat_msg(room_name: str, all_messages: bool = Query(False)):
|
|
||||||
if all_messages:
|
|
||||||
messages = await get_market_chat_messages(room_name)
|
|
||||||
else:
|
|
||||||
messages = await get_market_latest_chat_messages(room_name)
|
|
||||||
|
|
||||||
return messages
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/api/v1/currencies")
|
|
||||||
async def api_list_currencies_available():
|
|
||||||
return list(currencies.keys())
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.get("/api/v1/settings")
|
|
||||||
async def api_get_settings(wallet: WalletTypeInfo = Depends(require_admin_key)):
|
|
||||||
user = wallet.wallet.user
|
|
||||||
|
|
||||||
settings = await get_market_settings(user)
|
|
||||||
|
|
||||||
return settings
|
|
||||||
|
|
||||||
|
|
||||||
@market_ext.post("/api/v1/settings")
|
|
||||||
@market_ext.put("/api/v1/settings/{usr}")
|
|
||||||
async def api_set_settings(
|
|
||||||
data: SetSettings,
|
|
||||||
usr: Optional[str] = None,
|
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
):
|
|
||||||
if usr:
|
|
||||||
if usr != wallet.wallet.user:
|
|
||||||
return {"message": "Not your Market."}
|
|
||||||
|
|
||||||
settings = await get_market_settings(user=usr)
|
|
||||||
assert settings
|
|
||||||
|
|
||||||
if settings.user != wallet.wallet.user:
|
|
||||||
return {"message": "Not your Market."}
|
|
||||||
|
|
||||||
return await set_market_settings(usr, data)
|
|
||||||
|
|
||||||
user = wallet.wallet.user
|
|
||||||
|
|
||||||
return await create_market_settings(user, data)
|
|
Loading…
Add table
Reference in a new issue