diff --git a/crud.py b/crud.py
index 8f41bff..96c5bf0 100644
--- a/crud.py
+++ b/crud.py
@@ -1,10 +1,12 @@
import json
-from typing import Optional
+from typing import List, Optional
from lnbits.helpers import urlsafe_short_hash
from . import db
-from .models import Merchant, PartialMerchant
+from .models import Merchant, PartialMerchant, PartialZone, Zone
+
+######################################## MERCHANT ########################################
async def create_merchant(user_id: str, m: PartialMerchant) -> Merchant:
@@ -40,3 +42,66 @@ async def get_merchant_for_user(user_id: str) -> Optional[Merchant]:
)
return Merchant.from_row(row) if row else None
+
+
+######################################## ZONES ########################################
+
+
+async def create_zone(user_id: str, data: PartialZone) -> Zone:
+ zone_id = urlsafe_short_hash()
+ await db.execute(
+ f"""
+ INSERT INTO nostrmarket.zones (
+ id,
+ user_id,
+ name,
+ currency,
+ cost,
+ regions
+
+ )
+ VALUES (?, ?, ?, ?, ?, ?)
+ """,
+ (
+ zone_id,
+ user_id,
+ data.name,
+ data.currency,
+ data.cost,
+ json.dumps(data.countries),
+ ),
+ )
+
+ zone = await get_zone(user_id, zone_id)
+ assert zone, "Newly created zone couldn't be retrieved"
+ return zone
+
+
+async def update_zone(user_id: str, z: Zone) -> Optional[Zone]:
+ await db.execute(
+ f"UPDATE nostrmarket.zones SET name = ?, cost = ?, regions = ? WHERE id = ? AND user_id = ?",
+ (z.name, z.cost, json.dumps(z.countries), z.id, user_id),
+ )
+ return await get_zone(user_id, z.id)
+
+
+async def get_zone(user_id: str, zone_id: str) -> Optional[Zone]:
+ row = await db.fetchone(
+ "SELECT * FROM nostrmarket.zones WHERE user_id = ? AND id = ?",
+ (
+ user_id,
+ zone_id,
+ ),
+ )
+ return Zone.from_row(row) if row else None
+
+
+async def get_zones(user_id: str) -> List[Zone]:
+ rows = await db.fetchall(
+ "SELECT * FROM nostrmarket.zones WHERE user_id = ?", (user_id,)
+ )
+ return [Zone.from_row(row) for row in rows]
+
+
+async def delete_zone(zone_id: str) -> None:
+ await db.execute("DELETE FROM nostrmarket.zones WHERE id = ?", (zone_id,))
diff --git a/migrations.py b/migrations.py
index 51567b5..04a4fe1 100644
--- a/migrations.py
+++ b/migrations.py
@@ -58,8 +58,10 @@ async def m001_initial(db):
CREATE TABLE nostrmarket.zones (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
+ name TEXT NOT NULL,
+ currency TEXT NOT NULL,
cost REAL NOT NULL,
- countries TEXT NOT NULL
+ regions TEXT NOT NULL DEFAULT '[]'
);
"""
)
diff --git a/models.py b/models.py
index e6a4e5e..40bb57f 100644
--- a/models.py
+++ b/models.py
@@ -1,10 +1,12 @@
import json
from sqlite3 import Row
-from typing import Optional
+from typing import List, Optional
+from fastapi import Query
from pydantic import BaseModel
+######################################## MERCHANT ########################################
class MerchantConfig(BaseModel):
name: Optional[str]
@@ -23,3 +25,21 @@ class Merchant(PartialMerchant):
merchant = cls(**dict(row))
merchant.config = MerchantConfig(**json.loads(row["meta"]))
return merchant
+
+
+######################################## ZONES ########################################
+class PartialZone(BaseModel):
+ name: Optional[str]
+ currency: str
+ cost: float
+ countries: List[str] = []
+
+
+class Zone(PartialZone):
+ id: str
+
+ @classmethod
+ def from_row(cls, row: Row) -> "Zone":
+ zone = cls(**dict(row))
+ zone.countries = json.loads(row["regions"])
+ return zone
diff --git a/static/components/shipping-zones/shipping-zones.html b/static/components/shipping-zones/shipping-zones.html
new file mode 100644
index 0000000..23b55b5
--- /dev/null
+++ b/static/components/shipping-zones/shipping-zones.html
@@ -0,0 +1,98 @@
+
+
+
+
+
+ New Shipping Zone
+ Create a new shipping zone
+
+
+
+
+ {{zone.name}}
+ {{zone.countries.join(", ")}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Update
+ Delete
+
+
+ Create Shipping Zone
+
+
+
Cancel
+
+
+
+
+
diff --git a/static/components/shipping-zones/shipping-zones.js b/static/components/shipping-zones/shipping-zones.js
new file mode 100644
index 0000000..71e3ae6
--- /dev/null
+++ b/static/components/shipping-zones/shipping-zones.js
@@ -0,0 +1,189 @@
+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
+ console.log('### data', data)
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ },
+ sendZoneFormData: async function () {
+ console.log('### data', this.zoneDialog.data)
+ 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) {
+ console.log('### newZone', 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()
+ }
+ })
+}
diff --git a/static/js/index.js b/static/js/index.js
index 561c6ec..68ece51 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -3,6 +3,7 @@ const merchant = async () => {
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')
const nostr = window.NostrTools
@@ -12,6 +13,7 @@ const merchant = async () => {
data: function () {
return {
merchant: {},
+ shippingZones: [],
showKeys: false
}
},
@@ -40,9 +42,9 @@ const merchant = async () => {
getMerchant: async function () {
try {
const {data} = await LNbits.api.request(
- 'get',
+ 'GET',
'/nostrmarket/api/v1/merchant',
- this.g.user.wallets[0].adminkey
+ this.g.user.wallets[0].inkey
)
this.merchant = data
} catch (error) {
diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html
index fc154b4..b4378d8 100644
--- a/templates/nostrmarket/index.html
+++ b/templates/nostrmarket/index.html
@@ -61,12 +61,18 @@
-
+
+
+
+
+
Show Public or Private keys
@@ -103,6 +109,7 @@
+
{% endblock %}
diff --git a/views_api.py b/views_api.py
index ad4d66e..f294de0 100644
--- a/views_api.py
+++ b/views_api.py
@@ -1,15 +1,31 @@
from http import HTTPStatus
-from typing import Optional
+from typing import List, Optional
from fastapi import Depends
from fastapi.exceptions import HTTPException
from loguru import logger
-from lnbits.decorators import WalletTypeInfo, require_admin_key, require_invoice_key
+from lnbits.decorators import (
+ WalletTypeInfo,
+ get_key_type,
+ require_admin_key,
+ require_invoice_key,
+)
+from lnbits.utils.exchange_rates import currencies
from . import nostrmarket_ext
-from .crud import create_merchant, get_merchant_for_user
-from .models import Merchant, PartialMerchant
+from .crud import (
+ create_merchant,
+ create_zone,
+ delete_zone,
+ get_merchant_for_user,
+ get_zone,
+ get_zones,
+ update_zone,
+)
+from .models import Merchant, PartialMerchant, PartialZone, Zone
+
+######################################## MERCHANT ########################################
@nostrmarket_ext.post("/api/v1/merchant")
@@ -43,3 +59,85 @@ async def api_get_merchant(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot create merchant",
)
+
+
+######################################## ZONES ########################################
+
+
+@nostrmarket_ext.get("/api/v1/zone")
+async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[Zone]:
+ try:
+ return await get_zones(wallet.wallet.user)
+ except Exception as ex:
+ logger.warning(ex)
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail="Cannot create merchant",
+ )
+
+
+@nostrmarket_ext.post("/api/v1/zone")
+async def api_create_zone(
+ data: PartialZone, wallet: WalletTypeInfo = Depends(get_key_type)
+):
+ try:
+ zone = await create_zone(wallet.wallet.user, data)
+ return zone
+ except Exception as ex:
+ logger.warning(ex)
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail="Cannot create merchant",
+ )
+
+
+@nostrmarket_ext.patch("/api/v1/zone/{zone_id}")
+async def api_update_zone(
+ data: Zone,
+ zone_id: str,
+ wallet: WalletTypeInfo = Depends(require_admin_key),
+) -> Zone:
+ try:
+ zone = await get_zone(wallet.wallet.user, zone_id)
+ if not zone:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Zone does not exist.",
+ )
+ zone = await update_zone(wallet.wallet.user, data)
+ assert zone, "Cannot find updated zone"
+ return zone
+ except HTTPException as ex:
+ raise ex
+ except Exception as ex:
+ logger.warning(ex)
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail="Cannot create merchant",
+ )
+
+
+@nostrmarket_ext.delete("/api/v1/zone/{zone_id}")
+async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
+ try:
+ zone = await get_zone(wallet.wallet.user, zone_id)
+
+ if not zone:
+ raise HTTPException(
+ status_code=HTTPStatus.NOT_FOUND,
+ detail="Zone does not exist.",
+ )
+
+ await delete_zone(zone_id)
+
+ except Exception as ex:
+ logger.warning(ex)
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail="Cannot create merchant",
+ )
+
+
+@nostrmarket_ext.get("/api/v1/currencies")
+async def api_list_currencies_available():
+ return list(currencies.keys())