V1 (#106)
This commit is contained in:
parent
83c94e94db
commit
0fc26d096f
52 changed files with 6684 additions and 3120 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -16,6 +16,7 @@ __pycache__
|
||||||
htmlcov
|
htmlcov
|
||||||
test-reports
|
test-reports
|
||||||
tests/data/*.sqlite3
|
tests/data/*.sqlite3
|
||||||
|
node_modules
|
||||||
|
|
||||||
*.swo
|
*.swo
|
||||||
*.swp
|
*.swp
|
||||||
|
|
|
||||||
14
.prettierignore
Normal file
14
.prettierignore
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
**/.git
|
||||||
|
**/.svn
|
||||||
|
**/.hg
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
|
*.yml
|
||||||
|
|
||||||
|
**/static/market/*
|
||||||
|
**/static/js/nostr.bundle.js*
|
||||||
|
|
||||||
|
|
||||||
|
flake.lock
|
||||||
|
|
||||||
|
.venv
|
||||||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"insertPragma": false,
|
||||||
|
"printWidth": 80,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"useTabs": false,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"bracketSpacing": false
|
||||||
|
}
|
||||||
47
Makefile
Normal file
47
Makefile
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
all: format check
|
||||||
|
|
||||||
|
format: prettier black ruff
|
||||||
|
|
||||||
|
check: mypy pyright checkblack checkruff checkprettier
|
||||||
|
|
||||||
|
prettier:
|
||||||
|
poetry run ./node_modules/.bin/prettier --write .
|
||||||
|
pyright:
|
||||||
|
poetry run ./node_modules/.bin/pyright
|
||||||
|
|
||||||
|
mypy:
|
||||||
|
poetry run mypy .
|
||||||
|
|
||||||
|
black:
|
||||||
|
poetry run black .
|
||||||
|
|
||||||
|
ruff:
|
||||||
|
poetry run ruff check . --fix
|
||||||
|
|
||||||
|
checkruff:
|
||||||
|
poetry run ruff check .
|
||||||
|
|
||||||
|
checkprettier:
|
||||||
|
poetry run ./node_modules/.bin/prettier --check .
|
||||||
|
|
||||||
|
checkblack:
|
||||||
|
poetry run black --check .
|
||||||
|
|
||||||
|
checkeditorconfig:
|
||||||
|
editorconfig-checker
|
||||||
|
|
||||||
|
test:
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
DEBUG=true \
|
||||||
|
poetry run pytest
|
||||||
|
install-pre-commit-hook:
|
||||||
|
@echo "Installing pre-commit hook to git"
|
||||||
|
@echo "Uninstall the hook with poetry run pre-commit uninstall"
|
||||||
|
poetry run pre-commit install
|
||||||
|
|
||||||
|
pre-commit:
|
||||||
|
poetry run pre-commit run --all-files
|
||||||
|
|
||||||
|
|
||||||
|
checkbundle:
|
||||||
|
@echo "skipping checkbundle"
|
||||||
20
README.md
20
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Nostr Market ([NIP-15](https://github.com/nostr-protocol/nips/blob/master/15.md)) - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small>
|
# Nostr Market ([NIP-15](https://github.com/nostr-protocol/nips/blob/master/15.md)) - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small>
|
||||||
<small>For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions).</small>
|
|
||||||
|
|
||||||
|
<small>For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions).</small>
|
||||||
|
|
||||||
**Demo at Nostrica <a href="https://www.youtube.com/live/2NueacYJovA?feature=share&t=6846">here</a>**.
|
**Demo at Nostrica <a href="https://www.youtube.com/live/2NueacYJovA?feature=share&t=6846">here</a>**.
|
||||||
|
|
||||||
|
|
@ -8,15 +8,15 @@
|
||||||
|
|
||||||
> The concepts around resilience in Diagon Alley helped influence the creation of the NOSTR protocol, now we get to build Diagon Alley on NOSTR!
|
> The concepts around resilience in Diagon Alley helped influence the creation of the NOSTR protocol, now we get to build Diagon Alley on NOSTR!
|
||||||
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
This extension uses the LNbits [nostrclient](https://github.com/lnbits/nostrclient) extension, an extension that makes _nostrfying_ other extensions easy.
|
This extension uses the LNbits [nostrclient](https://github.com/lnbits/nostrclient) extension, an extension that makes _nostrfying_ other extensions easy.
|
||||||

|

|
||||||
|
|
||||||
- before you continue, please make sure that [nostrclient](https://github.com/lnbits/nostrclient) extension is installed, activated and correctly configured.
|
- before you continue, please make sure that [nostrclient](https://github.com/lnbits/nostrclient) extension is installed, activated and correctly configured.
|
||||||
- [nostrclient](https://github.com/lnbits/nostrclient) is usually installed as admin-only extension, so if you do not have admin access please ask an admin to confirm that [nostrclient](https://github.com/lnbits/nostrclient) is OK.
|
- [nostrclient](https://github.com/lnbits/nostrclient) is usually installed as admin-only extension, so if you do not have admin access please ask an admin to confirm that [nostrclient](https://github.com/lnbits/nostrclient) is OK.
|
||||||
- see the [Troubleshoot](https://github.com/lnbits/nostrclient#troubleshoot) section for more details on how to check the health of `nostrclient` extension
|
- see the [Troubleshoot](https://github.com/lnbits/nostrclient#troubleshoot) section for more details on how to check the health of `nostrclient` extension
|
||||||
|
|
||||||
|
|
||||||
## Create, or import, a merchant account
|
## Create, or import, a merchant account
|
||||||
|
|
||||||
As a merchant you need to provide a Nostr key pair, or the extension can generate one for you.
|
As a merchant you need to provide a Nostr key pair, or the extension can generate one for you.
|
||||||
|
|
@ -97,35 +97,39 @@ Make sure to add your `merchant` public key to the list:
|
||||||

|

|
||||||
|
|
||||||
### Styling
|
### Styling
|
||||||
|
|
||||||
In order to create a customized Marketplace, we use `naddr` as defined in [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md#shareable-identifiers-with-extra-metadata). You must create an event (kind: `30019`) that has all the custom properties, including merchants and relays, of your marketplace. Start by going to the marketplace page:
|
In order to create a customized Marketplace, we use `naddr` as defined in [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md#shareable-identifiers-with-extra-metadata). You must create an event (kind: `30019`) that has all the custom properties, including merchants and relays, of your marketplace. Start by going to the marketplace page:
|
||||||

|

|
||||||
|
|
||||||
You'll need to Login, and head over to *Marketplace Info*. Optionally import some merchants and relays, that will be included in the event. Click on *Edit* and fill out your marketplace custom info:
|
You'll need to Login, and head over to _Marketplace Info_. Optionally import some merchants and relays, that will be included in the event. Click on _Edit_ and fill out your marketplace custom info:
|
||||||

|

|
||||||
|
|
||||||
Fill in the optional fields:
|
Fill in the optional fields:
|
||||||
|
|
||||||
- Add a name to the Marketplace
|
- Add a name to the Marketplace
|
||||||
- Add a small description
|
- Add a small description
|
||||||
- Add a logo image URL
|
- Add a logo image URL
|
||||||
- Add a banner image URL (max height: 250px)
|
- Add a banner image URL (max height: 250px)
|
||||||
- Choose a theme
|
- Choose a theme
|
||||||
|
|
||||||
By clicking *Publish*, a `kind: 30019` event will be sent to the defined relays containing all the information about your custom Marketplace. On the left drawer, a button with *Copy Naddr* will show up.
|
By clicking _Publish_, a `kind: 30019` event will be sent to the defined relays containing all the information about your custom Marketplace. On the left drawer, a button with _Copy Naddr_ will show up.
|
||||||

|

|
||||||
|
|
||||||
You can then share your Marketplace, with the merchants and relays, banner, and style by using that Nostr identifier. The URL for the marketplace will be for example: `https://legend.lnbits.com/nostrmarket/market?naddr=naddr1qqfy6ctjddjhgurvv93k....`, you need to include the URL parameter `naddr=<your naddr>`. When a user visits that URL, the client will get the `30019` event and configure the Marketplace to what you defined. In the example bellow, a couple of merchants, relays, `autumn` theme, name (*Veggies Market*) and a header banner:
|
You can then share your Marketplace, with the merchants and relays, banner, and style by using that Nostr identifier. The URL for the marketplace will be for example: `https://legend.lnbits.com/nostrmarket/market?naddr=naddr1qqfy6ctjddjhgurvv93k....`, you need to include the URL parameter `naddr=<your naddr>`. When a user visits that URL, the client will get the `30019` event and configure the Marketplace to what you defined. In the example bellow, a couple of merchants, relays, `autumn` theme, name (_Veggies Market_) and a header banner:
|
||||||

|

|
||||||
|
|
||||||
The nostr event is a replaceable event, so you can change it to what you like and publish a new one to replace a previous one. For example adding a new merchant, or remove, change theme, add more relays,e tc...
|
The nostr event is a replaceable event, so you can change it to what you like and publish a new one to replace a previous one. For example adding a new merchant, or remove, change theme, add more relays,e tc...
|
||||||
|
|
||||||
|
|
||||||
## Troubleshoot
|
## Troubleshoot
|
||||||
|
|
||||||
### Check communication with Nostr
|
### Check communication with Nostr
|
||||||
|
|
||||||
In order to test that the integration with Nostr is working fine, one can add an `npub` to the chat box and check that DMs are working as expected:
|
In order to test that the integration with Nostr is working fine, one can add an `npub` to the chat box and check that DMs are working as expected:
|
||||||
|
|
||||||
https://user-images.githubusercontent.com/2951406/236777983-259f81d8-136f-48b3-bb73-80749819b5f9.mov
|
https://user-images.githubusercontent.com/2951406/236777983-259f81d8-136f-48b3-bb73-80749819b5f9.mov
|
||||||
|
|
||||||
### Restart connection to Nostr
|
### Restart connection to Nostr
|
||||||
|
|
||||||
If the communication with Nostr is not working then an admin user can `Restart` the Nostr connection.
|
If the communication with Nostr is not working then an admin user can `Restart` the Nostr connection.
|
||||||
|
|
||||||
Merchants can afterwards re-publish their products.
|
Merchants can afterwards re-publish their products.
|
||||||
|
|
@ -133,8 +137,8 @@ Merchants can afterwards re-publish their products.
|
||||||
https://user-images.githubusercontent.com/2951406/236778651-7ada9f6d-07a1-491c-ac9c-55530326c32a.mp4
|
https://user-images.githubusercontent.com/2951406/236778651-7ada9f6d-07a1-491c-ac9c-55530326c32a.mp4
|
||||||
|
|
||||||
### Check Nostrclient extension
|
### Check Nostrclient extension
|
||||||
- see the [Troubleshoot](https://github.com/lnbits/nostrclient#troubleshoot) section for more details on how to check the health of `nostrclient` extension
|
|
||||||
|
|
||||||
|
- see the [Troubleshoot](https://github.com/lnbits/nostrclient#troubleshoot) section for more details on how to check the health of `nostrclient` extension
|
||||||
|
|
||||||
## Aditional info
|
## Aditional info
|
||||||
|
|
||||||
|
|
|
||||||
19
__init__.py
19
__init__.py
|
|
@ -1,11 +1,10 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
from lnbits.helpers import template_renderer
|
from lnbits.helpers import template_renderer
|
||||||
from lnbits.tasks import create_permanent_unique_task
|
from lnbits.tasks import create_permanent_unique_task
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from .nostr.nostr_client import NostrClient
|
from .nostr.nostr_client import NostrClient
|
||||||
|
|
||||||
|
|
@ -24,14 +23,14 @@ nostrmarket_static_files = [
|
||||||
def nostrmarket_renderer():
|
def nostrmarket_renderer():
|
||||||
return template_renderer(["nostrmarket/templates"])
|
return template_renderer(["nostrmarket/templates"])
|
||||||
|
|
||||||
|
|
||||||
nostr_client: NostrClient = NostrClient()
|
nostr_client: NostrClient = NostrClient()
|
||||||
|
|
||||||
|
|
||||||
from .tasks import wait_for_nostr_events, wait_for_paid_invoices
|
from .tasks import wait_for_nostr_events, wait_for_paid_invoices # noqa
|
||||||
from .views import * # noqa
|
from .views import * # noqa
|
||||||
from .views_api import * # noqa
|
from .views_api import * # noqa
|
||||||
|
|
||||||
|
|
||||||
scheduled_tasks: list[asyncio.Task] = []
|
scheduled_tasks: list[asyncio.Task] = []
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -57,7 +56,13 @@ def nostrmarket_start():
|
||||||
await asyncio.sleep(15)
|
await asyncio.sleep(15)
|
||||||
await wait_for_nostr_events(nostr_client)
|
await wait_for_nostr_events(nostr_client)
|
||||||
|
|
||||||
task1 = create_permanent_unique_task("ext_nostrmarket_paid_invoices", wait_for_paid_invoices)
|
task1 = create_permanent_unique_task(
|
||||||
task2 = create_permanent_unique_task("ext_nostrmarket_subscribe_to_nostr_client", _subscribe_to_nostr_client)
|
"ext_nostrmarket_paid_invoices", wait_for_paid_invoices
|
||||||
task3 = create_permanent_unique_task("ext_nostrmarket_wait_for_events", _wait_for_nostr_events)
|
)
|
||||||
|
task2 = create_permanent_unique_task(
|
||||||
|
"ext_nostrmarket_subscribe_to_nostr_client", _subscribe_to_nostr_client
|
||||||
|
)
|
||||||
|
task3 = create_permanent_unique_task(
|
||||||
|
"ext_nostrmarket_wait_for_events", _wait_for_nostr_events
|
||||||
|
)
|
||||||
scheduled_tasks.extend([task1, task2, task3])
|
scheduled_tasks.extend([task1, task2, task3])
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "Nostr Market",
|
"name": "Nostr Market",
|
||||||
"short_description": "Nostr Webshop/market on LNbits",
|
"short_description": "Nostr Webshop/market on LNbits",
|
||||||
"tile": "/nostrmarket/static/images/bitcoin-shop.png",
|
"tile": "/nostrmarket/static/images/bitcoin-shop.png",
|
||||||
"min_lnbits_version": "0.12.6",
|
"min_lnbits_version": "1.0.0",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
{
|
{
|
||||||
"name": "motorina0",
|
"name": "motorina0",
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ Buy and sell things over Nostr, using NIP15 https://github.com/nostr-protocol/ni
|
||||||
Nostr was partly based on the the previous version of this extension "Diagon Alley", so lends itself very well to buying and sellinng over Nostr.
|
Nostr was partly based on the the previous version of this extension "Diagon Alley", so lends itself very well to buying and sellinng over Nostr.
|
||||||
|
|
||||||
The Nostr Market extension includes:
|
The Nostr Market extension includes:
|
||||||
* A merchant client to manage products, sales and communication with customers.
|
|
||||||
* A customer client to find and order products from merchants, communicate with merchants and track status of ordered products.
|
- A merchant client to manage products, sales and communication with customers.
|
||||||
|
- A customer client to find and order products from merchants, communicate with merchants and track status of ordered products.
|
||||||
|
|
||||||
All communication happens over NIP04 encrypted DMs.
|
All communication happens over NIP04 encrypted DMs.
|
||||||
|
|
||||||
|
|
|
||||||
11
helpers.py
11
helpers.py
|
|
@ -1,7 +1,6 @@
|
||||||
import base64
|
import base64
|
||||||
import json
|
|
||||||
import secrets
|
import secrets
|
||||||
from typing import Any, Optional, Tuple
|
from typing import Optional
|
||||||
|
|
||||||
import secp256k1
|
import secp256k1
|
||||||
from bech32 import bech32_decode, convertbits
|
from bech32 import bech32_decode, convertbits
|
||||||
|
|
@ -44,12 +43,14 @@ def encrypt_message(message: str, encryption_key, iv: Optional[bytes] = None) ->
|
||||||
encryptor = cipher.encryptor()
|
encryptor = cipher.encryptor()
|
||||||
encrypted_message = encryptor.update(padded_data) + encryptor.finalize()
|
encrypted_message = encryptor.update(padded_data) + encryptor.finalize()
|
||||||
|
|
||||||
return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}"
|
base64_message = base64.b64encode(encrypted_message).decode()
|
||||||
|
base64_iv = base64.b64encode(iv).decode()
|
||||||
|
return f"{base64_message}?iv={base64_iv}"
|
||||||
|
|
||||||
|
|
||||||
def sign_message_hash(private_key: str, hash: bytes) -> str:
|
def sign_message_hash(private_key: str, hash_: bytes) -> str:
|
||||||
privkey = secp256k1.PrivateKey(bytes.fromhex(private_key))
|
privkey = secp256k1.PrivateKey(bytes.fromhex(private_key))
|
||||||
sig = privkey.schnorr_sign(hash, None, raw=True)
|
sig = privkey.schnorr_sign(hash_, None, raw=True)
|
||||||
return sig.hex()
|
return sig.hex()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
async def m001_initial(db):
|
async def m001_initial(db):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Initial merchants table.
|
Initial merchants table.
|
||||||
"""
|
"""
|
||||||
|
|
@ -121,7 +120,10 @@ async def m001_initial(db):
|
||||||
Create indexes for message fetching
|
Create indexes for message fetching
|
||||||
"""
|
"""
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"CREATE INDEX idx_messages_timestamp ON nostrmarket.direct_messages (time DESC)"
|
"""
|
||||||
|
CREATE INDEX idx_messages_timestamp
|
||||||
|
ON nostrmarket.direct_messages (time DESC)
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"CREATE INDEX idx_event_id ON nostrmarket.direct_messages (event_id)"
|
"CREATE INDEX idx_event_id ON nostrmarket.direct_messages (event_id)"
|
||||||
|
|
@ -142,23 +144,26 @@ async def m001_initial(db):
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m002_update_stall_and_product(db):
|
async def m002_update_stall_and_product(db):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"ALTER TABLE nostrmarket.stalls ADD COLUMN pending BOOLEAN NOT NULL DEFAULT false;"
|
"""
|
||||||
)
|
ALTER TABLE nostrmarket.stalls
|
||||||
await db.execute(
|
ADD COLUMN pending BOOLEAN NOT NULL DEFAULT false;
|
||||||
"ALTER TABLE nostrmarket.stalls ADD COLUMN event_id TEXT;"
|
"""
|
||||||
)
|
)
|
||||||
|
await db.execute("ALTER TABLE nostrmarket.stalls ADD COLUMN event_id TEXT;")
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"ALTER TABLE nostrmarket.stalls ADD COLUMN event_created_at INTEGER;"
|
"ALTER TABLE nostrmarket.stalls ADD COLUMN event_created_at INTEGER;"
|
||||||
)
|
)
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"ALTER TABLE nostrmarket.products ADD COLUMN pending BOOLEAN NOT NULL DEFAULT false;"
|
"""
|
||||||
)
|
ALTER TABLE nostrmarket.products
|
||||||
await db.execute(
|
ADD COLUMN pending BOOLEAN NOT NULL DEFAULT false;
|
||||||
"ALTER TABLE nostrmarket.products ADD COLUMN event_id TEXT;"
|
"""
|
||||||
)
|
)
|
||||||
|
await db.execute("ALTER TABLE nostrmarket.products ADD COLUMN event_id TEXT;")
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"ALTER TABLE nostrmarket.products ADD COLUMN event_created_at INTEGER;"
|
"ALTER TABLE nostrmarket.products ADD COLUMN event_created_at INTEGER;"
|
||||||
)
|
)
|
||||||
|
|
@ -166,15 +171,21 @@ async def m002_update_stall_and_product(db):
|
||||||
|
|
||||||
async def m003_update_direct_message_type(db):
|
async def m003_update_direct_message_type(db):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"ALTER TABLE nostrmarket.direct_messages ADD COLUMN type INTEGER NOT NULL DEFAULT -1;"
|
"""
|
||||||
|
ALTER TABLE nostrmarket.direct_messages
|
||||||
|
ADD COLUMN type INTEGER NOT NULL DEFAULT -1;
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m004_add_merchant_timestamp(db):
|
async def m004_add_merchant_timestamp(db):
|
||||||
await db.execute(
|
await db.execute("ALTER TABLE nostrmarket.merchants ADD COLUMN time TIMESTAMP;")
|
||||||
f"ALTER TABLE nostrmarket.merchants ADD COLUMN time TIMESTAMP;"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def m005_update_product_activation(db):
|
async def m005_update_product_activation(db):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"ALTER TABLE nostrmarket.products ADD COLUMN active BOOLEAN NOT NULL DEFAULT true;"
|
"""
|
||||||
|
ALTER TABLE nostrmarket.products
|
||||||
|
ADD COLUMN active BOOLEAN NOT NULL DEFAULT true;
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
197
models.py
197
models.py
|
|
@ -2,12 +2,10 @@ import json
|
||||||
import time
|
import time
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from sqlite3 import Row
|
|
||||||
from typing import Any, List, Optional, Tuple
|
from typing import Any, List, Optional, Tuple
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from lnbits.utils.exchange_rates import btc_price, fiat_amount_as_satoshis
|
from lnbits.utils.exchange_rates import btc_price, fiat_amount_as_satoshis
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
decrypt_message,
|
decrypt_message,
|
||||||
|
|
@ -30,17 +28,17 @@ class Nostrable:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
######################################## MERCHANT ########################################
|
######################################## MERCHANT ######################################
|
||||||
|
|
||||||
|
|
||||||
class MerchantProfile(BaseModel):
|
class MerchantProfile(BaseModel):
|
||||||
name: Optional[str]
|
name: Optional[str] = None
|
||||||
about: Optional[str]
|
about: Optional[str] = None
|
||||||
picture: Optional[str]
|
picture: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class MerchantConfig(MerchantProfile):
|
class MerchantConfig(MerchantProfile):
|
||||||
event_id: Optional[str]
|
event_id: Optional[str] = None
|
||||||
sync_from_nostr = False
|
sync_from_nostr = False
|
||||||
active: bool = False
|
active: bool = False
|
||||||
restore_in_progress: Optional[bool] = False
|
restore_in_progress: Optional[bool] = False
|
||||||
|
|
@ -56,8 +54,8 @@ class Merchant(PartialMerchant, Nostrable):
|
||||||
id: str
|
id: str
|
||||||
time: Optional[int] = 0
|
time: Optional[int] = 0
|
||||||
|
|
||||||
def sign_hash(self, hash: bytes) -> str:
|
def sign_hash(self, hash_: bytes) -> str:
|
||||||
return sign_message_hash(self.private_key, hash)
|
return sign_message_hash(self.private_key, hash_)
|
||||||
|
|
||||||
def decrypt_message(self, encrypted_message: str, public_key: str) -> str:
|
def decrypt_message(self, encrypted_message: str, public_key: str) -> str:
|
||||||
encryption_key = get_shared_secret(self.private_key, public_key)
|
encryption_key = get_shared_secret(self.private_key, public_key)
|
||||||
|
|
@ -82,8 +80,8 @@ class Merchant(PartialMerchant, Nostrable):
|
||||||
return event
|
return event
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Merchant":
|
def from_row(cls, row: dict) -> "Merchant":
|
||||||
merchant = cls(**dict(row))
|
merchant = cls(**row)
|
||||||
merchant.config = MerchantConfig(**json.loads(row["meta"]))
|
merchant.config = MerchantConfig(**json.loads(row["meta"]))
|
||||||
return merchant
|
return merchant
|
||||||
|
|
||||||
|
|
@ -123,20 +121,16 @@ class Merchant(PartialMerchant, Nostrable):
|
||||||
|
|
||||||
|
|
||||||
######################################## ZONES ########################################
|
######################################## ZONES ########################################
|
||||||
class PartialZone(BaseModel):
|
class Zone(BaseModel):
|
||||||
id: Optional[str]
|
id: Optional[str] = None
|
||||||
name: Optional[str]
|
name: Optional[str] = None
|
||||||
currency: str
|
currency: str
|
||||||
cost: float
|
cost: float
|
||||||
countries: List[str] = []
|
countries: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
class Zone(PartialZone):
|
|
||||||
id: str
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Zone":
|
def from_row(cls, row: dict) -> "Zone":
|
||||||
zone = cls(**dict(row))
|
zone = cls(**row)
|
||||||
zone.countries = json.loads(row["regions"])
|
zone.countries = json.loads(row["regions"])
|
||||||
return zone
|
return zone
|
||||||
|
|
||||||
|
|
@ -145,12 +139,12 @@ class Zone(PartialZone):
|
||||||
|
|
||||||
|
|
||||||
class StallConfig(BaseModel):
|
class StallConfig(BaseModel):
|
||||||
image_url: Optional[str]
|
image_url: Optional[str] = None
|
||||||
description: Optional[str]
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class PartialStall(BaseModel):
|
class Stall(BaseModel, Nostrable):
|
||||||
id: Optional[str]
|
id: Optional[str] = None
|
||||||
wallet: str
|
wallet: str
|
||||||
name: str
|
name: str
|
||||||
currency: str = "sat"
|
currency: str = "sat"
|
||||||
|
|
@ -159,8 +153,8 @@ class PartialStall(BaseModel):
|
||||||
pending: bool = False
|
pending: bool = False
|
||||||
|
|
||||||
"""Last published nostr event for this Stall"""
|
"""Last published nostr event for this Stall"""
|
||||||
event_id: Optional[str]
|
event_id: Optional[str] = None
|
||||||
event_created_at: Optional[int]
|
event_created_at: Optional[int] = None
|
||||||
|
|
||||||
def validate_stall(self):
|
def validate_stall(self):
|
||||||
for z in self.shipping_zones:
|
for z in self.shipping_zones:
|
||||||
|
|
@ -169,10 +163,6 @@ class PartialStall(BaseModel):
|
||||||
f"Sipping zone '{z.name}' has different currency than stall."
|
f"Sipping zone '{z.name}' has different currency than stall."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Stall(PartialStall, Nostrable):
|
|
||||||
id: str
|
|
||||||
|
|
||||||
def to_nostr_event(self, pubkey: str) -> NostrEvent:
|
def to_nostr_event(self, pubkey: str) -> NostrEvent:
|
||||||
content = {
|
content = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
|
|
@ -181,6 +171,7 @@ class Stall(PartialStall, Nostrable):
|
||||||
"currency": self.currency,
|
"currency": self.currency,
|
||||||
"shipping": [dict(z) for z in self.shipping_zones],
|
"shipping": [dict(z) for z in self.shipping_zones],
|
||||||
}
|
}
|
||||||
|
assert self.id
|
||||||
event = NostrEvent(
|
event = NostrEvent(
|
||||||
pubkey=pubkey,
|
pubkey=pubkey,
|
||||||
created_at=round(time.time()),
|
created_at=round(time.time()),
|
||||||
|
|
@ -197,7 +188,7 @@ class Stall(PartialStall, Nostrable):
|
||||||
pubkey=pubkey,
|
pubkey=pubkey,
|
||||||
created_at=round(time.time()),
|
created_at=round(time.time()),
|
||||||
kind=5,
|
kind=5,
|
||||||
tags=[["e", self.event_id]],
|
tags=[["e", self.event_id or ""]],
|
||||||
content=f"Stall '{self.name}' deleted",
|
content=f"Stall '{self.name}' deleted",
|
||||||
)
|
)
|
||||||
delete_event.id = delete_event.event_id
|
delete_event.id = delete_event.event_id
|
||||||
|
|
@ -205,14 +196,14 @@ class Stall(PartialStall, Nostrable):
|
||||||
return delete_event
|
return delete_event
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Stall":
|
def from_row(cls, row: dict) -> "Stall":
|
||||||
stall = cls(**dict(row))
|
stall = cls(**row)
|
||||||
stall.config = StallConfig(**json.loads(row["meta"]))
|
stall.config = StallConfig(**json.loads(row["meta"]))
|
||||||
stall.shipping_zones = [Zone(**z) for z in json.loads(row["zones"])]
|
stall.shipping_zones = [Zone(**z) for z in json.loads(row["zones"])]
|
||||||
return stall
|
return stall
|
||||||
|
|
||||||
|
|
||||||
######################################## PRODUCTS ########################################
|
######################################## PRODUCTS ######################################
|
||||||
|
|
||||||
|
|
||||||
class ProductShippingCost(BaseModel):
|
class ProductShippingCost(BaseModel):
|
||||||
|
|
@ -221,15 +212,15 @@ class ProductShippingCost(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class ProductConfig(BaseModel):
|
class ProductConfig(BaseModel):
|
||||||
description: Optional[str]
|
description: Optional[str] = None
|
||||||
currency: Optional[str]
|
currency: Optional[str] = None
|
||||||
use_autoreply: Optional[bool] = False
|
use_autoreply: Optional[bool] = False
|
||||||
autoreply_message: Optional[str]
|
autoreply_message: Optional[str] = None
|
||||||
shipping: Optional[List[ProductShippingCost]] = []
|
shipping: List[ProductShippingCost] = []
|
||||||
|
|
||||||
|
|
||||||
class PartialProduct(BaseModel):
|
class Product(BaseModel, Nostrable):
|
||||||
id: Optional[str]
|
id: Optional[str] = None
|
||||||
stall_id: str
|
stall_id: str
|
||||||
name: str
|
name: str
|
||||||
categories: List[str] = []
|
categories: List[str] = []
|
||||||
|
|
@ -241,12 +232,8 @@ class PartialProduct(BaseModel):
|
||||||
config: ProductConfig = ProductConfig()
|
config: ProductConfig = ProductConfig()
|
||||||
|
|
||||||
"""Last published nostr event for this Product"""
|
"""Last published nostr event for this Product"""
|
||||||
event_id: Optional[str]
|
event_id: Optional[str] = None
|
||||||
event_created_at: Optional[int]
|
event_created_at: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class Product(PartialProduct, Nostrable):
|
|
||||||
id: str
|
|
||||||
|
|
||||||
def to_nostr_event(self, pubkey: str) -> NostrEvent:
|
def to_nostr_event(self, pubkey: str) -> NostrEvent:
|
||||||
content = {
|
content = {
|
||||||
|
|
@ -259,16 +246,17 @@ class Product(PartialProduct, Nostrable):
|
||||||
"price": self.price,
|
"price": self.price,
|
||||||
"quantity": self.quantity,
|
"quantity": self.quantity,
|
||||||
"active": self.active,
|
"active": self.active,
|
||||||
"shipping": [dict(s) for s in self.config.shipping or []]
|
"shipping": [dict(s) for s in self.config.shipping or []],
|
||||||
}
|
}
|
||||||
categories = [["t", tag] for tag in self.categories]
|
categories = [["t", tag] for tag in self.categories]
|
||||||
|
|
||||||
|
assert self.id
|
||||||
if self.active:
|
if self.active:
|
||||||
event = NostrEvent(
|
event = NostrEvent(
|
||||||
pubkey=pubkey,
|
pubkey=pubkey,
|
||||||
created_at=round(time.time()),
|
created_at=round(time.time()),
|
||||||
kind=30018,
|
kind=30018,
|
||||||
tags=[["d", self.id]] + categories,
|
tags=[["d", self.id], *categories],
|
||||||
content=json.dumps(content, separators=(",", ":"), ensure_ascii=False),
|
content=json.dumps(content, separators=(",", ":"), ensure_ascii=False),
|
||||||
)
|
)
|
||||||
event.id = event.event_id
|
event.id = event.event_id
|
||||||
|
|
@ -282,7 +270,7 @@ class Product(PartialProduct, Nostrable):
|
||||||
pubkey=pubkey,
|
pubkey=pubkey,
|
||||||
created_at=round(time.time()),
|
created_at=round(time.time()),
|
||||||
kind=5,
|
kind=5,
|
||||||
tags=[["e", self.event_id]],
|
tags=[["e", self.event_id or ""]],
|
||||||
content=f"Product '{self.name}' deleted",
|
content=f"Product '{self.name}' deleted",
|
||||||
)
|
)
|
||||||
delete_event.id = delete_event.event_id
|
delete_event.id = delete_event.event_id
|
||||||
|
|
@ -290,8 +278,8 @@ class Product(PartialProduct, Nostrable):
|
||||||
return delete_event
|
return delete_event
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Product":
|
def from_row(cls, row: dict) -> "Product":
|
||||||
product = cls(**dict(row))
|
product = cls(**row)
|
||||||
product.config = ProductConfig(**json.loads(row["meta"]))
|
product.config = ProductConfig(**json.loads(row["meta"]))
|
||||||
product.images = json.loads(row["image_urls"]) if "image_urls" in row else []
|
product.images = json.loads(row["image_urls"]) if "image_urls" in row else []
|
||||||
product.categories = json.loads(row["category_list"])
|
product.categories = json.loads(row["category_list"])
|
||||||
|
|
@ -302,6 +290,12 @@ class ProductOverview(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
price: float
|
price: float
|
||||||
|
product_shipping_cost: Optional[float] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_product(cls, p: Product) -> "ProductOverview":
|
||||||
|
assert p.id
|
||||||
|
return ProductOverview(id=p.id, name=p.name, price=p.price)
|
||||||
|
|
||||||
|
|
||||||
######################################## ORDERS ########################################
|
######################################## ORDERS ########################################
|
||||||
|
|
@ -313,9 +307,9 @@ class OrderItem(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class OrderContact(BaseModel):
|
class OrderContact(BaseModel):
|
||||||
nostr: Optional[str]
|
nostr: Optional[str] = None
|
||||||
phone: Optional[str]
|
phone: Optional[str] = None
|
||||||
email: Optional[str]
|
email: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class OrderExtra(BaseModel):
|
class OrderExtra(BaseModel):
|
||||||
|
|
@ -324,27 +318,33 @@ class OrderExtra(BaseModel):
|
||||||
btc_price: str
|
btc_price: str
|
||||||
shipping_cost: float = 0
|
shipping_cost: float = 0
|
||||||
shipping_cost_sat: float = 0
|
shipping_cost_sat: float = 0
|
||||||
fail_message: Optional[str]
|
fail_message: Optional[str] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_products(cls, products: List[Product]):
|
async def from_products(cls, products: List[Product]):
|
||||||
currency = products[0].config.currency if len(products) else "sat"
|
currency = products[0].config.currency if len(products) else "sat"
|
||||||
exchange_rate = (
|
exchange_rate = (
|
||||||
(await btc_price(currency)) if currency and currency != "sat" else 1
|
await btc_price(currency) if currency and currency != "sat" else 1
|
||||||
|
)
|
||||||
|
|
||||||
|
products_overview = [ProductOverview.from_product(p) for p in products]
|
||||||
|
return OrderExtra(
|
||||||
|
products=products_overview,
|
||||||
|
currency=currency or "sat",
|
||||||
|
btc_price=str(exchange_rate),
|
||||||
)
|
)
|
||||||
return OrderExtra(products=products, currency=currency, btc_price=exchange_rate)
|
|
||||||
|
|
||||||
|
|
||||||
class PartialOrder(BaseModel):
|
class PartialOrder(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
event_id: Optional[str]
|
event_id: Optional[str] = None
|
||||||
event_created_at: Optional[int]
|
event_created_at: Optional[int] = None
|
||||||
public_key: str
|
public_key: str
|
||||||
merchant_public_key: str
|
merchant_public_key: str
|
||||||
shipping_id: str
|
shipping_id: str
|
||||||
items: List[OrderItem]
|
items: List[OrderItem]
|
||||||
contact: Optional[OrderContact]
|
contact: Optional[OrderContact] = None
|
||||||
address: Optional[str]
|
address: Optional[str] = None
|
||||||
|
|
||||||
def validate_order(self):
|
def validate_order(self):
|
||||||
assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
|
assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
|
||||||
|
|
@ -383,10 +383,11 @@ class PartialOrder(BaseModel):
|
||||||
}
|
}
|
||||||
|
|
||||||
product_cost: float = 0 # todo
|
product_cost: float = 0 # todo
|
||||||
|
currency = "sat"
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
assert item.quantity > 0, "Quantity cannot be negative"
|
assert item.quantity > 0, "Quantity cannot be negative"
|
||||||
price = product_prices[item.product_id]["price"]
|
price = float(str(product_prices[item.product_id]["price"]))
|
||||||
currency = product_prices[item.product_id]["currency"]
|
currency = str(product_prices[item.product_id]["currency"])
|
||||||
if currency != "sat":
|
if currency != "sat":
|
||||||
price = await fiat_amount_as_satoshis(price, currency)
|
price = await fiat_amount_as_satoshis(price, currency)
|
||||||
product_cost += item.quantity * price
|
product_cost += item.quantity * price
|
||||||
|
|
@ -404,30 +405,39 @@ class PartialOrder(BaseModel):
|
||||||
if len(products) == 0:
|
if len(products) == 0:
|
||||||
return "[No Products]"
|
return "[No Products]"
|
||||||
receipt = ""
|
receipt = ""
|
||||||
product_prices = {}
|
product_prices: dict[str, ProductOverview] = {}
|
||||||
for p in products:
|
for p in products:
|
||||||
product_shipping_cost = next(
|
product_shipping_cost = next(
|
||||||
(s.cost for s in p.config.shipping if s.id == shipping_id), 0
|
(s.cost for s in p.config.shipping if s.id == shipping_id), 0
|
||||||
)
|
)
|
||||||
product_prices[p.id] = {
|
assert p.id
|
||||||
"name": p.name,
|
product_prices[p.id] = ProductOverview(
|
||||||
"price": p.price,
|
id=p.id,
|
||||||
"product_shipping_cost": product_shipping_cost
|
name=p.name,
|
||||||
}
|
price=p.price,
|
||||||
|
product_shipping_cost=product_shipping_cost,
|
||||||
|
)
|
||||||
|
|
||||||
currency = products[0].config.currency or "sat"
|
currency = products[0].config.currency or "sat"
|
||||||
products_cost: float = 0 # todo
|
products_cost: float = 0 # todo
|
||||||
items_receipts = []
|
items_receipts = []
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
prod = product_prices[item.product_id]
|
prod = product_prices[item.product_id]
|
||||||
price = prod["price"] + prod["product_shipping_cost"]
|
price = prod.price + (prod.product_shipping_cost or 0)
|
||||||
|
|
||||||
products_cost += item.quantity * price
|
products_cost += item.quantity * price
|
||||||
|
|
||||||
items_receipts.append(f"""[{prod["name"]}: {item.quantity} x ({prod["price"]} + {prod["product_shipping_cost"]}) = {item.quantity * price} {currency}] """)
|
items_receipts.append(
|
||||||
|
f"""[{prod.name}: {item.quantity} x ({prod.price}"""
|
||||||
|
f""" + {prod.product_shipping_cost})"""
|
||||||
|
f""" = {item.quantity * price} {currency}] """
|
||||||
|
)
|
||||||
|
|
||||||
receipt = "; ".join(items_receipts)
|
receipt = "; ".join(items_receipts)
|
||||||
receipt += f"[Products cost: {products_cost} {currency}] [Stall shipping cost: {stall_shipping_cost} {currency}]; "
|
receipt += (
|
||||||
|
f"[Products cost: {products_cost} {currency}] "
|
||||||
|
f"[Stall shipping cost: {stall_shipping_cost} {currency}]; "
|
||||||
|
)
|
||||||
receipt += f"[Total: {products_cost + stall_shipping_cost} {currency}]"
|
receipt += f"[Total: {products_cost + stall_shipping_cost} {currency}]"
|
||||||
|
|
||||||
return receipt
|
return receipt
|
||||||
|
|
@ -439,23 +449,23 @@ class Order(PartialOrder):
|
||||||
total: float
|
total: float
|
||||||
paid: bool = False
|
paid: bool = False
|
||||||
shipped: bool = False
|
shipped: bool = False
|
||||||
time: Optional[int]
|
time: Optional[int] = None
|
||||||
extra: OrderExtra
|
extra: OrderExtra
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Order":
|
def from_row(cls, row: dict) -> "Order":
|
||||||
contact = OrderContact(**json.loads(row["contact_data"]))
|
contact = OrderContact(**json.loads(row["contact_data"]))
|
||||||
extra = OrderExtra(**json.loads(row["extra_data"]))
|
extra = OrderExtra(**json.loads(row["extra_data"]))
|
||||||
items = [OrderItem(**z) for z in json.loads(row["order_items"])]
|
items = [OrderItem(**z) for z in json.loads(row["order_items"])]
|
||||||
order = cls(**dict(row), contact=contact, items=items, extra=extra)
|
order = cls(**row, contact=contact, items=items, extra=extra)
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
class OrderStatusUpdate(BaseModel):
|
class OrderStatusUpdate(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
message: Optional[str]
|
message: Optional[str] = None
|
||||||
paid: Optional[bool]
|
paid: Optional[bool] = False
|
||||||
shipped: Optional[bool]
|
shipped: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
class OrderReissue(BaseModel):
|
class OrderReissue(BaseModel):
|
||||||
|
|
@ -470,11 +480,11 @@ class PaymentOption(BaseModel):
|
||||||
|
|
||||||
class PaymentRequest(BaseModel):
|
class PaymentRequest(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
message: Optional[str]
|
message: Optional[str] = None
|
||||||
payment_options: List[PaymentOption]
|
payment_options: List[PaymentOption]
|
||||||
|
|
||||||
|
|
||||||
######################################## MESSAGE ########################################
|
######################################## MESSAGE #######################################
|
||||||
|
|
||||||
|
|
||||||
class DirectMessageType(Enum):
|
class DirectMessageType(Enum):
|
||||||
|
|
@ -487,13 +497,13 @@ class DirectMessageType(Enum):
|
||||||
|
|
||||||
|
|
||||||
class PartialDirectMessage(BaseModel):
|
class PartialDirectMessage(BaseModel):
|
||||||
event_id: Optional[str]
|
event_id: Optional[str] = None
|
||||||
event_created_at: Optional[int]
|
event_created_at: Optional[int] = None
|
||||||
message: str
|
message: str
|
||||||
public_key: str
|
public_key: str
|
||||||
type: int = DirectMessageType.PLAIN_TEXT.value
|
type: int = DirectMessageType.PLAIN_TEXT.value
|
||||||
incoming: bool = False
|
incoming: bool = False
|
||||||
time: Optional[int]
|
time: Optional[int] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_message(cls, msg) -> Tuple[DirectMessageType, Optional[Any]]:
|
def parse_message(cls, msg) -> Tuple[DirectMessageType, Optional[Any]]:
|
||||||
|
|
@ -511,29 +521,28 @@ class DirectMessage(PartialDirectMessage):
|
||||||
id: str
|
id: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "DirectMessage":
|
def from_row(cls, row: dict) -> "DirectMessage":
|
||||||
dm = cls(**dict(row))
|
return cls(**row)
|
||||||
return dm
|
|
||||||
|
|
||||||
|
|
||||||
######################################## CUSTOMERS ########################################
|
######################################## CUSTOMERS #####################################
|
||||||
|
|
||||||
|
|
||||||
class CustomerProfile(BaseModel):
|
class CustomerProfile(BaseModel):
|
||||||
name: Optional[str]
|
name: Optional[str] = None
|
||||||
about: Optional[str]
|
about: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class Customer(BaseModel):
|
class Customer(BaseModel):
|
||||||
merchant_id: str
|
merchant_id: str
|
||||||
public_key: str
|
public_key: str
|
||||||
event_created_at: Optional[int]
|
event_created_at: Optional[int] = None
|
||||||
profile: Optional[CustomerProfile]
|
profile: Optional[CustomerProfile] = None
|
||||||
unread_messages: int = 0
|
unread_messages: int = 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Customer":
|
def from_row(cls, row: dict) -> "Customer":
|
||||||
customer = cls(**dict(row))
|
customer = cls(**row)
|
||||||
customer.profile = (
|
customer.profile = (
|
||||||
CustomerProfile(**json.loads(row["meta"])) if "meta" in row else None
|
CustomerProfile(**json.loads(row["meta"])) if "meta" in row else None
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class NostrEvent(BaseModel):
|
||||||
kind: int
|
kind: int
|
||||||
tags: List[List[str]] = []
|
tags: List[List[str]] = []
|
||||||
content: str = ""
|
content: str = ""
|
||||||
sig: Optional[str]
|
sig: Optional[str] = None
|
||||||
|
|
||||||
def serialize(self) -> List:
|
def serialize(self) -> List:
|
||||||
return [0, self.pubkey, self.created_at, self.kind, self.tags, self.content]
|
return [0, self.pubkey, self.created_at, self.kind, self.tags, self.content]
|
||||||
|
|
@ -41,7 +41,7 @@ class NostrEvent(BaseModel):
|
||||||
f"Invalid public key: '{self.pubkey}' for event '{self.id}'"
|
f"Invalid public key: '{self.pubkey}' for event '{self.id}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
valid_signature = pub_key.schnorr_verify(
|
valid_signature = self.sig and pub_key.schnorr_verify(
|
||||||
bytes.fromhex(event_id), bytes.fromhex(self.sig), None, raw=True
|
bytes.fromhex(event_id), bytes.fromhex(self.sig), None, raw=True
|
||||||
)
|
)
|
||||||
if not valid_signature:
|
if not valid_signature:
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
from asyncio import Queue
|
from asyncio import Queue
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import Callable, List
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websocket import WebSocketApp
|
from websocket import WebSocketApp
|
||||||
|
|
||||||
from lnbits.app import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.helpers import encrypt_internal_message, urlsafe_short_hash
|
from lnbits.helpers import encrypt_internal_message, urlsafe_short_hash
|
||||||
|
|
||||||
from .event import NostrEvent
|
from .event import NostrEvent
|
||||||
|
|
@ -17,7 +17,7 @@ class NostrClient:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.recieve_event_queue: Queue = Queue()
|
self.recieve_event_queue: Queue = Queue()
|
||||||
self.send_req_queue: Queue = Queue()
|
self.send_req_queue: Queue = Queue()
|
||||||
self.ws: WebSocketApp = None
|
self.ws: Optional[WebSocketApp] = None
|
||||||
self.subscription_id = "nostrmarket-" + urlsafe_short_hash()[:32]
|
self.subscription_id = "nostrmarket-" + urlsafe_short_hash()[:32]
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
|
|
@ -30,7 +30,6 @@ class NostrClient:
|
||||||
async def connect_to_nostrclient_ws(self) -> WebSocketApp:
|
async def connect_to_nostrclient_ws(self) -> WebSocketApp:
|
||||||
logger.debug(f"Connecting to websockets for 'nostrclient' extension...")
|
logger.debug(f"Connecting to websockets for 'nostrclient' extension...")
|
||||||
|
|
||||||
|
|
||||||
relay_endpoint = encrypt_internal_message("relay")
|
relay_endpoint = encrypt_internal_message("relay")
|
||||||
on_open, on_message, on_error, on_close = self._ws_handlers()
|
on_open, on_message, on_error, on_close = self._ws_handlers()
|
||||||
ws = WebSocketApp(
|
ws = WebSocketApp(
|
||||||
|
|
@ -57,19 +56,18 @@ class NostrClient:
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
req = await self.send_req_queue.get()
|
req = await self.send_req_queue.get()
|
||||||
|
assert self.ws
|
||||||
self.ws.send(json.dumps(req))
|
self.ws.send(json.dumps(req))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
await asyncio.sleep(60)
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
|
||||||
async def get_event(self):
|
async def get_event(self):
|
||||||
value = await self.recieve_event_queue.get()
|
value = await self.recieve_event_queue.get()
|
||||||
if isinstance(value, ValueError):
|
if isinstance(value, ValueError):
|
||||||
raise value
|
raise value
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
async def publish_nostr_event(self, e: NostrEvent):
|
async def publish_nostr_event(self, e: NostrEvent):
|
||||||
await self.send_req_queue.put(["EVENT", e.dict()])
|
await self.send_req_queue.put(["EVENT", e.dict()])
|
||||||
|
|
||||||
|
|
@ -119,7 +117,7 @@ class NostrClient:
|
||||||
|
|
||||||
asyncio.create_task(unsubscribe_with_delay(subscription_id, duration))
|
asyncio.create_task(unsubscribe_with_delay(subscription_id, duration))
|
||||||
|
|
||||||
async def user_profile_temp_subscribe(self, public_key: str, duration=5) -> List:
|
async def user_profile_temp_subscribe(self, public_key: str, duration=5):
|
||||||
try:
|
try:
|
||||||
profile_filter = [{"kinds": [0], "authors": [public_key]}]
|
profile_filter = [{"kinds": [0], "authors": [public_key]}]
|
||||||
subscription_id = "profile-" + urlsafe_short_hash()[:32]
|
subscription_id = "profile-" + urlsafe_short_hash()[:32]
|
||||||
|
|
|
||||||
15
package.json
Normal file
15
package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "nostrmarket",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"pyright": "^1.1.358"
|
||||||
|
}
|
||||||
|
}
|
||||||
2616
poetry.lock
generated
Normal file
2616
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
96
pyproject.toml
Normal file
96
pyproject.toml
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "lnbits-nostrmarket"
|
||||||
|
version = "0.0.0"
|
||||||
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
|
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.10 | ^3.9"
|
||||||
|
lnbits = {version = "*", allow-prereleases = true}
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
black = "^24.3.0"
|
||||||
|
pytest-asyncio = "^0.21.0"
|
||||||
|
pytest = "^7.3.2"
|
||||||
|
mypy = "^1.5.1"
|
||||||
|
pre-commit = "^3.2.2"
|
||||||
|
ruff = "^0.3.2"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
exclude = "(nostr/*)"
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = [
|
||||||
|
"secp256k1.*",
|
||||||
|
"embit.*",
|
||||||
|
"lnbits.*",
|
||||||
|
"lnurl.*",
|
||||||
|
"loguru.*",
|
||||||
|
"fastapi.*",
|
||||||
|
"pydantic.*",
|
||||||
|
"pyqrcode.*",
|
||||||
|
"shortuuid.*",
|
||||||
|
"httpx.*",
|
||||||
|
]
|
||||||
|
ignore_missing_imports = "True"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
log_cli = false
|
||||||
|
testpaths = [
|
||||||
|
"tests"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 88
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
# Same as Black. + 10% rule of black
|
||||||
|
line-length = 88
|
||||||
|
exclude = [
|
||||||
|
"nostr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
# Enable:
|
||||||
|
# F - pyflakes
|
||||||
|
# E - pycodestyle errors
|
||||||
|
# W - pycodestyle warnings
|
||||||
|
# I - isort
|
||||||
|
# A - flake8-builtins
|
||||||
|
# C - mccabe
|
||||||
|
# N - naming
|
||||||
|
# UP - pyupgrade
|
||||||
|
# RUF - ruff
|
||||||
|
# B - bugbear
|
||||||
|
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
|
||||||
|
ignore = ["C901"]
|
||||||
|
|
||||||
|
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||||
|
fixable = ["ALL"]
|
||||||
|
unfixable = []
|
||||||
|
|
||||||
|
# Allow unused variables when underscore-prefixed.
|
||||||
|
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||||
|
|
||||||
|
# needed for pydantic
|
||||||
|
[tool.ruff.lint.pep8-naming]
|
||||||
|
classmethod-decorators = [
|
||||||
|
"root_validator",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Ignore unused imports in __init__.py files.
|
||||||
|
# [tool.ruff.lint.extend-per-file-ignores]
|
||||||
|
# "__init__.py" = ["F401", "F403"]
|
||||||
|
|
||||||
|
# [tool.ruff.lint.mccabe]
|
||||||
|
# max-complexity = 10
|
||||||
|
|
||||||
|
[tool.ruff.lint.flake8-bugbear]
|
||||||
|
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
|
||||||
|
extend-immutable-calls = [
|
||||||
|
"fastapi.Depends",
|
||||||
|
"fastapi.Query",
|
||||||
|
]
|
||||||
103
services.py
103
services.py
|
|
@ -2,10 +2,10 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.bolt11 import decode
|
from lnbits.bolt11 import decode
|
||||||
from lnbits.core.services import websocket_updater, create_invoice, get_wallet
|
from lnbits.core.crud import get_wallet
|
||||||
|
from lnbits.core.services import create_invoice, websocket_updater
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from . import nostr_client
|
from . import nostr_client
|
||||||
from .crud import (
|
from .crud import (
|
||||||
|
|
@ -75,7 +75,9 @@ async def create_new_order(
|
||||||
await create_order(merchant.id, order)
|
await create_order(merchant.id, order)
|
||||||
|
|
||||||
return PaymentRequest(
|
return PaymentRequest(
|
||||||
id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)], message=receipt
|
id=data.id,
|
||||||
|
payment_options=[PaymentOption(type="ln", link=invoice)],
|
||||||
|
message=receipt,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -89,12 +91,11 @@ async def build_order_with_payment(
|
||||||
shipping_zone = await get_zone(merchant_id, data.shipping_id)
|
shipping_zone = await get_zone(merchant_id, data.shipping_id)
|
||||||
assert shipping_zone, f"Shipping zone not found for order '{data.id}'"
|
assert shipping_zone, f"Shipping zone not found for order '{data.id}'"
|
||||||
|
|
||||||
|
assert shipping_zone.id
|
||||||
product_cost_sat, shipping_cost_sat = await data.costs_in_sats(
|
product_cost_sat, shipping_cost_sat = await data.costs_in_sats(
|
||||||
products, shipping_zone.id, shipping_zone.cost
|
products, shipping_zone.id, shipping_zone.cost
|
||||||
)
|
)
|
||||||
receipt = data.receipt(
|
receipt = data.receipt(products, shipping_zone.id, shipping_zone.cost)
|
||||||
products, shipping_zone.id, shipping_zone.cost
|
|
||||||
)
|
|
||||||
|
|
||||||
wallet_id = await get_wallet_for_product(data.items[0].product_id)
|
wallet_id = await get_wallet_for_product(data.items[0].product_id)
|
||||||
assert wallet_id, "Missing wallet for order `{data.id}`"
|
assert wallet_id, "Missing wallet for order `{data.id}`"
|
||||||
|
|
@ -106,7 +107,7 @@ async def build_order_with_payment(
|
||||||
if not success:
|
if not success:
|
||||||
raise ValueError(message)
|
raise ValueError(message)
|
||||||
|
|
||||||
payment_hash, invoice = await create_invoice(
|
payment = await create_invoice(
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet_id,
|
||||||
amount=round(product_cost_sat + shipping_cost_sat),
|
amount=round(product_cost_sat + shipping_cost_sat),
|
||||||
memo=f"Order '{data.id}' for pubkey '{data.public_key}'",
|
memo=f"Order '{data.id}' for pubkey '{data.public_key}'",
|
||||||
|
|
@ -124,19 +125,21 @@ async def build_order_with_payment(
|
||||||
order = Order(
|
order = Order(
|
||||||
**data.dict(),
|
**data.dict(),
|
||||||
stall_id=products[0].stall_id,
|
stall_id=products[0].stall_id,
|
||||||
invoice_id=payment_hash,
|
invoice_id=payment.payment_hash,
|
||||||
total=product_cost_sat + shipping_cost_sat,
|
total=product_cost_sat + shipping_cost_sat,
|
||||||
extra=extra,
|
extra=extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
return order, invoice, receipt
|
return order, payment.bolt11, receipt
|
||||||
|
|
||||||
|
|
||||||
async def update_merchant_to_nostr(
|
async def update_merchant_to_nostr(
|
||||||
merchant: Merchant, delete_merchant=False
|
merchant: Merchant, delete_merchant=False
|
||||||
) -> Merchant:
|
) -> Merchant:
|
||||||
stalls = await get_stalls(merchant.id)
|
stalls = await get_stalls(merchant.id)
|
||||||
|
event: Optional[NostrEvent] = None
|
||||||
for stall in stalls:
|
for stall in stalls:
|
||||||
|
assert stall.id
|
||||||
products = await get_products(merchant.id, stall.id)
|
products = await get_products(merchant.id, stall.id)
|
||||||
for product in products:
|
for product in products:
|
||||||
event = await sign_and_send_to_nostr(merchant, product, delete_merchant)
|
event = await sign_and_send_to_nostr(merchant, product, delete_merchant)
|
||||||
|
|
@ -150,6 +153,7 @@ async def update_merchant_to_nostr(
|
||||||
if delete_merchant:
|
if delete_merchant:
|
||||||
# merchant profile updates not supported yet
|
# merchant profile updates not supported yet
|
||||||
event = await sign_and_send_to_nostr(merchant, merchant, delete_merchant)
|
event = await sign_and_send_to_nostr(merchant, merchant, delete_merchant)
|
||||||
|
assert event
|
||||||
merchant.config.event_id = event.id
|
merchant.config.event_id = event.id
|
||||||
return merchant
|
return merchant
|
||||||
|
|
||||||
|
|
@ -227,25 +231,29 @@ async def update_products_for_order(
|
||||||
return success, message
|
return success, message
|
||||||
|
|
||||||
for p in products:
|
for p in products:
|
||||||
|
assert p.id
|
||||||
product = await update_product_quantity(p.id, p.quantity)
|
product = await update_product_quantity(p.id, p.quantity)
|
||||||
|
assert product
|
||||||
event = await sign_and_send_to_nostr(merchant, product)
|
event = await sign_and_send_to_nostr(merchant, product)
|
||||||
product.event_id = event.id
|
product.event_id = event.id
|
||||||
await update_product(merchant.id, product)
|
await update_product(merchant.id, product)
|
||||||
|
|
||||||
return True, "ok"
|
return True, "ok"
|
||||||
|
|
||||||
async def autoreply_for_products_in_order(
|
|
||||||
merchant: Merchant, order: Order
|
async def autoreply_for_products_in_order(merchant: Merchant, order: Order):
|
||||||
) -> Tuple[bool, str]:
|
|
||||||
product_ids = [i.product_id for i in order.items]
|
product_ids = [i.product_id for i in order.items]
|
||||||
|
|
||||||
products = await get_products_by_ids(merchant.id, product_ids)
|
products = await get_products_by_ids(merchant.id, product_ids)
|
||||||
products_with_autoreply = [p for p in products if p.config.use_autoreply]
|
products_with_autoreply = [p for p in products if p.config.use_autoreply]
|
||||||
|
|
||||||
for p in products_with_autoreply:
|
for p in products_with_autoreply:
|
||||||
dm_content = p.config.autoreply_message
|
dm_content = p.config.autoreply_message or ""
|
||||||
await send_dm(
|
await send_dm(
|
||||||
merchant, order.public_key, DirectMessageType.PLAIN_TEXT.value, dm_content
|
merchant,
|
||||||
|
order.public_key,
|
||||||
|
DirectMessageType.PLAIN_TEXT.value,
|
||||||
|
dm_content,
|
||||||
)
|
)
|
||||||
await asyncio.sleep(1) # do not send all autoreplies at once
|
await asyncio.sleep(1) # do not send all autoreplies at once
|
||||||
|
|
||||||
|
|
@ -253,7 +261,7 @@ async def autoreply_for_products_in_order(
|
||||||
async def send_dm(
|
async def send_dm(
|
||||||
merchant: Merchant,
|
merchant: Merchant,
|
||||||
other_pubkey: str,
|
other_pubkey: str,
|
||||||
type: str,
|
type_: int,
|
||||||
dm_content: str,
|
dm_content: str,
|
||||||
):
|
):
|
||||||
dm_event = merchant.build_dm_event(dm_content, other_pubkey)
|
dm_event = merchant.build_dm_event(dm_content, other_pubkey)
|
||||||
|
|
@ -263,7 +271,7 @@ async def send_dm(
|
||||||
event_created_at=dm_event.created_at,
|
event_created_at=dm_event.created_at,
|
||||||
message=dm_content,
|
message=dm_content,
|
||||||
public_key=other_pubkey,
|
public_key=other_pubkey,
|
||||||
type=type,
|
type=type_,
|
||||||
)
|
)
|
||||||
dm_reply = await create_direct_message(merchant.id, dm)
|
dm_reply = await create_direct_message(merchant.id, dm)
|
||||||
|
|
||||||
|
|
@ -296,7 +304,8 @@ async def compute_products_new_quantity(
|
||||||
return (
|
return (
|
||||||
False,
|
False,
|
||||||
[],
|
[],
|
||||||
f"Quantity not sufficient for product: '{p.name}' ({p.id}). Required '{required_quantity}' but only have '{p.quantity}'.",
|
f"Quantity not sufficient for product: '{p.name}' ({p.id})."
|
||||||
|
f" Required '{required_quantity}' but only have '{p.quantity}'.",
|
||||||
)
|
)
|
||||||
|
|
||||||
p.quantity -= required_quantity
|
p.quantity -= required_quantity
|
||||||
|
|
@ -306,9 +315,9 @@ async def compute_products_new_quantity(
|
||||||
|
|
||||||
async def process_nostr_message(msg: str):
|
async def process_nostr_message(msg: str):
|
||||||
try:
|
try:
|
||||||
type, *rest = json.loads(msg)
|
type_, *rest = json.loads(msg)
|
||||||
|
|
||||||
if type.upper() == "EVENT":
|
if type_.upper() == "EVENT":
|
||||||
_, event = rest
|
_, event = rest
|
||||||
event = NostrEvent(**event)
|
event = NostrEvent(**event)
|
||||||
if event.kind == 0:
|
if event.kind == 0:
|
||||||
|
|
@ -328,11 +337,11 @@ async def process_nostr_message(msg: str):
|
||||||
async def create_or_update_order_from_dm(
|
async def create_or_update_order_from_dm(
|
||||||
merchant_id: str, merchant_pubkey: str, dm: DirectMessage
|
merchant_id: str, merchant_pubkey: str, dm: DirectMessage
|
||||||
):
|
):
|
||||||
type, json_data = PartialDirectMessage.parse_message(dm.message)
|
type_, json_data = PartialDirectMessage.parse_message(dm.message)
|
||||||
if "id" not in json_data:
|
if not json_data or "id" not in json_data:
|
||||||
return
|
return
|
||||||
|
|
||||||
if type == DirectMessageType.CUSTOMER_ORDER:
|
if type_ == DirectMessageType.CUSTOMER_ORDER:
|
||||||
order = await extract_customer_order_from_dm(
|
order = await extract_customer_order_from_dm(
|
||||||
merchant_id, merchant_pubkey, dm, json_data
|
merchant_id, merchant_pubkey, dm, json_data
|
||||||
)
|
)
|
||||||
|
|
@ -348,7 +357,7 @@ async def create_or_update_order_from_dm(
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if type == DirectMessageType.PAYMENT_REQUEST:
|
if type_ == DirectMessageType.PAYMENT_REQUEST:
|
||||||
payment_request = PaymentRequest(**json_data)
|
payment_request = PaymentRequest(**json_data)
|
||||||
pr = next(
|
pr = next(
|
||||||
(o.link for o in payment_request.payment_options if o.type == "ln"), None
|
(o.link for o in payment_request.payment_options if o.type == "ln"), None
|
||||||
|
|
@ -356,14 +365,15 @@ async def create_or_update_order_from_dm(
|
||||||
if not pr:
|
if not pr:
|
||||||
return
|
return
|
||||||
invoice = decode(pr)
|
invoice = decode(pr)
|
||||||
|
total = invoice.amount_msat / 1000 if invoice.amount_msat else 0
|
||||||
await update_order(
|
await update_order(
|
||||||
merchant_id,
|
merchant_id,
|
||||||
payment_request.id,
|
payment_request.id,
|
||||||
**{"total": invoice.amount_msat / 1000, "invoice_id": invoice.payment_hash},
|
**{"total": total, "invoice_id": invoice.payment_hash},
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if type == DirectMessageType.ORDER_PAID_OR_SHIPPED:
|
if type_ == DirectMessageType.ORDER_PAID_OR_SHIPPED:
|
||||||
order_update = OrderStatusUpdate(**json_data)
|
order_update = OrderStatusUpdate(**json_data)
|
||||||
if order_update.paid:
|
if order_update.paid:
|
||||||
await update_order_paid_status(order_update.id, True)
|
await update_order_paid_status(order_update.id, True)
|
||||||
|
|
@ -380,16 +390,18 @@ async def extract_customer_order_from_dm(
|
||||||
)
|
)
|
||||||
extra = await OrderExtra.from_products(products)
|
extra = await OrderExtra.from_products(products)
|
||||||
order = Order(
|
order = Order(
|
||||||
id=json_data.get("id"),
|
id=str(json_data.get("id")),
|
||||||
event_id=dm.event_id,
|
event_id=dm.event_id,
|
||||||
event_created_at=dm.event_created_at,
|
event_created_at=dm.event_created_at,
|
||||||
public_key=dm.public_key,
|
public_key=dm.public_key,
|
||||||
merchant_public_key=merchant_pubkey,
|
merchant_public_key=merchant_pubkey,
|
||||||
shipping_id=json_data.get("shipping_id", "None"),
|
shipping_id=json_data.get("shipping_id", "None"),
|
||||||
items=order_items,
|
items=order_items,
|
||||||
contact=OrderContact(**json_data.get("contact"))
|
contact=(
|
||||||
|
OrderContact(**json_data.get("contact", {}))
|
||||||
if json_data.get("contact")
|
if json_data.get("contact")
|
||||||
else None,
|
else None
|
||||||
|
),
|
||||||
address=json_data.get("address"),
|
address=json_data.get("address"),
|
||||||
stall_id=products[0].stall_id if len(products) else "None",
|
stall_id=products[0].stall_id if len(products) else "None",
|
||||||
invoice_id="None",
|
invoice_id="None",
|
||||||
|
|
@ -406,12 +418,9 @@ async def _handle_nip04_message(event: NostrEvent):
|
||||||
|
|
||||||
if not merchant:
|
if not merchant:
|
||||||
p_tags = event.tag_values("p")
|
p_tags = event.tag_values("p")
|
||||||
merchant_public_key = p_tags[0] if len(p_tags) else None
|
if len(p_tags) and p_tags[0]:
|
||||||
merchant = (
|
merchant_public_key = p_tags[0]
|
||||||
await get_merchant_by_pubkey(merchant_public_key)
|
merchant = await get_merchant_by_pubkey(merchant_public_key)
|
||||||
if merchant_public_key
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
assert merchant, f"Merchant not found for public key '{merchant_public_key}'"
|
assert merchant, f"Merchant not found for public key '{merchant_public_key}'"
|
||||||
|
|
||||||
|
|
@ -461,21 +470,21 @@ async def _handle_outgoing_dms(
|
||||||
event: NostrEvent, merchant: Merchant, clear_text_msg: str
|
event: NostrEvent, merchant: Merchant, clear_text_msg: str
|
||||||
):
|
):
|
||||||
sent_to = event.tag_values("p")
|
sent_to = event.tag_values("p")
|
||||||
type, _ = PartialDirectMessage.parse_message(clear_text_msg)
|
type_, _ = PartialDirectMessage.parse_message(clear_text_msg)
|
||||||
if len(sent_to) != 0:
|
if len(sent_to) != 0:
|
||||||
dm = PartialDirectMessage(
|
dm = PartialDirectMessage(
|
||||||
event_id=event.id,
|
event_id=event.id,
|
||||||
event_created_at=event.created_at,
|
event_created_at=event.created_at,
|
||||||
message=clear_text_msg,
|
message=clear_text_msg,
|
||||||
public_key=sent_to[0],
|
public_key=sent_to[0],
|
||||||
type=type.value,
|
type=type_.value,
|
||||||
)
|
)
|
||||||
await create_direct_message(merchant.id, dm)
|
await create_direct_message(merchant.id, dm)
|
||||||
|
|
||||||
|
|
||||||
async def _handle_incoming_structured_dm(
|
async def _handle_incoming_structured_dm(
|
||||||
merchant: Merchant, dm: DirectMessage, json_data: dict
|
merchant: Merchant, dm: DirectMessage, json_data: dict
|
||||||
) -> Tuple[DirectMessageType, str]:
|
) -> Tuple[DirectMessageType, Optional[str]]:
|
||||||
try:
|
try:
|
||||||
if dm.type == DirectMessageType.CUSTOMER_ORDER.value and merchant.config.active:
|
if dm.type == DirectMessageType.CUSTOMER_ORDER.value and merchant.config.active:
|
||||||
json_resp = await _handle_new_order(
|
json_resp = await _handle_new_order(
|
||||||
|
|
@ -487,7 +496,7 @@ async def _handle_incoming_structured_dm(
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
|
|
||||||
return None, None
|
return DirectMessageType.PLAIN_TEXT, None
|
||||||
|
|
||||||
|
|
||||||
async def _persist_dm(
|
async def _persist_dm(
|
||||||
|
|
@ -570,9 +579,13 @@ async def _handle_new_order(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(e)
|
logger.debug(e)
|
||||||
payment_req = await create_new_failed_order(
|
payment_req = await create_new_failed_order(
|
||||||
merchant_id, merchant_public_key, dm, json_data, "Order received, but cannot be processed. Please contact merchant."
|
merchant_id,
|
||||||
|
merchant_public_key,
|
||||||
|
dm,
|
||||||
|
json_data,
|
||||||
|
"Order received, but cannot be processed. Please contact merchant.",
|
||||||
)
|
)
|
||||||
|
assert payment_req
|
||||||
response = {
|
response = {
|
||||||
"type": DirectMessageType.PAYMENT_REQUEST.value,
|
"type": DirectMessageType.PAYMENT_REQUEST.value,
|
||||||
**payment_req.dict(),
|
**payment_req.dict(),
|
||||||
|
|
@ -594,12 +607,14 @@ async def create_new_failed_order(
|
||||||
await create_order(merchant_id, order)
|
await create_order(merchant_id, order)
|
||||||
return PaymentRequest(id=order.id, message=fail_message, payment_options=[])
|
return PaymentRequest(id=order.id, message=fail_message, payment_options=[])
|
||||||
|
|
||||||
|
|
||||||
async def resubscribe_to_all_merchants():
|
async def resubscribe_to_all_merchants():
|
||||||
await nostr_client.unsubscribe_merchants()
|
await nostr_client.unsubscribe_merchants()
|
||||||
# give some time for the message to propagate
|
# give some time for the message to propagate
|
||||||
asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
await subscribe_to_all_merchants()
|
await subscribe_to_all_merchants()
|
||||||
|
|
||||||
|
|
||||||
async def subscribe_to_all_merchants():
|
async def subscribe_to_all_merchants():
|
||||||
ids = await get_merchants_ids_with_pubkeys()
|
ids = await get_merchants_ids_with_pubkeys()
|
||||||
public_keys = [pk for _, pk in ids]
|
public_keys = [pk for _, pk in ids]
|
||||||
|
|
@ -608,7 +623,9 @@ async def subscribe_to_all_merchants():
|
||||||
last_stall_time = await get_last_stall_update_time()
|
last_stall_time = await get_last_stall_update_time()
|
||||||
last_prod_time = await get_last_product_update_time()
|
last_prod_time = await get_last_product_update_time()
|
||||||
|
|
||||||
await nostr_client.subscribe_merchants(public_keys, last_dm_time, last_stall_time, last_prod_time, 0)
|
await nostr_client.subscribe_merchants(
|
||||||
|
public_keys, last_dm_time, last_stall_time, last_prod_time, 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _handle_new_customer(event: NostrEvent, merchant: Merchant):
|
async def _handle_new_customer(event: NostrEvent, merchant: Merchant):
|
||||||
|
|
|
||||||
170
static/components/direct-messages.js
Normal file
170
static/components/direct-messages.js
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
window.app.component('direct-messages', {
|
||||||
|
name: 'direct-messages',
|
||||||
|
props: ['active-chat-customer', 'merchant-id', 'adminkey', 'inkey'],
|
||||||
|
template: '#direct-messages',
|
||||||
|
delimiters: ['${', '}'],
|
||||||
|
watch: {
|
||||||
|
activeChatCustomer: async function (n) {
|
||||||
|
this.activePublicKey = n
|
||||||
|
},
|
||||||
|
activePublicKey: async function (n) {
|
||||||
|
await this.getDirectMessages(n)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
messagesAsJson: function () {
|
||||||
|
return this.messages.map(m => {
|
||||||
|
const dateFrom = moment(m.event_created_at * 1000).fromNow()
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(m.message)
|
||||||
|
return {
|
||||||
|
isJson: message.type >= 0,
|
||||||
|
dateFrom,
|
||||||
|
...m,
|
||||||
|
message
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
isJson: false,
|
||||||
|
dateFrom,
|
||||||
|
...m,
|
||||||
|
message: m.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
customers: [],
|
||||||
|
unreadMessages: 0,
|
||||||
|
activePublicKey: null,
|
||||||
|
messages: [],
|
||||||
|
newMessage: '',
|
||||||
|
showAddPublicKey: false,
|
||||||
|
newPublicKey: null,
|
||||||
|
showRawMessage: false,
|
||||||
|
rawMessage: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sendMessage: async function () {},
|
||||||
|
buildCustomerLabel: function (c) {
|
||||||
|
let label = `${c.profile.name || 'unknown'} ${c.profile.about || ''}`
|
||||||
|
if (c.unread_messages) {
|
||||||
|
label += `[new: ${c.unread_messages}]`
|
||||||
|
}
|
||||||
|
label += ` (${c.public_key.slice(0, 16)}...${c.public_key.slice(
|
||||||
|
c.public_key.length - 16
|
||||||
|
)}`
|
||||||
|
return label
|
||||||
|
},
|
||||||
|
getDirectMessages: async function (pubkey) {
|
||||||
|
if (!pubkey) {
|
||||||
|
this.messages = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/message/' + pubkey,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
this.messages = data
|
||||||
|
|
||||||
|
this.focusOnChatBox(this.messages.length - 1)
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getCustomers: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/customer',
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
this.customers = data
|
||||||
|
this.unreadMessages = data.filter(c => c.unread_messages).length
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sendDirectMesage: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/nostrmarket/api/v1/message',
|
||||||
|
this.adminkey,
|
||||||
|
{
|
||||||
|
message: this.newMessage,
|
||||||
|
public_key: this.activePublicKey
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.messages = this.messages.concat([data])
|
||||||
|
this.newMessage = ''
|
||||||
|
this.focusOnChatBox(this.messages.length - 1)
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addPublicKey: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/nostrmarket/api/v1/customer',
|
||||||
|
this.adminkey,
|
||||||
|
{
|
||||||
|
public_key: this.newPublicKey,
|
||||||
|
merchant_id: this.merchantId,
|
||||||
|
unread_messages: 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.newPublicKey = null
|
||||||
|
this.activePublicKey = data.public_key
|
||||||
|
await this.selectActiveCustomer()
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
} finally {
|
||||||
|
this.showAddPublicKey = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleNewMessage: async function (data) {
|
||||||
|
if (data.customerPubkey === this.activePublicKey) {
|
||||||
|
this.messages.push(data.dm)
|
||||||
|
this.focusOnChatBox(this.messages.length - 1)
|
||||||
|
// focus back on input box
|
||||||
|
}
|
||||||
|
this.getCustomersDebounced()
|
||||||
|
},
|
||||||
|
showOrderDetails: function (orderId, eventId) {
|
||||||
|
this.$emit('order-selected', {orderId, eventId})
|
||||||
|
},
|
||||||
|
showClientOrders: function () {
|
||||||
|
this.$emit('customer-selected', this.activePublicKey)
|
||||||
|
},
|
||||||
|
selectActiveCustomer: async function () {
|
||||||
|
await this.getDirectMessages(this.activePublicKey)
|
||||||
|
await this.getCustomers()
|
||||||
|
},
|
||||||
|
showMessageRawData: function (index) {
|
||||||
|
this.rawMessage = this.messages[index]?.message
|
||||||
|
this.showRawMessage = true
|
||||||
|
},
|
||||||
|
focusOnChatBox: function (index) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const lastChatBox = document.getElementsByClassName(
|
||||||
|
`chat-mesage-index-${index}`
|
||||||
|
)
|
||||||
|
if (lastChatBox && lastChatBox[0]) {
|
||||||
|
lastChatBox[0].scrollIntoView()
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {
|
||||||
|
await this.getCustomers()
|
||||||
|
this.getCustomersDebounced = _.debounce(this.getCustomers, 2000, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
<div>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-2">
|
|
||||||
<h6 class="text-subtitle1 q-my-none">Messages</h6>
|
|
||||||
</div>
|
|
||||||
<div class="col-4">
|
|
||||||
<q-badge v-if="unreadMessages" color="primary" outline><span v-text="unreadMessages"></span> new</q-badge>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<q-btn v-if="activePublicKey" @click="showClientOrders" unelevated outline class="float-right">Client
|
|
||||||
Orders</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<q-separator></q-separator>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-10">
|
|
||||||
<q-select v-model="activePublicKey"
|
|
||||||
:options="customers.map(c => ({label: buildCustomerLabel(c), value: c.public_key}))" label="Select Customer"
|
|
||||||
emit-value @input="selectActiveCustomer()">
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-2">
|
|
||||||
<q-btn label="Add" color="primary" class="float-right q-mt-md" @click="showAddPublicKey = true">
|
|
||||||
<q-tooltip>
|
|
||||||
Add a public key to chat with
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="chat-container" ref="chatCard">
|
|
||||||
<div class="chat-box">
|
|
||||||
<div class="chat-messages" style="height: 45vh">
|
|
||||||
<q-chat-message v-for="(dm, index) in messagesAsJson" :key="index" :name="dm.incoming ? 'customer': 'me'"
|
|
||||||
:text="dm.isJson ? [] : [dm.message]" :sent="!dm.incoming"
|
|
||||||
:stamp="dm.dateFrom"
|
|
||||||
:bg-color="dm.incoming ? 'white' : 'light-green-2'" :class="'chat-mesage-index-'+index">
|
|
||||||
<div v-if="dm.isJson">
|
|
||||||
<div v-if="dm.message.type === 0">
|
|
||||||
<strong>New order:</strong>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="dm.message.type === 1">
|
|
||||||
<strong>Reply sent for order: </strong>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="dm.message.type === 2">
|
|
||||||
<q-badge v-if="dm.message.paid" color="green">Paid </q-badge>
|
|
||||||
<q-badge v-if="dm.message.shipped" color="green">Shipped </q-badge>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span v-text="dm.message.message"></span>
|
|
||||||
<q-badge color="orange">
|
|
||||||
<span v-text="dm.message.id" @click="showOrderDetails(dm.message.id, dm.event_id)" class="cursor-pointer"></span>
|
|
||||||
</q-badge>
|
|
||||||
</div>
|
|
||||||
<q-badge @click="showMessageRawData(index)" class="cursor-pointer">...</q-badge>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</q-chat-message>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-card-section>
|
|
||||||
<q-form @submit="sendDirectMesage" class="full-width chat-input">
|
|
||||||
<q-input ref="newMessage" v-model="newMessage" placeholder="Message" class="full-width" dense outlined>
|
|
||||||
<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>
|
|
||||||
<div>
|
|
||||||
<q-dialog v-model="showAddPublicKey" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
|
||||||
<q-form @submit="addPublicKey" class="q-gutter-md">
|
|
||||||
<q-input filled dense v-model.trim="newPublicKey" label="Public Key (hex or nsec)"></q-input>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn unelevated color="primary" :disable="!newPublicKey" type="submit">Add</q-btn>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<q-dialog v-model="showRawMessage" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
|
||||||
<q-input filled dense type="textarea" rows="20" v-model.trim="rawMessage" label="Raw Data"></q-input>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
async function directMessages(path) {
|
|
||||||
const template = await loadTemplateAsync(path)
|
|
||||||
Vue.component('direct-messages', {
|
|
||||||
name: 'direct-messages',
|
|
||||||
props: ['active-chat-customer', 'merchant-id', 'adminkey', 'inkey'],
|
|
||||||
template,
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
activeChatCustomer: async function (n) {
|
|
||||||
this.activePublicKey = n
|
|
||||||
},
|
|
||||||
activePublicKey: async function (n) {
|
|
||||||
await this.getDirectMessages(n)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
messagesAsJson: function () {
|
|
||||||
return this.messages.map(m => {
|
|
||||||
const dateFrom = moment(m.event_created_at * 1000).fromNow()
|
|
||||||
try {
|
|
||||||
const message = JSON.parse(m.message)
|
|
||||||
return {
|
|
||||||
isJson: message.type >= 0,
|
|
||||||
dateFrom,
|
|
||||||
...m,
|
|
||||||
message
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
isJson: false,
|
|
||||||
dateFrom,
|
|
||||||
...m,
|
|
||||||
message: m.message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
customers: [],
|
|
||||||
unreadMessages: 0,
|
|
||||||
activePublicKey: null,
|
|
||||||
messages: [],
|
|
||||||
newMessage: '',
|
|
||||||
showAddPublicKey: false,
|
|
||||||
newPublicKey: null,
|
|
||||||
showRawMessage: false,
|
|
||||||
rawMessage: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
sendMessage: async function () { },
|
|
||||||
buildCustomerLabel: function (c) {
|
|
||||||
let label = `${c.profile.name || 'unknown'} ${c.profile.about || ''}`
|
|
||||||
if (c.unread_messages) {
|
|
||||||
label += `[new: ${c.unread_messages}]`
|
|
||||||
}
|
|
||||||
label += ` (${c.public_key.slice(0, 16)}...${c.public_key.slice(
|
|
||||||
c.public_key.length - 16
|
|
||||||
)}`
|
|
||||||
return label
|
|
||||||
},
|
|
||||||
getDirectMessages: async function (pubkey) {
|
|
||||||
if (!pubkey) {
|
|
||||||
this.messages = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
'/nostrmarket/api/v1/message/' + pubkey,
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
this.messages = data
|
|
||||||
|
|
||||||
this.focusOnChatBox(this.messages.length - 1)
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getCustomers: async function () {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
'/nostrmarket/api/v1/customer',
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
this.customers = data
|
|
||||||
this.unreadMessages = data.filter(c => c.unread_messages).length
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
sendDirectMesage: async function () {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
'/nostrmarket/api/v1/message',
|
|
||||||
this.adminkey,
|
|
||||||
{
|
|
||||||
message: this.newMessage,
|
|
||||||
public_key: this.activePublicKey
|
|
||||||
}
|
|
||||||
)
|
|
||||||
this.messages = this.messages.concat([data])
|
|
||||||
this.newMessage = ''
|
|
||||||
this.focusOnChatBox(this.messages.length - 1)
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addPublicKey: async function () {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
'/nostrmarket/api/v1/customer',
|
|
||||||
this.adminkey,
|
|
||||||
{
|
|
||||||
public_key: this.newPublicKey,
|
|
||||||
merchant_id: this.merchantId,
|
|
||||||
unread_messages: 0
|
|
||||||
}
|
|
||||||
)
|
|
||||||
this.newPublicKey = null
|
|
||||||
this.activePublicKey = data.public_key
|
|
||||||
await this.selectActiveCustomer()
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
} finally {
|
|
||||||
this.showAddPublicKey = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleNewMessage: async function (data) {
|
|
||||||
if (data.customerPubkey === this.activePublicKey) {
|
|
||||||
this.messages.push(data.dm)
|
|
||||||
this.focusOnChatBox(this.messages.length - 1)
|
|
||||||
// focus back on input box
|
|
||||||
}
|
|
||||||
this.getCustomersDebounced()
|
|
||||||
},
|
|
||||||
showOrderDetails: function (orderId, eventId) {
|
|
||||||
this.$emit('order-selected', { orderId, eventId })
|
|
||||||
},
|
|
||||||
showClientOrders: function () {
|
|
||||||
this.$emit('customer-selected', this.activePublicKey)
|
|
||||||
},
|
|
||||||
selectActiveCustomer: async function () {
|
|
||||||
await this.getDirectMessages(this.activePublicKey)
|
|
||||||
await this.getCustomers()
|
|
||||||
},
|
|
||||||
showMessageRawData: function (index) {
|
|
||||||
this.rawMessage = this.messages[index]?.message
|
|
||||||
this.showRawMessage = true
|
|
||||||
},
|
|
||||||
focusOnChatBox: function (index) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const lastChatBox = document.getElementsByClassName(
|
|
||||||
`chat-mesage-index-${index}`
|
|
||||||
)
|
|
||||||
if (lastChatBox && lastChatBox[0]) {
|
|
||||||
lastChatBox[0].scrollIntoView()
|
|
||||||
}
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created: async function () {
|
|
||||||
await this.getCustomers()
|
|
||||||
this.getCustomersDebounced = _.debounce(this.getCustomers, 2000, false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
22
static/components/key-pair.js
Normal file
22
static/components/key-pair.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
window.app.component('key-pair', {
|
||||||
|
name: 'key-pair',
|
||||||
|
template: '#key-pair',
|
||||||
|
delimiters: ['${', '}'],
|
||||||
|
props: ['public-key', 'private-key'],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
showPrivateKey: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
copyText: function (text, message, position) {
|
||||||
|
var notify = this.$q.notify
|
||||||
|
Quasar.copyToClipboard(text).then(function () {
|
||||||
|
notify({
|
||||||
|
message: message || 'Copied to clipboard!',
|
||||||
|
position: position || 'bottom'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
async function keyPair(path) {
|
|
||||||
const template = await loadTemplateAsync(path)
|
|
||||||
Vue.component('key-pair', {
|
|
||||||
name: 'key-pair',
|
|
||||||
template,
|
|
||||||
|
|
||||||
props: ['public-key', 'private-key'],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
showPrivateKey: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
copyText: function (text, message, position) {
|
|
||||||
var notify = this.$q.notify
|
|
||||||
Quasar.utils.copyToClipboard(text).then(function () {
|
|
||||||
notify({
|
|
||||||
message: message || 'Copied to clipboard!',
|
|
||||||
position: position || 'bottom'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
102
static/components/merchant-details.js
Normal file
102
static/components/merchant-details.js
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
window.app.component('merchant-details', {
|
||||||
|
name: 'merchant-details',
|
||||||
|
template: '#merchant-details',
|
||||||
|
props: ['merchant-id', 'adminkey', 'inkey', 'showKeys'],
|
||||||
|
delimiters: ['${', '}'],
|
||||||
|
data: function () {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleShowKeys: async function () {
|
||||||
|
this.$emit('toggle-show-keys')
|
||||||
|
},
|
||||||
|
|
||||||
|
republishMerchantData: async function () {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
`/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Merchant data republished to Nostr',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requeryMerchantData: async function () {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Merchant data refreshed from Nostr',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteMerchantTables: function () {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog(
|
||||||
|
`
|
||||||
|
Stalls, products and orders will be deleted also!
|
||||||
|
Are you sure you want to delete this merchant?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.onOk(async () => {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
'/nostrmarket/api/v1/merchant/' + this.merchantId,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.$emit('merchant-deleted', this.merchantId)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Merchant Deleted',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteMerchantFromNostr: function () {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog(
|
||||||
|
`
|
||||||
|
Do you want to remove the merchant from Nostr?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.onOk(async () => {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
`/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Merchant Deleted from Nostr',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {}
|
||||||
|
})
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
async function merchantDetails(path) {
|
|
||||||
const template = await loadTemplateAsync(path)
|
|
||||||
Vue.component('merchant-details', {
|
|
||||||
name: 'merchant-details',
|
|
||||||
props: ['merchant-id', 'adminkey', 'inkey','showKeys'],
|
|
||||||
template,
|
|
||||||
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleShowKeys: async function () {
|
|
||||||
this.$emit('toggle-show-keys')
|
|
||||||
},
|
|
||||||
|
|
||||||
republishMerchantData: async function () {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'PUT',
|
|
||||||
`/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
|
|
||||||
this.adminkey
|
|
||||||
)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Merchant data republished to Nostr',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
requeryMerchantData: async function () {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
|
|
||||||
this.adminkey
|
|
||||||
)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Merchant data refreshed from Nostr',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteMerchantTables: function () {
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog(
|
|
||||||
`
|
|
||||||
Stalls, products and orders will be deleted also!
|
|
||||||
Are you sure you want to delete this merchant?
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.onOk(async () => {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'DELETE',
|
|
||||||
'/nostrmarket/api/v1/merchant/' + this.merchantId,
|
|
||||||
this.adminkey
|
|
||||||
)
|
|
||||||
this.$emit('merchant-deleted', this.merchantId)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Merchant Deleted',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deleteMerchantFromNostr: function () {
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog(
|
|
||||||
`
|
|
||||||
Do you want to remove the merchant from Nostr?
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.onOk(async () => {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'DELETE',
|
|
||||||
`/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
|
|
||||||
this.adminkey
|
|
||||||
)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Merchant Deleted from Nostr',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created: async function () {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
406
static/components/order-list.js
Normal file
406
static/components/order-list.js
Normal file
|
|
@ -0,0 +1,406 @@
|
||||||
|
window.app.component('order-list', {
|
||||||
|
name: 'order-list',
|
||||||
|
props: ['stall-id', 'customer-pubkey-filter', 'adminkey', 'inkey'],
|
||||||
|
template: '#order-list',
|
||||||
|
delimiters: ['${', '}'],
|
||||||
|
watch: {
|
||||||
|
customerPubkeyFilter: async function (n) {
|
||||||
|
this.search.publicKey = n
|
||||||
|
this.search.isPaid = {label: 'All', id: null}
|
||||||
|
this.search.isShipped = {label: 'All', id: null}
|
||||||
|
await this.getOrders()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
orders: [],
|
||||||
|
stalls: [],
|
||||||
|
selectedOrder: null,
|
||||||
|
shippingMessage: '',
|
||||||
|
showShipDialog: false,
|
||||||
|
filter: '',
|
||||||
|
search: {
|
||||||
|
publicKey: null,
|
||||||
|
isPaid: {
|
||||||
|
label: 'All',
|
||||||
|
id: null
|
||||||
|
},
|
||||||
|
isShipped: {
|
||||||
|
label: 'All',
|
||||||
|
id: null
|
||||||
|
},
|
||||||
|
restoring: false
|
||||||
|
},
|
||||||
|
customers: [],
|
||||||
|
ternaryOptions: [
|
||||||
|
{
|
||||||
|
label: 'All',
|
||||||
|
id: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Yes',
|
||||||
|
id: 'true'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'No',
|
||||||
|
id: 'false'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
zoneOptions: [],
|
||||||
|
ordersTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
align: 'left',
|
||||||
|
label: '',
|
||||||
|
field: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Order ID',
|
||||||
|
field: 'id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'total',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Total Sats',
|
||||||
|
field: 'total'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fiat',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Total Fiat',
|
||||||
|
field: 'fiat'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'paid',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Paid',
|
||||||
|
field: 'paid'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shipped',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Shipped',
|
||||||
|
field: 'shipped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'public_key',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Customer',
|
||||||
|
field: 'pubkey'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'event_created_at',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Created At',
|
||||||
|
field: 'event_created_at'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
customerOptions: function () {
|
||||||
|
const options = this.customers.map(c => ({
|
||||||
|
label: this.buildCustomerLabel(c),
|
||||||
|
value: c.public_key
|
||||||
|
}))
|
||||||
|
options.unshift({label: 'All', value: null, id: null})
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toShortId: function (value) {
|
||||||
|
return value.substring(0, 5) + '...' + value.substring(value.length - 5)
|
||||||
|
},
|
||||||
|
formatDate: function (value) {
|
||||||
|
return Quasar.date.formatDate(new Date(value * 1000), 'YYYY-MM-DD HH:mm')
|
||||||
|
},
|
||||||
|
satBtc(val, showUnit = true) {
|
||||||
|
return satOrBtc(val, showUnit, true)
|
||||||
|
},
|
||||||
|
formatFiat(value, currency) {
|
||||||
|
return Math.trunc(value) + ' ' + currency
|
||||||
|
},
|
||||||
|
shortLabel(value = '') {
|
||||||
|
if (value.length <= 44) return value
|
||||||
|
return value.substring(0, 20) + '...'
|
||||||
|
},
|
||||||
|
productName: function (order, productId) {
|
||||||
|
product = order.extra.products.find(p => p.id === productId)
|
||||||
|
if (product) {
|
||||||
|
return product.name
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
productPrice: function (order, productId) {
|
||||||
|
product = order.extra.products.find(p => p.id === productId)
|
||||||
|
if (product) {
|
||||||
|
return `${product.price} ${order.extra.currency}`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
orderTotal: function (order) {
|
||||||
|
const productCost = order.items.reduce((t, item) => {
|
||||||
|
product = order.extra.products.find(p => p.id === item.product_id)
|
||||||
|
return t + item.quantity * product.price
|
||||||
|
}, 0)
|
||||||
|
return productCost + order.extra.shipping_cost
|
||||||
|
},
|
||||||
|
getOrders: async function () {
|
||||||
|
try {
|
||||||
|
const ordersPath = this.stallId
|
||||||
|
? `stall/order/${this.stallId}`
|
||||||
|
: 'order'
|
||||||
|
|
||||||
|
const query = []
|
||||||
|
if (this.search.publicKey) {
|
||||||
|
query.push(`pubkey=${this.search.publicKey}`)
|
||||||
|
}
|
||||||
|
if (this.search.isPaid.id) {
|
||||||
|
query.push(`paid=${this.search.isPaid.id}`)
|
||||||
|
}
|
||||||
|
if (this.search.isShipped.id) {
|
||||||
|
query.push(`shipped=${this.search.isShipped.id}`)
|
||||||
|
}
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/nostrmarket/api/v1/${ordersPath}?${query.join('&')}`,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
this.orders = data.map(s => ({...s, expanded: false}))
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getOrder: async function (orderId) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/nostrmarket/api/v1/order/${orderId}`,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
return {...data, expanded: false, isNew: true}
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restoreOrder: async function (eventId) {
|
||||||
|
console.log('### restoreOrder', eventId)
|
||||||
|
try {
|
||||||
|
this.search.restoring = true
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
`/nostrmarket/api/v1/order/restore/${eventId}`,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
await this.getOrders()
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Order restored!'
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
} finally {
|
||||||
|
this.search.restoring = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restoreOrders: async function () {
|
||||||
|
try {
|
||||||
|
this.search.restoring = true
|
||||||
|
await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
`/nostrmarket/api/v1/orders/restore`,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
await this.getOrders()
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Orders restored!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
} finally {
|
||||||
|
this.search.restoring = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reissueOrderInvoice: async function (order) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
`/nostrmarket/api/v1/order/reissue`,
|
||||||
|
this.adminkey,
|
||||||
|
{
|
||||||
|
id: order.id,
|
||||||
|
shipping_id: order.shipping_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Order invoice reissued!'
|
||||||
|
})
|
||||||
|
data.expanded = order.expanded
|
||||||
|
|
||||||
|
const i = this.orders.map(o => o.id).indexOf(order.id)
|
||||||
|
if (i !== -1) {
|
||||||
|
this.orders[i] = {...this.orders[i], ...data}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateOrderShipped: async function () {
|
||||||
|
this.selectedOrder.shipped = !this.selectedOrder.shipped
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'PATCH',
|
||||||
|
`/nostrmarket/api/v1/order/${this.selectedOrder.id}`,
|
||||||
|
this.adminkey,
|
||||||
|
{
|
||||||
|
id: this.selectedOrder.id,
|
||||||
|
message: this.shippingMessage,
|
||||||
|
shipped: this.selectedOrder.shipped
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Order updated!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
this.showShipDialog = false
|
||||||
|
},
|
||||||
|
addOrder: async function (data) {
|
||||||
|
if (
|
||||||
|
!this.search.publicKey ||
|
||||||
|
this.search.publicKey === data.customerPubkey
|
||||||
|
) {
|
||||||
|
const orderData = JSON.parse(data.dm.message)
|
||||||
|
const i = this.orders.map(o => o.id).indexOf(orderData.id)
|
||||||
|
if (i === -1) {
|
||||||
|
const order = await this.getOrder(orderData.id)
|
||||||
|
this.orders.unshift(order)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderSelected: async function (orderId, eventId) {
|
||||||
|
const order = await this.getOrder(orderId)
|
||||||
|
if (!order) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog(
|
||||||
|
'Order could not be found. Do you want to restore it from this direct message?'
|
||||||
|
)
|
||||||
|
.onOk(async () => {
|
||||||
|
const restoredOrder = await this.restoreOrder(eventId)
|
||||||
|
console.log('### restoredOrder', restoredOrder)
|
||||||
|
if (restoredOrder) {
|
||||||
|
restoredOrder.expanded = true
|
||||||
|
restoredOrder.isNew = false
|
||||||
|
this.orders = [restoredOrder]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
order.expanded = true
|
||||||
|
order.isNew = false
|
||||||
|
this.orders = [order]
|
||||||
|
},
|
||||||
|
getZones: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/zone',
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
return data.map(z => ({
|
||||||
|
id: z.id,
|
||||||
|
value: z.id,
|
||||||
|
label: z.name
|
||||||
|
? `${z.name} (${z.countries.join(', ')})`
|
||||||
|
: z.countries.join(', ')
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
getStalls: async function (pending = false) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/nostrmarket/api/v1/stall?pending=${pending}`,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
return data.map(s => ({...s, expanded: false}))
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
getStallZones: function (stallId) {
|
||||||
|
const stall = this.stalls.find(s => s.id === stallId)
|
||||||
|
if (!stall) return []
|
||||||
|
|
||||||
|
return this.zoneOptions.filter(z =>
|
||||||
|
stall.shipping_zones.find(s => s.id === z.id)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
showShipOrderDialog: function (order) {
|
||||||
|
this.selectedOrder = order
|
||||||
|
this.shippingMessage = order.shipped
|
||||||
|
? 'The order has been shipped!'
|
||||||
|
: 'The order has NOT yet been shipped!'
|
||||||
|
|
||||||
|
// do not change the status yet
|
||||||
|
this.selectedOrder.shipped = !order.shipped
|
||||||
|
this.showShipDialog = true
|
||||||
|
},
|
||||||
|
customerSelected: function (customerPubkey) {
|
||||||
|
this.$emit('customer-selected', customerPubkey)
|
||||||
|
},
|
||||||
|
getCustomers: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/customer',
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
this.customers = data
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildCustomerLabel: function (c) {
|
||||||
|
let label = `${c.profile.name || 'unknown'} ${c.profile.about || ''}`
|
||||||
|
if (c.unread_messages) {
|
||||||
|
label += `[new: ${c.unread_messages}]`
|
||||||
|
}
|
||||||
|
label += ` (${c.public_key.slice(0, 16)}...${c.public_key.slice(
|
||||||
|
c.public_key.length - 16
|
||||||
|
)}`
|
||||||
|
return label
|
||||||
|
},
|
||||||
|
orderPaid: function (orderId) {
|
||||||
|
const order = this.orders.find(o => o.id === orderId)
|
||||||
|
if (order) {
|
||||||
|
order.paid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {
|
||||||
|
if (this.stallId) {
|
||||||
|
await this.getOrders()
|
||||||
|
}
|
||||||
|
await this.getCustomers()
|
||||||
|
this.zoneOptions = await this.getZones()
|
||||||
|
this.stalls = await this.getStalls()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
<div>
|
|
||||||
<div class="row q-mb-md">
|
|
||||||
<div class="col-md-4 col-sm-6 q-pr-lg">
|
|
||||||
<q-select v-model="search.publicKey" :options="customerOptions" label="Customer" emit-value class="text-wrap">
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 col-sm-6 q-pr-lg">
|
|
||||||
<q-select v-model="search.isPaid" :options="ternaryOptions" label="Paid" emit-value>
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 col-sm-6 q-pr-lg">
|
|
||||||
<q-select v-model="search.isShipped" :options="ternaryOptions" label="Shipped" emit-value>
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-sm-6">
|
|
||||||
|
|
||||||
<q-btn-dropdown @click="getOrders()" :disable="search.restoring" outline unelevated split
|
|
||||||
class="q-pt-md float-right" :label="search.restoring ? 'Restoring Orders...' : 'Load Orders'">
|
|
||||||
<q-spinner v-if="search.restoring" color="primary" size="2.55em" class="q-pt-md float-right"></q-spinner>
|
|
||||||
<q-item @click="restoreOrders" clickable v-close-popup>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Restore Orders</q-item-label>
|
|
||||||
<q-item-label caption>Restore previous orders from Nostr</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-btn-dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-md">
|
|
||||||
<div class="col">
|
|
||||||
<q-table flat dense :data="orders" row-key="id" :columns="ordersTable.columns"
|
|
||||||
:pagination.sync="ordersTable.pagination" :filter="filter">
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn size="sm" color="primary" round dense @click="props.row.expanded= !props.row.expanded"
|
|
||||||
:icon="props.row.expanded? 'remove' : 'add'" />
|
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td key="id" :props="props">
|
|
||||||
{{toShortId(props.row.id)}}
|
|
||||||
<q-badge v-if="props.row.isNew" color="orange">new</q-badge></q-td>
|
|
||||||
<q-td key="total" :props="props">
|
|
||||||
{{satBtc(props.row.total)}}
|
|
||||||
</q-td>
|
|
||||||
<q-td key="fiat" :props="props">
|
|
||||||
<span v-if="props.row.extra.currency !== 'sat'">
|
|
||||||
{{orderTotal(props.row)}} {{props.row.extra.currency}}
|
|
||||||
</span>
|
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td key="paid" :props="props">
|
|
||||||
<q-checkbox v-model="props.row.paid" :label="props.row.paid ? 'Yes' : 'No'" disable readonly
|
|
||||||
size="sm"></q-checkbox>
|
|
||||||
</q-td>
|
|
||||||
<q-td key="shipped" :props="props">
|
|
||||||
<q-checkbox v-model="props.row.shipped" @input="showShipOrderDialog(props.row)"
|
|
||||||
:label="props.row.shipped ? 'Yes' : 'No'" size="sm"></q-checkbox>
|
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td key="public_key" :props="props">
|
|
||||||
<span @click="customerSelected(props.row.public_key)" class="cursor-pointer">
|
|
||||||
{{toShortId(props.row.public_key)}}
|
|
||||||
</span>
|
|
||||||
</q-td>
|
|
||||||
<q-td key="event_created_at" :props="props">
|
|
||||||
{{formatDate(props.row.event_created_at)}}
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
<q-tr v-if="props.row.expanded" :props="props">
|
|
||||||
<q-td colspan="100%">
|
|
||||||
<div class="row items-center no-wrap">
|
|
||||||
<div class="col-3 q-pr-lg">Products:</div>
|
|
||||||
<div class="col-8">
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-1"><strong>Quantity</strong></div>
|
|
||||||
<div class="col-1"></div>
|
|
||||||
<div class="col-4"><strong>Name</strong></div>
|
|
||||||
<div class="col-2"><strong>Price</strong></div>
|
|
||||||
<div class="col-4"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-1"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg"></div>
|
|
||||||
<div class="col-8">
|
|
||||||
<div v-for="item in props.row.items" class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-1">{{item.quantity}}</div>
|
|
||||||
<div class="col-1">x</div>
|
|
||||||
<div class="col-4">
|
|
||||||
<p :title="productName(props.row, item.product_id)">
|
|
||||||
{{shortLabel(productName(props.row, item.product_id))}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-2">
|
|
||||||
{{productPrice(props.row, item.product_id)}}
|
|
||||||
</div>
|
|
||||||
<div class="col-4"></div>
|
|
||||||
</div>
|
|
||||||
<div v-if="props.row.extra.shipping_cost" class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-1"></div>
|
|
||||||
<div class="col-1"></div>
|
|
||||||
<div class="col-4">Shipping Cost</div>
|
|
||||||
<div class="col-2">
|
|
||||||
{{props.row.extra.shipping_cost}} {{props.row.extra.currency}}
|
|
||||||
</div>
|
|
||||||
<div class="col-4"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-1"></div>
|
|
||||||
</div>
|
|
||||||
<div v-if="props.row.extra.currency !== 'sat'" class="row items-center no-wrap q-mb-md q-mt-md">
|
|
||||||
<div class="col-3 q-pr-lg">Exchange Rate (1 BTC):</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-input filled dense readonly disabled
|
|
||||||
:value="formatFiat(props.row.extra.btc_price, props.row.extra.currency)" type="text"></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
<div v-if="props.row.extra.fail_message" class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">Error:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-badge color="pink"><span v-text="props.row.extra.fail_message"></span></q-badge>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center no-wrap q-mb-md q-mt-md">
|
|
||||||
<div class="col-3 q-pr-lg">Order ID:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-input filled dense readonly disabled v-model.trim="props.row.id" type="text"></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">Customer Public Key:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-input filled dense readonly disabled v-model.trim="props.row.public_key" type="text"></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="props.row.address" class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">Address:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-input filled dense readonly disabled v-model.trim="props.row.address" type="text"></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="props.row.contact.phone" class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">Phone:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-input filled dense readonly disabled v-model.trim="props.row.contact.phone" type="text"></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
<div v-if="props.row.contact.email" class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">Email:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-input filled dense readonly disabled v-model.trim="props.row.contact.email" type="text"></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">Shipping Zone:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-select :options="getStallZones(props.row.stall_id)" filled dense emit-value
|
|
||||||
v-model.trim="props.row.shipping_id" label="Shipping Zones"></q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">Invoice ID:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-input filled dense readonly disabled v-model.trim="props.row.invoice_id" type="text"></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-3">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg"></div>
|
|
||||||
|
|
||||||
<div class="col-9">
|
|
||||||
<q-btn @click="reissueOrderInvoice(props.row)" unelevated color="primary" type="submit"
|
|
||||||
class="float-left" label="Reissue Invoice"></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-dialog v-model="showShipDialog" position="top">
|
|
||||||
<q-card v-if="selectedOrder" class="q-pa-lg q-pt-xl" style="width: 500px">
|
|
||||||
<q-form @submit="updateOrderShipped" class="q-gutter-md">
|
|
||||||
<q-input filled dense v-model.trim="shippingMessage" label="Shipping Message" type="textarea"
|
|
||||||
rows="4"></q-input>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn unelevated color="primary" type="submit"
|
|
||||||
:label="selectedOrder.shipped? 'Unship Order' : 'Ship Order'"></q-btn>
|
|
||||||
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,409 +0,0 @@
|
||||||
async function orderList(path) {
|
|
||||||
const template = await loadTemplateAsync(path)
|
|
||||||
Vue.component('order-list', {
|
|
||||||
name: 'order-list',
|
|
||||||
props: ['stall-id', 'customer-pubkey-filter', 'adminkey', 'inkey'],
|
|
||||||
template,
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
customerPubkeyFilter: async function (n) {
|
|
||||||
this.search.publicKey = n
|
|
||||||
this.search.isPaid = { label: 'All', id: null }
|
|
||||||
this.search.isShipped = { label: 'All', id: null }
|
|
||||||
await this.getOrders()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
orders: [],
|
|
||||||
stalls: [],
|
|
||||||
selectedOrder: null,
|
|
||||||
shippingMessage: '',
|
|
||||||
showShipDialog: false,
|
|
||||||
filter: '',
|
|
||||||
search: {
|
|
||||||
publicKey: null,
|
|
||||||
isPaid: {
|
|
||||||
label: 'All',
|
|
||||||
id: null
|
|
||||||
},
|
|
||||||
isShipped: {
|
|
||||||
label: 'All',
|
|
||||||
id: null
|
|
||||||
},
|
|
||||||
restoring: false
|
|
||||||
},
|
|
||||||
customers: [],
|
|
||||||
ternaryOptions: [
|
|
||||||
{
|
|
||||||
label: 'All',
|
|
||||||
id: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Yes',
|
|
||||||
id: 'true'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'No',
|
|
||||||
id: 'false'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
zoneOptions: [],
|
|
||||||
ordersTable: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
align: 'left',
|
|
||||||
label: '',
|
|
||||||
field: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'id',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Order ID',
|
|
||||||
field: 'id'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'total',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Total Sats',
|
|
||||||
field: 'total'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fiat',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Total Fiat',
|
|
||||||
field: 'fiat'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'paid',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Paid',
|
|
||||||
field: 'paid'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'shipped',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Shipped',
|
|
||||||
field: 'shipped'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'public_key',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Customer',
|
|
||||||
field: 'pubkey'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'event_created_at',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Created At',
|
|
||||||
field: 'event_created_at'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
customerOptions: function () {
|
|
||||||
const options = this.customers.map(c => ({ label: this.buildCustomerLabel(c), value: c.public_key }))
|
|
||||||
options.unshift({ label: 'All', value: null, id: null })
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toShortId: function (value) {
|
|
||||||
return value.substring(0, 5) + '...' + value.substring(value.length - 5)
|
|
||||||
},
|
|
||||||
formatDate: function (value) {
|
|
||||||
return Quasar.utils.date.formatDate(
|
|
||||||
new Date(value * 1000),
|
|
||||||
'YYYY-MM-DD HH:mm'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
satBtc(val, showUnit = true) {
|
|
||||||
return satOrBtc(val, showUnit, true)
|
|
||||||
},
|
|
||||||
formatFiat(value, currency) {
|
|
||||||
return Math.trunc(value) + ' ' + currency
|
|
||||||
},
|
|
||||||
shortLabel(value = ''){
|
|
||||||
if (value.length <= 44) return value
|
|
||||||
return value.substring(0, 20) + '...'
|
|
||||||
},
|
|
||||||
productName: function (order, productId) {
|
|
||||||
product = order.extra.products.find(p => p.id === productId)
|
|
||||||
if (product) {
|
|
||||||
return product.name
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
},
|
|
||||||
productPrice: function (order, productId) {
|
|
||||||
product = order.extra.products.find(p => p.id === productId)
|
|
||||||
if (product) {
|
|
||||||
return `${product.price} ${order.extra.currency}`
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
},
|
|
||||||
orderTotal: function (order) {
|
|
||||||
const productCost = order.items.reduce((t, item) => {
|
|
||||||
product = order.extra.products.find(p => p.id === item.product_id)
|
|
||||||
return t + item.quantity * product.price
|
|
||||||
}, 0)
|
|
||||||
return productCost + order.extra.shipping_cost
|
|
||||||
},
|
|
||||||
getOrders: async function () {
|
|
||||||
try {
|
|
||||||
const ordersPath = this.stallId
|
|
||||||
? `stall/order/${this.stallId}`
|
|
||||||
: 'order'
|
|
||||||
|
|
||||||
const query = []
|
|
||||||
if (this.search.publicKey) {
|
|
||||||
query.push(`pubkey=${this.search.publicKey}`)
|
|
||||||
}
|
|
||||||
if (this.search.isPaid.id) {
|
|
||||||
query.push(`paid=${this.search.isPaid.id}`)
|
|
||||||
}
|
|
||||||
if (this.search.isShipped.id) {
|
|
||||||
query.push(`shipped=${this.search.isShipped.id}`)
|
|
||||||
}
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/nostrmarket/api/v1/${ordersPath}?${query.join('&')}`,
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
this.orders = data.map(s => ({ ...s, expanded: false }))
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getOrder: async function (orderId) {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/nostrmarket/api/v1/order/${orderId}`,
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
return { ...data, expanded: false, isNew: true }
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
restoreOrder: async function (eventId) {
|
|
||||||
console.log('### restoreOrder', eventId)
|
|
||||||
try {
|
|
||||||
this.search.restoring = true
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'PUT',
|
|
||||||
`/nostrmarket/api/v1/order/restore/${eventId}`,
|
|
||||||
this.adminkey
|
|
||||||
)
|
|
||||||
await this.getOrders()
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Order restored!'
|
|
||||||
})
|
|
||||||
return data
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
} finally {
|
|
||||||
this.search.restoring = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
restoreOrders: async function () {
|
|
||||||
try {
|
|
||||||
this.search.restoring = true
|
|
||||||
await LNbits.api.request(
|
|
||||||
'PUT',
|
|
||||||
`/nostrmarket/api/v1/orders/restore`,
|
|
||||||
this.adminkey
|
|
||||||
)
|
|
||||||
await this.getOrders()
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Orders restored!'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
} finally {
|
|
||||||
this.search.restoring = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
reissueOrderInvoice: async function (order) {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'PUT',
|
|
||||||
`/nostrmarket/api/v1/order/reissue`,
|
|
||||||
this.adminkey,
|
|
||||||
{
|
|
||||||
id: order.id,
|
|
||||||
shipping_id: order.shipping_id
|
|
||||||
}
|
|
||||||
)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Order invoice reissued!'
|
|
||||||
})
|
|
||||||
data.expanded = order.expanded
|
|
||||||
|
|
||||||
const i = this.orders.map(o => o.id).indexOf(order.id)
|
|
||||||
if (i !== -1) {
|
|
||||||
this.orders[i] = { ...this.orders[i], ...data }
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateOrderShipped: async function () {
|
|
||||||
this.selectedOrder.shipped = !this.selectedOrder.shipped
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'PATCH',
|
|
||||||
`/nostrmarket/api/v1/order/${this.selectedOrder.id}`,
|
|
||||||
this.adminkey,
|
|
||||||
{
|
|
||||||
id: this.selectedOrder.id,
|
|
||||||
message: this.shippingMessage,
|
|
||||||
shipped: this.selectedOrder.shipped
|
|
||||||
}
|
|
||||||
)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Order updated!'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
this.showShipDialog = false
|
|
||||||
},
|
|
||||||
addOrder: async function (data) {
|
|
||||||
if (
|
|
||||||
!this.search.publicKey ||
|
|
||||||
this.search.publicKey === data.customerPubkey
|
|
||||||
) {
|
|
||||||
const orderData = JSON.parse(data.dm.message)
|
|
||||||
const i = this.orders.map(o => o.id).indexOf(orderData.id)
|
|
||||||
if (i === -1) {
|
|
||||||
const order = await this.getOrder(orderData.id)
|
|
||||||
this.orders.unshift(order)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
orderSelected: async function (orderId, eventId) {
|
|
||||||
const order = await this.getOrder(orderId)
|
|
||||||
if (!order) {
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog(
|
|
||||||
"Order could not be found. Do you want to restore it from this direct message?"
|
|
||||||
)
|
|
||||||
.onOk(async () => {
|
|
||||||
const restoredOrder = await this.restoreOrder(eventId)
|
|
||||||
console.log('### restoredOrder', restoredOrder)
|
|
||||||
if (restoredOrder) {
|
|
||||||
restoredOrder.expanded = true
|
|
||||||
restoredOrder.isNew = false
|
|
||||||
this.orders = [restoredOrder]
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
order.expanded = true
|
|
||||||
order.isNew = false
|
|
||||||
this.orders = [order]
|
|
||||||
},
|
|
||||||
getZones: async function () {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
'/nostrmarket/api/v1/zone',
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
return data.map(z => ({
|
|
||||||
id: z.id,
|
|
||||||
value: z.id,
|
|
||||||
label: z.name
|
|
||||||
? `${z.name} (${z.countries.join(', ')})`
|
|
||||||
: z.countries.join(', ')
|
|
||||||
}))
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
getStalls: async function (pending = false) {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/nostrmarket/api/v1/stall?pending=${pending}`,
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
return data.map(s => ({ ...s, expanded: false }))
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
getStallZones: function (stallId) {
|
|
||||||
const stall = this.stalls.find(s => s.id === stallId)
|
|
||||||
if (!stall) return []
|
|
||||||
|
|
||||||
return this.zoneOptions.filter(z => stall.shipping_zones.find(s => s.id === z.id))
|
|
||||||
},
|
|
||||||
showShipOrderDialog: function (order) {
|
|
||||||
this.selectedOrder = order
|
|
||||||
this.shippingMessage = order.shipped
|
|
||||||
? 'The order has been shipped!'
|
|
||||||
: 'The order has NOT yet been shipped!'
|
|
||||||
|
|
||||||
// do not change the status yet
|
|
||||||
this.selectedOrder.shipped = !order.shipped
|
|
||||||
this.showShipDialog = true
|
|
||||||
},
|
|
||||||
customerSelected: function (customerPubkey) {
|
|
||||||
this.$emit('customer-selected', customerPubkey)
|
|
||||||
},
|
|
||||||
getCustomers: async function () {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
'/nostrmarket/api/v1/customer',
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
this.customers = data
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
buildCustomerLabel: function (c) {
|
|
||||||
let label = `${c.profile.name || 'unknown'} ${c.profile.about || ''}`
|
|
||||||
if (c.unread_messages) {
|
|
||||||
label += `[new: ${c.unread_messages}]`
|
|
||||||
}
|
|
||||||
label += ` (${c.public_key.slice(0, 16)}...${c.public_key.slice(
|
|
||||||
c.public_key.length - 16
|
|
||||||
)}`
|
|
||||||
return label
|
|
||||||
},
|
|
||||||
orderPaid: function (orderId) {
|
|
||||||
const order = this.orders.find(o => o.id === orderId)
|
|
||||||
if (order) {
|
|
||||||
order.paid = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created: async function () {
|
|
||||||
if (this.stallId) {
|
|
||||||
await this.getOrders()
|
|
||||||
}
|
|
||||||
await this.getCustomers()
|
|
||||||
this.zoneOptions = await this.getZones()
|
|
||||||
this.stalls = await this.getStalls()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
183
static/components/shipping-zones.js
Normal file
183
static/components/shipping-zones.js
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
window.app.component('shipping-zones', {
|
||||||
|
name: 'shipping-zones',
|
||||||
|
props: ['adminkey', 'inkey'],
|
||||||
|
template: '#shipping-zones',
|
||||||
|
delimiters: ['${', '}'],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
zones: [],
|
||||||
|
zoneDialog: {
|
||||||
|
showDialog: false,
|
||||||
|
data: {
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
countries: [],
|
||||||
|
cost: 0,
|
||||||
|
currency: 'sat'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
currencies: [],
|
||||||
|
shippingZoneOptions: [
|
||||||
|
'Free (digital)',
|
||||||
|
'Flat rate',
|
||||||
|
'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',
|
||||||
|
'Romania',
|
||||||
|
'Russia',
|
||||||
|
'Saudi Arabia',
|
||||||
|
'Singapore',
|
||||||
|
'Spain',
|
||||||
|
'Sweden',
|
||||||
|
'Switzerland',
|
||||||
|
'Thailand',
|
||||||
|
'Turkey',
|
||||||
|
'Ukraine',
|
||||||
|
'United Kingdom**',
|
||||||
|
'United States***',
|
||||||
|
'Vietnam',
|
||||||
|
'China'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openZoneDialog: function (data) {
|
||||||
|
data = data || {
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
countries: [],
|
||||||
|
cost: 0,
|
||||||
|
currency: 'sat'
|
||||||
|
}
|
||||||
|
this.zoneDialog.data = data
|
||||||
|
|
||||||
|
this.zoneDialog.showDialog = true
|
||||||
|
},
|
||||||
|
createZone: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/nostrmarket/api/v1/zone',
|
||||||
|
this.adminkey,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
this.zones = data
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getZones: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/zone',
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
this.zones = data
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendZoneFormData: async function () {
|
||||||
|
this.zoneDialog.showDialog = false
|
||||||
|
if (this.zoneDialog.data.id) {
|
||||||
|
await this.updateShippingZone(this.zoneDialog.data)
|
||||||
|
} else {
|
||||||
|
await this.createShippingZone(this.zoneDialog.data)
|
||||||
|
}
|
||||||
|
await this.getZones()
|
||||||
|
},
|
||||||
|
createShippingZone: async function (newZone) {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/nostrmarket/api/v1/zone',
|
||||||
|
this.adminkey,
|
||||||
|
newZone
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Zone created!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateShippingZone: async function (updatedZone) {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'PATCH',
|
||||||
|
`/nostrmarket/api/v1/zone/${updatedZone.id}`,
|
||||||
|
this.adminkey,
|
||||||
|
updatedZone
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Zone updated!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteShippingZone: async function () {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
`/nostrmarket/api/v1/zone/${this.zoneDialog.data.id}`,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Zone deleted!'
|
||||||
|
})
|
||||||
|
await this.getZones()
|
||||||
|
this.zoneDialog.showDialog = false
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getCurrencies() {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/currencies',
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
|
||||||
|
this.currencies = ['sat', ...data]
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {
|
||||||
|
await this.getZones()
|
||||||
|
await this.getCurrencies()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
async function shippingZones(path) {
|
|
||||||
const template = await loadTemplateAsync(path)
|
|
||||||
Vue.component('shipping-zones', {
|
|
||||||
name: 'shipping-zones',
|
|
||||||
props: ['adminkey', 'inkey'],
|
|
||||||
template,
|
|
||||||
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
zones: [],
|
|
||||||
zoneDialog: {
|
|
||||||
showDialog: false,
|
|
||||||
data: {
|
|
||||||
id: null,
|
|
||||||
name: '',
|
|
||||||
countries: [],
|
|
||||||
cost: 0,
|
|
||||||
currency: 'sat'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
currencies: [],
|
|
||||||
shippingZoneOptions: [
|
|
||||||
'Free (digital)',
|
|
||||||
'Flat rate',
|
|
||||||
'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',
|
|
||||||
'Romania',
|
|
||||||
'Russia',
|
|
||||||
'Saudi Arabia',
|
|
||||||
'Singapore',
|
|
||||||
'Spain',
|
|
||||||
'Sweden',
|
|
||||||
'Switzerland',
|
|
||||||
'Thailand',
|
|
||||||
'Turkey',
|
|
||||||
'Ukraine',
|
|
||||||
'United Kingdom**',
|
|
||||||
'United States***',
|
|
||||||
'Vietnam',
|
|
||||||
'China'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
openZoneDialog: function (data) {
|
|
||||||
data = data || {
|
|
||||||
id: null,
|
|
||||||
name: '',
|
|
||||||
countries: [],
|
|
||||||
cost: 0,
|
|
||||||
currency: 'sat'
|
|
||||||
}
|
|
||||||
this.zoneDialog.data = data
|
|
||||||
|
|
||||||
this.zoneDialog.showDialog = true
|
|
||||||
},
|
|
||||||
createZone: async function () {
|
|
||||||
try {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
'/nostrmarket/api/v1/zone',
|
|
||||||
this.adminkey,
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
this.zones = data
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getZones: async function () {
|
|
||||||
try {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
'/nostrmarket/api/v1/zone',
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
this.zones = data
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sendZoneFormData: async function () {
|
|
||||||
this.zoneDialog.showDialog = false
|
|
||||||
if (this.zoneDialog.data.id) {
|
|
||||||
await this.updateShippingZone(this.zoneDialog.data)
|
|
||||||
} else {
|
|
||||||
await this.createShippingZone(this.zoneDialog.data)
|
|
||||||
}
|
|
||||||
await this.getZones()
|
|
||||||
},
|
|
||||||
createShippingZone: async function (newZone) {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
'/nostrmarket/api/v1/zone',
|
|
||||||
this.adminkey,
|
|
||||||
newZone
|
|
||||||
)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Zone created!'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateShippingZone: async function (updatedZone) {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'PATCH',
|
|
||||||
`/nostrmarket/api/v1/zone/${updatedZone.id}`,
|
|
||||||
this.adminkey,
|
|
||||||
updatedZone
|
|
||||||
)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Zone updated!'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteShippingZone: async function () {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'DELETE',
|
|
||||||
`/nostrmarket/api/v1/zone/${this.zoneDialog.data.id}`,
|
|
||||||
this.adminkey
|
|
||||||
)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Zone deleted!'
|
|
||||||
})
|
|
||||||
await this.getZones()
|
|
||||||
this.zoneDialog.showDialog = false
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async getCurrencies() {
|
|
||||||
try {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
'/nostrmarket/api/v1/currencies',
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
|
|
||||||
this.currencies = ['sat', ...data]
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created: async function () {
|
|
||||||
await this.getZones()
|
|
||||||
await this.getCurrencies()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
338
static/components/stall-details.js
Normal file
338
static/components/stall-details.js
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
window.app.component('stall-details', {
|
||||||
|
name: 'stall-details',
|
||||||
|
template: '#stall-details',
|
||||||
|
delimiters: ['${', '}'],
|
||||||
|
props: [
|
||||||
|
'stall-id',
|
||||||
|
'adminkey',
|
||||||
|
'inkey',
|
||||||
|
'wallet-options',
|
||||||
|
'zone-options',
|
||||||
|
'currencies'
|
||||||
|
],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
tab: 'products',
|
||||||
|
stall: null,
|
||||||
|
products: [],
|
||||||
|
pendingProducts: [],
|
||||||
|
productDialog: {
|
||||||
|
showDialog: false,
|
||||||
|
showRestore: false,
|
||||||
|
url: true,
|
||||||
|
data: null
|
||||||
|
},
|
||||||
|
productsFilter: '',
|
||||||
|
productsTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
align: 'left',
|
||||||
|
label: '',
|
||||||
|
field: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit',
|
||||||
|
align: 'left',
|
||||||
|
label: '',
|
||||||
|
field: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'activate',
|
||||||
|
align: 'left',
|
||||||
|
label: '',
|
||||||
|
field: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
align: 'left',
|
||||||
|
label: 'ID',
|
||||||
|
field: 'id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Name',
|
||||||
|
field: 'name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'price',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Price',
|
||||||
|
field: 'price'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quantity',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Quantity',
|
||||||
|
field: 'quantity'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredZoneOptions: function () {
|
||||||
|
if (!this.stall) return []
|
||||||
|
return this.zoneOptions.filter(z => z.currency === this.stall.currency)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
mapStall: function (stall) {
|
||||||
|
stall.shipping_zones.forEach(
|
||||||
|
z =>
|
||||||
|
(z.label = z.name
|
||||||
|
? `${z.name} (${z.countries.join(', ')})`
|
||||||
|
: z.countries.join(', '))
|
||||||
|
)
|
||||||
|
return stall
|
||||||
|
},
|
||||||
|
newEmtpyProductData: function () {
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
categories: [],
|
||||||
|
images: [],
|
||||||
|
image: null,
|
||||||
|
price: 0,
|
||||||
|
|
||||||
|
quantity: 0,
|
||||||
|
config: {
|
||||||
|
description: '',
|
||||||
|
use_autoreply: false,
|
||||||
|
autoreply_message: '',
|
||||||
|
shipping: (this.stall.shipping_zones || []).map(z => ({
|
||||||
|
id: z.id,
|
||||||
|
name: z.name,
|
||||||
|
cost: 0
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getStall: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/stall/' + this.stallId,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
this.stall = this.mapStall(data)
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateStall: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
'/nostrmarket/api/v1/stall/' + this.stallId,
|
||||||
|
this.adminkey,
|
||||||
|
this.stall
|
||||||
|
)
|
||||||
|
this.stall = this.mapStall(data)
|
||||||
|
this.$emit('stall-updated', this.stall)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Stall Updated',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteStall: function () {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog(
|
||||||
|
`
|
||||||
|
Products and orders will be deleted also!
|
||||||
|
Are you sure you want to delete this stall?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.onOk(async () => {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
'/nostrmarket/api/v1/stall/' + this.stallId,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.$emit('stall-deleted', this.stallId)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Stall Deleted',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addProductImage: function () {
|
||||||
|
if (!isValidImageUrl(this.productDialog.data.image)) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Not a valid image URL',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.productDialog.data.images.push(this.productDialog.data.image)
|
||||||
|
this.productDialog.data.image = null
|
||||||
|
},
|
||||||
|
removeProductImage: function (imageUrl) {
|
||||||
|
const index = this.productDialog.data.images.indexOf(imageUrl)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.productDialog.data.images.splice(index, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getProducts: async function (pending = false) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/nostrmarket/api/v1/stall/product/${this.stall.id}?pending=${pending}`,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendProductFormData: function () {
|
||||||
|
const data = {
|
||||||
|
stall_id: this.stall.id,
|
||||||
|
id: this.productDialog.data.id,
|
||||||
|
name: this.productDialog.data.name,
|
||||||
|
|
||||||
|
images: this.productDialog.data.images,
|
||||||
|
price: this.productDialog.data.price,
|
||||||
|
quantity: this.productDialog.data.quantity,
|
||||||
|
categories: this.productDialog.data.categories,
|
||||||
|
config: this.productDialog.data.config
|
||||||
|
}
|
||||||
|
this.productDialog.showDialog = false
|
||||||
|
if (this.productDialog.data.id) {
|
||||||
|
data.pending = false
|
||||||
|
this.updateProduct(data)
|
||||||
|
} else {
|
||||||
|
this.createProduct(data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateProduct: async function (product) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PATCH',
|
||||||
|
'/nostrmarket/api/v1/product/' + product.id,
|
||||||
|
this.adminkey,
|
||||||
|
product
|
||||||
|
)
|
||||||
|
const index = this.products.findIndex(r => r.id === product.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.products.splice(index, 1, data)
|
||||||
|
} else {
|
||||||
|
this.products.unshift(data)
|
||||||
|
}
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Product Updated',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createProduct: async function (payload) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/nostrmarket/api/v1/product',
|
||||||
|
this.adminkey,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
this.products.unshift(data)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Product Created',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
editProduct: async function (product) {
|
||||||
|
const emptyShipping = this.newEmtpyProductData().config.shipping
|
||||||
|
this.productDialog.data = {...product}
|
||||||
|
this.productDialog.data.config.shipping = emptyShipping.map(
|
||||||
|
shippingZone => {
|
||||||
|
const existingShippingCost = (product.config.shipping || []).find(
|
||||||
|
ps => ps.id === shippingZone.id
|
||||||
|
)
|
||||||
|
shippingZone.cost = existingShippingCost?.cost || 0
|
||||||
|
return shippingZone
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.productDialog.showDialog = true
|
||||||
|
},
|
||||||
|
deleteProduct: async function (productId) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to delete this product?')
|
||||||
|
.onOk(async () => {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
'/nostrmarket/api/v1/product/' + productId,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.products = _.reject(this.products, function (obj) {
|
||||||
|
return obj.id === productId
|
||||||
|
})
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Product deleted',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showNewProductDialog: async function (data) {
|
||||||
|
this.productDialog.data = data || this.newEmtpyProductData()
|
||||||
|
this.productDialog.showDialog = true
|
||||||
|
},
|
||||||
|
openSelectPendingProductDialog: async function () {
|
||||||
|
this.productDialog.showRestore = true
|
||||||
|
this.pendingProducts = await this.getProducts(true)
|
||||||
|
},
|
||||||
|
openRestoreProductDialog: async function (pendingProduct) {
|
||||||
|
pendingProduct.pending = true
|
||||||
|
await this.showNewProductDialog(pendingProduct)
|
||||||
|
},
|
||||||
|
restoreAllPendingProducts: async function () {
|
||||||
|
for (const p of this.pendingProducts) {
|
||||||
|
p.pending = false
|
||||||
|
await this.updateProduct(p)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
customerSelectedForOrder: function (customerPubkey) {
|
||||||
|
this.$emit('customer-selected-for-order', customerPubkey)
|
||||||
|
},
|
||||||
|
shortLabel(value = '') {
|
||||||
|
if (value.length <= 44) return value
|
||||||
|
return value.substring(0, 40) + '...'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {
|
||||||
|
await this.getStall()
|
||||||
|
this.products = await this.getProducts()
|
||||||
|
this.productDialog.data = this.newEmtpyProductData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
<div>
|
|
||||||
<q-tabs v-model="tab" no-caps class="bg-dark text-white shadow-2">
|
|
||||||
<q-tab name="info" label="Stall Info"></q-tab>
|
|
||||||
<q-tab name="products" label="Products"></q-tab>
|
|
||||||
<q-tab name="orders" label="Orders"></q-tab>
|
|
||||||
</q-tabs>
|
|
||||||
<q-tab-panels v-model="tab">
|
|
||||||
<q-tab-panel name="info">
|
|
||||||
<div v-if="stall">
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">ID:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-input filled dense readonly disabled v-model.trim="stall.id" type="text"></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">Name:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-input filled dense v-model.trim="stall.name" type="text"></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">Description:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-input filled dense v-model.trim="stall.config.description" type="textarea" rows="3"
|
|
||||||
label="Description"></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">Wallet:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-select filled dense emit-value v-model="stall.wallet" :options="walletOptions" label="Wallet *">
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">Currency:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-select filled dense v-model="stall.currency" type="text" label="Unit" :options="currencies"></q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">Shipping Zones:</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg">
|
|
||||||
<q-select :options="filteredZoneOptions" filled dense multiple v-model.trim="stall.shipping_zones"
|
|
||||||
label="Shipping Zones"></q-select>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center q-mt-xl">
|
|
||||||
<div class="col-6 q-pr-lg">
|
|
||||||
<q-btn outline unelevated class="float-left" color="primary" @click="updateStall()">Update Stall</q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<q-btn outline unelevated icon="cancel" class="float-right" @click="deleteStall()">Delete Stall</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="products">
|
|
||||||
<div v-if="stall">
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-3 q-pr-lg">
|
|
||||||
|
|
||||||
<q-btn-dropdown @click="showNewProductDialog()" outline unelevated split class="float-left" color="primary"
|
|
||||||
label="New Product">
|
|
||||||
<q-item @click="showNewProductDialog()" clickable v-close-popup>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>New Product</q-item-label>
|
|
||||||
<q-item-label caption>Create a new product</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item @click="openSelectPendingProductDialog" clickable v-close-popup>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Restore Product</q-item-label>
|
|
||||||
<q-item-label caption>Restore existing product from Nostr</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-btn-dropdown>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="col-6 col-sm-8 q-pr-lg"></div>
|
|
||||||
<div class="col-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col-12">
|
|
||||||
<q-table flat dense :data="products" row-key="id" :columns="productsTable.columns"
|
|
||||||
:pagination.sync="productsTable.pagination" :filter="productsFilter">
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn size="sm" color="grey" dense @click="deleteProduct(props.row.id)" icon="delete" />
|
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn size="sm" color="primary" dense @click="editProduct(props.row)" icon="edit" />
|
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-toggle
|
|
||||||
@input="updateProduct({ ...props.row, active: props.row.active })"
|
|
||||||
size="xs"
|
|
||||||
checked-icon="check"
|
|
||||||
v-model="props.row.active"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td key="id" :props="props"> {{props.row.id}} </q-td>
|
|
||||||
<q-td key="name" :props="props"> {{shortLabel(props.row.name)}} </q-td>
|
|
||||||
<q-td key="price" :props="props"> {{props.row.price}} </q-td>
|
|
||||||
<q-td key="quantity" :props="props">
|
|
||||||
{{props.row.quantity}}
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="orders">
|
|
||||||
<div v-if="stall">
|
|
||||||
<order-list :adminkey="adminkey" :inkey="inkey" :stall-id="stallId"
|
|
||||||
@customer-selected="customerSelectedForOrder"></order-list>
|
|
||||||
</div>
|
|
||||||
</q-tab-panel>
|
|
||||||
</q-tab-panels>
|
|
||||||
<q-dialog v-model="productDialog.showDialog" position="top">
|
|
||||||
<q-card v-if="stall && productDialog.data" class="q-pa-lg q-pt-xl" style="width: 500px">
|
|
||||||
<q-form @submit="sendProductFormData" class="q-gutter-md">
|
|
||||||
<q-input filled dense v-model.trim="productDialog.data.name" label="Name"></q-input>
|
|
||||||
|
|
||||||
<q-input filled dense v-model.trim="productDialog.data.config.description" label="Description"></q-input>
|
|
||||||
|
|
||||||
<div class="row q-mb-sm">
|
|
||||||
<div class="col">
|
|
||||||
<q-input filled dense v-model.number="productDialog.data.price" type="number"
|
|
||||||
:label="'Price (' + stall.currency + ') *'" :step="stall.currency != 'sat' ? '0.01' : '1'"
|
|
||||||
:mask="stall.currency != 'sat' ? '#.##' : '#'" fill-mask="0" reverse-fill-mask></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col q-ml-md">
|
|
||||||
<q-input filled dense v-model.number="productDialog.data.quantity" type="number" label="Quantity"></q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<q-expansion-item group="advanced" label="Categories"
|
|
||||||
caption="Add tags to producsts, make them easy to search.">
|
|
||||||
<div class="q-pl-sm q-pt-sm">
|
|
||||||
<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 (Hit Enter to add)" placeholder="crafts,robots,etc"></q-select>
|
|
||||||
</div>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-expansion-item group="advanced" label="Images" caption="Add images for product.">
|
|
||||||
<div class="q-pl-sm q-pt-sm">
|
|
||||||
<q-input filled dense v-model.trim="productDialog.data.image" @keydown.enter="addProductImage" type="url"
|
|
||||||
label="Image URL">
|
|
||||||
<q-btn @click="addProductImage" dense flat icon="add"></q-btn></q-input>
|
|
||||||
|
|
||||||
<q-chip v-for="imageUrl in productDialog.data.images" :key="imageUrl" removable
|
|
||||||
@remove="removeProductImage(imageUrl)" color="primary" text-color="white">
|
|
||||||
<span v-text="imageUrl.split('/').pop()"></span>
|
|
||||||
</q-chip>
|
|
||||||
</div>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
|
|
||||||
<q-expansion-item group="advanced" label="Custom Shipping Cost"
|
|
||||||
caption="Configure custom shipping costs for this product">
|
|
||||||
<div v-for="zone of productDialog.data.config.shipping" class="row q-mb-sm q-ml-lg q-mt-sm">
|
|
||||||
<div class="col">
|
|
||||||
<span v-text="zone.name"></span>
|
|
||||||
</div>
|
|
||||||
<div class="col q-pr-md">
|
|
||||||
<q-input v-model="zone.cost" filled dense type="number" label="Extra cost">
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item group="advanced" label="Autoreply" caption="Autoreply when paid">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row q-mb-sm">
|
|
||||||
<div class="col">
|
|
||||||
<q-checkbox v-model="productDialog.data.config.use_autoreply" dense
|
|
||||||
label="Send a direct message when paid" class="q-ml-sm" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-mb-sm q-ml-sm">
|
|
||||||
<div class="col">
|
|
||||||
<q-input v-model="productDialog.data.config.autoreply_message" filled dense type="textarea" rows="5"
|
|
||||||
label="Autoreply message" hint="It can include link to a digital asset">
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn v-if="productDialog.data.id" type="submit"
|
|
||||||
:label="productDialog.data.pending ? 'Restore Product' : 'Update Product'" unelevated
|
|
||||||
color="primary"></q-btn>
|
|
||||||
|
|
||||||
<q-btn v-else unelevated color="primary" :disable="!productDialog.data.price
|
|
||||||
|| !productDialog.data.name
|
|
||||||
|| !productDialog.data.quantity" type="submit">Create Product</q-btn>
|
|
||||||
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<q-dialog v-model="productDialog.showRestore" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
|
||||||
<div v-if="pendingProducts && pendingProducts.length" class="row q-mt-lg">
|
|
||||||
<q-item v-for="pendingProduct of pendingProducts" :key="pendingProduct.id" tag="label" class="full-width"
|
|
||||||
v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label><span v-text="pendingProduct.name"></span></q-item-label>
|
|
||||||
<q-item-label caption><span v-text="pendingProduct.config?.description"></span></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section class="q-pl-xl float-right">
|
|
||||||
<q-btn @click="openRestoreProductDialog(pendingProduct)" v-close-popup flat color="green"
|
|
||||||
class="q-ml-auto float-right">Restore</q-btn>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section class="float-right">
|
|
||||||
<q-btn @click="deleteProduct(pendingProduct.id)" v-close-popup color="red" class="q-ml-auto float-right"
|
|
||||||
icon="cancel"></q-btn>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
There are no products to be restored.
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn @click="restoreAllPendingProducts" v-close-popup flat color="green">Restore All</q-btn>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,334 +0,0 @@
|
||||||
async function stallDetails(path) {
|
|
||||||
const template = await loadTemplateAsync(path)
|
|
||||||
|
|
||||||
Vue.component('stall-details', {
|
|
||||||
name: 'stall-details',
|
|
||||||
template,
|
|
||||||
|
|
||||||
props: [
|
|
||||||
'stall-id',
|
|
||||||
'adminkey',
|
|
||||||
'inkey',
|
|
||||||
'wallet-options',
|
|
||||||
'zone-options',
|
|
||||||
'currencies'
|
|
||||||
],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
tab: 'products',
|
|
||||||
stall: null,
|
|
||||||
products: [],
|
|
||||||
pendingProducts: [],
|
|
||||||
productDialog: {
|
|
||||||
showDialog: false,
|
|
||||||
showRestore: false,
|
|
||||||
url: true,
|
|
||||||
data: null
|
|
||||||
},
|
|
||||||
productsFilter: '',
|
|
||||||
productsTable: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
align: 'left',
|
|
||||||
label: '',
|
|
||||||
field: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'edit',
|
|
||||||
align: 'left',
|
|
||||||
label: '',
|
|
||||||
field: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'activate',
|
|
||||||
align: 'left',
|
|
||||||
label: '',
|
|
||||||
field: ''
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'id',
|
|
||||||
align: 'left',
|
|
||||||
label: 'ID',
|
|
||||||
field: 'id'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Name',
|
|
||||||
field: 'name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'price',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Price',
|
|
||||||
field: 'price'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'quantity',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Quantity',
|
|
||||||
field: 'quantity'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
filteredZoneOptions: function () {
|
|
||||||
if (!this.stall) return []
|
|
||||||
return this.zoneOptions.filter(z => z.currency === this.stall.currency)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
mapStall: function (stall) {
|
|
||||||
stall.shipping_zones.forEach(
|
|
||||||
z =>
|
|
||||||
(z.label = z.name
|
|
||||||
? `${z.name} (${z.countries.join(', ')})`
|
|
||||||
: z.countries.join(', '))
|
|
||||||
)
|
|
||||||
return stall
|
|
||||||
},
|
|
||||||
newEmtpyProductData: function() {
|
|
||||||
return {
|
|
||||||
id: null,
|
|
||||||
name: '',
|
|
||||||
categories: [],
|
|
||||||
images: [],
|
|
||||||
image: null,
|
|
||||||
price: 0,
|
|
||||||
|
|
||||||
quantity: 0,
|
|
||||||
config: {
|
|
||||||
description: '',
|
|
||||||
use_autoreply: false,
|
|
||||||
autoreply_message: '',
|
|
||||||
shipping: (this.stall.shipping_zones || []).map(z => ({id: z.id, name: z.name, cost: 0}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getStall: async function () {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
'/nostrmarket/api/v1/stall/' + this.stallId,
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
this.stall = this.mapStall(data)
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateStall: async function () {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'PUT',
|
|
||||||
'/nostrmarket/api/v1/stall/' + this.stallId,
|
|
||||||
this.adminkey,
|
|
||||||
this.stall
|
|
||||||
)
|
|
||||||
this.stall = this.mapStall(data)
|
|
||||||
this.$emit('stall-updated', this.stall)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Stall Updated',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteStall: function () {
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog(
|
|
||||||
`
|
|
||||||
Products and orders will be deleted also!
|
|
||||||
Are you sure you want to delete this stall?
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.onOk(async () => {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'DELETE',
|
|
||||||
'/nostrmarket/api/v1/stall/' + this.stallId,
|
|
||||||
this.adminkey
|
|
||||||
)
|
|
||||||
this.$emit('stall-deleted', this.stallId)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Stall Deleted',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
addProductImage: function () {
|
|
||||||
if (!isValidImageUrl(this.productDialog.data.image)) {
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'warning',
|
|
||||||
message: 'Not a valid image URL',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.productDialog.data.images.push(this.productDialog.data.image)
|
|
||||||
this.productDialog.data.image = null
|
|
||||||
},
|
|
||||||
removeProductImage: function (imageUrl) {
|
|
||||||
const index = this.productDialog.data.images.indexOf(imageUrl)
|
|
||||||
if (index !== -1) {
|
|
||||||
this.productDialog.data.images.splice(index, 1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getProducts: async function (pending = false) {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/nostrmarket/api/v1/stall/product/${this.stall.id}?pending=${pending}`,
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
return data
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sendProductFormData: function () {
|
|
||||||
const data = {
|
|
||||||
stall_id: this.stall.id,
|
|
||||||
id: this.productDialog.data.id,
|
|
||||||
name: this.productDialog.data.name,
|
|
||||||
|
|
||||||
images: this.productDialog.data.images,
|
|
||||||
price: this.productDialog.data.price,
|
|
||||||
quantity: this.productDialog.data.quantity,
|
|
||||||
categories: this.productDialog.data.categories,
|
|
||||||
config: this.productDialog.data.config
|
|
||||||
}
|
|
||||||
this.productDialog.showDialog = false
|
|
||||||
if (this.productDialog.data.id) {
|
|
||||||
data.pending = false
|
|
||||||
this.updateProduct(data)
|
|
||||||
} else {
|
|
||||||
this.createProduct(data)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateProduct: async function (product) {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'PATCH',
|
|
||||||
'/nostrmarket/api/v1/product/' + product.id,
|
|
||||||
this.adminkey,
|
|
||||||
product
|
|
||||||
)
|
|
||||||
const index = this.products.findIndex(r => r.id === product.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
this.products.splice(index, 1, data)
|
|
||||||
} else {
|
|
||||||
this.products.unshift(data)
|
|
||||||
}
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Product Updated',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createProduct: async function (payload) {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
'/nostrmarket/api/v1/product',
|
|
||||||
this.adminkey,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
this.products.unshift(data)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Product Created',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
editProduct: async function (product) {
|
|
||||||
const emptyShipping = this.newEmtpyProductData().config.shipping
|
|
||||||
this.productDialog.data = { ...product }
|
|
||||||
this.productDialog.data.config.shipping = emptyShipping.map(shippingZone => {
|
|
||||||
const existingShippingCost = (product.config.shipping || []).find(ps => ps.id === shippingZone.id)
|
|
||||||
shippingZone.cost = existingShippingCost?.cost || 0
|
|
||||||
return shippingZone
|
|
||||||
})
|
|
||||||
|
|
||||||
this.productDialog.showDialog = true
|
|
||||||
},
|
|
||||||
deleteProduct: async function (productId) {
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog('Are you sure you want to delete this product?')
|
|
||||||
.onOk(async () => {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'DELETE',
|
|
||||||
'/nostrmarket/api/v1/product/' + productId,
|
|
||||||
this.adminkey
|
|
||||||
)
|
|
||||||
this.products = _.reject(this.products, function (obj) {
|
|
||||||
return obj.id === productId
|
|
||||||
})
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Product deleted',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
showNewProductDialog: async function (data) {
|
|
||||||
this.productDialog.data = data || this.newEmtpyProductData()
|
|
||||||
this.productDialog.showDialog = true
|
|
||||||
},
|
|
||||||
openSelectPendingProductDialog: async function () {
|
|
||||||
this.productDialog.showRestore = true
|
|
||||||
this.pendingProducts = await this.getProducts(true)
|
|
||||||
},
|
|
||||||
openRestoreProductDialog: async function (pendingProduct) {
|
|
||||||
pendingProduct.pending = true
|
|
||||||
await this.showNewProductDialog(pendingProduct)
|
|
||||||
},
|
|
||||||
restoreAllPendingProducts: async function () {
|
|
||||||
for (const p of this.pendingProducts){
|
|
||||||
p.pending = false
|
|
||||||
await this.updateProduct(p)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
customerSelectedForOrder: function (customerPubkey) {
|
|
||||||
this.$emit('customer-selected-for-order', customerPubkey)
|
|
||||||
},
|
|
||||||
shortLabel(value = ''){
|
|
||||||
if (value.length <= 44) return value
|
|
||||||
return value.substring(0, 40) + '...'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created: async function () {
|
|
||||||
await this.getStall()
|
|
||||||
this.products = await this.getProducts()
|
|
||||||
this.productDialog.data = this.newEmtpyProductData()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
262
static/components/stall-list.js
Normal file
262
static/components/stall-list.js
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
window.app.component('stall-list', {
|
||||||
|
name: 'stall-list',
|
||||||
|
template: '#stall-list',
|
||||||
|
delimiters: ['${', '}'],
|
||||||
|
props: [`adminkey`, 'inkey', 'wallet-options'],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
filter: '',
|
||||||
|
stalls: [],
|
||||||
|
pendingStalls: [],
|
||||||
|
currencies: [],
|
||||||
|
stallDialog: {
|
||||||
|
show: false,
|
||||||
|
showRestore: false,
|
||||||
|
data: {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
wallet: null,
|
||||||
|
currency: 'sat',
|
||||||
|
shippingZones: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
zoneOptions: [],
|
||||||
|
stallsTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
align: 'left',
|
||||||
|
label: '',
|
||||||
|
field: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Name',
|
||||||
|
field: 'id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'currency',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Currency',
|
||||||
|
field: 'currency'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Description',
|
||||||
|
field: 'description'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shippingZones',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Shipping Zones',
|
||||||
|
field: 'shippingZones'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredZoneOptions: function () {
|
||||||
|
return this.zoneOptions.filter(
|
||||||
|
z => z.currency === this.stallDialog.data.currency
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sendStallFormData: async function () {
|
||||||
|
const stallData = {
|
||||||
|
name: this.stallDialog.data.name,
|
||||||
|
wallet: this.stallDialog.data.wallet,
|
||||||
|
currency: this.stallDialog.data.currency,
|
||||||
|
shipping_zones: this.stallDialog.data.shippingZones,
|
||||||
|
config: {
|
||||||
|
description: this.stallDialog.data.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.stallDialog.data.id) {
|
||||||
|
stallData.id = this.stallDialog.data.id
|
||||||
|
await this.restoreStall(stallData)
|
||||||
|
} else {
|
||||||
|
await this.createStall(stallData)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createStall: async function (stall) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/nostrmarket/api/v1/stall',
|
||||||
|
this.adminkey,
|
||||||
|
stall
|
||||||
|
)
|
||||||
|
this.stallDialog.show = false
|
||||||
|
data.expanded = false
|
||||||
|
this.stalls.unshift(data)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Stall created!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restoreStall: async function (stallData) {
|
||||||
|
try {
|
||||||
|
stallData.pending = false
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
`/nostrmarket/api/v1/stall/${stallData.id}`,
|
||||||
|
this.adminkey,
|
||||||
|
stallData
|
||||||
|
)
|
||||||
|
this.stallDialog.show = false
|
||||||
|
data.expanded = false
|
||||||
|
this.stalls.unshift(data)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Stall restored!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteStall: async function (pendingStall) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog(
|
||||||
|
`
|
||||||
|
Are you sure you want to delete this pending stall '${pendingStall.name}'?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.onOk(async () => {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
'/nostrmarket/api/v1/stall/' + pendingStall.id,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Pending Stall Deleted',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getCurrencies: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/currencies',
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
|
||||||
|
return ['sat', ...data]
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
getStalls: async function (pending = false) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/nostrmarket/api/v1/stall?pending=${pending}`,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
return data.map(s => ({...s, expanded: false}))
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
getZones: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/zone',
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
return data.map(z => ({
|
||||||
|
...z,
|
||||||
|
label: z.name
|
||||||
|
? `${z.name} (${z.countries.join(', ')})`
|
||||||
|
: z.countries.join(', ')
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
handleStallDeleted: function (stallId) {
|
||||||
|
this.stalls = _.reject(this.stalls, function (obj) {
|
||||||
|
return obj.id === stallId
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleStallUpdated: function (stall) {
|
||||||
|
const index = this.stalls.findIndex(r => r.id === stall.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
stall.expanded = true
|
||||||
|
this.stalls.splice(index, 1, stall)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openCreateStallDialog: async function (stallData) {
|
||||||
|
this.currencies = await this.getCurrencies()
|
||||||
|
this.zoneOptions = await this.getZones()
|
||||||
|
if (!this.zoneOptions || !this.zoneOptions.length) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Please create a Shipping Zone first!'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.stallDialog.data = stallData || {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
wallet: null,
|
||||||
|
currency: 'sat',
|
||||||
|
shippingZones: []
|
||||||
|
}
|
||||||
|
this.stallDialog.show = true
|
||||||
|
},
|
||||||
|
openSelectPendingStallDialog: async function () {
|
||||||
|
this.stallDialog.showRestore = true
|
||||||
|
this.pendingStalls = await this.getStalls(true)
|
||||||
|
},
|
||||||
|
openRestoreStallDialog: async function (pendingStall) {
|
||||||
|
const shippingZonesIds = this.zoneOptions.map(z => z.id)
|
||||||
|
await this.openCreateStallDialog({
|
||||||
|
id: pendingStall.id,
|
||||||
|
name: pendingStall.name,
|
||||||
|
description: pendingStall.config?.description,
|
||||||
|
currency: pendingStall.currency,
|
||||||
|
shippingZones: (pendingStall.shipping_zones || [])
|
||||||
|
.filter(z => shippingZonesIds.indexOf(z.id) !== -1)
|
||||||
|
.map(z => ({
|
||||||
|
...z,
|
||||||
|
label: z.name
|
||||||
|
? `${z.name} (${z.countries.join(', ')})`
|
||||||
|
: z.countries.join(', ')
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
customerSelectedForOrder: function (customerPubkey) {
|
||||||
|
this.$emit('customer-selected-for-order', customerPubkey)
|
||||||
|
},
|
||||||
|
shortLabel(value = '') {
|
||||||
|
if (value.length <= 64) return value
|
||||||
|
return value.substring(0, 60) + '...'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {
|
||||||
|
this.stalls = await this.getStalls()
|
||||||
|
this.currencies = await this.getCurrencies()
|
||||||
|
this.zoneOptions = await this.getZones()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
<div>
|
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
|
||||||
<div class="col q-pr-lg">
|
|
||||||
|
|
||||||
<q-btn-dropdown @click="openCreateStallDialog()" outline unelevated split class="float-left" color="primary"
|
|
||||||
label="New Stall (Store)">
|
|
||||||
<q-item @click="openCreateStallDialog()" clickable v-close-popup>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>New Stall</q-item-label>
|
|
||||||
<q-item-label caption>Create a new stall</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item @click="openSelectPendingStallDialog" clickable v-close-popup>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Restore Stall</q-item-label>
|
|
||||||
<q-item-label caption>Restore existing stall from Nostr</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-btn-dropdown>
|
|
||||||
<q-input borderless dense debounce="300" v-model="filter" placeholder="Search" class="float-right">
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon name="search"></q-icon>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-table flat dense :data="stalls" row-key="id" :columns="stallsTable.columns"
|
|
||||||
:pagination.sync="stallsTable.pagination" :filter="filter">
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn size="sm" color="primary" round dense @click="props.row.expanded= !props.row.expanded"
|
|
||||||
:icon="props.row.expanded? 'remove' : 'add'" />
|
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td key="id" :props="props"> {{shortLabel(props.row.name)}} </q-td>
|
|
||||||
<q-td key="currency" :props="props"> {{props.row.currency}} </q-td>
|
|
||||||
<q-td key="description" :props="props">
|
|
||||||
{{shortLabel(props.row.config.description)}}
|
|
||||||
</q-td>
|
|
||||||
<q-td key="shippingZones" :props="props">
|
|
||||||
<div>
|
|
||||||
{{shortLabel(props.row.shipping_zones.filter(z => !!z.name).map(z => z.name).join(', '))}}
|
|
||||||
</div>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
<q-tr v-if="props.row.expanded" :props="props">
|
|
||||||
<q-td colspan="100%">
|
|
||||||
<div class="row items-center q-mb-lg">
|
|
||||||
<div class="col-12">
|
|
||||||
<stall-details :stall-id="props.row.id" :adminkey="adminkey" :inkey="inkey"
|
|
||||||
:wallet-options="walletOptions" :zone-options="zoneOptions" :currencies="currencies"
|
|
||||||
@stall-deleted="handleStallDeleted" @stall-updated="handleStallUpdated"
|
|
||||||
@customer-selected-for-order="customerSelectedForOrder"></stall-details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<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-input filled dense v-model.trim="stallDialog.data.description" type="textarea" rows="3"
|
|
||||||
label="Description"></q-input>
|
|
||||||
<q-select filled dense emit-value v-model="stallDialog.data.wallet" :options="walletOptions" label="Wallet *">
|
|
||||||
</q-select>
|
|
||||||
<q-select filled dense v-model="stallDialog.data.currency" type="text" label="Unit"
|
|
||||||
:options="currencies"></q-select>
|
|
||||||
<q-select :options="filteredZoneOptions" filled dense multiple v-model.trim="stallDialog.data.shippingZones"
|
|
||||||
label="Shipping Zones"></q-select>
|
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn unelevated color="primary" :disable="!stallDialog.data.name
|
|
||||||
|| !stallDialog.data.currency
|
|
||||||
|| !stallDialog.data.wallet
|
|
||||||
|| !stallDialog.data.shippingZones
|
|
||||||
|| !stallDialog.data.shippingZones.length" type="submit"
|
|
||||||
:label="stallDialog.data.id ? 'Restore Stall' : 'Create Stall'"></q-btn>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<q-dialog v-model="stallDialog.showRestore" position="top">
|
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
|
||||||
<div v-if="pendingStalls && pendingStalls.length" class="row q-mt-lg">
|
|
||||||
<q-item v-for="pendingStall of pendingStalls" :key="pendingStall.id" tag="label" class="full-width" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label><span v-text="pendingStall.name"></span></q-item-label>
|
|
||||||
<q-item-label caption><span v-text="pendingStall.config?.description"></span></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section class="q-pl-xl float-right">
|
|
||||||
<q-btn @click="openRestoreStallDialog(pendingStall)" v-close-popup flat color="green"
|
|
||||||
class="q-ml-auto float-right">Restore</q-btn>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section class="float-right">
|
|
||||||
<q-btn @click="deleteStall(pendingStall)" v-close-popup color="red" class="q-ml-auto float-right"
|
|
||||||
icon="cancel"></q-btn>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
There are no stalls to be restored.
|
|
||||||
</div>
|
|
||||||
<div class="row q-mt-lg">
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,266 +0,0 @@
|
||||||
async function stallList(path) {
|
|
||||||
const template = await loadTemplateAsync(path)
|
|
||||||
Vue.component('stall-list', {
|
|
||||||
name: 'stall-list',
|
|
||||||
template,
|
|
||||||
|
|
||||||
props: [`adminkey`, 'inkey', 'wallet-options'],
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
filter: '',
|
|
||||||
stalls: [],
|
|
||||||
pendingStalls: [],
|
|
||||||
currencies: [],
|
|
||||||
stallDialog: {
|
|
||||||
show: false,
|
|
||||||
showRestore: false,
|
|
||||||
data: {
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
wallet: null,
|
|
||||||
currency: 'sat',
|
|
||||||
shippingZones: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
zoneOptions: [],
|
|
||||||
stallsTable: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
align: 'left',
|
|
||||||
label: '',
|
|
||||||
field: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'id',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Name',
|
|
||||||
field: 'id'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'currency',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Currency',
|
|
||||||
field: 'currency'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Description',
|
|
||||||
field: 'description'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'shippingZones',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Shipping Zones',
|
|
||||||
field: 'shippingZones'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
filteredZoneOptions: function () {
|
|
||||||
return this.zoneOptions.filter(
|
|
||||||
z => z.currency === this.stallDialog.data.currency
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
sendStallFormData: async function () {
|
|
||||||
const stallData = {
|
|
||||||
name: this.stallDialog.data.name,
|
|
||||||
wallet: this.stallDialog.data.wallet,
|
|
||||||
currency: this.stallDialog.data.currency,
|
|
||||||
shipping_zones: this.stallDialog.data.shippingZones,
|
|
||||||
config: {
|
|
||||||
description: this.stallDialog.data.description
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.stallDialog.data.id) {
|
|
||||||
stallData.id = this.stallDialog.data.id
|
|
||||||
await this.restoreStall(stallData)
|
|
||||||
} else {
|
|
||||||
await this.createStall(stallData)
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
createStall: async function (stall) {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
'/nostrmarket/api/v1/stall',
|
|
||||||
this.adminkey,
|
|
||||||
stall
|
|
||||||
)
|
|
||||||
this.stallDialog.show = false
|
|
||||||
data.expanded = false
|
|
||||||
this.stalls.unshift(data)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Stall created!'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
restoreStall: async function (stallData) {
|
|
||||||
try {
|
|
||||||
stallData.pending = false
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'PUT',
|
|
||||||
`/nostrmarket/api/v1/stall/${stallData.id}`,
|
|
||||||
this.adminkey,
|
|
||||||
stallData
|
|
||||||
)
|
|
||||||
this.stallDialog.show = false
|
|
||||||
data.expanded = false
|
|
||||||
this.stalls.unshift(data)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Stall restored!'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteStall: async function (pendingStall) {
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog(
|
|
||||||
`
|
|
||||||
Are you sure you want to delete this pending stall '${pendingStall.name}'?
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.onOk(async () => {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'DELETE',
|
|
||||||
'/nostrmarket/api/v1/stall/' + pendingStall.id,
|
|
||||||
this.adminkey
|
|
||||||
)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Pending Stall Deleted',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getCurrencies: async function () {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
'/nostrmarket/api/v1/currencies',
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
|
|
||||||
return ['sat', ...data]
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
getStalls: async function (pending = false) {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/nostrmarket/api/v1/stall?pending=${pending}`,
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
return data.map(s => ({ ...s, expanded: false }))
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
getZones: async function () {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
'/nostrmarket/api/v1/zone',
|
|
||||||
this.inkey
|
|
||||||
)
|
|
||||||
return data.map(z => ({
|
|
||||||
...z,
|
|
||||||
label: z.name
|
|
||||||
? `${z.name} (${z.countries.join(', ')})`
|
|
||||||
: z.countries.join(', ')
|
|
||||||
}))
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
handleStallDeleted: function (stallId) {
|
|
||||||
this.stalls = _.reject(this.stalls, function (obj) {
|
|
||||||
return obj.id === stallId
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleStallUpdated: function (stall) {
|
|
||||||
const index = this.stalls.findIndex(r => r.id === stall.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
stall.expanded = true
|
|
||||||
this.stalls.splice(index, 1, stall)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openCreateStallDialog: async function (stallData) {
|
|
||||||
this.currencies = await this.getCurrencies()
|
|
||||||
this.zoneOptions = await this.getZones()
|
|
||||||
if (!this.zoneOptions || !this.zoneOptions.length) {
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'warning',
|
|
||||||
message: 'Please create a Shipping Zone first!'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.stallDialog.data = stallData || {
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
wallet: null,
|
|
||||||
currency: 'sat',
|
|
||||||
shippingZones: []
|
|
||||||
}
|
|
||||||
this.stallDialog.show = true
|
|
||||||
},
|
|
||||||
openSelectPendingStallDialog: async function () {
|
|
||||||
this.stallDialog.showRestore = true
|
|
||||||
this.pendingStalls = await this.getStalls(true)
|
|
||||||
},
|
|
||||||
openRestoreStallDialog: async function (pendingStall) {
|
|
||||||
const shippingZonesIds = this.zoneOptions.map(z => z.id)
|
|
||||||
await this.openCreateStallDialog({
|
|
||||||
id: pendingStall.id,
|
|
||||||
name: pendingStall.name,
|
|
||||||
description: pendingStall.config?.description,
|
|
||||||
currency: pendingStall.currency,
|
|
||||||
shippingZones: (pendingStall.shipping_zones || [])
|
|
||||||
.filter(z => shippingZonesIds.indexOf(z.id) !== -1)
|
|
||||||
.map(z => ({
|
|
||||||
...z,
|
|
||||||
label: z.name
|
|
||||||
? `${z.name} (${z.countries.join(', ')})`
|
|
||||||
: z.countries.join(', ')
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
customerSelectedForOrder: function (customerPubkey) {
|
|
||||||
this.$emit('customer-selected-for-order', customerPubkey)
|
|
||||||
},
|
|
||||||
shortLabel(value = ''){
|
|
||||||
if (value.length <= 64) return value
|
|
||||||
return value.substring(0, 60) + '...'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created: async function () {
|
|
||||||
this.stalls = await this.getStalls()
|
|
||||||
this.currencies = await this.getCurrencies()
|
|
||||||
this.zoneOptions = await this.getZones()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +1,8 @@
|
||||||
const merchant = async () => {
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
|
|
||||||
await keyPair('static/components/key-pair/key-pair.html')
|
|
||||||
await shippingZones('static/components/shipping-zones/shipping-zones.html')
|
|
||||||
await stallDetails('static/components/stall-details/stall-details.html')
|
|
||||||
await stallList('static/components/stall-list/stall-list.html')
|
|
||||||
await orderList('static/components/order-list/order-list.html')
|
|
||||||
await directMessages('static/components/direct-messages/direct-messages.html')
|
|
||||||
await merchantDetails(
|
|
||||||
'static/components/merchant-details/merchant-details.html'
|
|
||||||
)
|
|
||||||
|
|
||||||
const nostr = window.NostrTools
|
const nostr = window.NostrTools
|
||||||
|
|
||||||
new Vue({
|
window.app = Vue.createApp({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
merchant: {},
|
merchant: {},
|
||||||
|
|
@ -67,20 +54,18 @@ const merchant = async () => {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: "Cannot fetch merchant!"
|
message: 'Cannot fetch merchant!'
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const message = merchant.config.active ?
|
const message = merchant.config.active
|
||||||
'New orders will not be processed. Are you sure you want to deactivate?' :
|
? 'New orders will not be processed. Are you sure you want to deactivate?'
|
||||||
merchant.config.restore_in_progress ?
|
: merchant.config.restore_in_progress
|
||||||
'Merchant restore from nostr in progress. Please wait!! ' +
|
? 'Merchant restore from nostr in progress. Please wait!! ' +
|
||||||
'Activating now can lead to duplicate order processing. Click "OK" if you want to activate anyway?' :
|
'Activating now can lead to duplicate order processing. Click "OK" if you want to activate anyway?'
|
||||||
'Are you sure you want activate this merchant?'
|
: 'Are you sure you want activate this merchant?'
|
||||||
|
|
||||||
LNbits.utils
|
LNbits.utils.confirmDialog(message).onOk(async () => {
|
||||||
.confirmDialog(message)
|
|
||||||
.onOk(async () => {
|
|
||||||
await this.toggleMerchant()
|
await this.toggleMerchant()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
@ -89,7 +74,7 @@ const merchant = async () => {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'PUT',
|
'PUT',
|
||||||
`/nostrmarket/api/v1/merchant/${this.merchant.id}/toggle`,
|
`/nostrmarket/api/v1/merchant/${this.merchant.id}/toggle`,
|
||||||
this.g.user.wallets[0].adminkey,
|
this.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
const state = data.config.active ? 'activated' : 'disabled'
|
const state = data.config.active ? 'activated' : 'disabled'
|
||||||
this.merchant = data
|
this.merchant = data
|
||||||
|
|
@ -153,7 +138,10 @@ const merchant = async () => {
|
||||||
this.orderPubkey = customerPubkey
|
this.orderPubkey = customerPubkey
|
||||||
},
|
},
|
||||||
showOrderDetails: async function (orderData) {
|
showOrderDetails: async function (orderData) {
|
||||||
await this.$refs.orderListRef.orderSelected(orderData.orderId, orderData.eventId)
|
await this.$refs.orderListRef.orderSelected(
|
||||||
|
orderData.orderId,
|
||||||
|
orderData.eventId
|
||||||
|
)
|
||||||
},
|
},
|
||||||
waitForNotifications: async function () {
|
waitForNotifications: async function () {
|
||||||
if (!this.merchant) return
|
if (!this.merchant) return
|
||||||
|
|
@ -199,7 +187,6 @@ const merchant = async () => {
|
||||||
// order paid
|
// order paid
|
||||||
// order shipped
|
// order shipped
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
|
|
@ -230,12 +217,12 @@ const merchant = async () => {
|
||||||
created: async function () {
|
created: async function () {
|
||||||
await this.getMerchant()
|
await this.getMerchant()
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
if (!this.wsConnection || this.wsConnection.readyState !== WebSocket.OPEN) {
|
if (
|
||||||
|
!this.wsConnection ||
|
||||||
|
this.wsConnection.readyState !== WebSocket.OPEN
|
||||||
|
) {
|
||||||
await this.waitForNotifications()
|
await this.waitForNotifications()
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
merchant()
|
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@
|
||||||
<script src="/nostrmarket/static/market/js/nostr.bundle.js"></script>
|
<script src="/nostrmarket/static/market/js/nostr.bundle.js"></script>
|
||||||
<script src="/nostrmarket/static/market/js/bolt11-decoder.js"></script>
|
<script src="/nostrmarket/static/market/js/bolt11-decoder.js"></script>
|
||||||
<script src="/nostrmarket/static/market/js/utils.js"></script>
|
<script src="/nostrmarket/static/market/js/utils.js"></script>
|
||||||
<link rel=icon type=image/png sizes=128x128 href="/nostrmarket/static/market/icons/favicon-128x128.png">
|
<link rel="icon" type="image/png" sizes="128x128" href="/nostrmarket/static/market/icons/favicon-128x128.png">
|
||||||
<link rel=icon type=image/png sizes=96x96 href="/nostrmarket/static/market/icons/favicon-96x96.png">
|
<link rel="icon" type="image/png" sizes="96x96" href="/nostrmarket/static/market/icons/favicon-96x96.png">
|
||||||
<link rel=icon type=image/png sizes=32x32 href="/nostrmarket/static/market/icons/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/nostrmarket/static/market/icons/favicon-32x32.png">
|
||||||
<link rel=icon type=image/png sizes=16x16 href="/nostrmarket/static/market/icons/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/nostrmarket/static/market/icons/favicon-16x16.png">
|
||||||
<link rel=icon type=image/ico href="/nostrmarket/static/market/favicon.ico">
|
<link rel="icon" type="image/ico" href="/nostrmarket/static/market/favicon.ico">
|
||||||
<script type="module" crossorigin src="/nostrmarket/static/market/assets/index.923cbbf9.js"></script>
|
<script type="module" crossorigin src="/nostrmarket/static/market/assets/index.923cbbf9.js"></script>
|
||||||
<link rel="stylesheet" href="/nostrmarket/static/market/assets/index.73d462e5.css">
|
<link rel="stylesheet" href="/nostrmarket/static/market/assets/index.73d462e5.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
5
tasks.py
5
tasks.py
|
|
@ -1,10 +1,9 @@
|
||||||
from asyncio import Queue
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from asyncio import Queue
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
from lnbits.tasks import register_invoice_listener
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from .nostr.nostr_client import NostrClient
|
from .nostr.nostr_client import NostrClient
|
||||||
from .services import (
|
from .services import (
|
||||||
|
|
|
||||||
169
templates/nostrmarket/components/direct-messages.html
Normal file
169
templates/nostrmarket/components/direct-messages.html
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
<div>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-2">
|
||||||
|
<h6 class="text-subtitle1 q-my-none">Messages</h6>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<q-badge v-if="unreadMessages" color="primary" outline
|
||||||
|
><span v-text="unreadMessages"></span> new</q-badge
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-btn
|
||||||
|
v-if="activePublicKey"
|
||||||
|
@click="showClientOrders"
|
||||||
|
unelevated
|
||||||
|
outline
|
||||||
|
class="float-right"
|
||||||
|
>Client Orders</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<q-separator></q-separator>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-10">
|
||||||
|
<q-select
|
||||||
|
v-model="activePublicKey"
|
||||||
|
:options="customers.map(c => ({label: buildCustomerLabel(c), value: c.public_key}))"
|
||||||
|
label="Select Customer"
|
||||||
|
emit-value
|
||||||
|
@input="selectActiveCustomer()"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<q-btn
|
||||||
|
label="Add"
|
||||||
|
color="primary"
|
||||||
|
class="float-right q-mt-md"
|
||||||
|
@click="showAddPublicKey = true"
|
||||||
|
>
|
||||||
|
<q-tooltip> Add a public key to chat with </q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="chat-container" ref="chatCard">
|
||||||
|
<div class="chat-box">
|
||||||
|
<div class="chat-messages" style="height: 45vh">
|
||||||
|
<q-chat-message
|
||||||
|
v-for="(dm, index) in messagesAsJson"
|
||||||
|
:key="index"
|
||||||
|
:name="dm.incoming ? 'customer': 'me'"
|
||||||
|
:sent="!dm.incoming"
|
||||||
|
:stamp="dm.dateFrom"
|
||||||
|
:bg-color="dm.incoming ? 'white' : 'light-green-2'"
|
||||||
|
:class="'chat-mesage-index-'+index"
|
||||||
|
>
|
||||||
|
<div v-if="dm.isJson">
|
||||||
|
<div v-if="dm.message.type === 0">
|
||||||
|
<strong>New order:</strong>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="dm.message.type === 1">
|
||||||
|
<strong>Reply sent for order: </strong>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="dm.message.type === 2">
|
||||||
|
<q-badge v-if="dm.message.paid" color="green">Paid </q-badge>
|
||||||
|
<q-badge v-if="dm.message.shipped" color="green"
|
||||||
|
>Shipped
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span v-text="dm.message.message"></span>
|
||||||
|
<q-badge color="orange">
|
||||||
|
<span
|
||||||
|
v-text="dm.message.id"
|
||||||
|
@click="showOrderDetails(dm.message.id, dm.event_id)"
|
||||||
|
class="cursor-pointer"
|
||||||
|
></span>
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
<q-badge
|
||||||
|
@click="showMessageRawData(index)"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>...</q-badge
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-else><span v-text="dm.message"></span></div>
|
||||||
|
</q-chat-message>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-card-section>
|
||||||
|
<q-form @submit="sendDirectMesage" class="full-width chat-input">
|
||||||
|
<q-input
|
||||||
|
ref="newMessage"
|
||||||
|
v-model="newMessage"
|
||||||
|
placeholder="Message"
|
||||||
|
class="full-width"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<div>
|
||||||
|
<q-dialog v-model="showAddPublicKey" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
|
<q-form @submit="addPublicKey" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="newPublicKey"
|
||||||
|
label="Public Key (hex or nsec)"
|
||||||
|
></q-input>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="!newPublicKey"
|
||||||
|
type="submit"
|
||||||
|
>Add</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
<q-dialog v-model="showRawMessage" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
|
<q-form>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
type="textarea"
|
||||||
|
rows="20"
|
||||||
|
v-model.trim="rawMessage"
|
||||||
|
label="Raw Data"
|
||||||
|
></q-input>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Close</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -13,11 +13,11 @@
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="text-center q-mb-lg cursor-pointer">
|
<div class="text-center q-mb-lg cursor-pointer">
|
||||||
<q-responsive :ratio="1" class="q-mx-xl" @click="copyText(publicKey)">
|
<q-responsive :ratio="1" class="q-mx-xl" @click="copyText(publicKey)">
|
||||||
<qrcode
|
<lnbits-qrcode
|
||||||
:value="publicKey"
|
:value="publicKey"
|
||||||
:options="{width: 250}"
|
:options="{width: 250}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></lnbits-qrcode>
|
||||||
</q-responsive>
|
</q-responsive>
|
||||||
<small><span v-text="publicKey"></span><br />Click to copy</small>
|
<small><span v-text="publicKey"></span><br />Click to copy</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -30,11 +30,7 @@
|
||||||
class="q-mx-xl"
|
class="q-mx-xl"
|
||||||
@click="copyText(privateKey)"
|
@click="copyText(privateKey)"
|
||||||
>
|
>
|
||||||
<qrcode
|
<lnbits-qrcode :value="privateKey"></lnbits-qrcode>
|
||||||
:value="privateKey"
|
|
||||||
:options="{width: 250}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
</q-responsive>
|
</q-responsive>
|
||||||
<small><span v-text="privateKey"></span><br />Click to copy</small>
|
<small><span v-text="privateKey"></span><br />Click to copy</small>
|
||||||
</div>
|
</div>
|
||||||
369
templates/nostrmarket/components/order-list.html
Normal file
369
templates/nostrmarket/components/order-list.html
Normal file
|
|
@ -0,0 +1,369 @@
|
||||||
|
<div>
|
||||||
|
<div class="row q-mb-md">
|
||||||
|
<div class="col-md-4 col-sm-6 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
v-model="search.publicKey"
|
||||||
|
:options="customerOptions"
|
||||||
|
label="Customer"
|
||||||
|
emit-value
|
||||||
|
class="text-wrap"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 col-sm-6 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
v-model="search.isPaid"
|
||||||
|
:options="ternaryOptions"
|
||||||
|
label="Paid"
|
||||||
|
emit-value
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 col-sm-6 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
v-model="search.isShipped"
|
||||||
|
:options="ternaryOptions"
|
||||||
|
label="Shipped"
|
||||||
|
emit-value
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-sm-6">
|
||||||
|
<q-btn-dropdown
|
||||||
|
@click="getOrders()"
|
||||||
|
:disable="search.restoring"
|
||||||
|
outline
|
||||||
|
unelevated
|
||||||
|
split
|
||||||
|
class="q-pt-md float-right"
|
||||||
|
:label="search.restoring ? 'Restoring Orders...' : 'Load Orders'"
|
||||||
|
>
|
||||||
|
<q-spinner
|
||||||
|
v-if="search.restoring"
|
||||||
|
color="primary"
|
||||||
|
size="2.55em"
|
||||||
|
class="q-pt-md float-right"
|
||||||
|
></q-spinner>
|
||||||
|
<q-item @click="restoreOrders" clickable v-close-popup>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Restore Orders</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Restore previous orders from Nostr</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-md">
|
||||||
|
<div class="col">
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:rows="orders"
|
||||||
|
row-key="id"
|
||||||
|
:columns="ordersTable.columns"
|
||||||
|
v-model:pagination="ordersTable.pagination"
|
||||||
|
:filter="filter"
|
||||||
|
>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
@click="props.row.expanded= !props.row.expanded"
|
||||||
|
:icon="props.row.expanded? 'remove' : 'add'"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="id" :props="props">
|
||||||
|
<span v-text="toShortId(props.row.id)"></span>
|
||||||
|
|
||||||
|
<q-badge v-if="props.row.isNew" color="orange">new</q-badge></q-td
|
||||||
|
>
|
||||||
|
<q-td key="total" :props="props">
|
||||||
|
<span v-text="satBtc(props.row.total)"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="fiat" :props="props">
|
||||||
|
<span v-if="props.row.extra.currency !== 'sat'">
|
||||||
|
<span v-text="orderTotal(props.row)"></span
|
||||||
|
><span v-text="props.row.extra.currency"></span>
|
||||||
|
</span>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="paid" :props="props">
|
||||||
|
<q-checkbox
|
||||||
|
v-model="props.row.paid"
|
||||||
|
:label="props.row.paid ? 'Yes' : 'No'"
|
||||||
|
disable
|
||||||
|
readonly
|
||||||
|
size="sm"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="shipped" :props="props">
|
||||||
|
<q-checkbox
|
||||||
|
v-model="props.row.shipped"
|
||||||
|
@update:model-value="showShipOrderDialog(props.row)"
|
||||||
|
:label="props.row.shipped ? 'Yes' : 'No'"
|
||||||
|
size="sm"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="public_key" :props="props">
|
||||||
|
<span
|
||||||
|
@click="customerSelected(props.row.public_key)"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
<span v-text="toShortId(props.row.public_key)"></span>
|
||||||
|
</span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="event_created_at" :props="props">
|
||||||
|
<span v-text="formatDate(props.row.event_created_at)"></span>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
<q-tr v-if="props.row.expanded" :props="props">
|
||||||
|
<q-td colspan="100%">
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<div class="col-3 q-pr-lg">Products:</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-1"><strong>Quantity</strong></div>
|
||||||
|
<div class="col-1"></div>
|
||||||
|
<div class="col-4"><strong>Name</strong></div>
|
||||||
|
<div class="col-2"><strong>Price</strong></div>
|
||||||
|
<div class="col-4"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg"></div>
|
||||||
|
<div class="col-8">
|
||||||
|
<div
|
||||||
|
v-for="item in props.row.items"
|
||||||
|
class="row items-center no-wrap q-mb-md"
|
||||||
|
>
|
||||||
|
<div class="col-1">
|
||||||
|
<span v-text="item.quantity"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-1">x</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<p :title="productName(props.row, item.product_id)">
|
||||||
|
<span
|
||||||
|
v-text="shortLabel(productName(props.row, item.product_id))"
|
||||||
|
></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<span
|
||||||
|
v-text="productPrice(props.row, item.product_id)"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-4"></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="props.row.extra.shipping_cost"
|
||||||
|
class="row items-center no-wrap q-mb-md"
|
||||||
|
>
|
||||||
|
<div class="col-1"></div>
|
||||||
|
<div class="col-1"></div>
|
||||||
|
<div class="col-4">Shipping Cost</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<span v-text="props.row.extra.shipping_cost"></span>
|
||||||
|
<span v-text="props.row.extra.currency"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-4"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-1"></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="props.row.extra.currency !== 'sat'"
|
||||||
|
class="row items-center no-wrap q-mb-md q-mt-md"
|
||||||
|
>
|
||||||
|
<div class="col-3 q-pr-lg">Exchange Rate (1 BTC):</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
disabled
|
||||||
|
:value="formatFiat(props.row.extra.btc_price, props.row.extra.currency)"
|
||||||
|
type="text"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="props.row.extra.fail_message"
|
||||||
|
class="row items-center no-wrap q-mb-md"
|
||||||
|
>
|
||||||
|
<div class="col-3 q-pr-lg">Error:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-badge color="pink"
|
||||||
|
><span v-text="props.row.extra.fail_message"></span
|
||||||
|
></q-badge>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md q-mt-md">
|
||||||
|
<div class="col-3 q-pr-lg">Order ID:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
disabled
|
||||||
|
v-model.trim="props.row.id"
|
||||||
|
type="text"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">Customer Public Key:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
disabled
|
||||||
|
v-model.trim="props.row.public_key"
|
||||||
|
type="text"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="props.row.address"
|
||||||
|
class="row items-center no-wrap q-mb-md"
|
||||||
|
>
|
||||||
|
<div class="col-3 q-pr-lg">Address:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
disabled
|
||||||
|
v-model.trim="props.row.address"
|
||||||
|
type="text"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="props.row.contact.phone"
|
||||||
|
class="row items-center no-wrap q-mb-md"
|
||||||
|
>
|
||||||
|
<div class="col-3 q-pr-lg">Phone:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
disabled
|
||||||
|
v-model.trim="props.row.contact.phone"
|
||||||
|
type="text"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="props.row.contact.email"
|
||||||
|
class="row items-center no-wrap q-mb-md"
|
||||||
|
>
|
||||||
|
<div class="col-3 q-pr-lg">Email:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
disabled
|
||||||
|
v-model.trim="props.row.contact.email"
|
||||||
|
type="text"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">Shipping Zone:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
:options="getStallZones(props.row.stall_id)"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model.trim="props.row.shipping_id"
|
||||||
|
label="Shipping Zones"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">Invoice ID:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
disabled
|
||||||
|
v-model.trim="props.row.invoice_id"
|
||||||
|
type="text"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg"></div>
|
||||||
|
|
||||||
|
<div class="col-9">
|
||||||
|
<q-btn
|
||||||
|
@click="reissueOrderInvoice(props.row)"
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
class="float-left"
|
||||||
|
label="Reissue Invoice"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-dialog v-model="showShipDialog" position="top">
|
||||||
|
<q-card v-if="selectedOrder" class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
|
<q-form @submit="updateOrderShipped" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="shippingMessage"
|
||||||
|
label="Shipping Message"
|
||||||
|
type="textarea"
|
||||||
|
rows="4"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
:label="selectedOrder.shipped? 'Unship Order' : 'Ship Order'"
|
||||||
|
></q-btn>
|
||||||
|
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
||||||
|
|
@ -22,12 +22,13 @@
|
||||||
@click="openZoneDialog(zone)"
|
@click="openZoneDialog(zone)"
|
||||||
>
|
>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>{{zone.name}}</q-item-label>
|
<q-item-label><span v-text="zone.name"></span></q-item-label>
|
||||||
<q-item-label caption>{{zone.countries.join(", ")}}</q-item-label>
|
<q-item-label caption
|
||||||
|
><span v-text="zone.countries.join('', '')"></span
|
||||||
|
></q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item> </q-list
|
||||||
</q-list></q-btn-dropdown
|
></q-btn-dropdown>
|
||||||
>
|
|
||||||
|
|
||||||
<q-dialog v-model="zoneDialog.showDialog" position="top">
|
<q-dialog v-model="zoneDialog.showDialog" position="top">
|
||||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
466
templates/nostrmarket/components/stall-details.html
Normal file
466
templates/nostrmarket/components/stall-details.html
Normal file
|
|
@ -0,0 +1,466 @@
|
||||||
|
<div>
|
||||||
|
<q-tabs v-model="tab" no-caps class="bg-dark text-white shadow-2">
|
||||||
|
<q-tab name="info" label="Stall Info"></q-tab>
|
||||||
|
<q-tab name="products" label="Products"></q-tab>
|
||||||
|
<q-tab name="orders" label="Orders"></q-tab>
|
||||||
|
</q-tabs>
|
||||||
|
<q-tab-panels v-model="tab">
|
||||||
|
<q-tab-panel name="info">
|
||||||
|
<div v-if="stall">
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">ID:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
disabled
|
||||||
|
v-model.trim="stall.id"
|
||||||
|
type="text"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">Name:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="stall.name"
|
||||||
|
type="text"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">Description:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="stall.config.description"
|
||||||
|
type="textarea"
|
||||||
|
rows="3"
|
||||||
|
label="Description"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">Wallet:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="stall.wallet"
|
||||||
|
:options="walletOptions"
|
||||||
|
label="Wallet *"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">Currency:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="stall.currency"
|
||||||
|
type="text"
|
||||||
|
label="Unit"
|
||||||
|
:options="currencies"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">Shipping Zones:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
:options="filteredZoneOptions"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
multiple
|
||||||
|
v-model.trim="stall.shipping_zones"
|
||||||
|
label="Shipping Zones"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center q-mt-xl">
|
||||||
|
<div class="col-6 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
unelevated
|
||||||
|
class="float-left"
|
||||||
|
color="primary"
|
||||||
|
@click="updateStall()"
|
||||||
|
>Update Stall</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
unelevated
|
||||||
|
icon="cancel"
|
||||||
|
class="float-right"
|
||||||
|
@click="deleteStall()"
|
||||||
|
>Delete Stall</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="products">
|
||||||
|
<div v-if="stall">
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">
|
||||||
|
<q-btn-dropdown
|
||||||
|
@click="showNewProductDialog()"
|
||||||
|
outline
|
||||||
|
unelevated
|
||||||
|
split
|
||||||
|
class="float-left"
|
||||||
|
color="primary"
|
||||||
|
label="New Product"
|
||||||
|
>
|
||||||
|
<q-item @click="showNewProductDialog()" clickable v-close-popup>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>New Product</q-item-label>
|
||||||
|
<q-item-label caption>Create a new product</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
@click="openSelectPendingProductDialog"
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Restore Product</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Restore existing product from Nostr</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg"></div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:rows="products"
|
||||||
|
row-key="id"
|
||||||
|
:columns="productsTable.columns"
|
||||||
|
v-model:pagination="productsTable.pagination"
|
||||||
|
:filter="productsFilter"
|
||||||
|
>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="grey"
|
||||||
|
dense
|
||||||
|
@click="deleteProduct(props.row.id)"
|
||||||
|
icon="delete"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
dense
|
||||||
|
@click="editProduct(props.row)"
|
||||||
|
icon="edit"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-toggle
|
||||||
|
@update:model-value="updateProduct({ ...props.row, active: !props.row.active })"
|
||||||
|
size="xs"
|
||||||
|
checked-icon="check"
|
||||||
|
v-model="props.row.active"
|
||||||
|
color="green"
|
||||||
|
unchecked-icon="clear"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="id" :props="props"
|
||||||
|
><span v-text="props.row.id"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="name" :props="props">
|
||||||
|
<span v-text="shortLabel(props.row.name)"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="price" :props="props"
|
||||||
|
><span v-text="props.row.price"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="quantity" :props="props">
|
||||||
|
<span v-text="props.row.quantity"></span>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="orders">
|
||||||
|
<div v-if="stall">
|
||||||
|
<order-list
|
||||||
|
:adminkey="adminkey"
|
||||||
|
:inkey="inkey"
|
||||||
|
:stall-id="stallId"
|
||||||
|
@customer-selected="customerSelectedForOrder"
|
||||||
|
></order-list>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
<q-dialog v-model="productDialog.showDialog" position="top">
|
||||||
|
<q-card
|
||||||
|
v-if="stall && productDialog.data"
|
||||||
|
class="q-pa-lg q-pt-xl"
|
||||||
|
style="width: 500px"
|
||||||
|
>
|
||||||
|
<q-form @submit="sendProductFormData" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="productDialog.data.name"
|
||||||
|
label="Name"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="productDialog.data.config.description"
|
||||||
|
label="Description"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<div class="row q-mb-sm">
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="productDialog.data.price"
|
||||||
|
type="number"
|
||||||
|
:label="'Price (' + stall.currency + ') *'"
|
||||||
|
:step="stall.currency != 'sat' ? '0.01' : '1'"
|
||||||
|
:mask="stall.currency != 'sat' ? '#.##' : '#'"
|
||||||
|
fill-mask="0"
|
||||||
|
reverse-fill-mask
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-ml-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="productDialog.data.quantity"
|
||||||
|
type="number"
|
||||||
|
label="Quantity"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
group="advanced"
|
||||||
|
label="Categories"
|
||||||
|
caption="Add tags to producsts, make them easy to search."
|
||||||
|
>
|
||||||
|
<div class="q-pl-sm q-pt-sm">
|
||||||
|
<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 (Hit Enter to add)"
|
||||||
|
placeholder="crafts,robots,etc"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
group="advanced"
|
||||||
|
label="Images"
|
||||||
|
caption="Add images for product."
|
||||||
|
>
|
||||||
|
<div class="q-pl-sm q-pt-sm">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="productDialog.data.image"
|
||||||
|
@keydown.enter="addProductImage"
|
||||||
|
type="url"
|
||||||
|
label="Image URL"
|
||||||
|
>
|
||||||
|
<q-btn @click="addProductImage" dense flat icon="add"></q-btn
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<q-chip
|
||||||
|
v-for="imageUrl in productDialog.data.images"
|
||||||
|
:key="imageUrl"
|
||||||
|
removable
|
||||||
|
@remove="removeProductImage(imageUrl)"
|
||||||
|
color="primary"
|
||||||
|
text-color="white"
|
||||||
|
>
|
||||||
|
<span v-text="imageUrl.split('/').pop()"></span>
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-expansion-item
|
||||||
|
group="advanced"
|
||||||
|
label="Custom Shipping Cost"
|
||||||
|
caption="Configure custom shipping costs for this product"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="zone of productDialog.data.config.shipping"
|
||||||
|
class="row q-mb-sm q-ml-lg q-mt-sm"
|
||||||
|
>
|
||||||
|
<div class="col">
|
||||||
|
<span v-text="zone.name"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col q-pr-md">
|
||||||
|
<q-input
|
||||||
|
v-model="zone.cost"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
type="number"
|
||||||
|
label="Extra cost"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item
|
||||||
|
group="advanced"
|
||||||
|
label="Autoreply"
|
||||||
|
caption="Autoreply when paid"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row q-mb-sm">
|
||||||
|
<div class="col">
|
||||||
|
<q-checkbox
|
||||||
|
v-model="productDialog.data.config.use_autoreply"
|
||||||
|
dense
|
||||||
|
label="Send a direct message when paid"
|
||||||
|
class="q-ml-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mb-sm q-ml-sm">
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
v-model="productDialog.data.config.autoreply_message"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
type="textarea"
|
||||||
|
rows="5"
|
||||||
|
label="Autoreply message"
|
||||||
|
hint="It can include link to a digital asset"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
v-if="productDialog.data.id"
|
||||||
|
type="submit"
|
||||||
|
:label="productDialog.data.pending ? 'Restore Product' : 'Update Product'"
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
></q-btn>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="!productDialog.data.price
|
||||||
|
|| !productDialog.data.name
|
||||||
|
|| !productDialog.data.quantity"
|
||||||
|
type="submit"
|
||||||
|
>Create Product</q-btn
|
||||||
|
>
|
||||||
|
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
<q-dialog v-model="productDialog.showRestore" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
|
<div v-if="pendingProducts && pendingProducts.length" class="row q-mt-lg">
|
||||||
|
<q-item
|
||||||
|
v-for="pendingProduct of pendingProducts"
|
||||||
|
:key="pendingProduct.id"
|
||||||
|
tag="label"
|
||||||
|
class="full-width"
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
><span v-text="pendingProduct.name"></span
|
||||||
|
></q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
><span v-text="pendingProduct.config?.description"></span
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section class="q-pl-xl float-right">
|
||||||
|
<q-btn
|
||||||
|
@click="openRestoreProductDialog(pendingProduct)"
|
||||||
|
v-close-popup
|
||||||
|
flat
|
||||||
|
color="green"
|
||||||
|
class="q-ml-auto float-right"
|
||||||
|
>Restore</q-btn
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section class="float-right">
|
||||||
|
<q-btn
|
||||||
|
@click="deleteProduct(pendingProduct.id)"
|
||||||
|
v-close-popup
|
||||||
|
color="red"
|
||||||
|
class="q-ml-auto float-right"
|
||||||
|
icon="cancel"
|
||||||
|
></q-btn>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</div>
|
||||||
|
<div v-else>There are no products to be restored.</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
@click="restoreAllPendingProducts"
|
||||||
|
v-close-popup
|
||||||
|
flat
|
||||||
|
color="green"
|
||||||
|
>Restore All</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
||||||
215
templates/nostrmarket/components/stall-list.html
Normal file
215
templates/nostrmarket/components/stall-list.html
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
<div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col q-pr-lg">
|
||||||
|
<q-btn-dropdown
|
||||||
|
@click="openCreateStallDialog()"
|
||||||
|
outline
|
||||||
|
unelevated
|
||||||
|
split
|
||||||
|
class="float-left"
|
||||||
|
color="primary"
|
||||||
|
label="New Stall (Store)"
|
||||||
|
>
|
||||||
|
<q-item @click="openCreateStallDialog()" clickable v-close-popup>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>New Stall</q-item-label>
|
||||||
|
<q-item-label caption>Create a new stall</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item @click="openSelectPendingStallDialog" clickable v-close-popup>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Restore Stall</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Restore existing stall from Nostr</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-btn-dropdown>
|
||||||
|
<q-input
|
||||||
|
borderless
|
||||||
|
dense
|
||||||
|
debounce="300"
|
||||||
|
v-model="filter"
|
||||||
|
placeholder="Search"
|
||||||
|
class="float-right"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:rows="stalls"
|
||||||
|
row-key="id"
|
||||||
|
:columns="stallsTable.columns"
|
||||||
|
v-model:pagination="stallsTable.pagination"
|
||||||
|
:filter="filter"
|
||||||
|
>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
@click="props.row.expanded= !props.row.expanded"
|
||||||
|
:icon="props.row.expanded? 'remove' : 'add'"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
|
||||||
|
<q-td key="id" :props="props"
|
||||||
|
><span v-text="shortLabel(props.row.name)"></span
|
||||||
|
></q-td>
|
||||||
|
<q-td key="currency" :props="props"
|
||||||
|
><span v-text="props.row.currency"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="description" :props="props">
|
||||||
|
<span v-text="shortLabel(props.row.config.description)"></span>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="shippingZones" :props="props">
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
v-text="shortLabel(props.row.shipping_zones.filter(z => !!z.name).map(z => z.name).join(', '))"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
<q-tr v-if="props.row.expanded" :props="props">
|
||||||
|
<q-td colspan="100%">
|
||||||
|
<div class="row items-center q-mb-lg">
|
||||||
|
<div class="col-12">
|
||||||
|
<stall-details
|
||||||
|
:stall-id="props.row.id"
|
||||||
|
:adminkey="adminkey"
|
||||||
|
:inkey="inkey"
|
||||||
|
:wallet-options="walletOptions"
|
||||||
|
:zone-options="zoneOptions"
|
||||||
|
:currencies="currencies"
|
||||||
|
@stall-deleted="handleStallDeleted"
|
||||||
|
@stall-updated="handleStallUpdated"
|
||||||
|
@customer-selected-for-order="customerSelectedForOrder"
|
||||||
|
></stall-details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<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-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="stallDialog.data.description"
|
||||||
|
type="textarea"
|
||||||
|
rows="3"
|
||||||
|
label="Description"
|
||||||
|
></q-input>
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="stallDialog.data.wallet"
|
||||||
|
:options="walletOptions"
|
||||||
|
label="Wallet *"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="stallDialog.data.currency"
|
||||||
|
type="text"
|
||||||
|
label="Unit"
|
||||||
|
:options="currencies"
|
||||||
|
></q-select>
|
||||||
|
<q-select
|
||||||
|
:options="filteredZoneOptions"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
multiple
|
||||||
|
v-model.trim="stallDialog.data.shippingZones"
|
||||||
|
label="Shipping Zones"
|
||||||
|
></q-select>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="!stallDialog.data.name
|
||||||
|
|| !stallDialog.data.currency
|
||||||
|
|| !stallDialog.data.wallet
|
||||||
|
|| !stallDialog.data.shippingZones
|
||||||
|
|| !stallDialog.data.shippingZones.length"
|
||||||
|
type="submit"
|
||||||
|
:label="stallDialog.data.id ? 'Restore Stall' : 'Create Stall'"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
<q-dialog v-model="stallDialog.showRestore" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
|
<div v-if="pendingStalls && pendingStalls.length" class="row q-mt-lg">
|
||||||
|
<q-item
|
||||||
|
v-for="pendingStall of pendingStalls"
|
||||||
|
:key="pendingStall.id"
|
||||||
|
tag="label"
|
||||||
|
class="full-width"
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
><span v-text="pendingStall.name"></span
|
||||||
|
></q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
><span v-text="pendingStall.config?.description"></span
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section class="q-pl-xl float-right">
|
||||||
|
<q-btn
|
||||||
|
@click="openRestoreStallDialog(pendingStall)"
|
||||||
|
v-close-popup
|
||||||
|
flat
|
||||||
|
color="green"
|
||||||
|
class="q-ml-auto float-right"
|
||||||
|
>Restore</q-btn
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section class="float-right">
|
||||||
|
<q-btn
|
||||||
|
@click="deleteStall(pendingStall)"
|
||||||
|
v-close-popup
|
||||||
|
color="red"
|
||||||
|
class="q-ml-auto float-right"
|
||||||
|
icon="cancel"
|
||||||
|
></q-btn>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</div>
|
||||||
|
<div v-else>There are no stalls to be restored.</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -18,14 +18,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<q-toggle
|
<q-toggle
|
||||||
@input="toggleMerchantState()"
|
@update:model-value="toggleMerchantState()"
|
||||||
size="md"
|
size="md"
|
||||||
checked-icon="check"
|
checked-icon="check"
|
||||||
v-model="merchant.config.active"
|
v-model="merchant.config.active"
|
||||||
color="primary"
|
color="primary"
|
||||||
unchecked-icon="clear"
|
unchecked-icon="clear"
|
||||||
class="float-left"
|
class="float-left"
|
||||||
/> <span v-text="merchant.config.active ? 'Accepting Orders': 'Orders Paused'"></span>
|
/>
|
||||||
|
<span
|
||||||
|
v-text="merchant.config.active ? 'Accepting Orders': 'Orders Paused'"
|
||||||
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<shipping-zones
|
<shipping-zones
|
||||||
|
|
@ -232,15 +235,38 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<template id="key-pair"
|
||||||
|
>{% include("nostrmarket/components/key-pair.html") %}</template
|
||||||
|
>
|
||||||
|
<template id="shipping-zones"
|
||||||
|
>{% include("nostrmarket/components/shipping-zones.html") %}</template
|
||||||
|
>
|
||||||
|
<template id="stall-details"
|
||||||
|
>{% include("nostrmarket/components/stall-details.html") %}</template
|
||||||
|
>
|
||||||
|
<template id="stall-list"
|
||||||
|
>{% include("nostrmarket/components/stall-list.html") %}</template
|
||||||
|
>
|
||||||
|
<template id="order-list"
|
||||||
|
>{% include("nostrmarket/components/order-list.html") %}</template
|
||||||
|
><template id="direct-messages"
|
||||||
|
>{% include("nostrmarket/components/direct-messages.html") %}</template
|
||||||
|
>
|
||||||
|
|
||||||
|
<template id="merchant-details"
|
||||||
|
>{% include("nostrmarket/components/merchant-details.html") %}</template
|
||||||
|
>
|
||||||
|
|
||||||
<script src="{{ url_for('nostrmarket_static', path='js/nostr.bundle.js') }}"></script>
|
<script src="{{ url_for('nostrmarket_static', path='js/nostr.bundle.js') }}"></script>
|
||||||
<script src="{{ url_for('nostrmarket_static', path='js/utils.js') }}"></script>
|
<script src="{{ url_for('nostrmarket_static', path='js/utils.js') }}"></script>
|
||||||
<script src="{{ url_for('nostrmarket_static', path='components/key-pair/key-pair.js') }}"></script>
|
|
||||||
<script src="{{ url_for('nostrmarket_static', path='components/shipping-zones/shipping-zones.js') }}"></script>
|
|
||||||
<script src="{{ url_for('nostrmarket_static', path='components/stall-details/stall-details.js') }}"></script>
|
|
||||||
<script src="{{ url_for('nostrmarket_static', path='components/stall-list/stall-list.js') }}"></script>
|
|
||||||
<script src="{{ url_for('nostrmarket_static', path='components/order-list/order-list.js') }}"></script>
|
|
||||||
<script src="{{ url_for('nostrmarket_static', path='components/direct-messages/direct-messages.js') }}"></script>
|
|
||||||
<script src="{{ url_for('nostrmarket_static', path='components/merchant-details/merchant-details.js') }}"></script>
|
|
||||||
<script src="{{ url_for('nostrmarket_static', path='js/index.js') }}"></script>
|
<script src="{{ url_for('nostrmarket_static', path='js/index.js') }}"></script>
|
||||||
|
|
||||||
|
<script src="{{ static_url_for('nostrmarket/static', 'components/key-pair.js') }}"></script>
|
||||||
|
<script src="{{ static_url_for('nostrmarket/static', 'components/shipping-zones.js') }}"></script>
|
||||||
|
<script src="{{ static_url_for('nostrmarket/static', 'components/stall-details.js') }}"></script>
|
||||||
|
<script src="{{ static_url_for('nostrmarket/static', 'components/stall-list.js') }}"></script>
|
||||||
|
<script src="{{ static_url_for('nostrmarket/static', 'components/order-list.js') }}"></script>
|
||||||
|
<script src="{{ static_url_for('nostrmarket/static', 'components/direct-messages.js') }}"></script>
|
||||||
|
<script src="{{ static_url_for('nostrmarket/static', 'components/merchant-details.js') }}"></script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,59 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>Nostr Market App</title>
|
<title>Nostr Market App</title>
|
||||||
<meta charset=utf-8>
|
<meta charset="utf-8" />
|
||||||
<meta name=description content="A Nostr marketplace">
|
<meta name="description" content="A Nostr marketplace" />
|
||||||
<meta name=format-detection content="telephone=no">
|
<meta name="format-detection" content="telephone=no" />
|
||||||
<meta name=msapplication-tap-highlight content=no>
|
<meta name="msapplication-tap-highlight" content="no" />
|
||||||
<meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width">
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width"
|
||||||
|
/>
|
||||||
|
|
||||||
<script src="{{ url_for('nostrmarket_static', path='market/js/nostr.bundle.js') }}"></script>
|
<script src="{{ url_for('nostrmarket_static', path='market/js/nostr.bundle.js') }}"></script>
|
||||||
<script src="{{ url_for('nostrmarket_static', path='market/js/bolt11-decoder.js') }}"></script>
|
<script src="{{ url_for('nostrmarket_static', path='market/js/bolt11-decoder.js') }}"></script>
|
||||||
<script src="{{ url_for('nostrmarket_static', path='market/js/utils.js') }}"></script>
|
<script src="{{ url_for('nostrmarket_static', path='market/js/utils.js') }}"></script>
|
||||||
|
|
||||||
<link rel=icon type=image/png sizes=128x128
|
<link
|
||||||
href="{{ url_for('nostrmarket_static', path='market/icons/favicon-128x128.png')}}">
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="128x128"
|
||||||
|
href="{{ url_for('nostrmarket_static', path='market/icons/favicon-128x128.png')}}"
|
||||||
|
/>
|
||||||
|
|
||||||
<link rel=icon type=image/png sizes=128x128
|
<link
|
||||||
href="{{ url_for('nostrmarket_static', path='market/icons/favicon-96x96.png')}}">
|
rel="icon"
|
||||||
<link rel=icon type=image/png sizes=128x128
|
type="image/png"
|
||||||
href="{{ url_for('nostrmarket_static', path='market/icons/favicon-32x32.png')}}">
|
sizes="128x128"
|
||||||
<link rel=icon type=image/png sizes=128x128 href="{{ url_for('nostrmarket_static', path='market/favicon.ico')}}">
|
href="{{ url_for('nostrmarket_static', path='market/icons/favicon-96x96.png')}}"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="128x128"
|
||||||
|
href="{{ url_for('nostrmarket_static', path='market/icons/favicon-32x32.png')}}"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="128x128"
|
||||||
|
href="{{ url_for('nostrmarket_static', path='market/favicon.ico')}}"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Note: the .js and .css build IDs must be updated when a new version si released for 'static/market/index.html'-->
|
<!-- Note: the .js and .css build IDs must be updated when a new version si released for 'static/market/index.html'-->
|
||||||
<script type="module" crossorigin
|
<script
|
||||||
src="{{ url_for('nostrmarket_static', path='market/assets/index.923cbbf9.js')}}"></script>
|
type="module"
|
||||||
<link rel="stylesheet" href="{{ url_for('nostrmarket_static', path='market/assets/index.73d462e5.css')}}">
|
crossorigin
|
||||||
|
src="{{ url_for('nostrmarket_static', path='market/assets/index.923cbbf9.js')}}"
|
||||||
|
></script>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('nostrmarket_static', path='market/assets/index.73d462e5.css')}}"
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id=q-app></div>
|
<div id="q-app"></div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
7
toc.md
7
toc.md
|
|
@ -1,22 +1,29 @@
|
||||||
# Terms and Conditions for LNbits Extension
|
# Terms and Conditions for LNbits Extension
|
||||||
|
|
||||||
## 1. Acceptance of Terms
|
## 1. Acceptance of Terms
|
||||||
|
|
||||||
By installing and using the LNbits extension ("Extension"), you agree to be bound by these terms and conditions ("Terms"). If you do not agree to these Terms, do not use the Extension.
|
By installing and using the LNbits extension ("Extension"), you agree to be bound by these terms and conditions ("Terms"). If you do not agree to these Terms, do not use the Extension.
|
||||||
|
|
||||||
## 2. License
|
## 2. License
|
||||||
|
|
||||||
The Extension is free and open-source software, released under [specify the FOSS license here, e.g., GPL-3.0, MIT, etc.]. You are permitted to use, copy, modify, and distribute the Extension under the terms of that license.
|
The Extension is free and open-source software, released under [specify the FOSS license here, e.g., GPL-3.0, MIT, etc.]. You are permitted to use, copy, modify, and distribute the Extension under the terms of that license.
|
||||||
|
|
||||||
## 3. No Warranty
|
## 3. No Warranty
|
||||||
|
|
||||||
The Extension is provided "as is" and with all faults, and the developer expressly disclaims all warranties of any kind, whether express, implied, statutory, or otherwise, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, and any warranties arising out of course of dealing or usage of trade. No advice or information, whether oral or written, obtained from the developer or elsewhere will create any warranty not expressly stated in this Terms.
|
The Extension is provided "as is" and with all faults, and the developer expressly disclaims all warranties of any kind, whether express, implied, statutory, or otherwise, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, and any warranties arising out of course of dealing or usage of trade. No advice or information, whether oral or written, obtained from the developer or elsewhere will create any warranty not expressly stated in this Terms.
|
||||||
|
|
||||||
## 4. Limitation of Liability
|
## 4. Limitation of Liability
|
||||||
|
|
||||||
In no event will the developer be liable to you or any third party for any direct, indirect, incidental, special, consequential, or punitive damages, including lost profit, lost revenue, loss of data, or other damages arising out of or in connection with your use of the Extension, even if the developer has been advised of the possibility of such damages. The foregoing limitation of liability shall apply to the fullest extent permitted by law in the applicable jurisdiction.
|
In no event will the developer be liable to you or any third party for any direct, indirect, incidental, special, consequential, or punitive damages, including lost profit, lost revenue, loss of data, or other damages arising out of or in connection with your use of the Extension, even if the developer has been advised of the possibility of such damages. The foregoing limitation of liability shall apply to the fullest extent permitted by law in the applicable jurisdiction.
|
||||||
|
|
||||||
## 5. Modification of Terms
|
## 5. Modification of Terms
|
||||||
|
|
||||||
The developer reserves the right to modify these Terms at any time. You are advised to review these Terms periodically for any changes. Changes to these Terms are effective when they are posted on the appropriate location within or associated with the Extension.
|
The developer reserves the right to modify these Terms at any time. You are advised to review these Terms periodically for any changes. Changes to these Terms are effective when they are posted on the appropriate location within or associated with the Extension.
|
||||||
|
|
||||||
## 6. General Provisions
|
## 6. General Provisions
|
||||||
|
|
||||||
If any provision of these Terms is held to be invalid or unenforceable, that provision will be enforced to the maximum extent permissible, and the other provisions of these Terms will remain in full force and effect. These Terms constitute the entire agreement between you and the developer regarding the use of the Extension.
|
If any provision of these Terms is held to be invalid or unenforceable, that provision will be enforced to the maximum extent permissible, and the other provisions of these Terms will remain in full force and effect. These Terms constitute the entire agreement between you and the developer regarding the use of the Extension.
|
||||||
|
|
||||||
## 7. Contact Information
|
## 7. Contact Information
|
||||||
|
|
||||||
If you have any questions about these Terms, please contact the developer at [developer's contact information].
|
If you have any questions about these Terms, please contact the developer at [developer's contact information].
|
||||||
11
views.py
11
views.py
|
|
@ -1,13 +1,8 @@
|
||||||
import json
|
from fastapi import Depends, Request
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from fastapi import Depends, Query, Request
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from loguru import logger
|
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
|
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_user_exists
|
||||||
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
from . import nostrmarket_ext, nostrmarket_renderer
|
from . import nostrmarket_ext, nostrmarket_renderer
|
||||||
|
|
||||||
|
|
@ -18,7 +13,7 @@ templates = Jinja2Templates(directory="templates")
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
return nostrmarket_renderer().TemplateResponse(
|
return nostrmarket_renderer().TemplateResponse(
|
||||||
"nostrmarket/index.html",
|
"nostrmarket/index.html",
|
||||||
{"request": request, "user": user.dict()},
|
{"request": request, "user": user.json()},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
211
views_api.py
211
views_api.py
|
|
@ -4,16 +4,14 @@ from typing import List, Optional
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.core.services import websocket_updater
|
from lnbits.core.services import websocket_updater
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
get_key_type,
|
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.utils.exchange_rates import currencies
|
from lnbits.utils.exchange_rates import currencies
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from . import nostr_client, nostrmarket_ext
|
from . import nostr_client, nostrmarket_ext
|
||||||
from .crud import (
|
from .crud import (
|
||||||
|
|
@ -71,9 +69,6 @@ from .models import (
|
||||||
PartialDirectMessage,
|
PartialDirectMessage,
|
||||||
PartialMerchant,
|
PartialMerchant,
|
||||||
PartialOrder,
|
PartialOrder,
|
||||||
PartialProduct,
|
|
||||||
PartialStall,
|
|
||||||
PartialZone,
|
|
||||||
PaymentOption,
|
PaymentOption,
|
||||||
PaymentRequest,
|
PaymentRequest,
|
||||||
Product,
|
Product,
|
||||||
|
|
@ -81,16 +76,16 @@ from .models import (
|
||||||
Zone,
|
Zone,
|
||||||
)
|
)
|
||||||
from .services import (
|
from .services import (
|
||||||
reply_to_structured_dm,
|
|
||||||
build_order_with_payment,
|
build_order_with_payment,
|
||||||
create_or_update_order_from_dm,
|
create_or_update_order_from_dm,
|
||||||
|
reply_to_structured_dm,
|
||||||
resubscribe_to_all_merchants,
|
resubscribe_to_all_merchants,
|
||||||
sign_and_send_to_nostr,
|
sign_and_send_to_nostr,
|
||||||
subscribe_to_all_merchants,
|
subscribe_to_all_merchants,
|
||||||
update_merchant_to_nostr,
|
update_merchant_to_nostr,
|
||||||
)
|
)
|
||||||
|
|
||||||
######################################## MERCHANT ########################################
|
######################################## MERCHANT ######################################
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.post("/api/v1/merchant")
|
@nostrmarket_ext.post("/api/v1/merchant")
|
||||||
|
|
@ -101,16 +96,16 @@ async def api_create_merchant(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_by_pubkey(data.public_key)
|
merchant = await get_merchant_by_pubkey(data.public_key)
|
||||||
assert merchant == None, "A merchant already uses this public key"
|
assert merchant is None, "A merchant already uses this public key"
|
||||||
|
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant == None, "A merchant already exists for this user"
|
assert merchant is None, "A merchant already exists for this user"
|
||||||
|
|
||||||
merchant = await create_merchant(wallet.wallet.user, data)
|
merchant = await create_merchant(wallet.wallet.user, data)
|
||||||
|
|
||||||
await create_zone(
|
await create_zone(
|
||||||
merchant.id,
|
merchant.id,
|
||||||
PartialZone(
|
Zone(
|
||||||
id=f"online-{merchant.public_key}",
|
id=f"online-{merchant.public_key}",
|
||||||
name="Online",
|
name="Online",
|
||||||
currency="sat",
|
currency="sat",
|
||||||
|
|
@ -128,13 +123,13 @@ async def api_create_merchant(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot create merchant",
|
detail="Cannot create merchant",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/merchant")
|
@nostrmarket_ext.get("/api/v1/merchant")
|
||||||
|
|
@ -145,11 +140,12 @@ async def api_get_merchant(
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
if not merchant:
|
if not merchant:
|
||||||
return
|
return None
|
||||||
|
|
||||||
merchant = await touch_merchant(wallet.wallet.user, merchant.id)
|
merchant = await touch_merchant(wallet.wallet.user, merchant.id)
|
||||||
|
assert merchant
|
||||||
last_dm_time = await get_last_direct_messages_time(merchant.id)
|
last_dm_time = await get_last_direct_messages_time(merchant.id)
|
||||||
|
assert merchant.time
|
||||||
merchant.config.restore_in_progress = (merchant.time - last_dm_time) < 30
|
merchant.config.restore_in_progress = (merchant.time - last_dm_time) < 30
|
||||||
|
|
||||||
return merchant
|
return merchant
|
||||||
|
|
@ -158,7 +154,7 @@ async def api_get_merchant(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot get merchant",
|
detail="Cannot get merchant",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.delete("/api/v1/merchant/{merchant_id}")
|
@nostrmarket_ext.delete("/api/v1/merchant/{merchant_id}")
|
||||||
|
|
@ -186,16 +182,17 @@ async def api_delete_merchant(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot get merchant",
|
detail="Cannot get merchant",
|
||||||
)
|
) from ex
|
||||||
finally:
|
finally:
|
||||||
await subscribe_to_all_merchants()
|
await subscribe_to_all_merchants()
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.put("/api/v1/merchant/{merchant_id}/nostr")
|
@nostrmarket_ext.put("/api/v1/merchant/{merchant_id}/nostr")
|
||||||
async def api_republish_merchant(
|
async def api_republish_merchant(
|
||||||
merchant_id: str,
|
merchant_id: str,
|
||||||
|
|
@ -213,13 +210,14 @@ async def api_republish_merchant(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot republish to nostr",
|
detail="Cannot republish to nostr",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/merchant/{merchant_id}/nostr")
|
@nostrmarket_ext.get("/api/v1/merchant/{merchant_id}/nostr")
|
||||||
async def api_refresh_merchant(
|
async def api_refresh_merchant(
|
||||||
|
|
@ -237,13 +235,13 @@ async def api_refresh_merchant(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot refresh from nostr",
|
detail="Cannot refresh from nostr",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.put("/api/v1/merchant/{merchant_id}/toggle")
|
@nostrmarket_ext.put("/api/v1/merchant/{merchant_id}/toggle")
|
||||||
|
|
@ -264,17 +262,17 @@ async def api_toggle_merchant(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot get merchant",
|
detail="Cannot get merchant",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.delete("/api/v1/merchant/{merchant_id}/nostr")
|
@nostrmarket_ext.delete("/api/v1/merchant/{merchant_id}/nostr")
|
||||||
async def api_delete_merchant(
|
async def api_delete_merchant_on_nostr(
|
||||||
merchant_id: str,
|
merchant_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
):
|
):
|
||||||
|
|
@ -290,20 +288,22 @@ async def api_delete_merchant(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot get merchant",
|
detail="Cannot get merchant",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
######################################## ZONES ########################################
|
######################################## ZONES ########################################
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/zone")
|
@nostrmarket_ext.get("/api/v1/zone")
|
||||||
async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[Zone]:
|
async def api_get_zones(
|
||||||
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
|
) -> List[Zone]:
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant, "Merchant cannot be found"
|
assert merchant, "Merchant cannot be found"
|
||||||
|
|
@ -312,18 +312,18 @@ async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot get zone",
|
detail="Cannot get zone",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.post("/api/v1/zone")
|
@nostrmarket_ext.post("/api/v1/zone")
|
||||||
async def api_create_zone(
|
async def api_create_zone(
|
||||||
data: PartialZone, wallet: WalletTypeInfo = Depends(require_admin_key)
|
data: Zone, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
|
|
@ -334,13 +334,13 @@ async def api_create_zone(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot create zone",
|
detail="Cannot create zone",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.patch("/api/v1/zone/{zone_id}")
|
@nostrmarket_ext.patch("/api/v1/zone/{zone_id}")
|
||||||
|
|
@ -365,15 +365,14 @@ async def api_update_zone(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except HTTPException as ex:
|
|
||||||
raise ex
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot update zone",
|
detail="Cannot update zone",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.delete("/api/v1/zone/{zone_id}")
|
@nostrmarket_ext.delete("/api/v1/zone/{zone_id}")
|
||||||
|
|
@ -394,13 +393,13 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot delete zone",
|
detail="Cannot delete zone",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
######################################## STALLS ########################################
|
######################################## STALLS ########################################
|
||||||
|
|
@ -408,7 +407,7 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi
|
||||||
|
|
||||||
@nostrmarket_ext.post("/api/v1/stall")
|
@nostrmarket_ext.post("/api/v1/stall")
|
||||||
async def api_create_stall(
|
async def api_create_stall(
|
||||||
data: PartialStall,
|
data: Stall,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> Stall:
|
) -> Stall:
|
||||||
try:
|
try:
|
||||||
|
|
@ -430,13 +429,13 @@ async def api_create_stall(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot create stall",
|
detail="Cannot create stall",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.put("/api/v1/stall/{stall_id}")
|
@nostrmarket_ext.put("/api/v1/stall/{stall_id}")
|
||||||
|
|
@ -459,23 +458,24 @@ async def api_update_stall(
|
||||||
await update_stall(merchant.id, stall)
|
await update_stall(merchant.id, stall)
|
||||||
|
|
||||||
return stall
|
return stall
|
||||||
except HTTPException as ex:
|
|
||||||
raise ex
|
|
||||||
except (ValueError, AssertionError) as ex:
|
except (ValueError, AssertionError) as ex:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot update stall",
|
detail="Cannot update stall",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/stall/{stall_id}")
|
@nostrmarket_ext.get("/api/v1/stall/{stall_id}")
|
||||||
async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_get_stall(
|
||||||
|
stall_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant, "Merchant cannot be found"
|
assert merchant, "Merchant cannot be found"
|
||||||
|
|
@ -490,7 +490,7 @@ async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except HTTPException as ex:
|
except HTTPException as ex:
|
||||||
raise ex
|
raise ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
|
@ -498,12 +498,13 @@ async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot get stall",
|
detail="Cannot get stall",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/stall")
|
@nostrmarket_ext.get("/api/v1/stall")
|
||||||
async def api_get_stalls(
|
async def api_get_stalls(
|
||||||
pending: Optional[bool] = False, wallet: WalletTypeInfo = Depends(get_key_type)
|
pending: Optional[bool] = False,
|
||||||
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
|
|
@ -514,13 +515,13 @@ async def api_get_stalls(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot get stalls",
|
detail="Cannot get stalls",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/stall/product/{stall_id}")
|
@nostrmarket_ext.get("/api/v1/stall/product/{stall_id}")
|
||||||
|
|
@ -538,13 +539,13 @@ async def api_get_stall_products(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot get stall products",
|
detail="Cannot get stall products",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/stall/order/{stall_id}")
|
@nostrmarket_ext.get("/api/v1/stall/order/{stall_id}")
|
||||||
|
|
@ -566,13 +567,13 @@ async def api_get_stall_orders(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot get stall products",
|
detail="Cannot get stall products",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.delete("/api/v1/stall/{stall_id}")
|
@nostrmarket_ext.delete("/api/v1/stall/{stall_id}")
|
||||||
|
|
@ -600,23 +601,21 @@ async def api_delete_stall(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except HTTPException as ex:
|
|
||||||
raise ex
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot delete stall",
|
detail="Cannot delete stall",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
######################################## PRODUCTS ########################################
|
######################################## PRODUCTS ######################################
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.post("/api/v1/product")
|
@nostrmarket_ext.post("/api/v1/product")
|
||||||
async def api_create_product(
|
async def api_create_product(
|
||||||
data: PartialProduct,
|
data: Product,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> Product:
|
) -> Product:
|
||||||
try:
|
try:
|
||||||
|
|
@ -639,13 +638,13 @@ async def api_create_product(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot create product",
|
detail="Cannot create product",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.patch("/api/v1/product/{product_id}")
|
@nostrmarket_ext.patch("/api/v1/product/{product_id}")
|
||||||
|
|
@ -675,13 +674,13 @@ async def api_update_product(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot update product",
|
detail="Cannot update product",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/product/{product_id}")
|
@nostrmarket_ext.get("/api/v1/product/{product_id}")
|
||||||
|
|
@ -699,13 +698,13 @@ async def api_get_product(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot get product",
|
detail="Cannot get product",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.delete("/api/v1/product/{product_id}")
|
@nostrmarket_ext.delete("/api/v1/product/{product_id}")
|
||||||
|
|
@ -731,15 +730,13 @@ async def api_delete_product(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except HTTPException as ex:
|
|
||||||
raise ex
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot delete product",
|
detail="Cannot delete product",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
######################################## ORDERS ########################################
|
######################################## ORDERS ########################################
|
||||||
|
|
@ -764,15 +761,13 @@ async def api_get_order(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except HTTPException as ex:
|
|
||||||
raise ex
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot get order",
|
detail="Cannot get order",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/order")
|
@nostrmarket_ext.get("/api/v1/order")
|
||||||
|
|
@ -780,7 +775,7 @@ async def api_get_orders(
|
||||||
paid: Optional[bool] = None,
|
paid: Optional[bool] = None,
|
||||||
shipped: Optional[bool] = None,
|
shipped: Optional[bool] = None,
|
||||||
pubkey: Optional[str] = None,
|
pubkey: Optional[str] = None,
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
|
|
@ -794,13 +789,13 @@ async def api_get_orders(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot get orders",
|
detail="Cannot get orders",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.patch("/api/v1/order/{order_id}")
|
@nostrmarket_ext.patch("/api/v1/order/{order_id}")
|
||||||
|
|
@ -809,7 +804,7 @@ async def api_update_order_status(
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> Order:
|
) -> Order:
|
||||||
try:
|
try:
|
||||||
assert data.shipped != None, "Shipped value is required for order"
|
assert data.shipped is not None, "Shipped value is required for order"
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant, "Merchant cannot be found for order {data.id}"
|
assert merchant, "Merchant cannot be found for order {data.id}"
|
||||||
|
|
||||||
|
|
@ -852,20 +847,20 @@ async def api_update_order_status(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot update order",
|
detail="Cannot update order",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.put("/api/v1/order/restore/{event_id}")
|
@nostrmarket_ext.put("/api/v1/order/restore/{event_id}")
|
||||||
async def api_restore_order(
|
async def api_restore_order(
|
||||||
event_id: str,
|
event_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> Order:
|
) -> Optional[Order]:
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant, "Merchant cannot be found"
|
assert merchant, "Merchant cannot be found"
|
||||||
|
|
@ -881,13 +876,13 @@ async def api_restore_order(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot restore order",
|
detail="Cannot restore order",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.put("/api/v1/orders/restore")
|
@nostrmarket_ext.put("/api/v1/orders/restore")
|
||||||
|
|
@ -906,20 +901,20 @@ async def api_restore_orders(
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Failed to restore order from event '{dm.event_id}': '{str(e)}'."
|
f"Failed to restore order from event '{dm.event_id}': '{e!s}'."
|
||||||
)
|
)
|
||||||
|
|
||||||
except AssertionError as ex:
|
except AssertionError as ex:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot restore orders",
|
detail="Cannot restore orders",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.put("/api/v1/order/reissue")
|
@nostrmarket_ext.put("/api/v1/order/reissue")
|
||||||
|
|
@ -955,7 +950,9 @@ async def api_reissue_order_invoice(
|
||||||
**order_update,
|
**order_update,
|
||||||
)
|
)
|
||||||
payment_req = PaymentRequest(
|
payment_req = PaymentRequest(
|
||||||
id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)], message=receipt
|
id=data.id,
|
||||||
|
payment_options=[PaymentOption(type="ln", link=invoice)],
|
||||||
|
message=receipt,
|
||||||
)
|
)
|
||||||
response = {
|
response = {
|
||||||
"type": DirectMessageType.PAYMENT_REQUEST.value,
|
"type": DirectMessageType.PAYMENT_REQUEST.value,
|
||||||
|
|
@ -975,25 +972,25 @@ async def api_reissue_order_invoice(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot reissue order invoice",
|
detail="Cannot reissue order invoice",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
######################################## DIRECT MESSAGES ########################################
|
######################################## DIRECT MESSAGES ###############################
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/message/{public_key}")
|
@nostrmarket_ext.get("/api/v1/message/{public_key}")
|
||||||
async def api_get_messages(
|
async def api_get_messages(
|
||||||
public_key: str, wallet: WalletTypeInfo = Depends(get_key_type)
|
public_key: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||||
) -> List[DirectMessage]:
|
) -> List[DirectMessage]:
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant, f"Merchant cannot be found"
|
assert merchant, "Merchant cannot be found"
|
||||||
|
|
||||||
messages = await get_direct_messages(merchant.id, public_key)
|
messages = await get_direct_messages(merchant.id, public_key)
|
||||||
await update_customer_no_unread_messages(merchant.id, public_key)
|
await update_customer_no_unread_messages(merchant.id, public_key)
|
||||||
|
|
@ -1002,13 +999,13 @@ async def api_get_messages(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot get direct message",
|
detail="Cannot get direct message",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.post("/api/v1/message")
|
@nostrmarket_ext.post("/api/v1/message")
|
||||||
|
|
@ -1017,7 +1014,7 @@ async def api_create_message(
|
||||||
) -> DirectMessage:
|
) -> DirectMessage:
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant, f"Merchant cannot be found"
|
assert merchant, "Merchant cannot be found"
|
||||||
|
|
||||||
dm_event = merchant.build_dm_event(data.message, data.public_key)
|
dm_event = merchant.build_dm_event(data.message, data.public_key)
|
||||||
data.event_id = dm_event.id
|
data.event_id = dm_event.id
|
||||||
|
|
@ -1031,38 +1028,38 @@ async def api_create_message(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot create message",
|
detail="Cannot create message",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
######################################## CUSTOMERS ########################################
|
######################################## CUSTOMERS #####################################
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/customer")
|
@nostrmarket_ext.get("/api/v1/customer")
|
||||||
async def api_get_customers(
|
async def api_get_customers(
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
) -> List[Customer]:
|
) -> List[Customer]:
|
||||||
try:
|
try:
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant, f"Merchant cannot be found"
|
assert merchant, "Merchant cannot be found"
|
||||||
return await get_customers(merchant.id)
|
return await get_customers(merchant.id)
|
||||||
|
|
||||||
except AssertionError as ex:
|
except AssertionError as ex:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot create message",
|
detail="Cannot create message",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.post("/api/v1/customer")
|
@nostrmarket_ext.post("/api/v1/customer")
|
||||||
|
|
@ -1079,7 +1076,7 @@ async def api_create_customer(
|
||||||
assert merchant.id == data.merchant_id, "Invalid merchant id for user"
|
assert merchant.id == data.merchant_id, "Invalid merchant id for user"
|
||||||
|
|
||||||
existing_customer = await get_customer(merchant.id, pubkey)
|
existing_customer = await get_customer(merchant.id, pubkey)
|
||||||
assert existing_customer == None, "This public key already exists"
|
assert existing_customer is None, "This public key already exists"
|
||||||
|
|
||||||
customer = await create_customer(
|
customer = await create_customer(
|
||||||
merchant.id, Customer(merchant_id=merchant.id, public_key=pubkey)
|
merchant.id, Customer(merchant_id=merchant.id, public_key=pubkey)
|
||||||
|
|
@ -1092,13 +1089,13 @@ async def api_create_customer(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=str(ex),
|
detail=str(ex),
|
||||||
)
|
) from ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Cannot create customer",
|
detail="Cannot create customer",
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
######################################## OTHER ########################################
|
######################################## OTHER ########################################
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue