Merge pull request #18 from lnbits/update_product_stock

Update product stock
This commit is contained in:
Vlad Stan 2023-03-16 15:29:54 +02:00 committed by GitHub
commit 45c7744282
5 changed files with 194 additions and 21 deletions

14
crud.py
View file

@ -287,6 +287,20 @@ async def update_product(merchant_id: str, product: Product) -> Product:
return updated_product return updated_product
async def update_product_quantity(
product_id: str, new_quantity: int
) -> Optional[Product]:
await db.execute(
f"UPDATE nostrmarket.products SET quantity = ? WHERE id = ?",
(new_quantity, product_id),
)
row = await db.fetchone(
"SELECT * FROM nostrmarket.products WHERE id = ?",
(product_id,),
)
return Product.from_row(row) if row else None
async def get_product(merchant_id: str, product_id: str) -> Optional[Product]: async def get_product(merchant_id: str, product_id: str) -> Optional[Product]:
row = await db.fetchone( row = await db.fetchone(
"SELECT * FROM nostrmarket.products WHERE merchant_id =? AND id = ?", "SELECT * FROM nostrmarket.products WHERE merchant_id =? AND id = ?",

View file

@ -1,5 +1,5 @@
import json import json
from typing import Optional from typing import List, Optional, Tuple
from loguru import logger from loguru import logger
@ -14,6 +14,8 @@ from .crud import (
get_products_by_ids, get_products_by_ids,
get_wallet_for_product, get_wallet_for_product,
update_order_paid_status, update_order_paid_status,
update_product,
update_product_quantity,
) )
from .helpers import order_from_json from .helpers import order_from_json
from .models import ( from .models import (
@ -21,11 +23,13 @@ from .models import (
Nostrable, Nostrable,
Order, Order,
OrderExtra, OrderExtra,
OrderItem,
OrderStatusUpdate, OrderStatusUpdate,
PartialDirectMessage, PartialDirectMessage,
PartialOrder, PartialOrder,
PaymentOption, PaymentOption,
PaymentRequest, PaymentRequest,
Product,
) )
from .nostr.event import NostrEvent from .nostr.event import NostrEvent
from .nostr.nostr_client import publish_nostr_event from .nostr.nostr_client import publish_nostr_event
@ -52,6 +56,13 @@ async def create_new_order(
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}`"
product_ids = [i.product_id for i in data.items]
success, _, message = await compute_products_new_quantity(
merchant.id, product_ids, data.items
)
if not success:
return PaymentRequest(id=data.id, message=message, payment_options=[])
payment_hash, invoice = await create_invoice( payment_hash, invoice = await create_invoice(
wallet_id=wallet_id, wallet_id=wallet_id,
amount=round(total_amount), amount=round(total_amount),
@ -95,20 +106,78 @@ async def handle_order_paid(order_id: str, merchant_pubkey: str):
try: try:
order = await update_order_paid_status(order_id, True) order = await update_order_paid_status(order_id, True)
assert order, f"Paid order cannot be found. Order id: {order_id}" assert order, f"Paid order cannot be found. Order id: {order_id}"
order_status = OrderStatusUpdate(
id=order_id, message="Payment received.", paid=True, shipped=order.shipped
)
merchant = await get_merchant_by_pubkey(merchant_pubkey) merchant = await get_merchant_by_pubkey(merchant_pubkey)
assert merchant, f"Merchant cannot be found for order {order_id}" assert merchant, f"Merchant cannot be found for order {order_id}"
# todo: lock
success, message = await update_products_for_order(merchant, order)
await notify_client_of_order_status(order, merchant, success, message)
except Exception as ex:
logger.warning(ex)
async def notify_client_of_order_status(
order: Order, merchant: Merchant, success: bool, message: str
):
dm_content = ""
if success:
order_status = OrderStatusUpdate(
id=order.id,
message="Payment received.",
paid=True,
shipped=order.shipped,
)
dm_content = json.dumps( dm_content = json.dumps(
order_status.dict(), separators=(",", ":"), ensure_ascii=False order_status.dict(), separators=(",", ":"), ensure_ascii=False
) )
else:
dm_content = f"Order cannot be fulfilled. Reason: {message}"
dm_event = merchant.build_dm_event(dm_content, order.public_key) dm_event = merchant.build_dm_event(dm_content, order.public_key)
await publish_nostr_event(dm_event) await publish_nostr_event(dm_event)
except Exception as ex:
logger.warning(ex)
async def update_products_for_order(
merchant: Merchant, order: Order
) -> Tuple[bool, str]:
product_ids = [i.product_id for i in order.items]
success, products, message = await compute_products_new_quantity(
merchant.id, product_ids, order.items
)
if not success:
return success, message
for p in products:
product = await update_product_quantity(p.id, p.quantity)
event = await sign_and_send_to_nostr(merchant, product)
product.config.event_id = event.id
await update_product(merchant.id, product)
return True, "ok"
async def compute_products_new_quantity(
merchant_id: str, product_ids: List[str], items: List[OrderItem]
) -> Tuple[bool, List[Product], str]:
products: List[Product] = await get_products_by_ids(merchant_id, product_ids)
for p in products:
required_quantity = next(
(i.quantity for i in items if i.product_id == p.id), None
)
if not required_quantity:
return False, [], f"Product not found for order: {p.id}"
if p.quantity < required_quantity:
return (
False,
[],
f"Quantity not sufficient for product: {p.id}. Required {required_quantity} but only have {p.quantity}",
)
p.quantity -= required_quantity
return True, products, "ok"
async def process_nostr_message(msg: str): async def process_nostr_message(msg: str):

View file

@ -342,8 +342,9 @@ async function customerStall(path) {
let json = JSON.parse(text) let json = JSON.parse(text)
if (json.id != this.activeOrder) return if (json.id != this.activeOrder) return
if (json.payment_options) { if (json.payment_options) {
let payment_request = json.payment_options.find(o => o.type == 'ln') let payment_request = json.payment_options.find(
.link o => o.type == 'ln'
).link
if (!payment_request) return if (!payment_request) return
this.loading = false this.loading = false
this.qrCodeDialog.data.payment_request = payment_request this.qrCodeDialog.data.payment_request = payment_request

View file

@ -78,10 +78,7 @@ const merchant = async () => {
message: 'Merchant Created!' message: 'Merchant Created!'
}) })
} catch (error) { } catch (error) {
this.$q.notify({ LNbits.utils.notifyApiError(error)
type: 'negative',
message: `${error}`
})
} }
}, },
getMerchant: async function () { getMerchant: async function () {

View file

@ -32,6 +32,7 @@ from .crud import (
delete_stall, delete_stall,
delete_zone, delete_zone,
get_direct_messages, get_direct_messages,
get_merchant_by_pubkey,
get_merchant_for_user, get_merchant_for_user,
get_order, get_order,
get_orders, get_orders,
@ -78,6 +79,9 @@ async def api_create_merchant(
) -> Merchant: ) -> Merchant:
try: try:
merchant = await get_merchant_by_pubkey(data.public_key)
assert merchant == 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 == None, "A merchant already exists for this user"
@ -85,6 +89,11 @@ async def api_create_merchant(
await subscribe_to_direct_messages(data.public_key, 0) await subscribe_to_direct_messages(data.public_key, 0)
return merchant return merchant
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -128,6 +137,11 @@ async def api_delete_merchant(
await unsubscribe_from_direct_messages(merchant.public_key) await unsubscribe_from_direct_messages(merchant.public_key)
await delete_merchant(merchant.id) await delete_merchant(merchant.id)
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -145,6 +159,11 @@ async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[
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"
return await get_zones(merchant.id) return await get_zones(merchant.id)
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -162,6 +181,11 @@ async def api_create_zone(
assert merchant, "Merchant cannot be found" assert merchant, "Merchant cannot be found"
zone = await create_zone(merchant.id, data) zone = await create_zone(merchant.id, data)
return zone return zone
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -188,6 +212,11 @@ async def api_update_zone(
zone = await update_zone(merchant.id, data) zone = await update_zone(merchant.id, data)
assert zone, "Cannot find updated zone" assert zone, "Cannot find updated zone"
return zone return zone
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except HTTPException as ex: except HTTPException as ex:
raise ex raise ex
except Exception as ex: except Exception as ex:
@ -212,7 +241,11 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi
) )
await delete_zone(merchant.id, zone_id) await delete_zone(merchant.id, zone_id)
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -242,7 +275,8 @@ async def api_create_stall(
await update_stall(merchant.id, stall) await update_stall(merchant.id, stall)
return stall return stall
except ValueError 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),
@ -277,7 +311,7 @@ async def api_update_stall(
return stall return stall
except HTTPException as ex: except HTTPException as ex:
raise ex raise ex
except ValueError 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),
@ -302,6 +336,11 @@ async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_
detail="Stall does not exist.", detail="Stall does not exist.",
) )
return stall return stall
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except HTTPException as ex: except HTTPException as ex:
raise ex raise ex
except Exception as ex: except Exception as ex:
@ -319,6 +358,11 @@ async def api_get_stalls(wallet: WalletTypeInfo = Depends(get_key_type)):
assert merchant, "Merchant cannot be found" assert merchant, "Merchant cannot be found"
stalls = await get_stalls(merchant.id) stalls = await get_stalls(merchant.id)
return stalls return stalls
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -337,6 +381,11 @@ async def api_get_stall_products(
assert merchant, "Merchant cannot be found" assert merchant, "Merchant cannot be found"
products = await get_products(merchant.id, stall_id) products = await get_products(merchant.id, stall_id)
return products return products
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -355,6 +404,11 @@ async def api_get_stall_orders(
assert merchant, "Merchant cannot be found" assert merchant, "Merchant cannot be found"
orders = await get_orders_for_stall(merchant.id, stall_id) orders = await get_orders_for_stall(merchant.id, stall_id)
return orders return orders
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -384,6 +438,11 @@ async def api_delete_stall(
stall.config.event_id = event.id stall.config.event_id = event.id
await update_stall(merchant.id, stall) await update_stall(merchant.id, stall)
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except HTTPException as ex: except HTTPException as ex:
raise ex raise ex
except Exception as ex: except Exception as ex:
@ -419,7 +478,7 @@ async def api_create_product(
await update_product(merchant.id, product) await update_product(merchant.id, product)
return product return product
except ValueError 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),
@ -451,14 +510,12 @@ async def api_update_product(
product.config.currency = stall.currency product.config.currency = stall.currency
product = await update_product(merchant.id, product) product = await update_product(merchant.id, product)
event = await sign_and_send_to_nostr(merchant, product) event = await sign_and_send_to_nostr(merchant, product)
product.config.event_id = event.id product.config.event_id = event.id
await update_product(merchant.id, product) await update_product(merchant.id, product)
return product return product
except ValueError 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),
@ -482,6 +539,11 @@ async def api_get_product(
products = await get_product(merchant.id, product_id) products = await get_product(merchant.id, product_id)
return products return products
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -509,6 +571,11 @@ async def api_delete_product(
await delete_product(merchant.id, product_id) await delete_product(merchant.id, product_id)
await sign_and_send_to_nostr(merchant, product, True) await sign_and_send_to_nostr(merchant, product, True)
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except HTTPException as ex: except HTTPException as ex:
raise ex raise ex
except Exception as ex: except Exception as ex:
@ -537,6 +604,11 @@ async def api_get_order(order_id: str, wallet: WalletTypeInfo = Depends(get_key_
detail="Order does not exist.", detail="Order does not exist.",
) )
return order return order
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except HTTPException as ex: except HTTPException as ex:
raise ex raise ex
except Exception as ex: except Exception as ex:
@ -555,6 +627,11 @@ async def api_get_orders(wallet: WalletTypeInfo = Depends(get_key_type)):
orders = await get_orders(merchant.id) orders = await get_orders(merchant.id)
return orders return orders
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -587,6 +664,11 @@ async def api_update_order_status(
return order return order
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -608,6 +690,11 @@ async def api_get_messages(
messages = await get_direct_messages(merchant.id, public_key) messages = await get_direct_messages(merchant.id, public_key)
return messages return messages
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -632,6 +719,11 @@ async def api_create_message(
await publish_nostr_event(dm_event) await publish_nostr_event(dm_event)
return dm return dm
except AssertionError as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(ex),
)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(