feat: basic stall flow
This commit is contained in:
parent
c15b765a7d
commit
e9b7494bb6
8 changed files with 432 additions and 14 deletions
63
crud.py
63
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
|
||||
|
|
|
|||
|
|
@ -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 '{}'
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
|
|
|||
27
models.py
27
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
|
||||
|
|
|
|||
157
static/components/stall-list/stall-list.html
Normal file
157
static/components/stall-list/stall-list.html
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
<div>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col q-pr-lg">
|
||||
<q-btn
|
||||
@click="stallDialog.show = true"
|
||||
unelevated
|
||||
color="green"
|
||||
class="float-left"
|
||||
>New Stall</q-btn
|
||||
>
|
||||
<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 class="col-auto">
|
||||
<q-btn outline color="grey" label="...">
|
||||
<q-menu auto-close>
|
||||
<q-list style="min-width: 100px">
|
||||
<q-item clickable>
|
||||
<q-item-section @click="exportrelayCSV"
|
||||
>Export to CSV</q-item-section
|
||||
>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div> -->
|
||||
</div>
|
||||
<!--
|
||||
<q-table
|
||||
flat
|
||||
dense
|
||||
:data="stalls"
|
||||
row-key="id"
|
||||
:columns="relaysTable.columns"
|
||||
:pagination.sync="relaysTable.pagination"
|
||||
:filter="filter"
|
||||
>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="accent"
|
||||
round
|
||||
dense
|
||||
@click="props.row.expanded= !props.row.expanded"
|
||||
:icon="props.row.expanded? 'remove' : 'add'"
|
||||
/>
|
||||
</q-td>
|
||||
|
||||
<q-td key="id" :props="props">
|
||||
<a style="color: unset" :href="props.row.id" target="_blank">
|
||||
{{props.row.id}}</a
|
||||
>
|
||||
</q-td>
|
||||
<q-td key="toggle" :props="props">
|
||||
<q-toggle
|
||||
size="sm"
|
||||
color="secodary"
|
||||
v-model="props.row.active"
|
||||
@input="showToggleRelayDialog(props.row)"
|
||||
></q-toggle>
|
||||
</q-td>
|
||||
<q-td auto-width> {{props.row.name}} </q-td>
|
||||
<q-td key="description" :props="props">
|
||||
{{props.row.description}}
|
||||
</q-td>
|
||||
<q-td key="pubkey" :props="props">
|
||||
<div>{{props.row.pubkey}}</div>
|
||||
</q-td>
|
||||
<q-td key="contact" :props="props">
|
||||
<div>{{props.row.contact}}</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">
|
||||
<relay-details
|
||||
:relay-id="props.row.id"
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:wallet-options="g.user.walletOptions"
|
||||
@relay-deleted="handleRelayDeleted"
|
||||
@relay-updated="handleRelayUpdated"
|
||||
></relay-details>
|
||||
</div>
|
||||
</div>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</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-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="zoneOptions"
|
||||
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.wallet == null"
|
||||
type="submit"
|
||||
>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>
|
||||
</div>
|
||||
</div>
|
||||
84
static/components/stall-list/stall-list.js
Normal file
84
static/components/stall-list/stall-list.js
Normal file
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -86,16 +86,17 @@
|
|||
></key-pair>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<q-card>
|
||||
<q-card class="q-mt-lg">
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-6"></div>
|
||||
<div class="col-6"></div>
|
||||
</div>
|
||||
<stall-list
|
||||
:adminkey="g.user.wallets[0].adminkey"
|
||||
:inkey="g.user.wallets[0].inkey"
|
||||
:wallet-options="g.user.walletOptions"
|
||||
></stall-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
|
|
@ -115,9 +116,10 @@
|
|||
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script>
|
||||
|
||||
<script src="{{ url_for('nostrmarket_static', path='js/utils.js') }}"></script>
|
||||
<script src="{{ url_for('nostrmarket_static', path='components/stall-details/stall-details.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='js/index.js') }}"></script>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
86
views_api.py
86
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())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue