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())