diff --git a/crud.py b/crud.py
index 96c5bf0..e66f639 100644
--- a/crud.py
+++ b/crud.py
@@ -4,7 +4,7 @@ from typing import List, Optional
from lnbits.helpers import urlsafe_short_hash
from . import db
-from .models import Merchant, PartialMerchant, PartialZone, Zone
+from .models import Merchant, PartialMerchant, PartialStall, PartialZone, Stall, Zone
######################################## MERCHANT ########################################
@@ -105,3 +105,64 @@ async def get_zones(user_id: str) -> List[Zone]:
async def delete_zone(zone_id: str) -> None:
await db.execute("DELETE FROM nostrmarket.zones WHERE id = ?", (zone_id,))
+
+
+######################################## STALL ########################################
+
+
+async def create_stall(user_id: str, data: PartialStall) -> Stall:
+ stall_id = urlsafe_short_hash()
+ await db.execute(
+ f"""
+ INSERT INTO nostrmarket.stalls (user_id, id, wallet, name, currency, zones, meta)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ user_id,
+ stall_id,
+ data.wallet,
+ data.name,
+ data.currency,
+ json.dumps(data.shipping_zones),
+ json.dumps(dict(data.config)),
+ ),
+ )
+
+ stall = await get_stall(user_id, stall_id)
+ assert stall, "Newly created stall couldn't be retrieved"
+ return stall
+
+
+async def get_stall(user_id: str, stall_id: str) -> Optional[Stall]:
+ row = await db.fetchone(
+ "SELECT * FROM nostrmarket.stalls WHERE user_id = ? AND id = ?",
+ (
+ user_id,
+ stall_id,
+ ),
+ )
+ return Stall.from_row(row) if row else None
+
+
+async def get_stalls(user_id: str) -> List[Stall]:
+ rows = await db.fetchone(
+ "SELECT * FROM nostrmarket.stalls WHERE user_id = ?",
+ (user_id,),
+ )
+ return [Stall.from_row(row) for row in rows]
+
+
+async def update_stall(user_id: str, stall_id: str, **kwargs) -> Optional[Stall]:
+ q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
+ await db.execute(
+ f"UPDATE market.stalls SET {q} WHERE user_id = ? AND id = ?",
+ (*kwargs.values(), user_id, stall_id),
+ )
+ row = await db.fetchone(
+ "SELECT * FROM market.stalls WHERE user_id =? AND id = ?",
+ (
+ user_id,
+ stall_id,
+ ),
+ )
+ return Stall.from_row(row) if row else None
diff --git a/migrations.py b/migrations.py
index 04a4fe1..6d20f9b 100644
--- a/migrations.py
+++ b/migrations.py
@@ -18,15 +18,17 @@ async def m001_initial(db):
"""
Initial stalls table.
"""
+ # user_id, id, wallet, name, currency, zones, meta
await db.execute(
"""
CREATE TABLE nostrmarket.stalls (
+ user_id TEXT NOT NULL,
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
name TEXT NOT NULL,
currency TEXT,
- shipping_zones TEXT NOT NULL,
- rating REAL DEFAULT 0
+ zones TEXT NOT NULL DEFAULT '[]',
+ meta TEXT NOT NULL DEFAULT '{}'
);
"""
)
diff --git a/models.py b/models.py
index 40bb57f..8f98f24 100644
--- a/models.py
+++ b/models.py
@@ -43,3 +43,30 @@ class Zone(PartialZone):
zone = cls(**dict(row))
zone.countries = json.loads(row["regions"])
return zone
+
+
+######################################## STALLS ########################################
+
+
+class StallConfig(BaseModel):
+ image_url: Optional[str]
+ fiat_base_multiplier: int = 1 # todo: reminder wht is this for?
+
+
+class PartialStall(BaseModel):
+ wallet: str
+ name: str
+ currency: str = "sat"
+ shipping_zones: List[str] = []
+ config: StallConfig = StallConfig()
+
+
+class Stall(PartialStall):
+ id: str
+
+ @classmethod
+ def from_row(cls, row: Row) -> "Stall":
+ stall = cls(**dict(row))
+ stall.config = StallConfig(**json.loads(row["meta"]))
+ stall.shipping_zones = json.loads(row["zones"])
+ return stall
diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html
new file mode 100644
index 0000000..6778614
--- /dev/null
+++ b/static/components/stall-list/stall-list.html
@@ -0,0 +1,157 @@
+
+
+
+ New Stall
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Create Stall
+ Cancel
+
+
+
+
+
+
diff --git a/static/components/stall-list/stall-list.js b/static/components/stall-list/stall-list.js
new file mode 100644
index 0000000..6ba13dd
--- /dev/null
+++ b/static/components/stall-list/stall-list.js
@@ -0,0 +1,84 @@
+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: [],
+ currencies: [],
+ stallDialog: {
+ show: false,
+ data: {
+ name: '',
+ wallet: null,
+ currency: 'sat',
+ shippingZones: []
+ }
+ },
+ zoneOptions: []
+ }
+ },
+ methods: {
+ sendStallFormData: async function () {
+ console.log('### sendStallFormData', this.stallDialog.data)
+
+ await this.createStall({
+ name: this.stallDialog.data.name,
+ wallet: this.stallDialog.data.wallet,
+ currency: this.stallDialog.data.currency,
+ shipping_zones: this.stallDialog.data.shippingZones.map(z => z.id),
+ config: {}
+ })
+ },
+ createStall: async function (stall) {
+ console.log('### createStall', stall)
+ try {
+ const {data} = await LNbits.api.request(
+ 'POST',
+ '/nostrmarket/api/v1/stall',
+ this.adminkey,
+ stall
+ )
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ },
+ getCurrencies: async function () {
+ try {
+ const {data} = await LNbits.api.request(
+ 'GET',
+ '/nostrmarket/api/v1/currencies',
+ this.inkey
+ )
+
+ this.currencies = ['sat', ...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.zoneOptions = data.map(z => ({
+ id: z.id,
+ label: `${z.name} (${z.countries.join(', ')})`
+ }))
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ }
+ },
+ created: async function () {
+ await this.getCurrencies()
+ await this.getZones()
+ }
+ })
+}
diff --git a/static/js/index.js b/static/js/index.js
index 68ece51..343b5eb 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -1,9 +1,10 @@
const merchant = async () => {
Vue.component(VueQrcode.name, VueQrcode)
- await stallDetails('static/components/stall-details/stall-details.html')
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')
const nostr = window.NostrTools
diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html
index 1235bf2..8e61d74 100644
--- a/templates/nostrmarket/index.html
+++ b/templates/nostrmarket/index.html
@@ -86,15 +86,16 @@
>
+
+
+
+
+
-
-
-
-
-
@@ -115,9 +116,10 @@
-
+
+
{% endblock %}
diff --git a/views_api.py b/views_api.py
index f294de0..2488b7c 100644
--- a/views_api.py
+++ b/views_api.py
@@ -16,14 +16,18 @@ from lnbits.utils.exchange_rates import currencies
from . import nostrmarket_ext
from .crud import (
create_merchant,
+ create_stall,
create_zone,
delete_zone,
get_merchant_for_user,
+ get_stall,
+ get_stalls,
get_zone,
get_zones,
+ update_stall,
update_zone,
)
-from .models import Merchant, PartialMerchant, PartialZone, Zone
+from .models import Merchant, PartialMerchant, PartialStall, PartialZone, Stall, Zone
######################################## MERCHANT ########################################
@@ -138,6 +142,86 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi
)
+######################################## STALLS ########################################
+
+
+@nostrmarket_ext.post("/api/v1/stall")
+async def api_create_stall(
+ data: PartialStall,
+ wallet: WalletTypeInfo = Depends(require_invoice_key),
+):
+ try:
+ stall = await create_stall(wallet.wallet.user, data=data)
+ return stall.dict()
+ except Exception as ex:
+ logger.warning(ex)
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail="Cannot create stall",
+ )
+
+
+@nostrmarket_ext.put("/api/v1/stall/{stall_id}")
+async def api_update_stall(
+ data: Stall,
+ wallet: WalletTypeInfo = Depends(require_invoice_key),
+):
+ try:
+ stall = await get_stall(wallet.wallet.user, data.id)
+ if not stall:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Stall does not exist.",
+ )
+ stall = await update_stall(wallet.wallet.user, data.id, **data.dict())
+ assert stall, "Cannot fetch updated stall"
+ return stall.dict()
+ except HTTPException as ex:
+ raise ex
+ except Exception as ex:
+ logger.warning(ex)
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail="Cannot create stall",
+ )
+
+
+@nostrmarket_ext.get("/api/v1/stall/{stall_id}")
+async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
+ try:
+ stall = await get_stall(wallet.wallet.user, stall_id)
+ if not stall:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Stall does not exist.",
+ )
+ return stall
+ except HTTPException as ex:
+ raise ex
+ except Exception as ex:
+ logger.warning(ex)
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail="Cannot create stall",
+ )
+
+
+@nostrmarket_ext.get("/api/v1/stall")
+async def api_gey_stalls(wallet: WalletTypeInfo = Depends(get_key_type)):
+ try:
+ stalls = await get_stalls(wallet.wallet.user)
+ return stalls
+ except Exception as ex:
+ logger.warning(ex)
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail="Cannot create stall",
+ )
+
+
+######################################## OTHER ########################################
+
+
@nostrmarket_ext.get("/api/v1/currencies")
async def api_list_currencies_available():
return list(currencies.keys())