diff --git a/models.py b/models.py index 3b42bab..effd7f9 100644 --- a/models.py +++ b/models.py @@ -215,11 +215,17 @@ def from_row(cls, row: Row) -> "Stall": ######################################## PRODUCTS ######################################## +class ProductShippingCost(BaseModel): + id: str + cost: int + + class ProductConfig(BaseModel): description: Optional[str] currency: Optional[str] use_autoreply: Optional[bool] = False autoreply_message: Optional[str] + shipping: Optional[List[ProductShippingCost]] = [] class PartialProduct(BaseModel): @@ -251,6 +257,7 @@ def to_nostr_event(self, pubkey: str) -> NostrEvent: "currency": self.config.currency, "price": self.price, "quantity": self.quantity, + "shipping": [dict(s) for s in self.config.shipping or []] } categories = [["t", tag] for tag in self.categories] @@ -358,24 +365,67 @@ def validate_order_items(self, product_list: List[Product]): ) async def costs_in_sats( - self, products: List[Product], shipping_cost: float + self, products: List[Product], shipping_id: str, stall_shipping_cost: float ) -> Tuple[float, float]: product_prices = {} for p in products: - product_prices[p.id] = p + product_shipping_cost = next( + (s.cost for s in p.config.shipping if s.id == shipping_id), 0 + ) + product_prices[p.id] = { + "price": p.price + product_shipping_cost, + "currency": p.config.currency or "sat", + } product_cost: float = 0 # todo for item in self.items: - price = product_prices[item.product_id].price - currency = product_prices[item.product_id].config.currency or "sat" + assert item.quantity > 0, "Quantity cannot be negative" + price = product_prices[item.product_id]["price"] + currency = product_prices[item.product_id]["currency"] if currency != "sat": price = await fiat_amount_as_satoshis(price, currency) product_cost += item.quantity * price if currency != "sat": - shipping_cost = await fiat_amount_as_satoshis(shipping_cost, currency) + stall_shipping_cost = await fiat_amount_as_satoshis( + stall_shipping_cost, currency + ) + + return product_cost, stall_shipping_cost + + def receipt( + self, products: List[Product], shipping_id: str, stall_shipping_cost: float + ) -> str: + if len(products) == 0: + return "[No Products]" + receipt = "" + product_prices = {} + for p in products: + product_shipping_cost = next( + (s.cost for s in p.config.shipping if s.id == shipping_id), 0 + ) + product_prices[p.id] = { + "name": p.name, + "price": p.price, + "product_shipping_cost": product_shipping_cost + } + + currency = products[0].config.currency or "sat" + products_cost: float = 0 # todo + items_receipts = [] + for item in self.items: + prod = product_prices[item.product_id] + price = prod["price"] + prod["product_shipping_cost"] + + products_cost += item.quantity * price + + items_receipts.append(f"""[{prod["name"]}: {item.quantity} x ({prod["price"]} + {prod["product_shipping_cost"]}) = {item.quantity * price} {currency}] """) + + receipt = "; ".join(items_receipts) + receipt += f"[Products cost: {products_cost} {currency}] [Stall shipping cost: {stall_shipping_cost} {currency}]; " + receipt += f"[Total: {products_cost + stall_shipping_cost} {currency}]" - return product_cost, shipping_cost + return receipt class Order(PartialOrder): diff --git a/services.py b/services.py index dcdb057..255fb6e 100644 --- a/services.py +++ b/services.py @@ -69,13 +69,13 @@ async def create_new_order( if data.event_id and await get_order_by_event_id(merchant.id, data.event_id): return None - order, invoice = await build_order_with_payment( + order, invoice, receipt = await build_order_with_payment( merchant.id, merchant.public_key, data ) await create_order(merchant.id, order) return PaymentRequest( - id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)] + id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)], message=receipt ) @@ -90,7 +90,10 @@ async def build_order_with_payment( assert shipping_zone, f"Shipping zone not found for order '{data.id}'" product_cost_sat, shipping_cost_sat = await data.costs_in_sats( - products, shipping_zone.cost + products, shipping_zone.id, shipping_zone.cost + ) + receipt = data.receipt( + products, shipping_zone.id, shipping_zone.cost ) wallet_id = await get_wallet_for_product(data.items[0].product_id) @@ -126,7 +129,7 @@ async def build_order_with_payment( extra=extra, ) - return order, invoice + return order, invoice, receipt async def update_merchant_to_nostr( @@ -567,7 +570,7 @@ async def _handle_new_order( except Exception as e: logger.debug(e) payment_req = await create_new_failed_order( - merchant_id, merchant_public_key, dm, json_data, str(e) + merchant_id, merchant_public_key, dm, json_data, "Order received, but cannot be processed. Please contact merchant." ) response = { diff --git a/static/components/stall-details/stall-details.html b/static/components/stall-details/stall-details.html index 62b8873..7346611 100644 --- a/static/components/stall-details/stall-details.html +++ b/static/components/stall-details/stall-details.html @@ -102,20 +102,11 @@ {{props.row.id}} - {{props.row.name}} + {{shortLabel(props.row.name)}} {{props.row.price}} {{props.row.quantity}} - - -
- {{props.row.categories.filter(c => c).join(', ')}} -
-
- - {{props.row.config.description}} - @@ -131,35 +122,66 @@ - + - - - +
+
+ +
+
+ +
+
- - - - - + +
+ +
+
- + +
+ + + + + + +
+
+ + + +
+
+ +
+
+ + +
+
+
+
- +
diff --git a/static/components/stall-details/stall-details.js b/static/components/stall-details/stall-details.js index b77ef5e..32fcad5 100644 --- a/static/components/stall-details/stall-details.js +++ b/static/components/stall-details/stall-details.js @@ -23,19 +23,7 @@ async function stallDetails(path) { showDialog: false, showRestore: false, url: true, - data: { - id: null, - name: '', - categories: [], - images: [], - image: null, - price: 0, - - quantity: 0, - config: { - description: '' - } - } + data: null }, productsFilter: '', productsTable: { @@ -76,18 +64,6 @@ async function stallDetails(path) { align: 'left', label: 'Quantity', field: 'quantity' - }, - { - name: 'categories', - align: 'left', - label: 'Categories', - field: 'categories' - }, - { - name: 'description', - align: 'left', - label: 'Description', - field: 'description' } ], pagination: { @@ -112,6 +88,24 @@ async function stallDetails(path) { ) return stall }, + newEmtpyProductData: function() { + return { + id: null, + name: '', + categories: [], + images: [], + image: null, + price: 0, + + quantity: 0, + config: { + description: '', + use_autoreply: false, + autoreply_message: '', + shipping: (this.stall.shipping_zones || []).map(z => ({id: z.id, name: z.name, cost: 0})) + } + } + }, getStall: async function () { try { const { data } = await LNbits.api.request( @@ -202,7 +196,7 @@ async function stallDetails(path) { } }, sendProductFormData: function () { - var data = { + const data = { stall_id: this.stall.id, id: this.productDialog.data.id, name: this.productDialog.data.name, @@ -265,7 +259,14 @@ async function stallDetails(path) { } }, editProduct: async function (product) { + const emptyShipping = this.newEmtpyProductData().config.shipping this.productDialog.data = { ...product } + this.productDialog.data.config.shipping = emptyShipping.map(shippingZone => { + const existingShippingCost = (product.config.shipping || []).find(ps => ps.id === shippingZone.id) + shippingZone.cost = existingShippingCost?.cost || 0 + return shippingZone + }) + this.productDialog.showDialog = true }, deleteProduct: async function (productId) { @@ -293,19 +294,7 @@ async function stallDetails(path) { }) }, showNewProductDialog: async function (data) { - this.productDialog.data = data || { - id: null, - name: '', - description: '', - categories: [], - image: null, - images: [], - price: 0, - quantity: 0, - config: { - description: '' - } - } + this.productDialog.data = data || this.newEmtpyProductData() this.productDialog.showDialog = true }, openSelectPendingProductDialog: async function () { @@ -324,11 +313,16 @@ async function stallDetails(path) { }, customerSelectedForOrder: function (customerPubkey) { this.$emit('customer-selected-for-order', customerPubkey) + }, + shortLabel(value = ''){ + if (value.length <= 44) return value + return value.substring(0, 40) + '...' } }, created: async function () { await this.getStall() this.products = await this.getProducts() + this.productDialog.data = this.newEmtpyProductData() } }) } diff --git a/static/components/stall-list/stall-list.html b/static/components/stall-list/stall-list.html index 607e944..c364eb2 100644 --- a/static/components/stall-list/stall-list.html +++ b/static/components/stall-list/stall-list.html @@ -34,15 +34,14 @@ :icon="props.row.expanded? 'remove' : 'add'" /> - {{props.row.name}} + {{shortLabel(props.row.name)}} {{props.row.currency}} - {{props.row.config.description}} + {{shortLabel(props.row.config.description)}}
- {{props.row.shipping_zones.filter(z => !!z.name).map(z => - z.name).join(', ')}} + {{shortLabel(props.row.shipping_zones.filter(z => !!z.name).map(z => z.name).join(', '))}}
diff --git a/static/components/stall-list/stall-list.js b/static/components/stall-list/stall-list.js index bc3053a..e384895 100644 --- a/static/components/stall-list/stall-list.js +++ b/static/components/stall-list/stall-list.js @@ -251,6 +251,10 @@ async function stallList(path) { }, customerSelectedForOrder: function (customerPubkey) { this.$emit('customer-selected-for-order', customerPubkey) + }, + shortLabel(value = ''){ + if (value.length <= 64) return value + return value.substring(0, 60) + '...' } }, created: async function () {