Pre-cascade prerequisite for aiolabs/lnbits#17 (signer abstraction phase 1), which lands an m002 startup job that fail-closed NULLs the legacy `accounts.prvkey` column. This commit migrates the two sites in `views_api.py` that read `account.prvkey` so they no longer silently undo m002, and fail-closed cleanly when prvkey is missing. Scope intentionally narrow — this is the prvkey-elimination subset of aiolabs/nostrmarket#5. The full phase A (envelope-encrypt `merchants.private_key` → `signer_blob`) and phase B (route `Merchant.sign_hash` through core's `NostrSigner`) work remains tracked under that issue. ## What changed ### views_api.py — `_auto_create_merchant` Was: lazy fallback that, if `account.prvkey` was missing, generated a fresh keypair and wrote it back into the account (lines 112-118). After m002 NULLs `accounts.prvkey`, this regenerate-and-write-back path would silently undo the migration AND change the user's Nostr pubkey out from under them. Now: no longer touches the account. Asserts `account.prvkey` is present (matching the existing pubkey assertion) with a clear fail-closed message pointing at aiolabs/nostrmarket#5 for the phase A/B fix. For accounts that still carry a plaintext prvkey (pre-m002, FakeWallet local dev, etc.) the auto-provision path continues to work unchanged. For migrated accounts, the assertion fires fast with an actionable error. Removed the regenerate block entirely. Dropped now-unused imports: `update_account`, `generate_keypair`. ### views_api.py — `api_migrate_merchant_keys` Was: same `account and account.pubkey and account.prvkey` assertion with the generic message "Account has no Nostr keypair". Now: assertion updated with the same bridge-state framing — points at aiolabs/nostrmarket#5 for the phase A/B fix. ## Acceptance - [x] regenerate-and-write-back block removed (would undo m002) - [x] `account.prvkey` references in views_api.py are assertions only (fail-closed guards, not data reads) - [x] unused imports dropped (`update_account`, `generate_keypair`) - [x] error messages reference aiolabs/nostrmarket#5 for the phase A/B fix path Manual smoke / version bump / tag / catalog entry deferred until the lnbits cascade lands AND phase A's schema migration ships; this commit alone doesn't change the on-disk merchants table. ## Out of scope (per aiolabs/nostrmarket#5) - Phase A: envelope-encrypting `merchants.private_key` column. - Phase B (full): refactoring `Merchant.sign_hash` / `helpers.sign_message_hash` through core's `NostrSigner`. - Phase C: NIP-46 bunker + NIP-26 delegation variants. - Re-enabling `_create_default_merchant` on the lnbits core side. ## Cross-references - aiolabs/nostrmarket#5 — issue this is a partial step toward - aiolabs/lnbits#17 — the cascading signer-abstraction PR whose m002 fail-closed NULLs `accounts.prvkey` - aiolabs/lnbits#21 — umbrella audit (5 affected extensions) - aiolabs/events#23 / aiolabs/tasks#3 / aiolabs/restaurant#11 — sister migrations already on signer-abstraction branches Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1256 lines
39 KiB
Python
1256 lines
39 KiB
Python
import json
|
|
from http import HTTPStatus
|
|
from typing import List, Optional
|
|
|
|
from fastapi import Depends
|
|
from fastapi.exceptions import HTTPException
|
|
from lnbits.core.crud import get_account
|
|
from lnbits.core.services import websocket_updater
|
|
from lnbits.decorators import (
|
|
WalletTypeInfo,
|
|
require_admin_key,
|
|
require_invoice_key,
|
|
)
|
|
from lnbits.utils.exchange_rates import currencies
|
|
from loguru import logger
|
|
|
|
from . import nostr_client, nostrmarket_ext
|
|
from .crud import (
|
|
create_customer,
|
|
create_direct_message,
|
|
create_merchant,
|
|
create_product,
|
|
create_stall,
|
|
create_zone,
|
|
delete_merchant,
|
|
delete_merchant_direct_messages,
|
|
delete_merchant_orders,
|
|
delete_merchant_products,
|
|
delete_merchant_stalls,
|
|
delete_merchant_zones,
|
|
delete_product,
|
|
delete_stall,
|
|
delete_zone,
|
|
get_customer,
|
|
get_customers,
|
|
get_direct_message_by_event_id,
|
|
get_direct_messages,
|
|
get_last_direct_messages_time,
|
|
get_merchant_by_pubkey,
|
|
get_merchant_for_user,
|
|
update_merchant_keys,
|
|
get_order,
|
|
get_order_by_event_id,
|
|
get_orders,
|
|
get_orders_for_stall,
|
|
get_orders_from_direct_messages,
|
|
get_product,
|
|
get_products,
|
|
get_stall,
|
|
get_stalls,
|
|
get_zone,
|
|
get_zones,
|
|
touch_merchant,
|
|
update_customer_no_unread_messages,
|
|
update_merchant,
|
|
update_order,
|
|
update_order_shipped_status,
|
|
update_product,
|
|
update_stall,
|
|
update_zone,
|
|
)
|
|
from .helpers import normalize_public_key
|
|
from .models import (
|
|
CreateMerchantRequest,
|
|
Customer,
|
|
DirectMessage,
|
|
DirectMessageType,
|
|
Merchant,
|
|
MerchantConfig,
|
|
Order,
|
|
OrderReissue,
|
|
OrderStatusUpdate,
|
|
PartialDirectMessage,
|
|
PartialMerchant,
|
|
PartialOrder,
|
|
PaymentOption,
|
|
PaymentRequest,
|
|
Product,
|
|
Stall,
|
|
Zone,
|
|
)
|
|
from .services import (
|
|
build_order_with_payment,
|
|
create_or_update_order_from_dm,
|
|
provision_merchant,
|
|
reply_to_structured_dm,
|
|
resubscribe_to_all_merchants,
|
|
send_dm,
|
|
sign_and_send_to_nostr,
|
|
subscribe_to_all_merchants,
|
|
update_merchant_to_nostr,
|
|
)
|
|
|
|
######################################## MERCHANT ######################################
|
|
|
|
|
|
async def _auto_create_merchant(
|
|
wallet: WalletTypeInfo,
|
|
config: MerchantConfig | None = None,
|
|
) -> Merchant:
|
|
"""
|
|
Lazy fallback: provision a merchant from the user's account keypair when
|
|
the LNbits-side eager provisioning didn't run.
|
|
|
|
Delegates to services.provision_merchant — the canonical implementation.
|
|
|
|
Pre-cascade bridge state (see aiolabs/nostrmarket#5):
|
|
After aiolabs/lnbits#17 m002 lands, `accounts.prvkey` is fail-closed
|
|
NULL'd for migrated accounts (the cleartext nsec lives encrypted in
|
|
`signer_config`, owned by the core signer abstraction). Auto-provision
|
|
cannot extract that cleartext to copy into `merchants.private_key`,
|
|
so this path fails-closed when prvkey is missing. The proper fix is
|
|
phase A (envelope-encrypt `merchants.private_key` → `signer_blob`)
|
|
followed by phase B (route `Merchant.sign_hash` through core's
|
|
`NostrSigner`) per aiolabs/nostrmarket#5. Until then, migrated
|
|
accounts must explicitly provision a merchant through the future
|
|
phase-A-aware flow.
|
|
|
|
The previous regenerate-and-write-back block (generated a fresh
|
|
keypair and stored it into the account) was removed because it
|
|
would silently undo m002's NULL'ing.
|
|
"""
|
|
account = await get_account(wallet.wallet.user)
|
|
assert account, "User account not found"
|
|
assert account.pubkey and account.prvkey, (
|
|
"Account has no plaintext Nostr keypair available for merchant "
|
|
"provisioning (see aiolabs/nostrmarket#5 for the phase A/B fix)"
|
|
)
|
|
|
|
merchant = await provision_merchant(
|
|
user_id=wallet.wallet.user,
|
|
wallet_id=wallet.wallet.id,
|
|
public_key=account.pubkey,
|
|
private_key=account.prvkey,
|
|
display_name=account.username,
|
|
config=config,
|
|
)
|
|
|
|
await resubscribe_to_all_merchants()
|
|
await nostr_client.merchant_temp_subscription(account.pubkey)
|
|
|
|
return merchant
|
|
|
|
|
|
@nostrmarket_ext.post("/api/v1/merchant")
|
|
async def api_create_merchant(
|
|
data: CreateMerchantRequest,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> Merchant:
|
|
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant is None, "A merchant already exists for this user"
|
|
|
|
return await _auto_create_merchant(wallet, data.config)
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot create merchant",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.get("/api/v1/merchant")
|
|
async def api_get_merchant(
|
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
|
) -> Merchant:
|
|
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
if not merchant:
|
|
# Auto-provision merchant from the user's account keypair
|
|
merchant = await _auto_create_merchant(wallet)
|
|
|
|
merchant = await touch_merchant(wallet.wallet.user, merchant.id)
|
|
assert merchant
|
|
last_dm_time = await get_last_direct_messages_time(merchant.id)
|
|
assert merchant.time
|
|
merchant.config.restore_in_progress = (merchant.time - last_dm_time) < 30
|
|
|
|
# Detect keypair rotation: account key no longer matches merchant key
|
|
account = await get_account(wallet.wallet.user)
|
|
if account and account.pubkey and account.pubkey != merchant.public_key:
|
|
merchant.config.key_mismatch = True
|
|
|
|
return merchant
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot get merchant",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.delete("/api/v1/merchant/{merchant_id}")
|
|
async def api_delete_merchant(
|
|
merchant_id: str,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
):
|
|
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
assert merchant.id == merchant_id, "Wrong merchant ID"
|
|
|
|
await nostr_client.unsubscribe_merchants()
|
|
|
|
await delete_merchant_orders(merchant.id)
|
|
await delete_merchant_products(merchant.id)
|
|
await delete_merchant_stalls(merchant.id)
|
|
await delete_merchant_direct_messages(merchant.id)
|
|
await delete_merchant_zones(merchant.id)
|
|
|
|
await delete_merchant(merchant.id)
|
|
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot get merchant",
|
|
) from ex
|
|
finally:
|
|
await subscribe_to_all_merchants()
|
|
|
|
|
|
@nostrmarket_ext.post("/api/v1/merchant/{merchant_id}/migrate-keys")
|
|
async def api_migrate_merchant_keys(
|
|
merchant_id: str,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> Merchant:
|
|
"""
|
|
Migrate a merchant to the current account keypair.
|
|
|
|
When a user rotates their Nostr keypair, the merchant still holds the old
|
|
key. This endpoint updates the merchant's keys to match the account,
|
|
then republishes all stalls and products under the new identity.
|
|
|
|
Orders and DM history are preserved (they reference customer pubkeys,
|
|
not the merchant key). Old stall/product events on relays become
|
|
orphaned — clients following the new pubkey will see the fresh events.
|
|
"""
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
assert merchant.id == merchant_id, "Wrong merchant ID"
|
|
|
|
account = await get_account(wallet.wallet.user)
|
|
# account.prvkey is fail-closed NULL'd by aiolabs/lnbits#17 m002
|
|
# for migrated accounts. Rotation cannot copy a cleartext nsec
|
|
# into merchants.private_key until phase A lands — see
|
|
# aiolabs/nostrmarket#5 for the migration plan.
|
|
assert account and account.pubkey and account.prvkey, (
|
|
"Account has no plaintext Nostr keypair available for key "
|
|
"rotation (see aiolabs/nostrmarket#5 for the phase A/B fix)"
|
|
)
|
|
|
|
if account.pubkey == merchant.public_key:
|
|
return merchant # already in sync
|
|
|
|
# Check no other merchant is using the new pubkey
|
|
existing = await get_merchant_by_pubkey(account.pubkey)
|
|
assert existing is None, (
|
|
"Another merchant already uses this public key"
|
|
)
|
|
|
|
old_pubkey = merchant.public_key
|
|
|
|
# Update merchant keys in DB
|
|
merchant = await update_merchant_keys(
|
|
wallet.wallet.user, merchant.id,
|
|
account.prvkey, account.pubkey,
|
|
)
|
|
assert merchant
|
|
|
|
# Republish all stalls and products under the new key
|
|
merchant = await update_merchant_to_nostr(merchant)
|
|
|
|
logger.info(
|
|
f"[NOSTRMARKET] Migrated merchant {merchant.id} "
|
|
f"from {old_pubkey[:16]}... to {account.pubkey[:16]}..."
|
|
)
|
|
|
|
# Resubscribe with new pubkey
|
|
await resubscribe_to_all_merchants()
|
|
|
|
return merchant
|
|
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot migrate merchant keys",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.patch("/api/v1/merchant/{merchant_id}")
|
|
async def api_update_merchant(
|
|
merchant_id: str,
|
|
config: MerchantConfig,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
assert merchant.id == merchant_id, "Wrong merchant ID"
|
|
|
|
updated_merchant = await update_merchant(
|
|
wallet.wallet.user, merchant_id, config
|
|
)
|
|
return updated_merchant
|
|
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot update merchant",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.put("/api/v1/merchant/{merchant_id}/nostr")
|
|
async def api_republish_merchant(
|
|
merchant_id: str,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
assert merchant.id == merchant_id, "Wrong merchant ID"
|
|
|
|
merchant = await update_merchant_to_nostr(merchant)
|
|
await update_merchant(wallet.wallet.user, merchant.id, merchant.config)
|
|
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot republish to nostr",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.get("/api/v1/merchant/{merchant_id}/nostr")
|
|
async def api_refresh_merchant(
|
|
merchant_id: str,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
assert merchant.id == merchant_id, "Wrong merchant ID"
|
|
|
|
await nostr_client.merchant_temp_subscription(merchant.public_key)
|
|
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot refresh from nostr",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.put("/api/v1/merchant/{merchant_id}/toggle")
|
|
async def api_toggle_merchant(
|
|
merchant_id: str,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> Merchant:
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
assert merchant.id == merchant_id, "Wrong merchant ID"
|
|
|
|
merchant.config.active = not merchant.config.active
|
|
await update_merchant(wallet.wallet.user, merchant.id, merchant.config)
|
|
|
|
return merchant
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot get merchant",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.delete("/api/v1/merchant/{merchant_id}/nostr")
|
|
async def api_delete_merchant_on_nostr(
|
|
merchant_id: str,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
assert merchant.id == merchant_id, "Wrong merchant ID"
|
|
|
|
merchant = await update_merchant_to_nostr(merchant, True)
|
|
await update_merchant(wallet.wallet.user, merchant.id, merchant.config)
|
|
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot get merchant",
|
|
) from ex
|
|
|
|
|
|
######################################## ZONES ########################################
|
|
|
|
|
|
@nostrmarket_ext.get("/api/v1/zone")
|
|
async def api_get_zones(
|
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
|
) -> List[Zone]:
|
|
try:
|
|
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),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot get zone",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.post("/api/v1/zone")
|
|
async def api_create_zone(
|
|
data: Zone, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
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),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot create zone",
|
|
) from ex
|
|
|
|
|
|
@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:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
zone = await get_zone(merchant.id, zone_id)
|
|
if not zone:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_FOUND,
|
|
detail="Zone does not exist.",
|
|
)
|
|
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),
|
|
) from ex
|
|
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot update zone",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.delete("/api/v1/zone/{zone_id}")
|
|
async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
zone = await get_zone(merchant.id, zone_id)
|
|
|
|
if not zone:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_FOUND,
|
|
detail="Zone does not exist.",
|
|
)
|
|
|
|
await delete_zone(merchant.id, zone_id)
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot delete zone",
|
|
) from ex
|
|
|
|
|
|
######################################## STALLS ########################################
|
|
|
|
|
|
@nostrmarket_ext.post("/api/v1/stall")
|
|
async def api_create_stall(
|
|
data: Stall,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> Stall:
|
|
try:
|
|
# shipping_zones = await
|
|
data.validate_stall()
|
|
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
stall = await create_stall(merchant.id, data=data)
|
|
|
|
event = await sign_and_send_to_nostr(merchant, stall)
|
|
|
|
stall.event_id = event.id
|
|
await update_stall(merchant.id, stall)
|
|
|
|
return stall
|
|
|
|
except (ValueError, AssertionError) as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot create stall",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.put("/api/v1/stall/{stall_id}")
|
|
async def api_update_stall(
|
|
data: Stall,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> Stall:
|
|
try:
|
|
data.validate_stall()
|
|
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
|
|
stall = await update_stall(merchant.id, data)
|
|
assert stall, "Cannot update stall"
|
|
|
|
event = await sign_and_send_to_nostr(merchant, stall)
|
|
|
|
stall.event_id = event.id
|
|
await update_stall(merchant.id, stall)
|
|
|
|
return stall
|
|
|
|
except (ValueError, AssertionError) as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot update stall",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.get("/api/v1/stall/{stall_id}")
|
|
async def api_get_stall(
|
|
stall_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
|
):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
stall = await get_stall(merchant.id, stall_id)
|
|
if not stall:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_FOUND,
|
|
detail="Stall does not exist.",
|
|
)
|
|
return stall
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except HTTPException as ex:
|
|
raise ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot get stall",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.get("/api/v1/stall")
|
|
async def api_get_stalls(
|
|
pending: Optional[bool] = False,
|
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
|
):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
stalls = await get_stalls(merchant.id, pending)
|
|
return stalls
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot get stalls",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.get("/api/v1/stall/product/{stall_id}")
|
|
async def api_get_stall_products(
|
|
stall_id: str,
|
|
pending: Optional[bool] = False,
|
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
|
):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
products = await get_products(merchant.id, stall_id, pending)
|
|
return products
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot get stall products",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.get("/api/v1/stall/order/{stall_id}")
|
|
async def api_get_stall_orders(
|
|
stall_id: str,
|
|
paid: Optional[bool] = None,
|
|
shipped: Optional[bool] = None,
|
|
pubkey: Optional[str] = None,
|
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
|
):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
orders = await get_orders_for_stall(
|
|
merchant.id, stall_id, paid=paid, shipped=shipped, public_key=pubkey
|
|
)
|
|
return orders
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot get stall products",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.delete("/api/v1/stall/{stall_id}")
|
|
async def api_delete_stall(
|
|
stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
stall = await get_stall(merchant.id, stall_id)
|
|
if not stall:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_FOUND,
|
|
detail="Stall does not exist.",
|
|
)
|
|
|
|
await delete_stall(merchant.id, stall_id)
|
|
|
|
event = await sign_and_send_to_nostr(merchant, stall, True)
|
|
|
|
stall.event_id = event.id
|
|
await update_stall(merchant.id, stall)
|
|
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot delete stall",
|
|
) from ex
|
|
|
|
|
|
######################################## PRODUCTS ######################################
|
|
|
|
|
|
@nostrmarket_ext.post("/api/v1/product")
|
|
async def api_create_product(
|
|
data: Product,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> Product:
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
|
|
stall = await get_stall(merchant.id, data.stall_id)
|
|
assert stall, "Stall missing for product"
|
|
data.config.currency = stall.currency
|
|
|
|
# Re-publish the parent stall before publishing the product. NIP-33
|
|
# parameterized replaceable events make this idempotent on relays.
|
|
# This guarantees the customer client never sees a product whose
|
|
# parent stall isn't on the relay (e.g., when the original stall
|
|
# publish failed transiently or never ran).
|
|
try:
|
|
stall_event = await sign_and_send_to_nostr(merchant, stall)
|
|
stall.event_id = stall_event.id
|
|
await update_stall(merchant.id, stall)
|
|
except Exception as ex:
|
|
logger.warning(
|
|
f"[NOSTRMARKET] Failed to refresh stall {stall.id} "
|
|
f"before product publish: {ex}"
|
|
)
|
|
|
|
product = await create_product(merchant.id, data=data)
|
|
|
|
event = await sign_and_send_to_nostr(merchant, product)
|
|
|
|
product.event_id = event.id
|
|
await update_product(merchant.id, product)
|
|
|
|
return product
|
|
except (ValueError, AssertionError) as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot create product",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.patch("/api/v1/product/{product_id}")
|
|
async def api_update_product(
|
|
product_id: str,
|
|
product: Product,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> Product:
|
|
try:
|
|
if product_id != product.id:
|
|
raise ValueError("Bad product ID")
|
|
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
|
|
stall = await get_stall(merchant.id, product.stall_id)
|
|
assert stall, "Stall missing for product"
|
|
product.config.currency = stall.currency
|
|
|
|
product = await update_product(merchant.id, product)
|
|
event = await sign_and_send_to_nostr(merchant, product)
|
|
product.event_id = event.id
|
|
await update_product(merchant.id, product)
|
|
|
|
return product
|
|
except (ValueError, AssertionError) as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot update product",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.get("/api/v1/product/{product_id}")
|
|
async def api_get_product(
|
|
product_id: str,
|
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
|
) -> Optional[Product]:
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
|
|
products = await get_product(merchant.id, product_id)
|
|
return products
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot get product",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.delete("/api/v1/product/{product_id}")
|
|
async def api_delete_product(
|
|
product_id: str,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
|
|
product = await get_product(merchant.id, product_id)
|
|
if not product:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_FOUND,
|
|
detail="Product does not exist.",
|
|
)
|
|
|
|
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),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot delete product",
|
|
) from ex
|
|
|
|
|
|
######################################## ORDERS ########################################
|
|
|
|
|
|
@nostrmarket_ext.get("/api/v1/order/{order_id}")
|
|
async def api_get_order(
|
|
order_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
|
):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
|
|
order = await get_order(merchant.id, order_id)
|
|
if not order:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_FOUND,
|
|
detail="Order does not exist.",
|
|
)
|
|
return order
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot get order",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.get("/api/v1/order")
|
|
async def api_get_orders(
|
|
paid: Optional[bool] = None,
|
|
shipped: Optional[bool] = None,
|
|
pubkey: Optional[str] = None,
|
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
|
):
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
|
|
orders = await get_orders(
|
|
merchant_id=merchant.id, paid=paid, shipped=shipped, public_key=pubkey
|
|
)
|
|
return orders
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot get orders",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.patch("/api/v1/order/{order_id}")
|
|
async def api_update_order_status(
|
|
data: OrderStatusUpdate,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> Order:
|
|
try:
|
|
assert data.shipped is not None, "Shipped value is required for order"
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found for order {data.id}"
|
|
|
|
order = await update_order_shipped_status(merchant.id, data.id, data.shipped)
|
|
assert order, "Cannot find updated order"
|
|
|
|
data.paid = order.paid
|
|
dm_content = json.dumps(
|
|
{"type": DirectMessageType.ORDER_PAID_OR_SHIPPED.value, **data.dict()},
|
|
separators=(",", ":"),
|
|
ensure_ascii=False,
|
|
)
|
|
|
|
await send_dm(
|
|
merchant,
|
|
order.public_key,
|
|
DirectMessageType.ORDER_PAID_OR_SHIPPED.value,
|
|
dm_content,
|
|
)
|
|
|
|
return order
|
|
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot update order",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.put("/api/v1/order/restore/{event_id}")
|
|
async def api_restore_order(
|
|
event_id: str,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> Optional[Order]:
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
|
|
dm = await get_direct_message_by_event_id(merchant.id, event_id)
|
|
assert dm, "Canot find direct message"
|
|
|
|
await create_or_update_order_from_dm(merchant.id, merchant.public_key, dm)
|
|
|
|
return await get_order_by_event_id(merchant.id, event_id)
|
|
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot restore order",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.put("/api/v1/orders/restore")
|
|
async def api_restore_orders(
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> None:
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
|
|
dms = await get_orders_from_direct_messages(merchant.id)
|
|
for dm in dms:
|
|
try:
|
|
await create_or_update_order_from_dm(
|
|
merchant.id, merchant.public_key, dm
|
|
)
|
|
except Exception as e:
|
|
logger.debug(
|
|
f"Failed to restore order from event '{dm.event_id}': '{e!s}'."
|
|
)
|
|
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot restore orders",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.put("/api/v1/order/reissue")
|
|
async def api_reissue_order_invoice(
|
|
reissue_data: OrderReissue,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> Order:
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
|
|
data = await get_order(merchant.id, reissue_data.id)
|
|
assert data, "Order cannot be found"
|
|
|
|
if reissue_data.shipping_id:
|
|
data.shipping_id = reissue_data.shipping_id
|
|
|
|
order, invoice, receipt = await build_order_with_payment(
|
|
merchant.id, merchant.public_key, PartialOrder(**data.dict())
|
|
)
|
|
|
|
order_update = {
|
|
"stall_id": order.stall_id,
|
|
"total": order.total,
|
|
"invoice_id": order.invoice_id,
|
|
"shipping_id": order.shipping_id,
|
|
"extra_data": json.dumps(order.extra.dict()),
|
|
}
|
|
|
|
await update_order(
|
|
merchant.id,
|
|
order.id,
|
|
**order_update,
|
|
)
|
|
payment_req = PaymentRequest(
|
|
id=data.id,
|
|
payment_options=[PaymentOption(type="ln", link=invoice)],
|
|
message=receipt,
|
|
)
|
|
response = {
|
|
"type": DirectMessageType.PAYMENT_REQUEST.value,
|
|
**payment_req.dict(),
|
|
}
|
|
dm_reply = json.dumps(response, separators=(",", ":"), ensure_ascii=False)
|
|
|
|
await reply_to_structured_dm(
|
|
merchant,
|
|
order.public_key,
|
|
DirectMessageType.PAYMENT_REQUEST.value,
|
|
dm_reply,
|
|
)
|
|
|
|
return order
|
|
except (AssertionError, ValueError) as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot reissue order invoice",
|
|
) from ex
|
|
|
|
|
|
######################################## DIRECT MESSAGES ###############################
|
|
|
|
|
|
@nostrmarket_ext.get("/api/v1/message/{public_key}")
|
|
async def api_get_messages(
|
|
public_key: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
|
) -> List[DirectMessage]:
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
|
|
messages = await get_direct_messages(merchant.id, public_key)
|
|
await update_customer_no_unread_messages(merchant.id, public_key)
|
|
return messages
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot get direct message",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.post("/api/v1/message")
|
|
async def api_create_message(
|
|
data: PartialDirectMessage, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
) -> DirectMessage:
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
|
|
dm_reply = await send_dm(
|
|
merchant,
|
|
data.public_key,
|
|
data.type,
|
|
data.message,
|
|
)
|
|
return dm_reply
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot create message",
|
|
) from ex
|
|
|
|
|
|
######################################## CUSTOMERS #####################################
|
|
|
|
|
|
@nostrmarket_ext.get("/api/v1/customer")
|
|
async def api_get_customers(
|
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
|
) -> List[Customer]:
|
|
try:
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "Merchant cannot be found"
|
|
return await get_customers(merchant.id)
|
|
|
|
except AssertionError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot create message",
|
|
) from ex
|
|
|
|
|
|
@nostrmarket_ext.post("/api/v1/customer")
|
|
async def api_create_customer(
|
|
data: Customer,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> Customer:
|
|
|
|
try:
|
|
pubkey = normalize_public_key(data.public_key)
|
|
|
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
|
assert merchant, "A merchant does not exists for this user"
|
|
assert merchant.id == data.merchant_id, "Invalid merchant id for user"
|
|
|
|
existing_customer = await get_customer(merchant.id, pubkey)
|
|
assert existing_customer is None, "This public key already exists"
|
|
|
|
customer = await create_customer(
|
|
merchant.id, Customer(merchant_id=merchant.id, public_key=pubkey)
|
|
)
|
|
|
|
await nostr_client.user_profile_temp_subscribe(pubkey)
|
|
|
|
return customer
|
|
except (ValueError, AssertionError) as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
) from ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot create customer",
|
|
) from ex
|
|
|
|
|
|
######################################## OTHER ########################################
|
|
|
|
|
|
@nostrmarket_ext.get("/api/v1/currencies")
|
|
async def api_list_currencies_available():
|
|
return list(currencies.keys())
|
|
|
|
|
|
@nostrmarket_ext.put("/api/v1/restart")
|
|
async def restart_nostr_client(wallet: WalletTypeInfo = Depends(require_admin_key)):
|
|
try:
|
|
await nostr_client.restart()
|
|
except Exception as ex:
|
|
logger.warning(ex)
|