diff --git a/crud.py b/crud.py index 92bb703..3917270 100644 --- a/crud.py +++ b/crud.py @@ -287,6 +287,20 @@ async def update_product(merchant_id: str, product: Product) -> 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]: row = await db.fetchone( "SELECT * FROM nostrmarket.products WHERE merchant_id =? AND id = ?", diff --git a/services.py b/services.py index 245a8e7..d262a51 100644 --- a/services.py +++ b/services.py @@ -1,5 +1,5 @@ import json -from typing import Optional +from typing import List, Optional, Tuple from loguru import logger @@ -14,6 +14,8 @@ from .crud import ( get_products_by_ids, get_wallet_for_product, update_order_paid_status, + update_product, + update_product_quantity, ) from .helpers import order_from_json from .models import ( @@ -21,11 +23,13 @@ from .models import ( Nostrable, Order, OrderExtra, + OrderItem, OrderStatusUpdate, PartialDirectMessage, PartialOrder, PaymentOption, PaymentRequest, + Product, ) from .nostr.event import NostrEvent 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) 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( wallet_id=wallet_id, amount=round(total_amount), @@ -95,20 +106,78 @@ async def handle_order_paid(order_id: str, merchant_pubkey: str): try: order = await update_order_paid_status(order_id, True) 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) 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( 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) - await publish_nostr_event(dm_event) - except Exception as ex: - logger.warning(ex) + dm_event = merchant.build_dm_event(dm_content, order.public_key) + await publish_nostr_event(dm_event) + + +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): diff --git a/static/components/customer-stall/customer-stall.js b/static/components/customer-stall/customer-stall.js index dcf1adf..9a864a2 100644 --- a/static/components/customer-stall/customer-stall.js +++ b/static/components/customer-stall/customer-stall.js @@ -342,8 +342,9 @@ async function customerStall(path) { let json = JSON.parse(text) if (json.id != this.activeOrder) return if (json.payment_options) { - let payment_request = json.payment_options.find(o => o.type == 'ln') - .link + let payment_request = json.payment_options.find( + o => o.type == 'ln' + ).link if (!payment_request) return this.loading = false this.qrCodeDialog.data.payment_request = payment_request diff --git a/static/js/index.js b/static/js/index.js index 435cfd8..d530977 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -78,10 +78,7 @@ const merchant = async () => { message: 'Merchant Created!' }) } catch (error) { - this.$q.notify({ - type: 'negative', - message: `${error}` - }) + LNbits.utils.notifyApiError(error) } }, getMerchant: async function () { diff --git a/views_api.py b/views_api.py index ba3c0f6..7728e1b 100644 --- a/views_api.py +++ b/views_api.py @@ -32,6 +32,7 @@ from .crud import ( delete_stall, delete_zone, get_direct_messages, + get_merchant_by_pubkey, get_merchant_for_user, get_order, get_orders, @@ -78,6 +79,9 @@ async def api_create_merchant( ) -> Merchant: 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) 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) return merchant + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except Exception as ex: logger.warning(ex) raise HTTPException( @@ -128,6 +137,11 @@ async def api_delete_merchant( await unsubscribe_from_direct_messages(merchant.public_key) await delete_merchant(merchant.id) + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except Exception as ex: logger.warning(ex) 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) assert merchant, "Merchant cannot be found" return await get_zones(merchant.id) + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except Exception as ex: logger.warning(ex) raise HTTPException( @@ -162,6 +181,11 @@ async def api_create_zone( assert merchant, "Merchant cannot be found" zone = await create_zone(merchant.id, data) return zone + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except Exception as ex: logger.warning(ex) raise HTTPException( @@ -188,6 +212,11 @@ async def api_update_zone( zone = await update_zone(merchant.id, data) assert zone, "Cannot find updated zone" return zone + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except HTTPException as ex: raise 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) - + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except Exception as ex: logger.warning(ex) raise HTTPException( @@ -242,7 +275,8 @@ async def api_create_stall( await update_stall(merchant.id, stall) return stall - except ValueError as ex: + + except (ValueError, AssertionError) as ex: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=str(ex), @@ -277,7 +311,7 @@ async def api_update_stall( return stall except HTTPException as ex: raise ex - except ValueError as ex: + except (ValueError, AssertionError) as ex: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, 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.", ) return stall + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except HTTPException as ex: raise 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" stalls = await get_stalls(merchant.id) return stalls + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except Exception as ex: logger.warning(ex) raise HTTPException( @@ -337,6 +381,11 @@ async def api_get_stall_products( assert merchant, "Merchant cannot be found" products = await get_products(merchant.id, stall_id) return products + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except Exception as ex: logger.warning(ex) raise HTTPException( @@ -355,6 +404,11 @@ async def api_get_stall_orders( assert merchant, "Merchant cannot be found" orders = await get_orders_for_stall(merchant.id, stall_id) return orders + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except Exception as ex: logger.warning(ex) raise HTTPException( @@ -384,6 +438,11 @@ async def api_delete_stall( stall.config.event_id = event.id await update_stall(merchant.id, stall) + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except HTTPException as ex: raise ex except Exception as ex: @@ -419,7 +478,7 @@ async def api_create_product( await update_product(merchant.id, product) return product - except ValueError as ex: + except (ValueError, AssertionError) as ex: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=str(ex), @@ -451,14 +510,12 @@ async def api_update_product( product.config.currency = stall.currency product = await update_product(merchant.id, product) - event = await sign_and_send_to_nostr(merchant, product) - product.config.event_id = event.id await update_product(merchant.id, product) return product - except ValueError as ex: + except (ValueError, AssertionError) as ex: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=str(ex), @@ -482,6 +539,11 @@ async def api_get_product( products = await get_product(merchant.id, product_id) return products + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except Exception as ex: logger.warning(ex) raise HTTPException( @@ -509,6 +571,11 @@ async def api_delete_product( await delete_product(merchant.id, product_id) 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: raise 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.", ) return order + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except HTTPException as ex: raise 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) return orders + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except Exception as ex: logger.warning(ex) raise HTTPException( @@ -587,6 +664,11 @@ async def api_update_order_status( return order + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except Exception as ex: logger.warning(ex) raise HTTPException( @@ -608,6 +690,11 @@ async def api_get_messages( messages = await get_direct_messages(merchant.id, public_key) return messages + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except Exception as ex: logger.warning(ex) raise HTTPException( @@ -632,6 +719,11 @@ async def api_create_message( await publish_nostr_event(dm_event) return dm + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) except Exception as ex: logger.warning(ex) raise HTTPException(